Skip to content

Support QP interface via MOI#30

Merged
mlubin merged 10 commits intojump-dev:mainfrom
mtanneau:qp-interface-moi
Apr 21, 2026
Merged

Support QP interface via MOI#30
mlubin merged 10 commits intojump-dev:mainfrom
mtanneau:qp-interface-moi

Conversation

@mtanneau
Copy link
Copy Markdown
Contributor

This PR introduces support for solving Quadratic Programs with cuOpt.

Notes:

  • cuOpt only supports QPs with convex objectives. The MOI wrapper does not perform any convexity checks, it passes the matrix as-is to the solver
  • while building the wrapper, I encountered an upstream bug (see [BUG] cuOpt crashes when solving QP with no linear constraints NVIDIA/cuopt#759) that led me to disable two MOI tests
  • I have tested this locally on a DGX Spark machine as follows:
    • OS: Ubuntu (Linux aarch64)
    • GPU: GB10
    • cuOpt: v25.12.0

Please see extra comments in the diff

Comment thread Project.toml
Comment thread src/MOI_wrapper.jl
Comment thread src/MOI_wrapper.jl Outdated
Comment thread src/MOI_wrapper.jl Outdated
# Is this a QP or an LP?
has_quadratic_objective = length(qobj_matrix_values) > 0
if has_quadratic_objective && has_integrality
error("cuOpt does not support models with quadratic objectives _and_ integer variables")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the best error / error message is here, happy to modify this as requested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that cuOpt does not support MIQPs, only MILP or continuous QP

Copy link
Copy Markdown
Member

@odow odow Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is okay for now.

As other options: you could remove the error and make it so that TerminationStatus was MOI.INVALID_MODEL. Or you could throw(MOI.SetAttributeNotAllowed(obj_attr, "cuOpt does not support ...")) but it's a coin toss whether the objective or integer constraint is the problem. And MOI doesn't have a bridge to fix this so the error is going to propagate to the user anyway.

Comment thread src/MOI_wrapper.jl Outdated
Comment thread src/MOI_wrapper.jl Outdated
Comment thread src/MOI_wrapper.jl Outdated
Comment thread src/MOI_wrapper.jl Outdated
@mlubin
Copy link
Copy Markdown
Member

mlubin commented Jan 11, 2026

From the README:

Note: This version of cuOpt.jl supports the Nvidia cuOpt 25.08, 25.10, and 25.12 releases.

Which version of cuOpt introduced this QP API?

@mtanneau
Copy link
Copy Markdown
Contributor Author

mtanneau commented Jan 11, 2026

Which version of cuOpt introduced this QP API?

It was added to the C API in cuOpt v25.12 (See release notes)

@mlubin
Copy link
Copy Markdown
Member

mlubin commented Jan 11, 2026

@rgsl888prabhu should weigh in on if we want to bump the minimum supported version of cuOpt to 25.12 or add appropriate error messages when users try to solve QPs with an older version. Bumping the required version is ok with me since it makes maintenance of the wrapper easier.

Comment thread src/MOI_wrapper.jl Outdated
@blegat
Copy link
Copy Markdown
Member

blegat commented Jan 12, 2026

I think DCO is failing because the commit made via the github UI were not signed off

@mtanneau
Copy link
Copy Markdown
Contributor Author

I think DCO is failing because the commit made via the github UI were not signed off

Thank you, fixed and rebased

Copy link
Copy Markdown
Collaborator

@rg20 rg20 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for implementing the QP interface for cuOpt!

I think the cuOpt version support might need changes in cuOpt.jl file. The QP support only works from 25.12.

Comment thread src/MOI_wrapper.jl
v = qterm.coefficient
if i == j
# Adjust diagonal coefficients to match cuOpt convention
v /= 2
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is accurate.

cuOpt expects users to provide the true objective function.

For example: if you are minimizing x1^2 + x2^2, the matrix should be [1 0; 0 1], cuOpt internally minimizes for (1/2) [x1 x2] [2 0; 0 2] [x1; x2] in this case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cuOpt expects users to provide the true objective function.

https://www.youtube.com/watch?v=M31xoZGyj9w&t=2698s

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to the off-diagonal terms?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cuOpt is not necessarily expecting the matrix to be symmetric.

If the objective is xT Q x + cT x, we internally symmetrize and solve for (1/2) xT (Q + QT) x + cT x

So if the objective is x1^2 + x2^2 + 2 x1 x2,

Q can be: [1 2; 0 1], or [1 0; 2 1] or [1 1; 1; 1]

Copy link
Copy Markdown
Contributor Author

@mtanneau mtanneau Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(including the links for completeness)

I believe the /2 factor for diagonal terms is required for correctness, but agreed it's not super clear why at first.

  1. The current MOI wrapper uses a JuMP-level data structure to store the optimization model as the user builds it. That internal representation abides by MOI conventions.
  2. when optimize! gets called, the problem data is flushed from the JuMP-level data store into a cuOpt-level data structure --> this is why this PR mostly modifies the copy_to function.
  3. Hence, when we access the quadratic objective in copy_to, we get as input an MOI.ScalarQuadraticFunction, which abides by the MOI convention described in the docs above.

Here is a small example to illustrate this

using JuMP

model = Model()
@variable(model, x)
@variable(model, y)
@objective(model, Min, x*x + 2*x*y + 3*y*y)
objective_function(model)  # x² + 2 x*y + 3 y²

# Now, access the MOI representation
F = MOI.get(model, MOI.ObjectiveFunctionType())
f = MOI.get(model, MOI.ObjectiveFunction{F}())
f.quadratic terms

the last line outputs

3-element Vector{MathOptInterface.ScalarQuadraticTerm{Float64}}:
 MathOptInterface.ScalarQuadraticTerm{Float64}(2.0, MOI.VariableIndex(1), MOI.VariableIndex(1))
 MathOptInterface.ScalarQuadraticTerm{Float64}(2.0, MOI.VariableIndex(1), MOI.VariableIndex(2))
 MathOptInterface.ScalarQuadraticTerm{Float64}(6.0, MOI.VariableIndex(2), MOI.VariableIndex(2))

--> you can see that the diagonal coefficients were multiplied by 2. This was done by MOI in accordance with its convention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rg20 I can add the following:

  • add link to the MOI docs on quadratic function in that comment, to provide additional context
  • add a couple of unit tests to validate the cuOpt solution and objective value when solving QP from MOI

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My conclusion with this all is that the only valid way is to test, test, test, test, and test. There are too many subtleties to try and logically reason about the transformations, and every time I do, I end up making a mistake.

There are tests in MOI for various cases, but these are precisely the ones that you're skipping because of the upstream bug... 😢 ("test_objective_qp_ObjectiveFunction_zero_ofdiag" and "test_objective_qp_ObjectiveFunction_edge_cases").

Copy link
Copy Markdown
Collaborator

@rg20 rg20 Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtanneau thanks for explaining the subtleties in the MOI wrapper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more unit tests that specifically trigger the QP objective.
Notes:

  • the upstream issue regarding QP with no constraints might get fixed in cuopt v26.02 (tracking Use augmented system when there are no constraints NVIDIA/cuopt#765), which would allow to run more MOI-level tests.
  • if the team is OK with adding JuMP as a test dependency, I'm happy to add similar tests where the QP is built from JuMP, not from MOI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JuMP shouldn't be a test dependency; MOI-style tests should be sufficient

@rgsl888prabhu
Copy link
Copy Markdown
Collaborator

@rgsl888prabhu should weigh in on if we want to bump the minimum supported version of cuOpt to 25.12 or add appropriate error messages when users try to solve QPs with an older version. Bumping the required version is ok with me since it makes maintenance of the wrapper easier.

@mlubin Yes, lets bump it to 25.12.

@rgsl888prabhu
Copy link
Copy Markdown
Collaborator

Shall we bump this 26.02 and merge this PR ?

@mtanneau
Copy link
Copy Markdown
Contributor Author

mtanneau commented Mar 3, 2026 via email

@mtanneau
Copy link
Copy Markdown
Contributor Author

mtanneau commented Mar 3, 2026

Re-v26.02: I opened a separate issue (#32) and will open a separate PR for the C API and existing MOI interface.

I'll keep all the QP API wrapping in MOI in this PR, so will come back here once #32 is addressed.

@rgsl888prabhu
Copy link
Copy Markdown
Collaborator

@odow @rg20 shall we merge this ?

@odow
Copy link
Copy Markdown
Member

odow commented Mar 4, 2026

No opinion. I assume it is passing tests?

@rgsl888prabhu
Copy link
Copy Markdown
Collaborator

@mtanneau can you please confirm all tests are passing ?

@mtanneau
Copy link
Copy Markdown
Contributor Author

mtanneau commented Mar 4, 2026

I just re-ran tests locally on this branch, building with cuOpt v26.02. Putting aside the same errors that I encountered in #33 (i.e., upstream issue in cuOpt where dual infeasibility is detected at presolve), tests are passing.

I would rather merge #33 first, so that the cuOpt compat requirements matches the C API bindings.
This branch sets cuopt compat to v26.02, but uses C API bindings that were generated from v25.XX, which feels off (despite working seemingly well).

@mlubin
Copy link
Copy Markdown
Member

mlubin commented Apr 16, 2026

@mtanneau please feel free to rebase this and we'll get it reviewed and landed quickly.

Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
@mtanneau
Copy link
Copy Markdown
Contributor Author

rebased and updated!
Tests are passing locally with cuOpt v26.04

image

Comment thread test/MOI_wrapper.jl Outdated
0.25;
atol = 1e-4,
rtol = 1e-4,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like a good idea to test the dual solutions as well. Note you might run into NVIDIA/cuopt#1119.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't the MOI tests hitting this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if NVIDIA/cuopt#1119 is a bug in cuopt or just in the cvxpy wrapper. If it's the latter then the MOI tests won't hit it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the MOI interface doesn't appear to support querying MOI.ConstraintDual attribute, so I would suggest holding off on that and adding support in a separate PR

Comment thread test/MOI_wrapper.jl Outdated
MOI.ScalarAffineFunction(
[MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(1.0, y)],
0.0,
),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use operator overloading to make the functions: 1.0 * x + 1.0 * y. It makes things a lot clearer.

Comment thread test/MOI_wrapper.jl Outdated
],
MOI.ScalarAffineTerm{Float64}[],
0.0,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here: 0.5 * x * x + 0.5 * y * y. Then there's no confusion with the 1/2

Comment thread test/MOI_wrapper.jl Outdated
],
[MOI.ScalarAffineTerm(-4.0, x), MOI.ScalarAffineTerm(+4.0, y)],
4.0,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strongly prefer to create new test functions rather than having long complicated tests.

You can test:

  • a diagonal QP objective
  • an off-diagonal QP objective
  • changing from QP to LP
  • etc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you even need these tests? They should be covered by one the ones in MOI

Copy link
Copy Markdown
Contributor Author

@mtanneau mtanneau Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I wrote those tests to

  • circumvent some of the excluded MOI tests and/or upstream issues,
  • validate the MOI vs cuOpt convention regarding QP objective (that infamous 1/2 coefficient)

I removed them as they are indeed covered by MOI tests.

Signed-off-by: mtanneau <mathieu.tanneau@gmail.com>
@mlubin mlubin merged commit 706afa4 into jump-dev:main Apr 21, 2026
3 checks passed
mlubin referenced this pull request Apr 21, 2026
Signed-off-by: Miles Lubin <mlubin@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

6 participants