You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Support for matrix style model construction (MathProgBase/LinearQuadratic style) in modern Julia + MOI #34
I’m reaching out about support or example workflows for constructing optimization models from raw matrix data in modern Julia, similar to how it was done with MathProgBase’s LinearQuadratic interface.
A bit of background on my situation:
• Years ago I implemented a large scale unit commitment MILP in Julia 0.6.4, using MathProgBase.jl and specifically the LinearQuadratic models interface, together with Gurobi as the solver.
• The model is truly large: on the order of ~2 million variables and constraints. I built the model from big matrices and vectors (A, b, c, etc.) in a Matlab style fashion and handed that directly to the solver.
Now I’d like to move the codebase to a current Julia version, but without changing the overall program structure—i.e., keeping a MathProgBase / LinearQuadratic style formulation rather than rewriting everything in JuMP or another high level modeling layer.
Question / request:
Is there any officially supported or recommended way in the current Julia / MOI ecosystem to construct a large LP/MILP directly from matrices and pass it to a solver, similar to the old MathProgBase LinearQuadratic approach?
Thanks for any pointers or suggestions. Even a small example or outline of how to plug matrix data into MOI, or a note on current limitations, would be very valuable.
We've been advising people to just go with @constraint(model, A * x <= b) and check if that's really a performance issue. We also developed this replacement for that specific use case of MathProgBase in this package: https://github.com/jump-dev/MatrixOptInterface.jl. Let us know if you get into any trouble with it.
Wait, I thought this issue was opened on the MathOptInterface package but I don't see any trace that the issue was transferred. Sorry, then my answer is a bit weird if that's the case ^^
Hi, sorry for the confusion — the issue is indeed on the MatrixOptInterface side, not MathOptInterface.
We tested MatrixOptInterface in detail for large-scale models and found that the bottleneck is the copy_to step, which scales mainly with the number of constraints and becomes very slow at ~10^6 scale.
Happy to repost benchmark details if useful.
@odow Thanks for the link! This looks related to our original MathProgBase transition question from December.
Our current issue is about copy_to performance at large scale, which we described above.
So it's the copy_to from a model in here to Gurobi.Optimizer ? What you pin point a method that is being the bottleneck ? Our maybe share the flamegraph ?
yes — the bottleneck I see is specifically the MOI.copy_to(opt, lp) where lp is a MatrixOptInterface.LPForm (matrix form) and opt is Gurobi.Optimizer().
I isolated it by timing components separately (matrix build + LPForm creation are negligible; integrality constraints are basically free; copy_to dominates and scales mostly with the number of constraints).
I don’t yet have a flamegraph, but I can produce one with Julia’s profiler around MOI.copy_to and share it. Is an SVG/HTML flamegraph ok, or would a “top hotspots” text summary be enough?
If helpful, I can also paste the minimal benchmark snippet we used (varying #constraints with fixed nnz/row) so you can reproduce locally.
Yes, exactly: bottleneck is MOI.copy_to(opt, lp) where lp is MatrixOptInterface.LPForm and opt is Gurobi.Optimizer().
I profiled a “copy_to-only” reproducer (lp built once; then just MOI.copy_to). The hotspots are in MOI’s generic copy path: MOI.Utilities.copy.default_copy_to -> pass_nonvariable_constraints_fallback / _copy_constraints.
Inside that, a lot of time is spent (1) extracting rows from SparseMatrixCSC via getindex(A, i, J::UnitRange) (tons of searchsortedfirst), and (2) calling Gurobi.add_constraint(ScalarAffineFunction, LessThan) which ends up in GRBaddconstr (appears in the profile), i.e., constraints are added one-by-one.
This matches our benchmarks: runtime scales mostly with #constraints. I attached a flamegraph SVG.
I also generated the same profile using StatProfilerHTML (interactive HTML), which makes the hotspots much clearer. I’ve uploaded that version as well in case it’s helpful.
It seems this is due to getting rows in SparseMatrixCSC that is slow. Getting a row in a transpose of SparseMatrixCSC would be equivalent to getting a column which might be much faster. Does it help to pass copy(A')' instead of A to the LPForm ? From a quick look, it does not seem that SparseArrays has a shortcut for getting a row form a transpose of sparse matrix so I don't think that it will help :/ We should add a special case for transpose matrices here
Thanks! We tried A_fast = copy(transpose(A))' (i.e., passing copy(A')'-style) but for us it got worse:
original copy_to ~43s vs A_fast ~73s at (nvar=ncon=100k, nnz/row=5).
So it seems the Adjoint/transpose trick doesn’t help in our case (maybe it triggers slower generic getindex paths).
Next we’ll try a CSR (row-oriented) sparse matrix for A (e.g., via SparseMatricesCSR.jl) since the profile suggests heavy row extraction.
I’ll report back with numbers.
So it seems the slowdown isn’t fixed by changing the matrix storage. From the profile it still looks like the dominant cost is MOI’s generic constraint-copy path ending up adding constraints one-by-one (add_constraint -> GRBaddconstr) plus lots of small allocations / index mapping.
Given this, my conclusion is that for million-scale constraints we likely need a bulk-loading approach (Gurobi API GRBaddconstrs / GRBaddvars style) rather than copy_to.
Happy to share more profiling info if useful.
Given this, my conclusion is that for million-scale constraints we likely need a bulk-loading approach (Gurobi API GRBaddconstrs / GRBaddvars style) rather than copy_to.
People routinely build and solve problems of 10^6 scale using JuMP without issue.
I think your issue is first creating the sparse CSC, and then doing a copy_to.
My strong suggestion would be that you just use JuMP.
This package has not had much development time put into it.
How long does it take to build your input matrices? Can you share a minimal reproducible example? What is your standard form?
From a quick look, it does not seem that SparseArrays has a shortcut for getting a row form a transpose of sparse matrix so I don't think that it will help :/ We should add a special case for transpose matrices here
so until we add a special case for transpose matrices here, it won't help. Until then, I think it will actually be faster to just use JuMP and do @constraint(model, A * x <= b) ?
I can take a look at making it fast with the transpose after #35 is merged. Then it will use MOI.Utilities.MatrixOfConstraints. This issue is a good motivation for using MOI.Utilities.MatrixOfConstraints instead of redefining new structs
It would be difficult to provide a small example. Instead, I will upload a section of the code that builds the equality constraints in A.
Currently, it is a 413857×2248008 SparseMatrixCSC with 2962010 stored entries, and it builds in less than 10 seconds. I don't know how long it would take with JUMP. But that's not the real question. The real reason I use this structure (instead of JUMP) is that I have built an Excel model in which I can easily rewrite the code if I make any changes to the mathematical model. That's why I'm looking for a direct replacement for MathProgBase.loadproblem!.
@blegat Just to help us plan on our side: do you have a rough sense whether this is something you’d expect in the near term, or more long-term? This affects whether we postpone migrating our existing Julia 0.6.4 codebase, or commit to a larger rewrite now.
Either way, thanks again for looking into this — we really appreciate the insight.
jump-dev/MathOptInterface.jl#2915 has been merged, so you can try MOI master and #35. Without the transpose trick, it might be a bit faster but the performance should be similar. With the transpose trick, it should be quite a lot faster.
I tried to install MatrixOptInterface PR #35 directly but #refs/pull/35/head isn’t found in the repo. Just to confirm: when you say “MOI master and #35”, which repo/branch does #35 refer to? (We’re currently on MOI#master + MatrixOptInterface#master.)
We checked the MatrixOptInterface commit 510c505... and it no longer defines LPForm (only AbstractLPForm and MILP show up). Our benchmark script was built around LPForm, so we need to adapt it to the new API. Could you confirm the intended replacement (should we construct MatOI.MILP?), and an example of how to build it from (sense, c, A, l/u, lb/ub)?
Update: I got a huge speedup with MOI master (incl. #2915) + MatrixOptInterface lp_form, using the transpose trick.
I used opt = MOI.instantiate(Gurobi.Optimizer; with_bridge_type=Float64) (i.e., light bridging).
Synthetic benchmark (nnz_per_row=5):
• 100k vars / 100k cons: original A copy_to = 119.7 s, transpose trick = 0.619 s (~193×)
• 1M vars / 1M cons: transpose trick copy_to = 7.56 s
Also note: even when setting c_lb = -Inf, lp_form still emits (ScalarAffineFunction, Interval) constraints, so bridges are required for Gurobi unless lp_form special-cases infinite bounds.
Thanks a lot!
Thank you for letting us know! Just to make sure we understand correctly: is it expected that we should still explicitly pass the transpose-style matrix (i.e. the transpose trick) to benefit from the fast path, or should this eventually be handled automatically inside MOI/MatrixOptInterface?
Activity
blegat commentedon Dec 9, 2025
We've been advising people to just go with
@constraint(model, A * x <= b)and check if that's really a performance issue. We also developed this replacement for that specific use case of MathProgBase in this package: https://github.com/jump-dev/MatrixOptInterface.jl. Let us know if you get into any trouble with it.lpaizs commentedon Dec 9, 2025
Hi, thanks so much for the quick reply!
Really appreciate the pointer. I’ll give MatrixOptInterface.jl a try and see how it works with my big model.
blegat commentedon Jan 15, 2026
Wait, I thought this issue was opened on the MathOptInterface package but I don't see any trace that the issue was transferred. Sorry, then my answer is a bit weird if that's the case ^^
odow commentedon Jan 15, 2026
Sorry I'm a bit late to this issue. Check out this tutorial: https://jump.dev/MathOptInterface.jl/stable/tutorials/mathprogbase/
lpaizs commentedon Jan 16, 2026
Hi, sorry for the confusion — the issue is indeed on the MatrixOptInterface side, not MathOptInterface.
We tested MatrixOptInterface in detail for large-scale models and found that the bottleneck is the
copy_tostep, which scales mainly with the number of constraints and becomes very slow at ~10^6 scale.Happy to repost benchmark details if useful.
lpaizs commentedon Jan 16, 2026
@odow Thanks for the link! This looks related to our original MathProgBase transition question from December.
Our current issue is about copy_to performance at large scale, which we described above.
blegat commentedon Jan 17, 2026
So it's the
copy_tofrom a model in here toGurobi.Optimizer? What you pin point a method that is being the bottleneck ? Our maybe share the flamegraph ?lpaizs commentedon Jan 17, 2026
yes — the bottleneck I see is specifically the MOI.copy_to(opt, lp) where lp is a MatrixOptInterface.LPForm (matrix form) and opt is Gurobi.Optimizer().
I isolated it by timing components separately (matrix build + LPForm creation are negligible; integrality constraints are basically free; copy_to dominates and scales mostly with the number of constraints).
I don’t yet have a flamegraph, but I can produce one with Julia’s profiler around MOI.copy_to and share it. Is an SVG/HTML flamegraph ok, or would a “top hotspots” text summary be enough?
If helpful, I can also paste the minimal benchmark snippet we used (varying #constraints with fixed nnz/row) so you can reproduce locally.
lpaizs commentedon Jan 17, 2026
Yes, exactly: bottleneck is
MOI.copy_to(opt, lp)wherelpisMatrixOptInterface.LPFormandoptisGurobi.Optimizer().I profiled a “copy_to-only” reproducer (lp built once; then just
MOI.copy_to). The hotspots are in MOI’s generic copy path:MOI.Utilities.copy.default_copy_to->pass_nonvariable_constraints_fallback/_copy_constraints.Inside that, a lot of time is spent (1) extracting rows from
SparseMatrixCSCviagetindex(A, i, J::UnitRange)(tons ofsearchsortedfirst), and (2) callingGurobi.add_constraint(ScalarAffineFunction, LessThan)which ends up inGRBaddconstr(appears in the profile), i.e., constraints are added one-by-one.This matches our benchmarks: runtime scales mostly with #constraints. I attached a flamegraph SVG.
lpaizs commentedon Jan 17, 2026
I also generated the same profile using StatProfilerHTML (interactive HTML), which makes the hotspots much clearer. I’ve uploaded that version as well in case it’s helpful.
index.html
blegat commentedon Jan 17, 2026
It seems this is due to getting rows in SparseMatrixCSC that is slow. Getting a row in a transpose of
SparseMatrixCSCwould be equivalent to getting a column which might be much faster. Does it help to passcopy(A')'instead ofAto theLPForm? From a quick look, it does not seem thatSparseArrayshas a shortcut for getting a row form a transpose of sparse matrix so I don't think that it will help :/ We should add a special case for transpose matrices herelpaizs commentedon Jan 18, 2026
Thanks! We tried
A_fast = copy(transpose(A))'(i.e., passingcopy(A')'-style) but for us it got worse:original
copy_to~43s vs A_fast ~73s at (nvar=ncon=100k, nnz/row=5).So it seems the
Adjoint/transpose trick doesn’t help in our case (maybe it triggers slower generic getindex paths).Next we’ll try a CSR (row-oriented) sparse matrix for A (e.g., via SparseMatricesCSR.jl) since the profile suggests heavy row extraction.
I’ll report back with numbers.
lpaizs commentedon Jan 18, 2026
We tested the transpose trick and also a CSR matrix, but both got worse on my side.
At (nvar=ncon=100k, nnz/row=5):
So it seems the slowdown isn’t fixed by changing the matrix storage. From the profile it still looks like the dominant cost is MOI’s generic constraint-copy path ending up adding constraints one-by-one (
add_constraint->GRBaddconstr) plus lots of small allocations / index mapping.Given this, my conclusion is that for million-scale constraints we likely need a bulk-loading approach (Gurobi API
GRBaddconstrs/GRBaddvarsstyle) rather thancopy_to.Happy to share more profiling info if useful.
odow commentedon Jan 20, 2026
People routinely build and solve problems of 10^6 scale using JuMP without issue.
I think your issue is first creating the sparse CSC, and then doing a copy_to.
My strong suggestion would be that you just use JuMP.
This package has not had much development time put into it.
How long does it take to build your input matrices? Can you share a minimal reproducible example? What is your standard form?
blegat commentedon Jan 20, 2026
Yes, that aligns with my earlier observation that
so until we add a special case for transpose matrices here, it won't help. Until then, I think it will actually be faster to just use JuMP and do
@constraint(model, A * x <= b)?I can take a look at making it fast with the transpose after #35 is merged. Then it will use
MOI.Utilities.MatrixOfConstraints. This issue is a good motivation for usingMOI.Utilities.MatrixOfConstraintsinstead of redefining newstructslpaizs commentedon Jan 21, 2026
@odow
epmm_optim1.txt
It would be difficult to provide a small example. Instead, I will upload a section of the code that builds the equality constraints in A.
Currently, it is a 413857×2248008 SparseMatrixCSC with 2962010 stored entries, and it builds in less than 10 seconds. I don't know how long it would take with JUMP. But that's not the real question. The real reason I use this structure (instead of JUMP) is that I have built an Excel model in which I can easily rewrite the code if I make any changes to the mathematical model. That's why I'm looking for a direct replacement for MathProgBase.loadproblem!.
@blegat Just to help us plan on our side: do you have a rough sense whether this is something you’d expect in the near term, or more long-term? This affects whether we postpone migrating our existing Julia 0.6.4 codebase, or commit to a larger rewrite now.
Either way, thanks again for looking into this — we really appreciate the insight.
blegat commentedon Jan 21, 2026
I was planning to finish #35 once we tag a new release of MOI. We can probably tag one in the coming days and see whether this helps
lpaizs commentedon Jan 21, 2026
That sounds great, thanks for looking into this so quickly. I'll wait for the new MOI release and test my benchmarks once it’s available.
blegat commentedon Jan 21, 2026
Actually, if you don't mind trying branches, you can already try #35 with MOI master, I'll make a PR to MOI to speed up the transpose case
lpaizs commentedon Jan 21, 2026
Ok. I'am happy to try MOI master with #35 and report back with benchmark results.
blegat commentedon Jan 21, 2026
Here it is jump-dev/MathOptInterface.jl#2915
I would be interested to know the speedup in your case
blegat commentedon Jan 22, 2026
jump-dev/MathOptInterface.jl#2915 has been merged, so you can try MOI master and #35. Without the transpose trick, it might be a bit faster but the performance should be similar. With the transpose trick, it should be quite a lot faster.
lpaizs commentedon Jan 22, 2026
I tried to install MatrixOptInterface PR #35 directly but
#refs/pull/35/headisn’t found in the repo. Just to confirm: when you say “MOI master and #35”, which repo/branch does #35 refer to? (We’re currently on MOI#master + MatrixOptInterface#master.)lpaizs commentedon Jan 22, 2026
We checked the MatrixOptInterface commit 510c505... and it no longer defines LPForm (only AbstractLPForm and MILP show up). Our benchmark script was built around LPForm, so we need to adapt it to the new API. Could you confirm the intended replacement (should we construct MatOI.MILP?), and an example of how to build it from (sense, c, A, l/u, lb/ub)?
blegat commentedon Jan 23, 2026
The branches are MOI#master + MatrixOptInterface#bl/matrix_of_constraints
Replacing
LPFormbylp_formshould be enough.lpaizs commentedon Jan 24, 2026
Update: I got a huge speedup with MOI master (incl. #2915) + MatrixOptInterface lp_form, using the transpose trick.
I used opt = MOI.instantiate(Gurobi.Optimizer; with_bridge_type=Float64) (i.e., light bridging).
Synthetic benchmark (nnz_per_row=5):
• 100k vars / 100k cons: original A copy_to = 119.7 s, transpose trick = 0.619 s (~193×)
• 1M vars / 1M cons: transpose trick copy_to = 7.56 s
Also note: even when setting c_lb = -Inf, lp_form still emits (ScalarAffineFunction, Interval) constraints, so bridges are required for Gurobi unless lp_form special-cases infinite bounds.
Thanks a lot!
blegat commentedon Feb 27, 2026
Glad to read this ! The fix is now included in MOI v1.49 and I merged the MatrixOptInterface#bl/matrix_of_constraints branch
lpaizs commentedon Mar 2, 2026
Thank you for letting us know! Just to make sure we understand correctly: is it expected that we should still explicitly pass the transpose-style matrix (i.e. the transpose trick) to benefit from the fast path, or should this eventually be handled automatically inside MOI/MatrixOptInterface?
blegat commentedon Mar 3, 2026
Yes you should still use the transpose trick, no short term plan to do this automatically
lpaizs commentedon Mar 4, 2026
Thanks for confirming! We'll keep using the transpose trick then.