Skip to content

Conversation

@mtanneau
Copy link

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

# 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
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
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
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.

@mlubin
Copy link
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
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
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.

@blegat
Copy link
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
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
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.

v = qterm.coefficient
if i == j
# Adjust diagonal coefficients to match cuOpt convention
v /= 2
Copy link
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
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
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
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
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
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
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
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
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
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
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.

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