Gurobi MIP start, calling GRBread to provide starting vector using a *.mst/ *.sol file

Hi,

i am currently developing a mixed integer model and need to pass an initial solution from a previous run to Gurobi. I probably could do it using JuMP’s setvalue() function. It would be much more handy however, to just generate an *.mst file from a solved model to then pass it to the solver before the next run.

I was already successful in generating an *.mst file by using MyJuMPModel.internalModel.inner. This is how I generated the file:

grb_mod = MyJuMPModel.internalModel.inner
write_model(grb_mod, "out.mst")

I read on Gurobi’s website that the function GRBread() can be used to provide the solver with a starting vector, by passing an *.mst file. Unfortunately, this function is currently not exposed in the Gurobi package. Thus, I was trying to expose this function with limited success. Limited success meaning that I do not get error messages but Gurobi does not seem to use the starting vector.

In the Gurobi package I added the following function to grb_model.jl:

function grb_read(model::Model, filename::String)
    @assert isascii(filename) # TODO: support non-ascii file names
    @assert model.ptr_model != C_NULL
        ret = @grb_ccall(read, Cint,
        (Ptr{Void}, Ptr{UInt8}),
        model.ptr_model, filename)
    if ret != 0
        throw(GurobiError(model.env, ret))
    end
    nothing
end

Also, I added this function in the export section of the Gurobi.jl file.
This function is based on a call of GRBread() in one of Gurobi’s '.cpp files.

GRBModel::read(const string& filename)
{
  if (Cmodel == NULL) throw
    GRBException("Model not loaded", GRB_ERROR_INTERNAL);
  int error = GRBread(Cmodel, filename.c_str());
  if (error) throw GRBException(string(GRBgeterrormsg(Cenv)), error);
}

Some advice or thoughts on this would be great.

Is it possible to use *.mst files for this purpose?
Since, I was not getting any errors, but the internalModel does not seem to change, I figured that something must be wrong with the pointer to the model in the function. I do not have any experience with ccalls in Julia.

Anton


I provided a detailed solution in an answer below.

Do you have a MWE that demonstrates that Gurobi doesn’t use the MIP start?

You many need to update the model after adding the MIP start.
https://github.com/JuliaOpt/Gurobi.jl/blob/8dd94aebf2b22004cef3b6f3ad7ea00d00b66a1d/src/grb_model.jl#L89-L96

If you are calling solve from JuMP afterwards, you probably need to remove any existing solution to prevent JuMP from calling setwarmstart:
https://github.com/JuliaOpt/JuMP.jl/blob/v0.18.1/src/solvers.jl#L399

Hope that is enough to point you in the right direction.

1 Like

Thanks a lot! Using update_model!() did the trick :slight_smile: I am not so certain though that the mip start is used. Here is a MWE.

using Gurobi
using JuMP

dir = "C:/Users/..."
cd(dir)

simple_model = Model(solver=GurobiSolver())

@variable(simple_model, product_1 >= 0, Int)
@variable(simple_model, product_2 >= 0, Int)
@variable(simple_model, product_3 >= 0, Int)
@variable(simple_model, z >= 0)

@objective(simple_model, Max, z)

@constraint(simple_model, 2 * product_1 + 5 * product_2 + 4 * product_3 == z)
@constraint(simple_model, 0.5 * product_1 + 2 * product_2 + 1 * product_3 <= 6)

JuMP.build(simple_model)

After accessing the “Start”-Array in the Gurobi.Model I just get the default values:

start_1 = Gurobi.get_dblattrarray(simple_model.internalModel.inner, "Start", 1, 4)
println(start_1)

[1.0e101, 1.0e101, 1.0e101, 1.0e101]

Using the solution file (simple_out.sol) from a previous run, by employing the function I added to grb_model.jl, I then get the right start vector:

inject_mip_start = true
if inject_mip_start
        grb_read(simple_model.internalModel.inner, "simple_out.sol")
        update_model!(simple_model.internalModel.inner)
end

start_2 = Gurobi.get_dblattrarray(simple_model.internalModel.inner, "Start", 1, 4)
println(start_2)

[12.0, 0.0, 0.0, 24.0]

If I do not use update_model!(), the start values are not updated in the model. Now, I would like to test, whether this solution is used during optimization. I used optimize(simple_model.internalModel.inner) as well as solve(simple_model) right after building the model using JuMP and after injecting the mip start. Unfortunately, I get the same solver output. It does not seem that this initial solution is taken advantage of.

Are you aware of anything that might cause this, or a way of verifying that the mip start is used? Btw, do you know where JuMP.solve() and JuMP.build() are defined? I could not find these functions in the JuMP-repo on Github. Thanks a lot, sorry for all these questions :zipper_mouth_face:


In order for this example to work, the the function grb_read() (as provided in the initial post) must be added to the local grb_model.jl file in the local Gurobi package. Also, grb_read must be added to the export section in the Gurobi.jl file.

A solution file must be produced before it can be injected. For this it is possible to use:
write_model(simple_model.internalModel.inner, "simple_out.sol")
… after initially solving the model with inject_mip_start = false.


OK, last update, the solution as presented works :slight_smile: It is crucial to call Gurobi.optimize() instead of JuMP.solve(). Thanks again, this was definitely pointing me in the right direction :slight_smile: I verified it, by applying it to a much bigger model. The issue was, that my simple_model was too less of a challenge, as an optimal solution could instantly be obtained.

Would it be possible that I create a PR so grb_read() could be merged into the Gurobi package?

The following works for me.

using Gurobi
using JuMP

simple_model = Model(solver=GurobiSolver(Presolve=0))

@variable(simple_model, product_1 >= 0, Int)
@variable(simple_model, product_2 >= 0, Int)
@variable(simple_model, product_3 >= 0, Int)
@variable(simple_model, z >= 0)

@objective(simple_model, Max, z)

@constraint(simple_model, 2 * product_1 + 5 * product_2 + 4 * product_3 == z)
@constraint(simple_model, 0.5 * product_1 + 2 * product_2 + 1 * product_3 <= 6)

JuMP.build(simple_model)

start_1 = Gurobi.get_dblattrarray(simple_model.internalModel.inner, "Start", 1, 4)

solve(simple_model)
write_model(simple_model.internalModel.inner, "simple_out.sol")

simple_model.colVal .= NaN

grb_read(simple_model.internalModel.inner, "simple_out.sol")
Gurobi.update_model!(simple_model.internalModel.inner)

solve(simple_model)

Here is the Gurobi log.

Academic license - for non-commercial use only
Optimize a model with 2 rows, 4 columns and 7 nonzeros
Variable types: 1 continuous, 3 integer (0 binary)
Coefficient statistics:
  Matrix range     [5e-01, 5e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [6e+00, 6e+00]
Found heuristic solution: objective -0
Variable types: 0 continuous, 4 integer (0 binary)

Root relaxation: objective 2.400000e+01, 3 iterations, 0.00 seconds

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

*    0     0               0      24.0000000   24.00000  0.00%     -    0s

Explored 0 nodes (3 simplex iterations) in 0.00 seconds
Thread count was 8 (of 8 available processors)

Solution count 2: 24 -0

Optimal solution found (tolerance 1.00e-04)
Best objective 2.400000000000e+01, best bound 2.400000000000e+01, gap 0.0000%
Optimize a model with 2 rows, 4 columns and 7 nonzeros
Variable types: 1 continuous, 3 integer (0 binary)
Coefficient statistics:
  Matrix range     [5e-01, 5e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [6e+00, 6e+00]

Loaded MIP start with objective 24
MIP start did not produce a new incumbent solution

Variable types: 0 continuous, 4 integer (0 binary)

Root relaxation: cutoff, 2 iterations, 0.00 seconds

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0     cutoff    0        24.00000   24.00000  0.00%     -    0s

Explored 0 nodes (2 simplex iterations) in 0.00 seconds
Thread count was 8 (of 8 available processors)

Solution count 1: 24

Optimal solution found (tolerance 1.00e-04)
Best objective 2.400000000000e+01, best bound 2.400000000000e+01, gap 0.0000%

I could not find these functions in the JuMP-repo on Github

JuMP is going through a re-write, so you will need to look at the release-0.18 branch

Would it be possible that I create a PR so grb_read() could be merged into the Gurobi package?

Yes please.

I was wondering if there is a way to achieve this with a more recent version of JuMP? When I run grb_read(m.internalModel.inner, "output.sol"), I got the error type Model has no field internalModel.

I’m using JuMP v1.0.0 and Julia 1.6.

We have greatly improved and simplified things since the post in 2018. The equivalent is now:

using JuMP, Gurobi
model = direct_model(Gurobi.Optimizer())
set_optimizer_attribute(model, "Presolve", 0)
@variable(model, product_1 >= 0, Int)
@variable(model, product_2 >= 0, Int)
@variable(model, product_3 >= 0, Int)
@variable(model, z >= 0)
@objective(model, Max, z)
@constraint(model, 2 * product_1 + 5 * product_2 + 4 * product_3 == z)
@constraint(model, 0.5 * product_1 + 2 * product_2 + 1 * product_3 <= 6)
optimize!(model)
grb = backend(model)
GRBwrite(grb, "simple_out.sol")
set_start_value.(all_variables(model), nothing)
GRBread(grb, "simple_out.sol")
optimize!(model)
2 Likes

Thank you very much, Oscar!

1 Like