diff --git a/Manifest.toml b/Manifest.toml index 04b7c1d..901feca 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.12.2" manifest_format = "2.0" -project_hash = "2ccee5e212b4700701dc3d1848104837eff8853c" +project_hash = "f7e9da03273385049341b25c4033f9502b2f4291" [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" @@ -35,9 +35,9 @@ version = "0.5.0" [[deps.CUDA_Driver_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "2023be0b10c56d259ea84a94dbfc021aa452f2c6" +git-tree-sha1 = "63b4911c80ade9de10ec4b766e99cb1a628f465f" uuid = "4ee394cb-3365-5eb0-8335-949819d2adfc" -version = "13.0.2+0" +version = "13.1.0+0" [[deps.CUDA_Runtime_jll]] deps = ["Artifacts", "CUDA_Driver_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "TOML"] @@ -47,9 +47,9 @@ version = "0.19.2+0" [[deps.CUDA_SDK_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "8c546633bdb21b7b806276c0677a5656b4f675bc" +git-tree-sha1 = "f2f5388f3141f39a0d2a9145ceaa280fb055ec60" uuid = "6cbf2f2e-7e60-5632-ac76-dca2274e0be0" -version = "13.0.2+1" +version = "13.1.0+0" [[deps.Clang]] deps = ["CEnum", "Clang_jll", "Downloads", "Pkg", "TOML"] @@ -334,9 +334,9 @@ version = "1.3.3" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "0f27480397253da18fe2c12a4ba4eb9eb208bf3d" +git-tree-sha1 = "522f093a29b31a93e34eaea17ba055d850edea28" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.5.0" +version = "1.5.1" [[deps.Printf]] deps = ["Unicode"] @@ -460,13 +460,13 @@ version = "1.3.1+2" deps = ["CUDA_Runtime_jll", "CUDA_SDK_jll", "Clang", "MathOptInterface", "Test", "cuPDLPx_jll"] path = "." uuid = "bcd6524d-1420-4b17-a582-359cb8a71a63" -version = "0.1.4" +version = "0.1.5" [[deps.cuPDLPx_jll]] deps = ["Artifacts", "CUDA_Runtime_jll", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "TOML", "Zlib_jll"] -git-tree-sha1 = "ba09f738d421a949c9058167743c33275cf58400" +git-tree-sha1 = "de696a114f309c8832934c6234b923eb54f31784" uuid = "bca5daad-f4d3-5101-ae12-8b63679c982c" -version = "0.1.4+0" +version = "0.1.5+0" [[deps.libLLVM_jll]] deps = ["Artifacts", "Libdl"] diff --git a/Project.toml b/Project.toml index b886991..f5aba18 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "cuPDLPx" uuid = "bcd6524d-1420-4b17-a582-359cb8a71a63" -version = "0.1.4" +version = "0.1.5" [deps] CUDA_Runtime_jll = "76a88914-d11a-5bdc-97e0-2f5a05c973a2" @@ -16,4 +16,4 @@ CUDA_SDK_jll = "13.0.2" Clang = "0.19.0" MathOptInterface = "1.46.0" Test = "1.11.0" -cuPDLPx_jll = "0.1.4" +cuPDLPx_jll = "0.1.5" diff --git a/src/LibcuPDLPx.jl b/src/LibcuPDLPx.jl index 30adc03..f309e39 100644 --- a/src/LibcuPDLPx.jl +++ b/src/LibcuPDLPx.jl @@ -1,6 +1,7 @@ module LibcuPDLPx using cuPDLPx_jll +const libcupdlpx = cuPDLPx_jll.libcupdlpx export cuPDLPx_jll @enum termination_reason_t::UInt32 begin @@ -51,15 +52,15 @@ end struct pdhg_parameters_t l_inf_ruiz_iterations::Cint - has_pock_chambolle_alpha::Bool + has_pock_chambolle_alpha::Cint pock_chambolle_alpha::Cdouble - bound_objective_rescaling::Bool - verbose::Bool + bound_objective_rescaling::Cint + verbose::Cint termination_evaluation_frequency::Cint termination_criteria::termination_criteria_t restart_params::restart_parameters_t reflection_coefficient::Cdouble - feasibility_polishing::Bool + feasibility_polishing::Cint end struct cupdlpx_result_t diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 677724a..62b8554 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -1,6 +1,7 @@ import MathOptInterface as MOI -# Inspired from `Clp.jl/src/MOI_wrapper/MOI_wrapper.jl` +const Lib = cuPDLPx.LibcuPDLPx + MOI.Utilities.@product_of_sets( _LPProductOfSets, MOI.EqualTo{T}, @@ -15,11 +16,7 @@ const OptimizerCache = MOI.Utilities.GenericModel{ MOI.Utilities.VariablesContainer{Cdouble}, MOI.Utilities.MatrixOfConstraints{ Cdouble, - MOI.Utilities.MutableSparseMatrixCSC{ - Cdouble, - Cint, - MOI.Utilities.ZeroBasedIndexing, - }, + MOI.Utilities.MutableSparseMatrixCSC{Cdouble,Cint,MOI.Utilities.ZeroBasedIndexing}, MOI.Utilities.Hyperrectangle{Cdouble}, _LPProductOfSets{Cdouble}, }, @@ -37,17 +34,22 @@ const BOUND_SETS = Union{ """ Optimizer() -Create a new cuPDLP optimizer. +Create a new cuPDLPx optimizer. """ mutable struct Optimizer <: MOI.AbstractOptimizer - result::Union{Nothing,LibcuPDLPx.cupdlpx_result_t} + result::Union{Nothing,Lib.cupdlpx_result_t} + parameters::Lib.pdhg_parameters_t + sets::Union{Nothing,_LPProductOfSets{Cdouble}} max_sense::Bool + silent::Bool function Optimizer() - return new( - nothing, - false, + params_ref = Ref{Lib.pdhg_parameters_t}() + Lib.set_default_parameters( + Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref), ) + + return new(nothing, params_ref[], nothing, false, false) end end @@ -56,58 +58,79 @@ function MOI.default_cache(::Optimizer, ::Type) end # ==================== -# empty functions +# Helper: Immutable Update # ==================== - -function MOI.is_empty(optimizer::Optimizer) - return isnothing(optimizer.result) +function _update_immutable(obj::T, field::Symbol, value) where {T} + args = map(fieldnames(T)) do f + f == field ? value : getfield(obj, f) + end + return T(args...) end -function MOI.empty!(optimizer::Optimizer) - optimizer.result = nothing - return -end +# ==================== +# Parameters +# ==================== MOI.get(::Optimizer, ::MOI.SolverName) = "cuPDLPx" -# MOI.RawOptimizerAttribute - function MOI.supports(::Optimizer, param::MOI.RawOptimizerAttribute) - error("TODO") - return hasfield(PdhgParameters, Symbol(param.name)) + return hasfield(Lib.pdhg_parameters_t, Symbol(param.name)) end function MOI.set(optimizer::Optimizer, param::MOI.RawOptimizerAttribute, value) - error("TODO") if !MOI.supports(optimizer, param) throw(MOI.UnsupportedAttribute(param)) end - setfield!(optimizer.parameters, Symbol(param.name), value) + optimizer.parameters = + _update_immutable(optimizer.parameters, Symbol(param.name), value) return end function MOI.get(optimizer::Optimizer, param::MOI.RawOptimizerAttribute) - error("TODO") if !MOI.supports(optimizer, param) throw(MOI.UnsupportedAttribute(param)) end return getfield(optimizer.parameters, Symbol(param.name)) end -# MOI.Silent +MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true +function MOI.set(optimizer::Optimizer, ::MOI.TimeLimitSec, value::Real) + current_criteria = optimizer.parameters.termination_criteria + new_criteria = _update_immutable(current_criteria, :time_sec_limit, Float64(value)) + optimizer.parameters = + _update_immutable(optimizer.parameters, :termination_criteria, new_criteria) + return +end -MOI.supports(::Optimizer, ::MOI.Silent) = true +function MOI.get(optimizer::Optimizer, ::MOI.TimeLimitSec) + return optimizer.parameters.termination_criteria.time_sec_limit +end +MOI.supports(::Optimizer, ::MOI.Silent) = true function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool) - error("TODO") optimizer.silent = value + new_verbose = value ? 0 : 1 + optimizer.parameters = _update_immutable(optimizer.parameters, :verbose, new_verbose) return end - MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent +# ==================== +# Empty & Status +# ==================== + +function MOI.is_empty(optimizer::Optimizer) + return isnothing(optimizer.result) +end + +function MOI.empty!(optimizer::Optimizer) + optimizer.result = nothing + optimizer.sets = nothing + return +end + # ======================================== -# Supported constraints and objectives +# Constraints & Objectives # ======================================== function MOI.supports_constraint( @@ -126,7 +149,6 @@ function MOI.supports_constraint( return true end - MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true function MOI.supports( @@ -137,65 +159,80 @@ function MOI.supports( end # =============================== -# Optimize and post-optimize +# Optimize # =============================== function _flip_sense(optimizer::Optimizer, obj) return optimizer.max_sense ? -obj : obj end -function sparse_matrix(A::MOI.Utilities.MutableSparseMatrixCSC{Cdouble,Cint,MOI.Utilities.ZeroBasedIndexing}) - A_csc = LibcuPDLPx.MatrixCSC( +function create_matrix_desc_ref( + A::MOI.Utilities.MutableSparseMatrixCSC{Cdouble,Cint,MOI.Utilities.ZeroBasedIndexing}, +) + A_csc = Lib.MatrixCSC( length(A.rowval), pointer(A.colptr), pointer(A.rowval), - pointer(A.nzval) + pointer(A.nzval), ) - # 1. Allocate zeroed struct on Julia side - A_desc_ref = Ref{LibcuPDLPx.matrix_desc_t}() - A_desc_ref[] = LibcuPDLPx.matrix_desc_t(ntuple(_ -> UInt8(0), 56)) # Clear memory - A_desc_ptr = Base.unsafe_convert(Ptr{LibcuPDLPx.matrix_desc_t}, A_desc_ref) + desc_ref = Ref{Lib.matrix_desc_t}() - # 2. Set Scalar Fields - A_desc_ptr.m = Cint(A.m) - A_desc_ptr.n = Cint(A.n) - A_desc_ptr.fmt = LibcuPDLPx.matrix_csc - A_desc_ptr.zero_tolerance = 0.0 + desc_val = Lib.matrix_desc_t(ntuple(_ -> UInt8(0), 56)) + desc_ref[] = desc_val - # 3. Set The Union Data - A_desc_ptr.data.csc = A_csc + desc_ptr = Base.unsafe_convert(Ptr{Lib.matrix_desc_t}, desc_ref) - return A_desc_ptr + desc_ptr.m = Cint(A.m) + desc_ptr.n = Cint(A.n) + desc_ptr.fmt = Lib.matrix_csc + desc_ptr.zero_tolerance = 1e-12 + desc_ptr.data.csc = A_csc + + return desc_ref end function MOI.optimize!(dest::Optimizer, src::OptimizerCache) MOI.empty!(dest) + if src.constraints.coefficients.n == 0 + dest.result = nothing + return MOI.Utilities.identity_index_map(src), false + end + dest.max_sense = MOI.get(src, MOI.ObjectiveSense()) == MOI.MAX_SENSE obj = MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) + c = zeros(Cdouble, src.constraints.coefficients.n) for term in obj.terms c[term.variable.value] += _flip_sense(dest, term.coefficient) end obj_const = [_flip_sense(dest, MOI.constant(obj))] - prob = LibcuPDLPx.create_lp_problem( + + dest.sets = src.constraints.sets + + matrix_desc_ref = create_matrix_desc_ref(src.constraints.coefficients) + + matrix_desc_ptr = Base.unsafe_convert(Ptr{Lib.matrix_desc_t}, matrix_desc_ref) + + prob = Lib.create_lp_problem( pointer(c), - sparse_matrix(src.constraints.coefficients), + matrix_desc_ptr, pointer(src.constraints.constants.lower), pointer(src.constraints.constants.upper), pointer(src.variables.lower), pointer(src.variables.upper), - pointer(obj_const) + pointer(obj_const), ) @assert prob != C_NULL - params_ref = Ref{Lib.pdhg_parameters_t}() - Lib.set_default_parameters(Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref)) + params_ref = Ref(dest.parameters) params_ptr = Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref) result_ptr = Lib.solve_lp_problem(prob, params_ptr) @assert result_ptr != C_NULL + dest.result = unsafe_load(result_ptr) + return MOI.Utilities.identity_index_map(src), false end @@ -206,29 +243,28 @@ function MOI.optimize!(dest::Optimizer, src::MOI.ModelLike) return index_map, false end +# ==================== +# Result Getters +# ==================== + function MOI.get(optimizer::Optimizer, ::MOI.SolveTimeSec) return optimizer.result.cumulative_time_sec end function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString) - if isnothing(optimizer.result) - return "Optimize not called" - else - error("TODO") - end + return isnothing(optimizer.result) ? "Optimize not called" : "Solver finished" end const _TERMINATION_STATUS_MAP = Dict( - LibcuPDLPx.TERMINATION_REASON_UNSPECIFIED => MOI.OPTIMIZE_NOT_CALLED, - LibcuPDLPx.TERMINATION_REASON_OPTIMAL => MOI.OPTIMAL, - LibcuPDLPx.TERMINATION_REASON_PRIMAL_INFEASIBLE => MOI.INFEASIBLE, - LibcuPDLPx.TERMINATION_REASON_DUAL_INFEASIBLE => MOI.DUAL_INFEASIBLE, - LibcuPDLPx.TERMINATION_REASON_TIME_LIMIT => MOI.TIME_LIMIT, - LibcuPDLPx.TERMINATION_REASON_ITERATION_LIMIT => MOI.ITERATION_LIMIT, - LibcuPDLPx.TERMINATION_REASON_FEAS_POLISH_SUCCESS => MOI.OTHER_ERROR, # TODO + Lib.TERMINATION_REASON_UNSPECIFIED => MOI.OPTIMIZE_NOT_CALLED, + Lib.TERMINATION_REASON_OPTIMAL => MOI.OPTIMAL, + Lib.TERMINATION_REASON_PRIMAL_INFEASIBLE => MOI.INFEASIBLE, + Lib.TERMINATION_REASON_DUAL_INFEASIBLE => MOI.DUAL_INFEASIBLE, + Lib.TERMINATION_REASON_TIME_LIMIT => MOI.TIME_LIMIT, + Lib.TERMINATION_REASON_ITERATION_LIMIT => MOI.ITERATION_LIMIT, + Lib.TERMINATION_REASON_FEAS_POLISH_SUCCESS => MOI.OTHER_ERROR, ) -# Implements getter for result value and statuses function MOI.get(optimizer::Optimizer, ::MOI.TerminationStatus) return isnothing(optimizer.result) ? MOI.OPTIMIZE_NOT_CALLED : _TERMINATION_STATUS_MAP[optimizer.result.termination_reason] @@ -236,19 +272,29 @@ end function MOI.get(optimizer::Optimizer, attr::MOI.ObjectiveValue) MOI.check_result_index_bounds(optimizer, attr) - return _flip_sense(optimizer, optimizer.result.iteration_stats[end].convergence_information[].primal_objective) + return _flip_sense(optimizer, optimizer.result.primal_objective_value) end function MOI.get(optimizer::Optimizer, attr::MOI.DualObjectiveValue) MOI.check_result_index_bounds(optimizer, attr) - return _flip_sense(optimizer, optimizer.result.iteration_stats[end].convergence_information[].dual_objective) + return _flip_sense(optimizer, optimizer.result.dual_objective_value) end const _PRIMAL_STATUS_MAP = Dict( + Lib.TERMINATION_REASON_UNSPECIFIED => MOI.NO_SOLUTION, + Lib.TERMINATION_REASON_OPTIMAL => MOI.FEASIBLE_POINT, + Lib.TERMINATION_REASON_PRIMAL_INFEASIBLE => MOI.NO_SOLUTION, + Lib.TERMINATION_REASON_DUAL_INFEASIBLE => MOI.INFEASIBILITY_CERTIFICATE, + Lib.TERMINATION_REASON_TIME_LIMIT => MOI.UNKNOWN_RESULT_STATUS, + Lib.TERMINATION_REASON_ITERATION_LIMIT => MOI.UNKNOWN_RESULT_STATUS, + Lib.TERMINATION_REASON_FEAS_POLISH_SUCCESS => MOI.UNKNOWN_RESULT_STATUS, +) + +const _DUAL_STATUS_MAP = Dict( LibcuPDLPx.TERMINATION_REASON_UNSPECIFIED => MOI.NO_SOLUTION, LibcuPDLPx.TERMINATION_REASON_OPTIMAL => MOI.FEASIBLE_POINT, - LibcuPDLPx.TERMINATION_REASON_PRIMAL_INFEASIBLE => MOI.NO_SOLUTION, - LibcuPDLPx.TERMINATION_REASON_DUAL_INFEASIBLE => MOI.INFEASIBILITY_CERTIFICATE, + LibcuPDLPx.TERMINATION_REASON_PRIMAL_INFEASIBLE => MOI.INFEASIBILITY_CERTIFICATE, + LibcuPDLPx.TERMINATION_REASON_DUAL_INFEASIBLE => MOI.NO_SOLUTION, LibcuPDLPx.TERMINATION_REASON_TIME_LIMIT => MOI.UNKNOWN_RESULT_STATUS, LibcuPDLPx.TERMINATION_REASON_ITERATION_LIMIT => MOI.UNKNOWN_RESULT_STATUS, LibcuPDLPx.TERMINATION_REASON_FEAS_POLISH_SUCCESS => MOI.UNKNOWN_RESULT_STATUS, @@ -261,25 +307,11 @@ function MOI.get(optimizer::Optimizer, attr::MOI.PrimalStatus) return _PRIMAL_STATUS_MAP[optimizer.result.termination_reason] end -function MOI.get( - optimizer::Optimizer, - attr::MOI.VariablePrimal, - vi::MOI.VariableIndex, -) +function MOI.get(optimizer::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex) MOI.check_result_index_bounds(optimizer, attr) - return optimizer.result.primal_solution[vi.value] + return unsafe_load(optimizer.result.primal_solution, vi.value) end -const _DUAL_STATUS_MAP = Dict( - LibcuPDLPx.TERMINATION_REASON_UNSPECIFIED => MOI.NO_SOLUTION, - LibcuPDLPx.TERMINATION_REASON_OPTIMAL => MOI.FEASIBLE_POINT, - LibcuPDLPx.TERMINATION_REASON_PRIMAL_INFEASIBLE => MOI.INFEASIBILITY_CERTIFICATE, - LibcuPDLPx.TERMINATION_REASON_DUAL_INFEASIBLE => MOI.NO_SOLUTION, - LibcuPDLPx.TERMINATION_REASON_TIME_LIMIT => MOI.UNKNOWN_RESULT_STATUS, - LibcuPDLPx.TERMINATION_REASON_ITERATION_LIMIT => MOI.UNKNOWN_RESULT_STATUS, - LibcuPDLPx.TERMINATION_REASON_FEAS_POLISH_SUCCESS => MOI.UNKNOWN_RESULT_STATUS, -) - function MOI.get(optimizer::Optimizer, attr::MOI.DualStatus) if attr.result_index > MOI.get(optimizer, MOI.ResultCount()) return MOI.NO_SOLUTION @@ -290,25 +322,13 @@ end function MOI.get( optimizer::Optimizer, attr::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}}, + ci::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}}, ) MOI.check_result_index_bounds(optimizer, attr) - return optimizer.result.dual_solution[ci.value] -end - -function MOI.get( - optimizer::Optimizer, - attr::MOI.ConstraintDual, - ci::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64}}, -) - MOI.check_result_index_bounds(optimizer, attr) - return optimizer.result.dual_solution[optimizer.num_equalities + ci.value] + row = only(MOI.Utilities.rows(optimizer.sets, ci)) + return unsafe_load(optimizer.result.dual_solution, row) end function MOI.get(optimizer::Optimizer, ::MOI.ResultCount) - if isnothing(optimizer.result) - return 0 - else - return 1 - end + return isnothing(optimizer.result) ? 0 : 1 end diff --git a/test/runtests.jl b/test/runtests.jl index ab78c07..cefc274 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -73,7 +73,8 @@ const Lib = cuPDLPx.LibcuPDLPx # If alignment/padding is wrong, these will likely be garbage values. # verbose is usually a boolean (0 or 1) - @test (params.verbose == true) || (params.verbose == false) + # @test (params.verbose == true) || (params.verbose == false) + @test params.verbose isa Integer # Time limit should be positive (usually infinity or a large number) @test params.termination_criteria.time_sec_limit > 0 @@ -291,7 +292,7 @@ const Lib = cuPDLPx.LibcuPDLPx A_desc_ptr.m = Cint(m_cons) A_desc_ptr.n = Cint(n_vars) A_desc_ptr.fmt = Lib.matrix_csr - A_desc_ptr.zero_tolerance = 0.0 + A_desc_ptr.zero_tolerance = 1e-12 # 3. Set The Union Data A_desc_ptr.data.csr = A_csr