diff --git a/Manifest.toml b/Manifest.toml index a5f837c..535c841 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.11.5" +julia_version = "1.12.2" manifest_format = "2.0" -project_hash = "50848fc542d76b4c16d74beb234d94121ecda552" +project_hash = "54309def7c908068951b3458be89fae48b84f475" [[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"] @@ -59,9 +59,9 @@ version = "0.19.0" [[deps.Clang_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "TOML", "Zlib_jll", "libLLVM_jll"] -git-tree-sha1 = "ebfc8e89823ec2c85ed9fedabe52db149da4c5ec" +git-tree-sha1 = "f85df021a5fd31ac59ea7126232b2875a848544f" uuid = "0ee61d77-7f21-5576-8119-9fcc46b10100" -version = "16.0.6+5" +version = "18.1.7+4" [[deps.CodecBzip2]] deps = ["Bzip2_jll", "TranscodingStreams"] @@ -94,13 +94,7 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.1+0" - -[[deps.DataStructures]] -deps = ["OrderedCollections"] -git-tree-sha1 = "e357641bb3e0638d353c4b29ea0e40ea644066a6" -uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.19.3" +version = "1.3.0+1" [[deps.Dates]] deps = ["Printf"] @@ -127,7 +121,7 @@ version = "0.9.5" [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" +version = "1.7.0" [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" @@ -185,6 +179,11 @@ version = "1.14.3" [deps.JSON3.weakdeps] ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + [[deps.LazyArtifacts]] deps = ["Artifacts", "Pkg"] uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" @@ -196,24 +195,24 @@ uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" version = "0.6.4" [[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.6.0+0" +version = "8.15.0+0" [[deps.LibGit2]] -deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" version = "1.11.0" [[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.7.2+0" +version = "1.9.0+0" [[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.0+1" +version = "1.11.3+1" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" @@ -222,7 +221,7 @@ version = "1.11.0" [[deps.LinearAlgebra]] deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.11.0" +version = "1.12.0" [[deps.LogExpFunctions]] deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] @@ -250,20 +249,15 @@ uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" version = "0.5.16" [[deps.Markdown]] -deps = ["Base64"] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" version = "1.11.0" [[deps.MathOptInterface]] -deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "DataStructures", "ForwardDiff", "JSON3", "LinearAlgebra", "MutableArithmetics", "NaNMath", "OrderedCollections", "PrecompileTools", "Printf", "SparseArrays", "SpecialFunctions", "Test"] -git-tree-sha1 = "a2cbab4256690aee457d136752c404e001f27768" +deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "ForwardDiff", "JSON3", "LinearAlgebra", "MutableArithmetics", "NaNMath", "OrderedCollections", "PrecompileTools", "Printf", "SparseArrays", "SpecialFunctions", "Test"] +git-tree-sha1 = "181c2611c7aa6a362fdf937b1e2af55e6691181f" uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -version = "1.46.0" - -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.6+0" +version = "1.48.0" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" @@ -271,7 +265,7 @@ version = "1.11.0" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.12.12" +version = "2025.5.20" [[deps.MutableArithmetics]] deps = ["LinearAlgebra", "SparseArrays", "Test"] @@ -287,17 +281,22 @@ version = "1.1.3" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" +version = "1.3.0" [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.27+1" +version = "0.3.29+0" [[deps.OpenLibm_jll]] deps = ["Artifacts", "Libdl"] uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.5+0" +version = "0.8.7+0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.4+0" [[deps.OpenSpecFun_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] @@ -319,7 +318,7 @@ version = "2.8.3" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.11.0" +version = "1.12.0" [deps.Pkg.extensions] REPLExt = "REPL" @@ -329,15 +328,15 @@ version = "1.11.0" [[deps.PrecompileTools]] deps = ["Preferences"] -git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.2.1" +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"] @@ -345,6 +344,7 @@ uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" version = "1.11.0" [[deps.Profile]] +deps = ["StyledStrings"] uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" version = "1.11.0" @@ -364,7 +364,7 @@ version = "1.11.0" [[deps.SparseArrays]] deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.11.0" +version = "1.12.0" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] @@ -413,10 +413,14 @@ version = "2.6.0" Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" + [[deps.SuiteSparse_jll]] deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.7.0+0" +version = "7.8.3+2" [[deps.TOML]] deps = ["Dates"] @@ -450,30 +454,36 @@ version = "1.11.0" [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.13+1" +version = "1.3.1+2" + +[[deps.cuPDLPx]] +deps = ["CUDA_Runtime_jll", "CUDA_SDK_jll", "Clang", "MathOptInterface", "Test", "cuPDLPx_jll"] +path = "." +uuid = "bcd6524d-1420-4b17-a582-359cb8a71a63" +version = "0.2.2" [[deps.cuPDLPx_jll]] deps = ["Artifacts", "CUDA_Runtime_jll", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "TOML", "Zlib_jll"] -git-tree-sha1 = "ba09f738d421a949c9058167743c33275cf58400" +git-tree-sha1 = "84f425232e24b1d8a961c5fc3b3d30e2d9b834ad" uuid = "bca5daad-f4d3-5101-ae12-8b63679c982c" -version = "0.1.4+0" +version = "0.2.2+0" [[deps.libLLVM_jll]] deps = ["Artifacts", "Libdl"] uuid = "8f36deef-c2a5-5394-99ed-8e07531fb29a" -version = "16.0.6+5" +version = "18.1.7+5" [[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.11.0+0" +version = "5.15.0+0" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.59.0+0" +version = "1.64.0+1" [[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+2" +version = "17.7.0+0" diff --git a/Project.toml b/Project.toml index b886991..0b687ae 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.2.2" [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.2.2" diff --git a/README.md b/README.md index 1102b7c..d53cde1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # cuPDLPx.jl Julia interface for cuPDLPx. + +## Use with JuMP + +To use cuPDLPx with JuMP, use `cuPDLPx.Optimizer`: + +```julia +using JuMP, cuPDLPx +model = Model(cuPDLPx.Optimizer) +``` diff --git a/src/LibcuPDLPx.jl b/src/LibcuPDLPx.jl index 30adc03..d481825 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,25 +52,39 @@ 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 + sv_max_iter::Cint + sv_tol::Cdouble termination_criteria::termination_criteria_t restart_params::restart_parameters_t reflection_coefficient::Cdouble - feasibility_polishing::Bool + feasibility_polishing::Cint + presolve::Cint end struct cupdlpx_result_t num_variables::Cint num_constraints::Cint + num_nonzeros::Cint + + num_reduced_variables::Cint + num_reduced_constraints::Cint + num_reduced_nonzeros::Cint + primal_solution::Ptr{Cdouble} dual_solution::Ptr{Cdouble} + reduced_cost::Ptr{Cdouble} + total_count::Cint rescaling_time_sec::Cdouble cumulative_time_sec::Cdouble + presolve_time::Cdouble + presolve_status::Cint + absolute_primal_residual::Cdouble relative_primal_residual::Cdouble absolute_dual_residual::Cdouble diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl new file mode 100644 index 0000000..fa128e8 --- /dev/null +++ b/src/MOI_wrapper.jl @@ -0,0 +1,357 @@ +import MathOptInterface as MOI + +const Lib = cuPDLPx.LibcuPDLPx + +MOI.Utilities.@product_of_sets( + _LPProductOfSets, + MOI.EqualTo{T}, + MOI.GreaterThan{T}, + MOI.LessThan{T}, + MOI.Interval{T}, +) + +const OptimizerCache = MOI.Utilities.GenericModel{ + Cdouble, + MOI.Utilities.ObjectiveContainer{Cdouble}, + MOI.Utilities.VariablesContainer{Cdouble}, + MOI.Utilities.MatrixOfConstraints{ + Cdouble, + MOI.Utilities.MutableSparseMatrixCSC{Cdouble,Cint,MOI.Utilities.ZeroBasedIndexing}, + MOI.Utilities.Hyperrectangle{Cdouble}, + _LPProductOfSets{Cdouble}, + }, +} + +Base.show(io::IO, ::Type{OptimizerCache}) = print(io, "cuPDLPx.OptimizerCache") + +const BOUND_SETS = Union{ + MOI.GreaterThan{Float64}, + MOI.LessThan{Float64}, + MOI.EqualTo{Float64}, + MOI.Interval{Float64}, +} + +""" + Optimizer() + +Create a new cuPDLPx optimizer. +""" +mutable struct Optimizer <: MOI.AbstractOptimizer + result::Union{Nothing,Lib.cupdlpx_result_t} + native_result_ptr::Ptr{Lib.cupdlpx_result_t} + native_problem_ptr::Ptr{Lib.lp_problem_t} + parameters::Lib.pdhg_parameters_t + sets::Union{Nothing,_LPProductOfSets{Cdouble}} + max_sense::Bool + silent::Bool + + function Optimizer() + params_ref = Ref{Lib.pdhg_parameters_t}() + Lib.set_default_parameters( + Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref), + ) + + return new(nothing, C_NULL, C_NULL, params_ref[], nothing, false, false) + end +end + +function MOI.default_cache(::Optimizer, ::Type) + return OptimizerCache() +end + +# ==================== +# Helper: Immutable Update +# ==================== +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 + +# ==================== +# Parameters +# ==================== + +MOI.get(::Optimizer, ::MOI.SolverName) = "cuPDLPx" + +function MOI.supports(::Optimizer, param::MOI.RawOptimizerAttribute) + return hasfield(Lib.pdhg_parameters_t, Symbol(param.name)) +end + +function MOI.set(optimizer::Optimizer, param::MOI.RawOptimizerAttribute, value) + if !MOI.supports(optimizer, param) + throw(MOI.UnsupportedAttribute(param)) + end + optimizer.parameters = + _update_immutable(optimizer.parameters, Symbol(param.name), value) + return +end + +function MOI.get(optimizer::Optimizer, param::MOI.RawOptimizerAttribute) + if !MOI.supports(optimizer, param) + throw(MOI.UnsupportedAttribute(param)) + end + return getfield(optimizer.parameters, Symbol(param.name)) +end + +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 + +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) + optimizer.silent = value + 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) + if optimizer.native_result_ptr != C_NULL + Lib.cupdlpx_result_free(optimizer.native_result_ptr) + optimizer.native_result_ptr = C_NULL + end + if optimizer.native_problem_ptr != C_NULL + Lib.lp_problem_free(optimizer.native_problem_ptr) + optimizer.native_problem_ptr = C_NULL + end + optimizer.result = nothing + optimizer.sets = nothing + optimizer.max_sense = false + return +end + +# ======================================== +# Constraints & Objectives +# ======================================== + +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.VariableIndex}, + ::Type{<:BOUND_SETS}, +) + return true +end + +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.ScalarAffineFunction{Float64}}, + ::Type{<:BOUND_SETS}, +) + return true +end + +MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true + +function MOI.supports( + ::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, +) + return true +end + +# =============================== +# Optimize +# =============================== + +function _flip_sense(optimizer::Optimizer, obj) + return optimizer.max_sense ? -obj : obj +end + +function create_matrix_desc_ref( + A::MOI.Utilities.MutableSparseMatrixCSC{Cdouble,Cint,MOI.Utilities.ZeroBasedIndexing}, +) + colptr_ptr = isempty(A.colptr) ? C_NULL : pointer(A.colptr) + rowval_ptr = isempty(A.rowval) ? C_NULL : pointer(A.rowval) + nzval_ptr = isempty(A.nzval) ? C_NULL : pointer(A.nzval) + A_csc = Lib.MatrixCSC( + length(A.rowval), + colptr_ptr, + rowval_ptr, + nzval_ptr, + ) + + desc_ref = Ref{Lib.matrix_desc_t}() + + desc_val = Lib.matrix_desc_t(ntuple(_ -> UInt8(0), 56)) + desc_ref[] = desc_val + + desc_ptr = Base.unsafe_convert(Ptr{Lib.matrix_desc_t}, desc_ref) + + 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) + dest.max_sense = MOI.get(src, MOI.ObjectiveSense()) == MOI.MAX_SENSE + if src.constraints.coefficients.n == 0 + dest.result = nothing + return MOI.Utilities.identity_index_map(src), false + end + + 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))] + + 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) + + # TODO: not working + # solve_params = dest.parameters + # if dest.silent + # solve_params = _update_immutable(solve_params, :verbose, Cint(0)) + # end + # params_ref = Ref{Lib.pdhg_parameters_t}(solve_params) + # params_ptr = 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) + + GC.@preserve params_ref begin + prob = Lib.create_lp_problem( + pointer(c), + matrix_desc_ptr, + pointer(src.constraints.constants.lower), + pointer(src.constraints.constants.upper), + pointer(src.variables.lower), + pointer(src.variables.upper), + pointer(obj_const), + ) + @assert prob != C_NULL + + result_ptr = Lib.solve_lp_problem(prob, params_ptr) + @assert result_ptr != C_NULL + + dest.result = unsafe_load(result_ptr) + dest.native_result_ptr = result_ptr + Lib.lp_problem_free(prob) + end + + return MOI.Utilities.identity_index_map(src), false +end + +function MOI.optimize!(dest::Optimizer, src::MOI.ModelLike) + cache = OptimizerCache() + index_map = MOI.copy_to(cache, src) + MOI.optimize!(dest, cache) + 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) + return isnothing(optimizer.result) ? "Optimize not called" : "Solver finished" +end + +const _TERMINATION_STATUS_MAP = Dict( + 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, +) + +function MOI.get(optimizer::Optimizer, ::MOI.TerminationStatus) + return isnothing(optimizer.result) ? MOI.OPTIMIZE_NOT_CALLED : + _TERMINATION_STATUS_MAP[optimizer.result.termination_reason] +end + +function MOI.get(optimizer::Optimizer, attr::MOI.ObjectiveValue) + MOI.check_result_index_bounds(optimizer, attr) + 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.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.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.PrimalStatus) + if attr.result_index > MOI.get(optimizer, MOI.ResultCount()) + return MOI.NO_SOLUTION + end + return _PRIMAL_STATUS_MAP[optimizer.result.termination_reason] +end + +function MOI.get(optimizer::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex) + MOI.check_result_index_bounds(optimizer, attr) + return unsafe_load(optimizer.result.primal_solution, vi.value) +end + +function MOI.get(optimizer::Optimizer, attr::MOI.DualStatus) + if attr.result_index > MOI.get(optimizer, MOI.ResultCount()) + return MOI.NO_SOLUTION + end + return _DUAL_STATUS_MAP[optimizer.result.termination_reason] +end + +function MOI.get( + optimizer::Optimizer, + attr::MOI.ConstraintDual, + ci::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}}, +) + MOI.check_result_index_bounds(optimizer, attr) + row = only(MOI.Utilities.rows(optimizer.sets, ci)) + return unsafe_load(optimizer.result.dual_solution, row) +end + +function MOI.get(optimizer::Optimizer, ::MOI.ResultCount) + return isnothing(optimizer.result) ? 0 : 1 +end diff --git a/src/cuPDLPx.jl b/src/cuPDLPx.jl index cdad5a1..7154a79 100644 --- a/src/cuPDLPx.jl +++ b/src/cuPDLPx.jl @@ -2,5 +2,6 @@ module cuPDLPx # using Base.Enums include("LibcuPDLPx.jl") +include("MOI_wrapper.jl") -end # module cuPDLPx \ No newline at end of file +end # module cuPDLPx diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl new file mode 100644 index 0000000..456903c --- /dev/null +++ b/test/MOI_wrapper.jl @@ -0,0 +1,89 @@ +module TestMOI + +using Test +import MathOptInterface as MOI +import cuPDLPx + +function test_runtests() + optimizer = cuPDLPx.Optimizer() + MOI.set(optimizer, MOI.Silent(), true) # comment this to enable output + model = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + optimizer, + ), + Float64, + ) + config = MOI.Test.Config( + rtol = 1e-1, + atol = 1e-1, + exclude = Any[ + MOI.ConstraintBasisStatus, + MOI.VariableBasisStatus, + MOI.ConstraintName, + MOI.VariableName, + MOI.ObjectiveBound, + MOI.SolverVersion, + ], + ) + MOI.Test.runtests( + model, + config, + exclude = [ + # No constraint so cuPDLP fails with the CUDA error + # `Grid dimensions should be non-null` + r"^test_variable_solve_with_lowerbound$", + r"^test_variable_solve_with_upperbound$", + r"^test_solve_result_index$", + r"^test_solve_TerminationStatus_DUAL_INFEASIBLE$", + r"^test_modification_transform_singlevariable_lessthan$", + r"^test_modification_const_scalar_objective$", + r"^test_DualObjectiveValue_Max_VariableIndex_LessThan$", + r"^test_DualObjectiveValue_Min_VariableIndex_GreaterThan$", + r"^test_attribute_RawStatusString$", + r"^test_attribute_SolveTimeSec$", + r"^test_solve_optimize_twice$", + r"^test_solve_VariableIndex_ConstraintDual_MAX_SENSE$", + r"^test_solve_VariableIndex_ConstraintDual_MIN_SENSE$", + r"^test_modification_set_singlevariable_lessthan$", + r"^test_objective_ObjectiveFunction_VariableIndex$", + r"^test_objective_ObjectiveFunction_blank$", + r"^test_objective_ObjectiveFunction_constant$", + r"^test_objective_ObjectiveFunction_duplicate_terms$", + r"^test_objective_FEASIBILITY_SENSE_clears_objective$", + r"^test_modification_coef_scalar_objective$", + r"^test_linear_variable_open_intervals$", + r"^test_modification_delete_variable_with_single_variable_obj$", + r"^test_modification_delete_variables_in_a_batch$", + # Not all constraints have finite bounds on at least one side. + r"^test_linear_open_intervals$", + # Error from MOI fallback : Fallback getter for variable constraint dual does not support other variable-wise constraints on the variable. + r"^test_linear_integration_delete_variables$", + # Expression: isapprox(target, obj, config) + # Evaluated: isapprox(-41.980944701810785, -37.700944701810776, ...) + r"^test_infeasible_affine_MAX_SENSE_offset$", + # Expression: isapprox(-target, obj, config) + # Evaluated: isapprox(14.020439047717783, 17.100439047717785, ...) + r"^test_infeasible_affine_MIN_SENSE$", + # Expression: isapprox(-target, obj, config) + # Evaluated: isapprox(14.020439047717783, 18.300439047717788, ...) + r"^test_infeasible_affine_MIN_SENSE_offset$", + ] + ) + return +end + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +end # module + +TestMOI.runtests() diff --git a/test/runtests.jl b/test/runtests.jl index 8d01c18..e94514e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,104 +7,55 @@ const Lib = cuPDLPx.LibcuPDLPx # ========================================== # 1. Basic Enum Mapping Tests - # Verify that Julia Enum values match C header definitions # ========================================== @testset "Enum Mapping" begin - # Check termination_reason_t @test Int(Lib.TERMINATION_REASON_OPTIMAL) == 1 @test Int(Lib.TERMINATION_REASON_TIME_LIMIT) == 4 - # Check matrix_format_t @test Int(Lib.matrix_dense) == 0 @test Int(Lib.matrix_csr) == 1 @test Int(Lib.matrix_csc) == 2 end # ========================================== - # 2. Union Memory Layout Tests (MatrixData) - # Verify if the generated code handles memory addresses correctly. - # The 'x + 0' logic should map the specific struct pointers to the same address as the raw data. + # 2. Union Memory Layout Tests # ========================================== @testset "Union Memory Layout (MatrixData)" begin - # MatrixData is defined as a 32-byte blob @test sizeof(Lib.MatrixData) == 32 - - # Simulate creating a zero-initialized Union data_blob = Lib.MatrixData(ntuple(_ -> UInt8(0), 32)) data_ref = Ref(data_blob) data_ptr = Base.unsafe_convert(Ptr{Lib.MatrixData}, data_ref) - - # Test the .csr / .dense property accessors - # Logic: The converted pointer address must match the original pointer address - # because in a Union, all members start at offset 0. - ptr_csr = data_ptr.csr - ptr_dense = data_ptr.dense - - @test ptr_csr isa Ptr{Lib.MatrixCSR} - @test ptr_dense isa Ptr{Lib.MatrixDense} - - # Critical Check: Verify memory addresses are identical - @test Int(data_ptr) == Int(ptr_csr) - @test Int(data_ptr) == Int(ptr_dense) - println(" > MatrixData Union logic verified (Address offsets are correct).") + @test data_ptr.csr isa Ptr{Lib.MatrixCSR} + @test Int(data_ptr) == Int(data_ptr.csr) + println(" > MatrixData Union logic verified.") end # ========================================== - # 3. Real C Function Call Test (Core Verification) + # 3. C Function Call Test # ========================================== @testset "C Function Call: set_default_parameters" begin - # 1. Allocate memory on the Julia side - # Create an uninitialized pdhg_parameters_t struct on the heap params_ref = Ref{Lib.pdhg_parameters_t}() - - # Get the raw pointer params_ptr = Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref) - - # 2. Call the C function - # This function writes default values into the memory pointed to by params_ptr. Lib.set_default_parameters(params_ptr) - - # 3. Read and verify results - # Retrieve the modified struct from the Ref params = params_ref[] - - # Verify sanity of the loaded values. - # 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) - # Time limit should be positive (usually infinity or a large number) + @test params.verbose isa Integer @test params.termination_criteria.time_sec_limit > 0 - - # Check nested struct alignment - # eps_optimal_relative should be a small positive number (e.g., 1e-6) - @test params.termination_criteria.eps_optimal_relative < 1.0 - @test params.termination_criteria.eps_optimal_relative >= 0.0 - - # Check restart parameters - @test params.restart_params.k_p isa Float64 - - println(" > Successfully called set_default_parameters from C library.") - println(" > Defaults loaded check: Time Limit = $(params.termination_criteria.time_sec_limit)") + println(" > Successfully called set_default_parameters.") end # ========================================== - # 4. Struct Size Sanity Check - # Ensure Julia's matrix_desc_t matches the C definition padding + # 4. Struct Size Check # ========================================== @testset "Struct Size Sanity Check" begin - # matrix_desc_t is defined as NTuple{56, UInt8} in the generated code @test sizeof(Lib.matrix_desc_t) == 56 end # ========================================== - # 6. Integration Test: Read MPS from file + # 6. Integration Test: MPS File # ========================================== @testset "MPS File Solve (AFIRO)" begin - # 1. Create a temporary MPS file - # AFIRO is a standard small Netlib LP test case (27 rows, 32 cols) mps_content = """ NAME AFIRO ROWS @@ -194,20 +145,13 @@ const Lib = cuPDLPx.LibcuPDLPx write(mps_path, mps_content) println(" > Created temp MPS file at: $mps_path") - # 2. Read MPS using the C library - # Note: read_mps_file returns a Ptr{lp_problem_t} prob = Lib.read_mps_file(mps_path) - @test prob != C_NULL - # Verify problem dimensions (AFIRO should be small) prob_data = unsafe_load(prob) println(" > Loaded Problem: $(prob_data.num_variables) vars, $(prob_data.num_constraints) cons") - # AFIRO stats: 32 variables, 27 constraints (values depend on presolve/parsing, but checking >0 is good) @test prob_data.num_variables > 0 - @test prob_data.num_constraints > 0 - # 3. Solve params_ref = Ref{Lib.pdhg_parameters_t}() Lib.set_default_parameters(Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref)) params_ptr = Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref) @@ -219,14 +163,12 @@ const Lib = cuPDLPx.LibcuPDLPx println(" > MPS Solve Status: $(result.termination_reason)") println(" > MPS Primal Obj: $(result.primal_objective_value)") - # AFIRO optimal objective is approx -4.6475314286e+02 target_obj = -464.75 @test isapprox(result.primal_objective_value, target_obj, rtol=1e-2) - # 4. Cleanup Lib.cupdlpx_result_free(result_ptr) Lib.lp_problem_free(prob) - rm(mps_path) # delete temp file + rm(mps_path) end # ========================================== @@ -235,44 +177,28 @@ const Lib = cuPDLPx.LibcuPDLPx @testset "Direct API Solve" begin println("\n > Starting Direct API Solve Test...") - # ------------------------------------------------------- - # Problem Data Definition - # maximize - # x + y + 2 z - # subject to - # x + 2 y + 3 z <= 4 - # x + y >= 1 - # 0 <= x, y, z <= 1 - # ------------------------------------------------------- - n_vars = 3 - m_cons = 2 - - # 1. Objective Vector (Minimize direction) + # 1. Objective Vector c = Cdouble[-1.0, -1.0, -2.0] - obj_const = Cdouble[0.0] # Must be a pointer + obj_const = Cdouble[0.0] - # 2. Variable Bounds (Relax Binary -> [0, 1]) + # 2. Variable Bounds var_lb = Cdouble[0.0, 0.0, 0.0] var_ub = Cdouble[1.0, 1.0, 1.0] - # 3. Constraint Matrix Data (CSR Format) - # Row 0: 1.0, 2.0, 3.0 (indices 0, 1, 2) - # Row 1: -1.0, -1.0 (indices 0, 1) + # 3. Constraint Matrix Data csr_vals = Cdouble[1.0, 2.0, 3.0, -1.0, -1.0] csr_col_ind = Cint[0, 1, 2, 0, 1] csr_row_ptr = Cint[0, 3, 5] nnz = Cint(5) # 4. Constraint Bounds - # Row 0: ... <= 4.0 -> (-Inf, 4.0] - # Row 1: ... <= -1.0 -> (-Inf, -1.0] con_lb = Cdouble[-Inf, -Inf] con_ub = Cdouble[4.0, -1.0] - # ------------------------------------------------------- - # C Struct Construction - # ------------------------------------------------------- - + # 5. Refs + A_desc_ref = Ref{Lib.matrix_desc_t}() + params_ref = Ref{Lib.pdhg_parameters_t}() + # A. Construct MatrixCSR A_csr = Lib.MatrixCSR( nnz, @@ -282,24 +208,15 @@ const Lib = cuPDLPx.LibcuPDLPx ) # B. Construct matrix_desc_t - # 1. Allocate zeroed struct on Julia side - A_desc_ref = Ref{Lib.matrix_desc_t}() - A_desc_ref[] = Lib.matrix_desc_t(ntuple(_ -> UInt8(0), 56)) # Clear memory + A_desc_ref[] = Lib.matrix_desc_t(ntuple(_ -> UInt8(0), 56)) A_desc_ptr = Base.unsafe_convert(Ptr{Lib.matrix_desc_t}, A_desc_ref) - # 2. Set Scalar Fields - 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.m = Cint(2) # m_cons = 2 + A_desc_ptr.n = Cint(3) # n_vars = 3 - # 3. Set The Union Data + A_desc_ptr.fmt = Lib.matrix_csr + A_desc_ptr.zero_tolerance = 1e-12 A_desc_ptr.data.csr = A_csr - - # ------------------------------------------------------- - # Create and Solve - # ------------------------------------------------------- - # Create LP Problem prob = Lib.create_lp_problem( pointer(c), @@ -313,9 +230,8 @@ const Lib = cuPDLPx.LibcuPDLPx @test prob != C_NULL # Setup Parameters - params_ref = Ref{Lib.pdhg_parameters_t}() + Lib.set_default_parameters(Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref)) params_ptr = Base.unsafe_convert(Ptr{Lib.pdhg_parameters_t}, params_ref) - Lib.set_default_parameters(params_ptr) # Solve result_ptr = Lib.solve_lp_problem(prob, params_ptr) @@ -323,21 +239,16 @@ const Lib = cuPDLPx.LibcuPDLPx result = unsafe_load(result_ptr) - # ------------------------------------------------------- - # Validation - # ------------------------------------------------------- println(" > Direct Solve Status: $(result.termination_reason)") println(" > Direct Solve Obj: $(result.primal_objective_value)") - - # Optimal happens at x=1, y=0, z=1 => Obj = 3. - # Since we minimized negative: Target = -3.0 @test result.termination_reason == Lib.TERMINATION_REASON_OPTIMAL @test isapprox(result.primal_objective_value, -3.0, atol=1e-4) - # Cleanup Lib.cupdlpx_result_free(result_ptr) Lib.lp_problem_free(prob) println(" > Direct API Solve test passed.") end -end \ No newline at end of file +end + +include("MOI_wrapper.jl")