-
Notifications
You must be signed in to change notification settings - Fork 3
Support QP interface via MOI #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a926678
bbcb4f4
ace38fd
a7ff4a2
7c6a48a
067d973
1841980
c108151
b27e7d0
2ee820d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,8 @@ | |
| # The HiGHS wrapper is released under an MIT license, a copy of which can be | ||
| # found in `/thirdparty/THIRD_PARTY_LICENSES` or at https://opensource.org/licenses/MIT. | ||
|
|
||
| import SparseArrays | ||
|
|
||
| import MathOptInterface as MOI | ||
| const CleverDicts = MOI.Utilities.CleverDicts | ||
|
|
||
|
|
@@ -621,8 +623,13 @@ end | |
|
|
||
| function MOI.supports( | ||
| ::Optimizer, | ||
| ::Union{MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}}, | ||
| ) | ||
| ::MOI.ObjectiveFunction{F}, | ||
| ) where { | ||
| F<:Union{ | ||
| MOI.ScalarAffineFunction{Float64}, | ||
| MOI.ScalarQuadraticFunction{Float64}, | ||
mtanneau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| } | ||
| return true | ||
| end | ||
|
|
||
|
|
@@ -651,6 +658,7 @@ function _check_input_data(dest::Optimizer, src::MOI.ModelLike) | |
| if attr in ( | ||
| MOI.Name(), | ||
| MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), | ||
| MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), | ||
| MOI.ObjectiveSense(), | ||
| ) | ||
| continue | ||
|
|
@@ -903,17 +911,95 @@ function _get_objective_data( | |
|
|
||
| objective_sense = sense == MOI.MIN_SENSE ? CUOPT_MINIMIZE : CUOPT_MAXIMIZE | ||
|
|
||
| objective_coefficients = zeros(Float64, numcol) | ||
| F = MOI.get(src, MOI.ObjectiveFunctionType()) | ||
| f_obj = MOI.get(src, MOI.ObjectiveFunction{F}()) | ||
|
|
||
| objective_offset, | ||
| objective_coefficients_linear, | ||
| qobj_row_offsets, | ||
| qobj_col_indices, | ||
| qobj_matrix_values = _get_objective_data(f_obj, mapping, numcol) | ||
|
|
||
| return objective_sense, | ||
| objective_offset, | ||
| objective_coefficients_linear, | ||
| qobj_row_offsets, | ||
| qobj_col_indices, | ||
| qobj_matrix_values | ||
| end | ||
|
|
||
| function _get_objective_data( | ||
| f_obj::MOI.ScalarAffineFunction, | ||
| mapping, | ||
| numcol::Int32, | ||
| ) | ||
| objective_offset = f_obj.constant | ||
|
|
||
| objective_coefficients_linear = zeros(Float64, numcol) | ||
| for term in f_obj.terms | ||
| objective_coefficients[mapping[term.variable].value] += term.coefficient | ||
| i = mapping[term.variable].value | ||
| objective_coefficients_linear[i] += term.coefficient | ||
| end | ||
|
|
||
| return objective_offset, | ||
| objective_coefficients_linear, | ||
| Int32[0], | ||
| Int32[], | ||
| Float64[] | ||
| end | ||
|
|
||
| function _get_objective_data( | ||
| f_obj::MOI.ScalarQuadraticFunction, | ||
| mapping, | ||
| numcol::Int32, | ||
| ) | ||
| objective_offset = f_obj.constant | ||
|
|
||
| return objective_sense, objective_offset, objective_coefficients | ||
| objective_coefficients_linear = zeros(Float64, numcol) | ||
| for term in f_obj.affine_terms | ||
| i = mapping[term.variable].value | ||
| objective_coefficients_linear[i] += term.coefficient | ||
| end | ||
|
|
||
| # Extract quadratic objective | ||
| Qtrows = Int32[] | ||
| Qtcols = Int32[] | ||
| Qtvals = Float64[] | ||
| sizehint!(Qtrows, length(f_obj.quadratic_terms)) | ||
| sizehint!(Qtcols, length(f_obj.quadratic_terms)) | ||
| sizehint!(Qtvals, length(f_obj.quadratic_terms)) | ||
| for qterm in f_obj.quadratic_terms | ||
| i = mapping[qterm.variable_1].value | ||
| j = mapping[qterm.variable_2].value | ||
| v = qterm.coefficient | ||
| # MOI stores quadratic functions as `¹/₂ xᵀQx + aᵀx + b`, with `Q` symmetric... | ||
| # (https://jump.dev/MathOptInterface.jl/stable/reference/standard_form/#MathOptInterface.ScalarQuadraticFunction) | ||
| # ... whereas cuOpt expects a QP objective of the form `¹xᵀQx + aᵀx + b`, | ||
| # where `Q` need not be symmetric | ||
| # --> we need to scale diagonal coeffs. by ¹/₂ to match cuOpt convention | ||
| if i == j | ||
| v /= 2 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens to the off-diagonal terms?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 So if the objective is
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (including the links for completeness)
I believe the
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 termsthe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rg20 I can add the following:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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").
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mtanneau thanks for explaining the subtleties in the MOI wrapper.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added more unit tests that specifically trigger the QP objective.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| end | ||
|
|
||
| # We are building a COO of Qᵀ --> swap i and j | ||
| push!(Qtrows, j) | ||
| push!(Qtcols, i) | ||
| push!(Qtvals, v) | ||
| end | ||
| # CSC of Qᵀ is CSR of Q | ||
| Qt = SparseArrays.sparse(Qtrows, Qtcols, Qtvals, numcol, numcol) | ||
| qobj_matrix_values = Qt.nzval | ||
| qobj_row_offsets = Qt.colptr | ||
| qobj_col_indices = Qt.rowval | ||
| # Revert to 0-based indexing | ||
| qobj_row_offsets .-= Int32(1) | ||
| qobj_col_indices .-= Int32(1) | ||
|
|
||
| return objective_offset, | ||
| objective_coefficients_linear, | ||
| qobj_row_offsets, | ||
| qobj_col_indices, | ||
| qobj_matrix_values | ||
| end | ||
|
|
||
| function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) | ||
|
|
@@ -970,27 +1056,63 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) | |
| has_integrality = true | ||
| end | ||
|
|
||
| objective_sense, objective_offset, objective_coefficients = | ||
| _get_objective_data(dest, src, mapping, numcol) | ||
|
|
||
| ref_problem = Ref{cuOptOptimizationProblem}() | ||
| ret = cuOptCreateRangedProblem( | ||
| numrow, | ||
| numcol, | ||
| objective_sense, | ||
| objective_offset, | ||
| objective_coefficients, | ||
| constraint_matrix_row_offsets, | ||
| constraint_matrix_column_indices, | ||
| constraint_matrix_coefficients, | ||
| rowlower, | ||
| rowupper, | ||
| collower, | ||
| colupper, | ||
| var_type, | ||
| ref_problem, | ||
| ) | ||
| _check_ret(ret, "cuOptCreateRangedProblem") | ||
| objective_sense, | ||
| objective_offset, | ||
| objective_coefficients, | ||
| qobj_row_offsets, | ||
| qobj_col_indices, | ||
| qobj_matrix_values = _get_objective_data(dest, src, mapping, numcol) | ||
|
|
||
| # 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", | ||
| ) | ||
| end | ||
|
|
||
| if has_quadratic_objective | ||
| ref_problem = Ref{cuOptOptimizationProblem}() | ||
| ret = cuOptCreateQuadraticRangedProblem( | ||
| numrow, | ||
| numcol, | ||
| objective_sense, | ||
| objective_offset, | ||
| objective_coefficients, | ||
| qobj_row_offsets, | ||
| qobj_col_indices, | ||
| qobj_matrix_values, | ||
| constraint_matrix_row_offsets, | ||
| constraint_matrix_column_indices, | ||
| constraint_matrix_coefficients, | ||
| rowlower, | ||
| rowupper, | ||
| collower, | ||
| colupper, | ||
| ref_problem, | ||
| ) | ||
| _check_ret(ret, "cuOptCreateQuadraticRangedProblem") | ||
| else | ||
| ref_problem = Ref{cuOptOptimizationProblem}() | ||
| ret = cuOptCreateRangedProblem( | ||
| numrow, | ||
| numcol, | ||
| objective_sense, | ||
| objective_offset, | ||
| objective_coefficients, | ||
| constraint_matrix_row_offsets, | ||
| constraint_matrix_column_indices, | ||
| constraint_matrix_coefficients, | ||
| rowlower, | ||
| rowupper, | ||
| collower, | ||
| colupper, | ||
| var_type, | ||
| ref_problem, | ||
| ) | ||
| _check_ret(ret, "cuOptCreateRangedProblem") | ||
| end | ||
|
|
||
| dest.cuopt_problem = ref_problem[] | ||
|
|
||
| ref_settings = Ref{cuOptSolverSettings}() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.