Using optimizer callbacks within JuMP to terminate MIP solve

I’ve got a MIP problem in JuMP, for which I would like the branch-and-bound to terminate as soon as the incumbent reaches a particular value, or as soon as the lower bound reaches a (different) particular value.

I’ve found an example of how this can be done in Python (using callbacks) on Gurobi’s website.

https://support.gurobi.com/hc/en-us/articles/360047717291-How-do-I-use-callbacks-to-terminate-the-solver-

My understanding is that JuMP’s support for callbacks is limited, but certain solver-specific callbacks can be implemented. Before I spend too much time reading through source code, I would like to know if this is possible, and if any examples exist that could be used as a starting point.

Thanks.

Ideally, something like this. But I’m getting a segfault when I try to terminate. (Edit: I think this may have been due to Julia’s GC mixing with the early terminate. I can’t reproduce if I turn it off.)

using JuMP
import Gurobi

softlimit = 5
hardlimit = 100

model = direct_model(
    Gurobi.Optimizer(
        OutputFlag = 0,
        Cuts = 0,
        Presolve = 0,
        PreCrush = 1,
        Heuristics = 0,
    )
)
@variable(model, 0 <= x <= 2.5, Int)
@variable(model, 0 <= y <= 2.5, Int)
@objective(model, Max, y)

function softtime(cb_data, cb_where)
    if cb_where == Gurobi.CB_MIP
        @info("Executing callback")
        runtime = Gurobi.cbget_runtime(cb_data, cb_where)
        objbst = Gurobi.cbget_mip_objbst(cb_data, cb_where)
        objbnd = Gurobi.cbget_mip_objbnd(cb_data, cb_where)
        gap = abs((objbst - objbnd) / objbst)
        @info("Terminating at $(runtime) with $(gap)")
        Gurobi.terminate(backend(model).inner)
    end
    return
end

set_optimizer_attribute(model, "TimeLimit", hardlimit)
MOI.set(model, Gurobi.CallbackFunction(), softtime)
GC.enable(false)  # You might want this
optimize!(model)
GC.enable(true)  # You might want this

Thanks Oscar.

I ran your code (both with and without the GC enabled) and it seemed to work fine, and I didn’t get a segmentation fault.

Was there a reason for the optimizer settings (no cuts, presolve or heuristics etc.)?

That’s just so it triggers the callback on a small problem. Otherwise the optimal solution is found at the root node before the branch and bound routine (and callbacks) starts to get called.

I’ve got this working in my application; thanks again. However, for some reason, I wasn’t able to directly access the inner field of my model and had to use

Gurobi.terminate(backend(model).optimizer.model.inner)

instead.

It’s for this reason I used direct_model(Gurobi.Optimizer()) instead of Model(Gurobi.Optimizer).

In the second case, you have to fight your way through the layers of bridging and caching optimizers to reach .inner.