Repository navigation
perf(gurobi): load the model in one loadModel call and read solutions in bulk - #1126
Conversation
… in bulk The Gurobi direct build passes columns, the linear rows (CSC), the objective (incl. the quadratic part) and the sense to a single gurobipy.loadModel call instead of addMVar + setObjective + addMConstr. Names are set with one whole-model setAttr each; SOS and indicator constraints are added over the model's Var list. gurobipy < 12 or a missing env falls back to the previous addMVar path. The solution and duals are read back with one getAttr call each instead of a property read per variable and constraint. Approach ported from fluxopt/specsolve#1942. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4nx8GJ8mSNvkTagja9dMj
Merging this PR will improve performance by 61.49%2 warnings, 1 note
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
Note
The following content was generated by AI.
Changes proposed in this Pull Request
This ports the approach of fluxopt/specsolve#1942 (MIT licensed) to linopy's Gurobi direct path. specsolve#1942 swapped
addMVar+ per-blockaddMConstrfor one column-wisegurobipy.loadModelcall. It passes senses asS1bytes and reads results back with whole-modelgetAttr/setAttrinstead of throughMVar/MConstrhandles. No code was copied. The change is reimplemented on top of linopy'sMatrices.Gurobi._build_solver_model(used byto_gurobipyandsolve(io_api="direct")):gurobipy.loadModel(...)call withobj,lb,ub,vtype(only when the model has discrete columns),constr_csc(M.A.tocsc()),sense(asS1bytes),rhs,modelsenseandqobj_coo. The quadratic objective0.5 x'Qxis folded into the upper triangle ofQ, with the coefficients halved.set_names=True, names are set with one whole-modelsetAttr("VarName"/"ConstrName", ...)call each.loadModelcannot take, are still added overgm.getVars(). That logic is moved unchanged into a shared helper,_add_sos_and_indicators.loadModel, so it is gone before the names are allocated.addMVar/addMConstrbuild is kept as_build_solver_model_mvar. It is the fallback for gurobipy < 12 (noloadModel) and forenv=None, becauseloadModelrequires anEnv.Solution readback in
Gurobi._solve:X,Piand (in the from-file case)VarName/ConstrNameare each read with onem.getAttr(attr, objs)call, instead of[v.X for v in vars_]and the like. The dualexceptnow also catchesGurobiError, because the bulkgetAttrraises that on a MILP where the property read raisedAttributeError.Behaviour is unchanged. A new test checks that the
loadModelbuild and the oldaddMVarbuild give identical Gurobi models. It compares the A matrix, the Q matrix (symmetrised), Obj, LB, UB, VType, RHS, Sense, VarName, ConstrName, ModelSense, ObjCon, SOS and general-constraint counts. It covers LP min and max, a MILP with binary, integer and semi-continuous columns, a QP (max, with a cross term), a model without constraints and a model with SOS and indicator constraints, each with names on and off. Other tests check that both builds reach the same optimum, and thatModel.solve(io_api="direct")returns the same primal and dual values asio_api="lp".Benchmarks
The build step was timed by calling
Gurobi._build_solver_modelon a precomputedMatrices, fororigin/masterand for this branch. Each variant ran in a fresh process. Peak RSS is theVmHWMdelta during the build. Gurobi 13.0.3.set_names=TrueMemory: the CSR to CSC conversion that
loadModelneeds is a transient copy of A, about 22 MB at 3M nnz (the peak-versus-retained gap above). It is freed straight afterloadModel. Even with that copy, peak memory is about 4x lower than master, because noMVar/MLinExpr/MConstrobjects are built.Solution readback could only be timed in isolation, because a size-limited license cannot solve a large model. On 500k variables, reading an attribute with
[v.LB for v in vars]took 0.52 s, andm.getAttr("LB", vars)took 0.13 s.Benchmark output
The model is
x[i,j] >= 0(n×n) andy[i]in [-5, 5], with constraintssum_j x[i,j] + 2 y[i] >= iandx - 0.5 x.shift(dim_0=1) <= 10, and objective1.5 Σx + Σy(plusΣy² + Σx²for the quadratic variant).Tests
pytest testwith Gurobi 13 (size-limited license): 5278 passed, 117 skipped.ruff check,ruff formatandmypy .are clean.Checklist
AGENTS.md).doc.doc/release_notes.rstof the upcoming release is included.Generated by Claude Code