Schur design constraints and sparse Schur complement solver - #623
Open
michel2323 wants to merge 31 commits into
Open
Schur design constraints and sparse Schur complement solver#623michel2323 wants to merge 31 commits into
michel2323 wants to merge 31 commits into
Conversation
Contributor
|
Your PR requires formatting changes to meet the project's style guidelines. Click here to view the suggested changes.diff --git a/lib/MadNLPGPU/ext/MadNLPGPUAMDGPUExt/rocm.jl b/lib/MadNLPGPU/ext/MadNLPGPUAMDGPUExt/rocm.jl
index 3375891..ecd2ea8 100644
--- a/lib/MadNLPGPU/ext/MadNLPGPUAMDGPUExt/rocm.jl
+++ b/lib/MadNLPGPU/ext/MadNLPGPUAMDGPUExt/rocm.jl
@@ -9,7 +9,7 @@ function MadNLP.MadNLPOptions{T}(
kkt_system = dense_callback ? MadNLP.DenseCondensedKKTSystem : MadNLP.SparseCondensedKKTSystem,
linear_solver = MadNLPGPU.LapackROCmSolver,
tol = MadNLP.get_tolerance(T,kkt_system),
- bound_relax_factor = (kkt_system <: MadNLP.SparseCondensedKKTSystem) ? tol : T(1.0e-8),
+ bound_relax_factor = (kkt_system <: MadNLP.SparseCondensedKKTSystem) ? tol : T(1.0e-8),
) where {T, VT <: ROCVector{T}}
return MadNLP.MadNLPOptions{T}(
tol = tol,
@@ -28,9 +28,9 @@ function MadNLP.create_kkt_system(
) where {T, VT <: ROCVector{T}}
return error(
"SchurComplementCondensedKKTSystem is not supported on ROCm/AMDGPU: there is no ROCm " *
- "Schur implementation (the batched per-scenario factorization and Schur reduction are " *
- "cuDSS / CUBLAS-specific). Use a CUDA device for the GPU Schur path, or solve the Schur " *
- "system on CPU."
+ "Schur implementation (the batched per-scenario factorization and Schur reduction are " *
+ "cuDSS / CUBLAS-specific). Use a CUDA device for the GPU Schur path, or solve the Schur " *
+ "system on CPU."
)
end
diff --git a/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cuda_schur.jl b/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cuda_schur.jl
index 75a06b4..9c1c364 100644
--- a/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cuda_schur.jl
+++ b/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cuda_schur.jl
@@ -250,14 +250,18 @@ function _compute_det_scatter(kkt::GPUSchurComplementCondensedKKTSystem)
n_iA = kkt.n_per_s_ineq_Akk
nz = Array(kkt.gpu_ineq_Akk_nzpos)
slotA = n_iA > 0 ? Int[((i - 1) ÷ n_iA) * nnzb + nz[i] for i in eachindex(nz)] : Int[]
- Akk = _segment_scatter(slotA,
- Array(kkt.gpu_ineq_Akk_jcoo1), Array(kkt.gpu_ineq_Akk_jcoo2), Array(kkt.gpu_ineq_Akk_bufidx))
+ Akk = _segment_scatter(
+ slotA,
+ Array(kkt.gpu_ineq_Akk_jcoo1), Array(kkt.gpu_ineq_Akk_jcoo2), Array(kkt.gpu_ineq_Akk_bufidx)
+ )
# ineq → C_dk: linear slot into (blk × m × ns) = v + (d-1)*blk + (k-1)*blk*m
n_iC = kkt.n_per_s_ineq_Cdk
vv = Array(kkt.gpu_ineq_Cdk_col); dd = Array(kkt.gpu_ineq_Cdk_row)
slotC = n_iC > 0 ? Int[vv[i] + (dd[i] - 1) * blk + ((i - 1) ÷ n_iC) * blk * m for i in eachindex(vv)] : Int[]
- Cdk = _segment_scatter(slotC,
- Array(kkt.gpu_ineq_Cdk_jcoo_d), Array(kkt.gpu_ineq_Cdk_jcoo_v), Array(kkt.gpu_ineq_Cdk_bufidx))
+ Cdk = _segment_scatter(
+ slotC,
+ Array(kkt.gpu_ineq_Cdk_jcoo_d), Array(kkt.gpu_ineq_Cdk_jcoo_v), Array(kkt.gpu_ineq_Cdk_bufidx)
+ )
# ineq → S: merge scenario + design-only contributions, slot = nzpos
slotS = vcat(Array(kkt.schur_ineq_S_nzpos), Array(kkt.schur_design_ineq_S_nzpos))
j1S = vcat(Array(kkt.schur_ineq_S_jcoo1), Array(kkt.schur_design_ineq_S_jcoo1))
@@ -305,9 +309,11 @@ function _compute_det_scatter(kkt::GPUSchurComplementCondensedKKTSystem)
njac = length(kkt.jac)
jt_lin = _segment_lin_scatter(Array(kkt.jt_csc_map), collect(1:njac))
- return (Akk = Akk, Cdk = Cdk, S = S, hessAkk = hessAkk, hessCdk = hessCdk, hessS = hessS,
- hess_lin = hess_lin, hess_full = hess_full, hess_full_perm = hess_full_perm,
- jt_lin = jt_lin)
+ return (
+ Akk = Akk, Cdk = Cdk, S = S, hessAkk = hessAkk, hessCdk = hessCdk, hessS = hessS,
+ hess_lin = hess_lin, hess_full = hess_full, hess_full_perm = hess_full_perm,
+ jt_lin = jt_lin,
+ )
end
function _get_det_scatter(kkt::GPUSchurComplementCondensedKKTSystem)
@@ -317,7 +323,7 @@ end
# --- Dispatch: GPU path when callback uses CuVector ---
function MadNLP.create_kkt_system(
- ::Type{MadNLP.SchurComplementCondensedKKTSystem},
+ ::Type{MadNLP.SchurComplementCondensedKKTSystem},
cb::MadNLP.SparseCallback{T, VT},
linear_solver::Type;
opt_linear_solver=MadNLP.default_options(linear_solver),
@@ -479,7 +485,7 @@ function MadNLP.create_kkt_system(
gpu_hess_Cdk_row = CuVector{Int}(coupled_inv[flat.all_hess_Cdk_row])
gpu_hess_Cdk_col = CuVector{Int}(flat.all_hess_Cdk_col)
gpu_pr_diag_global = CuVector{Int}(flat.all_pr_diag_global)
- gpu_pr_diag_nzpos = CuVector{Int}(flat.all_pr_diag_nzpos)
+ gpu_pr_diag_nzpos = CuVector{Int}(flat.all_pr_diag_nzpos)
gpu_ineq_Akk_nzpos = CuVector{Int}(flat.all_ineq_Akk_nzpos)
gpu_ineq_Akk_jcoo1 = CuVector{Int}(flat.all_ineq_Akk_jcoo1)
gpu_ineq_Akk_jcoo2 = CuVector{Int}(flat.all_ineq_Akk_jcoo2)
@@ -620,7 +626,7 @@ end
# iteration. Passing `det` to a specialized inner method makes every `det.X` access (and the
# kernel launches taking them) type-stable after a single dynamic dispatch at the call.
MadNLP.build_kkt!(kkt::GPUSchurComplementCondensedKKTSystem) = _schur_build_kkt!(kkt, _get_det_scatter(kkt))
-function _schur_build_kkt!(kkt::GPUSchurComplementCondensedKKTSystem{T}, det) where T
+function _schur_build_kkt!(kkt::GPUSchurComplementCondensedKKTSystem{T}, det) where {T}
ns = kkt.ns
nv = kkt.nv
nd = kkt.nd
@@ -767,7 +773,7 @@ end
# --- solve_kkt! ---
function MadNLP.solve_kkt!(
- kkt::GPUSchurComplementCondensedKKTSystem{T},
+ kkt::GPUSchurComplementCondensedKKTSystem{T},
w::MadNLP.AbstractKKTVector{T},
) where T
@@ -866,20 +872,20 @@ end
# hit many times per Richardson step) out of the hot body so the SpMV dispatches statically.
function MadNLP.mul!(
w::MadNLP.AbstractKKTVector{T, VT},
- kkt::GPUSchurComplementCondensedKKTSystem{T},
+ kkt::GPUSchurComplementCondensedKKTSystem{T},
x::MadNLP.AbstractKKTVector,
alpha = one(T),
beta = zero(T),
-) where {T, VT <: CuVector{T}}
+ ) where {T, VT <: CuVector{T}}
return _schur_mul!(w, kkt, x, alpha, beta, _get_det_scatter(kkt).hess_full)
end
function _schur_mul!(
- w::MadNLP.AbstractKKTVector{T, VT},
- kkt::GPUSchurComplementCondensedKKTSystem{T},
- x::MadNLP.AbstractKKTVector,
- alpha,
- beta,
- hess_full,
+ w::MadNLP.AbstractKKTVector{T, VT},
+ kkt::GPUSchurComplementCondensedKKTSystem{T},
+ x::MadNLP.AbstractKKTVector,
+ alpha,
+ beta,
+ hess_full,
) where {T, VT <: CuVector{T}}
n = MadNLP.num_variables(kkt)
diff --git a/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cudss.jl b/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cudss.jl
index b2810dc..247f972 100644
--- a/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cudss.jl
+++ b/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/cudss.jl
@@ -4,7 +4,7 @@ MadNLP.@kwdef mutable struct CudssSolverOptions <: MadNLP.AbstractOptions
cudss_ordering::ORDERING = DEFAULT_ORDERING
cudss_perm::Vector{Cint} = Cint[]
cudss_ir::Int = 0
- cudss_ir_tol::Float64 = 1e-8
+ cudss_ir_tol::Float64 = 1.0e-8
cudss_pivot_threshold::Float64 = 0.0
cudss_pivot_epsilon::Float64 = 0.0
cudss_matching_alg::String = "default"
diff --git a/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/kernels_schur.jl b/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/kernels_schur.jl
index 85d5651..a5578b0 100644
--- a/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/kernels_schur.jl
+++ b/lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/kernels_schur.jl
@@ -30,11 +30,11 @@ end
# `scen_var_global` (flat, length ns*nv, scenario-major), since a scenario's
# variables need not be contiguous.
@kernel function _extract_rhs_kernel!(
- rhs_k,
- @Const(wx),
- @Const(scen_var_global),
- @Const(nv),
- @Const(ns),
+ rhs_k,
+ @Const(wx),
+ @Const(scen_var_global),
+ @Const(nv),
+ @Const(ns),
)
i = @index(Global)
k = (i - 1) ÷ nv + 1
@@ -47,11 +47,11 @@ end
# Write back per-scenario solution to global wx
@kernel function _writeback_rhs_kernel!(
- wx,
- @Const(rhs_k),
- @Const(scen_var_global),
- @Const(nv),
- @Const(ns),
+ wx,
+ @Const(rhs_k),
+ @Const(scen_var_global),
+ @Const(nv),
+ @Const(ns),
)
i = @index(Global)
k = (i - 1) ÷ nv + 1
diff --git a/src/IPM/options.jl b/src/IPM/options.jl
index fddd006..8494544 100644
--- a/src/IPM/options.jl
+++ b/src/IPM/options.jl
@@ -214,14 +214,14 @@ function MadNLPOptions{T}(
callback = dense_callback ? DenseCallback : SparseCallback,
kkt_system = dense_callback ? DenseCondensedKKTSystem : SparseKKTSystem,
linear_solver = dense_callback ? LapackCPUSolver : default_sparse_solver(nlp),
- tol = get_tolerance(T,kkt_system),
- # Condensed KKT systems (Sparse + Schur) relax equalities into the barrier — the SAME
- # systems selected for RelaxEquality below. Flooring those slacks at a tiny 1e-8 box blows up
- # the condensation weights z/s and ruins the conditioning of the (SPD) factorization, so relax
- # by `tol` there (feasible to ~tol, but well-conditioned). Non-condensed systems keep equalities
- # exact and use the tight 1e-8. Must match the CUDA / ROCm extension constructors so a problem
- # behaves identically on CPU and GPU.
- bound_relax_factor = (kkt_system <: CondensedKKTSystems) ? tol : T(1.0e-8),
+ tol = get_tolerance(T, kkt_system),
+ # Condensed KKT systems (Sparse + Schur) relax equalities into the barrier — the SAME
+ # systems selected for RelaxEquality below. Flooring those slacks at a tiny 1e-8 box blows up
+ # the condensation weights z/s and ruins the conditioning of the (SPD) factorization, so relax
+ # by `tol` there (feasible to ~tol, but well-conditioned). Non-condensed systems keep equalities
+ # exact and use the tight 1e-8. Must match the CUDA / ROCm extension constructors so a problem
+ # behaves identically on CPU and GPU.
+ bound_relax_factor = (kkt_system <: CondensedKKTSystems) ? tol : T(1.0e-8),
) where {T}
return MadNLPOptions{T}(
tol = tol,
@@ -234,7 +234,7 @@ end
get_tolerance(::Type{T},::Type{KKT}) where {T, KKT} = 10^round(log10(eps(T))/2)
get_tolerance(::Type{T},::Type{SparseCondensedKKTSystem}) where T = 10^(round(log10(eps(T))/4))
-get_tolerance(::Type{T},::Type{SchurComplementCondensedKKTSystem}) where T = 10^(round(log10(eps(T))/4))
+get_tolerance(::Type{T}, ::Type{SchurComplementCondensedKKTSystem}) where {T} = 10^(round(log10(eps(T)) / 4))
default_sparse_solver(nlp::AbstractNLPModel) = MumpsSolver
diff --git a/src/KKT/Schur/schur.jl b/src/KKT/Schur/schur.jl
index b93c2e0..32d5f97 100644
--- a/src/KKT/Schur/schur.jl
+++ b/src/KKT/Schur/schur.jl
@@ -107,7 +107,7 @@ struct SchurComplementCondensedKKTSystem{
# Per-scenario sparse augmented blocks (lower triangle only)
A_kk::Vector{SparseMatrixCSC{T, Int32}}
C_dk::Vector{MT} # ns × (m × blk_size) — dense, reduced to the m COUPLED
- # design rows (design vars that never couple are exactly zero)
+ # design rows (design vars that never couple are exactly zero)
# Sparse first-stage Schur complement (lower-triangular CSC, size nd × nd): the
# design block S_dd plus the condensed design inequalities. SPD. Only `m` design
@@ -395,11 +395,11 @@ function _build_schur_symbolic(
# and SPD — there is no equality saddle / bordered block.
isempty(ind_eq) || error(
"SchurComplementCondensedKKTSystem is RelaxEquality-only, but got $(length(ind_eq)) " *
- "constraint(s) kept as equalities. The bordered EnforceEquality saddle that the old " *
- "`SchurComplementKKTSystem` used was removed: the first-stage Schur complement is now SPD " *
- "and requires every constraint to be relaxed into the barrier. Pass " *
- "`equality_treatment=MadNLP.RelaxEquality` (the default when " *
- "`kkt_system=SchurComplementCondensedKKTSystem`) — do not override it with `EnforceEquality`."
+ "constraint(s) kept as equalities. The bordered EnforceEquality saddle that the old " *
+ "`SchurComplementKKTSystem` used was removed: the first-stage Schur complement is now SPD " *
+ "and requires every constraint to be relaxed into the barrier. Pass " *
+ "`equality_treatment=MadNLP.RelaxEquality` (the default when " *
+ "`kkt_system=SchurComplementCondensedKKTSystem`) — do not override it with `EnforceEquality`."
)
ind_ineq_set = Set(Int.(ind_ineq))
@@ -425,7 +425,7 @@ function _build_schur_symbolic(
if n_in_k != nc_ineq_per_s
error(
"SchurComplementCondensedKKTSystem requires uniform per-scenario constraint counts. " *
- "Scenario 1 has ineq=$nc_ineq_per_s; scenario $k has ineq=$n_in_k."
+ "Scenario 1 has ineq=$nc_ineq_per_s; scenario $k has ineq=$n_in_k."
)
end
end
@@ -921,7 +921,7 @@ function _flatten_block_maps(block_maps::Vector{ScenarioBlockMap})
bm1 = block_maps[1]
n_per_s_hess_Akk = length(bm1.hess_Akk_coo)
n_per_s_hess_Cdk = length(bm1.hess_Cdk_coo)
- n_per_s_pr_diag = length(bm1.pr_diag_global)
+ n_per_s_pr_diag = length(bm1.pr_diag_global)
n_per_s_ineq_Akk = length(bm1.ineq_Akk_nzpos)
n_per_s_ineq_Cdk = length(bm1.ineq_Cdk_row)
n_per_s_ineq_S = length(bm1.ineq_S_row)
@@ -965,7 +965,7 @@ function _flatten_block_maps(block_maps::Vector{ScenarioBlockMap})
end
function create_kkt_system(
- ::Type{SchurComplementCondensedKKTSystem},
+ ::Type{SchurComplementCondensedKKTSystem},
cb::SparseCallback{T,VT},
linear_solver::Type;
opt_linear_solver=default_options(linear_solver),
@@ -978,9 +978,9 @@ function create_kkt_system(
schur_var_scen = nothing,
schur_con_scen = nothing,
schur_scenario_linear_solver::Type=MumpsSolver,
- schur_scenario_opt_linear_solver=default_options(schur_scenario_linear_solver),
- schur_opt_linear_solver=nothing,
- kwargs...,
+ schur_scenario_opt_linear_solver = default_options(schur_scenario_linear_solver),
+ schur_opt_linear_solver = nothing,
+ kwargs...,
) where {T, VT}
isempty(kwargs) || Base.@warn(
@@ -1047,14 +1047,14 @@ function create_kkt_system(
# (nd ≫ m) tractable on CPU.
block_maps = [
let bm = sym.block_maps[k]
- ScenarioBlockMap(
- bm.hess_Akk_coo, bm.hess_Akk_nzpos,
- bm.hess_Cdk_coo, coupled_inv[bm.hess_Cdk_row], bm.hess_Cdk_col,
- bm.ineq_Akk_nzpos, bm.ineq_Akk_jcoo1, bm.ineq_Akk_jcoo2, bm.ineq_Akk_bufidx,
- coupled_inv[bm.ineq_Cdk_row], bm.ineq_Cdk_col, bm.ineq_Cdk_jcoo_d, bm.ineq_Cdk_jcoo_v, bm.ineq_Cdk_bufidx,
- bm.ineq_S_row, bm.ineq_S_col, bm.ineq_S_jcoo1, bm.ineq_S_jcoo2, bm.ineq_S_bufidx,
- bm.pr_diag_global, bm.pr_diag_nzpos,
- )
+ ScenarioBlockMap(
+ bm.hess_Akk_coo, bm.hess_Akk_nzpos,
+ bm.hess_Cdk_coo, coupled_inv[bm.hess_Cdk_row], bm.hess_Cdk_col,
+ bm.ineq_Akk_nzpos, bm.ineq_Akk_jcoo1, bm.ineq_Akk_jcoo2, bm.ineq_Akk_bufidx,
+ coupled_inv[bm.ineq_Cdk_row], bm.ineq_Cdk_col, bm.ineq_Cdk_jcoo_d, bm.ineq_Cdk_jcoo_v, bm.ineq_Cdk_bufidx,
+ bm.ineq_S_row, bm.ineq_S_col, bm.ineq_S_jcoo1, bm.ineq_S_jcoo2, bm.ineq_S_bufidx,
+ bm.pr_diag_global, bm.pr_diag_nzpos,
+ )
end for k in 1:ns
]
@@ -1260,7 +1260,7 @@ function factorize_kkt!(kkt::SchurComplementCondensedKKTSystem)
end
function solve_kkt!(
- kkt::SchurComplementCondensedKKTSystem,
+ kkt::SchurComplementCondensedKKTSystem,
w::AbstractKKTVector{T},
) where T
@@ -1359,7 +1359,7 @@ function solve_kkt!(
end
# KKT matrix-vector product for iterative refinement
-function mul!(w::AbstractKKTVector{T}, kkt::SchurComplementCondensedKKTSystem{T}, x::AbstractKKTVector, alpha = one(T), beta = zero(T)) where T
+function mul!(w::AbstractKKTVector{T}, kkt::SchurComplementCondensedKKTSystem{T}, x::AbstractKKTVector, alpha = one(T), beta = zero(T)) where {T}
n = num_variables(kkt)
ns_ineq = kkt.n_ineq
wx = @view(primal(w)[1:n])
diff --git a/test/schur_test.jl b/test/schur_test.jl
index 83181ce..67ea477 100644
--- a/test/schur_test.jl
+++ b/test/schur_test.jl
@@ -184,8 +184,8 @@ using MadNLPTests
# scenario var and the design var.
hess_I_ok = Int32[1, 2, 3]; hess_J_ok = Int32[1, 2, 3]
jac_I_ok = Int32[1, 1, 2, 2]; jac_J_ok = Int32[1, 3, 2, 3]
- ind_eq = Int32[]
- ind_ineq = Int32[1, 2]
+ ind_eq = Int32[]
+ ind_ineq = Int32[1, 2]
@test MadNLP._build_schur_symbolic(
Float64, n, m, ns, nv, nd, nc,
hess_I_ok, hess_J_ok, jac_I_ok, jac_J_ok, ind_eq, ind_ineq, |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #623 +/- ##
==========================================
+ Coverage 88.91% 89.13% +0.21%
==========================================
Files 55 55
Lines 5015 5125 +110
==========================================
+ Hits 4459 4568 +109
- Misses 556 557 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
michel2323
force-pushed
the
schur-design-constraints
branch
from
June 23, 2026 16:26
dffdbea to
e0734fb
Compare
michel2323
force-pushed
the
schur-design-constraints
branch
8 times, most recently
from
July 6, 2026 16:22
19804b7 to
a84c484
Compare
michel2323
force-pushed
the
schur-design-constraints
branch
from
July 20, 2026 14:44
a84c484 to
79a9fe1
Compare
… ubatch
cuDSS (through 0.8) stops reporting a usable inertia once matching is
enabled: it returns (0, 0) with info == 0 even though the factorization
is correct. InertiaBased/InertiaAuto then bump regularization forever and
dive into restoration. Recover the inertia from the sign counts of the
factor diagonal ("diag" data parameter) whenever cudss_matching is on,
using a buffer cached on the solver. Exact only for 1x1 pivots, which
holds for the quasi-definite condensed KKT systems this solver targets.
Also skip matching on uniform-batch solvers: setting matching_alg on a
ubatch solver fails the analysis phase with CUDSS_STATUS_NOT_SUPPORTED,
so the two-stage per-scenario batch solver must never enable it.
Add a sparse-callback path for SchurComplementKKTSystem covering two-stage
stochastic programs with design-only constraints. All equalities are relaxed to
condensed inequalities (slacks), so the per-scenario blocks and the first-stage
Schur complement are SPD on both CPU and GPU.
- Tag-based design/scenario layout supporting design-only constraints
(src/KKT/Schur/schur.jl).
- GPU sparse cuDSS Schur complement: batched per-scenario factorization plus a
sparse first-stage complement
(lib/MadNLPGPU/ext/MadNLPGPUCUDAExt/{cuda_schur,kernels_schur,lapackgpu}.jl).
- twostage_qp instance and CPU/GPU parity tests
(lib/MadNLPTests, test/schur_test.jl, lib/MadNLPGPU/test/schur_cuda_test.jl).
…uDSS IR 1->5 Equalities are always relaxed with slacks, matching SparseCondensedKKTSystem, so flip the option defaults (src/IPM/options.jl). Raise the default Schur cuDSS iterative-refinement steps 1->5: the batched per-scenario solves are inaccurate and the error compounds through the reduction Σ_k C_dk A_kk⁻¹ C_dk', so IR is needed for the relaxed GPU path to converge.
cuDSS 0.8 newly honors CUDSS_CONFIG_IR_TOL: with ir_n_steps > 0 AND ir_tol > 0 it arms a hard convergence check and returns CUDSS_STATUS_IR_FAILED when iterative refinement cannot reach ir_tol within the step budget (pre-0.8 it ignored ir_tol). The Schur path's near-singular per-scenario block at IPM iter 0 never reaches a tight tol in a few steps, so cuDSS aborts and crashes the solve (the refined solution is still valid). Default cudss_ir_tol 1e-8 -> 0.0 in CudssSolverOptions and set it explicitly in _default_schur_cudss_options(), which keeps the (load-bearing) ir_n_steps refinement passes but disarms the convergence gate. Single-mode is unaffected (cudss_ir = 0 there, so ir_tol is never set).
…convergence The GPU SchurComplementKKTSystem build_kkt! used atomic-add condensation scatters whose summation ORDER is nondeterministic. With the RelaxEquality condensation weights Σ≈1e8 (and the constraint Hessian scaled by the large relaxed-equality duals λ), that reordering perturbed the assembled blocks ~1e-7 run-to-run; amplified by the (legitimate) KKT conditioning this tipped the IPM step-acceptance residual across its threshold, making convergence a per-run roulette (~20% on the case9 SCOPF). The solve itself is bit-identical to the CPU path given identical blocks — only the assembly was nondeterministic, while CPU assembles sequentially. Replace the Σ-amplified scatters (inequality condensation into A_kk / C_dk / S, the large-valued Hessian scatters, and the Schur-reduction block) with deterministic variants: one thread per output nz-slot sums that slot's contributions in a fixed (sorted-by-slot) order with a plain `+=` (no two threads share a slot within a launch; launches are sequential), so the assembled blocks are reproducible. The sorted summation is also more accurate than random atomic order, which lowers the step residual. The per-slot segment structure depends only on the static sparsity pattern, so it is built once and memoized in a new `det_cache` Ref field on the kkt (no per-iteration sort; freed with the kkt). pr_diag scatters target distinct diagonal slots (no collision) and are left as-is. case9 (2 contingencies), tol=1e-4: GPU two-stage 20%→100% (47/47 across inertia free/based/auto), all reaching the true optimum 5352.103 — matching the CPU path, no retry and no loosened tolerance. case118 (176 scenarios): the one-time sort is ~330 ms (amortized; not per-iteration), per-iteration build_kkt! ~88 ms (dominated by the cuDSS factorizations, not the scatters; max contribution-per-slot ~60).
…tently on CPU/CUDA/ROCm
… Schur build
GPU two-stage Schur diverged from CPU on larger problems (case118, 176 scenarios):
the IPM pegged regularization (lg(rg)=10) and froze, while CPU converged with
near-zero regularization. Cause: build_kkt! assembled a wrong Schur complement S
(differs from CPU by ~4000x, cond 6.5e8 vs 5e4) even though A_kk and C_dk are
assembled identically (diff 0 / 1e-18) and A_kk is well-conditioned (cond 5e4).
Localized to the reduction tmp = A_kk⁻¹ C_dk_red', which is a single cuDSS
uniform-batch ("ubatch") solve with m right-hand sides per scenario. That solve
returns garbage (||tmp||~1e14 vs ~3) once the batch count ns is above ~14. The
single-RHS batched solve at the same point is exact (~1e-13), and forcing
asynchronous=false + synchronize does not help (not an async race).
Reproduced in pure CUDSS with a synthetic uniform batch: nrhs=1 is correct for all
nbatch; nrhs=54 is correct for nbatch<=12 and garbage for nbatch>=16 — independent
of the matrix. cuDSS's own batched tests only cover single-RHS, so multi-RHS ubatch
was never exercised upstream. This is a cuDSS-level bug worth reporting.
Workaround: solve the m coupled columns one at a time with the (correct) single-RHS
batched solve — column j across all ns scenarios — mirroring the CPU's
column-by-column A_kk \ C_dk'. The expensive batched factorization and the
per-scenario triangular solves stay fully batched over ns; only the build-phase
reduction loops its m RHS columns. Restore the single multi-RHS call once cuDSS
fixes multi-RHS ubatch.
Result: GPU two-stage case118 (nc=24) now SOLVE_SUCCEEDED in 34 iters with lg(rg)
healthy (→ -19.7), matching CPU's behaviour; case9 unchanged (16 iters, 5338.6725).
…ns error PR-623 review finding 10. `check_option_sanity` still classified `SchurComplementCondensedKKTSystem` as `is_kkt_dense`, a leftover from when its first-stage block was factored by a dense solver. After the sparse-Schur rewrite the first-stage complement is a sparse lower-triangular CSC on BOTH CPU (MumpsSolver / any :csc solver) and GPU (cuDSS), and the system is driven by a SparseCallback with no dense quasi-Newton wiring. Classifying it as dense let a `hessian_approximation = BFGS/DampedBFGS` pass sanity and then fail later with an obscure constructor/dispatch error. Drop Schur from `is_kkt_dense` so dense BFGS is rejected up front, and fix the stale comment.
The literal `Union{SparseCondensedKKTSystem,
SchurComplementCondensedKKTSystem}` was copy-pasted across the IPM
option defaults and both GPU extension option constructors. Extract a
single `const CondensedKKTSystems` in options.jl and reference it (and
`MadNLP.CondensedKKTSystems` from the CUDA extension).
…ty error PR-623 review finding 2. The `SchurComplementKKTSystem` name was kept as a silent `const` alias while the underlying defaults flipped (EnforceEquality->RelaxEquality, MakeParameter->RelaxBound, tighter->looser tol/bound_relax_factor), so unchanged user scripts silently got the relaxed semantics. Replace the const with `Base.@deprecate_binding` carrying a behavior-change message, so use of the old name warns and points at `SchurComplementCondensedKKTSystem`. Also improve the `_build_schur_symbolic` error raised when a user explicitly keeps equalities (e.g. `equality_treatment=EnforceEquality`): explain that the bordered saddle was removed and the first-stage complement is now SPD, instead of a terse raw ErrorException.
The ROCm option constructor special-cased Schur in its bound_relax_factor default, implying support that does not exist — there is no ROCm Schur implementation (the batched per-scenario factorization and Schur reduction are cuDSS/CUBLAS-specific). check_option_sanity passed and the generic CPU create_kkt_system then ran with VT=ROCVector and failed deep in construction with a raw MethodError/scalar-indexing error. Add a guarded ROCVector create_kkt_system that errors early with a clear message, and drop Schur from the ROCm bound_relax_factor union so it no longer implies support.
CPU took only `schur_scenario_linear_solver` (a solver type, no options passthrough) while GPU took `schur_scenario_opt_linear_solver`/`schur_opt_linear_solver` (option structs), and neither method had a kwargs catch-all — so a `kkt_options` dict written for one backend crashed the other with "unsupported keyword argument", and there was no way at all to pass options to the CPU scenario solvers (they were built with defaults). Both methods now accept the union of the kwargs plus a `kwargs...` catch-all that warns (instead of crashing) on inapplicable/unknown options: - CPU additionally accepts `schur_scenario_opt_linear_solver` (now threaded into the per-scenario solver constructors) and `schur_opt_linear_solver` (GPU-only; accepted-and-ignored with a warning). - GPU additionally accepts `schur_scenario_linear_solver` (cuDSS-only; ignored with a warning unless it is CUDSSSolver).
…ed columns
PR-623 review finding 7. The CPU forward elimination solved all `nd` design
columns per scenario and allocated full-width dense C_dk (nd x blk) / tmp_blk_nd
(blk x nd), though only the `m` coupled design columns are ever nonzero. For
SCOPF-shaped problems with nd >> m this meant ns*nd triangular solves per
iteration (instead of ns*m) and 2*ns dense nd x nv matrices — orders of magnitude
slower, and can exhaust memory on problems the GPU handles.
Mirror the GPU reduction: store C_dk reduced to its m coupled design rows
(m x blk) and tmp_blk_nd = A_kk^-1 C_dk_red' as (blk x m); solve only the m
coupled columns; accumulate the forward/back coupling through a contiguous reduced
buffer `rhs_d_red` (BLAS GEMV) and scatter into the coupled design rows. The Schur
reduction `mul!(schur_block, C_dk_red, tmp_red)` is now a plain contiguous GEMM
(BLAS), replacing the Vector{Int}-indexed views that hit the generic non-BLAS
fallback every iteration.
Verified: existing CPU Schur tests pass (m == nd), and a hand-checked m < nd
instance matches the SparseKKT reference to ~1e-8 at tol=1e-8.
PR-623 review cleanup. The existing two-stage test QP coupled every design variable to a scenario (m_coupled == nd always), so the coupled-subset reduction path (m < nd) had zero coverage. Add an `n_coupled` knob to `build_twostage_qp_general` (scenario constraints couple only the first `n_coupled` design vars; the rest are design-only) and a CPU testset that solves `m < nd` instances and matches the SparseKKT reference.
After the sparse-Schur rewrite the first-stage complement is a sparse lower-triangular CSC (factored by MumpsSolver / any :csc solver on CPU, cuDSS on GPU), not a dense nd x nd matrix. Update docs/src/lib/kkt.md and the CLAUDE.md architecture note accordingly, and fix the stale `SchurComplementKKTSystem` name.
…erride `supports_bunchkaufman_inertia(::LapackCUDASolver)=true` (+ inertia_bunchkaufman) was justified by the dense first-stage Schur block, which this branch removed from the GPU Schur path (both stages are now cuDSS). What remained was an untested behaviour change flipping is_inertia false->true for every dense-KKT + LapackCUDASolver + BUNCHKAUFMAN user (inertia-based correction plus an O(n^2) device->host copy per factorization, and a silent mis-read if cuSOLVER's sytrf pivot layout ever diverges from LAPACK). Revert to the AbstractLapackSolver default (inertia-free).
… Schur) The CudssSolverOptions struct default for cudss_ir_tol was changed globally 1e-8 -> 0.0, affecting ALL cuDSS users, not just the Schur path. With ir_tol=0.0 the cuDSS 0.8 CUDSS_STATUS_IR_FAILED gate is disarmed, so a non-Schur user running cudss_ir>0 gets unconverged solves returned as success (the IPM then proceeds with an inaccurate Newton step) instead of a loud failure. Restore the historical 1e-8 default; the Schur-specific 0.0 override stays scoped to _default_schur_cudss_options().
PR-623 review finding 1 (the one genuine GPU correctness bug). The GPU compress_jacobian! used the generic non-summing transfer! (view(jt_csc.nzVal, map) .= jac), which drops duplicate (constraint, variable) COO coordinates last-write-wins. ExaModels-style AD emits such duplicates, and the condensation kernels + the deterministic Hessian scatter all SUM duplicates, so jt_csc disagreed with the assembled A_kk/C_dk/S blocks -> jtprod! (jacl = J'y), mul!, and dual recovery used a different Jacobian than the factorized system. This is a GPU-only inconsistency (CPU _transfer! already sums with +=), and a plausible contributor to the case9 two-stage GPU divergence. Fix: build a deterministic segmented SUM (jt_lin) keyed by jt_csc nzval slot and use it in compress_jacobian!, mirroring the deterministic Hessian scatter.
PR-623 review finding 3. The GPU constructor accepted the positional linear_solver / opt_linear_solver chosen by the IPM but hard-wired cuDSS for both stages, silently ignoring e.g. linear_solver=LapackCUDASolver or a custom AbstractLinearSolver. Warn loudly when linear_solver was overridden (i.e. is not CUDSSSolver), directing users to schur_opt_linear_solver to configure the first-stage cuDSS solver, instead of silently substituting.
… double-wrap PR-623 review finding 9. CUDSSSolver.factorize!/solve_linear_system! caught every CUDSS.CUDSSError and rethrew a field-less FactorizationException/SolveException, erasing the status (ALLOC_FAILED, NOT_INITIALIZED, INTERNAL_ERROR, ...) — so a GPU OOM was indistinguishable from numerical breakdown once mapped to ERROR_IN_STEP_COMPUTATION. Log the cuDSS status (e.code) via the solver logger before converting. Also drop the redundant outer try/catch in the Schur build_kkt! forward elimination: solve_linear_system! already converts a cuDSS failure into a SolveException (which the IPM maps to ERROR_IN_STEP_COMPUTATION), so re-wrapping it as a FactorizationException was dead double-handling (its CUDSS.CUDSSError branch was unreachable).
These kernels were defined but never launched anywhere in
the repo (the assembly was rewritten to the deterministic scatters and the sparse
CSC path): _scatter_to_Cdk_batched!, _ineq_condense_{Akk,Cdk,S}_kernel!,
_init_S_{hess,diag}_kernel!, _ineq_condense_csc_kernel!, _scatter_schur_block!.
Only 7 of the 15 defined kernels were actually called; delete the 8 dead ones and
refresh the stale comment on the surviving _scatter_to_csc_atomic!.
`nd_aug` was a fossil of the removed bordered equality block (it always equals `nd` now that the Schur system is RelaxEquality-only). It was threaded through the symbolic return and both the CPU and GPU constructors for no reason. Collapse it to `nd` everywhere (and rename a coincidental test-local).
`scenario_x_multi` / `scenario_b_multi` and their extra
construction-time `cudss("analysis", ...)` call were leftovers of the broken cuDSS
multi-RHS ubatch workaround: every scenario solve in build_kkt!/solve_kkt! is now
single-RHS (the multi-RHS ubatch is broken above nbatch~=14), and the batched
scenario solver is already analyzed for its single-RHS shape inside the CUDSSSolver
constructor. Drop the two struct fields, the descriptors, and the redundant
analysis. GPU Schur tests still pass 30/30.
solve_kkt! steps 3 (forward reduction) and 5 (back substitution) ran a per-scenario `mul!` loop = ns kernel launches per solve. Replace each with one CUBLAS.gemm_strided_batched!: - step 3: strided-batched (m x 1) products C_dk_red[:,:,k]' rhs_k[:,k], reduced over the batch dim (sum!) into the reduced coupling RHS; - step 5: broadcast the shared reduced design update across scenarios and do the whole back-substitution rhs_k[:,k] -= tmp_red[:,:,k] * rhs_d_red as one batched GEMM. Two small preallocated buffers (rhs_d_red_batched (m,1,ns), rhs_d_red_mat (m,ns)) keep it allocation-free. GPU Schur tests pass 30/30 (matches CPU reference).
`_get_det_scatter` returns the memoized condensation-scatter structure as `::Any` (its concrete NamedTuple type is large), so accessing `det.X` in the per-iteration hot paths dynamic-dispatched every time. Introduce function barriers: build_kkt!, compress_hessian!, compress_jacobian!, mul!, and mul_hess_blk! each hoist the `_get_det_scatter(kkt)` access to a thin wrapper and pass the result to a specialized inner method, so every `det.X` access (and the kernels / SpMV taking them) is type-stable after a single dynamic dispatch. GPU Schur tests pass 30/30.
michel2323
force-pushed
the
schur-design-constraints
branch
from
July 20, 2026 16:51
79a9fe1 to
6dd1a69
Compare
is_inertia_correct for SchurComplementCondensedKKTSystem inspected only the inertia of the first-stage complement S, which is what the IPM reads off kkt.linear_solver. By Haynsworth additivity In(K) = Sum_k In(A_k) + In(S), so In(S) == (nd, 0, 0) certifies the condensed KKT matrix only if every per-scenario block A_k is positive definite as well. The omission is not conservative. An indefinite A_k makes its contribution -C_k A_k^-1 C_k' to S a positive shift, which can leave S positive definite: S then passes, delta_w is never raised, and the IPM accepts a direction with wrong curvature. Measured on a two-stage SCOPF model with ns = 176, 103-161 blocks were indefinite on most iterations while S stayed SPD; the solve ran 165 iterations at ir = 1, hit restoration and went NaN. build_kkt! now aggregates the block inertias into three counters on the KKT while it factorizes, and is_inertia_correct requires them to be zero. This costs nothing: MUMPS already computes the negative-pivot count in INFOG(12) during that same factorization, so the counters are a read rather than work. delta_w needed no change either -- regularize_diagonal! already feeds pr_diag into every A_k as well as into S_dd; only the trigger was broken. Also add is_inertia(::AbstractKKTSystem), defaulting to the capability of the KKT's linear solver, and switch InertiaAuto to dispatch on it. A KKT system whose matrix is factorized by more than one solver can now report that it certifies inertia only when all of them do; the Schur override requires the complement plus all ns block solvers. Blocks that cannot report inertia are skipped in the aggregation, so InertiaAuto falls back to InertiaFree and a forced InertiaBased raises InertiaException rather than silently certifying nothing. The ns = 176 case goes from NaN-at-restoration to SOLVE_SUCCEEDED, matching the equivalent single-block formulation to 5e-15 relative in the same 47 iterations and 105 factorizations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is another pass over the Schur complement-based solver while adding design constraint support and replacing the dense Schur complement solve with sparse CUDSS on the GPU.
I'm sorry that it is again a large diff. But the tag-based detection of cross and design only constraints and the change in the structure implied a new variable ordering.
I've added our classic SCOPF formulation that we used in ECP and elsewhere to ExaModelsPower.jl (no PR yet). This was very useful to debug. Currently, it passes all IEEE case 9 and 118 with the line contingencies we always used.
One major change was also that I did not relax the equality constraints in the GPU solver. This is fixed now.