From 3c010ea2061cfb75784fbab88dc9f81ab8ed26d5 Mon Sep 17 00:00:00 2001 From: Yan Zaretskiy Date: Tue, 7 Apr 2026 12:18:51 -0700 Subject: [PATCH 01/68] feat(barrier): add SOCP cone primitives and NT kernels Build the cone-local Jordan-product, scaling, and corrector kernels needed for the SOCP barrier path before augmented-system integration. --- cpp/src/barrier/second_order_cone.cuh | 919 ++++++++++++ cpp/tests/dual_simplex/CMakeLists.txt | 3 +- .../unit_tests/second_order_cone_test.cu | 1280 +++++++++++++++++ 3 files changed, 2201 insertions(+), 1 deletion(-) create mode 100644 cpp/src/barrier/second_order_cone.cuh create mode 100644 cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu diff --git a/cpp/src/barrier/second_order_cone.cuh b/cpp/src/barrier/second_order_cone.cuh new file mode 100644 index 0000000000..eb337b9ce1 --- /dev/null +++ b/cpp/src/barrier/second_order_cone.cuh @@ -0,0 +1,919 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +#include + +#include + +#include + +#include + +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex { + +// --------------------------------------------------------------------------- +// Shared reduction primitives +// --------------------------------------------------------------------------- + +template +using triplet_t = cuda::std::tuple; + +template +struct triplet_sum { + DI triplet_t operator()(const triplet_t& lhs, const triplet_t& rhs) const + { + const auto& [v0_l, v1_l, v2_l] = lhs; + const auto& [v0_r, v1_r, v2_r] = rhs; + return {v0_l + v0_r, v1_l + v1_r, v2_l + v2_r}; + } +}; + +template +using block_reduce_t = cub::BlockReduce; + +template +struct smem_reduce_t { + using ScalarReduce = block_reduce_t; + using TripletReduce = block_reduce_t, BLOCK_DIM>; + + union { + typename ScalarReduce::TempStorage scalar_temp; + typename TripletReduce::TempStorage triplet_temp; + f_t scalar_broadcast; + triplet_t triplet_broadcast; + }; +}; + +// --------------------------------------------------------------------------- +// reduce_broadcast: block-reduce a value, then broadcast to all threads. +// --------------------------------------------------------------------------- + +template +DI f_t reduce_broadcast(f_t val, smem_reduce_t& s) +{ + f_t agg = typename smem_reduce_t::ScalarReduce(s.scalar_temp).Sum(val); + __syncthreads(); + if (threadIdx.x == 0) { s.scalar_broadcast = agg; } + __syncthreads(); + return s.scalar_broadcast; +} + +template +DI triplet_t reduce_broadcast(triplet_t val, smem_reduce_t& s) +{ + auto agg = typename smem_reduce_t::TripletReduce(s.triplet_temp) + .Reduce(val, triplet_sum{}); + __syncthreads(); + if (threadIdx.x == 0) { s.triplet_broadcast = agg; } + __syncthreads(); + return s.triplet_broadcast; +} + +template +struct smem_warp_reduce_t { + static constexpr int warps_per_block = BLOCK_DIM / 32; + + using ScalarReduce = cub::WarpReduce; + using TripletReduce = cub::WarpReduce, 32>; + + union { + typename ScalarReduce::TempStorage scalar_temp[warps_per_block]; + typename TripletReduce::TempStorage triplet_temp[warps_per_block]; + f_t scalar_broadcast[warps_per_block]; + triplet_t triplet_broadcast[warps_per_block]; + }; +}; + +// --------------------------------------------------------------------------- +// reduce_broadcast: warp-reduce a value, then broadcast within the warp. +// --------------------------------------------------------------------------- + +template +DI f_t reduce_broadcast(f_t val, smem_warp_reduce_t& s) +{ + static_assert(BLOCK_DIM % 32 == 0, "Warp reduce requires warp-aligned CTAs"); + + int warp = threadIdx.x >> 5; + int lane = threadIdx.x & 31; + f_t agg = typename smem_warp_reduce_t::ScalarReduce(s.scalar_temp[warp]).Sum(val); + if (lane == 0) { s.scalar_broadcast[warp] = agg; } + __syncwarp(); + return s.scalar_broadcast[warp]; +} + +template +DI triplet_t reduce_broadcast(triplet_t val, smem_warp_reduce_t& s) +{ + static_assert(BLOCK_DIM % 32 == 0, "Warp reduce requires warp-aligned CTAs"); + + int warp = threadIdx.x >> 5; + int lane = threadIdx.x & 31; + auto agg = typename smem_warp_reduce_t::TripletReduce(s.triplet_temp[warp]) + .Reduce(val, triplet_sum{}); + if (lane == 0) { s.triplet_broadcast[warp] = agg; } + __syncwarp(); + return s.triplet_broadcast[warp]; +} + +// --------------------------------------------------------------------------- +// Apply H^{-1} to one vector per cone (one thread-block per cone). +// +// H^{-1}z = (1/η)(w̄₀z₀ − ζ, z₁ + (−z₀ + ζ/(1+w̄₀))w̄₁), ζ = w̄₁ᵀz₁ +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv_kernel(const f_t* __restrict__ z, + f_t* __restrict__ out, + const f_t* __restrict__ w_bar, + const f_t* __restrict__ inv_eta, + const f_t* __restrict__ inv_1pw0, + const i_t* __restrict__ cone_offsets, + i_t K) +{ + __shared__ smem_reduce_t smem; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + const f_t* w_cone = w_bar + off; + const f_t* z_cone = z + off; + f_t* out_cone = out + off; + + f_t z0 = z_cone[0]; + f_t w0 = w_cone[0]; + + // Phase 1: ζ = w̄₁ᵀ z₁ + f_t partial = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + partial += w_cone[j] * z_cone[j]; + } + f_t zeta = reduce_broadcast(partial, smem); + + // Phase 2: element-wise output + f_t ie = inv_eta[cone]; + f_t ipw = inv_1pw0[cone]; + f_t coeff = -z0 + zeta * ipw; + + if (threadIdx.x == 0) { out_cone[0] = (w0 * z0 - zeta) * ie; } + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + out_cone[j] = (z_cone[j] + coeff * w_cone[j]) * ie; + } +} + +// --------------------------------------------------------------------------- +// Apply H^{-2} to one vector per cone (one thread-block per cone). +// +// H^{-2}v = η⁻²(2u(uᵀv) − Jv), u = Jw̄, J = diag(1,−1,…,−1). +// +// One dot product (uᵀv) plus element-wise work — same structure as apply_Hinv. +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv2_kernel( + const f_t* __restrict__ v, + f_t* __restrict__ out, + const f_t* __restrict__ w_bar, + const f_t* __restrict__ inv_eta, + const i_t* __restrict__ cone_offsets, + i_t K) +{ + __shared__ smem_reduce_t smem; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + const f_t* w_cone = w_bar + off; + const f_t* v_cone = v + off; + f_t* out_cone = out + off; + + f_t v0 = v_cone[0]; + f_t w0 = w_cone[0]; + + // Phase 1: uᵀv = w̄₀v₀ − Σ w̄_j v_j (tail dot, then subtract from head) + f_t partial = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + partial += w_cone[j] * v_cone[j]; + } + f_t tail_dot = reduce_broadcast(partial, smem); + f_t uTv = w0 * v0 - tail_dot; + + // Phase 2: element-wise output + f_t ie_sq = inv_eta[cone] * inv_eta[cone]; + f_t coeff = f_t(2) * uTv * ie_sq; + + if (threadIdx.x == 0) { out_cone[0] = coeff * w0 - ie_sq * v0; } + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + out_cone[j] = -coeff * w_cone[j] + ie_sq * v_cone[j]; + } +} + +// --------------------------------------------------------------------------- +// Cone-algebra primitives for the deferred combined-step corrector: +// r_K = omega circ omega + dx_scaled circ dz_scaled - sigma mu e +// corr = omega \ r_K +// t_K = H^{-1} corr +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Jordan product for packed SOC vectors (one CTA per cone). +// +// For a, b in Q^q: (a ∘ b)_0 = a^T b, (a ∘ b)_j = a_0 b_j + b_0 a_j. +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void jordan_product_kernel( + const f_t* __restrict__ a, + const f_t* __restrict__ b, + f_t* __restrict__ out, + const i_t* __restrict__ cone_offsets, + i_t K) +{ + __shared__ smem_reduce_t smem; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + const f_t* a_cone = a + off; + const f_t* b_cone = b + off; + f_t* out_cone = out + off; + + f_t a0 = a_cone[0]; + f_t b0 = b_cone[0]; + + f_t partial = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + partial += a_cone[j] * b_cone[j]; + } + f_t tail_dot = reduce_broadcast(partial, smem); + + if (threadIdx.x == 0) { out_cone[0] = a0 * b0 + tail_dot; } + + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + out_cone[j] = a0 * b_cone[j] + b0 * a_cone[j]; + } +} + +// --------------------------------------------------------------------------- +// Inverse Jordan product for packed SOC vectors (one CTA per cone). +// +// For omega in int(Q^q) and vector r, +// (omega \ r)_0 = (omega_0 r_0 − nu) / rho +// (omega \ r)_j = ((nu/omega_0 − r_0)/rho) omega_j + r_j/omega_0 +// where nu = omega_1^T r_1 and rho = ||omega||_J^2 (stored per-cone). +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void inverse_jordan_product_kernel( + const f_t* __restrict__ omega, + const f_t* __restrict__ r, + const f_t* __restrict__ rho, + f_t* __restrict__ out, + const i_t* __restrict__ cone_offsets, + i_t K) +{ + __shared__ smem_reduce_t smem; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + const f_t* omega_cone = omega + off; + const f_t* r_cone = r + off; + f_t* out_cone = out + off; + + f_t omega_0 = omega_cone[0]; + f_t r_0 = r_cone[0]; + + f_t partial = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + partial += omega_cone[j] * r_cone[j]; + } + f_t nu = reduce_broadcast(partial, smem); + + f_t rho_val = rho[cone]; + f_t inv_rho = f_t(1) / rho_val; + f_t c_omega_j = ((nu / omega_0) - r_0) * inv_rho; + f_t c_r_j = f_t(1) / omega_0; + + if (threadIdx.x == 0) { out_cone[0] = (omega_0 * r_0 - nu) * inv_rho; } + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + out_cone[j] = c_omega_j * omega_cone[j] + c_r_j * r_cone[j]; + } +} + +// --------------------------------------------------------------------------- +// Fused corrector for the combined-step SOC correction (one CTA per cone). +// +// Computes in a single kernel launch: +// 1. dx = H^{-1} Δx_aff (affine scaled direction) +// 2. dz = −ω − dx (complementary direction) +// 3. r_K = ω∘ω + dx∘dz − σμ e (combined cone residual) +// 4. corr = ω \ r_K (inverse Jordan product) +// 5. t_K = H^{-1} corr (corrector for reduced RHS) +// +// Uses the `out` buffer as scratch (holds dx during phases 1–3) and writes +// the final t_K there, so zero extra temporary buffers are needed. +// +// Algebraic shortcut: the triplet (Σ ω_j², Σ ω_j dx_j, Σ dx_j²) computed +// for r_K_0 also yields ν = Σ ω_j r_K_j via a linear combination, avoiding +// a fourth reduction pass. +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void fused_corrector_kernel( + const f_t* __restrict__ dx_aff, + const f_t* __restrict__ omega, + const f_t* __restrict__ w_bar, + const f_t* __restrict__ inv_eta, + const f_t* __restrict__ inv_1pw0, + const f_t* __restrict__ rho, + f_t sigma_mu, + f_t* __restrict__ out, + const i_t* __restrict__ cone_offsets, + i_t K) +{ + __shared__ smem_reduce_t smem; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + const f_t* dx_a = dx_aff + off; + const f_t* omega_cone = omega + off; + const f_t* w_cone = w_bar + off; + f_t* out_cone = out + off; + + f_t ie = inv_eta[cone]; + f_t ipw = inv_1pw0[cone]; + f_t rho_val = rho[cone]; + f_t omega_0 = omega_cone[0]; + f_t w_0 = w_cone[0]; + f_t dx_a_0 = dx_a[0]; + + // ================================================================= + // Phase A — reduce ζ = Σ_{j≥1} w̄_j (Δx_aff)_j for H^{-1} + // ================================================================= + f_t partial = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + partial += w_cone[j] * dx_a[j]; + } + f_t zeta = reduce_broadcast(partial, smem); + + f_t dx_0 = (w_0 * dx_a_0 - zeta) * ie; + f_t coeff_a = -dx_a_0 + zeta * ipw; + f_t dz_0 = -omega_0 - dx_0; + + // ================================================================= + // Phase A→B — write dx to out; accumulate (A, B, C) for r_K and ν + // A = Σ ω_j², B = Σ ω_j dx_j, C = Σ dx_j² (j ≥ 1) + // ================================================================= + auto trip = triplet_t{}; + auto& [A_p, B_p, C_p] = trip; + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + f_t dx_j = (dx_a[j] + coeff_a * w_cone[j]) * ie; + out_cone[j] = dx_j; + f_t omega_j = omega_cone[j]; + A_p += omega_j * omega_j; + B_p += omega_j * dx_j; + C_p += dx_j * dx_j; + } + auto [A, B, C] = reduce_broadcast(trip, smem); + + // ================================================================= + // Phase B — form r_K_0, derive ν, then inverse-Jordan scalars + // ================================================================= + f_t r_K_0 = (omega_0 * omega_0 + A) + (dx_0 * dz_0 - B - C) - sigma_mu; + f_t nu = (f_t(2) * omega_0 - dx_0) * A - (omega_0 + f_t(2) * dx_0) * B; + + f_t inv_rho = f_t(1) / rho_val; + f_t corr_0 = (omega_0 * r_K_0 - nu) * inv_rho; + f_t inv_omega_0 = f_t(1) / omega_0; + f_t c_inv = (nu * inv_omega_0 - r_K_0) * inv_rho; + f_t p1 = c_inv + f_t(2) - dx_0 * inv_omega_0; + f_t p2 = -(f_t(1) + f_t(2) * dx_0 * inv_omega_0); + + // ================================================================= + // Phase B→C — accumulate ζ₂ = Σ_{j≥1} w̄_j corr_j for final H^{-1} + // corr_j = p1 ω_j + p2 dx_j (dx_j still in out_cone[j]) + // ================================================================= + f_t partial2 = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + f_t corr_j = p1 * omega_cone[j] + p2 * out_cone[j]; + partial2 += w_cone[j] * corr_j; + } + f_t zeta2 = reduce_broadcast(partial2, smem); + + // ================================================================= + // Phase C — write t_K = H^{-1}(corr) + // ================================================================= + f_t coeff_c = -corr_0 + zeta2 * ipw; + + if (threadIdx.x == 0) { out_cone[0] = (w_0 * corr_0 - zeta2) * ie; } + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + f_t corr_j = p1 * omega_cone[j] + p2 * out_cone[j]; + out_cone[j] = (corr_j + coeff_c * w_cone[j]) * ie; + } +} + +// --------------------------------------------------------------------------- +// Compute NT scaling from (s, lambda). +// +// Medium/large cones use one CTA per cone and stream s/lambda twice: +// Pass 1: reduce ||s_1||^2, ||lambda_1||^2, and s^T lambda. +// Pass 2: compute omega/w_bar directly from raw inputs and reduce ||w_bar_1||^2. +// +// Small cones (q <= 32) use one warp per cone and keep one element per lane in +// registers for the whole computation. In both paths, shared memory only stores +// per-warp partial reductions plus a small scalar broadcast struct. +// --------------------------------------------------------------------------- + +constexpr int small_cone_limit = 32; +constexpr int medium_cone_limit = 2048; +constexpr int small_block_dim = 64; +constexpr int medium_block_dim = 128; +constexpr int large_block_dim = 256; + +template +struct nt_broadcast_coeffs { + f_t w_from_s; + f_t w_from_lambda; + f_t omega_s_coeff; + f_t omega_lambda_coeff; +}; + +template +struct nt_block_storage { + smem_reduce_t reduce; + nt_broadcast_coeffs coeffs; +}; + +template +struct nt_warp_storage { + static constexpr int warps_per_block = BLOCK_DIM / 32; + + smem_warp_reduce_t reduce; + nt_broadcast_coeffs coeffs[warps_per_block]; +}; + +template +__global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_kernel(const f_t* __restrict__ s, + const f_t* __restrict__ lambda, + f_t* __restrict__ eta, + f_t* __restrict__ inv_eta, + f_t* __restrict__ inv_1pw0, + f_t* __restrict__ w_bar, + f_t* __restrict__ omega, + f_t* __restrict__ rho, + const i_t* __restrict__ cone_offsets, + const i_t* __restrict__ cone_ids, + i_t num_cones) +{ + static_assert(BLOCK_DIM % 32 == 0, "NT scaling kernel requires warp-aligned BLOCK_DIM"); + __shared__ nt_block_storage storage; + + i_t cone_idx = static_cast(blockIdx.x); + if (cone_idx >= num_cones) return; + + i_t cone = cone_ids[cone_idx]; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + + f_t s0 = s[off]; + f_t l0 = lambda[off]; + + auto partial = triplet_t{}; + auto& [s1_sq_p, l1_sq_p, sl_p] = partial; + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + f_t sj = s[off + j]; + f_t lj = lambda[off + j]; + s1_sq_p += sj * sj; + l1_sq_p += lj * lj; + sl_p += sj * lj; + } + + auto [s1_sq, l1_sq, sl] = reduce_broadcast(partial, storage.reduce); + f_t owner_eta = f_t(0); + f_t owner_inv_eta = f_t(0); + f_t owner_rho = f_t(0); + f_t owner_omega_0 = f_t(0); + if (threadIdx.x == 0) { + // Clamp radicands to zero: near the cone boundary, roundoff can make these + // slightly negative. + f_t s_J = sqrt(max(f_t(0), s0 * s0 - s1_sq)); + f_t l_J = sqrt(max(f_t(0), l0 * l0 - l1_sq)); + f_t inv_s_J = f_t(1) / s_J; + f_t inv_l_J = f_t(1) / l_J; + owner_rho = s_J * l_J; + owner_eta = sqrt(s_J / l_J); + owner_inv_eta = f_t(1) / owner_eta; + f_t scale = sqrt(owner_rho); + + f_t s_dot_l = (s0 * l0 + sl) * inv_s_J * inv_l_J; + f_t gamma = sqrt(max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); + f_t inv_2g = f_t(1) / (f_t(2) * gamma); + f_t sb0 = s0 * inv_s_J; + f_t lb0 = l0 * inv_l_J; + f_t D = sb0 + lb0 + f_t(2) * gamma; + f_t inv_D = f_t(1) / D; + f_t c_s = (gamma + sb0) * inv_D; + f_t c_l = (gamma + lb0) * inv_D; + + storage.coeffs.w_from_s = inv_2g * inv_s_J; + storage.coeffs.w_from_lambda = -inv_2g * inv_l_J; + // Name these by the raw tail element they multiply: + // omega_j = omega_s_coeff * s_j + omega_lambda_coeff * lambda_j. + // The closed-form NT expression is cross-coupled, so c_l multiplies s_j + // and c_s multiplies lambda_j. + storage.coeffs.omega_s_coeff = scale * c_l * inv_s_J; + storage.coeffs.omega_lambda_coeff = scale * c_s * inv_l_J; + owner_omega_0 = gamma * scale; + } + __syncthreads(); + + f_t w1_sq_partial = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + f_t sj = s[off + j]; + f_t lj = lambda[off + j]; + f_t wj = storage.coeffs.w_from_s * sj + storage.coeffs.w_from_lambda * lj; + w_bar[off + j] = wj; + omega[off + j] = storage.coeffs.omega_s_coeff * sj + storage.coeffs.omega_lambda_coeff * lj; + w1_sq_partial += wj * wj; + } + + f_t w1_sq = reduce_broadcast(w1_sq_partial, storage.reduce); + if (threadIdx.x == 0) { + f_t w0 = sqrt(f_t(1) + w1_sq); + omega[off] = owner_omega_0; + w_bar[off] = w0; + eta[cone] = owner_eta; + inv_eta[cone] = owner_inv_eta; + inv_1pw0[cone] = f_t(1) / (f_t(1) + w0); + rho[cone] = owner_rho; + } +} + +template +__global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_small_kernel( + const f_t* __restrict__ s, + const f_t* __restrict__ lambda, + f_t* __restrict__ eta, + f_t* __restrict__ inv_eta, + f_t* __restrict__ inv_1pw0, + f_t* __restrict__ w_bar, + f_t* __restrict__ omega, + f_t* __restrict__ rho, + const i_t* __restrict__ cone_offsets, + const i_t* __restrict__ cone_ids, + i_t num_cones) +{ + static_assert(BLOCK_DIM % 32 == 0, "Small-cone NT kernel requires warp-aligned CTAs"); + __shared__ nt_warp_storage storage; + + constexpr int warps_per_block = BLOCK_DIM / 32; + i_t warp_idx = + static_cast(blockIdx.x) * warps_per_block + static_cast(threadIdx.x >> 5); + if (warp_idx >= num_cones) return; + + int warp = threadIdx.x >> 5; + int lane = threadIdx.x & 31; + i_t cone = cone_ids[warp_idx]; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + + f_t sj = (lane < q) ? s[off + lane] : f_t(0); + f_t lj = (lane < q) ? lambda[off + lane] : f_t(0); + + auto partial = triplet_t{(lane > 0 && lane < q) ? sj * sj : f_t(0), + (lane > 0 && lane < q) ? lj * lj : f_t(0), + (lane > 0 && lane < q) ? sj * lj : f_t(0)}; + auto [s1_sq, l1_sq, sl] = reduce_broadcast(partial, storage.reduce); + + f_t owner_eta = f_t(0); + f_t owner_inv_eta = f_t(0); + f_t owner_rho = f_t(0); + f_t owner_omega_0 = f_t(0); + + if (lane == 0) { + f_t s0 = sj; + f_t l0 = lj; + f_t s_J = sqrt(max(f_t(0), s0 * s0 - s1_sq)); + f_t l_J = sqrt(max(f_t(0), l0 * l0 - l1_sq)); + f_t inv_s_J = f_t(1) / s_J; + f_t inv_l_J = f_t(1) / l_J; + owner_rho = s_J * l_J; + owner_eta = sqrt(s_J / l_J); + owner_inv_eta = f_t(1) / owner_eta; + f_t scale = sqrt(owner_rho); + + f_t s_dot_l = (s0 * l0 + sl) * inv_s_J * inv_l_J; + f_t gamma = sqrt(max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); + f_t inv_2g = f_t(1) / (f_t(2) * gamma); + f_t sb0 = s0 * inv_s_J; + f_t lb0 = l0 * inv_l_J; + f_t D = sb0 + lb0 + f_t(2) * gamma; + f_t inv_D = f_t(1) / D; + f_t c_s = (gamma + sb0) * inv_D; + f_t c_l = (gamma + lb0) * inv_D; + + storage.coeffs[warp].w_from_s = inv_2g * inv_s_J; + storage.coeffs[warp].w_from_lambda = -inv_2g * inv_l_J; + storage.coeffs[warp].omega_s_coeff = scale * c_l * inv_s_J; + storage.coeffs[warp].omega_lambda_coeff = scale * c_s * inv_l_J; + owner_omega_0 = gamma * scale; + } + __syncwarp(); + + f_t w1_sq = f_t(0); + if (lane > 0 && lane < q) { + f_t wj = storage.coeffs[warp].w_from_s * sj + storage.coeffs[warp].w_from_lambda * lj; + w_bar[off + lane] = wj; + omega[off + lane] = + storage.coeffs[warp].omega_s_coeff * sj + storage.coeffs[warp].omega_lambda_coeff * lj; + w1_sq = wj * wj; + } + w1_sq = reduce_broadcast(w1_sq, storage.reduce); + + if (lane == 0) { + f_t w0 = sqrt(f_t(1) + w1_sq); + omega[off] = owner_omega_0; + w_bar[off] = w0; + eta[cone] = owner_eta; + inv_eta[cone] = owner_inv_eta; + inv_1pw0[cone] = f_t(1) / (f_t(1) + w0); + rho[cone] = owner_rho; + } +} + +// --------------------------------------------------------------------------- +// Step length for a single (u, du) pair in Q^q. +// +// Finds the largest alpha in [0, alpha_max] such that u + alpha*du in Q^q. +// The cone condition u_0 + alpha*du_0 >= ||u_1 + alpha*du_1|| reduces to a +// linear test plus a quadratic a*alpha^2 + 2b*alpha + c >= 0 where +// a = du_0^2 - ||du_1||^2, b = u_0*du_0 - u_1^T du_1, c = u_0^2 - ||u_1||^2. +// --------------------------------------------------------------------------- +template +DI f_t +cone_step_length_single(const f_t* __restrict__ u, + const f_t* __restrict__ du, + i_t q, + typename block_reduce_t, BLOCK_DIM>::TempStorage& temp, + f_t alpha) +{ + auto partial = triplet_t{}; + auto& [du1_sq_p, u1du1_p, u1_sq_p] = partial; + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + f_t uj = u[j]; + f_t duj = du[j]; + du1_sq_p += duj * duj; + u1du1_p += uj * duj; + u1_sq_p += uj * uj; + } + + auto [du1_sq, u1du1, u1_sq] = + block_reduce_t, BLOCK_DIM>(temp).Reduce(partial, triplet_sum{}); + __syncthreads(); + + if (threadIdx.x == 0) { + f_t a = du[0] * du[0] - du1_sq; + f_t b = u[0] * du[0] - u1du1; + f_t c = max(f_t(0), u[0] * u[0] - u1_sq); + f_t disc = b * b - a * c; + + // Linear constraint: u_0 + alpha * du_0 >= 0. + if (du[0] < f_t(0)) { alpha = min(alpha, -u[0] / du[0]); } + + // Quadratic constraint. + if ((a > f_t(0) && b > f_t(0)) || disc < f_t(0)) { + // No positive root (parabola stays non-negative for alpha > 0). + } else if (a == f_t(0)) { + // Degenerate: 2b*alpha + c = 0. + if (b < f_t(0)) { alpha = min(alpha, c / (f_t(-2) * b)); } + } else if (c == f_t(0)) { + // Starting exactly on the cone boundary: take a full step only if the + // direction stays in the cone, otherwise the maximum feasible step is 0. + alpha = (a >= f_t(0)) ? alpha : f_t(0); + } else { + f_t t = -(b + copysign(sqrt(disc), b)); + f_t r1 = c / t; + f_t r2 = t / a; + if (r1 < f_t(0)) { r1 = alpha; } + if (r2 < f_t(0)) { r2 = alpha; } + alpha = min(alpha, min(r1, r2)); + } + } + return alpha; +} + +// --------------------------------------------------------------------------- +// Cone step length kernel (one block per cone). +// +// Computes, for each cone i, the largest alpha in [0, alpha_max] such that +// s_i + alpha * ds_i in Q^{q_i} AND lambda_i + alpha * dlambda_i in Q^{q_i}. +// The per-cone result is written to alpha[i]. +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void step_length_kernel( + const f_t* __restrict__ s, + const f_t* __restrict__ ds, + const f_t* __restrict__ lambda, + const f_t* __restrict__ dlambda, + f_t* __restrict__ alpha, + const i_t* __restrict__ cone_offsets, + i_t K, + f_t alpha_max) +{ + __shared__ typename block_reduce_t, BLOCK_DIM>::TempStorage temp_storage; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + + f_t alpha_s = + cone_step_length_single(s + off, ds + off, q, temp_storage, alpha_max); + f_t alpha_l = cone_step_length_single( + lambda + off, dlambda + off, q, temp_storage, alpha_max); + + if (threadIdx.x == 0) { alpha[cone] = min(alpha_s, alpha_l); } +} + +// --------------------------------------------------------------------------- +// Shift u into int(Q^q) if it is not already interior (one block per cone). +// +// alpha(u) = ||u_1|| - u_0. If alpha >= 0 (u on boundary or outside): +// u_0 <- u_0 + 1 + max(0, alpha) (shift along identity element e) +// +// Modifies u in place. Used once during initial-point computation. +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void interior_shift_kernel( + f_t* __restrict__ u, const i_t* __restrict__ cone_offsets, i_t K) +{ + __shared__ typename block_reduce_t::TempStorage temp_storage; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + + f_t tail_sq = f_t(0); + for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { + f_t v = u[off + j]; + tail_sq += v * v; + } + tail_sq = block_reduce_t(temp_storage).Sum(tail_sq); + + if (threadIdx.x == 0) { + f_t u1_norm = sqrt(tail_sq); + f_t gap = u1_norm - u[off]; + if (gap >= f_t(0)) { u[off] += f_t(1) + gap; } + } +} + +/** + * Owns device storage for second-order cone topology, iterates, and NT scaling. + * + * Flat arrays are packed by cone: elements [cone_offsets[i], cone_offsets[i+1]) + * belong to cone i, which has dimension cone_dims[i]. + * + * Search directions, RHS vectors, and workspace live directly in + * iteration_data_t (matching the existing LP/QP pattern where dx_aff, dual_rhs, + * etc. are all top-level fields of iteration_data_t). + */ +template +struct cone_data_t { + // --- Topology (set once at construction) --- + i_t K; // number of second-order cones + i_t m_c; // total cone dimension = sum of cone_dims + + rmm::device_uvector cone_offsets; // [K+1] prefix sums of cone_dims + rmm::device_uvector cone_dims; // [K] dimension q_i of each cone + + // --- Primal/dual cone iterates (rewritten each iteration) --- + rmm::device_uvector s; // [m_c] cone slack: s_i in int(Q^{q_i}) + rmm::device_uvector lambda; // [m_c] cone dual: lambda_i in int(Q^{q_i}) + + // --- NT scaling state (recomputed each iteration from s, lambda) --- + rmm::device_uvector eta; // [K] scaling factor eta_i = (||s_i||_J / ||lambda_i||_J)^{1/2} + rmm::device_uvector inv_eta; // [K] cached 1/eta_i + rmm::device_uvector inv_1pw0; // [K] cached 1/(1 + wbar_0_i) + rmm::device_uvector w_bar; // [m_c] NT scaling direction, unit J-norm, packed by cone + rmm::device_uvector omega; // [m_c] scaled variable omega_i = H_i^{-1} s_i, packed by cone + rmm::device_uvector rho; // [K] ||omega_i||^2_J = ||s_i||_J * ||lambda_i||_J + rmm::device_uvector small_cone_ids; // [n_small] cone ids with q <= 32 + rmm::device_uvector medium_cone_ids; // [n_medium] cone ids with 32 < q <= 2048 + rmm::device_uvector large_cone_ids; // [n_large] cone ids with q > 2048 + + cone_data_t(i_t K_in, const std::vector& dims, rmm::cuda_stream_view stream) + : K(K_in), + m_c(std::accumulate(dims.begin(), dims.end(), i_t(0))), + cone_offsets(K_in + 1, stream), + cone_dims(K_in, stream), + s(m_c, stream), + lambda(m_c, stream), + eta(K_in, stream), + inv_eta(K_in, stream), + inv_1pw0(K_in, stream), + w_bar(m_c, stream), + omega(m_c, stream), + rho(K_in, stream), + small_cone_ids(0, stream), + medium_cone_ids(0, stream), + large_cone_ids(0, stream) + { + std::vector offsets(K + 1, 0); + std::vector small_ids; + std::vector medium_ids; + std::vector large_ids; + + for (i_t i = 0; i < K; ++i) { + offsets[i + 1] = offsets[i] + dims[i]; + if (dims[i] <= small_cone_limit) { + small_ids.push_back(i); + } else if (dims[i] <= medium_cone_limit) { + medium_ids.push_back(i); + } else { + large_ids.push_back(i); + } + } + + auto init_device_vec = [&](auto& d_vec, const auto& h_vec) { + if (!h_vec.empty()) { + d_vec.resize(h_vec.size(), stream); + raft::copy(d_vec.data(), h_vec.data(), h_vec.size(), stream); + } + }; + + raft::copy(cone_offsets.data(), offsets.data(), K + 1, stream); + raft::copy(cone_dims.data(), dims.data(), K, stream); + init_device_vec(small_cone_ids, small_ids); + init_device_vec(medium_cone_ids, medium_ids); + init_device_vec(large_cone_ids, large_ids); + } +}; + +template +void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) +{ + auto launch_streaming_bucket = [&](auto& cone_ids, auto block_dim_ic) { + constexpr int block_dim = std::remove_cvref_t::value; + i_t bucket_size = static_cast(cone_ids.size()); + if (bucket_size == 0) return; + + nt_scaling_kernel + <<>>(cones.s.data(), + cones.lambda.data(), + cones.eta.data(), + cones.inv_eta.data(), + cones.inv_1pw0.data(), + cones.w_bar.data(), + cones.omega.data(), + cones.rho.data(), + cones.cone_offsets.data(), + cone_ids.data(), + bucket_size); + }; + + i_t small_count = static_cast(cones.small_cone_ids.size()); + if (small_count > 0) { + constexpr int warps_per_block = small_block_dim / 32; + i_t grid_dim = (small_count + warps_per_block - 1) / warps_per_block; + nt_scaling_small_kernel + <<>>(cones.s.data(), + cones.lambda.data(), + cones.eta.data(), + cones.inv_eta.data(), + cones.inv_1pw0.data(), + cones.w_bar.data(), + cones.omega.data(), + cones.rho.data(), + cones.cone_offsets.data(), + cones.small_cone_ids.data(), + small_count); + } + + launch_streaming_bucket(cones.medium_cone_ids, std::integral_constant{}); + launch_streaming_bucket(cones.large_cone_ids, std::integral_constant{}); +} + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/tests/dual_simplex/CMakeLists.txt b/cpp/tests/dual_simplex/CMakeLists.txt index 253ef95c83..1abeb62ded 100644 --- a/cpp/tests/dual_simplex/CMakeLists.txt +++ b/cpp/tests/dual_simplex/CMakeLists.txt @@ -1,9 +1,10 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on ConfigureTest(DUAL_SIMPLEX_TEST ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/solve.cpp ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/solve_barrier.cu + ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/second_order_cone_test.cu ) diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu new file mode 100644 index 0000000000..2f7d51dcae --- /dev/null +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu @@ -0,0 +1,1280 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +#include +#include + +#include + +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex::test { +namespace { + +template +auto build_offsets(const std::vector& dims) -> std::vector +{ + std::vector offsets(dims.size() + 1, 0); + for (std::size_t i = 0; i < dims.size(); ++i) { + offsets[i + 1] = offsets[i] + dims[i]; + } + return offsets; +} + +template +auto pack_cones(const std::vector>& cones) -> std::vector +{ + std::size_t total_size = 0; + for (const auto& cone : cones) { + total_size += cone.size(); + } + + std::vector packed; + packed.reserve(total_size); + for (const auto& cone : cones) { + packed.insert(packed.end(), cone.begin(), cone.end()); + } + return packed; +} + +template +auto slice_cone(const std::vector& packed, const std::vector& offsets, i_t cone) + -> std::vector +{ + auto begin = packed.begin() + offsets[cone]; + auto end = packed.begin() + offsets[cone + 1]; + return std::vector(begin, end); +} + +template +auto j_norm_sq(const std::vector& u) -> f_t +{ + if (u.empty()) { return f_t(0); } + + f_t tail_sq = f_t(0); + for (std::size_t j = 1; j < u.size(); ++j) { + tail_sq += u[j] * u[j]; + } + return u[0] * u[0] - tail_sq; +} + +template +auto tail_norm(const std::vector& u) -> f_t +{ + f_t tail_sq = f_t(0); + for (std::size_t j = 1; j < u.size(); ++j) { + tail_sq += u[j] * u[j]; + } + return std::sqrt(tail_sq); +} + +template +auto ref_apply_hinv_single(const std::vector& z, + const std::vector& w_bar, + f_t inv_eta, + f_t inv_1pw0) -> std::vector +{ + std::vector out(z.size(), f_t(0)); + if (z.empty()) { return out; } + + f_t zeta = f_t(0); + for (std::size_t j = 1; j < z.size(); ++j) { + zeta += w_bar[j] * z[j]; + } + + f_t coeff = -z[0] + zeta * inv_1pw0; + out[0] = (w_bar[0] * z[0] - zeta) * inv_eta; + for (std::size_t j = 1; j < z.size(); ++j) { + out[j] = (z[j] + coeff * w_bar[j]) * inv_eta; + } + return out; +} + +template +auto ref_apply_H_single(const std::vector& z, + const std::vector& w_bar, + f_t eta, + f_t inv_1pw0) -> std::vector +{ + std::vector out(z.size(), f_t(0)); + if (z.empty()) { return out; } + + f_t zeta = f_t(0); + for (std::size_t j = 1; j < z.size(); ++j) { + zeta += w_bar[j] * z[j]; + } + + f_t coeff = z[0] + zeta * inv_1pw0; + out[0] = (w_bar[0] * z[0] + zeta) * eta; + for (std::size_t j = 1; j < z.size(); ++j) { + out[j] = (z[j] + coeff * w_bar[j]) * eta; + } + return out; +} + +template +auto ref_apply_hinv2_single(const std::vector& v, const std::vector& w_bar, f_t inv_eta) + -> std::vector +{ + std::vector out(v.size(), f_t(0)); + if (v.empty()) { return out; } + + f_t uTv = w_bar[0] * v[0]; + for (std::size_t j = 1; j < v.size(); ++j) { + uTv -= w_bar[j] * v[j]; + } + + f_t ie_sq = inv_eta * inv_eta; + out[0] = ie_sq * (f_t(2) * w_bar[0] * uTv - v[0]); + for (std::size_t j = 1; j < v.size(); ++j) { + out[j] = ie_sq * (-f_t(2) * w_bar[j] * uTv + v[j]); + } + return out; +} + +template +struct nt_scaling_reference_t { + f_t eta{}; + f_t inv_eta{}; + f_t inv_1pw0{}; + f_t rho{}; + std::vector w_bar; + std::vector omega; +}; + +template +auto ref_nt_scaling_single(const std::vector& s, const std::vector& lambda) + -> nt_scaling_reference_t +{ + EXPECT_EQ(s.size(), lambda.size()); + EXPECT_FALSE(s.empty()); + + f_t s_j_norm_sq = j_norm_sq(s); + f_t l_j_norm_sq = j_norm_sq(lambda); + EXPECT_GT(s_j_norm_sq, f_t(0)); + EXPECT_GT(l_j_norm_sq, f_t(0)); + + f_t s_j_norm = std::sqrt(s_j_norm_sq); + f_t l_j_norm = std::sqrt(l_j_norm_sq); + f_t inv_s_j_norm = f_t(1) / s_j_norm; + f_t inv_l_j_norm = f_t(1) / l_j_norm; + + f_t dot_bar = (s[0] * lambda[0]) * inv_s_j_norm * inv_l_j_norm; + for (std::size_t j = 1; j < s.size(); ++j) { + dot_bar += (s[j] * lambda[j]) * inv_s_j_norm * inv_l_j_norm; + } + + f_t gamma = std::sqrt(std::max(f_t(0), (f_t(1) + dot_bar) * f_t(0.5))); + f_t inv_2g = f_t(1) / (f_t(2) * gamma); + + nt_scaling_reference_t ref{}; + ref.eta = std::sqrt(s_j_norm / l_j_norm); + ref.inv_eta = f_t(1) / ref.eta; + ref.rho = s_j_norm * l_j_norm; + ref.w_bar.assign(s.size(), f_t(0)); + + f_t w1_sq = f_t(0); + for (std::size_t j = 1; j < s.size(); ++j) { + ref.w_bar[j] = inv_2g * (s[j] * inv_s_j_norm - lambda[j] * inv_l_j_norm); + w1_sq += ref.w_bar[j] * ref.w_bar[j]; + } + + // Match the kernel's numerical cleanup path for w_bar[0]. + ref.w_bar[0] = std::sqrt(f_t(1) + w1_sq); + ref.inv_1pw0 = f_t(1) / (f_t(1) + ref.w_bar[0]); + ref.omega = ref_apply_hinv_single(s, ref.w_bar, ref.inv_eta, ref.inv_1pw0); + return ref; +} + +template +auto ref_step_length_single(const std::vector& u, const std::vector& du, f_t alpha_max) + -> f_t +{ + EXPECT_EQ(u.size(), du.size()); + EXPECT_FALSE(u.empty()); + + f_t du1_sq = f_t(0); + f_t u1du1 = f_t(0); + f_t u1_sq = f_t(0); + for (std::size_t j = 1; j < u.size(); ++j) { + du1_sq += du[j] * du[j]; + u1du1 += u[j] * du[j]; + u1_sq += u[j] * u[j]; + } + + f_t a = du[0] * du[0] - du1_sq; + f_t b = u[0] * du[0] - u1du1; + f_t c = std::max(f_t(0), u[0] * u[0] - u1_sq); + f_t disc = b * b - a * c; + + f_t alpha = alpha_max; + if (du[0] < f_t(0)) { alpha = std::min(alpha, -u[0] / du[0]); } + + if ((a > f_t(0) && b > f_t(0)) || disc < f_t(0)) { + // No positive root (parabola stays non-negative for alpha > 0). + } else if (a < f_t(0)) { + alpha = std::min(alpha, (b + std::sqrt(std::max(f_t(0), disc))) / (-a)); + } else if (a == f_t(0)) { + if (b < f_t(0)) { alpha = std::min(alpha, c / (f_t(-2) * b)); } + } else if (c == f_t(0)) { + alpha = (a >= f_t(0)) ? alpha : f_t(0); + } else if (b < f_t(0) && disc > f_t(0)) { + alpha = std::min(alpha, (-b - std::sqrt(disc)) / a); + } + + return alpha; +} + +template +auto ref_jordan_product_single(const std::vector& a, const std::vector& b) + -> std::vector +{ + EXPECT_EQ(a.size(), b.size()); + std::vector out(a.size(), f_t(0)); + if (a.empty()) { return out; } + + f_t dot = f_t(0); + for (std::size_t j = 0; j < a.size(); ++j) { + dot += a[j] * b[j]; + } + out[0] = dot; + for (std::size_t j = 1; j < a.size(); ++j) { + out[j] = a[0] * b[j] + b[0] * a[j]; + } + return out; +} + +template +auto ref_inverse_jordan_product_single(const std::vector& omega, + const std::vector& r, + f_t rho_val) -> std::vector +{ + EXPECT_EQ(omega.size(), r.size()); + std::vector out(omega.size(), f_t(0)); + if (omega.empty()) { return out; } + + f_t nu = f_t(0); + for (std::size_t j = 1; j < omega.size(); ++j) { + nu += omega[j] * r[j]; + } + + f_t inv_rho = f_t(1) / rho_val; + f_t omega_0 = omega[0]; + out[0] = (omega_0 * r[0] - nu) * inv_rho; + + f_t c_omega = ((nu / omega_0) - r[0]) * inv_rho; + f_t c_r = f_t(1) / omega_0; + for (std::size_t j = 1; j < omega.size(); ++j) { + out[j] = c_omega * omega[j] + c_r * r[j]; + } + return out; +} + +template +auto ref_fused_corrector_single(const std::vector& dx_aff, + const std::vector& omega, + const std::vector& w_bar, + f_t inv_eta, + f_t inv_1pw0, + f_t rho_val, + f_t sigma_mu) -> std::vector +{ + auto dx = ref_apply_hinv_single(dx_aff, w_bar, inv_eta, inv_1pw0); + + std::vector dz(dx.size()); + for (std::size_t j = 0; j < dx.size(); ++j) { + dz[j] = -omega[j] - dx[j]; + } + + auto r_K_1 = ref_jordan_product_single(omega, omega); + auto r_K_2 = ref_jordan_product_single(dx, dz); + + std::vector r_K(dx.size()); + for (std::size_t j = 0; j < dx.size(); ++j) { + r_K[j] = r_K_1[j] + r_K_2[j]; + } + r_K[0] -= sigma_mu; + + auto corr = ref_inverse_jordan_product_single(omega, r_K, rho_val); + return ref_apply_hinv_single(corr, w_bar, inv_eta, inv_1pw0); +} + +template +auto ref_interior_shift_single(std::vector u) -> std::vector +{ + if (u.empty()) { return u; } + + f_t gap = tail_norm(u) - u[0]; + if (gap >= f_t(0)) { u[0] += f_t(1) + gap; } + return u; +} + +template +auto make_patterned_cone(int q, f_t head, f_t scale) -> std::vector +{ + std::vector cone(q, f_t(0)); + cone[0] = head; + for (int j = 1; j < q; ++j) { + f_t sign = (j % 2 == 0) ? f_t(1) : f_t(-1); + cone[j] = sign * scale * static_cast((j % 7) + 1); + } + return cone; +} + +} // namespace + +class second_order_cone_test : public ::testing::Test { + protected: + using i_t = int; + using f_t = double; + static constexpr int dim = 256; + + raft::handle_t handle_; + rmm::cuda_stream_view stream_ = handle_.get_stream(); + + template + auto make_device_vector(const std::vector& host) -> rmm::device_uvector + { + rmm::device_uvector device(host.size(), stream_); + if (!host.empty()) { raft::copy(device.data(), host.data(), host.size(), stream_); } + sync(); + return device; + } + + template + auto copy_to_host(const rmm::device_uvector& device) -> std::vector + { + std::vector host(device.size()); + if (!host.empty()) { raft::copy(host.data(), device.data(), host.size(), stream_); } + sync(); + return host; + } + + template + void copy_to_device(rmm::device_uvector& device, const std::vector& host) + { + ASSERT_EQ(device.size(), host.size()); + if (!host.empty()) { raft::copy(device.data(), host.data(), host.size(), stream_); } + sync(); + } + + void sync() { RAFT_CUDA_TRY(cudaStreamSynchronize(stream_.value())); } + + template + void expect_vector_near(const std::vector& actual, + const std::vector& expected, + t_t atol, + t_t rtol, + const char* label) + { + ASSERT_EQ(actual.size(), expected.size()) << label << " size mismatch"; + for (std::size_t i = 0; i < actual.size(); ++i) { + EXPECT_NEAR(actual[i], expected[i], atol + rtol * std::abs(expected[i])) + << label << "[" << i << "]"; + } + } + + void launch_apply_hinv(const rmm::device_uvector& z, + rmm::device_uvector& out, + const rmm::device_uvector& w_bar, + const rmm::device_uvector& inv_eta, + const rmm::device_uvector& inv_1pw0, + const rmm::device_uvector& cone_offsets, + i_t k) + { + apply_Hinv_kernel<<>>( + z.data(), out.data(), w_bar.data(), inv_eta.data(), inv_1pw0.data(), cone_offsets.data(), k); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + sync(); + } + + void launch_step_length(const rmm::device_uvector& s, + const rmm::device_uvector& ds, + const rmm::device_uvector& lambda, + const rmm::device_uvector& dlambda, + rmm::device_uvector& alpha, + const rmm::device_uvector& cone_offsets, + i_t k, + f_t alpha_max) + { + step_length_kernel<<>>(s.data(), + ds.data(), + lambda.data(), + dlambda.data(), + alpha.data(), + cone_offsets.data(), + k, + alpha_max); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + sync(); + } + + void launch_interior_shift(rmm::device_uvector& u, + const rmm::device_uvector& cone_offsets, + i_t k) + { + interior_shift_kernel<<>>(u.data(), cone_offsets.data(), k); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + sync(); + } + + void launch_apply_hinv2(const rmm::device_uvector& v, + rmm::device_uvector& out, + const rmm::device_uvector& w_bar, + const rmm::device_uvector& inv_eta, + const rmm::device_uvector& cone_offsets, + i_t k) + { + apply_Hinv2_kernel<<>>( + v.data(), out.data(), w_bar.data(), inv_eta.data(), cone_offsets.data(), k); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + sync(); + } + + void launch_jordan_product(const rmm::device_uvector& a, + const rmm::device_uvector& b, + rmm::device_uvector& out, + const rmm::device_uvector& cone_offsets, + i_t k) + { + jordan_product_kernel + <<>>(a.data(), b.data(), out.data(), cone_offsets.data(), k); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + sync(); + } + + void launch_inverse_jordan_product(const rmm::device_uvector& omega, + const rmm::device_uvector& r, + const rmm::device_uvector& rho, + rmm::device_uvector& out, + const rmm::device_uvector& cone_offsets, + i_t k) + { + inverse_jordan_product_kernel<<>>( + omega.data(), r.data(), rho.data(), out.data(), cone_offsets.data(), k); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + sync(); + } + + void launch_fused_corrector(const rmm::device_uvector& dx_aff, + const rmm::device_uvector& omega, + const rmm::device_uvector& w_bar, + const rmm::device_uvector& inv_eta, + const rmm::device_uvector& inv_1pw0, + const rmm::device_uvector& rho, + f_t sigma_mu, + rmm::device_uvector& out, + const rmm::device_uvector& cone_offsets, + i_t k) + { + fused_corrector_kernel<<>>(dx_aff.data(), + omega.data(), + w_bar.data(), + inv_eta.data(), + inv_1pw0.data(), + rho.data(), + sigma_mu, + out.data(), + cone_offsets.data(), + k); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + sync(); + } +}; + +TEST_F(second_order_cone_test, cone_data_topology_and_bucket_partitioning) +{ + std::vector dims{1, 32, 33, 2048, 2049}; + cone_data_t cones(static_cast(dims.size()), dims, stream_); + + auto expected_offsets = build_offsets(dims); + auto actual_offsets = copy_to_host(cones.cone_offsets); + auto actual_dims = copy_to_host(cones.cone_dims); + auto small_ids = copy_to_host(cones.small_cone_ids); + auto medium_ids = copy_to_host(cones.medium_cone_ids); + auto large_ids = copy_to_host(cones.large_cone_ids); + + EXPECT_EQ(cones.K, static_cast(dims.size())); + EXPECT_EQ(cones.m_c, expected_offsets.back()); + EXPECT_EQ(actual_offsets, expected_offsets); + EXPECT_EQ(actual_dims, dims); + EXPECT_EQ(small_ids, std::vector({0, 1})); + EXPECT_EQ(medium_ids, std::vector({2, 3})); + EXPECT_EQ(large_ids, std::vector({4})); +} + +TEST_F(second_order_cone_test, nt_scaling_matches_reference_for_small_cone) +{ + // Borrowed from the Clarabel regression input, but checked against our own + // host-side NT formulas. + std::vector> s_cones{{1.5, 0.3, 0.4}}; + std::vector> lambda_cones{{2.0, 0.5, 0.5}}; + std::vector dims{3}; + + cone_data_t cones(1, dims, stream_); + copy_to_device(cones.s, pack_cones(s_cones)); + copy_to_device(cones.lambda, pack_cones(lambda_cones)); + + launch_nt_scaling(cones, stream_); + + auto eta = copy_to_host(cones.eta); + auto inv_eta = copy_to_host(cones.inv_eta); + auto inv_1pw0 = copy_to_host(cones.inv_1pw0); + auto rho = copy_to_host(cones.rho); + auto w_bar = copy_to_host(cones.w_bar); + auto omega = copy_to_host(cones.omega); + + auto ref = ref_nt_scaling_single(s_cones[0], lambda_cones[0]); + + EXPECT_NEAR(eta[0], ref.eta, 1e-12); + EXPECT_NEAR(inv_eta[0], ref.inv_eta, 1e-12); + EXPECT_NEAR(inv_1pw0[0], ref.inv_1pw0, 1e-12); + EXPECT_NEAR(rho[0], ref.rho, 1e-12); + expect_vector_near(w_bar, ref.w_bar, 1e-12, 1e-10, "w_bar"); + expect_vector_near(omega, ref.omega, 1e-12, 1e-10, "omega"); + + EXPECT_NEAR(j_norm_sq(w_bar), f_t(1), 1e-12); + EXPECT_NEAR(j_norm_sq(omega), rho[0], 1e-12); + + auto omega_from_apply_hinv = ref_apply_hinv_single(s_cones[0], w_bar, inv_eta[0], inv_1pw0[0]); + expect_vector_near(omega, omega_from_apply_hinv, 1e-12, 1e-10, "omega_consistency"); +} + +TEST_F(second_order_cone_test, nt_scaling_matches_reference_across_bucket_sizes) +{ + std::vector> s_cones{ + {2.0}, make_patterned_cone(33, 4.0, 0.01), make_patterned_cone(2049, 5.0, 0.001)}; + std::vector> lambda_cones{ + {0.5}, make_patterned_cone(33, 3.0, 0.0075), make_patterned_cone(2049, 4.0, 0.00075)}; + std::vector dims{1, 33, 2049}; + auto offsets = build_offsets(dims); + + cone_data_t cones(static_cast(dims.size()), dims, stream_); + copy_to_device(cones.s, pack_cones(s_cones)); + copy_to_device(cones.lambda, pack_cones(lambda_cones)); + + launch_nt_scaling(cones, stream_); + + auto eta = copy_to_host(cones.eta); + auto inv_eta = copy_to_host(cones.inv_eta); + auto inv_1pw0 = copy_to_host(cones.inv_1pw0); + auto rho = copy_to_host(cones.rho); + auto w_bar = copy_to_host(cones.w_bar); + auto omega = copy_to_host(cones.omega); + + for (i_t cone = 0; cone < static_cast(dims.size()); ++cone) { + auto ref = ref_nt_scaling_single(s_cones[cone], lambda_cones[cone]); + + EXPECT_NEAR(eta[cone], ref.eta, 1e-10) << "cone " << cone; + EXPECT_NEAR(inv_eta[cone], ref.inv_eta, 1e-10) << "cone " << cone; + EXPECT_NEAR(inv_1pw0[cone], ref.inv_1pw0, 1e-10) << "cone " << cone; + EXPECT_NEAR(rho[cone], ref.rho, 1e-10) << "cone " << cone; + + auto actual_w_bar = slice_cone(w_bar, offsets, cone); + auto actual_omega = slice_cone(omega, offsets, cone); + expect_vector_near(actual_w_bar, ref.w_bar, 1e-10, 1e-8, "w_bar"); + expect_vector_near(actual_omega, ref.omega, 1e-10, 1e-8, "omega"); + + EXPECT_NEAR(j_norm_sq(actual_w_bar), f_t(1), 1e-10) << "cone " << cone; + EXPECT_NEAR(j_norm_sq(actual_omega), rho[cone], 1e-10) << "cone " << cone; + } +} + +TEST_F(second_order_cone_test, nt_scaling_omega_equals_H_times_lambda) +{ + std::vector> s_cones{{5.0, 1.0, -1.0, 0.5, 0.3}}; + std::vector> lambda_cones{{4.0, 0.5, 1.0, -0.3, 0.2}}; + std::vector dims{5}; + + cone_data_t cones(1, dims, stream_); + copy_to_device(cones.s, pack_cones(s_cones)); + copy_to_device(cones.lambda, pack_cones(lambda_cones)); + + launch_nt_scaling(cones, stream_); + + auto eta = copy_to_host(cones.eta); + auto inv_1pw0 = copy_to_host(cones.inv_1pw0); + auto w_bar = copy_to_host(cones.w_bar); + auto omega = copy_to_host(cones.omega); + + // NT symmetry: omega should equal both H^{-1}s and H*lambda. + auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, eta[0], inv_1pw0[0]); + expect_vector_near(omega, H_lambda, 1e-10, 1e-8, "omega_vs_H_lambda"); +} + +TEST_F(second_order_cone_test, nt_scaling_near_boundary_is_stable) +{ + // s and lambda barely inside the cone: ||tail||^2 ≈ head^2. + std::vector> s_cones{{1.00002, 0.6, 0.8, 1e-4, -2e-4}}; + std::vector> lambda_cones{{1.000015, 0.8, 0.6, -3e-5, 2e-5}}; + std::vector dims{5}; + + cone_data_t cones(1, dims, stream_); + copy_to_device(cones.s, pack_cones(s_cones)); + copy_to_device(cones.lambda, pack_cones(lambda_cones)); + + launch_nt_scaling(cones, stream_); + + auto eta = copy_to_host(cones.eta); + auto inv_eta = copy_to_host(cones.inv_eta); + auto inv_1pw0 = copy_to_host(cones.inv_1pw0); + auto w_bar = copy_to_host(cones.w_bar); + auto omega = copy_to_host(cones.omega); + + EXPECT_NEAR(j_norm_sq(w_bar), f_t(1), 1e-8) << "w_bar J-norm not 1 near boundary"; + EXPECT_GT(w_bar[0], tail_norm(w_bar)) << "w_bar not interior near boundary"; + + // Round-trip: H(omega) should equal s. + auto H_omega = ref_apply_H_single(omega, w_bar, eta[0], inv_1pw0[0]); + expect_vector_near(H_omega, pack_cones(s_cones), 1e-8, 1e-6, "H_omega_vs_s_near_boundary"); + + // Symmetry: omega should also equal H*lambda. + auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, eta[0], inv_1pw0[0]); + expect_vector_near(omega, H_lambda, 1e-8, 1e-6, "omega_vs_H_lambda_near_boundary"); +} + +TEST_F(second_order_cone_test, apply_hinv_matches_reference_for_packed_cones) +{ + std::vector dims{1, 3, 5}; + auto offsets = build_offsets(dims); + + std::vector> z_cones{{3.0}, {2.0, -1.0, 0.5}, {1.0, 0.25, -0.75, 0.5, -0.125}}; + std::vector> w_bar_cones{ + {1.0}, {0.0, 0.15, -0.05}, {0.0, 0.10, -0.20, 0.05, 0.15}}; + std::vector inv_eta_host{0.5, 1.25, 0.75}; + std::vector inv_1pw0_host(inv_eta_host.size(), 0.0); + + for (std::size_t cone = 0; cone < w_bar_cones.size(); ++cone) { + f_t w1_sq = f_t(0); + for (std::size_t j = 1; j < w_bar_cones[cone].size(); ++j) { + w1_sq += w_bar_cones[cone][j] * w_bar_cones[cone][j]; + } + w_bar_cones[cone][0] = std::sqrt(f_t(1) + w1_sq); + inv_1pw0_host[cone] = f_t(1) / (f_t(1) + w_bar_cones[cone][0]); + } + + auto z = make_device_vector(pack_cones(z_cones)); + auto w_bar = make_device_vector(pack_cones(w_bar_cones)); + auto inv_eta = make_device_vector(inv_eta_host); + auto inv_1pw0 = make_device_vector(inv_1pw0_host); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector out(z.size(), stream_); + + launch_apply_hinv(z, out, w_bar, inv_eta, inv_1pw0, d_offsets, static_cast(dims.size())); + + auto actual_out = copy_to_host(out); + auto expected = pack_cones(std::vector>{ + ref_apply_hinv_single(z_cones[0], w_bar_cones[0], inv_eta_host[0], inv_1pw0_host[0]), + ref_apply_hinv_single(z_cones[1], w_bar_cones[1], inv_eta_host[1], inv_1pw0_host[1]), + ref_apply_hinv_single(z_cones[2], w_bar_cones[2], inv_eta_host[2], inv_1pw0_host[2])}); + + expect_vector_near(actual_out, expected, 1e-12, 1e-10, "apply_hinv"); +} + +TEST_F(second_order_cone_test, step_length_matches_reference_and_handles_q1) +{ + std::vector dims{1, 3}; + auto offsets = build_offsets(dims); + + std::vector> s_cones{{2.0}, {5.0, 1.0, 1.0}}; + std::vector> ds_cones{{-3.0}, {-0.5, 0.1, 0.1}}; + std::vector> lambda_cones{{5.0}, {5.0, 1.0, 1.0}}; + std::vector> dlambda_cones{{1.0}, {-0.5, 0.1, 0.1}}; + f_t alpha_max = 10.0; + + auto s = make_device_vector(pack_cones(s_cones)); + auto ds = make_device_vector(pack_cones(ds_cones)); + auto lambda = make_device_vector(pack_cones(lambda_cones)); + auto dlambda = make_device_vector(pack_cones(dlambda_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector alpha(dims.size(), stream_); + + launch_step_length( + s, ds, lambda, dlambda, alpha, d_offsets, static_cast(dims.size()), alpha_max); + + auto actual_alpha = copy_to_host(alpha); + std::vector expected_alpha(dims.size(), alpha_max); + for (std::size_t cone = 0; cone < dims.size(); ++cone) { + expected_alpha[cone] = + std::min(ref_step_length_single(s_cones[cone], ds_cones[cone], alpha_max), + ref_step_length_single(lambda_cones[cone], dlambda_cones[cone], alpha_max)); + } + + expect_vector_near(actual_alpha, expected_alpha, 1e-12, 1e-10, "step_length"); + EXPECT_NEAR(actual_alpha[0], 2.0 / 3.0, 1e-12); + EXPECT_NEAR(actual_alpha[1], 5.5903758157691508, 1e-10); +} + +TEST_F(second_order_cone_test, step_length_matches_reference_for_large_cone) +{ + std::vector dims{513}; + auto offsets = build_offsets(dims); + + std::vector> s_cones{{make_patterned_cone(dims[0], 5.0, 0.01)}}; + std::vector> ds_cones{{make_patterned_cone(dims[0], -0.25, 0.002)}}; + std::vector> lambda_cones{{make_patterned_cone(dims[0], 6.0, 0.009)}}; + std::vector> dlambda_cones{{make_patterned_cone(dims[0], -0.15, 0.0015)}}; + f_t alpha_max = 20.0; + + auto s = make_device_vector(pack_cones(s_cones)); + auto ds = make_device_vector(pack_cones(ds_cones)); + auto lambda = make_device_vector(pack_cones(lambda_cones)); + auto dlambda = make_device_vector(pack_cones(dlambda_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector alpha(dims.size(), stream_); + + launch_step_length( + s, ds, lambda, dlambda, alpha, d_offsets, static_cast(dims.size()), alpha_max); + + auto actual_alpha = copy_to_host(alpha); + std::vector expected_alpha(dims.size(), alpha_max); + for (std::size_t cone = 0; cone < dims.size(); ++cone) { + expected_alpha[cone] = + std::min(ref_step_length_single(s_cones[cone], ds_cones[cone], alpha_max), + ref_step_length_single(lambda_cones[cone], dlambda_cones[cone], alpha_max)); + } + + expect_vector_near(actual_alpha, expected_alpha, 1e-12, 1e-10, "step_length_large"); + EXPECT_GT(actual_alpha[0], 0.0); + EXPECT_LT(actual_alpha[0], alpha_max); +} + +TEST_F(second_order_cone_test, step_length_boundary_c_zero_matches_clarabel_branch) +{ + std::vector dims{3}; + auto offsets = build_offsets(dims); + + // Boundary point: c = u^T J u = 1^2 - 1^2 - 0^2 = 0. + // Direction: a = du^T J du = 1^2 - 1^2 - 1^2 = -1 < 0. + // Clarabel's c == 0 branch returns 0 in this case because the direction + // leaves the cone immediately. + std::vector> s_cones{{1.0, 1.0, 0.0}}; + std::vector> ds_cones{{1.0, 1.0, 1.0}}; + std::vector> lambda_cones{{1.0, 1.0, 0.0}}; + std::vector> dlambda_cones{{1.0, 1.0, 1.0}}; + f_t alpha_max = 10.0; + + auto s = make_device_vector(pack_cones(s_cones)); + auto ds = make_device_vector(pack_cones(ds_cones)); + auto lambda = make_device_vector(pack_cones(lambda_cones)); + auto dlambda = make_device_vector(pack_cones(dlambda_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector alpha(dims.size(), stream_); + + launch_step_length( + s, ds, lambda, dlambda, alpha, d_offsets, static_cast(dims.size()), alpha_max); + + auto actual_alpha = copy_to_host(alpha); + ASSERT_EQ(actual_alpha.size(), 1); + EXPECT_EQ(actual_alpha[0], 0.0); +} + +TEST_F(second_order_cone_test, step_length_degenerate_a_zero) +{ + std::vector dims{2}; + auto offsets = build_offsets(dims); + + // u=(2,0), du=(-1,1): a = du_0^2 - du_1^2 = 1 - 1 = 0 (degenerate quadratic). + // Linear constraint: alpha <= 2. Degenerate branch: alpha = c/(-2b) = 4/2 = 2. + // But the linear constraint also gives alpha <= 2, so result is min(2, 2) = 2... + // Actually b = u0*du0 - u1*du1 = 2*(-1) - 0 = -2, c = u0^2 - u1^2 = 4. + // Degenerate: alpha = c/(-2b) = 4/4 = 1. And linear: alpha <= -u0/du0 = 2. + // So alpha = 1. + std::vector> s_cones{{2.0, 0.0}}; + std::vector> ds_cones{{-1.0, 1.0}}; + std::vector> lambda_cones{{5.0, 0.0}}; + std::vector> dlambda_cones{{0.0, 0.0}}; + + auto s = make_device_vector(pack_cones(s_cones)); + auto ds = make_device_vector(pack_cones(ds_cones)); + auto lambda = make_device_vector(pack_cones(lambda_cones)); + auto dlambda = make_device_vector(pack_cones(dlambda_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector alpha(1, stream_); + + launch_step_length(s, ds, lambda, dlambda, alpha, d_offsets, 1, 10.0); + + auto actual = copy_to_host(alpha); + EXPECT_NEAR(actual[0], 1.0, 1e-14); +} + +TEST_F(second_order_cone_test, step_length_safe_direction_returns_alpha_max) +{ + std::vector dims{3}; + auto offsets = build_offsets(dims); + + // Interior point with direction along the identity element — stays in cone forever. + std::vector> s_cones{{10.0, 0.0, 0.0}}; + std::vector> ds_cones{{1.0, 0.0, 0.0}}; + std::vector> lambda_cones{{10.0, 0.0, 0.0}}; + std::vector> dlambda_cones{{0.0, 0.1, 0.0}}; + + auto s = make_device_vector(pack_cones(s_cones)); + auto ds = make_device_vector(pack_cones(ds_cones)); + auto lambda = make_device_vector(pack_cones(lambda_cones)); + auto dlambda = make_device_vector(pack_cones(dlambda_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector alpha(1, stream_); + + launch_step_length(s, ds, lambda, dlambda, alpha, d_offsets, 1, 1.0); + + auto actual = copy_to_host(alpha); + EXPECT_DOUBLE_EQ(actual[0], 1.0); +} + +TEST_F(second_order_cone_test, step_length_boundary_tightness) +{ + std::vector dims{5}; + auto offsets = build_offsets(dims); + + std::vector> s_cones{{4.0, 1.0, -1.0, 0.5, 0.3}}; + std::vector> ds_cones{{-2.0, 1.0, 0.5, -0.3, 0.1}}; + std::vector> lambda_cones{{5.0, 0.5, 1.0, -0.3, 0.2}}; + std::vector> dlambda_cones{{-1.0, 2.0, 1.0, -0.5, 0.4}}; + + auto s = make_device_vector(pack_cones(s_cones)); + auto ds = make_device_vector(pack_cones(ds_cones)); + auto lambda = make_device_vector(pack_cones(lambda_cones)); + auto dlambda = make_device_vector(pack_cones(dlambda_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector alpha(1, stream_); + + launch_step_length(s, ds, lambda, dlambda, alpha, d_offsets, 1, 100.0); + + auto a = copy_to_host(alpha)[0]; + ASSERT_GT(a, 0.0); + + // At alpha, at least one of (s, lambda) should be on the cone boundary. + auto s_bnd = s_cones[0]; + auto l_bnd = lambda_cones[0]; + for (std::size_t j = 0; j < s_bnd.size(); ++j) { + s_bnd[j] += a * ds_cones[0][j]; + l_bnd[j] += a * dlambda_cones[0][j]; + } + f_t res_s = j_norm_sq(s_bnd); + f_t res_l = j_norm_sq(l_bnd); + EXPECT_GE(res_s, -1e-10) << "s left the cone"; + EXPECT_GE(res_l, -1e-10) << "lambda left the cone"; + EXPECT_NEAR(std::min(res_s, res_l), 0.0, 1e-10) << "neither hit the boundary"; + + // At (1 − ε) α, both should be strictly interior. + f_t a_int = a * (1.0 - 1e-8); + auto s_int = s_cones[0]; + auto l_int = lambda_cones[0]; + for (std::size_t j = 0; j < s_int.size(); ++j) { + s_int[j] += a_int * ds_cones[0][j]; + l_int[j] += a_int * dlambda_cones[0][j]; + } + EXPECT_GT(j_norm_sq(s_int), 0.0) << "s not interior at (1-eps)*alpha"; + EXPECT_GT(j_norm_sq(l_int), 0.0) << "lambda not interior at (1-eps)*alpha"; +} + +TEST_F(second_order_cone_test, interior_shift_matches_reference_and_preserves_tail) +{ + std::vector dims{1, 3, 4}; + auto offsets = build_offsets(dims); + + std::vector> cones{{-0.25}, {2.0, 0.3, 0.4}, {0.5, 0.6, 0.8, 0.0}}; + auto packed = pack_cones(cones); + auto expected = pack_cones(std::vector>{ref_interior_shift_single(cones[0]), + ref_interior_shift_single(cones[1]), + ref_interior_shift_single(cones[2])}); + + auto u = make_device_vector(packed); + auto d_offsets = make_device_vector(offsets); + launch_interior_shift(u, d_offsets, static_cast(dims.size())); + + auto actual = copy_to_host(u); + expect_vector_near(actual, expected, 1e-12, 1e-10, "interior_shift"); + + for (std::size_t cone = 0; cone < dims.size(); ++cone) { + auto shifted = slice_cone(actual, offsets, static_cast(cone)); + EXPECT_GT(shifted[0], tail_norm(shifted)); + for (std::size_t j = 1; j < shifted.size(); ++j) { + EXPECT_EQ(shifted[j], cones[cone][j]) << "cone " << cone << " tail " << j; + } + } +} + +TEST_F(second_order_cone_test, apply_hinv2_matches_reference_for_packed_cones) +{ + std::vector dims{1, 3, 5}; + auto offsets = build_offsets(dims); + + std::vector> v_cones{{3.0}, {2.0, -1.0, 0.5}, {1.0, 0.25, -0.75, 0.5, -0.125}}; + std::vector> w_bar_cones{ + {1.0}, {0.0, 0.15, -0.05}, {0.0, 0.10, -0.20, 0.05, 0.15}}; + std::vector inv_eta_host{0.5, 1.25, 0.75}; + + for (std::size_t cone = 0; cone < w_bar_cones.size(); ++cone) { + f_t w1_sq = f_t(0); + for (std::size_t j = 1; j < w_bar_cones[cone].size(); ++j) { + w1_sq += w_bar_cones[cone][j] * w_bar_cones[cone][j]; + } + w_bar_cones[cone][0] = std::sqrt(f_t(1) + w1_sq); + } + + auto v = make_device_vector(pack_cones(v_cones)); + auto w_bar = make_device_vector(pack_cones(w_bar_cones)); + auto inv_eta = make_device_vector(inv_eta_host); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector out(v.size(), stream_); + + launch_apply_hinv2(v, out, w_bar, inv_eta, d_offsets, static_cast(dims.size())); + + auto actual = copy_to_host(out); + auto expected = pack_cones(std::vector>{ + ref_apply_hinv2_single(v_cones[0], w_bar_cones[0], inv_eta_host[0]), + ref_apply_hinv2_single(v_cones[1], w_bar_cones[1], inv_eta_host[1]), + ref_apply_hinv2_single(v_cones[2], w_bar_cones[2], inv_eta_host[2])}); + + expect_vector_near(actual, expected, 1e-12, 1e-10, "apply_hinv2"); +} + +TEST_F(second_order_cone_test, apply_hinv2_equals_double_hinv_with_nt_scaling) +{ + std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; + std::vector dims{3, 5}; + auto offsets = build_offsets(dims); + + cone_data_t cones(static_cast(dims.size()), dims, stream_); + copy_to_device(cones.s, pack_cones(s_cones)); + copy_to_device(cones.lambda, pack_cones(lambda_cones)); + + launch_nt_scaling(cones, stream_); + + std::vector> v_cones{{1.0, -0.3, 0.2}, {0.5, 0.1, -0.15, 0.25, -0.1}}; + auto d_v = make_device_vector(pack_cones(v_cones)); + auto d_offsets = make_device_vector(offsets); + + // H^{-2} v (single kernel) + rmm::device_uvector d_hinv2(cones.omega.size(), stream_); + launch_apply_hinv2( + d_v, d_hinv2, cones.w_bar, cones.inv_eta, d_offsets, static_cast(dims.size())); + + // H^{-1}(H^{-1} v) (two passes) + rmm::device_uvector d_tmp(cones.omega.size(), stream_); + rmm::device_uvector d_double(cones.omega.size(), stream_); + launch_apply_hinv(d_v, + d_tmp, + cones.w_bar, + cones.inv_eta, + cones.inv_1pw0, + d_offsets, + static_cast(dims.size())); + launch_apply_hinv(d_tmp, + d_double, + cones.w_bar, + cones.inv_eta, + cones.inv_1pw0, + d_offsets, + static_cast(dims.size())); + + auto hinv2_actual = copy_to_host(d_hinv2); + auto double_actual = copy_to_host(d_double); + expect_vector_near(hinv2_actual, double_actual, 1e-10, 1e-8, "hinv2_vs_double_hinv"); +} + +TEST_F(second_order_cone_test, apply_hinv2_strided_loop_for_large_cone) +{ + std::vector dims{513}; + auto offsets = build_offsets(dims); + + auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); + auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); + + cone_data_t cones(1, dims, stream_); + copy_to_device(cones.s, s_cone); + copy_to_device(cones.lambda, lambda_cone); + + launch_nt_scaling(cones, stream_); + + auto v_cone = make_patterned_cone(dims[0], 3.0, 0.006); + auto d_v = make_device_vector(v_cone); + auto d_offsets = make_device_vector(offsets); + + // Direct H^{-2} apply + rmm::device_uvector d_hinv2(cones.omega.size(), stream_); + launch_apply_hinv2(d_v, d_hinv2, cones.w_bar, cones.inv_eta, d_offsets, 1); + + // Reference: two H^{-1} passes + rmm::device_uvector d_tmp(cones.omega.size(), stream_); + rmm::device_uvector d_double(cones.omega.size(), stream_); + launch_apply_hinv(d_v, d_tmp, cones.w_bar, cones.inv_eta, cones.inv_1pw0, d_offsets, 1); + launch_apply_hinv(d_tmp, d_double, cones.w_bar, cones.inv_eta, cones.inv_1pw0, d_offsets, 1); + + auto hinv2_actual = copy_to_host(d_hinv2); + auto double_actual = copy_to_host(d_double); + expect_vector_near(hinv2_actual, double_actual, 1e-8, 1e-6, "hinv2_large"); + + // Also check against CPU reference + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_host = copy_to_host(cones.inv_eta); + auto ref = ref_apply_hinv2_single(v_cone, w_bar_host, inv_eta_host[0]); + expect_vector_near(hinv2_actual, ref, 1e-8, 1e-6, "hinv2_large_ref"); +} + +TEST_F(second_order_cone_test, jordan_product_matches_reference_for_packed_cones) +{ + std::vector dims{1, 3, 4}; + auto offsets = build_offsets(dims); + + std::vector> a_cones{{2.0}, {2.0, 1.0, -0.5}, {3.0, 0.25, -0.75, 0.5}}; + std::vector> b_cones{{4.0}, {1.5, -0.5, 0.25}, {2.0, -0.25, 0.5, 1.0}}; + + auto a = make_device_vector(pack_cones(a_cones)); + auto b = make_device_vector(pack_cones(b_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector out(a.size(), stream_); + + launch_jordan_product(a, b, out, d_offsets, static_cast(dims.size())); + + auto actual = copy_to_host(out); + auto expected = + pack_cones(std::vector>{ref_jordan_product_single(a_cones[0], b_cones[0]), + ref_jordan_product_single(a_cones[1], b_cones[1]), + ref_jordan_product_single(a_cones[2], b_cones[2])}); + + expect_vector_near(actual, expected, 1e-12, 1e-10, "jordan_product"); +} + +TEST_F(second_order_cone_test, inverse_jordan_product_matches_reference_and_identity) +{ + std::vector dims{1, 3, 5}; + auto offsets = build_offsets(dims); + + std::vector> omega_cones{ + {2.0}, {2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> r_cones{{4.0}, {1.0, -0.25, 0.5}, {2.0, 0.5, -0.25, 0.25, 0.75}}; + + std::vector rho_host; + for (const auto& w : omega_cones) { + rho_host.push_back(j_norm_sq(w)); + } + + auto d_omega = make_device_vector(pack_cones(omega_cones)); + auto d_r = make_device_vector(pack_cones(r_cones)); + auto d_rho = make_device_vector(rho_host); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector d_out(d_omega.size(), stream_); + + launch_inverse_jordan_product( + d_omega, d_r, d_rho, d_out, d_offsets, static_cast(dims.size())); + + auto actual = copy_to_host(d_out); + auto expected = pack_cones(std::vector>{ + ref_inverse_jordan_product_single(omega_cones[0], r_cones[0], rho_host[0]), + ref_inverse_jordan_product_single(omega_cones[1], r_cones[1], rho_host[1]), + ref_inverse_jordan_product_single(omega_cones[2], r_cones[2], rho_host[2])}); + + expect_vector_near(actual, expected, 1e-12, 1e-10, "inverse_jordan_product"); + + // Identity check: omega circ (omega \ r) = r + auto d_inv = make_device_vector(actual); + rmm::device_uvector d_roundtrip(d_omega.size(), stream_); + launch_jordan_product(d_omega, d_inv, d_roundtrip, d_offsets, static_cast(dims.size())); + + auto roundtrip = copy_to_host(d_roundtrip); + auto r_packed = pack_cones(r_cones); + expect_vector_near(roundtrip, r_packed, 1e-10, 1e-8, "inverse_identity"); +} + +TEST_F(second_order_cone_test, jordan_and_inverse_jordan_strided_loop_for_large_cone) +{ + std::vector dims{513}; + auto offsets = build_offsets(dims); + + auto a_cone = make_patterned_cone(dims[0], 5.0, 0.005); + auto b_cone = make_patterned_cone(dims[0], 4.0, 0.004); + auto omega_cone = make_patterned_cone(dims[0], 6.0, 0.003); + f_t rho_val = j_norm_sq(omega_cone); + ASSERT_GT(rho_val, 0.0); + + std::vector> a_cones{a_cone}; + std::vector> b_cones{b_cone}; + std::vector> omega_cones{omega_cone}; + + auto d_a = make_device_vector(pack_cones(a_cones)); + auto d_b = make_device_vector(pack_cones(b_cones)); + auto d_omega = make_device_vector(pack_cones(omega_cones)); + auto d_rho = make_device_vector(std::vector{rho_val}); + auto d_offsets = make_device_vector(offsets); + + // Jordan product: strided path + rmm::device_uvector d_jp(d_a.size(), stream_); + launch_jordan_product(d_a, d_b, d_jp, d_offsets, 1); + auto jp_actual = copy_to_host(d_jp); + auto jp_expected = ref_jordan_product_single(a_cone, b_cone); + expect_vector_near(jp_actual, jp_expected, 1e-10, 1e-8, "jordan_large"); + + // Inverse Jordan product: strided path + identity + auto r_cone = make_patterned_cone(dims[0], 3.0, 0.006); + std::vector> r_cones{r_cone}; + + auto d_r = make_device_vector(pack_cones(r_cones)); + rmm::device_uvector d_inv(d_omega.size(), stream_); + launch_inverse_jordan_product(d_omega, d_r, d_rho, d_inv, d_offsets, 1); + + auto inv_actual = copy_to_host(d_inv); + auto inv_expected = ref_inverse_jordan_product_single(omega_cone, r_cone, rho_val); + expect_vector_near(inv_actual, inv_expected, 1e-10, 1e-8, "inv_jordan_large"); + + // Round-trip identity on the large cone + auto d_inv_vec = make_device_vector(inv_actual); + rmm::device_uvector d_rt(d_omega.size(), stream_); + launch_jordan_product(d_omega, d_inv_vec, d_rt, d_offsets, 1); + auto rt_actual = copy_to_host(d_rt); + expect_vector_near(rt_actual, r_cone, 1e-8, 1e-6, "identity_large"); +} + +TEST_F(second_order_cone_test, inverse_jordan_product_with_nt_scaling_rho) +{ + std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; + std::vector dims{3, 5}; + auto offsets = build_offsets(dims); + + cone_data_t cones(static_cast(dims.size()), dims, stream_); + copy_to_device(cones.s, pack_cones(s_cones)); + copy_to_device(cones.lambda, pack_cones(lambda_cones)); + + launch_nt_scaling(cones, stream_); + + auto omega_host = copy_to_host(cones.omega); + auto rho_host = copy_to_host(cones.rho); + + // Build an arbitrary r vector, run inverse Jordan with NT-produced rho/omega + std::vector> r_cones{{1.0, -0.3, 0.2}, {0.5, 0.1, -0.15, 0.25, -0.1}}; + auto d_r = make_device_vector(pack_cones(r_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector d_out(cones.omega.size(), stream_); + + launch_inverse_jordan_product( + cones.omega, d_r, cones.rho, d_out, d_offsets, static_cast(dims.size())); + + auto inv_actual = copy_to_host(d_out); + + // Verify host reference matches using NT-produced values + for (i_t c = 0; c < static_cast(dims.size()); ++c) { + auto omega_c = slice_cone(omega_host, offsets, c); + auto r_c = r_cones[c]; + auto ref = ref_inverse_jordan_product_single(omega_c, r_c, rho_host[c]); + auto actual = slice_cone(inv_actual, offsets, c); + expect_vector_near(actual, ref, 1e-10, 1e-8, "nt_rho_inv_jordan"); + } + + // Round-trip identity with NT-produced omega + rmm::device_uvector d_rt(cones.omega.size(), stream_); + auto d_inv = make_device_vector(inv_actual); + launch_jordan_product(cones.omega, d_inv, d_rt, d_offsets, static_cast(dims.size())); + auto rt_actual = copy_to_host(d_rt); + auto r_packed = pack_cones(r_cones); + expect_vector_near(rt_actual, r_packed, 1e-8, 1e-6, "nt_identity"); +} + +TEST_F(second_order_cone_test, fused_corrector_matches_reference_with_nt_scaling) +{ + std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; + std::vector dims{3, 5}; + auto offsets = build_offsets(dims); + + cone_data_t cones(static_cast(dims.size()), dims, stream_); + copy_to_device(cones.s, pack_cones(s_cones)); + copy_to_device(cones.lambda, pack_cones(lambda_cones)); + + launch_nt_scaling(cones, stream_); + + std::vector> dx_aff_cones{{0.3, -0.1, 0.2}, {-0.5, 0.2, 0.1, -0.3, 0.15}}; + f_t sigma_mu = 0.1; + + auto d_dx_aff = make_device_vector(pack_cones(dx_aff_cones)); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector d_out(cones.omega.size(), stream_); + + launch_fused_corrector(d_dx_aff, + cones.omega, + cones.w_bar, + cones.inv_eta, + cones.inv_1pw0, + cones.rho, + sigma_mu, + d_out, + d_offsets, + static_cast(dims.size())); + + auto actual = copy_to_host(d_out); + auto omega_host = copy_to_host(cones.omega); + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + auto inv_1pw0_h = copy_to_host(cones.inv_1pw0); + auto rho_h = copy_to_host(cones.rho); + + for (i_t c = 0; c < static_cast(dims.size()); ++c) { + auto ref = ref_fused_corrector_single(dx_aff_cones[c], + slice_cone(omega_host, offsets, c), + slice_cone(w_bar_host, offsets, c), + inv_eta_h[c], + inv_1pw0_h[c], + rho_h[c], + sigma_mu); + auto act = slice_cone(actual, offsets, c); + expect_vector_near(act, ref, 1e-10, 1e-8, "fused_corrector"); + } +} + +TEST_F(second_order_cone_test, fused_corrector_strided_loop_for_large_cone) +{ + std::vector dims{513}; + auto offsets = build_offsets(dims); + + auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); + auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); + + cone_data_t cones(1, dims, stream_); + copy_to_device(cones.s, s_cone); + copy_to_device(cones.lambda, lambda_cone); + + launch_nt_scaling(cones, stream_); + + auto dx_aff_cone = make_patterned_cone(dims[0], 0.5, 0.003); + f_t sigma_mu = 0.25; + + auto d_dx_aff = make_device_vector(dx_aff_cone); + auto d_offsets = make_device_vector(offsets); + rmm::device_uvector d_out(cones.omega.size(), stream_); + + launch_fused_corrector(d_dx_aff, + cones.omega, + cones.w_bar, + cones.inv_eta, + cones.inv_1pw0, + cones.rho, + sigma_mu, + d_out, + d_offsets, + 1); + + auto actual = copy_to_host(d_out); + auto omega_host = copy_to_host(cones.omega); + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + auto inv_1pw0_h = copy_to_host(cones.inv_1pw0); + auto rho_h = copy_to_host(cones.rho); + + auto ref = ref_fused_corrector_single( + dx_aff_cone, omega_host, w_bar_host, inv_eta_h[0], inv_1pw0_h[0], rho_h[0], sigma_mu); + expect_vector_near(actual, ref, 1e-8, 1e-6, "fused_corrector_large"); +} + +} // namespace cuopt::linear_programming::dual_simplex::test From 9ee0ed76ba5d482d8e25b314d296b53b5be20a2f Mon Sep 17 00:00:00 2001 From: Yan Zaretskiy Date: Tue, 7 Apr 2026 12:26:17 -0700 Subject: [PATCH 02/68] feat(barrier): integrate H^-2 cone blocks into the augmented system Wire the SOCP cone Hessian updates into the augmented solve path and fold in the follow-up cleanup that removes now-redundant kernel plumbing. --- cpp/src/barrier/barrier.cu | 117 +++++- cpp/src/barrier/second_order_cone.cuh | 322 ++++++++++------ .../unit_tests/second_order_cone_test.cu | 360 +++++++++++++++--- 3 files changed, 625 insertions(+), 174 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index 902e691e64..bb848e6037 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,7 @@ #include #include +#include #include #include @@ -163,6 +165,8 @@ class iteration_data_t { d_inv_diag(lp.num_cols, lp.handle_ptr->get_stream()), d_cols_to_remove(0, lp.handle_ptr->get_stream()), d_augmented_diagonal_indices_(0, lp.handle_ptr->get_stream()), + d_cone_csr_indices_(0, lp.handle_ptr->get_stream()), + d_cone_Q_values_(0, lp.handle_ptr->get_stream()), use_augmented(false), has_factorization(false), num_factorizations(0), @@ -321,6 +325,11 @@ class iteration_data_t { use_augmented = !Q_diagonal; } + if (cones_.has_value() && !use_augmented) { + n_dense_columns = 0; + use_augmented = true; + } + if (use_augmented) { settings.log.printf("Linear system : augmented\n"); } else { @@ -427,16 +436,87 @@ class iteration_data_t { i_t factorization_size = n + m; const f_t dual_perturb = 0.0; const f_t primal_perturb = 1e-6; + + const bool has_cones = cones_.has_value() && cones_->K > 0; + const i_t m_c = has_cones ? cones_->m_c : 0; + i_t total_block_nnz = 0; + + std::vector cone_offsets_host; + std::vector cone_block_offsets_host; + if (has_cones) { + cone_offsets_host.resize(cones_->K + 1); + cone_block_offsets_host.resize(cones_->K + 1); + raft::copy( + cone_offsets_host.data(), cones_->cone_offsets.data(), cones_->K + 1, stream_view_); + raft::copy( + cone_block_offsets_host.data(), cones_->block_offsets.data(), cones_->K + 1, stream_view_); + handle_ptr->sync_stream(); + total_block_nnz = cone_block_offsets_host[cones_->K]; + } + if (first_call) { - i_t new_nnz = 2 * nnzA + n + m + nnzQ; + i_t new_nnz = 2 * nnzA + n + m + nnzQ + total_block_nnz; csr_matrix_t augmented_CSR(n + m, n + m, new_nnz); std::vector augmented_diagonal_indices(n + m, -1); + std::vector cone_csr_indices_host(total_block_nnz, -1); + std::vector cone_Q_values_host(total_block_nnz, f_t(0)); i_t q = 0; i_t off_diag_Qnz = 0; for (i_t i = 0; i < n; i++) { augmented_CSR.row_start[i] = q; - if (nnzQ == 0) { + + const bool is_cone_row = has_cones && i >= cone_var_start_ && i < cone_var_start_ + m_c; + + if (is_cone_row) { + // Determine which cone this variable belongs to and its local row + i_t local_idx = i - cone_var_start_; + i_t k = 0; + while (k + 1 < cones_->K && cone_offsets_host[k + 1] <= local_idx) { + k++; + } + i_t local_r = local_idx - cone_offsets_host[k]; + i_t q_k = cone_offsets_host[k + 1] - cone_offsets_host[k]; + i_t cone_col_start = cone_var_start_ + cone_offsets_host[k]; + i_t block_base = cone_block_offsets_host[k] + local_r * q_k; + + // Merge-join: Q entries (sorted) with dense cone block columns (contiguous) + i_t qp = (nnzQ > 0) ? Q.col_start[i] : 0; + i_t q_end = (nnzQ > 0) ? Q.col_start[i + 1] : 0; + + // Q entries before cone block + while (qp < q_end && Q.i[qp] < cone_col_start) { + augmented_CSR.j[q] = Q.i[qp]; + augmented_CSR.x[q++] = -Q.x[qp]; + off_diag_Qnz++; + qp++; + } + + // Dense cone block, absorbing any Q entries that fall inside + for (i_t c = 0; c < q_k; c++) { + i_t col = cone_col_start + c; + f_t q_val = (c == local_r) ? dual_perturb : f_t(0); + + if (qp < q_end && Q.i[qp] == col) { + q_val += Q.x[qp]; + qp++; + } + + cone_csr_indices_host[block_base + c] = q; + cone_Q_values_host[block_base + c] = q_val; + if (col == i) { augmented_diagonal_indices[i] = q; } + augmented_CSR.j[q] = col; + augmented_CSR.x[q++] = f_t(0); + } + + // Q entries after cone block + while (qp < q_end) { + augmented_CSR.j[q] = Q.i[qp]; + augmented_CSR.x[q++] = -Q.x[qp]; + off_diag_Qnz++; + qp++; + } + } else if (nnzQ == 0) { augmented_diagonal_indices[i] = q; augmented_CSR.j[q] = i; augmented_CSR.x[q++] = -diag[i] - dual_perturb; @@ -489,8 +569,9 @@ class iteration_data_t { augmented_CSR.nz_max = q; augmented_CSR.j.resize(q); augmented_CSR.x.resize(q); - settings_.log.debug("augmented nz %d predicted %d\n", q, off_diag_Qnz + nnzA + n); - cuopt_assert(q == 2 * nnzA + n + m + off_diag_Qnz, "augmented nnz != predicted"); + i_t expected_nnz = 2 * nnzA + (n - m_c) + total_block_nnz + m + off_diag_Qnz; + settings_.log.debug("augmented nz %d predicted %d\n", q, expected_nnz); + cuopt_assert(q == expected_nnz, "augmented nnz != predicted"); cuopt_assert(A.col_start[n] == AT.col_start[m], "A nz != AT nz"); device_augmented.copy(augmented_CSR, handle_ptr->get_stream()); @@ -500,6 +581,20 @@ class iteration_data_t { augmented_diagonal_indices.data(), augmented_diagonal_indices.size(), handle_ptr->get_stream()); + + if (has_cones) { + d_cone_csr_indices_.resize(total_block_nnz, handle_ptr->get_stream()); + raft::copy(d_cone_csr_indices_.data(), + cone_csr_indices_host.data(), + total_block_nnz, + handle_ptr->get_stream()); + d_cone_Q_values_.resize(total_block_nnz, handle_ptr->get_stream()); + raft::copy(d_cone_Q_values_.data(), + cone_Q_values_host.data(), + total_block_nnz, + handle_ptr->get_stream()); + } + handle_ptr->sync_stream(); #ifdef CHECK_SYMMETRY csc_matrix_t augmented_transpose(1, 1, 1); @@ -538,6 +633,15 @@ class iteration_data_t { span_x[span_diag_indices[j]] = primal_perturb_value; }); RAFT_CHECK_CUDA(handle_ptr->get_stream()); + + if (has_cones) { + scatter_hinv2_into_augmented(*cones_, + device_augmented.x, + d_cone_csr_indices_, + d_cone_Q_values_, + handle_ptr->get_stream()); + RAFT_CHECK_CUDA(handle_ptr->get_stream()); + } } } @@ -1530,9 +1634,14 @@ class iteration_data_t { std::vector Qdiag; bool Q_diagonal; rmm::device_uvector d_augmented_diagonal_indices_; + rmm::device_uvector d_cone_csr_indices_; + rmm::device_uvector d_cone_Q_values_; bool indefinite_Q; cusparse_view_t cusparse_Q_view_; + std::optional> cones_; + i_t cone_var_start_ = 0; + bool use_augmented; i_t symbolic_status; diff --git a/cpp/src/barrier/second_order_cone.cuh b/cpp/src/barrier/second_order_cone.cuh index eb337b9ce1..ca23ed39cc 100644 --- a/cpp/src/barrier/second_order_cone.cuh +++ b/cpp/src/barrier/second_order_cone.cuh @@ -12,12 +12,18 @@ #include +#include #include #include #include +#include + +#include +#include +#include #include #include #include @@ -135,24 +141,25 @@ DI triplet_t reduce_broadcast(triplet_t val, smem_warp_reduce_t -__global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv_kernel(const f_t* __restrict__ z, - f_t* __restrict__ out, - const f_t* __restrict__ w_bar, - const f_t* __restrict__ inv_eta, - const f_t* __restrict__ inv_1pw0, - const i_t* __restrict__ cone_offsets, - i_t K) +__global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv_kernel( + raft::device_span z, + raft::device_span out, + raft::device_span w_bar, + raft::device_span inv_eta, + raft::device_span inv_1pw0, + raft::device_span cone_offsets, + i_t K) { __shared__ smem_reduce_t smem; i_t cone = static_cast(blockIdx.x); if (cone >= K) return; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - const f_t* w_cone = w_bar + off; - const f_t* z_cone = z + off; - f_t* out_cone = out + off; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + auto w_cone = w_bar.subspan(off, q); + auto z_cone = z.subspan(off, q); + auto out_cone = out.subspan(off, q); f_t z0 = z_cone[0]; f_t w0 = w_cone[0]; @@ -184,11 +191,11 @@ __global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv_kernel(const f_t* __rest // --------------------------------------------------------------------------- template __global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv2_kernel( - const f_t* __restrict__ v, - f_t* __restrict__ out, - const f_t* __restrict__ w_bar, - const f_t* __restrict__ inv_eta, - const i_t* __restrict__ cone_offsets, + raft::device_span v, + raft::device_span out, + raft::device_span w_bar, + raft::device_span inv_eta, + raft::device_span cone_offsets, i_t K) { __shared__ smem_reduce_t smem; @@ -196,11 +203,11 @@ __global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv2_kernel( i_t cone = static_cast(blockIdx.x); if (cone >= K) return; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - const f_t* w_cone = w_bar + off; - const f_t* v_cone = v + off; - f_t* out_cone = out + off; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + auto w_cone = w_bar.subspan(off, q); + auto v_cone = v.subspan(off, q); + auto out_cone = out.subspan(off, q); f_t v0 = v_cone[0]; f_t w0 = w_cone[0]; @@ -237,10 +244,10 @@ __global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv2_kernel( // --------------------------------------------------------------------------- template __global__ __launch_bounds__(BLOCK_DIM) void jordan_product_kernel( - const f_t* __restrict__ a, - const f_t* __restrict__ b, - f_t* __restrict__ out, - const i_t* __restrict__ cone_offsets, + raft::device_span a, + raft::device_span b, + raft::device_span out, + raft::device_span cone_offsets, i_t K) { __shared__ smem_reduce_t smem; @@ -248,11 +255,11 @@ __global__ __launch_bounds__(BLOCK_DIM) void jordan_product_kernel( i_t cone = static_cast(blockIdx.x); if (cone >= K) return; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - const f_t* a_cone = a + off; - const f_t* b_cone = b + off; - f_t* out_cone = out + off; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + auto a_cone = a.subspan(off, q); + auto b_cone = b.subspan(off, q); + auto out_cone = out.subspan(off, q); f_t a0 = a_cone[0]; f_t b0 = b_cone[0]; @@ -280,11 +287,11 @@ __global__ __launch_bounds__(BLOCK_DIM) void jordan_product_kernel( // --------------------------------------------------------------------------- template __global__ __launch_bounds__(BLOCK_DIM) void inverse_jordan_product_kernel( - const f_t* __restrict__ omega, - const f_t* __restrict__ r, - const f_t* __restrict__ rho, - f_t* __restrict__ out, - const i_t* __restrict__ cone_offsets, + raft::device_span omega, + raft::device_span r, + raft::device_span rho, + raft::device_span out, + raft::device_span cone_offsets, i_t K) { __shared__ smem_reduce_t smem; @@ -292,11 +299,11 @@ __global__ __launch_bounds__(BLOCK_DIM) void inverse_jordan_product_kernel( i_t cone = static_cast(blockIdx.x); if (cone >= K) return; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - const f_t* omega_cone = omega + off; - const f_t* r_cone = r + off; - f_t* out_cone = out + off; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + auto omega_cone = omega.subspan(off, q); + auto r_cone = r.subspan(off, q); + auto out_cone = out.subspan(off, q); f_t omega_0 = omega_cone[0]; f_t r_0 = r_cone[0]; @@ -337,15 +344,15 @@ __global__ __launch_bounds__(BLOCK_DIM) void inverse_jordan_product_kernel( // --------------------------------------------------------------------------- template __global__ __launch_bounds__(BLOCK_DIM) void fused_corrector_kernel( - const f_t* __restrict__ dx_aff, - const f_t* __restrict__ omega, - const f_t* __restrict__ w_bar, - const f_t* __restrict__ inv_eta, - const f_t* __restrict__ inv_1pw0, - const f_t* __restrict__ rho, + raft::device_span dx_aff, + raft::device_span omega, + raft::device_span w_bar, + raft::device_span inv_eta, + raft::device_span inv_1pw0, + raft::device_span rho, f_t sigma_mu, - f_t* __restrict__ out, - const i_t* __restrict__ cone_offsets, + raft::device_span out, + raft::device_span cone_offsets, i_t K) { __shared__ smem_reduce_t smem; @@ -353,12 +360,12 @@ __global__ __launch_bounds__(BLOCK_DIM) void fused_corrector_kernel( i_t cone = static_cast(blockIdx.x); if (cone >= K) return; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - const f_t* dx_a = dx_aff + off; - const f_t* omega_cone = omega + off; - const f_t* w_cone = w_bar + off; - f_t* out_cone = out + off; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + auto dx_a = dx_aff.subspan(off, q); + auto omega_cone = omega.subspan(off, q); + auto w_cone = w_bar.subspan(off, q); + auto out_cone = out.subspan(off, q); f_t ie = inv_eta[cone]; f_t ipw = inv_1pw0[cone]; @@ -473,17 +480,18 @@ struct nt_warp_storage { }; template -__global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_kernel(const f_t* __restrict__ s, - const f_t* __restrict__ lambda, - f_t* __restrict__ eta, - f_t* __restrict__ inv_eta, - f_t* __restrict__ inv_1pw0, - f_t* __restrict__ w_bar, - f_t* __restrict__ omega, - f_t* __restrict__ rho, - const i_t* __restrict__ cone_offsets, - const i_t* __restrict__ cone_ids, - i_t num_cones) +__global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_kernel( + raft::device_span s, + raft::device_span lambda, + raft::device_span eta, + raft::device_span inv_eta, + raft::device_span inv_1pw0, + raft::device_span w_bar, + raft::device_span omega, + raft::device_span rho, + raft::device_span cone_offsets, + raft::device_span cone_ids, + i_t num_cones) { static_assert(BLOCK_DIM % 32 == 0, "NT scaling kernel requires warp-aligned BLOCK_DIM"); __shared__ nt_block_storage storage; @@ -571,16 +579,16 @@ __global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_kernel(const f_t* __rest template __global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_small_kernel( - const f_t* __restrict__ s, - const f_t* __restrict__ lambda, - f_t* __restrict__ eta, - f_t* __restrict__ inv_eta, - f_t* __restrict__ inv_1pw0, - f_t* __restrict__ w_bar, - f_t* __restrict__ omega, - f_t* __restrict__ rho, - const i_t* __restrict__ cone_offsets, - const i_t* __restrict__ cone_ids, + raft::device_span s, + raft::device_span lambda, + raft::device_span eta, + raft::device_span inv_eta, + raft::device_span inv_1pw0, + raft::device_span w_bar, + raft::device_span omega, + raft::device_span rho, + raft::device_span cone_offsets, + raft::device_span cone_ids, i_t num_cones) { static_assert(BLOCK_DIM % 32 == 0, "Small-cone NT kernel requires warp-aligned CTAs"); @@ -671,12 +679,12 @@ __global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_small_kernel( // --------------------------------------------------------------------------- template DI f_t -cone_step_length_single(const f_t* __restrict__ u, - const f_t* __restrict__ du, - i_t q, +cone_step_length_single(raft::device_span u, + raft::device_span du, typename block_reduce_t, BLOCK_DIM>::TempStorage& temp, f_t alpha) { + i_t q = static_cast(u.size()); auto partial = triplet_t{}; auto& [du1_sq_p, u1du1_p, u1_sq_p] = partial; for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { @@ -731,12 +739,12 @@ cone_step_length_single(const f_t* __restrict__ u, // --------------------------------------------------------------------------- template __global__ __launch_bounds__(BLOCK_DIM) void step_length_kernel( - const f_t* __restrict__ s, - const f_t* __restrict__ ds, - const f_t* __restrict__ lambda, - const f_t* __restrict__ dlambda, - f_t* __restrict__ alpha, - const i_t* __restrict__ cone_offsets, + raft::device_span s, + raft::device_span ds, + raft::device_span lambda, + raft::device_span dlambda, + raft::device_span alpha, + raft::device_span cone_offsets, i_t K, f_t alpha_max) { @@ -748,10 +756,10 @@ __global__ __launch_bounds__(BLOCK_DIM) void step_length_kernel( i_t off = cone_offsets[cone]; i_t q = cone_offsets[cone + 1] - off; - f_t alpha_s = - cone_step_length_single(s + off, ds + off, q, temp_storage, alpha_max); + f_t alpha_s = cone_step_length_single( + s.subspan(off, q), ds.subspan(off, q), temp_storage, alpha_max); f_t alpha_l = cone_step_length_single( - lambda + off, dlambda + off, q, temp_storage, alpha_max); + lambda.subspan(off, q), dlambda.subspan(off, q), temp_storage, alpha_max); if (threadIdx.x == 0) { alpha[cone] = min(alpha_s, alpha_l); } } @@ -766,7 +774,7 @@ __global__ __launch_bounds__(BLOCK_DIM) void step_length_kernel( // --------------------------------------------------------------------------- template __global__ __launch_bounds__(BLOCK_DIM) void interior_shift_kernel( - f_t* __restrict__ u, const i_t* __restrict__ cone_offsets, i_t K) + raft::device_span u, raft::device_span cone_offsets, i_t K) { __shared__ typename block_reduce_t::TempStorage temp_storage; @@ -791,11 +799,15 @@ __global__ __launch_bounds__(BLOCK_DIM) void interior_shift_kernel( } /** - * Owns device storage for second-order cone topology, iterates, and NT scaling. + * Device storage for second-order cone topology, NT scaling, and iterate views. * * Flat arrays are packed by cone: elements [cone_offsets[i], cone_offsets[i+1]) * belong to cone i, which has dimension cone_dims[i]. * + * Primal/dual iterates (s, lambda) are non-owning spans, pre-sliced by the + * caller to cover the cone portion of the global x/z vectors. The caller + * must keep the underlying memory alive. + * * Search directions, RHS vectors, and workspace live directly in * iteration_data_t (matching the existing LP/QP pattern where dx_aff, dual_rhs, * etc. are all top-level fields of iteration_data_t). @@ -806,12 +818,13 @@ struct cone_data_t { i_t K; // number of second-order cones i_t m_c; // total cone dimension = sum of cone_dims - rmm::device_uvector cone_offsets; // [K+1] prefix sums of cone_dims - rmm::device_uvector cone_dims; // [K] dimension q_i of each cone + rmm::device_uvector cone_offsets; // [K+1] prefix sums of cone_dims + rmm::device_uvector cone_dims; // [K] dimension q_i of each cone + rmm::device_uvector block_offsets; // [K+1] prefix sums of q_i^2 (for dense block build) - // --- Primal/dual cone iterates (rewritten each iteration) --- - rmm::device_uvector s; // [m_c] cone slack: s_i in int(Q^{q_i}) - rmm::device_uvector lambda; // [m_c] cone dual: lambda_i in int(Q^{q_i}) + // --- Primal/dual cone iterates (non-owning views, set by caller) --- + raft::device_span s; // [m_c] cone slack: s_i in int(Q^{q_i}) + raft::device_span lambda; // [m_c] cone dual: lambda_i in int(Q^{q_i}) // --- NT scaling state (recomputed each iteration from s, lambda) --- rmm::device_uvector eta; // [K] scaling factor eta_i = (||s_i||_J / ||lambda_i||_J)^{1/2} @@ -824,13 +837,18 @@ struct cone_data_t { rmm::device_uvector medium_cone_ids; // [n_medium] cone ids with 32 < q <= 2048 rmm::device_uvector large_cone_ids; // [n_large] cone ids with q > 2048 - cone_data_t(i_t K_in, const std::vector& dims, rmm::cuda_stream_view stream) + cone_data_t(i_t K_in, + const std::vector& dims, + raft::device_span s_in, + raft::device_span lambda_in, + rmm::cuda_stream_view stream) : K(K_in), m_c(std::accumulate(dims.begin(), dims.end(), i_t(0))), cone_offsets(K_in + 1, stream), cone_dims(K_in, stream), - s(m_c, stream), - lambda(m_c, stream), + block_offsets(K_in + 1, stream), + s(s_in), + lambda(lambda_in), eta(K_in, stream), inv_eta(K_in, stream), inv_1pw0(K_in, stream), @@ -842,12 +860,14 @@ struct cone_data_t { large_cone_ids(0, stream) { std::vector offsets(K + 1, 0); + std::vector blk_offsets(K + 1, 0); std::vector small_ids; std::vector medium_ids; std::vector large_ids; for (i_t i = 0; i < K; ++i) { - offsets[i + 1] = offsets[i] + dims[i]; + offsets[i + 1] = offsets[i] + dims[i]; + blk_offsets[i + 1] = blk_offsets[i] + dims[i] * dims[i]; if (dims[i] <= small_cone_limit) { small_ids.push_back(i); } else if (dims[i] <= medium_cone_limit) { @@ -866,12 +886,76 @@ struct cone_data_t { raft::copy(cone_offsets.data(), offsets.data(), K + 1, stream); raft::copy(cone_dims.data(), dims.data(), K, stream); + raft::copy(block_offsets.data(), blk_offsets.data(), K + 1, stream); init_device_vec(small_cone_ids, small_ids); init_device_vec(medium_cone_ids, medium_ids); init_device_vec(large_cone_ids, large_ids); } }; +// --------------------------------------------------------------------------- +// Compute flat H^{-2} cone-block entries and scatter them into the augmented +// CSR value array. +// +// The caller provides one flat entry per dense cone-block element: +// - `csr_indices[e]` gives the destination slot in `augmented_x` +// - `q_values[e]` stores any pre-merged Q contribution for that slot +// +// For each flat entry we identify its owning cone from `block_offsets`, +// recover local (r, c) coordinates, evaluate H_k^{-2}(r, c), and scatter +// -(H_k^{-2}(r, c) + q_values[e]) +// into `augmented_x[csr_indices[e]]`. +// --------------------------------------------------------------------------- +template +void scatter_hinv2_into_augmented(const cone_data_t& cones, + rmm::device_uvector& augmented_x, + const rmm::device_uvector& csr_indices, + const rmm::device_uvector& q_values, + rmm::cuda_stream_view stream) +{ + i_t count = static_cast(csr_indices.size()); + if (count == 0) return; + + auto values = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_w_bar = cuopt::make_span(cones.w_bar), + span_inv_eta = cuopt::make_span(cones.inv_eta), + span_block_offsets = cuopt::make_span(cones.block_offsets), + span_cone_offsets = cuopt::make_span(cones.cone_offsets), + span_q_values = cuopt::make_span(q_values)] __device__(i_t e) -> f_t { + i_t lo = 0; + i_t hi = static_cast(span_block_offsets.size()) - 1; + while (lo + 1 < hi) { + i_t mid = lo + (hi - lo) / 2; + if (span_block_offsets[mid] <= e) { + lo = mid; + } else { + hi = mid; + } + } + i_t cone = lo; + i_t off = span_cone_offsets[cone]; + i_t q = span_cone_offsets[cone + 1] - off; + i_t blk_off = span_block_offsets[cone]; + i_t local = e - blk_off; + i_t r = local / q; + i_t c = local % q; + + f_t ie_sq = span_inv_eta[cone] * span_inv_eta[cone]; + f_t w0 = span_w_bar[off]; + f_t u_r = (r == 0) ? w0 : -span_w_bar[off + r]; + f_t u_c = (c == 0) ? w0 : -span_w_bar[off + c]; + f_t val = f_t(2) * u_r * ie_sq * u_c; + f_t diag_correction = (r == 0) ? -ie_sq : ie_sq; + if (r == c) { val += diag_correction; } + + return -val - span_q_values[e]; + }); + + thrust::scatter( + rmm::exec_policy(stream), values, values + count, csr_indices.begin(), augmented_x.begin()); +} + template void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) { @@ -881,16 +965,16 @@ void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view strea if (bucket_size == 0) return; nt_scaling_kernel - <<>>(cones.s.data(), - cones.lambda.data(), - cones.eta.data(), - cones.inv_eta.data(), - cones.inv_1pw0.data(), - cones.w_bar.data(), - cones.omega.data(), - cones.rho.data(), - cones.cone_offsets.data(), - cone_ids.data(), + <<>>(cones.s, + cones.lambda, + cuopt::make_span(cones.eta), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.inv_1pw0), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.omega), + cuopt::make_span(cones.rho), + cuopt::make_span(cones.cone_offsets), + cuopt::make_span(cone_ids), bucket_size); }; @@ -899,16 +983,16 @@ void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view strea constexpr int warps_per_block = small_block_dim / 32; i_t grid_dim = (small_count + warps_per_block - 1) / warps_per_block; nt_scaling_small_kernel - <<>>(cones.s.data(), - cones.lambda.data(), - cones.eta.data(), - cones.inv_eta.data(), - cones.inv_1pw0.data(), - cones.w_bar.data(), - cones.omega.data(), - cones.rho.data(), - cones.cone_offsets.data(), - cones.small_cone_ids.data(), + <<>>(cones.s, + cones.lambda, + cuopt::make_span(cones.eta), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.inv_1pw0), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.omega), + cuopt::make_span(cones.rho), + cuopt::make_span(cones.cone_offsets), + cuopt::make_span(cones.small_cone_ids), small_count); } diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu index 2f7d51dcae..611a236562 100644 --- a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu @@ -121,6 +121,24 @@ auto ref_apply_H_single(const std::vector& z, return out; } +template +auto ref_build_hinv2_block_single(const std::vector& w_bar, f_t inv_eta) -> std::vector +{ + std::size_t q = w_bar.size(); + std::vector block(q * q, f_t(0)); + f_t ie_sq = inv_eta * inv_eta; + + for (std::size_t r = 0; r < q; ++r) { + f_t u_r = (r == 0) ? w_bar[0] : -w_bar[r]; + for (std::size_t c = 0; c < q; ++c) { + f_t u_c = (c == 0) ? w_bar[0] : -w_bar[c]; + f_t j_rc = (r == c) ? ((r == 0) ? f_t(1) : f_t(-1)) : f_t(0); + block[r * q + c] = ie_sq * (f_t(2) * u_r * u_c - j_rc); + } + } + return block; +} + template auto ref_apply_hinv2_single(const std::vector& v, const std::vector& w_bar, f_t inv_eta) -> std::vector @@ -391,8 +409,13 @@ class second_order_cone_test : public ::testing::Test { const rmm::device_uvector& cone_offsets, i_t k) { - apply_Hinv_kernel<<>>( - z.data(), out.data(), w_bar.data(), inv_eta.data(), inv_1pw0.data(), cone_offsets.data(), k); + apply_Hinv_kernel<<>>(cuopt::make_span(z), + cuopt::make_span(out), + cuopt::make_span(w_bar), + cuopt::make_span(inv_eta), + cuopt::make_span(inv_1pw0), + cuopt::make_span(cone_offsets), + k); RAFT_CUDA_TRY(cudaPeekAtLastError()); sync(); } @@ -406,12 +429,12 @@ class second_order_cone_test : public ::testing::Test { i_t k, f_t alpha_max) { - step_length_kernel<<>>(s.data(), - ds.data(), - lambda.data(), - dlambda.data(), - alpha.data(), - cone_offsets.data(), + step_length_kernel<<>>(cuopt::make_span(s), + cuopt::make_span(ds), + cuopt::make_span(lambda), + cuopt::make_span(dlambda), + cuopt::make_span(alpha), + cuopt::make_span(cone_offsets), k, alpha_max); RAFT_CUDA_TRY(cudaPeekAtLastError()); @@ -422,7 +445,8 @@ class second_order_cone_test : public ::testing::Test { const rmm::device_uvector& cone_offsets, i_t k) { - interior_shift_kernel<<>>(u.data(), cone_offsets.data(), k); + interior_shift_kernel + <<>>(cuopt::make_span(u), cuopt::make_span(cone_offsets), k); RAFT_CUDA_TRY(cudaPeekAtLastError()); sync(); } @@ -434,8 +458,12 @@ class second_order_cone_test : public ::testing::Test { const rmm::device_uvector& cone_offsets, i_t k) { - apply_Hinv2_kernel<<>>( - v.data(), out.data(), w_bar.data(), inv_eta.data(), cone_offsets.data(), k); + apply_Hinv2_kernel<<>>(cuopt::make_span(v), + cuopt::make_span(out), + cuopt::make_span(w_bar), + cuopt::make_span(inv_eta), + cuopt::make_span(cone_offsets), + k); RAFT_CUDA_TRY(cudaPeekAtLastError()); sync(); } @@ -446,8 +474,11 @@ class second_order_cone_test : public ::testing::Test { const rmm::device_uvector& cone_offsets, i_t k) { - jordan_product_kernel - <<>>(a.data(), b.data(), out.data(), cone_offsets.data(), k); + jordan_product_kernel<<>>(cuopt::make_span(a), + cuopt::make_span(b), + cuopt::make_span(out), + cuopt::make_span(cone_offsets), + k); RAFT_CUDA_TRY(cudaPeekAtLastError()); sync(); } @@ -459,8 +490,13 @@ class second_order_cone_test : public ::testing::Test { const rmm::device_uvector& cone_offsets, i_t k) { - inverse_jordan_product_kernel<<>>( - omega.data(), r.data(), rho.data(), out.data(), cone_offsets.data(), k); + inverse_jordan_product_kernel + <<>>(cuopt::make_span(omega), + cuopt::make_span(r), + cuopt::make_span(rho), + cuopt::make_span(out), + cuopt::make_span(cone_offsets), + k); RAFT_CUDA_TRY(cudaPeekAtLastError()); sync(); } @@ -476,25 +512,34 @@ class second_order_cone_test : public ::testing::Test { const rmm::device_uvector& cone_offsets, i_t k) { - fused_corrector_kernel<<>>(dx_aff.data(), - omega.data(), - w_bar.data(), - inv_eta.data(), - inv_1pw0.data(), - rho.data(), + fused_corrector_kernel<<>>(cuopt::make_span(dx_aff), + cuopt::make_span(omega), + cuopt::make_span(w_bar), + cuopt::make_span(inv_eta), + cuopt::make_span(inv_1pw0), + cuopt::make_span(rho), sigma_mu, - out.data(), - cone_offsets.data(), + cuopt::make_span(out), + cuopt::make_span(cone_offsets), k); RAFT_CUDA_TRY(cudaPeekAtLastError()); sync(); } + + void launch_cone_block_scatter(const cone_data_t& cones, + rmm::device_uvector& aug_x, + const rmm::device_uvector& csr_indices, + const rmm::device_uvector& q_values) + { + scatter_hinv2_into_augmented(cones, aug_x, csr_indices, q_values, stream_); + sync(); + } }; TEST_F(second_order_cone_test, cone_data_topology_and_bucket_partitioning) { std::vector dims{1, 32, 33, 2048, 2049}; - cone_data_t cones(static_cast(dims.size()), dims, stream_); + cone_data_t cones(static_cast(dims.size()), dims, {}, {}, stream_); auto expected_offsets = build_offsets(dims); auto actual_offsets = copy_to_host(cones.cone_offsets); @@ -520,9 +565,9 @@ TEST_F(second_order_cone_test, nt_scaling_matches_reference_for_small_cone) std::vector> lambda_cones{{2.0, 0.5, 0.5}}; std::vector dims{3}; - cone_data_t cones(1, dims, stream_); - copy_to_device(cones.s, pack_cones(s_cones)); - copy_to_device(cones.lambda, pack_cones(lambda_cones)); + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); launch_nt_scaling(cones, stream_); @@ -558,9 +603,13 @@ TEST_F(second_order_cone_test, nt_scaling_matches_reference_across_bucket_sizes) std::vector dims{1, 33, 2049}; auto offsets = build_offsets(dims); - cone_data_t cones(static_cast(dims.size()), dims, stream_); - copy_to_device(cones.s, pack_cones(s_cones)); - copy_to_device(cones.lambda, pack_cones(lambda_cones)); + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); launch_nt_scaling(cones, stream_); @@ -595,9 +644,9 @@ TEST_F(second_order_cone_test, nt_scaling_omega_equals_H_times_lambda) std::vector> lambda_cones{{4.0, 0.5, 1.0, -0.3, 0.2}}; std::vector dims{5}; - cone_data_t cones(1, dims, stream_); - copy_to_device(cones.s, pack_cones(s_cones)); - copy_to_device(cones.lambda, pack_cones(lambda_cones)); + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); launch_nt_scaling(cones, stream_); @@ -618,9 +667,9 @@ TEST_F(second_order_cone_test, nt_scaling_near_boundary_is_stable) std::vector> lambda_cones{{1.000015, 0.8, 0.6, -3e-5, 2e-5}}; std::vector dims{5}; - cone_data_t cones(1, dims, stream_); - copy_to_device(cones.s, pack_cones(s_cones)); - copy_to_device(cones.lambda, pack_cones(lambda_cones)); + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); launch_nt_scaling(cones, stream_); @@ -947,9 +996,13 @@ TEST_F(second_order_cone_test, apply_hinv2_equals_double_hinv_with_nt_scaling) std::vector dims{3, 5}; auto offsets = build_offsets(dims); - cone_data_t cones(static_cast(dims.size()), dims, stream_); - copy_to_device(cones.s, pack_cones(s_cones)); - copy_to_device(cones.lambda, pack_cones(lambda_cones)); + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); launch_nt_scaling(cones, stream_); @@ -993,9 +1046,9 @@ TEST_F(second_order_cone_test, apply_hinv2_strided_loop_for_large_cone) auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); - cone_data_t cones(1, dims, stream_); - copy_to_device(cones.s, s_cone); - copy_to_device(cones.lambda, lambda_cone); + auto d_s = make_device_vector(s_cone); + auto d_lambda = make_device_vector(lambda_cone); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); launch_nt_scaling(cones, stream_); @@ -1024,6 +1077,159 @@ TEST_F(second_order_cone_test, apply_hinv2_strided_loop_for_large_cone) expect_vector_near(hinv2_actual, ref, 1e-8, 1e-6, "hinv2_large_ref"); } +TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_matches_reference_with_nt_scaling) +{ + std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; + std::vector dims{3, 5}; + auto offsets = build_offsets(dims); + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); + + launch_nt_scaling(cones, stream_); + + auto block_offsets_host = copy_to_host(cones.block_offsets); + i_t total_blk = dims[0] * dims[0] + dims[1] * dims[1]; + std::vector q_vals(total_blk, f_t(0)); + std::vector csr_indices(total_blk); + constexpr i_t aug_offset = 2; + for (i_t e = 0; e < total_blk; ++e) { + csr_indices[e] = aug_offset + (total_blk - 1 - e); + } + auto d_csr_indices = make_device_vector(csr_indices); + auto d_q_values = make_device_vector(q_vals); + rmm::device_uvector d_aug_x(total_blk + aug_offset, stream_); + RAFT_CUDA_TRY( + cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * (total_blk + aug_offset), stream_)); + launch_cone_block_scatter(cones, d_aug_x, d_csr_indices, d_q_values); + + auto actual = copy_to_host(d_aug_x); + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + + for (i_t e = 0; e < aug_offset; ++e) { + EXPECT_EQ(actual[e], f_t(0)) << "untouched prefix entry " << e; + } + + i_t blk_off = 0; + for (i_t c = 0; c < static_cast(dims.size()); ++c) { + auto w_c = slice_cone(w_bar_host, offsets, c); + auto ref = ref_build_hinv2_block_single(w_c, inv_eta_h[c]); + i_t blk_sz = dims[c] * dims[c]; + for (i_t e = 0; e < blk_sz; ++e) { + EXPECT_NEAR(actual[csr_indices[blk_off + e]], -ref[e], 1e-10 + 1e-8 * std::abs(ref[e])) + << "cone " << c << " entry " << e; + } + blk_off += blk_sz; + } +} + +TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_matvec_matches_apply_hinv2) +{ + std::vector> s_cones{{5.0, 1.0, -1.0, 0.5, 0.3}}; + std::vector> lambda_cones{{4.0, 0.5, 1.0, -0.3, 0.2}}; + std::vector dims{5}; + i_t q = dims[0]; + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); + + launch_nt_scaling(cones, stream_); + + i_t total_blk = q * q; + std::vector csr_indices(total_blk); + std::iota(csr_indices.begin(), csr_indices.end(), 0); + std::vector q_vals(total_blk, f_t(0)); + auto d_csr_indices = make_device_vector(csr_indices); + auto d_q_values = make_device_vector(q_vals); + rmm::device_uvector d_aug_x(total_blk, stream_); + RAFT_CUDA_TRY(cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * total_blk, stream_)); + launch_cone_block_scatter(cones, d_aug_x, d_csr_indices, d_q_values); + + auto scattered = copy_to_host(d_aug_x); + std::vector block(total_blk); + for (i_t e = 0; e < total_blk; ++e) { + block[e] = -scattered[e]; + } + + std::vector> test_vectors{ + {1.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0, 0.0}, {0.3, -0.1, 0.2, -0.5, 0.15}}; + + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + + for (const auto& v : test_vectors) { + // Host mat-vec: y = block * v + std::vector y(q, f_t(0)); + for (i_t r = 0; r < q; ++r) { + for (i_t c = 0; c < q; ++c) { + y[r] += block[r * q + c] * v[c]; + } + } + + auto ref = ref_apply_hinv2_single(v, w_bar_host, inv_eta_h[0]); + expect_vector_near(y, ref, 1e-10, 1e-8, "block_matvec_vs_apply"); + } +} + +TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_large_cone) +{ + std::vector dims{513}; + + auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); + auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); + + auto d_s = make_device_vector(s_cone); + auto d_lambda = make_device_vector(lambda_cone); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); + + launch_nt_scaling(cones, stream_); + + i_t total_blk = dims[0] * dims[0]; + std::vector csr_indices(total_blk); + std::iota(csr_indices.begin(), csr_indices.end(), 0); + std::vector q_vals(total_blk, f_t(0)); + auto d_csr_indices = make_device_vector(csr_indices); + auto d_q_values = make_device_vector(q_vals); + rmm::device_uvector d_aug_x(total_blk, stream_); + RAFT_CUDA_TRY(cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * total_blk, stream_)); + launch_cone_block_scatter(cones, d_aug_x, d_csr_indices, d_q_values); + + auto scattered = copy_to_host(d_aug_x); + std::vector block(total_blk); + for (i_t e = 0; e < total_blk; ++e) { + block[e] = -scattered[e]; + } + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + + // Spot-check: block * e_0 should match apply_Hinv2(e_0) + i_t q = dims[0]; + std::vector col0(q); + for (i_t r = 0; r < q; ++r) { + col0[r] = block[r * q]; + } + std::vector e0(q, f_t(0)); + e0[0] = f_t(1); + auto ref = ref_apply_hinv2_single(e0, w_bar_host, inv_eta_h[0]); + expect_vector_near(col0, ref, 1e-8, 1e-6, "hinv2_block_col0_large"); + + // Symmetry check: block[r][c] == block[c][r] + for (i_t r = 0; r < std::min(q, i_t(50)); ++r) { + for (i_t c = r + 1; c < std::min(q, i_t(50)); ++c) { + EXPECT_NEAR(block[r * q + c], block[c * q + r], 1e-10) + << "asymmetry at (" << r << "," << c << ")"; + } + } +} + TEST_F(second_order_cone_test, jordan_product_matches_reference_for_packed_cones) { std::vector dims{1, 3, 4}; @@ -1144,9 +1350,13 @@ TEST_F(second_order_cone_test, inverse_jordan_product_with_nt_scaling_rho) std::vector dims{3, 5}; auto offsets = build_offsets(dims); - cone_data_t cones(static_cast(dims.size()), dims, stream_); - copy_to_device(cones.s, pack_cones(s_cones)); - copy_to_device(cones.lambda, pack_cones(lambda_cones)); + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); launch_nt_scaling(cones, stream_); @@ -1189,9 +1399,13 @@ TEST_F(second_order_cone_test, fused_corrector_matches_reference_with_nt_scaling std::vector dims{3, 5}; auto offsets = build_offsets(dims); - cone_data_t cones(static_cast(dims.size()), dims, stream_); - copy_to_device(cones.s, pack_cones(s_cones)); - copy_to_device(cones.lambda, pack_cones(lambda_cones)); + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); launch_nt_scaling(cones, stream_); @@ -1241,9 +1455,9 @@ TEST_F(second_order_cone_test, fused_corrector_strided_loop_for_large_cone) auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); - cone_data_t cones(1, dims, stream_); - copy_to_device(cones.s, s_cone); - copy_to_device(cones.lambda, lambda_cone); + auto d_s = make_device_vector(s_cone); + auto d_lambda = make_device_vector(lambda_cone); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); launch_nt_scaling(cones, stream_); @@ -1277,4 +1491,48 @@ TEST_F(second_order_cone_test, fused_corrector_strided_loop_for_large_cone) expect_vector_near(actual, ref, 1e-8, 1e-6, "fused_corrector_large"); } +TEST_F(second_order_cone_test, cone_block_scatter_with_q_overlap) +{ + std::vector> s_cones{{3.0, 0.5, -0.3}}; + std::vector> lambda_cones{{2.0, -0.2, 0.4}}; + std::vector dims{3}; + i_t K = 1; + i_t q_k = 3; + i_t total_block_nnz = q_k * q_k; + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(K, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); + launch_nt_scaling(cones, stream_); + + f_t dual_perturb = 1e-6; + std::vector q_vals(total_block_nnz, f_t(0)); + q_vals[0] = 0.5 + dual_perturb; + q_vals[4] = 0.3 + dual_perturb; + q_vals[8] = 0.1 + dual_perturb; + q_vals[1] = 0.05; + q_vals[3] = 0.05; + + std::vector cone_csr_indices(total_block_nnz); + std::iota(cone_csr_indices.begin(), cone_csr_indices.end(), 0); + auto d_cone_csr_indices = make_device_vector(cone_csr_indices); + auto d_cone_Q_values = make_device_vector(q_vals); + + rmm::device_uvector d_aug_x(total_block_nnz, stream_); + RAFT_CUDA_TRY(cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * total_block_nnz, stream_)); + + launch_cone_block_scatter(cones, d_aug_x, d_cone_csr_indices, d_cone_Q_values); + + auto actual = copy_to_host(d_aug_x); + auto w_bar_h = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + auto ref_block = ref_build_hinv2_block_single(w_bar_h, inv_eta_h[0]); + + for (i_t e = 0; e < total_block_nnz; ++e) { + f_t expected = -ref_block[e] - q_vals[e]; + EXPECT_NEAR(actual[e], expected, 1e-10 + 1e-8 * std::abs(expected)) + << "entry " << e << " (Q overlap test)"; + } +} + } // namespace cuopt::linear_programming::dual_simplex::test From 41f763c65dc633bfef01acb671a7b7a843a9c0d6 Mon Sep 17 00:00:00 2001 From: Yan Zaretskiy Date: Tue, 7 Apr 2026 12:26:29 -0700 Subject: [PATCH 03/68] fix(barrier): stabilize SOCP barrier convergence Tighten the augmented-system SOCP updates so the barrier path converges on the mixed LP/QP cone cases covered by the new regression tests. --- cpp/src/barrier/barrier.cu | 355 +++++++++++++++--- cpp/src/barrier/dense_vector.hpp | 22 ++ cpp/src/barrier/second_order_cone.cuh | 193 ++++++++++ cpp/src/dual_simplex/presolve.cpp | 82 ++-- cpp/src/dual_simplex/presolve.hpp | 2 + cpp/src/dual_simplex/scaling.cpp | 13 +- cpp/src/dual_simplex/solve.cpp | 20 + cpp/src/dual_simplex/user_problem.hpp | 2 + .../unit_tests/second_order_cone_test.cu | 189 +++++++--- .../dual_simplex/unit_tests/solve_barrier.cu | 131 +++++++ 10 files changed, 883 insertions(+), 126 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index bb848e6037..efa941bfe8 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -213,6 +213,8 @@ class iteration_data_t { d_complementarity_xz_rhs_(lp.num_cols, lp.handle_ptr->get_stream()), d_complementarity_wv_rhs_(0, lp.handle_ptr->get_stream()), d_dual_rhs_(lp.num_cols, lp.handle_ptr->get_stream()), + d_cone_rhs_term_(0, lp.handle_ptr->get_stream()), + d_cone_hinv2_dx_(0, lp.handle_ptr->get_stream()), d_Q_diag_(0, lp.handle_ptr->get_stream()), d_Qx_(Qin.m, lp.handle_ptr->get_stream()), restrict_u_(0), @@ -220,7 +222,9 @@ class iteration_data_t { sum_reduce_helper_(lp.handle_ptr->get_stream()), indefinite_Q(false), Q_diagonal(false), - symbolic_status(0) + symbolic_status(0), + cone_combined_step_(false), + cone_sigma_mu_(f_t(0)) { raft::common::nvtx::range fun_scope("Barrier: LP Data Creation"); @@ -262,6 +266,20 @@ class iteration_data_t { raft::copy(d_Q_diag_.data(), Qdiag.data(), Qdiag.size(), stream_view_); } + if (!lp.second_order_cone_dims.empty()) { + cone_var_start_ = lp.cone_var_start; + i_t total_cone_dim = + std::accumulate(lp.second_order_cone_dims.begin(), lp.second_order_cone_dims.end(), i_t(0)); + cuopt_assert(cone_var_start_ >= 0, "cone_var_start must be nonnegative"); + cuopt_assert(cone_var_start_ + total_cone_dim <= lp.num_cols, + "cone variables exceed problem dimension"); + cones_.emplace(static_cast(lp.second_order_cone_dims.size()), + lp.second_order_cone_dims, + raft::device_span{}, + raft::device_span{}, + stream_view_); + } + // Allocating GPU flag data for Form ADAT RAFT_CUDA_TRY(cub::DeviceSelect::Flagged( nullptr, @@ -1515,8 +1533,9 @@ class iteration_data_t { f_t beta, rmm::device_uvector& y) { - const i_t m = A.m; - const i_t n = A.n; + const i_t m = A.m; + const i_t n = A.n; + const bool has_cones = cones_.has_value() && cones_->K > 0; rmm::device_uvector d_x1(n, handle_ptr->get_stream()); rmm::device_uvector d_x2(m, handle_ptr->get_stream()); @@ -1535,12 +1554,25 @@ class iteration_data_t { // diag.pairwise_product(x1, r1); // r1 <- D * x_1 pairwise_multiply(d_x1.data(), d_diag_.data(), d_r1.data(), n, stream_view_); + if (has_cones) { + thrust::fill_n( + rmm::exec_policy(stream_view_), d_r1.begin() + cone_var_start_, cones_->m_c, f_t(0)); + } // r1 <- Q x1 + D x1 if (Q.n > 0) { // matrix_vector_multiply(Q, 1.0, x1, 1.0, r1); cusparse_Q_view_.spmv(1.0, d_x1, 1.0, d_r1); } + if (has_cones) { + accumulate_cone_hinv2_matvec( + raft::device_span(d_x1.data() + cone_var_start_, cones_->m_c), + *cones_, + d_cone_hinv2_dx_, + raft::device_span(d_r1.data() + cone_var_start_, cones_->m_c), + stream_view_); + RAFT_CHECK_CUDA(stream_view_); + } // y1 <- - alpha * r1 + beta * y1 // y1.axpy(-alpha, r1, beta); @@ -1739,6 +1771,8 @@ class iteration_data_t { rmm::device_uvector d_complementarity_xz_rhs_; rmm::device_uvector d_complementarity_wv_rhs_; rmm::device_uvector d_dual_rhs_; + rmm::device_uvector d_cone_rhs_term_; + rmm::device_uvector d_cone_hinv2_dx_; rmm::device_uvector d_Q_diag_; rmm::device_uvector d_Qx_; @@ -1748,6 +1782,9 @@ class iteration_data_t { transform_reduce_helper_t transform_reduce_helper_; sum_reduce_helper_t sum_reduce_helper_; + bool cone_combined_step_; + f_t cone_sigma_mu_; + rmm::cuda_stream_view stream_view_; const simplex_solver_settings_t& settings_; @@ -1846,6 +1883,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: initial_point"); const bool use_augmented = data.use_augmented; + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; // Perform a numerical factorization i_t status; @@ -2004,8 +2042,10 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.v[k] = -c[j] + epsilon; } } - // Now hande the case with no upper bounds + // Now hande the case with no upper bounds (skip cone variables) + const i_t cone_end = has_cones ? data.cone_var_start_ + data.cones_->m_c : 0; for (i_t j = 0; j < lp.num_cols; j++) { + if (has_cones && j >= data.cone_var_start_ && j < cone_end) continue; if (lp.upper[j] == inf) { if (c[j] > epsilon_adjust) { data.z[j] = c[j]; @@ -2034,7 +2074,8 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.v.multiply_scalar(-1.0); data.v.ensure_positive(epsilon_adjust); - data.z.ensure_positive(epsilon_adjust); + data.z.ensure_positive_skip_range( + epsilon_adjust, data.cone_var_start_, has_cones ? data.cones_->m_c : 0); } else { // First compute rhs = A*Dinv*c dense_vector_t rhs(lp.num_rows); @@ -2058,7 +2099,8 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.gather_upper_bounds(data.z, data.v); data.v.multiply_scalar(-1.0); data.v.ensure_positive(epsilon_adjust); - data.z.ensure_positive(epsilon_adjust); + data.z.ensure_positive_skip_range( + epsilon_adjust, data.cone_var_start_, has_cones ? data.cones_->m_c : 0); } // Verify A'*y + z - E*v - Q*x = c @@ -2075,9 +2117,33 @@ int barrier_solver_t::initial_point(iteration_data_t& data) settings.log.printf("||A^T y + z - E*v - Q*x - c ||: %e\n", vector_norm2(data.dual_residual)); #endif - // Make sure (w, x, v, z) > 0 + // Make sure (w, x, v, z) > 0; skip cone vars — handled by shift_into_interior below data.w.ensure_positive(epsilon_adjust); - data.x.ensure_positive(epsilon_adjust); + data.x.ensure_positive_skip_range( + epsilon_adjust, data.cone_var_start_, has_cones ? data.cones_->m_c : 0); + + if (has_cones) { + const auto& dims = lp.second_order_cone_dims; + i_t cs = data.cone_var_start_; + i_t off = 0; + + for (i_t k = 0; k < static_cast(dims.size()); ++k) { + i_t q_k = dims[k]; + auto shift_into_interior = [&](auto& vec) { + f_t tail_sq = f_t(0); + for (i_t j = 1; j < q_k; ++j) { + f_t v = vec[cs + off + j]; + tail_sq += v * v; + } + f_t gap = std::sqrt(tail_sq) - vec[cs + off]; + if (gap >= f_t(0)) { vec[cs + off] += f_t(1) + gap; } + }; + shift_into_interior(data.x); + shift_into_interior(data.z); + off += q_k; + } + } + #ifdef PRINT_INFO settings.log.printf("min v %e min z %e\n", data.v.minimum(), data.z.minimum()); #endif @@ -2272,16 +2338,51 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t& x, const rmm::device_uvector& dx) { + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const bool skip_cone_range = + has_cones && static_cast(x.size()) >= data.cone_var_start_ + data.cones_->m_c; + + auto ratio_test = [] HD(const thrust::tuple t) { + const f_t dx = thrust::get<0>(t); + const f_t x = thrust::get<1>(t); + if (dx < f_t(0.0)) return -x / dx; + return f_t(1.0); + }; + + if (skip_cone_range) { + i_t cs = data.cone_var_start_; + i_t mc = data.cones_->m_c; + f_t alpha = f_t(1); + + if (cs > 0) { + alpha = std::min(alpha, + data.transform_reduce_helper_.transform_reduce( + thrust::make_zip_iterator(dx.data(), x.data()), + thrust::minimum(), + ratio_test, + f_t(1), + cs, + stream_view_)); + } + i_t tail_start = cs + mc; + i_t tail_size = static_cast(x.size()) - tail_start; + if (tail_size > 0) { + alpha = std::min(alpha, + data.transform_reduce_helper_.transform_reduce( + thrust::make_zip_iterator(dx.data() + tail_start, x.data() + tail_start), + thrust::minimum(), + ratio_test, + f_t(1), + tail_size, + stream_view_)); + } + return alpha; + } + return data.transform_reduce_helper_.transform_reduce( thrust::make_zip_iterator(dx.data(), x.data()), thrust::minimum(), - [] HD(const thrust::tuple t) { - const f_t dx = thrust::get<0>(t); - const f_t x = thrust::get<1>(t); - - if (dx < f_t(0.0)) return -x / dx; - return f_t(1.0); - }, + ratio_test, f_t(1.0), x.size(), stream_view_); @@ -2300,6 +2401,9 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_tK > 0; + const i_t m_c = has_cones ? data.cones_->m_c : 0; + const i_t cone_var_start = data.cone_var_start_; { raft::common::nvtx::range fun_scope("Barrier: GPU allocation and copies"); @@ -2340,6 +2444,24 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_ts = raft::device_span(data.d_x_.data() + cone_var_start, m_c); + data.cones_->lambda = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + launch_nt_scaling(*data.cones_, stream_view_); + + if (data.cone_combined_step_) { + compute_combined_cone_rhs_term( + raft::device_span(data.d_dx_aff_.data() + cone_var_start, m_c), + *data.cones_, + data.cone_sigma_mu_, + data.d_cone_rhs_term_, + stream_view_); + } else { + compute_affine_cone_rhs_term(*data.cones_, data.d_cone_rhs_term_, stream_view_); + } + RAFT_CHECK_CUDA(stream_view_); + } + // Solves the linear system // // dw dx dy dv dz @@ -2353,13 +2475,18 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t{}, stream_view_.value()); RAFT_CHECK_CUDA(stream_view_); + + if (has_cones) { + thrust::fill_n( + rmm::exec_policy(stream_view_), data.d_diag_.begin() + cone_var_start, m_c, f_t(1)); + } // diag = z ./ x + E * (v ./ w) * E' if (data.n_upper_bounds > 0) { @@ -2450,22 +2577,45 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t thrust::tuple { - const f_t tmp = tmp3 + -(complementarity_xz_rhs / x) + dual_rhs; - return {tmp, inv_diag * tmp}; - }, - stream_view_.value()); + if (has_cones) { + thrust::for_each_n(rmm::exec_policy(stream_view_), + thrust::make_counting_iterator(0), + lp.num_cols, + [span_inv_diag = cuopt::make_span(data.d_inv_diag), + span_tmp3 = cuopt::make_span(data.d_tmp3_), + span_tmp4 = cuopt::make_span(data.d_tmp4_), + span_xz_rhs = cuopt::make_span(data.d_complementarity_xz_rhs_), + span_x = cuopt::make_span(data.d_x_), + span_dual_rhs = cuopt::make_span(data.d_dual_rhs_), + span_cone_rhs = cuopt::make_span(data.d_cone_rhs_term_), + cone_start = cone_var_start, + cone_size = m_c] __device__(i_t j) { + bool is_cone = (j >= cone_start && j < cone_start + cone_size); + f_t cone_rhs = is_cone ? span_cone_rhs[j - cone_start] : f_t(0); + f_t xz_term = is_cone ? f_t(0) : -(span_xz_rhs[j] / span_x[j]); + f_t tmp = span_tmp3[j] + xz_term + span_dual_rhs[j] + cone_rhs; + span_tmp3[j] = tmp; + span_tmp4[j] = span_inv_diag[j] * tmp; + }); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_inv_diag.data(), + data.d_tmp3_.data(), + data.d_complementarity_xz_rhs_.data(), + data.d_x_.data(), + data.d_dual_rhs_.data()), + thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + lp.num_cols, + [] HD(f_t inv_diag, f_t tmp3, f_t complementarity_xz_rhs, f_t x, f_t dual_rhs) + -> thrust::tuple { + const f_t tmp = tmp3 + -(complementarity_xz_rhs / x) + dual_rhs; + return {tmp, inv_diag * tmp}; + }, + stream_view_.value()); + } RAFT_CHECK_CUDA(stream_view_); raft::copy(data.d_r1_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); raft::copy(data.d_r1_prime_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); @@ -2749,18 +2899,42 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_dx_.data() + cone_var_start, m_c), + *data.cones_, + data.d_cone_rhs_term_, + data.d_cone_hinv2_dx_, + raft::device_span(data.d_dz_.data() + cone_var_start, m_c), + stream_view_); + + thrust::for_each_n(rmm::exec_policy(stream_view_), + thrust::make_counting_iterator(0), + data.d_dz_.size(), + [span_xz_rhs = cuopt::make_span(data.d_complementarity_xz_rhs_), + span_z = cuopt::make_span(data.d_z_), + span_dx = cuopt::make_span(data.d_dx_), + span_x = cuopt::make_span(data.d_x_), + span_dz = cuopt::make_span(data.d_dz_), + cone_start = cone_var_start, + cone_size = m_c] __device__(i_t j) { + if (j < cone_start || j >= cone_start + cone_size) { + span_dz[j] = (span_xz_rhs[j] - span_z[j] * span_dx[j]) / span_x[j]; + } + }); + } else { + // dz = (complementarity_xz_rhs - z.* dx) ./ x; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_complementarity_xz_rhs_.data(), + data.d_z_.data(), + data.d_dx_.data(), + data.d_x_.data()), + data.d_dz_.data(), + data.d_dz_.size(), + [] HD(f_t complementarity_xz_rhs, f_t z, f_t dx, f_t x) { + return (complementarity_xz_rhs - z * dx) / x; + }, + stream_view_.value()); + } RAFT_CHECK_CUDA(stream_view_); raft::copy(dz.data(), data.d_dz_.data(), data.d_dz_.size(), stream_view_); } @@ -2944,10 +3118,13 @@ template void barrier_solver_t::compute_affine_rhs(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: compute_affine_rhs"); + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; - data.primal_rhs = data.primal_residual; - data.bound_rhs = data.bound_residual; - data.dual_rhs = data.dual_residual; + data.primal_rhs = data.primal_residual; + data.bound_rhs = data.bound_residual; + data.dual_rhs = data.dual_residual; + data.cone_combined_step_ = false; + data.cone_sigma_mu_ = f_t(0); raft::copy(data.d_complementarity_xz_rhs_.data(), data.d_complementarity_xz_residual_.data(), @@ -2966,6 +3143,12 @@ void barrier_solver_t::compute_affine_rhs(iteration_data_t& [] HD(f_t xz_rhs) { return -xz_rhs; }, stream_view_.value()); RAFT_CHECK_CUDA(stream_view_); + if (has_cones) { + thrust::fill_n(rmm::exec_policy(stream_view_), + data.d_complementarity_xz_rhs_.begin() + data.cone_var_start_, + data.cones_->m_c, + f_t(0)); + } // w.*v -> -w .* v cub::DeviceTransform::Transform( data.d_complementarity_wv_rhs_.data(), @@ -2981,6 +3164,7 @@ void barrier_solver_t::compute_target_mu( iteration_data_t& data, f_t mu, f_t& mu_aff, f_t& sigma, f_t& new_mu) { raft::common::nvtx::range fun_scope("Barrier: compute_target_mu"); + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; f_t complementarity_aff_sum = 0.0; // TMP no copy and data should always be on the GPU @@ -2999,6 +3183,21 @@ void barrier_solver_t::compute_target_mu( f_t step_dual_aff = std::min(gpu_max_step_to_boundary(data, data.d_v_, data.d_dv_aff_), gpu_max_step_to_boundary(data, data.d_z_, data.d_dz_aff_)); + if (has_cones) { + i_t cs = data.cone_var_start_; + i_t mc = data.cones_->m_c; + step_primal_aff = std::min( + step_primal_aff, + compute_cone_step_length(*data.cones_, + raft::device_span(data.d_x_.data() + cs, mc), + raft::device_span(data.d_dx_aff_.data() + cs, mc), + raft::device_span(data.d_z_.data() + cs, mc), + raft::device_span(data.d_dz_aff_.data() + cs, mc), + step_primal_aff, + stream_view_)); + step_dual_aff = step_primal_aff; + } + if (data.Q.n > 0) { step_primal_aff = step_dual_aff = std::min(step_primal_aff, step_dual_aff); } // Compute complementarity_xz_aff_sum = sum(x_aff * z_aff), @@ -3050,9 +3249,12 @@ void barrier_solver_t::compute_target_mu( stream_view_); complementarity_aff_sum = complementarity_xz_aff_sum + complementarity_wv_aff_sum; - - mu_aff = (complementarity_aff_sum) / - (static_cast(data.x.size()) + static_cast(data.n_upper_bounds)); + f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); + if (has_cones) { + mu_denom -= static_cast(data.cones_->m_c); + mu_denom += static_cast(data.cones_->K); + } + mu_aff = complementarity_aff_sum / mu_denom; sigma = std::max(0.0, std::min(1.0, std::pow(mu_aff / mu, 3.0))); new_mu = sigma * mu_aff; } @@ -3061,6 +3263,7 @@ template void barrier_solver_t::compute_cc_rhs(iteration_data_t& data, f_t& new_mu) { raft::common::nvtx::range fun_scope("Barrier: compute_cc_rhs"); + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_dx_aff_.data(), data.d_dz_aff_.data()), @@ -3076,11 +3279,19 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data [new_mu] HD(f_t dw_aff, f_t dv_aff) { return -(dw_aff * dv_aff) + new_mu; }, stream_view_.value()); RAFT_CHECK_CUDA(stream_view_); + if (has_cones) { + thrust::fill_n(rmm::exec_policy(stream_view_), + data.d_complementarity_xz_rhs_.begin() + data.cone_var_start_, + data.cones_->m_c, + f_t(0)); + } // TMP should be CPU to 0 if CPU and GPU to 0 if GPU data.primal_rhs.set_scalar(0.0); data.bound_rhs.set_scalar(0.0); data.dual_rhs.set_scalar(0.0); + data.cone_combined_step_ = has_cones; + data.cone_sigma_mu_ = has_cones ? new_mu : f_t(0); } template @@ -3153,6 +3364,8 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ f_t& step_dual) { raft::common::nvtx::range fun_scope("Barrier: compute_primal_dual_step_length"); + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + f_t max_step_primal = 0.0; f_t max_step_dual = 0.0; max_step_primal = std::min(gpu_max_step_to_boundary(data, data.d_w_, data.d_dw_), @@ -3160,6 +3373,25 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ max_step_dual = std::min(gpu_max_step_to_boundary(data, data.d_v_, data.d_dv_), gpu_max_step_to_boundary(data, data.d_z_, data.d_dz_)); + if (has_cones) { + i_t cs = data.cone_var_start_; + i_t mc = data.cones_->m_c; + f_t cone_primal = + compute_single_cone_step_length(*data.cones_, + raft::device_span(data.d_x_.data() + cs, mc), + raft::device_span(data.d_dx_.data() + cs, mc), + f_t(1), + stream_view_); + f_t cone_dual = + compute_single_cone_step_length(*data.cones_, + raft::device_span(data.d_z_.data() + cs, mc), + raft::device_span(data.d_dz_.data() + cs, mc), + f_t(1), + stream_view_); + max_step_primal = std::min(max_step_primal, cone_primal); + max_step_dual = std::min(max_step_dual, cone_dual); + } + step_primal = step_scale * max_step_primal; step_dual = step_scale * max_step_dual; @@ -3253,6 +3485,13 @@ template void barrier_solver_t::compute_mu(iteration_data_t& data, f_t& mu) { raft::common::nvtx::range fun_scope("Barrier: compute_mu"); + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + + f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); + if (has_cones) { + mu_denom -= static_cast(data.cones_->m_c); + mu_denom += static_cast(data.cones_->K); + } mu = (data.sum_reduce_helper_.sum(data.d_complementarity_xz_residual_.begin(), data.d_complementarity_xz_residual_.size(), @@ -3260,7 +3499,7 @@ void barrier_solver_t::compute_mu(iteration_data_t& data, f_ data.sum_reduce_helper_.sum(data.d_complementarity_wv_residual_.begin(), data.d_complementarity_wv_residual_.size(), stream_view_)) / - (static_cast(data.x.size()) + static_cast(data.n_upper_bounds)); + mu_denom; } template @@ -3585,8 +3824,13 @@ lp_status_t barrier_solver_t::solve(f_t start_time, f_t complementarity_residual_norm = std::max(vector_norm_inf(data.complementarity_xz_residual, stream_view_), vector_norm_inf(data.complementarity_wv_residual, stream_view_)); - f_t mu = (data.complementarity_xz_residual.sum() + data.complementarity_wv_residual.sum()) / - (static_cast(n) + static_cast(num_upper_bounds)); + f_t mu_denom = static_cast(n) + static_cast(num_upper_bounds); + if (data.cones_.has_value() && data.cones_->K > 0) { + mu_denom -= static_cast(data.cones_->m_c); + mu_denom += static_cast(data.cones_->K); + } + f_t mu = + (data.complementarity_xz_residual.sum() + data.complementarity_wv_residual.sum()) / mu_denom; f_t norm_b = vector_norm_inf(data.b, stream_view_); f_t norm_c = vector_norm_inf(data.c, stream_view_); @@ -3627,9 +3871,14 @@ lp_status_t barrier_solver_t::solve(f_t start_time, relative_complementarity_residual, elapsed_time); + f_t duality_gap_abs = std::abs(primal_objective - dual_objective); + f_t duality_gap_rel = + duality_gap_abs / + std::max(f_t(1), std::min(std::abs(primal_objective), std::abs(dual_objective))); bool converged = primal_residual_norm < settings.barrier_relative_feasibility_tol && dual_residual_norm < settings.barrier_relative_optimality_tol && - complementarity_residual_norm < settings.barrier_relative_complementarity_tol; + (duality_gap_abs < settings.barrier_relative_complementarity_tol || + duality_gap_rel < settings.barrier_relative_complementarity_tol); data.d_complementarity_xz_residual_.resize(data.complementarity_xz_residual.size(), stream_view_); diff --git a/cpp/src/barrier/dense_vector.hpp b/cpp/src/barrier/dense_vector.hpp index f73a9a5fce..009b9ea955 100644 --- a/cpp/src/barrier/dense_vector.hpp +++ b/cpp/src/barrier/dense_vector.hpp @@ -184,6 +184,28 @@ class dense_vector_t : public std::vector { } } + void ensure_positive_skip_range(f_t epsilon_adjust, i_t skip_start, i_t skip_count) + { + if (skip_count == 0) { + ensure_positive(epsilon_adjust); + return; + } + const i_t n = this->size(); + const i_t skip_end = skip_start + skip_count; + f_t min_val = std::numeric_limits::max(); + for (i_t i = 0; i < n; i++) { + if (i >= skip_start && i < skip_end) continue; + min_val = std::min(min_val, (*this)[i]); + } + if (min_val <= 0.0) { + const f_t delta = -min_val + epsilon_adjust; + for (i_t i = 0; i < n; i++) { + if (i >= skip_start && i < skip_end) continue; + (*this)[i] += delta; + } + } + } + void bound_away_from_zero(f_t epsilon_adjust) { const i_t n = this->size(); diff --git a/cpp/src/barrier/second_order_cone.cuh b/cpp/src/barrier/second_order_cone.cuh index ca23ed39cc..4d69e787d1 100644 --- a/cpp/src/barrier/second_order_cone.cuh +++ b/cpp/src/barrier/second_order_cone.cuh @@ -764,6 +764,33 @@ __global__ __launch_bounds__(BLOCK_DIM) void step_length_kernel( if (threadIdx.x == 0) { alpha[cone] = min(alpha_s, alpha_l); } } +// --------------------------------------------------------------------------- +// Single-variable cone step length kernel (one block per cone). +// Like step_length_kernel but only checks u + alpha*du in Q^{q_i}. +// --------------------------------------------------------------------------- +template +__global__ __launch_bounds__(BLOCK_DIM) void step_length_single_kernel( + raft::device_span u, + raft::device_span du, + raft::device_span alpha, + raft::device_span cone_offsets, + i_t K, + f_t alpha_max) +{ + __shared__ typename block_reduce_t, BLOCK_DIM>::TempStorage temp_storage; + + i_t cone = static_cast(blockIdx.x); + if (cone >= K) return; + + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + + f_t a = cone_step_length_single( + u.subspan(off, q), du.subspan(off, q), temp_storage, alpha_max); + + if (threadIdx.x == 0) { alpha[cone] = a; } +} + // --------------------------------------------------------------------------- // Shift u into int(Q^q) if it is not already interior (one block per cone). // @@ -893,6 +920,104 @@ struct cone_data_t { } }; +template +void compute_affine_cone_rhs_term(const cone_data_t& cones, + rmm::device_uvector& out, + rmm::cuda_stream_view stream) +{ + out.resize(cones.m_c, stream); + if (cones.K == 0) return; + + apply_Hinv2_kernel + <<>>(cones.s, + cuopt::make_span(out), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.cone_offsets), + cones.K); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +template +void compute_combined_cone_rhs_term(raft::device_span dx_aff, + const cone_data_t& cones, + f_t sigma_mu, + rmm::device_uvector& out, + rmm::cuda_stream_view stream) +{ + out.resize(cones.m_c, stream); + if (cones.K == 0) return; + + fused_corrector_kernel + <<>>(dx_aff, + cuopt::make_span(cones.omega), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.inv_1pw0), + cuopt::make_span(cones.rho), + sigma_mu, + cuopt::make_span(out), + cuopt::make_span(cones.cone_offsets), + cones.K); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +template +void recover_cone_dz(raft::device_span dx, + const cone_data_t& cones, + const rmm::device_uvector& cone_rhs_term, + rmm::device_uvector& hinv2_dx, + raft::device_span dz, + rmm::cuda_stream_view stream) +{ + hinv2_dx.resize(cones.m_c, stream); + if (cones.K == 0) return; + + apply_Hinv2_kernel + <<>>(dx, + cuopt::make_span(hinv2_dx), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.cone_offsets), + cones.K); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + thrust::for_each_n( + rmm::exec_policy(stream), + thrust::make_counting_iterator(0), + cones.m_c, + [span_rhs = cuopt::make_span(cone_rhs_term), + span_hinv2 = cuopt::make_span(hinv2_dx), + span_dz = dz] __device__(i_t j) { span_dz[j] = -span_rhs[j] - span_hinv2[j]; }); +} + +template +void accumulate_cone_hinv2_matvec(raft::device_span x, + const cone_data_t& cones, + rmm::device_uvector& hinv2_x, + raft::device_span out, + rmm::cuda_stream_view stream) +{ + hinv2_x.resize(cones.m_c, stream); + if (cones.K == 0) return; + + apply_Hinv2_kernel + <<>>(x, + cuopt::make_span(hinv2_x), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.cone_offsets), + cones.K); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + thrust::for_each_n(rmm::exec_policy(stream), + thrust::make_counting_iterator(0), + cones.m_c, + [span_hinv2 = cuopt::make_span(hinv2_x), span_out = out] __device__(i_t j) { + span_out[j] += span_hinv2[j]; + }); +} + // --------------------------------------------------------------------------- // Compute flat H^{-2} cone-block entries and scatter them into the augmented // CSR value array. @@ -956,6 +1081,74 @@ void scatter_hinv2_into_augmented(const cone_data_t& cones, rmm::exec_policy(stream), values, values + count, csr_indices.begin(), augmented_x.begin()); } +// --------------------------------------------------------------------------- +// Compute the maximum feasible step length for the cone portion of (x, z). +// +// Launches step_length_kernel (one CTA per cone), then reduces the per-cone +// results to a single scalar. Returns min over all cones of the step length +// that keeps both x_K + alpha*dx_K and z_K + alpha*dz_K in their cones. +// --------------------------------------------------------------------------- +template +f_t compute_cone_step_length(const cone_data_t& cones, + raft::device_span x_K, + raft::device_span dx_K, + raft::device_span z_K, + raft::device_span dz_K, + f_t alpha_max, + rmm::cuda_stream_view stream) +{ + if (cones.K == 0) return alpha_max; + + rmm::device_uvector d_alpha(cones.K, stream); + step_length_kernel + <<>>(x_K, + dx_K, + z_K, + dz_K, + cuopt::make_span(d_alpha), + cuopt::make_span(cones.cone_offsets), + cones.K, + alpha_max); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + f_t result = thrust::reduce( + rmm::exec_policy(stream), d_alpha.begin(), d_alpha.end(), alpha_max, thrust::minimum()); + return result; +} + +template +f_t compute_single_cone_step_length(const cone_data_t& cones, + raft::device_span u_K, + raft::device_span du_K, + f_t alpha_max, + rmm::cuda_stream_view stream) +{ + if (cones.K == 0) return alpha_max; + + rmm::device_uvector d_alpha(cones.K, stream); + step_length_single_kernel<<>>( + u_K, du_K, cuopt::make_span(d_alpha), cuopt::make_span(cones.cone_offsets), cones.K, alpha_max); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + return thrust::reduce( + rmm::exec_policy(stream), d_alpha.begin(), d_alpha.end(), alpha_max, thrust::minimum()); +} + +// --------------------------------------------------------------------------- +// Shift cone slices of a vector into the strict interior of their cones. +// Operates on a subspan of the global vector (pre-sliced to cone portion). +// --------------------------------------------------------------------------- +template +void launch_interior_shift(raft::device_span u_K, + const cone_data_t& cones, + rmm::cuda_stream_view stream) +{ + if (cones.K == 0) return; + interior_shift_kernel + <<>>(u_K, cuopt::make_span(cones.cone_offsets), cones.K); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + template void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) { diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index c5ef847106..ab5f990809 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -22,22 +22,15 @@ namespace cuopt::linear_programming::dual_simplex { template i_t remove_empty_cols(lp_problem_t& problem, i_t& num_empty_cols, - presolve_info_t& presolve_info) + presolve_info_t& presolve_info, + const std::vector& is_cone_variable) { constexpr bool verbose = false; if (verbose) { printf("Removing %d empty columns\n", num_empty_cols); } - // We have a variable x_j that does not appear in any rows - // The cost function - // sum_{k != j} c_k * x_k + c_j * x_j - // becomes - // sum_{k != j} c_k * x_k + c_j * l_j if c_j > 0 - // or - // sum_{k != j} c_k * x_k + c_j * u_j if c_j < 0 presolve_info.removed_variables.reserve(num_empty_cols); presolve_info.removed_values.reserve(num_empty_cols); presolve_info.removed_reduced_costs.reserve(num_empty_cols); - // Check to see if a variable participates in a quadratic objective std::vector has_quadratic_term(problem.num_cols, false); if (problem.Q.n > 0) { @@ -45,7 +38,6 @@ i_t remove_empty_cols(lp_problem_t& problem, const i_t row_start = problem.Q.row_start[j]; const i_t row_end = problem.Q.row_start[j + 1]; if (row_end - row_start == 0) { continue; } - // Q is symmetric, so its sufficient to check only the row size has_quadratic_term[j] = true; } } @@ -55,11 +47,12 @@ i_t remove_empty_cols(lp_problem_t& problem, for (i_t j = 0; j < problem.num_cols; ++j) { bool remove_var = false; if ((problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { - if (problem.objective[j] >= 0 && problem.lower[j] > -inf && !has_quadratic_term[j]) { + bool non_removable = has_quadratic_term[j] || is_cone_variable[j]; + if (problem.objective[j] >= 0 && problem.lower[j] > -inf && !non_removable) { presolve_info.removed_values.push_back(problem.lower[j]); problem.obj_constant += problem.objective[j] * problem.lower[j]; remove_var = true; - } else if (problem.objective[j] <= 0 && problem.upper[j] < inf && !has_quadratic_term[j]) { + } else if (problem.objective[j] <= 0 && problem.upper[j] < inf && !non_removable) { presolve_info.removed_values.push_back(problem.upper[j]); problem.obj_constant += problem.objective[j] * problem.upper[j]; remove_var = true; @@ -570,16 +563,18 @@ void convert_user_problem(const user_problem_t& user_problem, } // Copy info from user_problem to problem - problem.num_rows = user_problem.num_rows; - problem.num_cols = user_problem.num_cols; - problem.A = user_problem.A; - problem.objective = user_problem.objective; - problem.obj_scale = user_problem.obj_scale; - problem.obj_constant = user_problem.obj_constant; - problem.objective_is_integral = user_problem.objective_is_integral; - problem.rhs = user_problem.rhs; - problem.lower = user_problem.lower; - problem.upper = user_problem.upper; + problem.num_rows = user_problem.num_rows; + problem.num_cols = user_problem.num_cols; + problem.A = user_problem.A; + problem.objective = user_problem.objective; + problem.obj_scale = user_problem.obj_scale; + problem.obj_constant = user_problem.obj_constant; + problem.objective_is_integral = user_problem.objective_is_integral; + problem.rhs = user_problem.rhs; + problem.lower = user_problem.lower; + problem.upper = user_problem.upper; + problem.cone_var_start = user_problem.cone_var_start; + problem.second_order_cone_dims = user_problem.second_order_cone_dims; // Make a copy of row_sense so we can modify it std::vector row_sense = user_problem.row_sense; @@ -636,6 +631,7 @@ void convert_user_problem(const user_problem_t& user_problem, settings.log.debug( "equality rows %d less rows %d columns %d\n", equal_rows, less_rows, problem.num_cols); if (settings.barrier && settings.dualize != 0 && user_problem.Q_values.size() == 0 && + user_problem.second_order_cone_dims.empty() && (settings.dualize == 1 || (settings.dualize == -1 && less_rows > 1.2 * problem.num_cols && equal_rows < 2e4))) { settings.log.debug("Dualizing in presolve\n"); @@ -821,10 +817,26 @@ i_t presolve(const lp_problem_t& original, { problem = original; std::vector row_sense(problem.num_rows, '='); + auto build_is_cone_variable = [](const lp_problem_t& current_problem) { + std::vector is_cone_variable(current_problem.num_cols, false); + if (!current_problem.second_order_cone_dims.empty()) { + i_t cone_end = current_problem.cone_var_start; + for (auto q_k : current_problem.second_order_cone_dims) { + cone_end += q_k; + } + for (i_t j = current_problem.cone_var_start; j < cone_end; ++j) { + is_cone_variable[j] = true; + } + } + return is_cone_variable; + }; + auto is_cone_variable = build_is_cone_variable(problem); // Check for free variables i_t free_variables = 0; for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } + if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { + free_variables++; + } } if (settings.barrier_presolve && free_variables > 0) { @@ -835,7 +847,7 @@ i_t presolve(const lp_problem_t& original, current_free_variables.reserve(problem.num_cols); constraints_to_check.reserve(problem.num_rows); for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf) { + if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { current_free_variables.push_back(j); const i_t col_start = problem.A.col_start[j]; const i_t col_end = problem.A.col_start[j + 1]; @@ -975,7 +987,9 @@ i_t presolve(const lp_problem_t& original, i_t new_free_variables = 0; for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf) { new_free_variables++; } + if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { + new_free_variables++; + } } if (removed_free_variables != 0) { settings.log.printf("Bounded %d free variables\n", removed_free_variables); @@ -1134,9 +1148,18 @@ i_t presolve(const lp_problem_t& original, } if (num_empty_cols > 0) { settings.log.printf("Presolve attempt to remove %d empty cols\n", num_empty_cols); - remove_empty_cols(problem, num_empty_cols, presolve_info); + remove_empty_cols(problem, num_empty_cols, presolve_info, is_cone_variable); } + is_cone_variable = build_is_cone_variable(problem); + + // Check for free variables (exclude cone variables — they are naturally unbounded) + free_variables = 0; + for (i_t j = 0; j < problem.num_cols; j++) { + if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { + free_variables++; + } + } problem.Q.check_matrix("Before free variable expansion"); if (settings.barrier_presolve && free_variables > 0) { @@ -1156,7 +1179,7 @@ i_t presolve(const lp_problem_t& original, i_t num_cols = problem.num_cols + free_variables; i_t nnz = problem.A.col_start[problem.num_cols]; for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf) { + if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { nnz += (problem.A.col_start[j + 1] - problem.A.col_start[j]); } } @@ -1173,7 +1196,7 @@ i_t presolve(const lp_problem_t& original, i_t q = problem.A.col_start[problem.num_cols]; i_t col = problem.num_cols; for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf) { + if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { for (i_t p = problem.A.col_start[j]; p < problem.A.col_start[j + 1]; p++) { i_t i = problem.A.i[p]; f_t aij = problem.A.x[p]; @@ -1286,7 +1309,8 @@ i_t presolve(const lp_problem_t& original, problem.num_cols = num_cols; } - if (settings.barrier_presolve && settings.folding != 0 && problem.Q.n == 0) { + if (settings.barrier_presolve && settings.folding != 0 && problem.Q.n == 0 && + problem.second_order_cone_dims.empty()) { folding(problem, settings, presolve_info); } diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index d570ea933e..b052f67e9c 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -49,6 +49,8 @@ struct lp_problem_t { f_t obj_constant; f_t obj_scale; // 1.0 for min, -1.0 for max bool objective_is_integral{false}; + i_t cone_var_start{0}; + std::vector second_order_cone_dims; void write_problem(const std::string& path) const { diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 1531c91486..54fe5dcf46 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -29,9 +29,20 @@ i_t column_scaling(const lp_problem_t& unscaled, } column_scaling.resize(n); + + i_t cone_start = unscaled.cone_var_start; + i_t cone_end = cone_start; + for (auto q_k : unscaled.second_order_cone_dims) { + cone_end += q_k; + } + f_t max = 0; f_t min = std::numeric_limits::max(); for (i_t j = 0; j < n; ++j) { + if (j >= cone_start && j < cone_end) { + column_scaling[j] = 1.0; + continue; + } const i_t col_start = scaled.A.col_start[j]; const i_t col_end = scaled.A.col_start[j + 1]; f_t sum = 0.0; diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index b7c619f246..9bc12bb925 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -345,6 +345,26 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us barrier_settings.barrier_presolve = true; dualize_info_t dualize_info; convert_user_problem(user_problem, barrier_settings, original_lp, new_slacks, dualize_info); + + if (!user_problem.second_order_cone_dims.empty()) { + i_t cone_end = user_problem.cone_var_start; + for (auto q_k : user_problem.second_order_cone_dims) { + cone_end += q_k; + } + for (i_t j = user_problem.cone_var_start; j < cone_end; ++j) { + if (user_problem.lower[j] != 0.0 && user_problem.lower[j] > -1e30) { + settings.log.printf("Error: explicit lower bound on conic variable %d is not supported\n", + j); + return lp_status_t::NUMERICAL_ISSUES; + } + if (user_problem.upper[j] < 1e30) { + settings.log.printf("Error: explicit upper bound on conic variable %d is not supported\n", + j); + return lp_status_t::NUMERICAL_ISSUES; + } + } + } + lp_solution_t lp_solution(original_lp.num_rows, original_lp.num_cols); // Presolve the linear program diff --git a/cpp/src/dual_simplex/user_problem.hpp b/cpp/src/dual_simplex/user_problem.hpp index 73c4c391be..8b0588064c 100644 --- a/cpp/src/dual_simplex/user_problem.hpp +++ b/cpp/src/dual_simplex/user_problem.hpp @@ -52,6 +52,8 @@ struct user_problem_t { std::vector Q_offsets; std::vector Q_indices; std::vector Q_values; + i_t cone_var_start{0}; + std::vector second_order_cone_dims; }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu index 611a236562..035fde5ac6 100644 --- a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu @@ -502,27 +502,38 @@ class second_order_cone_test : public ::testing::Test { } void launch_fused_corrector(const rmm::device_uvector& dx_aff, - const rmm::device_uvector& omega, - const rmm::device_uvector& w_bar, - const rmm::device_uvector& inv_eta, - const rmm::device_uvector& inv_1pw0, - const rmm::device_uvector& rho, + const cone_data_t& cones, f_t sigma_mu, - rmm::device_uvector& out, - const rmm::device_uvector& cone_offsets, - i_t k) + rmm::device_uvector& out) { - fused_corrector_kernel<<>>(cuopt::make_span(dx_aff), - cuopt::make_span(omega), - cuopt::make_span(w_bar), - cuopt::make_span(inv_eta), - cuopt::make_span(inv_1pw0), - cuopt::make_span(rho), - sigma_mu, - cuopt::make_span(out), - cuopt::make_span(cone_offsets), - k); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + compute_combined_cone_rhs_term(cuopt::make_span(dx_aff), cones, sigma_mu, out, stream_); + sync(); + } + + void launch_affine_cone_rhs(const cone_data_t& cones, rmm::device_uvector& out) + { + compute_affine_cone_rhs_term(cones, out, stream_); + sync(); + } + + void launch_recover_cone_dz(const rmm::device_uvector& dx, + const cone_data_t& cones, + const rmm::device_uvector& cone_rhs_term, + rmm::device_uvector& hinv2_dx, + rmm::device_uvector& dz) + { + recover_cone_dz( + cuopt::make_span(dx), cones, cone_rhs_term, hinv2_dx, cuopt::make_span(dz), stream_); + sync(); + } + + void launch_accumulate_cone_hinv2(const rmm::device_uvector& x, + const cone_data_t& cones, + rmm::device_uvector& hinv2_x, + rmm::device_uvector& out) + { + accumulate_cone_hinv2_matvec( + cuopt::make_span(x), cones, hinv2_x, cuopt::make_span(out), stream_); sync(); } @@ -1077,6 +1088,75 @@ TEST_F(second_order_cone_test, apply_hinv2_strided_loop_for_large_cone) expect_vector_near(hinv2_actual, ref, 1e-8, 1e-6, "hinv2_large_ref"); } +TEST_F(second_order_cone_test, affine_cone_rhs_matches_hinv2_of_primal) +{ + std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; + std::vector dims{3, 5}; + auto offsets = build_offsets(dims); + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); + launch_nt_scaling(cones, stream_); + + rmm::device_uvector d_out(cones.m_c, stream_); + launch_affine_cone_rhs(cones, d_out); + + auto actual = copy_to_host(d_out); + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + + for (i_t c = 0; c < static_cast(dims.size()); ++c) { + auto ref = ref_apply_hinv2_single(s_cones[c], slice_cone(w_bar_host, offsets, c), inv_eta_h[c]); + auto act = slice_cone(actual, offsets, c); + expect_vector_near(act, ref, 1e-10, 1e-8, "affine_cone_rhs"); + } +} + +TEST_F(second_order_cone_test, accumulate_cone_hinv2_matvec_matches_reference) +{ + std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; + std::vector> x_cones{{0.3, -0.1, 0.2}, {-0.5, 0.2, 0.1, -0.3, 0.15}}; + std::vector> base_cones{{1.0, 2.0, 3.0}, {0.5, -0.5, 0.25, -0.25, 0.75}}; + std::vector dims{3, 5}; + auto offsets = build_offsets(dims); + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + auto d_x = make_device_vector(pack_cones(x_cones)); + auto d_out = make_device_vector(pack_cones(base_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); + launch_nt_scaling(cones, stream_); + + rmm::device_uvector d_hinv2_x(cones.m_c, stream_); + launch_accumulate_cone_hinv2(d_x, cones, d_hinv2_x, d_out); + + auto actual = copy_to_host(d_out); + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + + for (i_t c = 0; c < static_cast(dims.size()); ++c) { + auto ref_hinv2 = + ref_apply_hinv2_single(x_cones[c], slice_cone(w_bar_host, offsets, c), inv_eta_h[c]); + auto actual_c = slice_cone(actual, offsets, c); + std::vector ref(actual_c.size()); + for (i_t j = 0; j < static_cast(ref.size()); ++j) { + ref[j] = base_cones[c][j] + ref_hinv2[j]; + } + expect_vector_near(actual_c, ref, 1e-10, 1e-8, "accumulate_cone_hinv2"); + } +} + TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_matches_reference_with_nt_scaling) { std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; @@ -1412,20 +1492,10 @@ TEST_F(second_order_cone_test, fused_corrector_matches_reference_with_nt_scaling std::vector> dx_aff_cones{{0.3, -0.1, 0.2}, {-0.5, 0.2, 0.1, -0.3, 0.15}}; f_t sigma_mu = 0.1; - auto d_dx_aff = make_device_vector(pack_cones(dx_aff_cones)); - auto d_offsets = make_device_vector(offsets); + auto d_dx_aff = make_device_vector(pack_cones(dx_aff_cones)); rmm::device_uvector d_out(cones.omega.size(), stream_); - launch_fused_corrector(d_dx_aff, - cones.omega, - cones.w_bar, - cones.inv_eta, - cones.inv_1pw0, - cones.rho, - sigma_mu, - d_out, - d_offsets, - static_cast(dims.size())); + launch_fused_corrector(d_dx_aff, cones, sigma_mu, d_out); auto actual = copy_to_host(d_out); auto omega_host = copy_to_host(cones.omega); @@ -1464,20 +1534,10 @@ TEST_F(second_order_cone_test, fused_corrector_strided_loop_for_large_cone) auto dx_aff_cone = make_patterned_cone(dims[0], 0.5, 0.003); f_t sigma_mu = 0.25; - auto d_dx_aff = make_device_vector(dx_aff_cone); - auto d_offsets = make_device_vector(offsets); + auto d_dx_aff = make_device_vector(dx_aff_cone); rmm::device_uvector d_out(cones.omega.size(), stream_); - launch_fused_corrector(d_dx_aff, - cones.omega, - cones.w_bar, - cones.inv_eta, - cones.inv_1pw0, - cones.rho, - sigma_mu, - d_out, - d_offsets, - 1); + launch_fused_corrector(d_dx_aff, cones, sigma_mu, d_out); auto actual = copy_to_host(d_out); auto omega_host = copy_to_host(cones.omega); @@ -1535,4 +1595,47 @@ TEST_F(second_order_cone_test, cone_block_scatter_with_q_overlap) } } +TEST_F(second_order_cone_test, recover_cone_dz_matches_reference) +{ + std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; + std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; + std::vector> dx_cones{{0.3, -0.1, 0.2}, {-0.5, 0.2, 0.1, -0.3, 0.15}}; + std::vector dims{3, 5}; + auto offsets = build_offsets(dims); + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + auto d_dx = make_device_vector(pack_cones(dx_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); + launch_nt_scaling(cones, stream_); + + rmm::device_uvector d_rhs(cones.m_c, stream_); + launch_affine_cone_rhs(cones, d_rhs); + + rmm::device_uvector d_hinv2_dx(cones.m_c, stream_); + rmm::device_uvector d_dz(cones.m_c, stream_); + launch_recover_cone_dz(d_dx, cones, d_rhs, d_hinv2_dx, d_dz); + + auto actual = copy_to_host(d_dz); + auto rhs_actual = copy_to_host(d_rhs); + auto w_bar_host = copy_to_host(cones.w_bar); + auto inv_eta_h = copy_to_host(cones.inv_eta); + + for (i_t c = 0; c < static_cast(dims.size()); ++c) { + auto ref_hinv2 = + ref_apply_hinv2_single(dx_cones[c], slice_cone(w_bar_host, offsets, c), inv_eta_h[c]); + auto rhs_c = slice_cone(rhs_actual, offsets, c); + auto act = slice_cone(actual, offsets, c); + std::vector ref(act.size()); + for (i_t j = 0; j < static_cast(ref.size()); ++j) { + ref[j] = -rhs_c[j] - ref_hinv2[j]; + } + expect_vector_near(act, ref, 1e-10, 1e-8, "recover_cone_dz"); + } +} + } // namespace cuopt::linear_programming::dual_simplex::test diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index abfe37c9fd..1bf604bff8 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -174,4 +175,134 @@ TEST(barrier, dual_variable_greater_than) EXPECT_NEAR(solution.z[1], 0.0, 1e-5); } +TEST(barrier, cone_metadata_preserved_through_barrier_setup) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 5; + constexpr int nz = 5; + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective.assign(n, 0.0); + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start.resize(n + 1); + for (int j = 0; j < n; ++j) { + user_problem.A.col_start[j] = j; + user_problem.A.i[j] = 0; + user_problem.A.x[j] = 1.0; + } + user_problem.A.col_start[n] = nz; + user_problem.rhs = {1.0}; + user_problem.row_sense = {'L'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.second_order_cone_dims = {2, 2}; + user_problem.cone_var_start = 1; + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + EXPECT_EQ(original_lp.second_order_cone_dims, user_problem.second_order_cone_dims); + EXPECT_EQ(original_lp.cone_var_start, user_problem.cone_var_start); + + lp_problem_t barrier_lp(user_problem.handle_ptr, + original_lp.num_rows, + original_lp.num_cols, + original_lp.A.col_start[original_lp.num_cols]); + std::vector column_scales; + column_scaling(original_lp, settings, barrier_lp, column_scales); + + EXPECT_EQ(barrier_lp.second_order_cone_dims, user_problem.second_order_cone_dims); + EXPECT_EQ(barrier_lp.cone_var_start, user_problem.cone_var_start); +} + +TEST(barrier, socp_min_x0_subject_to_norm_constraint) +{ + // minimize x_0 + // subject to x_1 = 1 + // (x_0, x_1, x_2) in Q^3 + // + // Optimal: x* = (1, 1, 0), obj* = 1 + + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 3; + constexpr int nz = 1; + + user_problem.num_rows = m; + user_problem.num_cols = n; + + user_problem.objective = {1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 0, 1, 1}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + + user_problem.rhs = {1.0}; + user_problem.row_sense = {'E'}; + + user_problem.lower = {0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "socp_norm_cone"; + + user_problem.cone_var_start = 0; + user_problem.second_order_cone_dims = {3}; + + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.set_log(true); + settings.log.log_to_console = false; + settings.log.enable_log_to_file(); + settings.log.set_log_file("/tmp/socp_barrier_test.log"); + + lp_solution_t solution(m, n); + printf("=== Calling solve_linear_program_with_barrier ===\n"); + fflush(stdout); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + printf("=== status=%d obj=%e x=[%e %e %e] ===\n", + static_cast(status), + solution.objective, + solution.x[0], + solution.x[1], + solution.x[2]); + fflush(stdout); + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[2]), 0.0, 1e-4); +} + } // namespace cuopt::linear_programming::dual_simplex::test From c99368f754ec9fb89008bea158e058a0f1e5f70e Mon Sep 17 00:00:00 2001 From: Yan Zaretskiy Date: Tue, 7 Apr 2026 12:27:08 -0700 Subject: [PATCH 04/68] refactor(dual_simplex): keep cone layout packed through presolve Flatten the remaining SOCP barrier plumbing, remove superseded cone kernels, and harden presolve so linear columns stay ahead of the trailing cone block. --- cpp/src/barrier/barrier.cu | 363 ++-- cpp/src/barrier/second_order_cone.cuh | 1695 +++++++---------- cpp/src/dual_simplex/presolve.cpp | 454 +++-- cpp/src/dual_simplex/scaling.cpp | 12 +- cpp/src/dual_simplex/solve.cpp | 10 + cpp/src/dual_simplex/vector_math.cuh | 21 +- .../infeasibility_information.cu | 6 - .../unit_tests/second_order_cone_test.cu | 656 +++---- .../dual_simplex/unit_tests/solve_barrier.cu | 704 ++++++- 9 files changed, 2199 insertions(+), 1722 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index efa941bfe8..1d937a4c5e 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -45,6 +45,7 @@ #include #include #include +#include namespace cuopt::linear_programming::dual_simplex { @@ -210,10 +211,10 @@ class iteration_data_t { d_dw_residual_(0, lp.handle_ptr->get_stream()), d_wv_residual_(0, lp.handle_ptr->get_stream()), d_bound_rhs_(0, lp.handle_ptr->get_stream()), - d_complementarity_xz_rhs_(lp.num_cols, lp.handle_ptr->get_stream()), + d_complementarity_xz_rhs_(0, lp.handle_ptr->get_stream()), d_complementarity_wv_rhs_(0, lp.handle_ptr->get_stream()), d_dual_rhs_(lp.num_cols, lp.handle_ptr->get_stream()), - d_cone_rhs_term_(0, lp.handle_ptr->get_stream()), + d_complementarity_target_(lp.num_cols, lp.handle_ptr->get_stream()), d_cone_hinv2_dx_(0, lp.handle_ptr->get_stream()), d_Q_diag_(0, lp.handle_ptr->get_stream()), d_Qx_(Qin.m, lp.handle_ptr->get_stream()), @@ -273,12 +274,17 @@ class iteration_data_t { cuopt_assert(cone_var_start_ >= 0, "cone_var_start must be nonnegative"); cuopt_assert(cone_var_start_ + total_cone_dim <= lp.num_cols, "cone variables exceed problem dimension"); + cuopt_assert(cone_var_start_ + total_cone_dim == lp.num_cols, + "barrier expects [linear | cone] layout"); cones_.emplace(static_cast(lp.second_order_cone_dims.size()), lp.second_order_cone_dims, raft::device_span{}, raft::device_span{}, stream_view_); } + i_t linear_xz_rhs_size = lp.num_cols; + if (cones_.has_value() && cones_->K > 0) { linear_xz_rhs_size -= cones_->m_c; } + d_complementarity_xz_rhs_.resize(linear_xz_rhs_size, stream_view_); // Allocating GPU flag data for Form ADAT RAFT_CUDA_TRY(cub::DeviceSelect::Flagged( @@ -1771,7 +1777,7 @@ class iteration_data_t { rmm::device_uvector d_complementarity_xz_rhs_; rmm::device_uvector d_complementarity_wv_rhs_; rmm::device_uvector d_dual_rhs_; - rmm::device_uvector d_cone_rhs_term_; + rmm::device_uvector d_complementarity_target_; rmm::device_uvector d_cone_hinv2_dx_; rmm::device_uvector d_Q_diag_; @@ -2327,10 +2333,32 @@ void barrier_solver_t::gpu_compute_residual_norms(const rmm::device_uv primal_residual_norm = std::max(device_vector_norm_inf(data.d_primal_residual_, stream_view_), device_vector_norm_inf(data.d_bound_residual_, stream_view_)); - dual_residual_norm = device_vector_norm_inf(data.d_dual_residual_, stream_view_); + dual_residual_norm = device_vector_norm_inf(data.d_dual_residual_, stream_view_); + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const i_t linear_xz_size = + has_cones ? data.cone_var_start_ : static_cast(data.d_complementarity_xz_residual_.size()); + auto linear_xz_span = + raft::device_span(data.d_complementarity_xz_residual_.data(), linear_xz_size); complementarity_residual_norm = - std::max(device_vector_norm_inf(data.d_complementarity_xz_residual_, stream_view_), + std::max(device_vector_norm_inf(linear_xz_span, stream_view_), device_vector_norm_inf(data.d_complementarity_wv_residual_, stream_view_)); + if (has_cones) { + f_t cone_complementarity_norm = f_t(0); + auto cone_dot = data.cones_->scratch.hinv2_tail_dot(); + segmented_sum(data.d_complementarity_xz_residual_.data() + data.cone_var_start_, + cuopt::make_span(data.cones_->cone_offsets), + data.cones_->K, + cone_dot, + data.cones_->scratch.segmented_reduce_workspace, + stream_view_); + cone_complementarity_norm = thrust::reduce(rmm::exec_policy(stream_view_), + cone_dot.begin(), + cone_dot.end(), + f_t(0), + thrust::maximum()); + complementarity_residual_norm = + std::max(complementarity_residual_norm, cone_complementarity_norm); + } } template @@ -2404,6 +2432,37 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_tK > 0; const i_t m_c = has_cones ? data.cones_->m_c : 0; const i_t cone_var_start = data.cone_var_start_; + const i_t linear_size = has_cones ? cone_var_start : lp.num_cols; + + auto fill_linear_target = [&](raft::device_span target, + raft::device_span xz_rhs, + raft::device_span x) { + if (target.empty()) return; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(xz_rhs.data(), x.data()), + target.data(), + target.size(), + [] HD(f_t complementarity_xz_rhs, f_t x_val) { return complementarity_xz_rhs / x_val; }, + stream_view_.value()); + RAFT_CHECK_CUDA(stream_view_); + }; + + auto recover_linear_dz = [&](raft::device_span target, + raft::device_span z, + raft::device_span dx_span, + raft::device_span x, + raft::device_span dz_span) { + if (dz_span.empty()) return; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(target.data(), z.data(), dx_span.data(), x.data()), + dz_span.data(), + dz_span.size(), + [] HD(f_t target_val, f_t z_val, f_t dx_val, f_t x_val) { + return target_val - (z_val * dx_val) / x_val; + }, + stream_view_.value()); + RAFT_CHECK_CUDA(stream_view_); + }; { raft::common::nvtx::range fun_scope("Barrier: GPU allocation and copies"); @@ -2449,17 +2508,30 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_tlambda = raft::device_span(data.d_z_.data() + cone_var_start, m_c); launch_nt_scaling(*data.cones_, stream_view_); + cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); + fill_linear_target( + raft::device_span(data.d_complementarity_target_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_x_.data(), linear_size)); + + auto cone_target = + raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c); if (data.cone_combined_step_) { compute_combined_cone_rhs_term( raft::device_span(data.d_dx_aff_.data() + cone_var_start, m_c), *data.cones_, data.cone_sigma_mu_, - data.d_cone_rhs_term_, - stream_view_); + cone_target, + stream_view_, + f_t(-1)); } else { - compute_affine_cone_rhs_term(*data.cones_, data.d_cone_rhs_term_, stream_view_); + compute_affine_cone_rhs_term(*data.cones_, cone_target, stream_view_, f_t(-1)); } RAFT_CHECK_CUDA(stream_view_); + } else { + fill_linear_target(cuopt::make_span(data.d_complementarity_target_), + cuopt::make_span(data.d_complementarity_xz_rhs_), + cuopt::make_span(data.d_x_)); } // Solves the linear system @@ -2556,7 +2628,7 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t(0), - lp.num_cols, - [span_inv_diag = cuopt::make_span(data.d_inv_diag), - span_tmp3 = cuopt::make_span(data.d_tmp3_), - span_tmp4 = cuopt::make_span(data.d_tmp4_), - span_xz_rhs = cuopt::make_span(data.d_complementarity_xz_rhs_), - span_x = cuopt::make_span(data.d_x_), - span_dual_rhs = cuopt::make_span(data.d_dual_rhs_), - span_cone_rhs = cuopt::make_span(data.d_cone_rhs_term_), - cone_start = cone_var_start, - cone_size = m_c] __device__(i_t j) { - bool is_cone = (j >= cone_start && j < cone_start + cone_size); - f_t cone_rhs = is_cone ? span_cone_rhs[j - cone_start] : f_t(0); - f_t xz_term = is_cone ? f_t(0) : -(span_xz_rhs[j] / span_x[j]); - f_t tmp = span_tmp3[j] + xz_term + span_dual_rhs[j] + cone_rhs; - span_tmp3[j] = tmp; - span_tmp4[j] = span_inv_diag[j] * tmp; - }); - } else { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_inv_diag.data(), - data.d_tmp3_.data(), - data.d_complementarity_xz_rhs_.data(), - data.d_x_.data(), - data.d_dual_rhs_.data()), - thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), - lp.num_cols, - [] HD(f_t inv_diag, f_t tmp3, f_t complementarity_xz_rhs, f_t x, f_t dual_rhs) - -> thrust::tuple { - const f_t tmp = tmp3 + -(complementarity_xz_rhs / x) + dual_rhs; - return {tmp, inv_diag * tmp}; - }, - stream_view_.value()); - } + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_inv_diag.data(), + data.d_tmp3_.data(), + data.d_dual_rhs_.data(), + data.d_complementarity_target_.data()), + thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + lp.num_cols, + [] HD(f_t inv_diag, f_t tmp3, f_t dual_rhs, f_t target) -> thrust::tuple { + const f_t tmp = tmp3 + dual_rhs - target; + return {tmp, inv_diag * tmp}; + }, + stream_view_.value()); RAFT_CHECK_CUDA(stream_view_); raft::copy(data.d_r1_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); raft::copy(data.d_r1_prime_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); @@ -2900,40 +2947,28 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_dx_.data() + cone_var_start, m_c), - *data.cones_, - data.d_cone_rhs_term_, - data.d_cone_hinv2_dx_, - raft::device_span(data.d_dz_.data() + cone_var_start, m_c), - stream_view_); - - thrust::for_each_n(rmm::exec_policy(stream_view_), - thrust::make_counting_iterator(0), - data.d_dz_.size(), - [span_xz_rhs = cuopt::make_span(data.d_complementarity_xz_rhs_), - span_z = cuopt::make_span(data.d_z_), - span_dx = cuopt::make_span(data.d_dx_), - span_x = cuopt::make_span(data.d_x_), - span_dz = cuopt::make_span(data.d_dz_), - cone_start = cone_var_start, - cone_size = m_c] __device__(i_t j) { - if (j < cone_start || j >= cone_start + cone_size) { - span_dz[j] = (span_xz_rhs[j] - span_z[j] * span_dx[j]) / span_x[j]; - } - }); + recover_cone_dz_from_target( + raft::device_span(data.d_dx_.data() + cone_var_start, m_c), + *data.cones_, + raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c), + data.d_cone_hinv2_dx_, + raft::device_span(data.d_dz_.data() + cone_var_start, m_c), + stream_view_); + + if (cone_var_start > 0) { + recover_linear_dz( + raft::device_span(data.d_complementarity_target_.data(), cone_var_start), + raft::device_span(data.d_z_.data(), cone_var_start), + raft::device_span(data.d_dx_.data(), cone_var_start), + raft::device_span(data.d_x_.data(), cone_var_start), + raft::device_span(data.d_dz_.data(), cone_var_start)); + } } else { - // dz = (complementarity_xz_rhs - z.* dx) ./ x; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_complementarity_xz_rhs_.data(), - data.d_z_.data(), - data.d_dx_.data(), - data.d_x_.data()), - data.d_dz_.data(), - data.d_dz_.size(), - [] HD(f_t complementarity_xz_rhs, f_t z, f_t dx, f_t x) { - return (complementarity_xz_rhs - z * dx) / x; - }, - stream_view_.value()); + recover_linear_dz(cuopt::make_span(data.d_complementarity_target_), + cuopt::make_span(data.d_z_), + cuopt::make_span(data.d_dx_), + cuopt::make_span(data.d_x_), + cuopt::make_span(data.d_dz_)); } RAFT_CHECK_CUDA(stream_view_); raft::copy(dz.data(), data.d_dz_.data(), data.d_dz_.size(), stream_view_); @@ -2943,18 +2978,29 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t out, + raft::device_span rhs, + raft::device_span z, + raft::device_span dz_span, + raft::device_span dx_span, + raft::device_span x) { + if (out.empty()) return; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(rhs.data(), z.data(), dz_span.data(), dx_span.data(), x.data()), + out.data(), + out.size(), + [] HD(f_t complementarity_xz_rhs, f_t z_val, f_t dz_val, f_t dx_val, f_t x_val) { + return z_val * dx_val + x_val * dz_val - complementarity_xz_rhs; + }, + stream_view_.value()); + }; + compute_linear_xz_residual( + raft::device_span(data.d_xz_residual_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_z_.data(), linear_size), + raft::device_span(data.d_dz_.data(), linear_size), + raft::device_span(data.d_dx_.data(), linear_size), + raft::device_span(data.d_x_.data(), linear_size)); RAFT_CHECK_CUDA(stream_view_); const f_t xz_residual_norm = device_vector_norm_inf(data.d_xz_residual_, stream_view_); @@ -3118,7 +3164,8 @@ template void barrier_solver_t::compute_affine_rhs(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: compute_affine_rhs"); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const i_t linear_size = has_cones ? data.cone_var_start_ : lp.num_cols; data.primal_rhs = data.primal_residual; data.bound_rhs = data.bound_residual; @@ -3126,29 +3173,24 @@ void barrier_solver_t::compute_affine_rhs(iteration_data_t& data.cone_combined_step_ = false; data.cone_sigma_mu_ = f_t(0); - raft::copy(data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_residual_.data(), - data.d_complementarity_xz_residual_.size(), - stream_view_); raft::copy(data.d_complementarity_wv_rhs_.data(), data.d_complementarity_wv_residual_.data(), data.d_complementarity_wv_residual_.size(), stream_view_); - // x.*z -> -x .* z - cub::DeviceTransform::Transform( - data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_rhs_.size(), - [] HD(f_t xz_rhs) { return -xz_rhs; }, - stream_view_.value()); + auto negate_linear_rhs = [&](raft::device_span out, raft::device_span residual) { + if (out.empty()) return; + cub::DeviceTransform::Transform( + residual.data(), + out.data(), + out.size(), + [] HD(f_t xz_rhs) { return -xz_rhs; }, + stream_view_.value()); + }; + negate_linear_rhs( + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_residual_.data(), linear_size)); RAFT_CHECK_CUDA(stream_view_); - if (has_cones) { - thrust::fill_n(rmm::exec_policy(stream_view_), - data.d_complementarity_xz_rhs_.begin() + data.cone_var_start_, - data.cones_->m_c, - f_t(0)); - } // w.*v -> -w .* v cub::DeviceTransform::Transform( data.d_complementarity_wv_rhs_.data(), @@ -3184,21 +3226,24 @@ void barrier_solver_t::compute_target_mu( gpu_max_step_to_boundary(data, data.d_z_, data.d_dz_aff_)); if (has_cones) { - i_t cs = data.cone_var_start_; - i_t mc = data.cones_->m_c; - step_primal_aff = std::min( - step_primal_aff, + i_t cs = data.cone_var_start_; + i_t mc = data.cones_->m_c; + auto [cone_p, cone_d] = compute_cone_step_length(*data.cones_, raft::device_span(data.d_x_.data() + cs, mc), raft::device_span(data.d_dx_aff_.data() + cs, mc), raft::device_span(data.d_z_.data() + cs, mc), raft::device_span(data.d_dz_aff_.data() + cs, mc), - step_primal_aff, - stream_view_)); - step_dual_aff = step_primal_aff; + std::min(step_primal_aff, step_dual_aff), + stream_view_); + f_t cone_aff = std::min(cone_p, cone_d); + step_primal_aff = std::min(step_primal_aff, cone_aff); + step_dual_aff = step_primal_aff; } - if (data.Q.n > 0) { step_primal_aff = step_dual_aff = std::min(step_primal_aff, step_dual_aff); } + if (data.Q.n > 0 || has_cones) { + step_primal_aff = step_dual_aff = std::min(step_primal_aff, step_dual_aff); + } // Compute complementarity_xz_aff_sum = sum(x_aff * z_aff), // where x_aff = x + step_primal_aff * dx_aff and z_aff = z + step_dual_aff * dz_aff @@ -3256,21 +3301,30 @@ void barrier_solver_t::compute_target_mu( } mu_aff = complementarity_aff_sum / mu_denom; sigma = std::max(0.0, std::min(1.0, std::pow(mu_aff / mu, 3.0))); - new_mu = sigma * mu_aff; + new_mu = sigma * mu; } template void barrier_solver_t::compute_cc_rhs(iteration_data_t& data, f_t& new_mu) { raft::common::nvtx::range fun_scope("Barrier: compute_cc_rhs"); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const i_t linear_size = has_cones ? data.cone_var_start_ : lp.num_cols; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_dx_aff_.data(), data.d_dz_aff_.data()), - data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_rhs_.size(), - [new_mu] HD(f_t dx_aff, f_t dz_aff) { return -(dx_aff * dz_aff) + new_mu; }, - stream_view_.value()); + auto fill_linear_cc_rhs = [&](raft::device_span out, + raft::device_span dx_aff, + raft::device_span dz_aff) { + if (out.empty()) return; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(dx_aff.data(), dz_aff.data()), + out.data(), + out.size(), + [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val) { return -(dx_aff_val * dz_aff_val) + new_mu; }, + stream_view_.value()); + }; + fill_linear_cc_rhs(raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_dx_aff_.data(), linear_size), + raft::device_span(data.d_dz_aff_.data(), linear_size)); RAFT_CHECK_CUDA(stream_view_); cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_dw_aff_.data(), data.d_dv_aff_.data()), @@ -3279,13 +3333,6 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data [new_mu] HD(f_t dw_aff, f_t dv_aff) { return -(dw_aff * dv_aff) + new_mu; }, stream_view_.value()); RAFT_CHECK_CUDA(stream_view_); - if (has_cones) { - thrust::fill_n(rmm::exec_policy(stream_view_), - data.d_complementarity_xz_rhs_.begin() + data.cone_var_start_, - data.cones_->m_c, - f_t(0)); - } - // TMP should be CPU to 0 if CPU and GPU to 0 if GPU data.primal_rhs.set_scalar(0.0); data.bound_rhs.set_scalar(0.0); @@ -3376,18 +3423,14 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ if (has_cones) { i_t cs = data.cone_var_start_; i_t mc = data.cones_->m_c; - f_t cone_primal = - compute_single_cone_step_length(*data.cones_, - raft::device_span(data.d_x_.data() + cs, mc), - raft::device_span(data.d_dx_.data() + cs, mc), - f_t(1), - stream_view_); - f_t cone_dual = - compute_single_cone_step_length(*data.cones_, - raft::device_span(data.d_z_.data() + cs, mc), - raft::device_span(data.d_dz_.data() + cs, mc), - f_t(1), - stream_view_); + auto [cone_primal, cone_dual] = + compute_cone_step_length(*data.cones_, + raft::device_span(data.d_x_.data() + cs, mc), + raft::device_span(data.d_dx_.data() + cs, mc), + raft::device_span(data.d_z_.data() + cs, mc), + raft::device_span(data.d_dz_.data() + cs, mc), + f_t(1), + stream_view_); max_step_primal = std::min(max_step_primal, cone_primal); max_step_dual = std::min(max_step_dual, cone_dual); } @@ -3395,7 +3438,7 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ step_primal = step_scale * max_step_primal; step_dual = step_scale * max_step_dual; - if (data.Q.n > 0) { step_primal = step_dual = std::min(step_primal, step_dual); } + if (data.Q.n > 0 || has_cones) { step_primal = step_dual = std::min(step_primal, step_dual); } } template @@ -3821,9 +3864,28 @@ lp_status_t barrier_solver_t::solve(f_t start_time, std::max(vector_norm_inf(data.primal_residual, stream_view_), vector_norm_inf(data.bound_residual, stream_view_)); f_t dual_residual_norm = vector_norm_inf(data.dual_residual, stream_view_); + const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const i_t linear_xz_size = + has_cones ? data.cone_var_start_ : static_cast(data.complementarity_xz_residual.size()); + auto linear_xz_span = + raft::host_span(data.complementarity_xz_residual.data(), linear_xz_size); f_t complementarity_residual_norm = - std::max(vector_norm_inf(data.complementarity_xz_residual, stream_view_), + std::max(vector_norm_inf(linear_xz_span, stream_view_), vector_norm_inf(data.complementarity_wv_residual, stream_view_)); + if (has_cones) { + f_t cone_complementarity_norm = f_t(0); + i_t off = data.cone_var_start_; + for (auto q_k : lp.second_order_cone_dims) { + f_t cone_dot = f_t(0); + for (i_t j = 0; j < q_k; ++j) { + cone_dot += data.complementarity_xz_residual[off + j]; + } + cone_complementarity_norm = std::max(cone_complementarity_norm, cone_dot); + off += q_k; + } + complementarity_residual_norm = + std::max(complementarity_residual_norm, cone_complementarity_norm); + } f_t mu_denom = static_cast(n) + static_cast(num_upper_bounds); if (data.cones_.has_value() && data.cones_->K > 0) { mu_denom -= static_cast(data.cones_->m_c); @@ -3880,11 +3942,14 @@ lp_status_t barrier_solver_t::solve(f_t start_time, (duality_gap_abs < settings.barrier_relative_complementarity_tol || duality_gap_rel < settings.barrier_relative_complementarity_tol); + const i_t linear_xz_rhs_size = + has_cones ? data.cone_var_start_ : static_cast(data.complementarity_xz_rhs.size()); + data.d_complementarity_xz_residual_.resize(data.complementarity_xz_residual.size(), stream_view_); data.d_complementarity_wv_residual_.resize(data.complementarity_wv_residual.size(), stream_view_); - data.d_complementarity_xz_rhs_.resize(data.complementarity_xz_rhs.size(), stream_view_); + data.d_complementarity_xz_rhs_.resize(linear_xz_rhs_size, stream_view_); data.d_complementarity_wv_rhs_.resize(data.complementarity_wv_rhs.size(), stream_view_); raft::copy(data.d_complementarity_xz_residual_.data(), data.complementarity_xz_residual.data(), @@ -3896,7 +3961,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, stream_view_); raft::copy(data.d_complementarity_xz_rhs_.data(), data.complementarity_xz_rhs.data(), - data.complementarity_xz_rhs.size(), + linear_xz_rhs_size, stream_view_); raft::copy(data.d_complementarity_wv_rhs_.data(), data.complementarity_wv_rhs.data(), diff --git a/cpp/src/barrier/second_order_cone.cuh b/cpp/src/barrier/second_order_cone.cuh index 4d69e787d1..3487707f12 100644 --- a/cpp/src/barrier/second_order_cone.cuh +++ b/cpp/src/barrier/second_order_cone.cuh @@ -15,814 +15,319 @@ #include #include -#include - -#include +#include #include #include #include -#include #include +#include #include -#include #include namespace cuopt::linear_programming::dual_simplex { // --------------------------------------------------------------------------- -// Shared reduction primitives -// --------------------------------------------------------------------------- - -template -using triplet_t = cuda::std::tuple; - -template -struct triplet_sum { - DI triplet_t operator()(const triplet_t& lhs, const triplet_t& rhs) const - { - const auto& [v0_l, v1_l, v2_l] = lhs; - const auto& [v0_r, v1_r, v2_r] = rhs; - return {v0_l + v0_r, v1_l + v1_r, v2_l + v2_r}; - } -}; - -template -using block_reduce_t = cub::BlockReduce; - -template -struct smem_reduce_t { - using ScalarReduce = block_reduce_t; - using TripletReduce = block_reduce_t, BLOCK_DIM>; - - union { - typename ScalarReduce::TempStorage scalar_temp; - typename TripletReduce::TempStorage triplet_temp; - f_t scalar_broadcast; - triplet_t triplet_broadcast; - }; -}; - -// --------------------------------------------------------------------------- -// reduce_broadcast: block-reduce a value, then broadcast to all threads. +// Flat cone kernels: segmented reductions compute per-cone scalars, then a +// single elementwise launch applies the result across all packed cone entries. +// This keeps the cone math vectorized instead of one block per cone. // --------------------------------------------------------------------------- -template -DI f_t reduce_broadcast(f_t val, smem_reduce_t& s) -{ - f_t agg = typename smem_reduce_t::ScalarReduce(s.scalar_temp).Sum(val); - __syncthreads(); - if (threadIdx.x == 0) { s.scalar_broadcast = agg; } - __syncthreads(); - return s.scalar_broadcast; -} +constexpr int flat_block_dim = 256; -template -DI triplet_t reduce_broadcast(triplet_t val, smem_reduce_t& s) +template +__global__ void apply_hinv2_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w_bar, + raft::device_span inv_eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + f_t output_scale) { - auto agg = typename smem_reduce_t::TripletReduce(s.triplet_temp) - .Reduce(val, triplet_sum{}); - __syncthreads(); - if (threadIdx.x == 0) { s.triplet_broadcast = agg; } - __syncthreads(); - return s.triplet_broadcast; + i_t flat_idx = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (flat_idx >= static_cast(out.size())) return; + + i_t cone = element_cone_ids[flat_idx]; + i_t cone_off = cone_offsets[cone]; + i_t local_idx = flat_idx - cone_off; + + f_t ie_sq = inv_eta[cone] * inv_eta[cone]; + f_t u_tv = w_bar[cone_off] * v[cone_off] - tail_dot[cone]; + f_t coeff = f_t(2) * u_tv * ie_sq; + int sign = (local_idx == 0) * 2 - 1; + f_t value = coeff * w_bar[flat_idx] - ie_sq * v[flat_idx]; + out[flat_idx] = output_scale * value * sign; } -template -struct smem_warp_reduce_t { - static constexpr int warps_per_block = BLOCK_DIM / 32; - - using ScalarReduce = cub::WarpReduce; - using TripletReduce = cub::WarpReduce, 32>; - - union { - typename ScalarReduce::TempStorage scalar_temp[warps_per_block]; - typename TripletReduce::TempStorage triplet_temp[warps_per_block]; - f_t scalar_broadcast[warps_per_block]; - triplet_t triplet_broadcast[warps_per_block]; - }; +template +struct corrector_raw_t { + f_t zeta; + f_t xi; + f_t psi; }; -// --------------------------------------------------------------------------- -// reduce_broadcast: warp-reduce a value, then broadcast within the warp. -// --------------------------------------------------------------------------- - -template -DI f_t reduce_broadcast(f_t val, smem_warp_reduce_t& s) -{ - static_assert(BLOCK_DIM % 32 == 0, "Warp reduce requires warp-aligned CTAs"); - - int warp = threadIdx.x >> 5; - int lane = threadIdx.x & 31; - f_t agg = typename smem_warp_reduce_t::ScalarReduce(s.scalar_temp[warp]).Sum(val); - if (lane == 0) { s.scalar_broadcast[warp] = agg; } - __syncwarp(); - return s.scalar_broadcast[warp]; -} - -template -DI triplet_t reduce_broadcast(triplet_t val, smem_warp_reduce_t& s) -{ - static_assert(BLOCK_DIM % 32 == 0, "Warp reduce requires warp-aligned CTAs"); - - int warp = threadIdx.x >> 5; - int lane = threadIdx.x & 31; - auto agg = typename smem_warp_reduce_t::TripletReduce(s.triplet_temp[warp]) - .Reduce(val, triplet_sum{}); - if (lane == 0) { s.triplet_broadcast[warp] = agg; } - __syncwarp(); - return s.triplet_broadcast[warp]; -} - -// --------------------------------------------------------------------------- -// Apply H^{-1} to one vector per cone (one thread-block per cone). -// -// H^{-1}z = (1/η)(w̄₀z₀ − ζ, z₁ + (−z₀ + ζ/(1+w̄₀))w̄₁), ζ = w̄₁ᵀz₁ -// --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv_kernel( - raft::device_span z, - raft::device_span out, - raft::device_span w_bar, - raft::device_span inv_eta, - raft::device_span inv_1pw0, - raft::device_span cone_offsets, - i_t K) -{ - __shared__ smem_reduce_t smem; - - i_t cone = static_cast(blockIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - auto w_cone = w_bar.subspan(off, q); - auto z_cone = z.subspan(off, q); - auto out_cone = out.subspan(off, q); - - f_t z0 = z_cone[0]; - f_t w0 = w_cone[0]; - - // Phase 1: ζ = w̄₁ᵀ z₁ - f_t partial = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - partial += w_cone[j] * z_cone[j]; +template +struct corrector_raw_sum_t { + HD corrector_raw_t operator()(const corrector_raw_t& lhs, + const corrector_raw_t& rhs) const + { + return {lhs.zeta + rhs.zeta, lhs.xi + rhs.xi, lhs.psi + rhs.psi}; } - f_t zeta = reduce_broadcast(partial, smem); - - // Phase 2: element-wise output - f_t ie = inv_eta[cone]; - f_t ipw = inv_1pw0[cone]; - f_t coeff = -z0 + zeta * ipw; +}; - if (threadIdx.x == 0) { out_cone[0] = (w0 * z0 - zeta) * ie; } - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - out_cone[j] = (z_cone[j] + coeff * w_cone[j]) * ie; +template +struct cone_scratch_t { + i_t K; + rmm::device_uvector> corrector_raw; // [K] {zeta, xi, psi} + rmm::device_uvector scalar_slots; // [6 * K] reusable K-length scalar scratch slots + rmm::device_uvector step_alpha_primal; + rmm::device_uvector step_alpha_dual; + rmm::device_uvector segmented_reduce_workspace; + + cone_scratch_t(i_t K_in, rmm::cuda_stream_view stream) + : K(K_in), + corrector_raw(K_in, stream), + scalar_slots(6 * K_in, stream), + step_alpha_primal(K_in, stream), + step_alpha_dual(K_in, stream), + segmented_reduce_workspace(0, stream) + { } -} -// --------------------------------------------------------------------------- -// Apply H^{-2} to one vector per cone (one thread-block per cone). -// -// H^{-2}v = η⁻²(2u(uᵀv) − Jv), u = Jw̄, J = diag(1,−1,…,−1). -// -// One dot product (uᵀv) plus element-wise work — same structure as apply_Hinv. -// --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void apply_Hinv2_kernel( - raft::device_span v, - raft::device_span out, - raft::device_span w_bar, - raft::device_span inv_eta, - raft::device_span cone_offsets, - i_t K) -{ - __shared__ smem_reduce_t smem; + raft::device_span hinv2_tail_dot() { return slot_span(0); } + raft::device_span step_s_du1_sq() { return slot_span(0); } + raft::device_span step_s_u1du1() { return slot_span(1); } + raft::device_span step_s_u1_sq() { return slot_span(2); } + raft::device_span step_l_du1_sq() { return slot_span(3); } + raft::device_span step_l_u1du1() { return slot_span(4); } + raft::device_span step_l_u1_sq() { return slot_span(5); } - i_t cone = static_cast(blockIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - auto w_cone = w_bar.subspan(off, q); - auto v_cone = v.subspan(off, q); - auto out_cone = out.subspan(off, q); + raft::device_span nt_s1_sq() { return slot_span(0); } + raft::device_span nt_l1_sq() { return slot_span(1); } + raft::device_span nt_sl() { return slot_span(2); } - f_t v0 = v_cone[0]; - f_t w0 = w_cone[0]; - - // Phase 1: uᵀv = w̄₀v₀ − Σ w̄_j v_j (tail dot, then subtract from head) - f_t partial = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - partial += w_cone[j] * v_cone[j]; - } - f_t tail_dot = reduce_broadcast(partial, smem); - f_t uTv = w0 * v0 - tail_dot; + raft::device_span step_alpha_primal_span() { return cuopt::make_span(step_alpha_primal); } + raft::device_span step_alpha_dual_span() { return cuopt::make_span(step_alpha_dual); } - // Phase 2: element-wise output - f_t ie_sq = inv_eta[cone] * inv_eta[cone]; - f_t coeff = f_t(2) * uTv * ie_sq; - - if (threadIdx.x == 0) { out_cone[0] = coeff * w0 - ie_sq * v0; } - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - out_cone[j] = -coeff * w_cone[j] + ie_sq * v_cone[j]; + private: + raft::device_span slot_span(i_t slot) + { + return raft::device_span(scalar_slots.data() + slot * K, K); } -} - -// --------------------------------------------------------------------------- -// Cone-algebra primitives for the deferred combined-step corrector: -// r_K = omega circ omega + dx_scaled circ dz_scaled - sigma mu e -// corr = omega \ r_K -// t_K = H^{-1} corr -// --------------------------------------------------------------------------- +}; -// --------------------------------------------------------------------------- -// Jordan product for packed SOC vectors (one CTA per cone). -// -// For a, b in Q^q: (a ∘ b)_0 = a^T b, (a ∘ b)_j = a_0 b_j + b_0 a_j. -// --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void jordan_product_kernel( - raft::device_span a, - raft::device_span b, - raft::device_span out, - raft::device_span cone_offsets, - i_t K) +template +__global__ void fused_corrector_write_kernel(raft::device_span s, + raft::device_span lambda, + raft::device_span dx_aff, + raft::device_span omega, + raft::device_span w_bar, + raft::device_span inv_eta, + raft::device_span inv_1pw0, + raft::device_span rho, + raft::device_span> raw, + raft::device_span out, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + f_t sigma_mu, + f_t output_scale) { - __shared__ smem_reduce_t smem; - - i_t cone = static_cast(blockIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - auto a_cone = a.subspan(off, q); - auto b_cone = b.subspan(off, q); - auto out_cone = out.subspan(off, q); - - f_t a0 = a_cone[0]; - f_t b0 = b_cone[0]; - - f_t partial = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - partial += a_cone[j] * b_cone[j]; + i_t flat_idx = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (flat_idx >= static_cast(out.size())) return; + + i_t cone = element_cone_ids[flat_idx]; + i_t cone_off = cone_offsets[cone]; + i_t local_idx = flat_idx - cone_off; + f_t ie = inv_eta[cone]; + f_t ipw = inv_1pw0[cone]; + f_t w0 = w_bar[cone_off]; + f_t omega0 = omega[cone_off]; + f_t dx_a0 = dx_aff[cone_off]; + auto raw_vals = raw[cone]; + f_t coeff_a = -dx_a0 + raw_vals.zeta * ipw; + f_t dx0 = (w0 * dx_a0 - raw_vals.zeta) * ie; + f_t dz0 = -omega0 - dx0; + f_t w_sq_sum = max(f_t(0), w0 * w0 - f_t(1)); + f_t w_omega_sum = f_t(0.5) * (ie * s[cone_off] - lambda[cone_off] / ie); + f_t omega_sq_sum = max(f_t(0), omega0 * omega0 - rho[cone]); + f_t omega_dx_sum = ie * (raw_vals.xi + coeff_a * w_omega_sum); + f_t dx_sq_sum = + ie * ie * (raw_vals.psi + f_t(2) * coeff_a * raw_vals.zeta + coeff_a * coeff_a * w_sq_sum); + f_t r_K_0 = (omega0 * omega0 + omega_sq_sum) + (dx0 * dz0 - omega_dx_sum - dx_sq_sum) - sigma_mu; + f_t nu = (f_t(2) * omega0 - dx0) * omega_sq_sum - (omega0 + f_t(2) * dx0) * omega_dx_sum; + f_t inv_rho = f_t(1) / rho[cone]; + f_t corr0 = (omega0 * r_K_0 - nu) * inv_rho; + f_t inv_omega0 = f_t(1) / omega0; + f_t c_inv = (nu * inv_omega0 - r_K_0) * inv_rho; + f_t p1 = c_inv + f_t(2) - dx0 * inv_omega0; + f_t p2 = -(f_t(1) + f_t(2) * dx0 * inv_omega0); + f_t w_dx_sum = ie * (raw_vals.zeta + coeff_a * w_sq_sum); + f_t zeta2 = p1 * w_omega_sum + p2 * w_dx_sum; + f_t coeff_c = -corr0 + zeta2 * ipw; + + if (local_idx == 0) { + out[flat_idx] = output_scale * ((w0 * corr0 - zeta2) * ie); + return; } - f_t tail_dot = reduce_broadcast(partial, smem); - if (threadIdx.x == 0) { out_cone[0] = a0 * b0 + tail_dot; } - - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - out_cone[j] = a0 * b_cone[j] + b0 * a_cone[j]; - } + f_t dx_j = (dx_aff[flat_idx] + coeff_a * w_bar[flat_idx]) * ie; + f_t corr_j = p1 * omega[flat_idx] + p2 * dx_j; + out[flat_idx] = output_scale * ((corr_j + coeff_c * w_bar[flat_idx]) * ie); } // --------------------------------------------------------------------------- -// Inverse Jordan product for packed SOC vectors (one CTA per cone). -// -// For omega in int(Q^q) and vector r, -// (omega \ r)_0 = (omega_0 r_0 − nu) / rho -// (omega \ r)_j = ((nu/omega_0 − r_0)/rho) omega_j + r_j/omega_0 -// where nu = omega_1^T r_1 and rho = ||omega||_J^2 (stored per-cone). +// Flattened NT scaling / step-length kernels. +// All follow the same pattern: segmented reduction to per-cone scalars, then +// flat or scalar kernels to write the packed cone outputs. // --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void inverse_jordan_product_kernel( - raft::device_span omega, - raft::device_span r, - raft::device_span rho, - raft::device_span out, - raft::device_span cone_offsets, - i_t K) -{ - __shared__ smem_reduce_t smem; - - i_t cone = static_cast(blockIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - auto omega_cone = omega.subspan(off, q); - auto r_cone = r.subspan(off, q); - auto out_cone = out.subspan(off, q); - - f_t omega_0 = omega_cone[0]; - f_t r_0 = r_cone[0]; - f_t partial = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - partial += omega_cone[j] * r_cone[j]; - } - f_t nu = reduce_broadcast(partial, smem); - - f_t rho_val = rho[cone]; - f_t inv_rho = f_t(1) / rho_val; - f_t c_omega_j = ((nu / omega_0) - r_0) * inv_rho; - f_t c_r_j = f_t(1) / omega_0; - - if (threadIdx.x == 0) { out_cone[0] = (omega_0 * r_0 - nu) * inv_rho; } - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - out_cone[j] = c_omega_j * omega_cone[j] + c_r_j * r_cone[j]; - } -} - -// --------------------------------------------------------------------------- -// Fused corrector for the combined-step SOC correction (one CTA per cone). -// -// Computes in a single kernel launch: -// 1. dx = H^{-1} Δx_aff (affine scaled direction) -// 2. dz = −ω − dx (complementary direction) -// 3. r_K = ω∘ω + dx∘dz − σμ e (combined cone residual) -// 4. corr = ω \ r_K (inverse Jordan product) -// 5. t_K = H^{-1} corr (corrector for reduced RHS) -// -// Uses the `out` buffer as scratch (holds dx during phases 1–3) and writes -// the final t_K there, so zero extra temporary buffers are needed. -// -// Algebraic shortcut: the triplet (Σ ω_j², Σ ω_j dx_j, Σ dx_j²) computed -// for r_K_0 also yields ν = Σ ω_j r_K_j via a linear combination, avoiding -// a fourth reduction pass. -// --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void fused_corrector_kernel( - raft::device_span dx_aff, - raft::device_span omega, - raft::device_span w_bar, - raft::device_span inv_eta, - raft::device_span inv_1pw0, - raft::device_span rho, - f_t sigma_mu, - raft::device_span out, - raft::device_span cone_offsets, - i_t K) +template +__global__ void nt_scaling_scalar_kernel(raft::device_span s, + raft::device_span lambda, + raft::device_span cone_offsets, + raft::device_span s1_sq, + raft::device_span l1_sq, + raft::device_span sl, + raft::device_span inv_eta, + raft::device_span inv_1pw0, + raft::device_span w_bar, + raft::device_span omega, + raft::device_span rho, + i_t K) { - __shared__ smem_reduce_t smem; - - i_t cone = static_cast(blockIdx.x); + i_t cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); if (cone >= K) return; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - auto dx_a = dx_aff.subspan(off, q); - auto omega_cone = omega.subspan(off, q); - auto w_cone = w_bar.subspan(off, q); - auto out_cone = out.subspan(off, q); - - f_t ie = inv_eta[cone]; - f_t ipw = inv_1pw0[cone]; - f_t rho_val = rho[cone]; - f_t omega_0 = omega_cone[0]; - f_t w_0 = w_cone[0]; - f_t dx_a_0 = dx_a[0]; - - // ================================================================= - // Phase A — reduce ζ = Σ_{j≥1} w̄_j (Δx_aff)_j for H^{-1} - // ================================================================= - f_t partial = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - partial += w_cone[j] * dx_a[j]; - } - f_t zeta = reduce_broadcast(partial, smem); - - f_t dx_0 = (w_0 * dx_a_0 - zeta) * ie; - f_t coeff_a = -dx_a_0 + zeta * ipw; - f_t dz_0 = -omega_0 - dx_0; - - // ================================================================= - // Phase A→B — write dx to out; accumulate (A, B, C) for r_K and ν - // A = Σ ω_j², B = Σ ω_j dx_j, C = Σ dx_j² (j ≥ 1) - // ================================================================= - auto trip = triplet_t{}; - auto& [A_p, B_p, C_p] = trip; - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - f_t dx_j = (dx_a[j] + coeff_a * w_cone[j]) * ie; - out_cone[j] = dx_j; - f_t omega_j = omega_cone[j]; - A_p += omega_j * omega_j; - B_p += omega_j * dx_j; - C_p += dx_j * dx_j; - } - auto [A, B, C] = reduce_broadcast(trip, smem); - - // ================================================================= - // Phase B — form r_K_0, derive ν, then inverse-Jordan scalars - // ================================================================= - f_t r_K_0 = (omega_0 * omega_0 + A) + (dx_0 * dz_0 - B - C) - sigma_mu; - f_t nu = (f_t(2) * omega_0 - dx_0) * A - (omega_0 + f_t(2) * dx_0) * B; - - f_t inv_rho = f_t(1) / rho_val; - f_t corr_0 = (omega_0 * r_K_0 - nu) * inv_rho; - f_t inv_omega_0 = f_t(1) / omega_0; - f_t c_inv = (nu * inv_omega_0 - r_K_0) * inv_rho; - f_t p1 = c_inv + f_t(2) - dx_0 * inv_omega_0; - f_t p2 = -(f_t(1) + f_t(2) * dx_0 * inv_omega_0); - - // ================================================================= - // Phase B→C — accumulate ζ₂ = Σ_{j≥1} w̄_j corr_j for final H^{-1} - // corr_j = p1 ω_j + p2 dx_j (dx_j still in out_cone[j]) - // ================================================================= - f_t partial2 = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - f_t corr_j = p1 * omega_cone[j] + p2 * out_cone[j]; - partial2 += w_cone[j] * corr_j; - } - f_t zeta2 = reduce_broadcast(partial2, smem); - - // ================================================================= - // Phase C — write t_K = H^{-1}(corr) - // ================================================================= - f_t coeff_c = -corr_0 + zeta2 * ipw; - - if (threadIdx.x == 0) { out_cone[0] = (w_0 * corr_0 - zeta2) * ie; } - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - f_t corr_j = p1 * omega_cone[j] + p2 * out_cone[j]; - out_cone[j] = (corr_j + coeff_c * w_cone[j]) * ie; - } -} - -// --------------------------------------------------------------------------- -// Compute NT scaling from (s, lambda). -// -// Medium/large cones use one CTA per cone and stream s/lambda twice: -// Pass 1: reduce ||s_1||^2, ||lambda_1||^2, and s^T lambda. -// Pass 2: compute omega/w_bar directly from raw inputs and reduce ||w_bar_1||^2. -// -// Small cones (q <= 32) use one warp per cone and keep one element per lane in -// registers for the whole computation. In both paths, shared memory only stores -// per-warp partial reductions plus a small scalar broadcast struct. -// --------------------------------------------------------------------------- - -constexpr int small_cone_limit = 32; -constexpr int medium_cone_limit = 2048; -constexpr int small_block_dim = 64; -constexpr int medium_block_dim = 128; -constexpr int large_block_dim = 256; - -template -struct nt_broadcast_coeffs { - f_t w_from_s; - f_t w_from_lambda; - f_t omega_s_coeff; - f_t omega_lambda_coeff; -}; - -template -struct nt_block_storage { - smem_reduce_t reduce; - nt_broadcast_coeffs coeffs; -}; - -template -struct nt_warp_storage { - static constexpr int warps_per_block = BLOCK_DIM / 32; - - smem_warp_reduce_t reduce; - nt_broadcast_coeffs coeffs[warps_per_block]; -}; - -template -__global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_kernel( - raft::device_span s, - raft::device_span lambda, - raft::device_span eta, - raft::device_span inv_eta, - raft::device_span inv_1pw0, - raft::device_span w_bar, - raft::device_span omega, - raft::device_span rho, - raft::device_span cone_offsets, - raft::device_span cone_ids, - i_t num_cones) -{ - static_assert(BLOCK_DIM % 32 == 0, "NT scaling kernel requires warp-aligned BLOCK_DIM"); - __shared__ nt_block_storage storage; - - i_t cone_idx = static_cast(blockIdx.x); - if (cone_idx >= num_cones) return; - - i_t cone = cone_ids[cone_idx]; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - - f_t s0 = s[off]; - f_t l0 = lambda[off]; - - auto partial = triplet_t{}; - auto& [s1_sq_p, l1_sq_p, sl_p] = partial; - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - f_t sj = s[off + j]; - f_t lj = lambda[off + j]; - s1_sq_p += sj * sj; - l1_sq_p += lj * lj; - sl_p += sj * lj; - } - - auto [s1_sq, l1_sq, sl] = reduce_broadcast(partial, storage.reduce); - f_t owner_eta = f_t(0); - f_t owner_inv_eta = f_t(0); - f_t owner_rho = f_t(0); - f_t owner_omega_0 = f_t(0); - if (threadIdx.x == 0) { - // Clamp radicands to zero: near the cone boundary, roundoff can make these - // slightly negative. - f_t s_J = sqrt(max(f_t(0), s0 * s0 - s1_sq)); - f_t l_J = sqrt(max(f_t(0), l0 * l0 - l1_sq)); - f_t inv_s_J = f_t(1) / s_J; - f_t inv_l_J = f_t(1) / l_J; - owner_rho = s_J * l_J; - owner_eta = sqrt(s_J / l_J); - owner_inv_eta = f_t(1) / owner_eta; - f_t scale = sqrt(owner_rho); - - f_t s_dot_l = (s0 * l0 + sl) * inv_s_J * inv_l_J; - f_t gamma = sqrt(max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); - f_t inv_2g = f_t(1) / (f_t(2) * gamma); - f_t sb0 = s0 * inv_s_J; - f_t lb0 = l0 * inv_l_J; - f_t D = sb0 + lb0 + f_t(2) * gamma; - f_t inv_D = f_t(1) / D; - f_t c_s = (gamma + sb0) * inv_D; - f_t c_l = (gamma + lb0) * inv_D; - - storage.coeffs.w_from_s = inv_2g * inv_s_J; - storage.coeffs.w_from_lambda = -inv_2g * inv_l_J; - // Name these by the raw tail element they multiply: - // omega_j = omega_s_coeff * s_j + omega_lambda_coeff * lambda_j. - // The closed-form NT expression is cross-coupled, so c_l multiplies s_j - // and c_s multiplies lambda_j. - storage.coeffs.omega_s_coeff = scale * c_l * inv_s_J; - storage.coeffs.omega_lambda_coeff = scale * c_s * inv_l_J; - owner_omega_0 = gamma * scale; - } - __syncthreads(); - - f_t w1_sq_partial = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - f_t sj = s[off + j]; - f_t lj = lambda[off + j]; - f_t wj = storage.coeffs.w_from_s * sj + storage.coeffs.w_from_lambda * lj; - w_bar[off + j] = wj; - omega[off + j] = storage.coeffs.omega_s_coeff * sj + storage.coeffs.omega_lambda_coeff * lj; - w1_sq_partial += wj * wj; - } - - f_t w1_sq = reduce_broadcast(w1_sq_partial, storage.reduce); - if (threadIdx.x == 0) { - f_t w0 = sqrt(f_t(1) + w1_sq); - omega[off] = owner_omega_0; - w_bar[off] = w0; - eta[cone] = owner_eta; - inv_eta[cone] = owner_inv_eta; - inv_1pw0[cone] = f_t(1) / (f_t(1) + w0); - rho[cone] = owner_rho; - } + i_t off = cone_offsets[cone]; + f_t s0 = s[off]; + f_t l0 = lambda[off]; + + f_t s_J = sqrt(max(f_t(0), s0 * s0 - s1_sq[cone])); + f_t l_J = sqrt(max(f_t(0), l0 * l0 - l1_sq[cone])); + f_t inv_s_J = f_t(1) / s_J; + f_t inv_l_J = f_t(1) / l_J; + f_t rho_val = s_J * l_J; + f_t inv_eta_v = sqrt(l_J / s_J); + f_t scale = sqrt(rho_val); + + f_t s_dot_l = (s0 * l0 + sl[cone]) * inv_s_J * inv_l_J; + f_t gamma = sqrt(max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); + f_t inv_2g = f_t(1) / (f_t(2) * gamma); + f_t sb0 = s0 * inv_s_J; + f_t lb0 = l0 * inv_l_J; + + f_t w0 = (sb0 + lb0) * inv_2g; + inv_eta[cone] = inv_eta_v; + inv_1pw0[cone] = f_t(1) / (f_t(1) + w0); + w_bar[off] = w0; + omega[off] = gamma * scale; + rho[cone] = rho_val; } -template -__global__ __launch_bounds__(BLOCK_DIM) void nt_scaling_small_kernel( - raft::device_span s, - raft::device_span lambda, - raft::device_span eta, - raft::device_span inv_eta, - raft::device_span inv_1pw0, - raft::device_span w_bar, - raft::device_span omega, - raft::device_span rho, - raft::device_span cone_offsets, - raft::device_span cone_ids, - i_t num_cones) +template +__global__ void nt_scaling_tail_kernel(raft::device_span s, + raft::device_span lambda, + raft::device_span inv_eta, + raft::device_span rho, + raft::device_span w_bar, + raft::device_span omega, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) { - static_assert(BLOCK_DIM % 32 == 0, "Small-cone NT kernel requires warp-aligned CTAs"); - __shared__ nt_warp_storage storage; - - constexpr int warps_per_block = BLOCK_DIM / 32; - i_t warp_idx = - static_cast(blockIdx.x) * warps_per_block + static_cast(threadIdx.x >> 5); - if (warp_idx >= num_cones) return; - - int warp = threadIdx.x >> 5; - int lane = threadIdx.x & 31; - i_t cone = cone_ids[warp_idx]; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - - f_t sj = (lane < q) ? s[off + lane] : f_t(0); - f_t lj = (lane < q) ? lambda[off + lane] : f_t(0); - - auto partial = triplet_t{(lane > 0 && lane < q) ? sj * sj : f_t(0), - (lane > 0 && lane < q) ? lj * lj : f_t(0), - (lane > 0 && lane < q) ? sj * lj : f_t(0)}; - auto [s1_sq, l1_sq, sl] = reduce_broadcast(partial, storage.reduce); - - f_t owner_eta = f_t(0); - f_t owner_inv_eta = f_t(0); - f_t owner_rho = f_t(0); - f_t owner_omega_0 = f_t(0); - - if (lane == 0) { - f_t s0 = sj; - f_t l0 = lj; - f_t s_J = sqrt(max(f_t(0), s0 * s0 - s1_sq)); - f_t l_J = sqrt(max(f_t(0), l0 * l0 - l1_sq)); - f_t inv_s_J = f_t(1) / s_J; - f_t inv_l_J = f_t(1) / l_J; - owner_rho = s_J * l_J; - owner_eta = sqrt(s_J / l_J); - owner_inv_eta = f_t(1) / owner_eta; - f_t scale = sqrt(owner_rho); - - f_t s_dot_l = (s0 * l0 + sl) * inv_s_J * inv_l_J; - f_t gamma = sqrt(max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); - f_t inv_2g = f_t(1) / (f_t(2) * gamma); - f_t sb0 = s0 * inv_s_J; - f_t lb0 = l0 * inv_l_J; - f_t D = sb0 + lb0 + f_t(2) * gamma; - f_t inv_D = f_t(1) / D; - f_t c_s = (gamma + sb0) * inv_D; - f_t c_l = (gamma + lb0) * inv_D; - - storage.coeffs[warp].w_from_s = inv_2g * inv_s_J; - storage.coeffs[warp].w_from_lambda = -inv_2g * inv_l_J; - storage.coeffs[warp].omega_s_coeff = scale * c_l * inv_s_J; - storage.coeffs[warp].omega_lambda_coeff = scale * c_s * inv_l_J; - owner_omega_0 = gamma * scale; - } - __syncwarp(); - - f_t w1_sq = f_t(0); - if (lane > 0 && lane < q) { - f_t wj = storage.coeffs[warp].w_from_s * sj + storage.coeffs[warp].w_from_lambda * lj; - w_bar[off + lane] = wj; - omega[off + lane] = - storage.coeffs[warp].omega_s_coeff * sj + storage.coeffs[warp].omega_lambda_coeff * lj; - w1_sq = wj * wj; - } - w1_sq = reduce_broadcast(w1_sq, storage.reduce); - - if (lane == 0) { - f_t w0 = sqrt(f_t(1) + w1_sq); - omega[off] = owner_omega_0; - w_bar[off] = w0; - eta[cone] = owner_eta; - inv_eta[cone] = owner_inv_eta; - inv_1pw0[cone] = f_t(1) / (f_t(1) + w0); - rho[cone] = owner_rho; - } + i_t flat_idx = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (flat_idx >= static_cast(w_bar.size())) return; + + i_t cone = element_cone_ids[flat_idx]; + i_t cone_off = cone_offsets[cone]; + if (flat_idx == cone_off) return; + + f_t s0 = s[cone_off]; + f_t l0 = lambda[cone_off]; + f_t inv_eta_val = inv_eta[cone]; + f_t rho_val = rho[cone]; + f_t scale = sqrt(rho_val); + + f_t s_J = scale / inv_eta_val; + f_t l_J = scale * inv_eta_val; + f_t inv_s_J = f_t(1) / s_J; + f_t inv_l_J = f_t(1) / l_J; + + f_t gamma = omega[cone_off] / scale; + f_t inv_2g = f_t(1) / (f_t(2) * gamma); + f_t sb0 = s0 * inv_s_J; + f_t lb0 = l0 * inv_l_J; + f_t D = sb0 + lb0 + f_t(2) * gamma; + f_t inv_D = f_t(1) / D; + f_t c_s = (gamma + sb0) * inv_D; + f_t c_l = (gamma + lb0) * inv_D; + + f_t w_from_s = inv_2g * inv_s_J; + f_t w_from_lambda = -inv_2g * inv_l_J; + f_t omega_s_coeff = scale * c_l * inv_s_J; + f_t omega_lambda_coeff = scale * c_s * inv_l_J; + + f_t sj = s[flat_idx]; + f_t lj = lambda[flat_idx]; + w_bar[flat_idx] = w_from_s * sj + w_from_lambda * lj; + omega[flat_idx] = omega_s_coeff * sj + omega_lambda_coeff * lj; } -// --------------------------------------------------------------------------- -// Step length for a single (u, du) pair in Q^q. -// -// Finds the largest alpha in [0, alpha_max] such that u + alpha*du in Q^q. -// The cone condition u_0 + alpha*du_0 >= ||u_1 + alpha*du_1|| reduces to a -// linear test plus a quadratic a*alpha^2 + 2b*alpha + c >= 0 where -// a = du_0^2 - ||du_1||^2, b = u_0*du_0 - u_1^T du_1, c = u_0^2 - ||u_1||^2. -// --------------------------------------------------------------------------- -template -DI f_t -cone_step_length_single(raft::device_span u, - raft::device_span du, - typename block_reduce_t, BLOCK_DIM>::TempStorage& temp, - f_t alpha) +template +DI f_t cone_step_length_from_scalars(f_t u0, f_t du0, f_t du1_sq, f_t u1du1, f_t c, f_t alpha_max) { - i_t q = static_cast(u.size()); - auto partial = triplet_t{}; - auto& [du1_sq_p, u1du1_p, u1_sq_p] = partial; - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - f_t uj = u[j]; - f_t duj = du[j]; - du1_sq_p += duj * duj; - u1du1_p += uj * duj; - u1_sq_p += uj * uj; - } - - auto [du1_sq, u1du1, u1_sq] = - block_reduce_t, BLOCK_DIM>(temp).Reduce(partial, triplet_sum{}); - __syncthreads(); - - if (threadIdx.x == 0) { - f_t a = du[0] * du[0] - du1_sq; - f_t b = u[0] * du[0] - u1du1; - f_t c = max(f_t(0), u[0] * u[0] - u1_sq); - f_t disc = b * b - a * c; - - // Linear constraint: u_0 + alpha * du_0 >= 0. - if (du[0] < f_t(0)) { alpha = min(alpha, -u[0] / du[0]); } - - // Quadratic constraint. - if ((a > f_t(0) && b > f_t(0)) || disc < f_t(0)) { - // No positive root (parabola stays non-negative for alpha > 0). - } else if (a == f_t(0)) { - // Degenerate: 2b*alpha + c = 0. - if (b < f_t(0)) { alpha = min(alpha, c / (f_t(-2) * b)); } - } else if (c == f_t(0)) { - // Starting exactly on the cone boundary: take a full step only if the - // direction stays in the cone, otherwise the maximum feasible step is 0. - alpha = (a >= f_t(0)) ? alpha : f_t(0); - } else { - f_t t = -(b + copysign(sqrt(disc), b)); - f_t r1 = c / t; - f_t r2 = t / a; - if (r1 < f_t(0)) { r1 = alpha; } - if (r2 < f_t(0)) { r2 = alpha; } - alpha = min(alpha, min(r1, r2)); - } + f_t a = du0 * du0 - du1_sq; + f_t b = u0 * du0 - u1du1; + f_t disc = b * b - a * c; + f_t alpha = alpha_max; + + if (du0 < f_t(0)) { alpha = min(alpha, -u0 / du0); } + if ((a > f_t(0) && b > f_t(0)) || disc < f_t(0)) { + return alpha; + } else if (a == f_t(0)) { + if (b < f_t(0)) { alpha = min(alpha, c / (f_t(-2) * b)); } + } else if (c == f_t(0)) { + alpha = (a >= f_t(0)) ? alpha : f_t(0); + } else { + f_t t = -(b + copysign(sqrt(disc), b)); + f_t r1 = c / t; + f_t r2 = t / a; + if (r1 < f_t(0)) { r1 = alpha; } + if (r2 < f_t(0)) { r2 = alpha; } + alpha = min(alpha, min(r1, r2)); } return alpha; } -// --------------------------------------------------------------------------- -// Cone step length kernel (one block per cone). -// -// Computes, for each cone i, the largest alpha in [0, alpha_max] such that -// s_i + alpha * ds_i in Q^{q_i} AND lambda_i + alpha * dlambda_i in Q^{q_i}. -// The per-cone result is written to alpha[i]. -// --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void step_length_kernel( - raft::device_span s, - raft::device_span ds, - raft::device_span lambda, - raft::device_span dlambda, - raft::device_span alpha, - raft::device_span cone_offsets, - i_t K, - f_t alpha_max) -{ - __shared__ typename block_reduce_t, BLOCK_DIM>::TempStorage temp_storage; - - i_t cone = static_cast(blockIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - - f_t alpha_s = cone_step_length_single( - s.subspan(off, q), ds.subspan(off, q), temp_storage, alpha_max); - f_t alpha_l = cone_step_length_single( - lambda.subspan(off, q), dlambda.subspan(off, q), temp_storage, alpha_max); - - if (threadIdx.x == 0) { alpha[cone] = min(alpha_s, alpha_l); } -} - -// --------------------------------------------------------------------------- -// Single-variable cone step length kernel (one block per cone). -// Like step_length_kernel but only checks u + alpha*du in Q^{q_i}. -// --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void step_length_single_kernel( - raft::device_span u, - raft::device_span du, - raft::device_span alpha, - raft::device_span cone_offsets, - i_t K, - f_t alpha_max) -{ - __shared__ typename block_reduce_t, BLOCK_DIM>::TempStorage temp_storage; - - i_t cone = static_cast(blockIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - - f_t a = cone_step_length_single( - u.subspan(off, q), du.subspan(off, q), temp_storage, alpha_max); - - if (threadIdx.x == 0) { alpha[cone] = a; } -} - -// --------------------------------------------------------------------------- -// Shift u into int(Q^q) if it is not already interior (one block per cone). -// -// alpha(u) = ||u_1|| - u_0. If alpha >= 0 (u on boundary or outside): -// u_0 <- u_0 + 1 + max(0, alpha) (shift along identity element e) -// -// Modifies u in place. Used once during initial-point computation. -// --------------------------------------------------------------------------- -template -__global__ __launch_bounds__(BLOCK_DIM) void interior_shift_kernel( - raft::device_span u, raft::device_span cone_offsets, i_t K) +template +__global__ void step_length_pair_kernel(raft::device_span s, + raft::device_span ds, + raft::device_span lambda, + raft::device_span dlambda, + raft::device_span alpha_primal, + raft::device_span alpha_dual, + raft::device_span s_du1_sq, + raft::device_span s_u1du1, + raft::device_span s_u1_sq, + raft::device_span l_du1_sq, + raft::device_span l_u1du1, + raft::device_span l_u1_sq, + raft::device_span cone_offsets, + f_t alpha_max, + i_t K) { - __shared__ typename block_reduce_t::TempStorage temp_storage; - - i_t cone = static_cast(blockIdx.x); + i_t cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); if (cone >= K) return; i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; + f_t s_c = max(f_t(0), s[off] * s[off] - s_u1_sq[cone]); + f_t l_c = max(f_t(0), lambda[off] * lambda[off] - l_u1_sq[cone]); - f_t tail_sq = f_t(0); - for (i_t j = 1 + static_cast(threadIdx.x); j < q; j += BLOCK_DIM) { - f_t v = u[off + j]; - tail_sq += v * v; - } - tail_sq = block_reduce_t(temp_storage).Sum(tail_sq); - - if (threadIdx.x == 0) { - f_t u1_norm = sqrt(tail_sq); - f_t gap = u1_norm - u[off]; - if (gap >= f_t(0)) { u[off] += f_t(1) + gap; } - } + alpha_primal[cone] = cone_step_length_from_scalars( + s[off], ds[off], s_du1_sq[cone], s_u1du1[cone], s_c, alpha_max); + alpha_dual[cone] = cone_step_length_from_scalars( + lambda[off], dlambda[off], l_du1_sq[cone], l_u1du1[cone], l_c, alpha_max); } /** @@ -835,9 +340,9 @@ __global__ __launch_bounds__(BLOCK_DIM) void interior_shift_kernel( * caller to cover the cone portion of the global x/z vectors. The caller * must keep the underlying memory alive. * - * Search directions, RHS vectors, and workspace live directly in - * iteration_data_t (matching the existing LP/QP pattern where dx_aff, dual_rhs, - * etc. are all top-level fields of iteration_data_t). + * Only persistent cone state lives here. Reusable per-iteration workspace sits + * under `scratch`, which keeps the mutating temporary buffers out of the + * persistent NT state. */ template struct cone_data_t { @@ -848,21 +353,21 @@ struct cone_data_t { rmm::device_uvector cone_offsets; // [K+1] prefix sums of cone_dims rmm::device_uvector cone_dims; // [K] dimension q_i of each cone rmm::device_uvector block_offsets; // [K+1] prefix sums of q_i^2 (for dense block build) + rmm::device_uvector block_entry_cone_ids; // [sum q_i^2] owning cone id for each block entry // --- Primal/dual cone iterates (non-owning views, set by caller) --- raft::device_span s; // [m_c] cone slack: s_i in int(Q^{q_i}) raft::device_span lambda; // [m_c] cone dual: lambda_i in int(Q^{q_i}) // --- NT scaling state (recomputed each iteration from s, lambda) --- - rmm::device_uvector eta; // [K] scaling factor eta_i = (||s_i||_J / ||lambda_i||_J)^{1/2} - rmm::device_uvector inv_eta; // [K] cached 1/eta_i + rmm::device_uvector + inv_eta; // [K] 1/eta_i where eta_i = (||s_i||_J / ||lambda_i||_J)^{1/2} rmm::device_uvector inv_1pw0; // [K] cached 1/(1 + wbar_0_i) rmm::device_uvector w_bar; // [m_c] NT scaling direction, unit J-norm, packed by cone rmm::device_uvector omega; // [m_c] scaled variable omega_i = H_i^{-1} s_i, packed by cone rmm::device_uvector rho; // [K] ||omega_i||^2_J = ||s_i||_J * ||lambda_i||_J - rmm::device_uvector small_cone_ids; // [n_small] cone ids with q <= 32 - rmm::device_uvector medium_cone_ids; // [n_medium] cone ids with 32 < q <= 2048 - rmm::device_uvector large_cone_ids; // [n_large] cone ids with q > 2048 + rmm::device_uvector element_cone_ids; // [m_c] owning cone id for each packed entry + cone_scratch_t scratch; cone_data_t(i_t K_in, const std::vector& dims, @@ -874,34 +379,31 @@ struct cone_data_t { cone_offsets(K_in + 1, stream), cone_dims(K_in, stream), block_offsets(K_in + 1, stream), + block_entry_cone_ids( + std::accumulate( + dims.begin(), dims.end(), i_t(0), [](i_t acc, i_t q) { return acc + q * q; }), + stream), s(s_in), lambda(lambda_in), - eta(K_in, stream), inv_eta(K_in, stream), inv_1pw0(K_in, stream), w_bar(m_c, stream), omega(m_c, stream), rho(K_in, stream), - small_cone_ids(0, stream), - medium_cone_ids(0, stream), - large_cone_ids(0, stream) + element_cone_ids(m_c, stream), + scratch(K_in, stream) { std::vector offsets(K + 1, 0); std::vector blk_offsets(K + 1, 0); - std::vector small_ids; - std::vector medium_ids; - std::vector large_ids; + std::vector cone_ids(m_c, 0); + std::vector block_cone_ids(block_entry_cone_ids.size(), 0); for (i_t i = 0; i < K; ++i) { offsets[i + 1] = offsets[i] + dims[i]; blk_offsets[i + 1] = blk_offsets[i] + dims[i] * dims[i]; - if (dims[i] <= small_cone_limit) { - small_ids.push_back(i); - } else if (dims[i] <= medium_cone_limit) { - medium_ids.push_back(i); - } else { - large_ids.push_back(i); - } + std::fill(cone_ids.begin() + offsets[i], cone_ids.begin() + offsets[i + 1], i); + std::fill( + block_cone_ids.begin() + blk_offsets[i], block_cone_ids.begin() + blk_offsets[i + 1], i); } auto init_device_vec = [&](auto& d_vec, const auto& h_vec) { @@ -914,86 +416,229 @@ struct cone_data_t { raft::copy(cone_offsets.data(), offsets.data(), K + 1, stream); raft::copy(cone_dims.data(), dims.data(), K, stream); raft::copy(block_offsets.data(), blk_offsets.data(), K + 1, stream); - init_device_vec(small_cone_ids, small_ids); - init_device_vec(medium_cone_ids, medium_ids); - init_device_vec(large_cone_ids, large_ids); + init_device_vec(block_entry_cone_ids, block_cone_ids); + init_device_vec(element_cone_ids, cone_ids); } }; +template +void segmented_sum(InputIt input, + raft::device_span cone_offsets, + i_t K, + raft::device_span out, + rmm::device_uvector& workspace, + rmm::cuda_stream_view stream) +{ + if (K == 0) return; + cuopt_assert(static_cast(out.size()) == K, "segmented_sum output must match cone count"); + + std::size_t temp_storage_bytes = 0; + cub::DeviceSegmentedReduce::Sum(nullptr, + temp_storage_bytes, + input, + out.data(), + K, + cone_offsets.data(), + cone_offsets.data() + 1, + stream.value()); + if (workspace.size() < temp_storage_bytes) { workspace.resize(temp_storage_bytes, stream); } + cub::DeviceSegmentedReduce::Sum(workspace.data(), + temp_storage_bytes, + input, + out.data(), + K, + cone_offsets.data(), + cone_offsets.data() + 1, + stream.value()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +template +void segmented_reduce(InputIt input, + raft::device_span cone_offsets, + i_t K, + rmm::device_uvector& out, + rmm::device_uvector& workspace, + ReduceOp reduce_op, + t_t initial_value, + rmm::cuda_stream_view stream) +{ + out.resize(K, stream); + if (K == 0) return; + + std::size_t temp_storage_bytes = 0; + cub::DeviceSegmentedReduce::Reduce(nullptr, + temp_storage_bytes, + input, + out.data(), + K, + cone_offsets.data(), + cone_offsets.data() + 1, + reduce_op, + initial_value, + stream.value()); + if (workspace.size() < temp_storage_bytes) { workspace.resize(temp_storage_bytes, stream); } + cub::DeviceSegmentedReduce::Reduce(workspace.data(), + temp_storage_bytes, + input, + out.data(), + K, + cone_offsets.data(), + cone_offsets.data() + 1, + reduce_op, + initial_value, + stream.value()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + template -void compute_affine_cone_rhs_term(const cone_data_t& cones, - rmm::device_uvector& out, - rmm::cuda_stream_view stream) +void apply_hinv2(raft::device_span v, + raft::device_span out, + raft::device_span w_bar, + raft::device_span inv_eta, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + raft::device_span tail_dot, + rmm::device_uvector& workspace, + i_t K, + rmm::cuda_stream_view stream, + f_t output_scale = f_t(1)) { - out.resize(cones.m_c, stream); - if (cones.K == 0) return; + if (K == 0) return; - apply_Hinv2_kernel - <<>>(cones.s, - cuopt::make_span(out), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.cone_offsets), - cones.K); + auto span_v = v; + auto span_w_bar = w_bar; + auto span_cone_offsets = cone_offsets; + auto span_element_cone_ids = element_cone_ids; + auto tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_v, span_w_bar, span_cone_offsets, span_element_cone_ids] HD(i_t idx) { + i_t cone = span_element_cone_ids[idx]; + i_t cone_off = span_cone_offsets[cone]; + return (idx == cone_off) ? f_t(0) : span_w_bar[idx] * span_v[idx]; + }); + segmented_sum(tail_terms, cone_offsets, K, tail_dot, workspace, stream); + + i_t grid_dim = (static_cast(out.size()) + flat_block_dim - 1) / flat_block_dim; + apply_hinv2_write_kernel<<>>( + v, out, w_bar, inv_eta, tail_dot, cone_offsets, element_cone_ids, output_scale); RAFT_CUDA_TRY(cudaPeekAtLastError()); } +template +void apply_hinv2(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream, + f_t output_scale = f_t(1)) +{ + apply_hinv2(v, + out, + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.cone_offsets), + cuopt::make_span(cones.element_cone_ids), + cones.scratch.hinv2_tail_dot(), + cones.scratch.segmented_reduce_workspace, + cones.K, + stream, + output_scale); +} + +template +void compute_affine_cone_rhs_term(cone_data_t& cones, + raft::device_span out, + rmm::cuda_stream_view stream, + f_t output_scale = f_t(1)) +{ + cuopt_assert(static_cast(out.size()) == cones.m_c, "cone rhs span must match cone size"); + if (cones.K == 0) return; + + apply_hinv2(cones.s, out, cones, stream, output_scale); +} + template void compute_combined_cone_rhs_term(raft::device_span dx_aff, - const cone_data_t& cones, + cone_data_t& cones, f_t sigma_mu, - rmm::device_uvector& out, - rmm::cuda_stream_view stream) + raft::device_span out, + rmm::cuda_stream_view stream, + f_t output_scale = f_t(1)) { - out.resize(cones.m_c, stream); + cuopt_assert(static_cast(out.size()) == cones.m_c, "cone rhs span must match cone size"); if (cones.K == 0) return; - fused_corrector_kernel - <<>>(dx_aff, - cuopt::make_span(cones.omega), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.inv_1pw0), - cuopt::make_span(cones.rho), - sigma_mu, - cuopt::make_span(out), - cuopt::make_span(cones.cone_offsets), - cones.K); + auto span_dx_aff = dx_aff; + auto span_w_bar = cuopt::make_span(cones.w_bar); + auto span_omega = cuopt::make_span(cones.omega); + auto span_cone_offsets = cuopt::make_span(cones.cone_offsets); + auto span_element_cone_id = cuopt::make_span(cones.element_cone_ids); + + auto raw_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_dx_aff, span_w_bar, span_omega, span_cone_offsets, span_element_cone_id] HD(i_t idx) { + i_t cone = span_element_cone_id[idx]; + i_t cone_off = span_cone_offsets[cone]; + if (idx == cone_off) { return corrector_raw_t{f_t(0), f_t(0), f_t(0)}; } + f_t dx_aff_j = span_dx_aff[idx]; + return corrector_raw_t{ + span_w_bar[idx] * dx_aff_j, span_omega[idx] * dx_aff_j, dx_aff_j * dx_aff_j}; + }); + segmented_reduce>(raw_terms, + cuopt::make_span(cones.cone_offsets), + cones.K, + cones.scratch.corrector_raw, + cones.scratch.segmented_reduce_workspace, + corrector_raw_sum_t{}, + corrector_raw_t{f_t(0), f_t(0), f_t(0)}, + stream); + + i_t grid_dim = (cones.m_c + flat_block_dim - 1) / flat_block_dim; + fused_corrector_write_kernel + <<>>(cones.s, + cones.lambda, + dx_aff, + cuopt::make_span(cones.omega), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.inv_1pw0), + cuopt::make_span(cones.rho), + cuopt::make_span(cones.scratch.corrector_raw), + out, + cuopt::make_span(cones.cone_offsets), + cuopt::make_span(cones.element_cone_ids), + sigma_mu, + output_scale); RAFT_CUDA_TRY(cudaPeekAtLastError()); } template -void recover_cone_dz(raft::device_span dx, - const cone_data_t& cones, - const rmm::device_uvector& cone_rhs_term, - rmm::device_uvector& hinv2_dx, - raft::device_span dz, - rmm::cuda_stream_view stream) +void recover_cone_dz_from_target(raft::device_span dx, + cone_data_t& cones, + raft::device_span cone_target, + rmm::device_uvector& hinv2_dx, + raft::device_span dz, + rmm::cuda_stream_view stream) { hinv2_dx.resize(cones.m_c, stream); if (cones.K == 0) return; - apply_Hinv2_kernel - <<>>(dx, - cuopt::make_span(hinv2_dx), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.cone_offsets), - cones.K); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + apply_hinv2(dx, cuopt::make_span(hinv2_dx), cones, stream); - thrust::for_each_n( - rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - cones.m_c, - [span_rhs = cuopt::make_span(cone_rhs_term), - span_hinv2 = cuopt::make_span(hinv2_dx), - span_dz = dz] __device__(i_t j) { span_dz[j] = -span_rhs[j] - span_hinv2[j]; }); + auto span_target = cone_target; + auto span_hinv2 = cuopt::make_span(hinv2_dx); + auto span_dz = dz; + thrust::for_each_n(rmm::exec_policy(stream), + thrust::make_counting_iterator(0), + cones.m_c, + [span_target, span_hinv2, span_dz] __device__(i_t j) { + span_dz[j] = span_target[j] - span_hinv2[j]; + }); } template void accumulate_cone_hinv2_matvec(raft::device_span x, - const cone_data_t& cones, + cone_data_t& cones, rmm::device_uvector& hinv2_x, raft::device_span out, rmm::cuda_stream_view stream) @@ -1001,21 +646,14 @@ void accumulate_cone_hinv2_matvec(raft::device_span x, hinv2_x.resize(cones.m_c, stream); if (cones.K == 0) return; - apply_Hinv2_kernel - <<>>(x, - cuopt::make_span(hinv2_x), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.cone_offsets), - cones.K); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + apply_hinv2(x, cuopt::make_span(hinv2_x), cones, stream); + auto span_hinv2 = cuopt::make_span(hinv2_x); + auto span_out = out; thrust::for_each_n(rmm::exec_policy(stream), thrust::make_counting_iterator(0), cones.m_c, - [span_hinv2 = cuopt::make_span(hinv2_x), span_out = out] __device__(i_t j) { - span_out[j] += span_hinv2[j]; - }); + [span_hinv2, span_out] __device__(i_t j) { span_out[j] += span_hinv2[j]; }); } // --------------------------------------------------------------------------- @@ -1026,11 +664,44 @@ void accumulate_cone_hinv2_matvec(raft::device_span x, // - `csr_indices[e]` gives the destination slot in `augmented_x` // - `q_values[e]` stores any pre-merged Q contribution for that slot // -// For each flat entry we identify its owning cone from `block_offsets`, -// recover local (r, c) coordinates, evaluate H_k^{-2}(r, c), and scatter +// For each flat entry we load its precomputed owning cone id, recover local +// (r, c) coordinates, evaluate H_k^{-2}(r, c), and write // -(H_k^{-2}(r, c) + q_values[e]) // into `augmented_x[csr_indices[e]]`. // --------------------------------------------------------------------------- +template +__global__ void scatter_hinv2_into_augmented_kernel( + raft::device_span augmented_x, + raft::device_span csr_indices, + raft::device_span q_values, + raft::device_span w_bar, + raft::device_span inv_eta, + raft::device_span cone_offsets, + raft::device_span block_offsets, + raft::device_span block_entry_cone_ids) +{ + i_t e = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (e >= static_cast(csr_indices.size())) return; + + i_t cone = block_entry_cone_ids[e]; + i_t off = cone_offsets[cone]; + i_t q = cone_offsets[cone + 1] - off; + i_t blk_off = block_offsets[cone]; + i_t local = e - blk_off; + i_t r = local / q; + i_t c = local % q; + + f_t ie_sq = inv_eta[cone] * inv_eta[cone]; + f_t w0 = w_bar[off]; + f_t u_r = (r == 0) ? w0 : -w_bar[off + r]; + f_t u_c = (c == 0) ? w0 : -w_bar[off + c]; + f_t val = f_t(2) * u_r * ie_sq * u_c; + f_t diag_correction = (r == 0) ? -ie_sq : ie_sq; + if (r == c) { val += diag_correction; } + + augmented_x[csr_indices[e]] = -val - q_values[e]; +} + template void scatter_hinv2_into_augmented(const cone_data_t& cones, rmm::device_uvector& augmented_x, @@ -1041,156 +712,246 @@ void scatter_hinv2_into_augmented(const cone_data_t& cones, i_t count = static_cast(csr_indices.size()); if (count == 0) return; - auto values = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [span_w_bar = cuopt::make_span(cones.w_bar), - span_inv_eta = cuopt::make_span(cones.inv_eta), - span_block_offsets = cuopt::make_span(cones.block_offsets), - span_cone_offsets = cuopt::make_span(cones.cone_offsets), - span_q_values = cuopt::make_span(q_values)] __device__(i_t e) -> f_t { - i_t lo = 0; - i_t hi = static_cast(span_block_offsets.size()) - 1; - while (lo + 1 < hi) { - i_t mid = lo + (hi - lo) / 2; - if (span_block_offsets[mid] <= e) { - lo = mid; - } else { - hi = mid; - } - } - i_t cone = lo; - i_t off = span_cone_offsets[cone]; - i_t q = span_cone_offsets[cone + 1] - off; - i_t blk_off = span_block_offsets[cone]; - i_t local = e - blk_off; - i_t r = local / q; - i_t c = local % q; - - f_t ie_sq = span_inv_eta[cone] * span_inv_eta[cone]; - f_t w0 = span_w_bar[off]; - f_t u_r = (r == 0) ? w0 : -span_w_bar[off + r]; - f_t u_c = (c == 0) ? w0 : -span_w_bar[off + c]; - f_t val = f_t(2) * u_r * ie_sq * u_c; - f_t diag_correction = (r == 0) ? -ie_sq : ie_sq; - if (r == c) { val += diag_correction; } - - return -val - span_q_values[e]; - }); + cuopt_assert(count == static_cast(cones.block_entry_cone_ids.size()), + "scatter expects one flat entry per cone-block coefficient"); - thrust::scatter( - rmm::exec_policy(stream), values, values + count, csr_indices.begin(), augmented_x.begin()); + i_t grid_dim = (count + flat_block_dim - 1) / flat_block_dim; + scatter_hinv2_into_augmented_kernel + <<>>(cuopt::make_span(augmented_x), + cuopt::make_span(csr_indices), + cuopt::make_span(q_values), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.cone_offsets), + cuopt::make_span(cones.block_offsets), + cuopt::make_span(cones.block_entry_cone_ids)); + RAFT_CUDA_TRY(cudaPeekAtLastError()); } // --------------------------------------------------------------------------- -// Compute the maximum feasible step length for the cone portion of (x, z). -// -// Launches step_length_kernel (one CTA per cone), then reduces the per-cone -// results to a single scalar. Returns min over all cones of the step length -// that keeps both x_K + alpha*dx_K and z_K + alpha*dz_K in their cones. +// Compute per-cone step lengths, then reduce them to the global maximum +// feasible primal/dual step. // --------------------------------------------------------------------------- template -f_t compute_cone_step_length(const cone_data_t& cones, - raft::device_span x_K, - raft::device_span dx_K, - raft::device_span z_K, - raft::device_span dz_K, - f_t alpha_max, - rmm::cuda_stream_view stream) +void compute_cone_step_length_per_cone(cone_data_t& cones, + raft::device_span x_K, + raft::device_span dx_K, + raft::device_span z_K, + raft::device_span dz_K, + raft::device_span alpha_primal, + raft::device_span alpha_dual, + f_t alpha_max, + rmm::cuda_stream_view stream) { - if (cones.K == 0) return alpha_max; - - rmm::device_uvector d_alpha(cones.K, stream); - step_length_kernel - <<>>(x_K, - dx_K, - z_K, - dz_K, - cuopt::make_span(d_alpha), - cuopt::make_span(cones.cone_offsets), - cones.K, - alpha_max); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - - f_t result = thrust::reduce( - rmm::exec_policy(stream), d_alpha.begin(), d_alpha.end(), alpha_max, thrust::minimum()); - return result; -} - -template -f_t compute_single_cone_step_length(const cone_data_t& cones, - raft::device_span u_K, - raft::device_span du_K, - f_t alpha_max, - rmm::cuda_stream_view stream) -{ - if (cones.K == 0) return alpha_max; + cuopt_assert(static_cast(alpha_primal.size()) == cones.K && + static_cast(alpha_dual.size()) == cones.K, + "step-length outputs must match cone count"); + if (cones.K == 0) return; - rmm::device_uvector d_alpha(cones.K, stream); - step_length_single_kernel<<>>( - u_K, du_K, cuopt::make_span(d_alpha), cuopt::make_span(cones.cone_offsets), cones.K, alpha_max); + auto span_offsets = cuopt::make_span(cones.cone_offsets); + auto span_elem = cuopt::make_span(cones.element_cone_ids); + + auto s_du1_sq = cones.scratch.step_s_du1_sq(); + auto s_u1du1 = cones.scratch.step_s_u1du1(); + auto s_u1_sq = cones.scratch.step_s_u1_sq(); + auto l_du1_sq = cones.scratch.step_l_du1_sq(); + auto l_u1du1 = cones.scratch.step_l_u1du1(); + auto l_u1_sq = cones.scratch.step_l_u1_sq(); + + auto span_x_K = x_K; + auto span_dx_K = dx_K; + auto span_z_K = z_K; + auto span_dz_K = dz_K; + + auto s_du1_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), [span_dx_K, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_dx_K[idx] * span_dx_K[idx]; + }); + segmented_sum(s_du1_sq_terms, + span_offsets, + cones.K, + s_du1_sq, + cones.scratch.segmented_reduce_workspace, + stream); + + auto s_u1du1_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_x_K, span_dx_K, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_x_K[idx] * span_dx_K[idx]; + }); + segmented_sum(s_u1du1_terms, + span_offsets, + cones.K, + s_u1du1, + cones.scratch.segmented_reduce_workspace, + stream); + + auto s_u1_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), [span_x_K, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_x_K[idx] * span_x_K[idx]; + }); + segmented_sum(s_u1_sq_terms, + span_offsets, + cones.K, + s_u1_sq, + cones.scratch.segmented_reduce_workspace, + stream); + + auto l_du1_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), [span_dz_K, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_dz_K[idx] * span_dz_K[idx]; + }); + segmented_sum(l_du1_sq_terms, + span_offsets, + cones.K, + l_du1_sq, + cones.scratch.segmented_reduce_workspace, + stream); + + auto l_u1du1_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_z_K, span_dz_K, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_z_K[idx] * span_dz_K[idx]; + }); + segmented_sum(l_u1du1_terms, + span_offsets, + cones.K, + l_u1du1, + cones.scratch.segmented_reduce_workspace, + stream); + + auto l_u1_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), [span_z_K, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_z_K[idx] * span_z_K[idx]; + }); + segmented_sum(l_u1_sq_terms, + span_offsets, + cones.K, + l_u1_sq, + cones.scratch.segmented_reduce_workspace, + stream); + + i_t grid_dim = (cones.K + flat_block_dim - 1) / flat_block_dim; + step_length_pair_kernel<<>>(x_K, + dx_K, + z_K, + dz_K, + alpha_primal, + alpha_dual, + s_du1_sq, + s_u1du1, + s_u1_sq, + l_du1_sq, + l_u1du1, + l_u1_sq, + span_offsets, + alpha_max, + cones.K); RAFT_CUDA_TRY(cudaPeekAtLastError()); - - return thrust::reduce( - rmm::exec_policy(stream), d_alpha.begin(), d_alpha.end(), alpha_max, thrust::minimum()); } -// --------------------------------------------------------------------------- -// Shift cone slices of a vector into the strict interior of their cones. -// Operates on a subspan of the global vector (pre-sliced to cone portion). -// --------------------------------------------------------------------------- template -void launch_interior_shift(raft::device_span u_K, - const cone_data_t& cones, - rmm::cuda_stream_view stream) +std::pair compute_cone_step_length(cone_data_t& cones, + raft::device_span x_K, + raft::device_span dx_K, + raft::device_span z_K, + raft::device_span dz_K, + f_t alpha_max, + rmm::cuda_stream_view stream) { - if (cones.K == 0) return; - interior_shift_kernel - <<>>(u_K, cuopt::make_span(cones.cone_offsets), cones.K); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + if (cones.K == 0) return {alpha_max, alpha_max}; + + auto alpha_primal = cones.scratch.step_alpha_primal_span(); + auto alpha_dual = cones.scratch.step_alpha_dual_span(); + + compute_cone_step_length_per_cone( + cones, x_K, dx_K, z_K, dz_K, alpha_primal, alpha_dual, alpha_max, stream); + + f_t primal = thrust::reduce(rmm::exec_policy(stream), + alpha_primal.begin(), + alpha_primal.end(), + alpha_max, + thrust::minimum()); + f_t dual = thrust::reduce(rmm::exec_policy(stream), + alpha_dual.begin(), + alpha_dual.end(), + alpha_max, + thrust::minimum()); + return {primal, dual}; } template void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) { - auto launch_streaming_bucket = [&](auto& cone_ids, auto block_dim_ic) { - constexpr int block_dim = std::remove_cvref_t::value; - i_t bucket_size = static_cast(cone_ids.size()); - if (bucket_size == 0) return; + if (cones.K == 0) return; - nt_scaling_kernel - <<>>(cones.s, + auto nt_s1_sq = cones.scratch.nt_s1_sq(); + auto nt_l1_sq = cones.scratch.nt_l1_sq(); + auto nt_sl = cones.scratch.nt_sl(); + + auto span_s = cones.s; + auto span_lambda = cones.lambda; + auto span_offsets = cuopt::make_span(cones.cone_offsets); + auto span_elem = cuopt::make_span(cones.element_cone_ids); + + auto s1_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), [span_s, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_s[idx] * span_s[idx]; + }); + segmented_sum( + s1_sq_terms, span_offsets, cones.K, nt_s1_sq, cones.scratch.segmented_reduce_workspace, stream); + + auto l1_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), [span_lambda, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_lambda[idx] * span_lambda[idx]; + }); + segmented_sum( + l1_sq_terms, span_offsets, cones.K, nt_l1_sq, cones.scratch.segmented_reduce_workspace, stream); + + auto sl_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_s, span_lambda, span_offsets, span_elem] HD(i_t idx) { + i_t cone = span_elem[idx]; + return (idx == span_offsets[cone]) ? f_t(0) : span_s[idx] * span_lambda[idx]; + }); + segmented_sum( + sl_terms, span_offsets, cones.K, nt_sl, cones.scratch.segmented_reduce_workspace, stream); + + i_t scalar_grid_dim = (cones.K + flat_block_dim - 1) / flat_block_dim; + nt_scaling_scalar_kernel + <<>>(cones.s, + cones.lambda, + span_offsets, + nt_s1_sq, + nt_l1_sq, + nt_sl, + cuopt::make_span(cones.inv_eta), + cuopt::make_span(cones.inv_1pw0), + cuopt::make_span(cones.w_bar), + cuopt::make_span(cones.omega), + cuopt::make_span(cones.rho), + cones.K); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + i_t grid_dim = (cones.m_c + flat_block_dim - 1) / flat_block_dim; + nt_scaling_tail_kernel + <<>>(cones.s, cones.lambda, - cuopt::make_span(cones.eta), cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.inv_1pw0), + cuopt::make_span(cones.rho), cuopt::make_span(cones.w_bar), cuopt::make_span(cones.omega), - cuopt::make_span(cones.rho), - cuopt::make_span(cones.cone_offsets), - cuopt::make_span(cone_ids), - bucket_size); - }; - - i_t small_count = static_cast(cones.small_cone_ids.size()); - if (small_count > 0) { - constexpr int warps_per_block = small_block_dim / 32; - i_t grid_dim = (small_count + warps_per_block - 1) / warps_per_block; - nt_scaling_small_kernel - <<>>(cones.s, - cones.lambda, - cuopt::make_span(cones.eta), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.inv_1pw0), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.omega), - cuopt::make_span(cones.rho), - cuopt::make_span(cones.cone_offsets), - cuopt::make_span(cones.small_cone_ids), - small_count); - } - - launch_streaming_bucket(cones.medium_cone_ids, std::integral_constant{}); - launch_streaming_bucket(cones.large_cone_ids, std::integral_constant{}); + span_offsets, + span_elem); + RAFT_CUDA_TRY(cudaPeekAtLastError()); } } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index ab5f990809..90ae6ff74e 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -19,11 +19,16 @@ namespace cuopt::linear_programming::dual_simplex { +template +static i_t linear_var_count(const lp_problem_t& problem) +{ + return problem.second_order_cone_dims.empty() ? problem.num_cols : problem.cone_var_start; +} + template i_t remove_empty_cols(lp_problem_t& problem, i_t& num_empty_cols, - presolve_info_t& presolve_info, - const std::vector& is_cone_variable) + presolve_info_t& presolve_info) { constexpr bool verbose = false; if (verbose) { printf("Removing %d empty columns\n", num_empty_cols); } @@ -32,9 +37,10 @@ i_t remove_empty_cols(lp_problem_t& problem, presolve_info.removed_reduced_costs.reserve(num_empty_cols); std::vector has_quadratic_term(problem.num_cols, false); + i_t linear_cols = linear_var_count(problem); if (problem.Q.n > 0) { - for (i_t j = 0; j < problem.num_cols; ++j) { + for (i_t j = 0; j < linear_cols; ++j) { const i_t row_start = problem.Q.row_start[j]; const i_t row_end = problem.Q.row_start[j + 1]; if (row_end - row_start == 0) { continue; } @@ -46,8 +52,8 @@ i_t remove_empty_cols(lp_problem_t& problem, i_t new_cols = 0; for (i_t j = 0; j < problem.num_cols; ++j) { bool remove_var = false; - if ((problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { - bool non_removable = has_quadratic_term[j] || is_cone_variable[j]; + if (j < linear_cols && (problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { + bool non_removable = has_quadratic_term[j]; if (problem.objective[j] >= 0 && problem.lower[j] > -inf && !non_removable) { presolve_info.removed_values.push_back(problem.lower[j]); problem.obj_constant += problem.objective[j] * problem.lower[j]; @@ -115,6 +121,12 @@ i_t remove_empty_cols(lp_problem_t& problem, problem.Q.check_matrix("After removing empty columns"); } + if (!problem.second_order_cone_dims.empty()) { + i_t new_cone_start = col_old_to_new[problem.cone_var_start]; + assert(new_cone_start != -1); + problem.cone_var_start = new_cone_start; + } + problem.objective = objective; problem.lower = lower; problem.upper = upper; @@ -264,6 +276,113 @@ i_t convert_less_than_to_equal(const user_problem_t& user_problem, // We must convert rows in the form: a_i^T x <= beta // into: a_i^T x + s_i = beta, s_i >= 0 + if (!problem.second_order_cone_dims.empty()) { + const i_t old_num_cols = problem.num_cols; + const i_t linear_cols = linear_var_count(problem); + const i_t num_slacks = less_rows; + const i_t num_cols = old_num_cols + num_slacks; + const i_t old_nnz = problem.A.col_start[old_num_cols]; + const i_t nnz = old_nnz + num_slacks; + const i_t new_cone_start = linear_cols + num_slacks; + + auto old_A = problem.A; + csc_matrix_t expanded_A(problem.A.m, num_cols, nnz); + + std::vector objective(num_cols, 0.0); + std::vector lower(num_cols, 0.0); + std::vector upper(num_cols, INFINITY); + std::vector old_to_new(old_num_cols, -1); + + for (i_t j = 0; j < linear_cols; ++j) { + old_to_new[j] = j; + objective[j] = problem.objective[j]; + lower[j] = problem.lower[j]; + upper[j] = problem.upper[j]; + } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + old_to_new[j] = j + num_slacks; + objective[old_to_new[j]] = problem.objective[j]; + lower[old_to_new[j]] = problem.lower[j]; + upper[old_to_new[j]] = problem.upper[j]; + } + + i_t nz = 0; + for (i_t j = 0; j < linear_cols; ++j) { + expanded_A.col_start[j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + + i_t slack_col = linear_cols; + for (i_t i = 0; i < problem.num_rows; i++) { + if (row_sense[i] == 'L') { + expanded_A.col_start[slack_col] = nz; + expanded_A.i[nz] = i; + expanded_A.x[nz] = 1.0; + new_slacks.push_back(slack_col); + row_sense[i] = 'E'; + ++slack_col; + ++nz; + --less_rows; + } + } + + for (i_t j = linear_cols; j < old_num_cols; ++j) { + i_t new_j = old_to_new[j]; + expanded_A.col_start[new_j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + expanded_A.col_start[num_cols] = nz; + assert(less_rows == 0); + assert(slack_col == new_cone_start); + assert(nz == nnz); + + if (problem.Q.n > 0) { + const auto old_Q = problem.Q; + const i_t q_nnz = old_Q.row_start[old_num_cols]; + + problem.Q.row_start.assign(num_cols + 1, 0); + for (i_t row = 0; row < old_num_cols; ++row) { + i_t new_row = old_to_new[row]; + problem.Q.row_start[new_row + 1] = old_Q.row_start[row + 1] - old_Q.row_start[row]; + } + for (i_t row = 0; row < num_cols; ++row) { + problem.Q.row_start[row + 1] += problem.Q.row_start[row]; + } + + problem.Q.j.resize(q_nnz); + problem.Q.x.resize(q_nnz); + auto row_starts = problem.Q.row_start; + for (i_t row = 0; row < old_num_cols; ++row) { + i_t new_row = old_to_new[row]; + for (i_t p = old_Q.row_start[row]; p < old_Q.row_start[row + 1]; ++p) { + problem.Q.j[row_starts[new_row]] = old_to_new[old_Q.j[p]]; + problem.Q.x[row_starts[new_row]] = old_Q.x[p]; + ++row_starts[new_row]; + } + } + problem.Q.m = num_cols; + problem.Q.n = num_cols; + problem.Q.nz_max = q_nnz; + } + + problem.A = expanded_A; + problem.A.n = num_cols; + problem.objective = objective; + problem.lower = lower; + problem.upper = upper; + problem.num_cols = num_cols; + problem.cone_var_start = new_cone_start; + return 0; + } + i_t num_cols = problem.num_cols + less_rows; i_t nnz = problem.A.col_start[problem.num_cols] + less_rows; problem.A.col_start.resize(num_cols + 1); @@ -817,26 +936,10 @@ i_t presolve(const lp_problem_t& original, { problem = original; std::vector row_sense(problem.num_rows, '='); - auto build_is_cone_variable = [](const lp_problem_t& current_problem) { - std::vector is_cone_variable(current_problem.num_cols, false); - if (!current_problem.second_order_cone_dims.empty()) { - i_t cone_end = current_problem.cone_var_start; - for (auto q_k : current_problem.second_order_cone_dims) { - cone_end += q_k; - } - for (i_t j = current_problem.cone_var_start; j < cone_end; ++j) { - is_cone_variable[j] = true; - } - } - return is_cone_variable; - }; - auto is_cone_variable = build_is_cone_variable(problem); // Check for free variables i_t free_variables = 0; - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { - free_variables++; - } + for (i_t j = 0; j < linear_var_count(problem); j++) { + if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } } if (settings.barrier_presolve && free_variables > 0) { @@ -846,8 +949,8 @@ i_t presolve(const lp_problem_t& original, std::vector row_marked(problem.num_rows, 0); current_free_variables.reserve(problem.num_cols); constraints_to_check.reserve(problem.num_rows); - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { + for (i_t j = 0; j < linear_var_count(problem); j++) { + if (problem.lower[j] == -inf && problem.upper[j] == inf) { current_free_variables.push_back(j); const i_t col_start = problem.A.col_start[j]; const i_t col_end = problem.A.col_start[j + 1]; @@ -986,10 +1089,8 @@ i_t presolve(const lp_problem_t& original, } i_t new_free_variables = 0; - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { - new_free_variables++; - } + for (i_t j = 0; j < linear_var_count(problem); j++) { + if (problem.lower[j] == -inf && problem.upper[j] == inf) { new_free_variables++; } } if (removed_free_variables != 0) { settings.log.printf("Bounded %d free variables\n", removed_free_variables); @@ -997,7 +1098,6 @@ i_t presolve(const lp_problem_t& original, assert(new_free_variables == free_variables - removed_free_variables); free_variables = new_free_variables; } - // The original problem may have a variable without a lower bound // but a finite upper bound // -inf < x_j <= u_j @@ -1142,23 +1242,19 @@ i_t presolve(const lp_problem_t& original, // Check for empty cols i_t num_empty_cols = 0; { - for (i_t j = 0; j < problem.num_cols; ++j) { + for (i_t j = 0; j < linear_var_count(problem); ++j) { if ((problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { num_empty_cols++; } } } if (num_empty_cols > 0) { settings.log.printf("Presolve attempt to remove %d empty cols\n", num_empty_cols); - remove_empty_cols(problem, num_empty_cols, presolve_info, is_cone_variable); + remove_empty_cols(problem, num_empty_cols, presolve_info); } - is_cone_variable = build_is_cone_variable(problem); - // Check for free variables (exclude cone variables — they are naturally unbounded) free_variables = 0; - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { - free_variables++; - } + for (i_t j = 0; j < linear_var_count(problem); j++) { + if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } } problem.Q.check_matrix("Before free variable expansion"); @@ -1175,123 +1271,164 @@ i_t presolve(const lp_problem_t& original, // becomes // sum_{k != j} c_k x_k + c_j v - c_j w - std::vector pair_index(problem.num_cols, -1); - i_t num_cols = problem.num_cols + free_variables; - i_t nnz = problem.A.col_start[problem.num_cols]; - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { - nnz += (problem.A.col_start[j + 1] - problem.A.col_start[j]); + const i_t old_num_cols = problem.num_cols; + const i_t linear_cols = linear_var_count(problem); + const i_t new_cone_start = + problem.second_order_cone_dims.empty() ? 0 : linear_cols + free_variables; + const i_t num_cols = old_num_cols + free_variables; + + auto old_A = problem.A; + auto old_Q = problem.Q; + auto old_objective = problem.objective; + auto old_lower = problem.lower; + auto old_upper = problem.upper; + + std::vector partner_index(old_num_cols, -1); + std::vector orig_to_new(old_num_cols, -1); + std::vector is_free(old_num_cols, false); + + i_t next_partner = linear_cols; + for (i_t j = 0; j < linear_cols; ++j) { + orig_to_new[j] = j; + if (old_lower[j] == -inf && old_upper[j] == inf) { + is_free[j] = true; + partner_index[j] = next_partner++; } } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + orig_to_new[j] = j + free_variables; + } + assert(next_partner == new_cone_start || problem.second_order_cone_dims.empty()); - problem.A.col_start.resize(num_cols + 1); - problem.A.i.resize(nnz); - problem.A.x.resize(nnz); - problem.lower.resize(num_cols); - problem.upper.resize(num_cols); - problem.objective.resize(num_cols); + i_t nnz = old_A.col_start[old_num_cols]; + for (i_t j = 0; j < linear_cols; ++j) { + if (is_free[j]) { nnz += old_A.col_start[j + 1] - old_A.col_start[j]; } + } - presolve_info.free_variable_pairs.resize(free_variables * 2); - i_t pair_count = 0; - i_t q = problem.A.col_start[problem.num_cols]; - i_t col = problem.num_cols; - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf && !is_cone_variable[j]) { - for (i_t p = problem.A.col_start[j]; p < problem.A.col_start[j + 1]; p++) { - i_t i = problem.A.i[p]; - f_t aij = problem.A.x[p]; - problem.A.i[q] = i; - problem.A.x[q] = -aij; - q++; + csc_matrix_t expanded_A(problem.A.m, num_cols, nnz); + i_t nz = 0; + for (i_t j = 0; j < linear_cols; ++j) { + expanded_A.col_start[j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + for (i_t j = 0; j < linear_cols; ++j) { + if (partner_index[j] != -1) { + expanded_A.col_start[partner_index[j]] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = -old_A.x[p]; + ++nz; } - problem.lower[col] = 0.0; - problem.upper[col] = inf; - problem.objective[col] = -problem.objective[j]; - presolve_info.free_variable_pairs[pair_count++] = j; - presolve_info.free_variable_pairs[pair_count++] = col; - pair_index[j] = col; - problem.A.col_start[++col] = q; - problem.lower[j] = 0.0; } } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + i_t new_j = orig_to_new[j]; + expanded_A.col_start[new_j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + expanded_A.col_start[num_cols] = nz; + + std::vector objective(num_cols); + std::vector lower(num_cols, -INFINITY); + std::vector upper(num_cols, INFINITY); + presolve_info.free_variable_pairs.clear(); + presolve_info.free_variable_pairs.reserve(free_variables * 2); + + for (i_t j = 0; j < linear_cols; ++j) { + objective[j] = old_objective[j]; + if (is_free[j]) { + lower[j] = 0.0; + upper[j] = inf; + objective[partner_index[j]] = -old_objective[j]; + lower[partner_index[j]] = 0.0; + upper[partner_index[j]] = inf; + presolve_info.free_variable_pairs.push_back(j); + presolve_info.free_variable_pairs.push_back(partner_index[j]); + } else { + lower[j] = old_lower[j]; + upper[j] = old_upper[j]; + } + } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + i_t new_j = orig_to_new[j]; + objective[new_j] = old_objective[j]; + lower[new_j] = old_lower[j]; + upper[new_j] = old_upper[j]; + } - if (problem.Q.n > 0) { + if (old_Q.n > 0) { std::vector row_counts(num_cols, 0); - i_t nz_count = problem.Q.row_start[problem.num_cols]; - for (i_t row = 0; row < problem.Q.n; row++) { - i_t q_start = problem.Q.row_start[row]; - i_t q_end = problem.Q.row_start[row + 1]; - row_counts[row] = q_end - q_start; - for (i_t qj = q_start; qj < q_end; qj++) { - i_t col = problem.Q.j[qj]; - if (pair_index[row] != -1 && pair_index[col] != -1) { - assert(pair_index[row] >= problem.num_cols); - assert(pair_index[col] >= problem.num_cols); - row_counts[row]++; - row_counts[pair_index[row]] += 2; - nz_count += 3; - } else if (pair_index[col] != -1) { - assert(pair_index[col] >= problem.num_cols); - row_counts[row]++; + i_t nz_count = 0; + for (i_t row = 0; row < old_Q.n; ++row) { + i_t new_row = orig_to_new[row]; + i_t partner_row = partner_index[row]; + i_t q_start = old_Q.row_start[row]; + i_t q_end = old_Q.row_start[row + 1]; + for (i_t qj = q_start; qj < q_end; ++qj) { + i_t col = old_Q.j[qj]; + i_t partner_col = partner_index[col]; + row_counts[new_row]++; + nz_count++; + if (partner_col != -1) { + row_counts[new_row]++; nz_count++; - } else if (pair_index[row] != -1) { - assert(pair_index[row] >= problem.num_cols); - row_counts[pair_index[row]]++; + } + if (partner_row != -1) { + row_counts[partner_row]++; nz_count++; + if (partner_col != -1) { + row_counts[partner_row]++; + nz_count++; + } } } } std::vector Q_row_start(num_cols + 1); Q_row_start[0] = 0; - for (i_t row = 0; row < num_cols; row++) { + for (i_t row = 0; row < num_cols; ++row) { Q_row_start[row + 1] = Q_row_start[row] + row_counts[row]; } std::vector Q_j(nz_count); std::vector Q_x(nz_count); auto row_starts = Q_row_start; - // First copy the original Q ma - for (i_t row = 0; row < problem.Q.n; row++) { - i_t q_start = problem.Q.row_start[row]; - i_t q_end = problem.Q.row_start[row + 1]; - i_t q_nz = Q_row_start[row]; - for (i_t qj = q_start; qj < q_end; qj++) { - i_t col = problem.Q.j[qj]; - f_t qij = problem.Q.x[qj]; - Q_j[q_nz] = col; - Q_x[q_nz] = qij; - q_nz++; - } - row_starts[row] = q_nz; - } - - // Expand the Q matrix for the free variables - for (i_t row = 0; row < problem.Q.n; row++) { - i_t q_start = problem.Q.row_start[row]; - i_t q_end = problem.Q.row_start[row + 1]; - for (i_t qj = q_start; qj < q_end; qj++) { - i_t col = problem.Q.j[qj]; - f_t qij = problem.Q.x[qj]; - if (pair_index[row] != -1 && pair_index[col] != -1) { - Q_j[row_starts[row]] = pair_index[col]; - Q_x[row_starts[row]] = -qij; - row_starts[row]++; - - Q_j[row_starts[pair_index[row]]] = col; - Q_x[row_starts[pair_index[row]]] = -qij; - row_starts[pair_index[row]]++; - - Q_j[row_starts[pair_index[row]]] = pair_index[col]; - Q_x[row_starts[pair_index[row]]] = qij; - row_starts[pair_index[row]]++; - } else if (pair_index[col] != -1) { - Q_j[row_starts[row]] = pair_index[col]; - Q_x[row_starts[row]] = -qij; - row_starts[row]++; - } else if (pair_index[row] != -1) { - Q_j[row_starts[pair_index[row]]] = col; - Q_x[row_starts[pair_index[row]]] = -qij; - row_starts[pair_index[row]]++; + for (i_t row = 0; row < old_Q.n; ++row) { + i_t new_row = orig_to_new[row]; + i_t partner_row = partner_index[row]; + i_t q_start = old_Q.row_start[row]; + i_t q_end = old_Q.row_start[row + 1]; + for (i_t qj = q_start; qj < q_end; ++qj) { + i_t col = old_Q.j[qj]; + f_t qij = old_Q.x[qj]; + i_t new_col = orig_to_new[col]; + i_t partner_col = partner_index[col]; + + Q_j[row_starts[new_row]] = new_col; + Q_x[row_starts[new_row]] = qij; + row_starts[new_row]++; + + if (partner_col != -1) { + Q_j[row_starts[new_row]] = partner_col; + Q_x[row_starts[new_row]] = -qij; + row_starts[new_row]++; + } + if (partner_row != -1) { + Q_j[row_starts[partner_row]] = new_col; + Q_x[row_starts[partner_row]] = -qij; + row_starts[partner_row]++; + if (partner_col != -1) { + Q_j[row_starts[partner_row]] = partner_col; + Q_x[row_starts[partner_row]] = qij; + row_starts[partner_row]++; + } } } } @@ -1304,9 +1441,13 @@ i_t presolve(const lp_problem_t& original, problem.Q.check_matrix("After free variable expansion"); } - // assert(problem.A.p[num_cols] == nnz); - problem.A.n = num_cols; - problem.num_cols = num_cols; + problem.A = expanded_A; + problem.A.n = num_cols; + problem.objective = objective; + problem.lower = lower; + problem.upper = upper; + problem.num_cols = num_cols; + if (!problem.second_order_cone_dims.empty()) { problem.cone_var_start = new_cone_start; } } if (settings.barrier_presolve && settings.folding != 0 && problem.Q.n == 0 && @@ -1388,7 +1529,7 @@ void crush_primal_solution(const user_problem_t& user_problem, // including previously added slacks, are reset before writing new values. solution.assign(problem.num_cols, 0.0); for (i_t j = 0; j < user_problem.num_cols; j++) { - solution[j] = user_solution[j]; + solution[user_col_to_problem_col(user_problem, problem, j)] = user_solution[j]; } std::vector primal_residual(problem.num_rows); @@ -1428,7 +1569,7 @@ void crush_primal_solution_with_slack(const user_problem_t& user_probl // Re-crush can be called with a reused output vector; clear stale entries first. solution.assign(problem.num_cols, 0.0); for (i_t j = 0; j < user_problem.num_cols; j++) { - solution[j] = user_solution[j]; + solution[user_col_to_problem_col(user_problem, problem, j)] = user_solution[j]; } std::vector primal_residual(problem.num_rows); @@ -1475,9 +1616,9 @@ f_t crush_dual_solution(const user_problem_t& user_problem, for (i_t i = 0; i < user_problem.num_rows; i++) { y[i] = user_y[i]; } - z.resize(problem.num_cols); + z.assign(problem.num_cols, 0.0); for (i_t j = 0; j < user_problem.num_cols; j++) { - z[j] = user_z[j]; + z[user_col_to_problem_col(user_problem, problem, j)] = user_z[j]; } std::vector is_range_row(problem.num_rows, false); @@ -1544,6 +1685,17 @@ f_t crush_dual_solution(const user_problem_t& user_problem, return dual_res_inf; } +template +static i_t user_col_to_problem_col(const user_problem_t& user_problem, + const lp_problem_t& problem, + i_t user_col) +{ + if (user_problem.second_order_cone_dims.empty()) { return user_col; } + if (problem.cone_var_start <= user_problem.cone_var_start) { return user_col; } + if (user_col < user_problem.cone_var_start) { return user_col; } + return problem.cone_var_start + (user_col - user_problem.cone_var_start); +} + template void uncrush_primal_solution(const user_problem_t& user_problem, const lp_problem_t& problem, @@ -1553,9 +1705,9 @@ void uncrush_primal_solution(const user_problem_t& user_problem, user_solution.resize(user_problem.num_cols); assert(problem.num_cols >= user_problem.num_cols); assert(solution.size() >= user_problem.num_cols); - std::copy(solution.begin(), - solution.begin() + std::min((i_t)solution.size(), user_problem.num_cols), - user_solution.data()); + for (i_t j = 0; j < user_problem.num_cols; ++j) { + user_solution[j] = solution[user_col_to_problem_col(user_problem, problem, j)]; + } } template @@ -1658,13 +1810,25 @@ void uncrush_solution(const presolve_info_t& presolve_info, if (num_free_variables > 0) { settings.log.printf("Post-solve: Handling free variables %d\n", num_free_variables); // We added free variables so we need to map the crushed solution back to the original variables + std::vector remove_partner(input_x.size(), false); for (i_t k = 0; k < 2 * num_free_variables; k += 2) { const i_t u = free_variable_pairs[k]; const i_t v = free_variable_pairs[k + 1]; input_x[u] -= input_x[v]; + remove_partner[v] = true; + } + std::vector compact_x; + std::vector compact_z; + compact_x.reserve(input_x.size() - num_free_variables); + compact_z.reserve(input_z.size() - num_free_variables); + for (i_t j = 0; j < static_cast(input_x.size()); ++j) { + if (!remove_partner[j]) { + compact_x.push_back(input_x[j]); + compact_z.push_back(input_z[j]); + } } - input_z.resize(input_z.size() - num_free_variables); - input_x.resize(input_x.size() - num_free_variables); + input_x = compact_x; + input_z = compact_z; } if (presolve_info.removed_variables.size() > 0) { diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 54fe5dcf46..65dcb49bfe 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -22,7 +22,7 @@ i_t column_scaling(const lp_problem_t& unscaled, i_t m = scaled.num_rows; i_t n = scaled.num_cols; - if (!settings.scale_columns || unscaled.Q.n > 0) { + if (!settings.scale_columns || unscaled.Q.n > 0 || !unscaled.second_order_cone_dims.empty()) { settings.log.printf("Skipping column scaling\n"); column_scaling.resize(n, 1.0); return 0; @@ -30,19 +30,9 @@ i_t column_scaling(const lp_problem_t& unscaled, column_scaling.resize(n); - i_t cone_start = unscaled.cone_var_start; - i_t cone_end = cone_start; - for (auto q_k : unscaled.second_order_cone_dims) { - cone_end += q_k; - } - f_t max = 0; f_t min = std::numeric_limits::max(); for (i_t j = 0; j < n; ++j) { - if (j >= cone_start && j < cone_end) { - column_scaling[j] = 1.0; - continue; - } const i_t col_start = scaled.A.col_start[j]; const i_t col_end = scaled.A.col_start[j + 1]; f_t sum = 0.0; diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 9bc12bb925..544647d5d0 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -349,8 +349,18 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us if (!user_problem.second_order_cone_dims.empty()) { i_t cone_end = user_problem.cone_var_start; for (auto q_k : user_problem.second_order_cone_dims) { + if (q_k <= 1) { + settings.log.printf( + "Error: second-order cone dimensions must be at least 2; use linear variables instead of " + "Q^1\n"); + return lp_status_t::NUMERICAL_ISSUES; + } cone_end += q_k; } + if (cone_end != user_problem.num_cols) { + settings.log.printf("Error: conic variables must form a trailing block [linear | cone]\n"); + return lp_status_t::NUMERICAL_ISSUES; + } for (i_t j = user_problem.cone_var_start; j < cone_end; ++j) { if (user_problem.lower[j] != 0.0 && user_problem.lower[j] > -1e30) { settings.log.printf("Error: explicit lower bound on conic variable %d is not supported\n", diff --git a/cpp/src/dual_simplex/vector_math.cuh b/cpp/src/dual_simplex/vector_math.cuh index abc7263858..32c75ea366 100644 --- a/cpp/src/dual_simplex/vector_math.cuh +++ b/cpp/src/dual_simplex/vector_math.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -9,6 +9,10 @@ #include +#include +#include +#include + #include #include @@ -28,6 +32,7 @@ struct norm_inf_max { template f_t device_custom_vector_norm_inf(InputIteratorT in, i_t size, rmm::cuda_stream_view stream_view) { + if (size == 0) { return 0; } // FIXME: Tmp storage stored in vector_math class. auto d_out = rmm::device_scalar(stream_view); rmm::device_uvector d_temp_storage(0, stream_view); @@ -62,6 +67,12 @@ f_t device_vector_norm_inf(const rmm::device_uvector& in, rmm::cuda_stream_ return device_custom_vector_norm_inf(in.data(), in.size(), stream_view); } +template +f_t device_vector_norm_inf(raft::device_span in, rmm::cuda_stream_view stream_view) +{ + return device_custom_vector_norm_inf(in.data(), in.size(), stream_view); +} + // TMP we should just have a CPU and GPU version to do the comparison // Should never have to norm inf a CPU vector if we are using the GPU template @@ -71,4 +82,12 @@ f_t vector_norm_inf(const std::vector& x, rmm::cuda_stream_view return device_vector_norm_inf(d_x, stream_view); } +template +f_t vector_norm_inf(raft::host_span x, rmm::cuda_stream_view stream_view) +{ + rmm::device_uvector d_x(x.size(), stream_view); + raft::copy(d_x.data(), x.data(), x.size(), stream_view); + return device_vector_norm_inf(d_x, stream_view); +} + } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/pdlp/termination_strategy/infeasibility_information.cu b/cpp/src/pdlp/termination_strategy/infeasibility_information.cu index 37972ba442..0e001b802f 100644 --- a/cpp/src/pdlp/termination_strategy/infeasibility_information.cu +++ b/cpp/src/pdlp/termination_strategy/infeasibility_information.cu @@ -24,13 +24,7 @@ #include #include -#include -#include -#include #include -#include -#include -#include namespace cuopt::linear_programming::detail { template diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu index 035fde5ac6..e6f48ac9de 100644 --- a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu @@ -326,16 +326,6 @@ auto ref_fused_corrector_single(const std::vector& dx_aff, return ref_apply_hinv_single(corr, w_bar, inv_eta, inv_1pw0); } -template -auto ref_interior_shift_single(std::vector u) -> std::vector -{ - if (u.empty()) { return u; } - - f_t gap = tail_norm(u) - u[0]; - if (gap >= f_t(0)) { u[0] += f_t(1) + gap; } - return u; -} - template auto make_patterned_cone(int q, f_t head, f_t scale) -> std::vector { @@ -401,54 +391,39 @@ class second_order_cone_test : public ::testing::Test { } } - void launch_apply_hinv(const rmm::device_uvector& z, - rmm::device_uvector& out, - const rmm::device_uvector& w_bar, - const rmm::device_uvector& inv_eta, - const rmm::device_uvector& inv_1pw0, - const rmm::device_uvector& cone_offsets, - i_t k) - { - apply_Hinv_kernel<<>>(cuopt::make_span(z), - cuopt::make_span(out), - cuopt::make_span(w_bar), - cuopt::make_span(inv_eta), - cuopt::make_span(inv_1pw0), - cuopt::make_span(cone_offsets), - k); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - sync(); - } - - void launch_step_length(const rmm::device_uvector& s, - const rmm::device_uvector& ds, - const rmm::device_uvector& lambda, - const rmm::device_uvector& dlambda, + void launch_step_length(rmm::device_uvector& s, + rmm::device_uvector& ds, + rmm::device_uvector& lambda, + rmm::device_uvector& dlambda, rmm::device_uvector& alpha, const rmm::device_uvector& cone_offsets, i_t k, f_t alpha_max) { - step_length_kernel<<>>(cuopt::make_span(s), - cuopt::make_span(ds), - cuopt::make_span(lambda), - cuopt::make_span(dlambda), - cuopt::make_span(alpha), - cuopt::make_span(cone_offsets), - k, - alpha_max); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - sync(); - } - - void launch_interior_shift(rmm::device_uvector& u, - const rmm::device_uvector& cone_offsets, - i_t k) - { - interior_shift_kernel - <<>>(cuopt::make_span(u), cuopt::make_span(cone_offsets), k); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + auto h_offsets = copy_to_host(cone_offsets); + std::vector dims(k, 0); + for (i_t cone = 0; cone < k; ++cone) { + dims[cone] = h_offsets[cone + 1] - h_offsets[cone]; + } + cone_data_t cones(k, dims, cuopt::make_span(s), cuopt::make_span(lambda), stream_); + rmm::device_uvector alpha_dual(k, stream_); + launch_nt_scaling(cones, stream_); + compute_cone_step_length_per_cone(cones, + cuopt::make_span(s), + cuopt::make_span(ds), + cuopt::make_span(lambda), + cuopt::make_span(dlambda), + cuopt::make_span(alpha), + cuopt::make_span(alpha_dual), + alpha_max, + stream_); sync(); + auto h_primal = copy_to_host(alpha); + auto h_dual = copy_to_host(alpha_dual); + for (i_t i = 0; i < k; ++i) { + h_primal[i] = std::min(h_primal[i], h_dual[i]); + } + copy_to_device(alpha, h_primal); } void launch_apply_hinv2(const rmm::device_uvector& v, @@ -458,77 +433,64 @@ class second_order_cone_test : public ::testing::Test { const rmm::device_uvector& cone_offsets, i_t k) { - apply_Hinv2_kernel<<>>(cuopt::make_span(v), - cuopt::make_span(out), - cuopt::make_span(w_bar), - cuopt::make_span(inv_eta), - cuopt::make_span(cone_offsets), - k); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - sync(); - } - - void launch_jordan_product(const rmm::device_uvector& a, - const rmm::device_uvector& b, - rmm::device_uvector& out, - const rmm::device_uvector& cone_offsets, - i_t k) - { - jordan_product_kernel<<>>(cuopt::make_span(a), - cuopt::make_span(b), - cuopt::make_span(out), - cuopt::make_span(cone_offsets), - k); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - sync(); - } - - void launch_inverse_jordan_product(const rmm::device_uvector& omega, - const rmm::device_uvector& r, - const rmm::device_uvector& rho, - rmm::device_uvector& out, - const rmm::device_uvector& cone_offsets, - i_t k) - { - inverse_jordan_product_kernel - <<>>(cuopt::make_span(omega), - cuopt::make_span(r), - cuopt::make_span(rho), - cuopt::make_span(out), - cuopt::make_span(cone_offsets), - k); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + auto h_offsets = copy_to_host(cone_offsets); + std::vector h_element_cone_ids(v.size(), 0); + for (i_t cone = 0; cone < k; ++cone) { + std::fill(h_element_cone_ids.begin() + h_offsets[cone], + h_element_cone_ids.begin() + h_offsets[cone + 1], + cone); + } + auto d_element_cone_ids = make_device_vector(h_element_cone_ids); + rmm::device_uvector tail_dot(k, stream_); + rmm::device_uvector workspace(0, stream_); + apply_hinv2(cuopt::make_span(v), + cuopt::make_span(out), + cuopt::make_span(w_bar), + cuopt::make_span(inv_eta), + cuopt::make_span(cone_offsets), + cuopt::make_span(d_element_cone_ids), + cuopt::make_span(tail_dot), + workspace, + k, + stream_); sync(); } void launch_fused_corrector(const rmm::device_uvector& dx_aff, - const cone_data_t& cones, + cone_data_t& cones, f_t sigma_mu, rmm::device_uvector& out) { - compute_combined_cone_rhs_term(cuopt::make_span(dx_aff), cones, sigma_mu, out, stream_); + out.resize(cones.m_c, stream_); + compute_combined_cone_rhs_term( + cuopt::make_span(dx_aff), cones, sigma_mu, cuopt::make_span(out), stream_); sync(); } - void launch_affine_cone_rhs(const cone_data_t& cones, rmm::device_uvector& out) + void launch_affine_cone_rhs(cone_data_t& cones, rmm::device_uvector& out) { - compute_affine_cone_rhs_term(cones, out, stream_); + out.resize(cones.m_c, stream_); + compute_affine_cone_rhs_term(cones, cuopt::make_span(out), stream_); sync(); } - void launch_recover_cone_dz(const rmm::device_uvector& dx, - const cone_data_t& cones, - const rmm::device_uvector& cone_rhs_term, - rmm::device_uvector& hinv2_dx, - rmm::device_uvector& dz) + void launch_recover_cone_dz_from_target(const rmm::device_uvector& dx, + cone_data_t& cones, + const rmm::device_uvector& cone_target, + rmm::device_uvector& hinv2_dx, + rmm::device_uvector& dz) { - recover_cone_dz( - cuopt::make_span(dx), cones, cone_rhs_term, hinv2_dx, cuopt::make_span(dz), stream_); + recover_cone_dz_from_target(cuopt::make_span(dx), + cones, + cuopt::make_span(cone_target), + hinv2_dx, + cuopt::make_span(dz), + stream_); sync(); } void launch_accumulate_cone_hinv2(const rmm::device_uvector& x, - const cone_data_t& cones, + cone_data_t& cones, rmm::device_uvector& hinv2_x, rmm::device_uvector& out) { @@ -547,25 +509,107 @@ class second_order_cone_test : public ::testing::Test { } }; -TEST_F(second_order_cone_test, cone_data_topology_and_bucket_partitioning) +TEST_F(second_order_cone_test, cone_data_topology_and_flat_index_maps) { - std::vector dims{1, 32, 33, 2048, 2049}; + std::vector dims{1, 2, 3, 4}; cone_data_t cones(static_cast(dims.size()), dims, {}, {}, stream_); auto expected_offsets = build_offsets(dims); auto actual_offsets = copy_to_host(cones.cone_offsets); auto actual_dims = copy_to_host(cones.cone_dims); - auto small_ids = copy_to_host(cones.small_cone_ids); - auto medium_ids = copy_to_host(cones.medium_cone_ids); - auto large_ids = copy_to_host(cones.large_cone_ids); + auto element_cone_ids = copy_to_host(cones.element_cone_ids); + auto block_cone_ids = copy_to_host(cones.block_entry_cone_ids); + + std::vector expected_element_cone_ids; + for (i_t cone = 0; cone < static_cast(dims.size()); ++cone) { + expected_element_cone_ids.insert(expected_element_cone_ids.end(), dims[cone], cone); + } + + std::vector expected_block_cone_ids; + for (i_t cone = 0; cone < static_cast(dims.size()); ++cone) { + expected_block_cone_ids.insert(expected_block_cone_ids.end(), dims[cone] * dims[cone], cone); + } EXPECT_EQ(cones.K, static_cast(dims.size())); EXPECT_EQ(cones.m_c, expected_offsets.back()); EXPECT_EQ(actual_offsets, expected_offsets); EXPECT_EQ(actual_dims, dims); - EXPECT_EQ(small_ids, std::vector({0, 1})); - EXPECT_EQ(medium_ids, std::vector({2, 3})); - EXPECT_EQ(large_ids, std::vector({4})); + EXPECT_EQ(element_cone_ids, expected_element_cone_ids); + EXPECT_EQ(block_cone_ids, expected_block_cone_ids); +} + +TEST_F(second_order_cone_test, cone_data_reuses_named_scratch_slots) +{ + std::vector> s_cones{{5.0, 1.0, 1.0}, {6.0, 1.0, -0.5, 0.25, 0.1}}; + std::vector> ds_cones{{-0.5, 0.1, 0.1}, {-0.2, 0.05, 0.03, -0.02, 0.01}}; + std::vector> lambda_cones{{5.0, 1.0, 1.0}, {4.0, 0.2, 0.3, -0.1, 0.05}}; + std::vector> dlambda_cones{{-0.5, 0.1, 0.1}, {-0.1, 0.02, -0.03, 0.01, -0.01}}; + std::vector dims{3, 5}; + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_ds = make_device_vector(pack_cones(ds_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + auto d_dlambda = make_device_vector(pack_cones(dlambda_cones)); + cone_data_t cones(static_cast(dims.size()), + dims, + cuopt::make_span(d_s), + cuopt::make_span(d_lambda), + stream_); + + EXPECT_EQ(cones.scratch.step_s_du1_sq().size(), dims.size()); + EXPECT_EQ(cones.scratch.step_s_u1du1().size(), dims.size()); + EXPECT_EQ(cones.scratch.step_l_du1_sq().size(), dims.size()); + EXPECT_EQ(cones.scratch.step_l_u1du1().size(), dims.size()); + EXPECT_EQ(cones.scratch.hinv2_tail_dot().size(), dims.size()); + EXPECT_EQ(cones.scratch.step_s_u1_sq().size(), dims.size()); + EXPECT_EQ(cones.scratch.step_l_u1_sq().size(), dims.size()); + EXPECT_EQ(cones.scratch.nt_s1_sq().size(), dims.size()); + EXPECT_EQ(cones.scratch.nt_l1_sq().size(), dims.size()); + EXPECT_EQ(cones.scratch.nt_sl().size(), dims.size()); + EXPECT_EQ(cones.scratch.step_alpha_primal_span().size(), dims.size()); + EXPECT_EQ(cones.scratch.step_alpha_dual_span().size(), dims.size()); + + auto s_du1_ptr = cones.scratch.step_s_du1_sq().data(); + auto s_u1du1_ptr = cones.scratch.step_s_u1du1().data(); + auto l_du1_ptr = cones.scratch.step_l_du1_sq().data(); + auto l_u1du1_ptr = cones.scratch.step_l_u1du1().data(); + auto hinv2_ptr = cones.scratch.hinv2_tail_dot().data(); + auto s_u1_sq_ptr = cones.scratch.step_s_u1_sq().data(); + auto l_u1_sq_ptr = cones.scratch.step_l_u1_sq().data(); + auto nt_s1_ptr = cones.scratch.nt_s1_sq().data(); + auto nt_l1_ptr = cones.scratch.nt_l1_sq().data(); + auto nt_sl_ptr = cones.scratch.nt_sl().data(); + auto alpha_p_ptr = cones.scratch.step_alpha_primal_span().data(); + auto alpha_d_ptr = cones.scratch.step_alpha_dual_span().data(); + + EXPECT_EQ(hinv2_ptr, s_du1_ptr); + EXPECT_EQ(s_du1_ptr, nt_s1_ptr); + EXPECT_EQ(s_u1du1_ptr, nt_l1_ptr); + EXPECT_EQ(s_u1_sq_ptr, nt_sl_ptr); + + compute_cone_step_length_per_cone(cones, + cuopt::make_span(d_s), + cuopt::make_span(d_ds), + cuopt::make_span(d_lambda), + cuopt::make_span(d_dlambda), + cones.scratch.step_alpha_primal_span(), + cones.scratch.step_alpha_dual_span(), + f_t(10.0), + stream_); + sync(); + + EXPECT_EQ(s_du1_ptr, cones.scratch.step_s_du1_sq().data()); + EXPECT_EQ(s_u1du1_ptr, cones.scratch.step_s_u1du1().data()); + EXPECT_EQ(l_du1_ptr, cones.scratch.step_l_du1_sq().data()); + EXPECT_EQ(l_u1du1_ptr, cones.scratch.step_l_u1du1().data()); + EXPECT_EQ(hinv2_ptr, cones.scratch.hinv2_tail_dot().data()); + EXPECT_EQ(s_u1_sq_ptr, cones.scratch.step_s_u1_sq().data()); + EXPECT_EQ(l_u1_sq_ptr, cones.scratch.step_l_u1_sq().data()); + EXPECT_EQ(nt_s1_ptr, cones.scratch.nt_s1_sq().data()); + EXPECT_EQ(nt_l1_ptr, cones.scratch.nt_l1_sq().data()); + EXPECT_EQ(nt_sl_ptr, cones.scratch.nt_sl().data()); + EXPECT_EQ(alpha_p_ptr, cones.scratch.step_alpha_primal_span().data()); + EXPECT_EQ(alpha_d_ptr, cones.scratch.step_alpha_dual_span().data()); } TEST_F(second_order_cone_test, nt_scaling_matches_reference_for_small_cone) @@ -582,7 +626,6 @@ TEST_F(second_order_cone_test, nt_scaling_matches_reference_for_small_cone) launch_nt_scaling(cones, stream_); - auto eta = copy_to_host(cones.eta); auto inv_eta = copy_to_host(cones.inv_eta); auto inv_1pw0 = copy_to_host(cones.inv_1pw0); auto rho = copy_to_host(cones.rho); @@ -591,7 +634,6 @@ TEST_F(second_order_cone_test, nt_scaling_matches_reference_for_small_cone) auto ref = ref_nt_scaling_single(s_cones[0], lambda_cones[0]); - EXPECT_NEAR(eta[0], ref.eta, 1e-12); EXPECT_NEAR(inv_eta[0], ref.inv_eta, 1e-12); EXPECT_NEAR(inv_1pw0[0], ref.inv_1pw0, 1e-12); EXPECT_NEAR(rho[0], ref.rho, 1e-12); @@ -624,7 +666,6 @@ TEST_F(second_order_cone_test, nt_scaling_matches_reference_across_bucket_sizes) launch_nt_scaling(cones, stream_); - auto eta = copy_to_host(cones.eta); auto inv_eta = copy_to_host(cones.inv_eta); auto inv_1pw0 = copy_to_host(cones.inv_1pw0); auto rho = copy_to_host(cones.rho); @@ -634,7 +675,6 @@ TEST_F(second_order_cone_test, nt_scaling_matches_reference_across_bucket_sizes) for (i_t cone = 0; cone < static_cast(dims.size()); ++cone) { auto ref = ref_nt_scaling_single(s_cones[cone], lambda_cones[cone]); - EXPECT_NEAR(eta[cone], ref.eta, 1e-10) << "cone " << cone; EXPECT_NEAR(inv_eta[cone], ref.inv_eta, 1e-10) << "cone " << cone; EXPECT_NEAR(inv_1pw0[cone], ref.inv_1pw0, 1e-10) << "cone " << cone; EXPECT_NEAR(rho[cone], ref.rho, 1e-10) << "cone " << cone; @@ -661,16 +701,60 @@ TEST_F(second_order_cone_test, nt_scaling_omega_equals_H_times_lambda) launch_nt_scaling(cones, stream_); - auto eta = copy_to_host(cones.eta); + auto inv_eta = copy_to_host(cones.inv_eta); auto inv_1pw0 = copy_to_host(cones.inv_1pw0); auto w_bar = copy_to_host(cones.w_bar); auto omega = copy_to_host(cones.omega); // NT symmetry: omega should equal both H^{-1}s and H*lambda. - auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, eta[0], inv_1pw0[0]); + auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, f_t(1) / inv_eta[0], inv_1pw0[0]); expect_vector_near(omega, H_lambda, 1e-10, 1e-8, "omega_vs_H_lambda"); } +TEST_F(second_order_cone_test, nt_scaling_tail_identities_match_heads) +{ + std::vector> s_cones{{5.0, 1.0, -1.0, 0.5, 0.3}}; + std::vector> lambda_cones{{4.0, 0.5, 1.0, -0.3, 0.2}}; + std::vector dims{5}; + + auto d_s = make_device_vector(pack_cones(s_cones)); + auto d_lambda = make_device_vector(pack_cones(lambda_cones)); + cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); + + launch_nt_scaling(cones, stream_); + + auto inv_eta = copy_to_host(cones.inv_eta); + auto rho = copy_to_host(cones.rho); + auto w_bar = copy_to_host(cones.w_bar); + auto omega = copy_to_host(cones.omega); + + f_t s_J = std::sqrt(j_norm_sq(s_cones[0])); + f_t l_J = std::sqrt(j_norm_sq(lambda_cones[0])); + f_t s_dot_raw = s_cones[0][0] * lambda_cones[0][0]; + for (std::size_t j = 1; j < s_cones[0].size(); ++j) { + s_dot_raw += s_cones[0][j] * lambda_cones[0][j]; + } + f_t s_dot_l = s_dot_raw / (s_J * l_J); + f_t gamma = std::sqrt(std::max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); + f_t w0_from_heads = (s_cones[0][0] / s_J + lambda_cones[0][0] / l_J) / (f_t(2) * gamma); + + f_t omega_tail_sq = f_t(0); + f_t w_omega_tail = f_t(0); + for (std::size_t j = 1; j < omega.size(); ++j) { + omega_tail_sq += omega[j] * omega[j]; + w_omega_tail += w_bar[j] * omega[j]; + } + + EXPECT_NEAR(omega_tail_sq, omega[0] * omega[0] - rho[0], 1e-10) + << "||omega_1||^2 should be derived from omega_0 and rho"; + EXPECT_NEAR(w_bar[0], w0_from_heads, 1e-10) + << "w_bar_0 should be derived directly from normalized cone heads"; + + f_t derived_w_omega = f_t(0.5) * (inv_eta[0] * s_cones[0][0] - lambda_cones[0][0] / inv_eta[0]); + EXPECT_NEAR(w_omega_tail, derived_w_omega, 1e-10) + << "w_bar_1^T omega_1 should be derived from cone heads"; +} + TEST_F(second_order_cone_test, nt_scaling_near_boundary_is_stable) { // s and lambda barely inside the cone: ||tail||^2 ≈ head^2. @@ -684,62 +768,25 @@ TEST_F(second_order_cone_test, nt_scaling_near_boundary_is_stable) launch_nt_scaling(cones, stream_); - auto eta = copy_to_host(cones.eta); auto inv_eta = copy_to_host(cones.inv_eta); auto inv_1pw0 = copy_to_host(cones.inv_1pw0); auto w_bar = copy_to_host(cones.w_bar); auto omega = copy_to_host(cones.omega); + f_t eta_val = f_t(1) / inv_eta[0]; + EXPECT_NEAR(j_norm_sq(w_bar), f_t(1), 1e-8) << "w_bar J-norm not 1 near boundary"; EXPECT_GT(w_bar[0], tail_norm(w_bar)) << "w_bar not interior near boundary"; // Round-trip: H(omega) should equal s. - auto H_omega = ref_apply_H_single(omega, w_bar, eta[0], inv_1pw0[0]); + auto H_omega = ref_apply_H_single(omega, w_bar, eta_val, inv_1pw0[0]); expect_vector_near(H_omega, pack_cones(s_cones), 1e-8, 1e-6, "H_omega_vs_s_near_boundary"); // Symmetry: omega should also equal H*lambda. - auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, eta[0], inv_1pw0[0]); + auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, eta_val, inv_1pw0[0]); expect_vector_near(omega, H_lambda, 1e-8, 1e-6, "omega_vs_H_lambda_near_boundary"); } -TEST_F(second_order_cone_test, apply_hinv_matches_reference_for_packed_cones) -{ - std::vector dims{1, 3, 5}; - auto offsets = build_offsets(dims); - - std::vector> z_cones{{3.0}, {2.0, -1.0, 0.5}, {1.0, 0.25, -0.75, 0.5, -0.125}}; - std::vector> w_bar_cones{ - {1.0}, {0.0, 0.15, -0.05}, {0.0, 0.10, -0.20, 0.05, 0.15}}; - std::vector inv_eta_host{0.5, 1.25, 0.75}; - std::vector inv_1pw0_host(inv_eta_host.size(), 0.0); - - for (std::size_t cone = 0; cone < w_bar_cones.size(); ++cone) { - f_t w1_sq = f_t(0); - for (std::size_t j = 1; j < w_bar_cones[cone].size(); ++j) { - w1_sq += w_bar_cones[cone][j] * w_bar_cones[cone][j]; - } - w_bar_cones[cone][0] = std::sqrt(f_t(1) + w1_sq); - inv_1pw0_host[cone] = f_t(1) / (f_t(1) + w_bar_cones[cone][0]); - } - - auto z = make_device_vector(pack_cones(z_cones)); - auto w_bar = make_device_vector(pack_cones(w_bar_cones)); - auto inv_eta = make_device_vector(inv_eta_host); - auto inv_1pw0 = make_device_vector(inv_1pw0_host); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector out(z.size(), stream_); - - launch_apply_hinv(z, out, w_bar, inv_eta, inv_1pw0, d_offsets, static_cast(dims.size())); - - auto actual_out = copy_to_host(out); - auto expected = pack_cones(std::vector>{ - ref_apply_hinv_single(z_cones[0], w_bar_cones[0], inv_eta_host[0], inv_1pw0_host[0]), - ref_apply_hinv_single(z_cones[1], w_bar_cones[1], inv_eta_host[1], inv_1pw0_host[1]), - ref_apply_hinv_single(z_cones[2], w_bar_cones[2], inv_eta_host[2], inv_1pw0_host[2])}); - - expect_vector_near(actual_out, expected, 1e-12, 1e-10, "apply_hinv"); -} - TEST_F(second_order_cone_test, step_length_matches_reference_and_handles_q1) { std::vector dims{1, 3}; @@ -938,33 +985,6 @@ TEST_F(second_order_cone_test, step_length_boundary_tightness) EXPECT_GT(j_norm_sq(l_int), 0.0) << "lambda not interior at (1-eps)*alpha"; } -TEST_F(second_order_cone_test, interior_shift_matches_reference_and_preserves_tail) -{ - std::vector dims{1, 3, 4}; - auto offsets = build_offsets(dims); - - std::vector> cones{{-0.25}, {2.0, 0.3, 0.4}, {0.5, 0.6, 0.8, 0.0}}; - auto packed = pack_cones(cones); - auto expected = pack_cones(std::vector>{ref_interior_shift_single(cones[0]), - ref_interior_shift_single(cones[1]), - ref_interior_shift_single(cones[2])}); - - auto u = make_device_vector(packed); - auto d_offsets = make_device_vector(offsets); - launch_interior_shift(u, d_offsets, static_cast(dims.size())); - - auto actual = copy_to_host(u); - expect_vector_near(actual, expected, 1e-12, 1e-10, "interior_shift"); - - for (std::size_t cone = 0; cone < dims.size(); ++cone) { - auto shifted = slice_cone(actual, offsets, static_cast(cone)); - EXPECT_GT(shifted[0], tail_norm(shifted)); - for (std::size_t j = 1; j < shifted.size(); ++j) { - EXPECT_EQ(shifted[j], cones[cone][j]) << "cone " << cone << " tail " << j; - } - } -} - TEST_F(second_order_cone_test, apply_hinv2_matches_reference_for_packed_cones) { std::vector dims{1, 3, 5}; @@ -1000,94 +1020,6 @@ TEST_F(second_order_cone_test, apply_hinv2_matches_reference_for_packed_cones) expect_vector_near(actual, expected, 1e-12, 1e-10, "apply_hinv2"); } -TEST_F(second_order_cone_test, apply_hinv2_equals_double_hinv_with_nt_scaling) -{ - std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; - std::vector dims{3, 5}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - - launch_nt_scaling(cones, stream_); - - std::vector> v_cones{{1.0, -0.3, 0.2}, {0.5, 0.1, -0.15, 0.25, -0.1}}; - auto d_v = make_device_vector(pack_cones(v_cones)); - auto d_offsets = make_device_vector(offsets); - - // H^{-2} v (single kernel) - rmm::device_uvector d_hinv2(cones.omega.size(), stream_); - launch_apply_hinv2( - d_v, d_hinv2, cones.w_bar, cones.inv_eta, d_offsets, static_cast(dims.size())); - - // H^{-1}(H^{-1} v) (two passes) - rmm::device_uvector d_tmp(cones.omega.size(), stream_); - rmm::device_uvector d_double(cones.omega.size(), stream_); - launch_apply_hinv(d_v, - d_tmp, - cones.w_bar, - cones.inv_eta, - cones.inv_1pw0, - d_offsets, - static_cast(dims.size())); - launch_apply_hinv(d_tmp, - d_double, - cones.w_bar, - cones.inv_eta, - cones.inv_1pw0, - d_offsets, - static_cast(dims.size())); - - auto hinv2_actual = copy_to_host(d_hinv2); - auto double_actual = copy_to_host(d_double); - expect_vector_near(hinv2_actual, double_actual, 1e-10, 1e-8, "hinv2_vs_double_hinv"); -} - -TEST_F(second_order_cone_test, apply_hinv2_strided_loop_for_large_cone) -{ - std::vector dims{513}; - auto offsets = build_offsets(dims); - - auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); - auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); - - auto d_s = make_device_vector(s_cone); - auto d_lambda = make_device_vector(lambda_cone); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - auto v_cone = make_patterned_cone(dims[0], 3.0, 0.006); - auto d_v = make_device_vector(v_cone); - auto d_offsets = make_device_vector(offsets); - - // Direct H^{-2} apply - rmm::device_uvector d_hinv2(cones.omega.size(), stream_); - launch_apply_hinv2(d_v, d_hinv2, cones.w_bar, cones.inv_eta, d_offsets, 1); - - // Reference: two H^{-1} passes - rmm::device_uvector d_tmp(cones.omega.size(), stream_); - rmm::device_uvector d_double(cones.omega.size(), stream_); - launch_apply_hinv(d_v, d_tmp, cones.w_bar, cones.inv_eta, cones.inv_1pw0, d_offsets, 1); - launch_apply_hinv(d_tmp, d_double, cones.w_bar, cones.inv_eta, cones.inv_1pw0, d_offsets, 1); - - auto hinv2_actual = copy_to_host(d_hinv2); - auto double_actual = copy_to_host(d_double); - expect_vector_near(hinv2_actual, double_actual, 1e-8, 1e-6, "hinv2_large"); - - // Also check against CPU reference - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_host = copy_to_host(cones.inv_eta); - auto ref = ref_apply_hinv2_single(v_cone, w_bar_host, inv_eta_host[0]); - expect_vector_near(hinv2_actual, ref, 1e-8, 1e-6, "hinv2_large_ref"); -} - TEST_F(second_order_cone_test, affine_cone_rhs_matches_hinv2_of_primal) { std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; @@ -1310,168 +1242,6 @@ TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_large_cone) } } -TEST_F(second_order_cone_test, jordan_product_matches_reference_for_packed_cones) -{ - std::vector dims{1, 3, 4}; - auto offsets = build_offsets(dims); - - std::vector> a_cones{{2.0}, {2.0, 1.0, -0.5}, {3.0, 0.25, -0.75, 0.5}}; - std::vector> b_cones{{4.0}, {1.5, -0.5, 0.25}, {2.0, -0.25, 0.5, 1.0}}; - - auto a = make_device_vector(pack_cones(a_cones)); - auto b = make_device_vector(pack_cones(b_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector out(a.size(), stream_); - - launch_jordan_product(a, b, out, d_offsets, static_cast(dims.size())); - - auto actual = copy_to_host(out); - auto expected = - pack_cones(std::vector>{ref_jordan_product_single(a_cones[0], b_cones[0]), - ref_jordan_product_single(a_cones[1], b_cones[1]), - ref_jordan_product_single(a_cones[2], b_cones[2])}); - - expect_vector_near(actual, expected, 1e-12, 1e-10, "jordan_product"); -} - -TEST_F(second_order_cone_test, inverse_jordan_product_matches_reference_and_identity) -{ - std::vector dims{1, 3, 5}; - auto offsets = build_offsets(dims); - - std::vector> omega_cones{ - {2.0}, {2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> r_cones{{4.0}, {1.0, -0.25, 0.5}, {2.0, 0.5, -0.25, 0.25, 0.75}}; - - std::vector rho_host; - for (const auto& w : omega_cones) { - rho_host.push_back(j_norm_sq(w)); - } - - auto d_omega = make_device_vector(pack_cones(omega_cones)); - auto d_r = make_device_vector(pack_cones(r_cones)); - auto d_rho = make_device_vector(rho_host); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector d_out(d_omega.size(), stream_); - - launch_inverse_jordan_product( - d_omega, d_r, d_rho, d_out, d_offsets, static_cast(dims.size())); - - auto actual = copy_to_host(d_out); - auto expected = pack_cones(std::vector>{ - ref_inverse_jordan_product_single(omega_cones[0], r_cones[0], rho_host[0]), - ref_inverse_jordan_product_single(omega_cones[1], r_cones[1], rho_host[1]), - ref_inverse_jordan_product_single(omega_cones[2], r_cones[2], rho_host[2])}); - - expect_vector_near(actual, expected, 1e-12, 1e-10, "inverse_jordan_product"); - - // Identity check: omega circ (omega \ r) = r - auto d_inv = make_device_vector(actual); - rmm::device_uvector d_roundtrip(d_omega.size(), stream_); - launch_jordan_product(d_omega, d_inv, d_roundtrip, d_offsets, static_cast(dims.size())); - - auto roundtrip = copy_to_host(d_roundtrip); - auto r_packed = pack_cones(r_cones); - expect_vector_near(roundtrip, r_packed, 1e-10, 1e-8, "inverse_identity"); -} - -TEST_F(second_order_cone_test, jordan_and_inverse_jordan_strided_loop_for_large_cone) -{ - std::vector dims{513}; - auto offsets = build_offsets(dims); - - auto a_cone = make_patterned_cone(dims[0], 5.0, 0.005); - auto b_cone = make_patterned_cone(dims[0], 4.0, 0.004); - auto omega_cone = make_patterned_cone(dims[0], 6.0, 0.003); - f_t rho_val = j_norm_sq(omega_cone); - ASSERT_GT(rho_val, 0.0); - - std::vector> a_cones{a_cone}; - std::vector> b_cones{b_cone}; - std::vector> omega_cones{omega_cone}; - - auto d_a = make_device_vector(pack_cones(a_cones)); - auto d_b = make_device_vector(pack_cones(b_cones)); - auto d_omega = make_device_vector(pack_cones(omega_cones)); - auto d_rho = make_device_vector(std::vector{rho_val}); - auto d_offsets = make_device_vector(offsets); - - // Jordan product: strided path - rmm::device_uvector d_jp(d_a.size(), stream_); - launch_jordan_product(d_a, d_b, d_jp, d_offsets, 1); - auto jp_actual = copy_to_host(d_jp); - auto jp_expected = ref_jordan_product_single(a_cone, b_cone); - expect_vector_near(jp_actual, jp_expected, 1e-10, 1e-8, "jordan_large"); - - // Inverse Jordan product: strided path + identity - auto r_cone = make_patterned_cone(dims[0], 3.0, 0.006); - std::vector> r_cones{r_cone}; - - auto d_r = make_device_vector(pack_cones(r_cones)); - rmm::device_uvector d_inv(d_omega.size(), stream_); - launch_inverse_jordan_product(d_omega, d_r, d_rho, d_inv, d_offsets, 1); - - auto inv_actual = copy_to_host(d_inv); - auto inv_expected = ref_inverse_jordan_product_single(omega_cone, r_cone, rho_val); - expect_vector_near(inv_actual, inv_expected, 1e-10, 1e-8, "inv_jordan_large"); - - // Round-trip identity on the large cone - auto d_inv_vec = make_device_vector(inv_actual); - rmm::device_uvector d_rt(d_omega.size(), stream_); - launch_jordan_product(d_omega, d_inv_vec, d_rt, d_offsets, 1); - auto rt_actual = copy_to_host(d_rt); - expect_vector_near(rt_actual, r_cone, 1e-8, 1e-6, "identity_large"); -} - -TEST_F(second_order_cone_test, inverse_jordan_product_with_nt_scaling_rho) -{ - std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; - std::vector dims{3, 5}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - - launch_nt_scaling(cones, stream_); - - auto omega_host = copy_to_host(cones.omega); - auto rho_host = copy_to_host(cones.rho); - - // Build an arbitrary r vector, run inverse Jordan with NT-produced rho/omega - std::vector> r_cones{{1.0, -0.3, 0.2}, {0.5, 0.1, -0.15, 0.25, -0.1}}; - auto d_r = make_device_vector(pack_cones(r_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector d_out(cones.omega.size(), stream_); - - launch_inverse_jordan_product( - cones.omega, d_r, cones.rho, d_out, d_offsets, static_cast(dims.size())); - - auto inv_actual = copy_to_host(d_out); - - // Verify host reference matches using NT-produced values - for (i_t c = 0; c < static_cast(dims.size()); ++c) { - auto omega_c = slice_cone(omega_host, offsets, c); - auto r_c = r_cones[c]; - auto ref = ref_inverse_jordan_product_single(omega_c, r_c, rho_host[c]); - auto actual = slice_cone(inv_actual, offsets, c); - expect_vector_near(actual, ref, 1e-10, 1e-8, "nt_rho_inv_jordan"); - } - - // Round-trip identity with NT-produced omega - rmm::device_uvector d_rt(cones.omega.size(), stream_); - auto d_inv = make_device_vector(inv_actual); - launch_jordan_product(cones.omega, d_inv, d_rt, d_offsets, static_cast(dims.size())); - auto rt_actual = copy_to_host(d_rt); - auto r_packed = pack_cones(r_cones); - expect_vector_near(rt_actual, r_packed, 1e-8, 1e-6, "nt_identity"); -} - TEST_F(second_order_cone_test, fused_corrector_matches_reference_with_nt_scaling) { std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; @@ -1595,7 +1365,7 @@ TEST_F(second_order_cone_test, cone_block_scatter_with_q_overlap) } } -TEST_F(second_order_cone_test, recover_cone_dz_matches_reference) +TEST_F(second_order_cone_test, recover_cone_dz_from_target_matches_reference) { std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; @@ -1615,13 +1385,19 @@ TEST_F(second_order_cone_test, recover_cone_dz_matches_reference) rmm::device_uvector d_rhs(cones.m_c, stream_); launch_affine_cone_rhs(cones, d_rhs); + auto rhs_actual = copy_to_host(d_rhs); + + std::vector target_host(rhs_actual.size(), f_t(0)); + for (std::size_t j = 0; j < rhs_actual.size(); ++j) { + target_host[j] = -rhs_actual[j]; + } + auto d_target = make_device_vector(target_host); rmm::device_uvector d_hinv2_dx(cones.m_c, stream_); rmm::device_uvector d_dz(cones.m_c, stream_); - launch_recover_cone_dz(d_dx, cones, d_rhs, d_hinv2_dx, d_dz); + launch_recover_cone_dz_from_target(d_dx, cones, d_target, d_hinv2_dx, d_dz); auto actual = copy_to_host(d_dz); - auto rhs_actual = copy_to_host(d_rhs); auto w_bar_host = copy_to_host(cones.w_bar); auto inv_eta_h = copy_to_host(cones.inv_eta); @@ -1634,7 +1410,7 @@ TEST_F(second_order_cone_test, recover_cone_dz_matches_reference) for (i_t j = 0; j < static_cast(ref.size()); ++j) { ref[j] = -rhs_c[j] - ref_hinv2[j]; } - expect_vector_near(act, ref, 1e-10, 1e-8, "recover_cone_dz"); + expect_vector_near(act, ref, 1e-10, 1e-8, "recover_cone_dz_from_target"); } } diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index 1bf604bff8..b558b8d50e 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -175,7 +175,7 @@ TEST(barrier, dual_variable_greater_than) EXPECT_NEAR(solution.z[1], 0.0, 1e-5); } -TEST(barrier, cone_metadata_preserved_through_barrier_setup) +TEST(barrier, cone_metadata_reindexed_when_slack_is_inserted_before_cones) { raft::handle_t handle{}; init_handler(&handle); @@ -219,8 +219,11 @@ TEST(barrier, cone_metadata_preserved_through_barrier_setup) lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + ASSERT_EQ(new_slacks.size(), 1); + EXPECT_EQ(new_slacks[0], 1); + EXPECT_EQ(original_lp.num_cols, 6); EXPECT_EQ(original_lp.second_order_cone_dims, user_problem.second_order_cone_dims); - EXPECT_EQ(original_lp.cone_var_start, user_problem.cone_var_start); + EXPECT_EQ(original_lp.cone_var_start, 2); lp_problem_t barrier_lp(user_problem.handle_ptr, original_lp.num_rows, @@ -230,7 +233,209 @@ TEST(barrier, cone_metadata_preserved_through_barrier_setup) column_scaling(original_lp, settings, barrier_lp, column_scales); EXPECT_EQ(barrier_lp.second_order_cone_dims, user_problem.second_order_cone_dims); - EXPECT_EQ(barrier_lp.cone_var_start, user_problem.cone_var_start); + EXPECT_EQ(barrier_lp.cone_var_start, 2); +} + +TEST(barrier, presolve_reindexes_cone_start_after_empty_column_removal) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 4; + constexpr int nz = 3; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 0, 1, 2, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = -1.0; + user_problem.A.i[2] = 0; + user_problem.A.x[2] = 0.5; + + user_problem.rhs = {1.0}; + user_problem.row_sense = {'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + presolve_info_t presolve_info; + lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); + ASSERT_EQ(presolve(original_lp, settings, presolved_lp, presolve_info), 0); + + EXPECT_EQ(presolved_lp.num_cols, 3); + EXPECT_EQ(presolved_lp.second_order_cone_dims, std::vector({3})); + EXPECT_EQ(presolved_lp.cone_var_start, 0); + + lp_problem_t barrier_lp(user_problem.handle_ptr, + presolved_lp.num_rows, + presolved_lp.num_cols, + presolved_lp.A.col_start[presolved_lp.num_cols]); + std::vector column_scales; + ASSERT_EQ(column_scaling(presolved_lp, settings, barrier_lp, column_scales), 0); + EXPECT_EQ(barrier_lp.cone_var_start, 0); +} + +TEST(barrier, presolve_packs_free_variable_partner_before_cones) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 5; + constexpr int nz = 5; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 1, 2, 3, 4, 5}; + for (int j = 0; j < n; ++j) { + user_problem.A.i[j] = 0; + user_problem.A.x[j] = 1.0; + } + + user_problem.rhs = {1.0}; + user_problem.row_sense = {'E'}; + // Two free linear vars ensure the new implied-bound pass cannot fully + // eliminate the free-variable expansion path before the cone block. + user_problem.lower = {-inf, -inf, 0.0, 0.0, 0.0}; + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 2; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + presolve_info_t presolve_info; + lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); + ASSERT_EQ(presolve(original_lp, settings, presolved_lp, presolve_info), 0); + + EXPECT_EQ(presolved_lp.num_cols, 7); + EXPECT_EQ(presolved_lp.cone_var_start, 4); + EXPECT_EQ(presolved_lp.second_order_cone_dims, std::vector({3})); + ASSERT_EQ(presolve_info.free_variable_pairs.size(), 4); + EXPECT_EQ(presolve_info.free_variable_pairs[0], 0); + EXPECT_EQ(presolve_info.free_variable_pairs[1], 2); + EXPECT_EQ(presolve_info.free_variable_pairs[2], 1); + EXPECT_EQ(presolve_info.free_variable_pairs[3], 3); +} + +TEST(barrier, uncrush_solution_removes_non_tail_free_variable_partner) +{ + using namespace cuopt::linear_programming::dual_simplex; + + presolve_info_t presolve_info; + presolve_info.free_variable_pairs = {0, 1}; + + simplex_solver_settings_t settings; + std::vector crushed_x{5.0, 2.0, 9.0, 8.0}; + std::vector crushed_y{}; + std::vector crushed_z{7.0, 11.0, 13.0, 17.0}; + std::vector uncrushed_x(3); + std::vector uncrushed_y(0); + std::vector uncrushed_z(3); + + uncrush_solution(presolve_info, + settings, + crushed_x, + crushed_y, + crushed_z, + uncrushed_x, + uncrushed_y, + uncrushed_z); + + EXPECT_EQ(uncrushed_x, std::vector({3.0, 9.0, 8.0})); + EXPECT_EQ(uncrushed_z, std::vector({7.0, 13.0, 17.0})); +} + +TEST(barrier, rejects_middle_cone_input_before_barrier) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 3; + constexpr int n = 5; + constexpr int nz = 3; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0, 0.0, 0.0, 0.0, 1.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 1, 1, 2, 2, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 2; + user_problem.A.x[2] = 1.0; + + user_problem.rhs = {2.0, 1.0, 3.0}; + user_problem.row_sense = {'E', 'E', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = true; + settings.dualize = 0; + lp_solution_t solution(m, n); + + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + EXPECT_EQ(status, lp_status_t::NUMERICAL_ISSUES); } TEST(barrier, socp_min_x0_subject_to_norm_constraint) @@ -305,4 +510,497 @@ TEST(barrier, socp_min_x0_subject_to_norm_constraint) EXPECT_NEAR(std::abs(solution.x[2]), 0.0, 1e-4); } +TEST(barrier, mixed_linear_and_soc_block) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize l + // subject to l - t = 0 + // u = 1 + // (t, u, v) in Q^3 + // + // Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 4; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 1, 2, 3, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = -1.0; + user_problem.A.i[2] = 1; + user_problem.A.x[2] = 1.0; + + user_problem.rhs = {0.0, 1.0}; + user_problem.row_sense = {'E', 'E'}; + + user_problem.lower = {0.0, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_soc_block"; + + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_soc_tail_coupling) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize t + // subject to l - u = 0 + // l + u = 2 + // (t, u, v) in Q^3 + // + // Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 4; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 2, 2, 4, 4}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 0; + user_problem.A.x[2] = -1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + + user_problem.rhs = {0.0, 2.0}; + user_problem.row_sense = {'E', 'E'}; + user_problem.lower = {0.0, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_soc_tail_coupling"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.scale_columns = true; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_soc_tail_coupling_with_inequality) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize t + // subject to l - u = 0 + // l + u >= 2 + // (t, u, v) in Q^3 + // + // Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 4; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 2, 2, 4, 4}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 0; + user_problem.A.x[2] = -1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + + user_problem.rhs = {0.0, 2.0}; + user_problem.row_sense = {'E', 'G'}; + user_problem.lower = {0.0, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_soc_tail_coupling_with_inequality"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.scale_columns = true; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_two_soc_blocks) +{ + // Variables ordered as [l1, l2 | t1, u1, v1 | t2, u2, v2], + // where (t1, u1, v1), (t2, u2, v2) \in Q^3. + // + // minimize t1 + t2 + // subject to l1 - u1 = 0 + // l2 - u2 = 0 + // l1 + l2 = 3 + // l1 - l2 = 1 + // + // Optimal: l1* = 2, l2* = 1, t1* = 2, u1* = 2, v1* = 0, + // t2* = 1, u2* = 1, v2* = 0, obj* = 3. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 4; + constexpr int n = 8; + constexpr int nz = 8; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l1, l2, t1, u1, v1, t2, u2, v2 + user_problem.A.col_start = {0, 3, 6, 6, 7, 7, 7, 8, 8}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 2; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 3; + user_problem.A.x[2] = 1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + user_problem.A.i[4] = 2; + user_problem.A.x[4] = 1.0; + user_problem.A.i[5] = 3; + user_problem.A.x[5] = -1.0; + user_problem.A.i[6] = 0; + user_problem.A.x[6] = -1.0; + user_problem.A.i[7] = 1; + user_problem.A.x[7] = -1.0; + + user_problem.rhs = {0.0, 0.0, 3.0, 1.0}; + user_problem.row_sense = {'E', 'E', 'E', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_two_soc_blocks"; + user_problem.cone_var_start = 2; + user_problem.second_order_cone_dims = {3, 3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 3.0, 1e-4); + EXPECT_NEAR(solution.x[0], 2.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 2.0, 1e-4); + EXPECT_NEAR(solution.x[3], 2.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[4]), 0.0, 1e-4); + EXPECT_NEAR(solution.x[5], 1.0, 1e-4); + EXPECT_NEAR(solution.x[6], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[7]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_two_soc_blocks_with_inequality) +{ + // Variables ordered as [l1, l2 | t1, u1, v1 | t2, u2, v2], + // where (t1, u1, v1), (t2, u2, v2) \in Q^3. + // + // minimize t1 + t2 + // subject to l1 - u1 = 0 + // l2 - u2 = 0 + // l1 + l2 >= 3 + // l1 - l2 = 1 + // + // Optimal: l1* = 2, l2* = 1, t1* = 2, u1* = 2, v1* = 0, + // t2* = 1, u2* = 1, v2* = 0, obj* = 3. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 4; + constexpr int n = 8; + constexpr int nz = 8; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l1, l2, t1, u1, v1, t2, u2, v2 + user_problem.A.col_start = {0, 3, 6, 6, 7, 7, 7, 8, 8}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 2; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 3; + user_problem.A.x[2] = 1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + user_problem.A.i[4] = 2; + user_problem.A.x[4] = 1.0; + user_problem.A.i[5] = 3; + user_problem.A.x[5] = -1.0; + user_problem.A.i[6] = 0; + user_problem.A.x[6] = -1.0; + user_problem.A.i[7] = 1; + user_problem.A.x[7] = -1.0; + + user_problem.rhs = {0.0, 0.0, 3.0, 1.0}; + user_problem.row_sense = {'E', 'E', 'G', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_two_soc_blocks_with_inequality"; + user_problem.cone_var_start = 2; + user_problem.second_order_cone_dims = {3, 3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.scale_columns = true; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 3.0, 1e-4); + EXPECT_NEAR(solution.x[0], 2.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 2.0, 1e-4); + EXPECT_NEAR(solution.x[3], 2.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[4]), 0.0, 1e-4); + EXPECT_NEAR(solution.x[5], 1.0, 1e-4); + EXPECT_NEAR(solution.x[6], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[7]), 0.0, 1e-4); +} + +TEST(barrier, free_linear_prefix_is_uncrushed_correctly_with_soc_block) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3 and l is free. + // + // minimize t + // subject to l - u = 0 + // u = 1 + // (t, u, v) in Q^3 + // + // Presolve splits the free linear variable into a partner column before the + // cone block, so the returned user-space solution must uncrush back to + // l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 3; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 1, 1, 3, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = -1.0; + user_problem.A.i[2] = 1; + user_problem.A.x[2] = 1.0; + + user_problem.rhs = {0.0, 1.0}; + user_problem.row_sense = {'E', 'E'}; + user_problem.lower = {-inf, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "free_linear_prefix_is_uncrushed_correctly_with_soc_block"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, qp_with_soc_block) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize 0.5 l^2 + t + // subject to l + u = 2 + // (t, u, v) in Q^3 + // + // Since t >= |u| and u = 2 - l with l >= 0, the objective becomes + // 0.5 l^2 + |2 - l|, which is minimized at l* = 1, u* = 1, t* = 1, v* = 0. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 4; + constexpr int nz = 2; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 1, 1, 2, 2}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = 1.0; + + user_problem.rhs = {2.0}; + user_problem.row_sense = {'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + + user_problem.Q_offsets = {0, 1, 1, 1, 1}; + user_problem.Q_indices = {0}; + user_problem.Q_values = {1.0}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "qp_with_soc_block"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.5, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + } // namespace cuopt::linear_programming::dual_simplex::test From a5dba68c29e39948b639265a18700d212fc616c0 Mon Sep 17 00:00:00 2001 From: Yan Zaretskiy Date: Tue, 7 Apr 2026 13:12:34 -0700 Subject: [PATCH 05/68] test(dual_simplex): expand SOCP barrier coverage and tighten layout checks Add row-cone and presolve regression cases around the SOCP barrier path, clean up the PR-facing test wording, and align the cone layout validation with the shared infinity convention. Signed-off-by: Yan Zaretskiy --- cpp/src/barrier/barrier.cu | 2 +- cpp/src/dual_simplex/presolve.cpp | 67 ++- cpp/src/dual_simplex/solve.cpp | 114 +++-- cpp/src/dual_simplex/user_problem.hpp | 2 + .../unit_tests/second_order_cone_test.cu | 9 +- .../dual_simplex/unit_tests/solve_barrier.cu | 420 +++++++++++++++++- 6 files changed, 561 insertions(+), 53 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index 1d937a4c5e..ae1364f027 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -2048,7 +2048,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.v[k] = -c[j] + epsilon; } } - // Now hande the case with no upper bounds (skip cone variables) + // Now handle the case with no upper bounds (skip cone variables) const i_t cone_end = has_cones ? data.cone_var_start_ + data.cones_->m_c : 0; for (i_t j = 0; j < lp.num_cols; j++) { if (has_cones && j >= data.cone_var_start_ && j < cone_end) continue; diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 90ae6ff74e..7adee4525a 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -25,6 +25,68 @@ static i_t linear_var_count(const lp_problem_t& problem) return problem.second_order_cone_dims.empty() ? problem.num_cols : problem.cone_var_start; } +template +static void lift_second_order_cone_rows(const user_problem_t& user_problem, + std::vector& row_sense, + lp_problem_t& problem) +{ + if (user_problem.second_order_cone_row_dims.empty()) { return; } + + auto& dims = user_problem.second_order_cone_row_dims; + const i_t lifted_row_count = std::accumulate(dims.begin(), dims.end(), i_t{0}); + const i_t cone_row_start = user_problem.cone_row_start; + + const i_t old_num_cols = problem.num_cols; + const i_t new_num_cols = old_num_cols + lifted_row_count; + const i_t old_nnz = problem.A.col_start[old_num_cols]; + const i_t new_nnz = old_nnz + lifted_row_count; + + auto old_A = problem.A; + csc_matrix_t lifted_A(problem.num_rows, new_num_cols, new_nnz); + + i_t nz = 0; + for (i_t j = 0; j < old_num_cols; ++j) { + lifted_A.col_start[j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + lifted_A.i[nz] = old_A.i[p]; + lifted_A.x[nz] = old_A.x[p]; + ++nz; + } + } + + for (i_t offset = 0; offset < lifted_row_count; ++offset) { + const i_t j = old_num_cols + offset; + const i_t row = cone_row_start + offset; + lifted_A.col_start[j] = nz; + lifted_A.i[nz] = row; + lifted_A.x[nz] = 1.0; + row_sense[row] = 'E'; + ++nz; + } + lifted_A.col_start[new_num_cols] = nz; + assert(nz == new_nnz); + + std::vector objective(new_num_cols, 0.0); + std::vector lower(new_num_cols, 0.0); + std::vector upper(new_num_cols, inf); + for (i_t j = 0; j < old_num_cols; ++j) { + objective[j] = problem.objective[j]; + lower[j] = problem.lower[j]; + upper[j] = problem.upper[j]; + } + + problem.A = lifted_A; + problem.A.n = new_num_cols; + problem.objective = objective; + problem.lower = lower; + problem.upper = upper; + problem.num_cols = new_num_cols; + if (problem.second_order_cone_dims.empty()) { problem.cone_var_start = old_num_cols; } + problem.second_order_cone_dims.insert(problem.second_order_cone_dims.end(), + user_problem.second_order_cone_row_dims.begin(), + user_problem.second_order_cone_row_dims.end()); +} + template i_t remove_empty_cols(lp_problem_t& problem, i_t& num_empty_cols, @@ -697,6 +759,9 @@ void convert_user_problem(const user_problem_t& user_problem, // Make a copy of row_sense so we can modify it std::vector row_sense = user_problem.row_sense; + if (settings.barrier && !user_problem.second_order_cone_row_dims.empty()) { + lift_second_order_cone_rows(user_problem, row_sense, problem); + } // The original problem can have constraints in the form // a_i^T x >= b, a_i^T x <= b, and a_i^T x == b @@ -750,7 +815,7 @@ void convert_user_problem(const user_problem_t& user_problem, settings.log.debug( "equality rows %d less rows %d columns %d\n", equal_rows, less_rows, problem.num_cols); if (settings.barrier && settings.dualize != 0 && user_problem.Q_values.size() == 0 && - user_problem.second_order_cone_dims.empty() && + problem.second_order_cone_dims.empty() && (settings.dualize == 1 || (settings.dualize == -1 && less_rows > 1.2 * problem.num_cols && equal_rows < 2e4))) { settings.log.debug("Dualizing in presolve\n"); diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 544647d5d0..021e0e8a20 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -37,6 +37,48 @@ namespace cuopt::linear_programming::dual_simplex { namespace { +template +bool validate_second_order_cone_row_metadata(const user_problem_t& user_problem, + const simplex_solver_settings_t& settings) +{ + if (user_problem.second_order_cone_row_dims.empty()) { return true; } + + i_t lifted_row_count = 0; + for (auto q_k : user_problem.second_order_cone_row_dims) { + if (q_k < 0) { + settings.log.printf("Error: second-order cone row dimensions must be nonnegative\n"); + return false; + } + lifted_row_count += q_k; + } + + if (user_problem.cone_row_start < 0) { + settings.log.printf("Error: cone_row_start must be nonnegative\n"); + return false; + } + + const i_t cone_row_end = user_problem.cone_row_start + lifted_row_count; + if (cone_row_end > user_problem.num_rows) { + settings.log.printf("Error: second-order cone row block exceeds the number of rows\n"); + return false; + } + + if (user_problem.num_range_rows > static_cast(user_problem.range_rows.size())) { + settings.log.printf("Error: range row metadata is inconsistent\n"); + return false; + } + + for (i_t k = 0; k < user_problem.num_range_rows; ++k) { + const i_t row = user_problem.range_rows[k]; + if (row >= user_problem.cone_row_start && row < cone_row_end) { + settings.log.printf("Error: range rows cannot intersect the second-order cone row block\n"); + return false; + } + } + + return true; +} + template void write_matlab(const std::string& filename, const dual_simplex::lp_problem_t& lp) { @@ -61,6 +103,42 @@ void write_matlab(const std::string& filename, const dual_simplex::lp_problem_t< fclose(fid); } +template +bool validate_barrier_cone_layout(const lp_problem_t& problem, + const simplex_solver_settings_t& settings) +{ + if (problem.second_order_cone_dims.empty()) { return true; } + + i_t cone_end = problem.cone_var_start; + for (auto q_k : problem.second_order_cone_dims) { + if (q_k <= 1) { + settings.log.printf( + "Error: second-order cone dimensions must be at least 2; use linear variables instead of " + "Q^1\n"); + return false; + } + cone_end += q_k; + } + + if (cone_end != problem.num_cols) { + settings.log.printf("Error: conic variables must form a trailing block [linear | cone]\n"); + return false; + } + + for (i_t j = problem.cone_var_start; j < cone_end; ++j) { + if (problem.lower[j] != 0.0 && problem.lower[j] > -inf) { + settings.log.printf("Error: explicit lower bound on conic variable %d is not supported\n", j); + return false; + } + if (problem.upper[j] < inf) { + settings.log.printf("Error: explicit upper bound on conic variable %d is not supported\n", j); + return false; + } + } + + return true; +} + } // namespace template @@ -344,35 +422,12 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us simplex_solver_settings_t barrier_settings = settings; barrier_settings.barrier_presolve = true; dualize_info_t dualize_info; + if (!validate_second_order_cone_row_metadata(user_problem, settings)) { + return lp_status_t::NUMERICAL_ISSUES; + } convert_user_problem(user_problem, barrier_settings, original_lp, new_slacks, dualize_info); - - if (!user_problem.second_order_cone_dims.empty()) { - i_t cone_end = user_problem.cone_var_start; - for (auto q_k : user_problem.second_order_cone_dims) { - if (q_k <= 1) { - settings.log.printf( - "Error: second-order cone dimensions must be at least 2; use linear variables instead of " - "Q^1\n"); - return lp_status_t::NUMERICAL_ISSUES; - } - cone_end += q_k; - } - if (cone_end != user_problem.num_cols) { - settings.log.printf("Error: conic variables must form a trailing block [linear | cone]\n"); - return lp_status_t::NUMERICAL_ISSUES; - } - for (i_t j = user_problem.cone_var_start; j < cone_end; ++j) { - if (user_problem.lower[j] != 0.0 && user_problem.lower[j] > -1e30) { - settings.log.printf("Error: explicit lower bound on conic variable %d is not supported\n", - j); - return lp_status_t::NUMERICAL_ISSUES; - } - if (user_problem.upper[j] < 1e30) { - settings.log.printf("Error: explicit upper bound on conic variable %d is not supported\n", - j); - return lp_status_t::NUMERICAL_ISSUES; - } - } + if (!validate_barrier_cone_layout(original_lp, settings)) { + return lp_status_t::NUMERICAL_ISSUES; } lp_solution_t lp_solution(original_lp.num_rows, original_lp.num_cols); @@ -604,7 +659,8 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us uncrush_primal_solution(user_problem, original_lp, lp_solution.x, solution.x); uncrush_dual_solution( user_problem, original_lp, lp_solution.y, lp_solution.z, solution.y, solution.z); - solution.objective = barrier_solution.objective; + solution.objective = + barrier_solution.user_objective / user_problem.obj_scale - user_problem.obj_constant; solution.user_objective = barrier_solution.user_objective; solution.l2_primal_residual = barrier_solution.l2_primal_residual; solution.l2_dual_residual = barrier_solution.l2_dual_residual; diff --git a/cpp/src/dual_simplex/user_problem.hpp b/cpp/src/dual_simplex/user_problem.hpp index 8b0588064c..db62891185 100644 --- a/cpp/src/dual_simplex/user_problem.hpp +++ b/cpp/src/dual_simplex/user_problem.hpp @@ -54,6 +54,8 @@ struct user_problem_t { std::vector Q_values; i_t cone_var_start{0}; std::vector second_order_cone_dims; + i_t cone_row_start{0}; + std::vector second_order_cone_row_dims; }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu index e6f48ac9de..b3e8974e8b 100644 --- a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu @@ -614,8 +614,7 @@ TEST_F(second_order_cone_test, cone_data_reuses_named_scratch_slots) TEST_F(second_order_cone_test, nt_scaling_matches_reference_for_small_cone) { - // Borrowed from the Clarabel regression input, but checked against our own - // host-side NT formulas. + // Fixed small-cone fixture validated against the host-side NT formulas. std::vector> s_cones{{1.5, 0.3, 0.4}}; std::vector> lambda_cones{{2.0, 0.5, 0.5}}; std::vector dims{3}; @@ -855,15 +854,15 @@ TEST_F(second_order_cone_test, step_length_matches_reference_for_large_cone) EXPECT_LT(actual_alpha[0], alpha_max); } -TEST_F(second_order_cone_test, step_length_boundary_c_zero_matches_clarabel_branch) +TEST_F(second_order_cone_test, step_length_boundary_c_zero_returns_zero) { std::vector dims{3}; auto offsets = build_offsets(dims); // Boundary point: c = u^T J u = 1^2 - 1^2 - 0^2 = 0. // Direction: a = du^T J du = 1^2 - 1^2 - 1^2 = -1 < 0. - // Clarabel's c == 0 branch returns 0 in this case because the direction - // leaves the cone immediately. + // The step length is 0 in this case because the direction leaves the cone + // immediately. std::vector> s_cones{{1.0, 1.0, 0.0}}; std::vector> ds_cones{{1.0, 1.0, 1.0}}; std::vector> lambda_cones{{1.0, 1.0, 0.0}}; diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index b558b8d50e..1b4a866221 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -5,8 +5,6 @@ */ /* clang-format on */ -#include - #include #include @@ -34,6 +32,72 @@ static void init_handler(const raft::handle_t* handle_ptr) handle_ptr->get_cusparse_handle(), CUSPARSE_POINTER_MODE_DEVICE, handle_ptr->get_stream())); } +template +static void populate_basic_qp_socp_problem(user_problem_t& user_problem, + bool explicit_cone_variables) +{ + constexpr i_t num_rows = 9; + constexpr f_t p00 = static_cast(1.4652521089139698); + constexpr f_t p01 = static_cast(0.6137176286085666); + constexpr f_t p02 = static_cast(-1.1527861771130112); + constexpr f_t p11 = static_cast(2.219109946678485); + constexpr f_t p12 = static_cast(-1.4400420548730628); + constexpr f_t p22 = static_cast(1.6014483534926371); + + user_problem.num_rows = num_rows; + user_problem.rhs = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0}; + user_problem.row_sense = {'L', 'L', 'L', 'L', 'L', 'L', 'E', 'E', 'E'}; + user_problem.num_range_rows = 0; + + if (explicit_cone_variables) { + user_problem.num_cols = 6; + user_problem.objective = {0.1, -2.0, 1.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = num_rows; + user_problem.A.n = user_problem.num_cols; + user_problem.A.nz_max = 12; + user_problem.A.reallocate(12); + user_problem.A.col_start = {0, 3, 6, 9, 10, 11, 12}; + user_problem.A.i = {0, 3, 6, 1, 4, 7, 2, 5, 8, 6, 7, 8}; + user_problem.A.x = {2.0, -2.0, 1.0, 2.0, -2.0, 1.0, 2.0, -2.0, 1.0, 1.0, 1.0, 1.0}; + + user_problem.lower = {-inf, -inf, -inf, 0.0, 0.0, 0.0}; + user_problem.upper.assign(user_problem.num_cols, inf); + + user_problem.Q_offsets = {0, 3, 6, 9, 9, 9, 9}; + user_problem.Q_indices = {0, 1, 2, 0, 1, 2, 0, 1, 2}; + user_problem.Q_values = {p00, p01, p02, p01, p11, p12, p02, p12, p22}; + + user_problem.cone_var_start = 3; + user_problem.second_order_cone_dims = {3}; + user_problem.problem_name = "basic_qp_socp_explicit_cone"; + } else { + user_problem.num_cols = 3; + user_problem.objective = {0.1, -2.0, 1.0}; + + user_problem.A.m = num_rows; + user_problem.A.n = user_problem.num_cols; + user_problem.A.nz_max = 9; + user_problem.A.reallocate(9); + user_problem.A.col_start = {0, 3, 6, 9}; + user_problem.A.i = {0, 3, 6, 1, 4, 7, 2, 5, 8}; + user_problem.A.x = {2.0, -2.0, 1.0, 2.0, -2.0, 1.0, 2.0, -2.0, 1.0}; + + user_problem.lower.assign(user_problem.num_cols, -inf); + user_problem.upper.assign(user_problem.num_cols, inf); + + user_problem.Q_offsets = {0, 3, 6, 9}; + user_problem.Q_indices = {0, 1, 2, 0, 1, 2, 0, 1, 2}; + user_problem.Q_values = {p00, p01, p02, p01, p11, p12, p02, p12, p22}; + + user_problem.cone_row_start = 6; + user_problem.second_order_cone_row_dims = {3}; + user_problem.problem_name = "basic_qp_socp_row_cone"; + } + + user_problem.var_types.assign(user_problem.num_cols, variable_type_t::CONTINUOUS); +} + TEST(barrier, chess_set) { namespace dual_simplex = cuopt::linear_programming::dual_simplex; @@ -140,7 +204,6 @@ TEST(barrier, dual_variable_greater_than) user_problem.A.x[nnz++] = 1.0; user_problem.A.i[nnz] = 1; user_problem.A.x[nnz++] = 2.0; - user_problem.A.print_matrix(); EXPECT_EQ(nnz, nz); user_problem.rhs.resize(m); @@ -210,7 +273,7 @@ TEST(barrier, cone_metadata_reindexed_when_slack_is_inserted_before_cones) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = false; + settings.barrier_presolve = true; settings.dualize = 0; settings.scale_columns = false; @@ -236,6 +299,225 @@ TEST(barrier, cone_metadata_reindexed_when_slack_is_inserted_before_cones) EXPECT_EQ(barrier_lp.cone_var_start, 2); } +TEST(barrier, row_cone_block_is_lifted_into_trailing_cone_variables) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 3; + constexpr int n = 2; + constexpr int nz = 4; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 2, 4}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 2; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 1; + user_problem.A.x[2] = -1.0; + user_problem.A.i[3] = 2; + user_problem.A.x[3] = 2.0; + + user_problem.rhs = {3.0, 1.0, 4.0}; + user_problem.row_sense = {'E', 'E', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_row_start = 0; + user_problem.second_order_cone_row_dims = {3}; + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + EXPECT_TRUE(new_slacks.empty()); + EXPECT_EQ(original_lp.num_cols, 5); + EXPECT_EQ(original_lp.cone_var_start, 2); + EXPECT_EQ(original_lp.second_order_cone_dims, std::vector({3})); + + for (int j = 2; j < 5; ++j) { + EXPECT_EQ(original_lp.A.col_start[j + 1] - original_lp.A.col_start[j], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[j]], j - 2); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[j]], 1.0); + EXPECT_EQ(original_lp.objective[j], 0.0); + EXPECT_EQ(original_lp.lower[j], 0.0); + EXPECT_EQ(original_lp.upper[j], inf); + } +} + +TEST(barrier, row_cone_block_and_scalar_inequality_order_as_linear_slack_then_cone) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 4; + constexpr int n = 1; + constexpr int nz = 2; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 2}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = -1.0; + + user_problem.rhs = {2.0, 0.0, 1.0, 0.0}; + user_problem.row_sense = {'L', 'E', 'E', 'E'}; + user_problem.lower = {0.0}; + user_problem.upper = {inf}; + user_problem.num_range_rows = 0; + user_problem.cone_row_start = 1; + user_problem.second_order_cone_row_dims = {3}; + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + ASSERT_EQ(new_slacks.size(), 1); + EXPECT_EQ(new_slacks[0], 1); + EXPECT_EQ(original_lp.num_cols, 5); + EXPECT_EQ(original_lp.cone_var_start, 2); + EXPECT_EQ(original_lp.second_order_cone_dims, std::vector({3})); + + EXPECT_EQ(original_lp.A.col_start[1] - original_lp.A.col_start[0], 2); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[0]], 0); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[0]], 1.0); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[0] + 1], 1); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[0] + 1], -1.0); + + EXPECT_EQ(original_lp.A.col_start[2] - original_lp.A.col_start[1], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[1]], 0); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[1]], 1.0); + + for (int j = 2; j < 5; ++j) { + EXPECT_EQ(original_lp.A.col_start[j + 1] - original_lp.A.col_start[j], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[j]], j - 1); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[j]], 1.0); + EXPECT_EQ(original_lp.objective[j], 0.0); + EXPECT_EQ(original_lp.lower[j], 0.0); + EXPECT_EQ(original_lp.upper[j], inf); + } +} + +TEST(barrier, explicit_and_lifted_cones_stay_contiguous_after_scalar_slack_insertion) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 4; + constexpr int n = 4; + constexpr int nz = 5; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 2, 3, 4, 5}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = -1.0; + user_problem.A.i[2] = 1; + user_problem.A.x[2] = 1.0; + user_problem.A.i[3] = 2; + user_problem.A.x[3] = 2.0; + user_problem.A.i[4] = 3; + user_problem.A.x[4] = -3.0; + + user_problem.rhs = {2.0, 0.0, 0.0, 0.0}; + user_problem.row_sense = {'L', 'E', 'E', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.cone_row_start = 1; + user_problem.second_order_cone_row_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + ASSERT_EQ(new_slacks.size(), 1); + EXPECT_EQ(new_slacks[0], 1); + EXPECT_EQ(original_lp.num_cols, 8); + EXPECT_EQ(original_lp.cone_var_start, 2); + EXPECT_EQ(original_lp.second_order_cone_dims, std::vector({3, 3})); + + EXPECT_EQ(original_lp.A.col_start[2] - original_lp.A.col_start[1], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[1]], 0); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[1]], 1.0); + + EXPECT_EQ(original_lp.A.col_start[3] - original_lp.A.col_start[2], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[2]], 1); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[2]], 1.0); + EXPECT_EQ(original_lp.A.col_start[4] - original_lp.A.col_start[3], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[3]], 2); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[3]], 2.0); + EXPECT_EQ(original_lp.A.col_start[5] - original_lp.A.col_start[4], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[4]], 3); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[4]], -3.0); + + for (int j = 5; j < 8; ++j) { + EXPECT_EQ(original_lp.A.col_start[j + 1] - original_lp.A.col_start[j], 1); + EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[j]], j - 4); + EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[j]], 1.0); + EXPECT_EQ(original_lp.objective[j], 0.0); + EXPECT_EQ(original_lp.lower[j], 0.0); + EXPECT_EQ(original_lp.upper[j], inf); + } +} + TEST(barrier, presolve_reindexes_cone_start_after_empty_column_removal) { raft::handle_t handle{}; @@ -487,22 +769,9 @@ TEST(barrier, socp_min_x0_subject_to_norm_constraint) settings.barrier = true; settings.barrier_presolve = false; settings.dualize = 0; - settings.set_log(true); - settings.log.log_to_console = false; - settings.log.enable_log_to_file(); - settings.log.set_log_file("/tmp/socp_barrier_test.log"); lp_solution_t solution(m, n); - printf("=== Calling solve_linear_program_with_barrier ===\n"); - fflush(stdout); auto status = solve_linear_program_with_barrier(user_problem, settings, solution); - printf("=== status=%d obj=%e x=[%e %e %e] ===\n", - static_cast(status), - solution.objective, - solution.x[0], - solution.x[1], - solution.x[2]); - fflush(stdout); EXPECT_EQ(status, lp_status_t::OPTIMAL); EXPECT_NEAR(solution.objective, 1.0, 1e-4); EXPECT_NEAR(solution.x[0], 1.0, 1e-4); @@ -510,6 +779,123 @@ TEST(barrier, socp_min_x0_subject_to_norm_constraint) EXPECT_NEAR(std::abs(solution.x[2]), 0.0, 1e-4); } +TEST(barrier, socp_min_x_subject_to_row_cone_metadata) +{ + // minimize x + // subject to -x + s_0 = 0 + // s_1 = 1 + // s_2 = 0 + // (s_0, s_1, s_2) in Q^3 + // + // Optimal: x* = 1, obj* = 1. + + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 3; + constexpr int n = 1; + constexpr int nz = 1; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 1}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = -1.0; + + user_problem.rhs = {0.0, 1.0, 0.0}; + user_problem.row_sense = {'E', 'E', 'E'}; + user_problem.lower = {0.0}; + user_problem.upper = {inf}; + user_problem.num_range_rows = 0; + user_problem.problem_name = "socp_row_cone_metadata"; + user_problem.cone_row_start = 0; + user_problem.second_order_cone_row_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.scale_columns = false; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); +} + +TEST(barrier, basic_qp_socp_row_cone_matches_reference_solution) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + populate_basic_qp_socp_problem(user_problem, false); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.scale_columns = false; + + lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.x[0], -0.5, 1e-3); + EXPECT_NEAR(solution.x[1], 0.435603, 1e-3); + EXPECT_NEAR(solution.x[2], -0.245459, 1e-3); + EXPECT_NEAR(solution.objective, -0.84590, 1e-3); +} + +TEST(barrier, basic_qp_socp_row_cone_matches_explicit_cone_formulation) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t row_cone_problem(&handle); + user_problem_t explicit_cone_problem(&handle); + populate_basic_qp_socp_problem(row_cone_problem, false); + populate_basic_qp_socp_problem(explicit_cone_problem, true); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.dualize = 0; + settings.scale_columns = false; + + lp_solution_t row_cone_solution(row_cone_problem.num_rows, + row_cone_problem.num_cols); + lp_solution_t explicit_cone_solution(explicit_cone_problem.num_rows, + explicit_cone_problem.num_cols); + + auto row_cone_status = + solve_linear_program_with_barrier(row_cone_problem, settings, row_cone_solution); + auto explicit_status = + solve_linear_program_with_barrier(explicit_cone_problem, settings, explicit_cone_solution); + + EXPECT_EQ(row_cone_status, lp_status_t::OPTIMAL); + EXPECT_EQ(explicit_status, lp_status_t::OPTIMAL); + EXPECT_NEAR(row_cone_solution.objective, explicit_cone_solution.objective, 1e-4); + EXPECT_NEAR(row_cone_solution.objective, -0.84590, 1e-3); + for (int j = 0; j < 3; ++j) { + EXPECT_NEAR(row_cone_solution.x[j], explicit_cone_solution.x[j], 1e-4); + } +} + TEST(barrier, mixed_linear_and_soc_block) { // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. From f4b9c8ed57a0cbff16ec90ad030c9f373023af4d Mon Sep 17 00:00:00 2001 From: YUWEN Chen Date: Wed, 15 Apr 2026 03:13:44 -0700 Subject: [PATCH 06/68] feat(mps_parser): add QCMATRIX parsing and quadratic-constraint model support --- .../include/mps_parser/mps_data_model.hpp | 39 ++++ .../include/mps_parser/parser.hpp | 2 + cpp/libmps_parser/src/mps_data_model.cpp | 52 ++++++ cpp/libmps_parser/src/mps_parser.cpp | 172 ++++++++++++++++-- cpp/libmps_parser/src/mps_parser.hpp | 18 ++ cpp/libmps_parser/tests/mps_parser_test.cpp | 45 +++++ 6 files changed, 315 insertions(+), 13 deletions(-) diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 6879e15d60..6022639f46 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -262,6 +262,40 @@ class mps_data_model_t { const i_t* Q_offsets, i_t size_offsets); + /** + * @brief CSR of Q for one quadratic constraint (MPS QCMATRIX). + * + * @c constraint_row_index is the row index in the linear constraint matrix A (0-based), + * matching the order of non-objective rows in the ROWS section. + */ + struct quadratic_constraint_matrix_t { + i_t constraint_row_index{}; + std::vector values; + std::vector indices; + std::vector offsets; + }; + + /** + * @brief Append one quadratic constraint matrix (QCMATRIX) in CSR format. + * + * @param constraint_row_index Row index in A (0-based), matching non-objective ROWS order. + * @param[in] Qc_values Values of the CSR representation; copied into the model. + * @param size_values Size of the Qc_values array. + * @param[in] Qc_indices Indices of the CSR representation; copied into the model. + * @param size_indices Size of the Qc_indices array. + * @param[in] Qc_offsets Offsets of the CSR representation; copied into the model. + * @param size_offsets Size of the Qc_offsets array. + */ + void append_quadratic_constraint_matrix(i_t constraint_row_index, + const f_t* Qc_values, + i_t size_values, + const i_t* Qc_indices, + i_t size_indices, + const i_t* Qc_offsets, + i_t size_offsets); + + const std::vector& get_quadratic_constraint_matrices() const; + i_t get_n_variables() const; i_t get_n_constraints() const; i_t get_nnz() const; @@ -306,6 +340,8 @@ class mps_data_model_t { bool has_quadratic_objective() const noexcept; + bool has_quadratic_constraints() const noexcept; + /** whether to maximize or minimize the objective function */ bool maximize_; /** @@ -361,6 +397,9 @@ class mps_data_model_t { std::vector Q_objective_indices_; std::vector Q_objective_offsets_; + /** One CSR matrix per QCMATRIX block, in order of appearance in the file */ + std::vector quadratic_constraint_matrices_; + }; // class mps_data_model_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/parser.hpp b/cpp/libmps_parser/include/mps_parser/parser.hpp index e8e8c342bd..6578ffb4d5 100644 --- a/cpp/libmps_parser/include/mps_parser/parser.hpp +++ b/cpp/libmps_parser/include/mps_parser/parser.hpp @@ -23,6 +23,8 @@ namespace cuopt::mps_parser { * QPS files (for quadratic programming). QPS files are MPS files with additional * sections: * - QUADOBJ: Defines quadratic terms in the objective function + * - QMATRIX: Full symmetric quadratic objective matrix (alternative to QUADOBJ) + * - QCMATRIX: Symmetric quadratic terms for a named constraint row (QCQP) * * Note: Compressed MPS files .mps.gz, .mps.bz2 can only be read if the compression * libraries zlib or libbzip2 are installed, respectively. diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 7d0d44a038..34e7d9b3d9 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -9,6 +9,7 @@ #include #include +#include namespace cuopt::mps_parser { @@ -219,6 +220,51 @@ void mps_data_model_t::set_quadratic_objective_matrix(const f_t* Q_val std::copy(Q_offsets, Q_offsets + size_offsets, Q_objective_offsets_.data()); } +template +void mps_data_model_t::append_quadratic_constraint_matrix(i_t constraint_row_index, + const f_t* Qc_values, + i_t size_values, + const i_t* Qc_indices, + i_t size_indices, + const i_t* Qc_offsets, + i_t size_offsets) +{ + if (size_values != 0) { + mps_parser_expects( + Qc_values != nullptr, error_type_t::ValidationError, "Qc_values cannot be null"); + } + if (size_indices != 0) { + mps_parser_expects( + Qc_indices != nullptr, error_type_t::ValidationError, "Qc_indices cannot be null"); + } + mps_parser_expects( + Qc_offsets != nullptr, error_type_t::ValidationError, "Qc_offsets cannot be null"); + mps_parser_expects( + size_offsets > 0, error_type_t::ValidationError, "size_offsets cannot be empty"); + + quadratic_constraint_matrix_t qcm; + qcm.constraint_row_index = constraint_row_index; + qcm.values.resize(size_values); + if (size_values > 0) { + std::copy(Qc_values, Qc_values + size_values, qcm.values.data()); + } + qcm.indices.resize(size_indices); + if (size_indices > 0) { + std::copy(Qc_indices, Qc_indices + size_indices, qcm.indices.data()); + } + qcm.offsets.resize(size_offsets); + std::copy(Qc_offsets, Qc_offsets + size_offsets, qcm.offsets.data()); + + quadratic_constraint_matrices_.push_back(std::move(qcm)); +} + +template +auto mps_data_model_t::get_quadratic_constraint_matrices() const + -> const std::vector& +{ + return quadratic_constraint_matrices_; +} + template const std::vector& mps_data_model_t::get_constraint_matrix_values() const { @@ -460,6 +506,12 @@ bool mps_data_model_t::has_quadratic_objective() const noexcept return !Q_objective_values_.empty(); } +template +bool mps_data_model_t::has_quadratic_constraints() const noexcept +{ + return !quadratic_constraint_matrices_.empty(); +} + // NOTE: Explicitly instantiate all types here in order to avoid linker error template class mps_data_model_t; diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 586544331f..9d3c2b45c4 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -432,6 +431,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.get_constraint_upper_bounds().size()); } + const i_t num_vars_for_quad = static_cast(var_names.size()); + problem.set_problem_name(problem_name); problem.set_objective_name(objective_name); problem.set_variable_names(std::move(var_names)); @@ -439,13 +440,17 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_row_names(std::move(row_names)); problem.set_maximize(maximize); - // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of - // O(nnz*log(nnz))) For QUADOBJ: handles upper triangular input by expanding to full symmetric - // matrix + // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of O(nnz*log(nnz))) + // For QUADOBJ: handles upper triangular input by expanding to full symmetric matrix. + // + // @p value_scale: + // QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); + // QCMATRIX uses 1.0 (symmetric Q defines xᵀQx directly in the constraint). auto build_csr_via_transpose = [](const std::vector>& entries, i_t num_rows, i_t num_cols, - bool is_quadobj = false) { + bool symmetrize_upper_triangular, + f_t value_scale) { struct CSRResult { std::vector values; std::vector indices; @@ -467,7 +472,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) // For QUADOBJ (upper triangular), add both (row,col) and (col,row) if off-diagonal csc_data[col].emplace_back(row, val); - if (is_quadobj && row != col) { csc_data[row].emplace_back(col, val); } + if (symmetrize_upper_triangular && row != col) { csc_data[row].emplace_back(col, val); } } // Second transpose: convert CSC to CSR (entries sorted by row, columns within rows sorted) @@ -485,9 +490,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) for (i_t row = 0; row < num_rows; ++row) { for (const auto& [col, val] : csr_data[row]) { - // While the mps format expects to optimize for 0.5 xT Q x, cuopt optimizes for xT Q x - // so we have to multiply the value by 0.5 to get the correct value. - result.values.push_back(val * 0.5); + result.values.push_back(val * value_scale); result.indices.push_back(col); } result.offsets.push_back(result.values.size()); @@ -500,8 +503,9 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) if (!quadobj_entries.empty()) { // Convert quadratic objective entries to CSR format using double transpose // QUADOBJ stores upper triangular elements, so we expand to full symmetric matrix - i_t num_vars = static_cast(var_names.size()); - auto csr_result = build_csr_via_transpose(quadobj_entries, num_vars, num_vars, true); + constexpr f_t k_mps_quad_half_scale = f_t(0.5); // MPS ½ xᵀQx vs internal xᵀQx + auto csr_result = + build_csr_via_transpose(quadobj_entries, num_vars_for_quad, num_vars_for_quad, true, k_mps_quad_half_scale); // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) problem.set_quadratic_objective_matrix(csr_result.values.data(), @@ -513,8 +517,9 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) } else if (!qmatrix_entries.empty()) { // Convert quadratic objective entries to CSR format using double transpose // QMATRIX stores full symmetric matrix - i_t num_vars = static_cast(var_names.size()); - auto csr_result = build_csr_via_transpose(qmatrix_entries, num_vars, num_vars, false); + constexpr f_t k_mps_quad_half_scale = f_t(0.5); + auto csr_result = + build_csr_via_transpose(qmatrix_entries, num_vars_for_quad, num_vars_for_quad, false, k_mps_quad_half_scale); // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) problem.set_quadratic_objective_matrix(csr_result.values.data(), @@ -524,6 +529,20 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) csr_result.offsets.data(), csr_result.offsets.size()); } + + // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs) + constexpr f_t k_qcmatrix_value_scale = f_t(1); + for (const auto& block : qcmatrix_blocks_) { + auto csr_result = build_csr_via_transpose( + block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); + problem.append_quadratic_constraint_matrix(block.constraint_row_id, + csr_result.values.data(), + csr_result.values.size(), + csr_result.indices.data(), + csr_result.indices.size(), + csr_result.offsets.data(), + csr_result.offsets.size()); + } } template @@ -599,6 +618,11 @@ void mps_parser_t::parse_string(char* buf) // these lines mark the start of a particular "section" if (line[0] != ' ') { skip_line = false; + // Leaving QCMATRIX: any non-QCMATRIX section header ends the current block + if (inside_qcmatrix_ && line.find("QCMATRIX", 0, 8) != 0) { + flush_qcmatrix_block(); + inside_qcmatrix_ = false; + } if (line.find("NAME", 0, 4) == 0) { encountered_sections.insert("NAME"); auto name_start = line.find_first_not_of(" \t", 4); @@ -709,6 +733,7 @@ void mps_parser_t::parse_string(char* buf) inside_objname_ = false; inside_objsense_ = false; inside_qmatrix_ = false; + inside_qcmatrix_ = false; inside_quadobj_ = true; } else if (line.find("QMATRIX", 0, 7) == 0) { encountered_sections.insert("QMATRIX"); @@ -721,6 +746,21 @@ void mps_parser_t::parse_string(char* buf) inside_objsense_ = false; inside_quadobj_ = false; inside_qmatrix_ = true; + inside_qcmatrix_ = false; + } else if (line.find("QCMATRIX", 0, 8) == 0) { + encountered_sections.insert("QCMATRIX"); + flush_qcmatrix_block(); + inside_rows_ = false; + inside_columns_ = false; + inside_rhs_ = false; + inside_bounds_ = false; + inside_ranges_ = false; + inside_objname_ = false; + inside_objsense_ = false; + inside_quadobj_ = false; + inside_qmatrix_ = false; + inside_qcmatrix_ = true; + parse_qcmatrix_header(line); } else if (line.find("ENDATA", 0, 6) == 0) { encountered_sections.insert("ENDATA"); break; @@ -737,6 +777,7 @@ void mps_parser_t::parse_string(char* buf) inside_objname_ = false; inside_quadobj_ = false; inside_qmatrix_ = false; + inside_qcmatrix_ = false; } else { mps_parser_expects(false, error_type_t::ValidationError, @@ -763,6 +804,8 @@ void mps_parser_t::parse_string(char* buf) parse_quad(line, true); } else if (inside_qmatrix_) { parse_quad(line, false); + } else if (inside_qcmatrix_) { + parse_qcmatrix_data(line); } else { mps_parser_expects(false, error_type_t::ValidationError, @@ -1282,6 +1325,109 @@ void mps_parser_t::parse_objname(std::string_view line) } } +template +void mps_parser_t::flush_qcmatrix_block() +{ + if (qcmatrix_active_row_id_ < 0) { return; } + if (qcmatrix_current_entries_.empty()) { + qcmatrix_active_row_id_ = -1; + return; + } + for (const auto& b : qcmatrix_blocks_) { + mps_parser_expects(b.constraint_row_id != qcmatrix_active_row_id_, + error_type_t::ValidationError, + "Duplicate QCMATRIX block for the same constraint row (index %d)", + static_cast(qcmatrix_active_row_id_)); + } + qcmatrix_raw_block_t block; + block.constraint_row_id = qcmatrix_active_row_id_; + block.entries = std::move(qcmatrix_current_entries_); + qcmatrix_blocks_.push_back(std::move(block)); + qcmatrix_active_row_id_ = -1; +} + +template +void mps_parser_t::parse_qcmatrix_header(std::string_view line) +{ + std::string row_name; + if (fixed_mps_format) { + mps_parser_expects(line.size() >= 19, + error_type_t::ValidationError, + "QCMATRIX header line too short! line=%s", + std::string(line).c_str()); + //fixed MPS: constraint name starts in column 12 (1-based) → 0-based index 11, 8 chars + row_name = std::string(trim(line.substr(11, 8))); + } else { + std::stringstream ss{std::string(line)}; + std::string kw; + ss >> kw; + mps_parser_expects(kw == "QCMATRIX", + error_type_t::ValidationError, + "Expected QCMATRIX keyword! line=%s", + std::string(line).c_str()); + ss >> row_name; + mps_parser_expects(!row_name.empty(), + error_type_t::ValidationError, + "QCMATRIX missing constraint row name! line=%s", + std::string(line).c_str()); + } + + auto row_it = row_names_map.find(row_name); + mps_parser_expects(row_it != row_names_map.end(), + error_type_t::ValidationError, + "Unknown constraint row name '%s' in QCMATRIX! line=%s", + row_name.c_str(), + std::string(line).c_str()); + + qcmatrix_active_row_id_ = row_it->second; +} + +template +void mps_parser_t::parse_qcmatrix_data(std::string_view line) +{ + mps_parser_expects(qcmatrix_active_row_id_ >= 0, + error_type_t::ValidationError, + "QCMATRIX data line before a valid QCMATRIX header! line=%s", + std::string(line).c_str()); + + std::string var1_name, var2_name; + f_t value; + + if (fixed_mps_format) { + mps_parser_expects(line.size() >= 25, + error_type_t::ValidationError, + "QCMATRIX data line should have at least 3 entities! line=%s", + std::string(line).c_str()); + + var1_name = std::string(trim(line.substr(4, 8))); + var2_name = std::string(trim(line.substr(14, 8))); + if (var1_name[0] == '$' || var2_name[0] == '$') return; + + i_t pos = 24; + value = get_numerical_bound(line, pos); + } else { + std::stringstream ss{std::string(line)}; + ss >> var1_name >> var2_name >> value; + if (var1_name[0] == '$' || var2_name[0] == '$') return; + } + + auto var1_it = var_names_map.find(var1_name); + auto var2_it = var_names_map.find(var2_name); + + mps_parser_expects(var1_it != var_names_map.end(), + error_type_t::ValidationError, + "Variable '%s' not found in QCMATRIX! line=%s", + var1_name.c_str(), + std::string(line).c_str()); + mps_parser_expects(var2_it != var_names_map.end(), + error_type_t::ValidationError, + "Variable '%s' not found in QCMATRIX! line=%s", + var2_name.c_str(), + std::string(line).c_str()); + + qcmatrix_current_entries_.emplace_back(var1_it->second, var2_it->second, value); +} + template void mps_parser_t::parse_quad(std::string_view line, bool is_quadobj) { diff --git a/cpp/libmps_parser/src/mps_parser.hpp b/cpp/libmps_parser/src/mps_parser.hpp index facad14c66..d73cfdd8b3 100644 --- a/cpp/libmps_parser/src/mps_parser.hpp +++ b/cpp/libmps_parser/src/mps_parser.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -130,6 +131,18 @@ class mps_parser_t { // QPS-specific parsing states bool inside_quadobj_{false}; bool inside_qmatrix_{false}; + bool inside_qcmatrix_{false}; + + /** (free-format) QCMATRIX: finalized blocks (row id + triples) */ + struct qcmatrix_raw_block_t { + i_t constraint_row_id{}; + std::vector> entries{}; + }; + std::vector qcmatrix_blocks_{}; + /** Triples for the QCMATRIX block currently being read (-1 row id means none) */ + i_t qcmatrix_active_row_id_{-1}; + std::vector> qcmatrix_current_entries_{}; + std::unordered_set encountered_sections{}; std::unordered_map row_names_map{}; std::unordered_map var_names_map{}; @@ -170,6 +183,11 @@ class mps_parser_t { // QPS-specific parsing methods void parse_quad(std::string_view line, bool is_quadobj); + // QCMATRIX-specific parsing methods + void flush_qcmatrix_block(); + void parse_qcmatrix_header(std::string_view line); + void parse_qcmatrix_data(std::string_view line); + }; // class mps_parser_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index f915fb2df5..e041b42419 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -855,6 +855,51 @@ TEST(qps_parser, quadratic_objective_basic) EXPECT_EQ(1.0, model.get_quadratic_objective_values()[1]); } +// ================================================================================================ +// QCMATRIX Support Tests +// ================================================================================================ + +TEST(qps_parser, qcmatrix_append_api) +{ + using model_t = mps_data_model_t; + model_t model; + + // Validate default-constructed struct shape. + model_t::quadratic_constraint_matrix_t default_qcm; + EXPECT_EQ(0, default_qcm.constraint_row_index); + EXPECT_TRUE(default_qcm.values.empty()); + EXPECT_TRUE(default_qcm.indices.empty()); + EXPECT_TRUE(default_qcm.offsets.empty()); + + // QC0: [[10, 2], [2, 2]] + const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; + const std::vector qc0_indices = {0, 1, 0, 1}; + const std::vector qc0_offsets = {0, 2, 4}; + model.append_quadratic_constraint_matrix( + 0, qc0_values.data(), qc0_values.size(), qc0_indices.data(), qc0_indices.size(), qc0_offsets.data(), qc0_offsets.size()); + + // QC1: [[4, 1], [1, 6]] + const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; + const std::vector qc1_indices = {0, 1, 0, 1}; + const std::vector qc1_offsets = {0, 2, 4}; + model.append_quadratic_constraint_matrix( + 1, qc1_values.data(), qc1_values.size(), qc1_indices.data(), qc1_indices.size(), qc1_offsets.data(), qc1_offsets.size()); + + ASSERT_TRUE(model.has_quadratic_constraints()); + const auto& qcs = model.get_quadratic_constraint_matrices(); + ASSERT_EQ(2u, qcs.size()); + + EXPECT_EQ(0, qcs[0].constraint_row_index); + EXPECT_EQ(qc0_values, qcs[0].values); + EXPECT_EQ(qc0_indices, qcs[0].indices); + EXPECT_EQ(qc0_offsets, qcs[0].offsets); + + EXPECT_EQ(1, qcs[1].constraint_row_index); + EXPECT_EQ(qc1_values, qcs[1].values); + EXPECT_EQ(qc1_indices, qcs[1].indices); + EXPECT_EQ(qc1_offsets, qcs[1].offsets); +} + // Test actual QPS files from the dataset TEST(qps_parser, test_qps_files) { From 43f09b4c4cf35e695cb44729714b7c9592a2cc81 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 15 Apr 2026 07:03:00 -0700 Subject: [PATCH 07/68] fix: address review suggestions (chatbot) --- cpp/libmps_parser/src/mps_data_model.cpp | 3 ++ cpp/libmps_parser/src/mps_parser.cpp | 40 ++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 34e7d9b3d9..718c8423d3 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -229,6 +229,9 @@ void mps_data_model_t::append_quadratic_constraint_matrix(i_t constrai const i_t* Qc_offsets, i_t size_offsets) { + mps_parser_expects( + constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); + if (size_values != 0) { mps_parser_expects( Qc_values != nullptr, error_type_t::ValidationError, "Qc_values cannot be null"); diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 9d3c2b45c4..f2ff0089fc 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -1406,9 +1406,23 @@ void mps_parser_t::parse_qcmatrix_data(std::string_view line) i_t pos = 24; value = get_numerical_bound(line, pos); } else { - std::stringstream ss{std::string(line)}; - ss >> var1_name >> var2_name >> value; - if (var1_name[0] == '$' || var2_name[0] == '$') return; + i_t pos = 0; + i_t end = 0; + const std::string_view var1_sv = get_next_string(line, pos, end); + mps_parser_expects(!var1_sv.empty(), + error_type_t::ValidationError, + "QCMATRIX data line missing first variable name! line=%s", + std::string(line).c_str()); + if (var1_sv[0] == '$') return; + const std::string_view var2_sv = get_next_string(line, pos, end); + mps_parser_expects(!var2_sv.empty(), + error_type_t::ValidationError, + "QCMATRIX data line missing second variable name! line=%s", + std::string(line).c_str()); + if (var2_sv[0] == '$') return; + value = get_numerical_bound(line, end); + var1_name = std::string(var1_sv); + var2_name = std::string(var2_sv); } auto var1_it = var_names_map.find(var1_name); @@ -1450,9 +1464,23 @@ void mps_parser_t::parse_quad(std::string_view line, bool is_quadobj) i_t pos = 24; value = get_numerical_bound(line, pos); } else { - std::stringstream ss{std::string(line)}; - ss >> var1_name >> var2_name >> value; - if (var1_name[0] == '$' || var2_name[0] == '$') return; + i_t pos = 0; + i_t end = 0; + const std::string_view var1_sv = get_next_string(line, pos, end); + mps_parser_expects(!var1_sv.empty(), + error_type_t::ValidationError, + "QUADOBJ/QMATRIX data line missing first variable name! line=%s", + std::string(line).c_str()); + if (var1_sv[0] == '$') return; + const std::string_view var2_sv = get_next_string(line, pos, end); + mps_parser_expects(!var2_sv.empty(), + error_type_t::ValidationError, + "QUADOBJ/QMATRIX data line missing second variable name! line=%s", + std::string(line).c_str()); + if (var2_sv[0] == '$') return; + value = get_numerical_bound(line, end); + var1_name = std::string(var1_sv); + var2_name = std::string(var2_sv); } // Find variable indices From ac9dd8a8e891d1bd4c1b446028a3a7995c5d6b23 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 17 Apr 2026 06:49:34 -0700 Subject: [PATCH 08/68] complete parsder for linear cost, ROW and RHS for QCQP --- .../include/mps_parser/mps_data_model.hpp | 61 ++++--- cpp/libmps_parser/src/mps_data_model.cpp | 89 ++++++--- cpp/libmps_parser/src/mps_parser.cpp | 63 +++++-- cpp/libmps_parser/tests/mps_parser_test.cpp | 169 ++++++++++++++++-- 4 files changed, 296 insertions(+), 86 deletions(-) diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 6022639f46..50f95e2496 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -263,38 +263,43 @@ class mps_data_model_t { i_t size_offsets); /** - * @brief CSR of Q for one quadratic constraint (MPS QCMATRIX). + * @brief One quadratic constraint as parsed from MPS sections (ROWS, COLUMNS, RHS, QCMATRIX). * - * @c constraint_row_index is the row index in the linear constraint matrix A (0-based), - * matching the order of non-objective rows in the ROWS section. + * This bundles all pieces of a quadratic row: + * - row identity and type (from ROWS), + * - sparse linear coefficients (from COLUMNS), + * - RHS value (from RHS), + * - quadratic matrix Q in CSR (from QCMATRIX). */ - struct quadratic_constraint_matrix_t { + struct quadratic_constraint_t { i_t constraint_row_index{}; - std::vector values; - std::vector indices; - std::vector offsets; + std::string constraint_row_name{}; + char constraint_row_type{}; + std::vector linear_values{}; + std::vector linear_indices{}; + f_t rhs_value{f_t(0)}; + std::vector quadratic_values{}; + std::vector quadratic_indices{}; + std::vector quadratic_offsets{}; }; - /** - * @brief Append one quadratic constraint matrix (QCMATRIX) in CSR format. - * - * @param constraint_row_index Row index in A (0-based), matching non-objective ROWS order. - * @param[in] Qc_values Values of the CSR representation; copied into the model. - * @param size_values Size of the Qc_values array. - * @param[in] Qc_indices Indices of the CSR representation; copied into the model. - * @param size_indices Size of the Qc_indices array. - * @param[in] Qc_offsets Offsets of the CSR representation; copied into the model. - * @param size_offsets Size of the Qc_offsets array. - */ - void append_quadratic_constraint_matrix(i_t constraint_row_index, - const f_t* Qc_values, - i_t size_values, - const i_t* Qc_indices, - i_t size_indices, - const i_t* Qc_offsets, - i_t size_offsets); + /** @brief Append one complete quadratic constraint (row + linear + rhs + quadratic Q). */ + void append_quadratic_constraint(i_t constraint_row_index, + const std::string& constraint_row_name, + char constraint_row_type, + const f_t* linear_values, + i_t linear_nnz, + const i_t* linear_indices, + i_t linear_indices_nnz, + f_t rhs_value, + const f_t* quadratic_values, + i_t quadratic_size_values, + const i_t* quadratic_indices, + i_t quadratic_size_indices, + const i_t* quadratic_offsets, + i_t quadratic_size_offsets); - const std::vector& get_quadratic_constraint_matrices() const; + const std::vector& get_quadratic_constraints() const; i_t get_n_variables() const; i_t get_n_constraints() const; @@ -397,8 +402,8 @@ class mps_data_model_t { std::vector Q_objective_indices_; std::vector Q_objective_offsets_; - /** One CSR matrix per QCMATRIX block, in order of appearance in the file */ - std::vector quadratic_constraint_matrices_; + /** One full quadratic constraint per QCMATRIX block, in order of appearance in the file */ + std::vector quadratic_constraints_; }; // class mps_data_model_t diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 718c8423d3..5feb405631 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -221,51 +221,82 @@ void mps_data_model_t::set_quadratic_objective_matrix(const f_t* Q_val } template -void mps_data_model_t::append_quadratic_constraint_matrix(i_t constraint_row_index, - const f_t* Qc_values, - i_t size_values, - const i_t* Qc_indices, - i_t size_indices, - const i_t* Qc_offsets, - i_t size_offsets) +void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_index, + const std::string& constraint_row_name, + char constraint_row_type, + const f_t* linear_values, + i_t linear_nnz, + const i_t* linear_indices, + i_t linear_indices_nnz, + f_t rhs_value, + const f_t* quadratic_values, + i_t quadratic_size_values, + const i_t* quadratic_indices, + i_t quadratic_size_indices, + const i_t* quadratic_offsets, + i_t quadratic_size_offsets) { mps_parser_expects( constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); - - if (size_values != 0) { + + mps_parser_expects(linear_nnz == linear_indices_nnz, + error_type_t::ValidationError, + "linear_values and linear_indices must have the same nnz count"); + if (linear_nnz != 0) { mps_parser_expects( - Qc_values != nullptr, error_type_t::ValidationError, "Qc_values cannot be null"); + linear_values != nullptr && linear_indices != nullptr, + error_type_t::ValidationError, + "linear_values and linear_indices cannot be null when linear_nnz > 0"); } - if (size_indices != 0) { + + if (quadratic_size_values != 0) { mps_parser_expects( - Qc_indices != nullptr, error_type_t::ValidationError, "Qc_indices cannot be null"); + quadratic_values != nullptr, error_type_t::ValidationError, "quadratic_values cannot be null"); } mps_parser_expects( - Qc_offsets != nullptr, error_type_t::ValidationError, "Qc_offsets cannot be null"); + quadratic_offsets != nullptr, error_type_t::ValidationError, "quadratic_offsets cannot be null"); + if (quadratic_size_indices != 0) { + mps_parser_expects( + quadratic_indices != nullptr, error_type_t::ValidationError, "quadratic_indices cannot be null"); + } mps_parser_expects( - size_offsets > 0, error_type_t::ValidationError, "size_offsets cannot be empty"); + quadratic_size_offsets > 0, error_type_t::ValidationError, "quadratic_size_offsets cannot be empty"); + + quadratic_constraint_t qc; + qc.constraint_row_index = constraint_row_index; + qc.constraint_row_name = constraint_row_name; + qc.constraint_row_type = constraint_row_type; + qc.rhs_value = rhs_value; + + qc.linear_values.resize(linear_nnz); + qc.linear_indices.resize(linear_nnz); + if (linear_nnz > 0) { + std::copy(linear_values, linear_values + linear_nnz, qc.linear_values.data()); + std::copy(linear_indices, linear_indices + linear_nnz, qc.linear_indices.data()); + } - quadratic_constraint_matrix_t qcm; - qcm.constraint_row_index = constraint_row_index; - qcm.values.resize(size_values); - if (size_values > 0) { - std::copy(Qc_values, Qc_values + size_values, qcm.values.data()); + qc.quadratic_values.resize(quadratic_size_values); + if (quadratic_size_values > 0) { + std::copy( + quadratic_values, quadratic_values + quadratic_size_values, qc.quadratic_values.data()); } - qcm.indices.resize(size_indices); - if (size_indices > 0) { - std::copy(Qc_indices, Qc_indices + size_indices, qcm.indices.data()); + qc.quadratic_indices.resize(quadratic_size_indices); + if (quadratic_size_indices > 0) { + std::copy( + quadratic_indices, quadratic_indices + quadratic_size_indices, qc.quadratic_indices.data()); } - qcm.offsets.resize(size_offsets); - std::copy(Qc_offsets, Qc_offsets + size_offsets, qcm.offsets.data()); + qc.quadratic_offsets.resize(quadratic_size_offsets); + std::copy( + quadratic_offsets, quadratic_offsets + quadratic_size_offsets, qc.quadratic_offsets.data()); - quadratic_constraint_matrices_.push_back(std::move(qcm)); + quadratic_constraints_.push_back(std::move(qc)); } template -auto mps_data_model_t::get_quadratic_constraint_matrices() const - -> const std::vector& +auto mps_data_model_t::get_quadratic_constraints() const + -> const std::vector& { - return quadratic_constraint_matrices_; + return quadratic_constraints_; } template @@ -512,7 +543,7 @@ bool mps_data_model_t::has_quadratic_objective() const noexcept template bool mps_data_model_t::has_quadratic_constraints() const noexcept { - return !quadratic_constraint_matrices_.empty(); + return !quadratic_constraints_.empty(); } // NOTE: Explicitly instantiate all types here in order to avoid linker error diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index f2ff0089fc..984fd213a4 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -271,6 +271,11 @@ ObjSenseType convert_to_obj_sense(const std::string& str) template void mps_parser_t::fill_problem(mps_data_model_t& problem) { + std::unordered_set quadratic_row_ids{}; + for (const auto& block : qcmatrix_blocks_) { + quadratic_row_ids.insert(block.constraint_row_id); + } + { std::vector h_offsets{}, h_indices{}; std::vector h_values{}; @@ -278,13 +283,17 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) h_offsets.push_back(0); for (i_t i = 0; i < (i_t)A_indices.size(); ++i) { i_t off = h_offsets.size() > 0 ? h_offsets[h_offsets.size() - 1] : 0; - for (const auto& idx_itr : A_indices[i]) { - h_indices.push_back(idx_itr); - } - for (const auto& val_itr : A_values[i]) { - h_values.push_back(val_itr); + // Keep quadratic-row linear coefficients out of global A; they are stored with each + // quadratic constraint object instead. + if (!quadratic_row_ids.count(i)) { + for (const auto& idx_itr : A_indices[i]) { + h_indices.push_back(idx_itr); + } + for (const auto& val_itr : A_values[i]) { + h_values.push_back(val_itr); + } + off += A_indices[i].size(); } - off += A_indices[i].size(); h_offsets.push_back(off); } @@ -317,6 +326,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) "nonzero vector. Nonzero has size %zu but the last offset is %d.", h_values.size(), h_offsets[h_offsets.size() - 1]); + } // Set b & c @@ -438,6 +448,13 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_variable_names(std::move(var_names)); problem.set_variable_types(std::move(var_types)); problem.set_row_names(std::move(row_names)); + { + std::vector row_types_host(row_types.size()); + for (size_t i = 0; i < row_types.size(); ++i) { + row_types_host[i] = static_cast(row_types[i]); + } + problem.set_row_types(row_types_host.data(), static_cast(row_types_host.size())); + } problem.set_maximize(maximize); // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of O(nnz*log(nnz))) @@ -530,18 +547,36 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) csr_result.offsets.size()); } - // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs) + // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs). + // Bundle row metadata, row-linear coefficients (from COLUMNS), rhs, and quadratic part together. constexpr f_t k_qcmatrix_value_scale = f_t(1); for (const auto& block : qcmatrix_blocks_) { auto csr_result = build_csr_via_transpose( block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); - problem.append_quadratic_constraint_matrix(block.constraint_row_id, - csr_result.values.data(), - csr_result.values.size(), - csr_result.indices.data(), - csr_result.indices.size(), - csr_result.offsets.data(), - csr_result.offsets.size()); + const i_t row_id = block.constraint_row_id; + mps_parser_expects(row_id >= 0 && row_id < problem.get_n_constraints(), + error_type_t::ValidationError, + "QCMATRIX row index %d is out of range for constraints", + static_cast(row_id)); + const i_t linear_nnz = static_cast(A_indices[row_id].size()); + const f_t* linear_val = linear_nnz > 0 ? A_values[row_id].data() : nullptr; + const i_t* linear_idx = linear_nnz > 0 ? A_indices[row_id].data() : nullptr; + + problem.append_quadratic_constraint( + row_id, + problem.get_row_names()[row_id], + problem.get_row_types()[row_id], + linear_val, + linear_nnz, + linear_idx, + linear_nnz, + problem.get_constraint_bounds()[row_id], + csr_result.values.data(), + static_cast(csr_result.values.size()), + csr_result.indices.data(), + static_cast(csr_result.indices.size()), + csr_result.offsets.data(), + static_cast(csr_result.offsets.size())); } } diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index e041b42419..dd1376621b 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -13,8 +13,10 @@ #include +#include #include #include +#include #include #include #include @@ -865,39 +867,176 @@ TEST(qps_parser, qcmatrix_append_api) model_t model; // Validate default-constructed struct shape. - model_t::quadratic_constraint_matrix_t default_qcm; + model_t::quadratic_constraint_t default_qcm; EXPECT_EQ(0, default_qcm.constraint_row_index); - EXPECT_TRUE(default_qcm.values.empty()); - EXPECT_TRUE(default_qcm.indices.empty()); - EXPECT_TRUE(default_qcm.offsets.empty()); + EXPECT_TRUE(default_qcm.quadratic_values.empty()); + EXPECT_TRUE(default_qcm.quadratic_indices.empty()); + EXPECT_TRUE(default_qcm.quadratic_offsets.empty()); + EXPECT_TRUE(default_qcm.linear_values.empty()); + EXPECT_TRUE(default_qcm.linear_indices.empty()); + EXPECT_EQ(0.0, default_qcm.rhs_value); // QC0: [[10, 2], [2, 2]] const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; const std::vector qc0_indices = {0, 1, 0, 1}; const std::vector qc0_offsets = {0, 2, 4}; - model.append_quadratic_constraint_matrix( - 0, qc0_values.data(), qc0_values.size(), qc0_indices.data(), qc0_indices.size(), qc0_offsets.data(), qc0_offsets.size()); + const std::vector qc0_linear_values = {1.0, 1.0}; + const std::vector qc0_linear_indices = {0, 1}; + model.append_quadratic_constraint(0, + "QC0", + 'L', + qc0_linear_values.data(), + qc0_linear_values.size(), + qc0_linear_indices.data(), + qc0_linear_indices.size(), + 5.0, + qc0_values.data(), + qc0_values.size(), + qc0_indices.data(), + qc0_indices.size(), + qc0_offsets.data(), + qc0_offsets.size()); // QC1: [[4, 1], [1, 6]] const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; const std::vector qc1_indices = {0, 1, 0, 1}; const std::vector qc1_offsets = {0, 2, 4}; - model.append_quadratic_constraint_matrix( - 1, qc1_values.data(), qc1_values.size(), qc1_indices.data(), qc1_indices.size(), qc1_offsets.data(), qc1_offsets.size()); + const std::vector qc1_linear_values = {3.0, 1.0}; + const std::vector qc1_linear_indices = {0, 1}; + model.append_quadratic_constraint(1, + "QC1", + 'L', + qc1_linear_values.data(), + qc1_linear_values.size(), + qc1_linear_indices.data(), + qc1_linear_indices.size(), + 10.0, + qc1_values.data(), + qc1_values.size(), + qc1_indices.data(), + qc1_indices.size(), + qc1_offsets.data(), + qc1_offsets.size()); ASSERT_TRUE(model.has_quadratic_constraints()); - const auto& qcs = model.get_quadratic_constraint_matrices(); + const auto& qcs = model.get_quadratic_constraints(); ASSERT_EQ(2u, qcs.size()); EXPECT_EQ(0, qcs[0].constraint_row_index); - EXPECT_EQ(qc0_values, qcs[0].values); - EXPECT_EQ(qc0_indices, qcs[0].indices); - EXPECT_EQ(qc0_offsets, qcs[0].offsets); + EXPECT_EQ("QC0", qcs[0].constraint_row_name); + EXPECT_EQ('L', qcs[0].constraint_row_type); + EXPECT_EQ(qc0_linear_values, qcs[0].linear_values); + EXPECT_EQ(qc0_linear_indices, qcs[0].linear_indices); + EXPECT_EQ(5.0, qcs[0].rhs_value); + EXPECT_EQ(qc0_values, qcs[0].quadratic_values); + EXPECT_EQ(qc0_indices, qcs[0].quadratic_indices); + EXPECT_EQ(qc0_offsets, qcs[0].quadratic_offsets); EXPECT_EQ(1, qcs[1].constraint_row_index); - EXPECT_EQ(qc1_values, qcs[1].values); - EXPECT_EQ(qc1_indices, qcs[1].indices); - EXPECT_EQ(qc1_offsets, qcs[1].offsets); + EXPECT_EQ("QC1", qcs[1].constraint_row_name); + EXPECT_EQ('L', qcs[1].constraint_row_type); + EXPECT_EQ(qc1_linear_values, qcs[1].linear_values); + EXPECT_EQ(qc1_linear_indices, qcs[1].linear_indices); + EXPECT_EQ(10.0, qcs[1].rhs_value); + EXPECT_EQ(qc1_values, qcs[1].quadratic_values); + EXPECT_EQ(qc1_indices, qcs[1].quadratic_indices); + EXPECT_EQ(qc1_offsets, qcs[1].quadratic_offsets); +} + +// QCQP MPS: each quadratic constraint bundles row + linear + rhs + quadratic. +TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) +{ + if (!file_exists("qcqp/QC_Test_1.mps")) { + GTEST_SKIP() << "qcqp/QC_Test_1.mps not in dataset root"; + } + const auto model = parse_mps( + cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/QC_Test_1.mps", false); + + ASSERT_TRUE(model.has_quadratic_constraints()); + const auto& qcs = model.get_quadratic_constraints(); + ASSERT_EQ(2u, qcs.size()); + + ASSERT_EQ(3, model.get_n_constraints()); + ASSERT_EQ(3u, model.get_row_names().size()); + EXPECT_EQ("LIN0", model.get_row_names()[0]); + EXPECT_EQ("QC0", model.get_row_names()[1]); + EXPECT_EQ("QC1", model.get_row_names()[2]); + EXPECT_EQ('L', model.get_row_types()[0]); + + // LIN0: 2*x1 + x2 ≤ 15 (linear row only; not duplicated in quadratic_constraints) + EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), + model.get_constraint_lower_bounds()[0]); + EXPECT_DOUBLE_EQ(15.0, model.get_constraint_upper_bounds()[0]); + const auto& A_off = model.get_constraint_matrix_offsets(); + const auto& A_val = model.get_constraint_matrix_values(); + const auto& A_idx = model.get_constraint_matrix_indices(); + ASSERT_EQ(2, A_off[1] - A_off[0]); + EXPECT_EQ(2.0, A_val[A_off[0] + 0]); + EXPECT_EQ(1.0, A_val[A_off[0] + 1]); + EXPECT_EQ(0, A_idx[A_off[0] + 0]); + EXPECT_EQ(1, A_idx[A_off[0] + 1]); + + // QC0: x1 + x2 + xᵀQ₀x ≤ 5 (row index 1: OBJ 'N' rows are not counted in row_names) + EXPECT_EQ(1, qcs[0].constraint_row_index); + EXPECT_EQ("QC0", qcs[0].constraint_row_name); + EXPECT_EQ('L', qcs[0].constraint_row_type); + ASSERT_EQ(2u, qcs[0].linear_values.size()); + EXPECT_EQ(1.0, qcs[0].linear_values[0]); + EXPECT_EQ(1.0, qcs[0].linear_values[1]); + EXPECT_EQ(0, qcs[0].linear_indices[0]); + EXPECT_EQ(1, qcs[0].linear_indices[1]); + EXPECT_DOUBLE_EQ(5.0, qcs[0].rhs_value); + EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), + model.get_constraint_lower_bounds()[qcs[0].constraint_row_index]); + EXPECT_DOUBLE_EQ(5.0, model.get_constraint_upper_bounds()[qcs[0].constraint_row_index]); + EXPECT_FALSE(qcs[0].quadratic_values.empty()); + + // QC1: 3*x1 + x2 + xᵀQ₁x ≤ 10 + EXPECT_EQ(2, qcs[1].constraint_row_index); + EXPECT_EQ("QC1", qcs[1].constraint_row_name); + EXPECT_EQ('L', qcs[1].constraint_row_type); + ASSERT_EQ(2u, qcs[1].linear_values.size()); + EXPECT_EQ(3.0, qcs[1].linear_values[0]); + EXPECT_EQ(1.0, qcs[1].linear_values[1]); + EXPECT_DOUBLE_EQ(10.0, qcs[1].rhs_value); + EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), + model.get_constraint_lower_bounds()[qcs[1].constraint_row_index]); + EXPECT_DOUBLE_EQ(10.0, model.get_constraint_upper_bounds()[qcs[1].constraint_row_index]); +} + +TEST(qps_parser, qcqp_p0033_mps_sections) +{ + if (!file_exists("qcqp/p0033_qc1.mps")) { + GTEST_SKIP() << "qcqp/p0033_qc1.mps not in dataset root"; + } + const auto model = parse_mps( + cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps", false); + + EXPECT_EQ(16, model.get_n_constraints()); + EXPECT_EQ(33, model.get_n_variables()); + ASSERT_EQ(16u, model.get_row_types().size()); + ASSERT_EQ(16u, model.get_row_names().size()); + + const auto& rnames = model.get_row_names(); + auto qc1_it = std::find(rnames.begin(), rnames.end(), std::string("QC1")); + ASSERT_NE(qc1_it, rnames.end()); + const int qc1_row = static_cast(qc1_it - rnames.begin()); + + std::vector coeff; + std::vector vars; + const auto& qcs = model.get_quadratic_constraints(); + ASSERT_EQ(4u, qcs.size()); + EXPECT_EQ(qc1_row, qcs[0].constraint_row_index); + ASSERT_EQ(1u, qcs[0].linear_values.size()); + EXPECT_DOUBLE_EQ(1.0, qcs[0].linear_values[0]); + + const auto& vnames = model.get_variable_names(); + auto c159_it = std::find(vnames.begin(), vnames.end(), std::string("C159")); + ASSERT_NE(c159_it, vnames.end()); + EXPECT_EQ(static_cast(c159_it - vnames.begin()), qcs[0].linear_indices[0]); + + EXPECT_DOUBLE_EQ(1.0, qcs[0].rhs_value); + EXPECT_FALSE(qcs[0].quadratic_values.empty()); } // Test actual QPS files from the dataset From 0bd6499f4bae32fb9cbfa50021248dfc07d17f6b Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 20 Apr 2026 06:59:00 -0700 Subject: [PATCH 09/68] Add support for reading and writting quadratical constrains into MPS; note that we separate quadratical constraint from the constraint matrix A and RHS --- .../cpu_optimization_problem.hpp | 19 ++ .../optimization_problem.hpp | 21 ++ .../optimization_problem_interface.hpp | 48 ++++ .../optimization_problem_utils.hpp | 33 +++ .../include/mps_parser/data_model_view.hpp | 29 ++ .../include/mps_parser/mps_data_model.hpp | 30 ++- .../include/mps_parser/utilities/span.hpp | 2 + cpp/libmps_parser/src/data_model_view.cpp | 68 +++++ cpp/libmps_parser/src/mps_data_model.cpp | 50 +++- cpp/libmps_parser/src/mps_parser.cpp | 132 ++++++---- cpp/libmps_parser/src/mps_writer.cpp | 247 ++++++++++++++++-- cpp/libmps_parser/tests/mps_parser_test.cpp | 94 +++++-- cpp/src/pdlp/cpu_optimization_problem.cpp | 96 +++++++ cpp/src/pdlp/optimization_problem.cu | 92 ++++++- 14 files changed, 863 insertions(+), 98 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp b/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp index 009a8ce84e..2865fc877f 100644 --- a/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp @@ -41,6 +41,8 @@ class mip_solution_interface_t; template class cpu_optimization_problem_t : public optimization_problem_interface_t { public: + using typename optimization_problem_interface_t::mps_quadratic_constraint_t; + cpu_optimization_problem_t(); // Setters @@ -113,6 +115,17 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t& get_quadratic_objective_values() const override; bool has_quadratic_objective() const override; + void set_quadratic_constraints(std::vector constraints) override; + bool has_quadratic_constraints() const override; + const std::vector& get_quadratic_constraints() const override; + // Additional methods for MPS export if quadratic constraints are present + void set_linear_constraint_mps_indices(std::vector indices) override; + void set_mps_declaration_constraint_row_count(i_t count) override; + void set_mps_all_constraint_row_names(std::vector names) override; + i_t get_mps_declaration_constraint_row_count() const override; + const std::vector& get_linear_constraint_mps_indices() const override; + const std::vector& get_mps_all_constraint_row_names() const override; + // Host getters - these are the only supported getters for CPU implementation std::vector get_constraint_matrix_values_host() const override; std::vector get_constraint_matrix_indices_host() const override; @@ -185,6 +198,12 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t Q_indices_; std::vector Q_values_; + std::vector quadratic_constraints_{}; + + std::vector linear_constraint_mps_indices_{}; + i_t mps_declaration_constraint_row_count_{0}; + std::vector mps_all_constraint_row_names_{}; + std::vector variable_lower_bounds_; std::vector variable_upper_bounds_; std::vector constraint_lower_bounds_; diff --git a/cpp/include/cuopt/linear_programming/optimization_problem.hpp b/cpp/include/cuopt/linear_programming/optimization_problem.hpp index df78dd17c7..27f5ac1168 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem.hpp @@ -72,6 +72,9 @@ class optimization_problem_t : public optimization_problem_interface_t static_assert(std::is_floating_point::value, "'optimization_problem_t' accepts only floating point types for weights"); + // nvcc does not always find base typedefs in derived class scope; inject explicitly. + using typename optimization_problem_interface_t::mps_quadratic_constraint_t; + /** * @brief A device-side view of the `optimization_problem_t` structure with * the RAII stuffs stripped out, to make it easy to work inside kernels @@ -196,6 +199,8 @@ class optimization_problem_t : public optimization_problem_interface_t i_t size_offsets, bool validate_positive_semi_definite = false) override; + void set_quadratic_constraints(std::vector constraints) override; + /** @copydoc optimization_problem_interface_t::set_variable_lower_bounds */ void set_variable_lower_bounds(const f_t* variable_lower_bounds, i_t size) override; /** @copydoc optimization_problem_interface_t::set_variable_upper_bounds */ @@ -259,7 +264,16 @@ class optimization_problem_t : public optimization_problem_interface_t const std::vector& get_quadratic_objective_offsets() const override; const std::vector& get_quadratic_objective_indices() const override; const std::vector& get_quadratic_objective_values() const override; + const std::vector& get_quadratic_constraints() const override; bool has_quadratic_objective() const override; + bool has_quadratic_constraints() const override; + + void set_linear_constraint_mps_indices(std::vector indices) override; + void set_mps_declaration_constraint_row_count(i_t count) override; + void set_mps_all_constraint_row_names(std::vector names) override; + i_t get_mps_declaration_constraint_row_count() const override; + const std::vector& get_linear_constraint_mps_indices() const override; + const std::vector& get_mps_all_constraint_row_names() const override; // ============================================================================ // Host getters @@ -376,6 +390,13 @@ class optimization_problem_t : public optimization_problem_interface_t std::vector Q_indices_; std::vector Q_values_; + /** QCQP: quadratic constraints **/ + std::vector quadratic_constraints_{}; + + std::vector linear_constraint_mps_indices_{}; + i_t mps_declaration_constraint_row_count_{0}; + std::vector mps_all_constraint_row_names_{}; + rmm::device_uvector variable_lower_bounds_; rmm::device_uvector variable_upper_bounds_; rmm::device_uvector constraint_lower_bounds_; diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp index 767e62e746..c4ee81d875 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include @@ -56,8 +57,55 @@ class optimization_problem_interface_t { static_assert(std::is_floating_point::value, "'optimization_problem_interface_t' accepts only floating point types for weights"); + /** Quadratic constraints as parsed/stored for MPS QCQP (QCMATRIX rows). */ + using mps_quadratic_constraint_t = + typename mps_parser::mps_data_model_t::quadratic_constraint_t; + virtual ~optimization_problem_interface_t() = default; + /** + * @brief Store quadratic constraints for MPS round-trip (linear + Q parts per QC row). + * @note Default implementation ignores; GPU/CPU implementations persist for write_to_mps. + */ + virtual void set_quadratic_constraints(std::vector constraints) + { + (void)constraints; + } + + /** @brief Whether quadratic constraint metadata is present (for MPS export). */ + virtual bool has_quadratic_constraints() const { return false; } + + /** @brief Quadratic constraints for MPS export (empty if none). */ + virtual const std::vector& get_quadratic_constraints() const + { + static const std::vector k_empty{}; + return k_empty; + } + + /** + * @brief When QCMATRIX rows are omitted from the linear CSR, maps linear CSR row j to the MPS + * ROWS declaration index. Used for MPS export only. + */ + virtual void set_linear_constraint_mps_indices(std::vector indices) { (void)indices; } + + virtual void set_mps_declaration_constraint_row_count(i_t count) { (void)count; } + + virtual void set_mps_all_constraint_row_names(std::vector names) { (void)names; } + + virtual i_t get_mps_declaration_constraint_row_count() const { return 0; } + + virtual const std::vector& get_linear_constraint_mps_indices() const + { + static const std::vector k_empty{}; + return k_empty; + } + + virtual const std::vector& get_mps_all_constraint_row_names() const + { + static const std::vector k_empty{}; + return k_empty; + } + // ============================================================================ // Setters (accept both CPU and GPU pointers) // ============================================================================ diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp index 90e853f530..bd05b3c1b2 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp @@ -109,6 +109,24 @@ void populate_from_mps_data_model(optimization_problem_interface_t* pr q_offsets.data(), n_vars + 1); } + // Handle quadratic constraints if present + if (data_model.has_quadratic_constraints()) { + problem->set_quadratic_constraints( + std::vector::quadratic_constraint_t>( + data_model.get_quadratic_constraints())); + } + + if (data_model.get_mps_declaration_constraint_row_count() > 0) { + problem->set_linear_constraint_mps_indices( + std::vector(data_model.get_linear_constraint_mps_indices())); + problem->set_mps_declaration_constraint_row_count(data_model.get_mps_declaration_constraint_row_count()); + problem->set_mps_all_constraint_row_names( + std::vector(data_model.get_mps_all_constraint_row_names())); + } else { + problem->set_linear_constraint_mps_indices({}); + problem->set_mps_declaration_constraint_row_count(0); + problem->set_mps_all_constraint_row_names({}); + } } /** @@ -266,6 +284,21 @@ void populate_from_data_model_view(optimization_problem_interface_t* p if (data_model->get_row_names().size() != 0) { problem->set_row_names(data_model->get_row_names()); } + + if (data_model->get_mps_declaration_constraint_row_count() > 0) { + const auto lmi = data_model->get_linear_constraint_mps_indices(); + if (lmi.size() > 0) { + problem->set_linear_constraint_mps_indices(std::vector( + lmi.data(), lmi.data() + static_cast(lmi.size()))); + } + problem->set_mps_declaration_constraint_row_count(data_model->get_mps_declaration_constraint_row_count()); + problem->set_mps_all_constraint_row_names( + std::vector(data_model->get_mps_all_constraint_row_names())); + } else { + problem->set_linear_constraint_mps_indices({}); + problem->set_mps_declaration_constraint_row_count(0); + problem->set_mps_all_constraint_row_names({}); + } } } // namespace cuopt::linear_programming diff --git a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp index c2a8f84980..9e2c0e112e 100644 --- a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp +++ b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp @@ -7,6 +7,7 @@ #pragma once +#include #include #include @@ -415,6 +416,27 @@ class data_model_view_t { */ bool is_Q_symmetrized() const noexcept; + /** + * @brief Quadratic constraints (MPS QCMATRIX); owned copy for writers when not using spans. + */ + void set_quadratic_constraints( + std::vector::quadratic_constraint_t> constraints); + + bool has_quadratic_constraints() const noexcept; + + const std::vector::quadratic_constraint_t>& + get_quadratic_constraints() const noexcept; + + void set_linear_constraint_mps_indices(const i_t* indices, i_t size); + span get_linear_constraint_mps_indices() const noexcept; + + void set_mps_declaration_constraint_row_count(i_t count); + i_t get_mps_declaration_constraint_row_count() const noexcept; + + void set_mps_all_constraint_row_names(std::vector names); + + const std::vector& get_mps_all_constraint_row_names() const noexcept; + private: bool maximize_{false}; span A_; @@ -444,6 +466,13 @@ class data_model_view_t { span Q_objective_indices_; span Q_objective_offsets_; bool is_Q_symmetrized_{false}; + + std::vector::quadratic_constraint_t> quadratic_constraints_; + + span linear_mps_indices_{}; + std::vector linear_mps_indices_owned_{}; + i_t mps_declaration_constraint_row_count_{0}; + std::vector mps_all_constraint_row_names_{}; }; // class data_model_view_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 50f95e2496..2a578c36c5 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -272,6 +272,7 @@ class mps_data_model_t { * - quadratic matrix Q in CSR (from QCMATRIX). */ struct quadratic_constraint_t { + /** ROWS declaration index (among all constraint rows), not an index into the linear CSR. */ i_t constraint_row_index{}; std::string constraint_row_name{}; char constraint_row_type{}; @@ -347,6 +348,25 @@ class mps_data_model_t { bool has_quadratic_constraints() const noexcept; + /** + * @brief When quadratic constraints are present, CSR rows are linear-only; entry j is the MPS + * ROWS declaration index for linear CSR row j. + */ + void set_linear_constraint_mps_indices(std::vector indices); + + /** + * @brief ROWS names in declaration order (size == declaration row count). Used for MPS export + * when linear CSR excludes quadratic rows. + */ + void set_mps_all_constraint_row_names(std::vector names); + + /** @brief Total ROWS constraint count (linear + quadratic) when QC rows are separated; else 0. */ + void set_mps_declaration_constraint_row_count(i_t count); + + const std::vector& get_linear_constraint_mps_indices() const; + const std::vector& get_mps_all_constraint_row_names() const; + i_t get_mps_declaration_constraint_row_count() const; + /** whether to maximize or minimize the objective function */ bool maximize_; /** @@ -383,7 +403,10 @@ class mps_data_model_t { std::string problem_name_; /** names of each of the variables in the OP */ std::vector var_names_{}; - /** names of each of the rows (aka constraints or objective) in the OP */ + /** + * names of linear constraint rows only when QCMATRIX rows are separated; otherwise all constraint + * rows (same as MPS ROWS order excluding objective). + */ std::vector row_names_{}; /** number of variables */ i_t n_vars_{0}; @@ -405,6 +428,11 @@ class mps_data_model_t { /** One full quadratic constraint per QCMATRIX block, in order of appearance in the file */ std::vector quadratic_constraints_; + /** Maps linear CSR row j -> MPS ROWS declaration index; non-empty iff QC rows are split out. */ + std::vector linear_constraint_mps_indices_{}; + i_t mps_declaration_constraint_row_count_{0}; + std::vector mps_all_constraint_row_names_{}; + }; // class mps_data_model_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/utilities/span.hpp b/cpp/libmps_parser/include/mps_parser/utilities/span.hpp index 02679cd378..7ad4f25d4c 100644 --- a/cpp/libmps_parser/include/mps_parser/utilities/span.hpp +++ b/cpp/libmps_parser/include/mps_parser/utilities/span.hpp @@ -18,6 +18,8 @@ class span { span(T* ptr, std::size_t size) : ptr_(ptr), size_(size) {} std::size_t size() const noexcept { return size_; } const T* data() const noexcept { return ptr_; } + T& operator[](std::size_t i) noexcept { return ptr_[i]; } + T const& operator[](std::size_t i) const noexcept { return ptr_[i]; } private: T* ptr_{nullptr}; diff --git a/cpp/libmps_parser/src/data_model_view.cpp b/cpp/libmps_parser/src/data_model_view.cpp index 62b441aa60..be32eb3d3a 100644 --- a/cpp/libmps_parser/src/data_model_view.cpp +++ b/cpp/libmps_parser/src/data_model_view.cpp @@ -355,6 +355,74 @@ bool data_model_view_t::is_Q_symmetrized() const noexcept return is_Q_symmetrized_; } +template +void data_model_view_t::set_quadratic_constraints( + std::vector::quadratic_constraint_t> constraints) +{ + quadratic_constraints_ = std::move(constraints); +} + +template +bool data_model_view_t::has_quadratic_constraints() const noexcept +{ + return !quadratic_constraints_.empty(); +} + +template +const std::vector::quadratic_constraint_t>& +data_model_view_t::get_quadratic_constraints() const noexcept +{ + return quadratic_constraints_; +} + +template +void data_model_view_t::set_linear_constraint_mps_indices(const i_t* indices, i_t size) +{ + if (size != 0) { + mps_parser_expects( + indices != nullptr, error_type_t::ValidationError, "linear MPS indices cannot be null"); + } + linear_mps_indices_owned_.assign(indices, indices + size); + if (linear_mps_indices_owned_.empty()) { + linear_mps_indices_ = span{}; + } else { + linear_mps_indices_ = span(linear_mps_indices_owned_.data(), + static_cast(linear_mps_indices_owned_.size())); + } +} + +template +span data_model_view_t::get_linear_constraint_mps_indices() const noexcept +{ + return linear_mps_indices_; +} + +template +void data_model_view_t::set_mps_declaration_constraint_row_count(i_t count) +{ + mps_declaration_constraint_row_count_ = count; +} + +template +i_t data_model_view_t::get_mps_declaration_constraint_row_count() const noexcept +{ + return mps_declaration_constraint_row_count_; +} + +template +void data_model_view_t::set_mps_all_constraint_row_names(std::vector names) +{ + mps_all_constraint_row_names_ = std::move(names); +} + +template +const std::vector& data_model_view_t::get_mps_all_constraint_row_names() const + noexcept +{ + return mps_all_constraint_row_names_; +} + + // NOTE: Explicitly instantiate all types here in order to avoid linker error template class data_model_view_t; diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 5feb405631..523a975447 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -220,6 +220,24 @@ void mps_data_model_t::set_quadratic_objective_matrix(const f_t* Q_val std::copy(Q_offsets, Q_offsets + size_offsets, Q_objective_offsets_.data()); } +template +void mps_data_model_t::set_linear_constraint_mps_indices(std::vector indices) +{ + linear_constraint_mps_indices_ = std::move(indices); +} + +template +void mps_data_model_t::set_mps_all_constraint_row_names(std::vector names) +{ + mps_all_constraint_row_names_ = std::move(names); +} + +template +void mps_data_model_t::set_mps_declaration_constraint_row_count(i_t count) +{ + mps_declaration_constraint_row_count_ = count; +} + template void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, @@ -292,13 +310,6 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ quadratic_constraints_.push_back(std::move(qc)); } -template -auto mps_data_model_t::get_quadratic_constraints() const - -> const std::vector& -{ - return quadratic_constraints_; -} - template const std::vector& mps_data_model_t::get_constraint_matrix_values() const { @@ -534,6 +545,31 @@ std::vector& mps_data_model_t::get_quadratic_objective_offsets() return Q_objective_offsets_; } +template +const std::vector& mps_data_model_t::get_linear_constraint_mps_indices() const +{ + return linear_constraint_mps_indices_; +} + +template +const std::vector& mps_data_model_t::get_mps_all_constraint_row_names() const +{ + return mps_all_constraint_row_names_; +} + +template +i_t mps_data_model_t::get_mps_declaration_constraint_row_count() const +{ + return mps_declaration_constraint_row_count_; +} + +template +auto mps_data_model_t::get_quadratic_constraints() const + -> const std::vector& +{ + return quadratic_constraints_; +} + template bool mps_data_model_t::has_quadratic_objective() const noexcept { diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 984fd213a4..1d31f2a9d8 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -271,6 +271,7 @@ ObjSenseType convert_to_obj_sense(const std::string& str) template void mps_parser_t::fill_problem(mps_data_model_t& problem) { + // count the row indices that are quadratic constraints std::unordered_set quadratic_row_ids{}; for (const auto& block : qcmatrix_blocks_) { quadratic_row_ids.insert(block.constraint_row_id); @@ -281,20 +282,19 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) std::vector h_values{}; h_offsets.push_back(0); + i_t num_linear_rows = 0; for (i_t i = 0; i < (i_t)A_indices.size(); ++i) { - i_t off = h_offsets.size() > 0 ? h_offsets[h_offsets.size() - 1] : 0; - // Keep quadratic-row linear coefficients out of global A; they are stored with each - // quadratic constraint object instead. - if (!quadratic_row_ids.count(i)) { - for (const auto& idx_itr : A_indices[i]) { - h_indices.push_back(idx_itr); - } - for (const auto& val_itr : A_values[i]) { - h_values.push_back(val_itr); - } - off += A_indices[i].size(); + // Quadratic constraint rows are omitted from the linear CSR; linear pieces live in each + // quadratic_constraint_t bundle. + if (quadratic_row_ids.count(i)) { continue; } + ++num_linear_rows; + for (const auto& idx_itr : A_indices[i]) { + h_indices.push_back(idx_itr); } - h_offsets.push_back(off); + for (const auto& val_itr : A_values[i]) { + h_values.push_back(val_itr); + } + h_offsets.push_back(static_cast(h_indices.size())); } problem.set_csr_constraint_matrix(h_values.data(), @@ -304,12 +304,13 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) h_offsets.data(), h_offsets.size()); - mps_parser_expects(A_indices.size() + 1 == h_offsets.size(), - error_type_t::ValidationError, - "The row indexing vector for the constraint matrix was not constructed " - "successfully. Should be size %zu, but was size %zu", - A_indices.size() + 1, - h_offsets.size()); + mps_parser_expects( + static_cast(num_linear_rows) + 1 == h_offsets.size(), + error_type_t::ValidationError, + "The row indexing vector for the constraint matrix was not constructed " + "successfully. Should be size %zu, but was size %zu", + static_cast(num_linear_rows) + 1, + h_offsets.size()); mps_parser_expects( h_indices.size() == h_values.size(), error_type_t::ValidationError, @@ -329,8 +330,13 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) } - // Set b & c - problem.set_constraint_bounds(b_values.data(), b_values.size()); + // Set b & c (RHS entries for quadratic rows are stored only on quadratic_constraint_t) + std::vector b_compacted{}; + b_compacted.reserve(b_values.size()); + for (i_t i = 0; i < (i_t)b_values.size(); ++i) { + if (!quadratic_row_ids.count(i)) { b_compacted.push_back(b_values[i]); } + } + problem.set_constraint_bounds(b_compacted.data(), static_cast(b_compacted.size())); problem.set_objective_coefficients(c_values.data(), c_values.size()); // Set offset and scaling factor of objective function @@ -352,22 +358,24 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.get_variable_lower_bounds().size(), problem.get_variable_upper_bounds().size()); - // Determine the constraint bounds based on row types + // Determine the constraint bounds based on row types (quadratic rows use bundles only, not counted here) { std::vector h_constraint_lower_bounds{}; std::vector h_constraint_upper_bounds{}; for (i_t i = 0; i < (i_t)row_types.size(); ++i) { + if (quadratic_row_ids.count(i)) { continue; } if (row_types[i] == Equality) { h_constraint_lower_bounds.push_back(b_values[i]); h_constraint_upper_bounds.push_back(b_values[i]); + const size_t r = h_constraint_lower_bounds.size() - 1; if (ranges_values.size() > 0 && ranges_values[i] != unset_range_value) // Add range value if specified { - mps_parser_expects(!std::isnan(h_constraint_lower_bounds[i]), + mps_parser_expects(!std::isnan(h_constraint_lower_bounds[r]), error_type_t::ValidationError, "Constraints lower bound %d shouldn't be nan", i); - mps_parser_expects(!std::isnan(h_constraint_upper_bounds[i]), + mps_parser_expects(!std::isnan(h_constraint_upper_bounds[r]), error_type_t::ValidationError, "Constraints upper bound %d shouldn't be nan", i); @@ -376,17 +384,18 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) "Equality range value %d shouldn't be nan", i); if (ranges_values[i] < f_t(0)) - h_constraint_lower_bounds[i] = h_constraint_lower_bounds[i] + ranges_values[i]; + h_constraint_lower_bounds[r] = h_constraint_lower_bounds[r] + ranges_values[i]; else // Positive - h_constraint_upper_bounds[i] = h_constraint_upper_bounds[i] + ranges_values[i]; + h_constraint_upper_bounds[r] = h_constraint_upper_bounds[r] + ranges_values[i]; } } else if (row_types[i] == GreaterThanOrEqual) { h_constraint_lower_bounds.push_back(b_values[i]); h_constraint_upper_bounds.push_back(std::numeric_limits::infinity()); + const size_t r = h_constraint_lower_bounds.size() - 1; if (ranges_values.size() > 0 && ranges_values[i] != unset_range_value) // Add range value if specified { - mps_parser_expects(!std::isnan(h_constraint_lower_bounds[i]), + mps_parser_expects(!std::isnan(h_constraint_lower_bounds[r]), error_type_t::ValidationError, "Constraints lower bound %d shouldn't be nan", i); @@ -394,15 +403,16 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) error_type_t::ValidationError, "Greater range value %d shouldn't be nan", i); - h_constraint_upper_bounds[i] = h_constraint_lower_bounds[i] + std::abs(ranges_values[i]); + h_constraint_upper_bounds[r] = h_constraint_lower_bounds[r] + std::abs(ranges_values[i]); } } else if (row_types[i] == LesserThanOrEqual) { h_constraint_lower_bounds.push_back(-std::numeric_limits::infinity()); h_constraint_upper_bounds.push_back(b_values[i]); + const size_t r = h_constraint_lower_bounds.size() - 1; if (ranges_values.size() > 0 && ranges_values[i] != unset_range_value) // Add range value if specified { - mps_parser_expects(!std::isnan(h_constraint_upper_bounds[i]), + mps_parser_expects(!std::isnan(h_constraint_upper_bounds[r]), error_type_t::ValidationError, "Constraints upper bound %d shouldn't be nan", i); @@ -410,17 +420,18 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) error_type_t::ValidationError, "Lesser range value %d shouldn't be nan", i); - h_constraint_lower_bounds[i] = h_constraint_upper_bounds[i] - std::abs(ranges_values[i]); + h_constraint_lower_bounds[r] = h_constraint_upper_bounds[r] - std::abs(ranges_values[i]); } } else { mps_parser_expects(false, error_type_t::ValidationError, "Unsupported row type was passed to the Optimization Problem"); } + const size_t r = h_constraint_lower_bounds.size() - 1; mps_parser_expects( - !std::isnan(h_constraint_lower_bounds[i]), error_type_t::ValidationError, "Cannot be nan"); + !std::isnan(h_constraint_lower_bounds[r]), error_type_t::ValidationError, "Cannot be nan"); mps_parser_expects( - !std::isnan(h_constraint_upper_bounds[i]), error_type_t::ValidationError, "Cannot be nan"); + !std::isnan(h_constraint_upper_bounds[r]), error_type_t::ValidationError, "Cannot be nan"); } problem.set_constraint_lower_bounds(h_constraint_lower_bounds.data(), @@ -447,14 +458,6 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_objective_name(objective_name); problem.set_variable_names(std::move(var_names)); problem.set_variable_types(std::move(var_types)); - problem.set_row_names(std::move(row_names)); - { - std::vector row_types_host(row_types.size()); - for (size_t i = 0; i < row_types.size(); ++i) { - row_types_host[i] = static_cast(row_types[i]); - } - problem.set_row_types(row_types_host.data(), static_cast(row_types_host.size())); - } problem.set_maximize(maximize); // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of O(nnz*log(nnz))) @@ -554,23 +557,24 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) auto csr_result = build_csr_via_transpose( block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); const i_t row_id = block.constraint_row_id; - mps_parser_expects(row_id >= 0 && row_id < problem.get_n_constraints(), - error_type_t::ValidationError, - "QCMATRIX row index %d is out of range for constraints", - static_cast(row_id)); + mps_parser_expects( + row_id >= 0 && row_id < static_cast(row_types.size()), + error_type_t::ValidationError, + "QCMATRIX row index %d is out of range for constraints", + static_cast(row_id)); const i_t linear_nnz = static_cast(A_indices[row_id].size()); const f_t* linear_val = linear_nnz > 0 ? A_values[row_id].data() : nullptr; const i_t* linear_idx = linear_nnz > 0 ? A_indices[row_id].data() : nullptr; problem.append_quadratic_constraint( row_id, - problem.get_row_names()[row_id], - problem.get_row_types()[row_id], + row_names[row_id], + static_cast(row_types[row_id]), linear_val, linear_nnz, linear_idx, linear_nnz, - problem.get_constraint_bounds()[row_id], + b_values[row_id], csr_result.values.data(), static_cast(csr_result.values.size()), csr_result.indices.data(), @@ -578,6 +582,42 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) csr_result.offsets.data(), static_cast(csr_result.offsets.size())); } + + std::vector linear_mps_indices{}; + linear_mps_indices.reserve(row_types.size()); + for (i_t i = 0; i < static_cast(row_types.size()); ++i) { + if (!quadratic_row_ids.count(i)) { linear_mps_indices.push_back(i); } + } + + if (!quadratic_row_ids.empty()) { + problem.set_linear_constraint_mps_indices(std::move(linear_mps_indices)); + problem.set_mps_declaration_constraint_row_count(static_cast(row_names.size())); + problem.set_mps_all_constraint_row_names( + std::vector(row_names.begin(), row_names.end())); + + std::vector linear_row_names{}; + std::vector row_types_linear{}; + linear_row_names.reserve(row_names.size()); + row_types_linear.reserve(row_names.size()); + for (size_t i = 0; i < row_names.size(); ++i) { + if (!quadratic_row_ids.count(static_cast(i))) { + linear_row_names.push_back(row_names[i]); + row_types_linear.push_back(static_cast(row_types[i])); + } + } + problem.set_row_names(std::move(linear_row_names)); + problem.set_row_types(row_types_linear.data(), static_cast(row_types_linear.size())); + } else { + problem.set_linear_constraint_mps_indices({}); + problem.set_mps_declaration_constraint_row_count(0); + problem.set_mps_all_constraint_row_names({}); + std::vector row_types_host(row_types.size()); + for (size_t i = 0; i < row_types.size(); ++i) { + row_types_host[i] = static_cast(row_types[i]); + } + problem.set_row_names(std::move(row_names)); + problem.set_row_types(row_types_host.data(), static_cast(row_types_host.size())); + } } template diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index 3a0997774b..41771d7be6 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -19,9 +20,30 @@ #include #include #include +#include +#include namespace cuopt::mps_parser { +namespace { + +template +char linear_row_type_from_bounds(f_t cl, f_t cu) +{ + if (cl == cu) { return 'E'; } + if (std::isinf(cu)) { return 'G'; } + return 'L'; +} + +inline int linear_csr_row_for_mps_decl_index( + std::unordered_map const& linear_csr_row_for_mps_decl, size_t decl_k) +{ + auto const it = linear_csr_row_for_mps_decl.find(decl_k); + return it == linear_csr_row_for_mps_decl.end() ? -1 : it->second; +} + +} // namespace + template mps_writer_t::mps_writer_t(const data_model_view_t& problem) : problem_(problem) { @@ -103,6 +125,25 @@ data_model_view_t mps_writer_t::create_view( static_cast(Q_offsets.size())); } + if (model.has_quadratic_constraints()) { + view.set_quadratic_constraints( + std::vector::quadratic_constraint_t>( + model.get_quadratic_constraints())); + } + + if (model.get_mps_declaration_constraint_row_count() > 0) { + view.set_mps_declaration_constraint_row_count(model.get_mps_declaration_constraint_row_count()); + const auto& lmi = model.get_linear_constraint_mps_indices(); + if (!lmi.empty()) { + view.set_linear_constraint_mps_indices(lmi.data(), static_cast(lmi.size())); + } + const auto& all_names = model.get_mps_all_constraint_row_names(); + if (!all_names.empty()) { + view.set_mps_all_constraint_row_names( + std::vector(all_names.begin(), all_names.end())); + } + } + return view; } @@ -130,6 +171,26 @@ void mps_writer_t::write(const std::string& mps_file_path) else n_constraints = problem_.get_constraint_lower_bounds().size(); + const bool qc_rows_separated = + problem_.get_mps_declaration_constraint_row_count() > 0 && + problem_.get_linear_constraint_mps_indices().size() > 0 && + !problem_.get_mps_all_constraint_row_names().empty(); + + i_t n_mps_declaration_rows = n_constraints; + std::unordered_map linear_csr_row_for_mps_decl; + if (qc_rows_separated) { + n_mps_declaration_rows = problem_.get_mps_declaration_constraint_row_count(); + span const ltd = problem_.get_linear_constraint_mps_indices(); + linear_csr_row_for_mps_decl.reserve(static_cast(ltd.size())); + size_t const n_decl_sz = static_cast(n_mps_declaration_rows); + for (size_t j = 0; j < ltd.size(); ++j) { + i_t const decl_row = ltd[j]; + if (decl_row < 0) { continue; } + size_t const d = static_cast(decl_row); + if (d < n_decl_sz) { linear_csr_row_for_mps_decl.try_emplace(d, static_cast(j)); } + } + } + std::vector objective_coefficients(problem_.get_objective_coefficients().size()); std::vector constraint_lower_bounds(n_constraints); std::vector constraint_upper_bounds(n_constraints); @@ -206,20 +267,65 @@ void mps_writer_t::write(const std::string& mps_file_path) if (problem_.get_sense()) { mps_file << "OBJSENSE\n MAXIMIZE\n"; } + // sort the quadratic constraints by the constraint row index, useful for both ROWS and RHS sections + using qc_t = typename mps_data_model_t::quadratic_constraint_t; + std::vector qcs_by_decl_row; + if (qc_rows_separated) { + std::vector const& qcs = problem_.get_quadratic_constraints(); + size_t const n_decl = static_cast(n_mps_declaration_rows); + qcs_by_decl_row.reserve(qcs.size()); + for (qc_t const& qc : qcs) { + size_t const idx = static_cast(qc.constraint_row_index); + if (idx < n_decl) { qcs_by_decl_row.push_back(&qc); } + } + std::stable_sort(qcs_by_decl_row.begin(), + qcs_by_decl_row.end(), + [](qc_t const* lhs, qc_t const* rhs) { + return lhs->constraint_row_index < rhs->constraint_row_index; + }); + } + // ROWS section mps_file << "ROWS\n"; mps_file << " N " << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) << "\n"; - for (size_t i = 0; i < (size_t)n_constraints; i++) { - std::string row_name = - i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); - char type = 'L'; - if (constraint_lower_bounds[i] == constraint_upper_bounds[i]) - type = 'E'; - else if (std::isinf(constraint_upper_bounds[i])) - type = 'G'; - mps_file << " " << type << " " << row_name << "\n"; + if (!qc_rows_separated) { + for (size_t k = 0; k < (size_t)n_mps_declaration_rows; ++k) { + std::string row_name = k < problem_.get_row_names().size() ? problem_.get_row_names()[k] + : "R" + std::to_string(k); + char const type = + linear_row_type_from_bounds(constraint_lower_bounds[k], constraint_upper_bounds[k]); + mps_file << " " << type << " " << row_name << "\n"; + } + } else { + size_t const n_decl = static_cast(n_mps_declaration_rows); + const auto& alln = problem_.get_mps_all_constraint_row_names(); + size_t qc_idx = 0; + for (size_t k = 0; k < n_decl; ++k) { + std::string row_name = alln[k]; + char type = 'L'; + // find the quadratic constraint that corresponds to the current row + while (qc_idx < qcs_by_decl_row.size() && + static_cast(qcs_by_decl_row[qc_idx]->constraint_row_index) < k) { + ++qc_idx; + } + qc_t const* const qc_match = + (qc_idx < qcs_by_decl_row.size() && + static_cast(qcs_by_decl_row[qc_idx]->constraint_row_index) == k) + ? qcs_by_decl_row[qc_idx] + : nullptr; + if (qc_match != nullptr) { + type = qc_match->constraint_row_type; + } else { + int const lj = linear_csr_row_for_mps_decl_index(linear_csr_row_for_mps_decl, k); + if (lj >= 0) { + size_t const j = static_cast(lj); + type = linear_row_type_from_bounds(constraint_lower_bounds[j], constraint_upper_bounds[j]); + } + } + mps_file << " " << type << " " << row_name << "\n"; + } } // COLUMNS section @@ -230,9 +336,13 @@ void mps_writer_t::write(const std::string& mps_file_path) std::vector var_in_constraint(n_variables, false); std::map>> integral_col_nnzs; std::map>> continuous_col_nnzs; - for (size_t row_id = 0; row_id < (size_t)n_constraints; row_id++) { - for (size_t k = (size_t)constraint_matrix_offsets[row_id]; - k < (size_t)constraint_matrix_offsets[row_id + 1]; + + // iterate over the constraint matrix and add the nonzeros to the integral and continuous col_nnzs maps + for (size_t csr_row = 0; csr_row < (size_t)n_constraints; csr_row++) { + const i_t row_id = + qc_rows_separated ? problem_.get_linear_constraint_mps_indices()[csr_row] : static_cast(csr_row); + for (size_t k = (size_t)constraint_matrix_offsets[csr_row]; + k < (size_t)constraint_matrix_offsets[csr_row + 1]; k++) { size_t var = (size_t)constraint_matrix_indices[k]; if (variable_types[var] == 'I') { @@ -244,6 +354,23 @@ void mps_writer_t::write(const std::string& mps_file_path) } } + // Quadratic constraint rows omit linear coefficients from global A; add them from QC bundles. + if (problem_.has_quadratic_constraints()) { + for (const auto& qc : problem_.get_quadratic_constraints()) { + const size_t row_id = static_cast(qc.constraint_row_index); + for (size_t t = 0; t < qc.linear_indices.size(); ++t) { + size_t var = static_cast(qc.linear_indices[t]); + f_t val = qc.linear_values[t]; + if (variable_types[var] == 'I') { + integral_col_nnzs[var].emplace_back(row_id, val); + } else { + continuous_col_nnzs[var].emplace_back(row_id, val); + } + var_in_constraint[var] = true; + } + } + } + // Record and explicitely declared variables not contained in any constraint. std::vector orphan_continuous_vars; std::vector orphan_integer_vars; @@ -276,9 +403,15 @@ void mps_writer_t::write(const std::string& mps_file_path) ? problem_.get_variable_names()[var_id] : "C" + std::to_string(var_id); for (auto& nnz : nnzs) { - std::string row_name = nnz.first < problem_.get_row_names().size() - ? problem_.get_row_names()[nnz.first] - : "R" + std::to_string(nnz.first); + std::string row_name; + if (qc_rows_separated && + static_cast(nnz.first) < problem_.get_mps_all_constraint_row_names().size()) { + row_name = problem_.get_mps_all_constraint_row_names()[nnz.first]; + } else if (static_cast(nnz.first) < problem_.get_row_names().size()) { + row_name = problem_.get_row_names()[nnz.first]; + } else { + row_name = "R" + std::to_string(nnz.first); + } mps_file << " " << col_name << " " << row_name << " " << nnz.second << "\n"; } // Write objective coefficients @@ -293,19 +426,54 @@ void mps_writer_t::write(const std::string& mps_file_path) // RHS section mps_file << "RHS\n"; - for (size_t i = 0; i < (size_t)n_constraints; i++) { - std::string row_name = - i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); - - f_t rhs; - if (constraint_bounds.size() > 0) - rhs = constraint_bounds[i]; - else if (std::isinf(constraint_lower_bounds[i])) { - rhs = constraint_upper_bounds[i]; - } else if (std::isinf(constraint_upper_bounds[i])) { - rhs = constraint_lower_bounds[i]; - } else { // RANGES, encode the lower bound - rhs = constraint_lower_bounds[i]; + size_t qc_idx_rhs = 0; + for (size_t k = 0; k < (size_t)n_mps_declaration_rows; ++k) { + std::string row_name; + f_t rhs{0}; + + if (!qc_rows_separated) { + row_name = k < problem_.get_row_names().size() ? problem_.get_row_names()[k] + : "R" + std::to_string(k); + if (constraint_bounds.size() > 0) + rhs = constraint_bounds[k]; + else if (std::isinf(constraint_lower_bounds[k])) { + rhs = constraint_upper_bounds[k]; + } else if (std::isinf(constraint_upper_bounds[k])) { + rhs = constraint_lower_bounds[k]; + } else { + rhs = constraint_lower_bounds[k]; + } + } else { + const auto& alln = problem_.get_mps_all_constraint_row_names(); + row_name = alln[k]; + while (qc_idx_rhs < qcs_by_decl_row.size() && + static_cast(qcs_by_decl_row[qc_idx_rhs]->constraint_row_index) < k) { + ++qc_idx_rhs; + } + qc_t const* const qc_match = + (qc_idx_rhs < qcs_by_decl_row.size() && + static_cast(qcs_by_decl_row[qc_idx_rhs]->constraint_row_index) == k) + ? qcs_by_decl_row[qc_idx_rhs] + : nullptr; + if (qc_match != nullptr) { + rhs = qc_match->rhs_value; + } else { + int const lj = linear_csr_row_for_mps_decl_index(linear_csr_row_for_mps_decl, k); + mps_parser_expects(lj >= 0, + error_type_t::ValidationError, + "RHS row %zu has no linear or quadratic mapping", + k); + const size_t j = static_cast(lj); + if (constraint_bounds.size() > 0) + rhs = constraint_bounds[j]; + else if (std::isinf(constraint_lower_bounds[j])) { + rhs = constraint_upper_bounds[j]; + } else if (std::isinf(constraint_upper_bounds[j])) { + rhs = constraint_lower_bounds[j]; + } else { + rhs = constraint_lower_bounds[j]; + } + } } if (std::isfinite(rhs) && rhs != 0.0) { @@ -427,6 +595,29 @@ void mps_writer_t::write(const std::string& mps_file_path) } } + // QCMATRIX sections for quadratic constraints (QCQP) + if (problem_.has_quadratic_constraints()) { + for (const auto& qc : problem_.get_quadratic_constraints()) { + mps_file << "QCMATRIX " << qc.constraint_row_name << "\n"; + const i_t n_quad_rows = static_cast(qc.quadratic_offsets.size()) - 1; + for (i_t i = 0; i < n_quad_rows; ++i) { + std::string row_var_name = static_cast(i) < problem_.get_variable_names().size() + ? problem_.get_variable_names()[i] + : "C" + std::to_string(i); + for (i_t p = qc.quadratic_offsets[i]; p < qc.quadratic_offsets[i + 1]; ++p) { + i_t j = qc.quadratic_indices[p]; + f_t v = qc.quadratic_values[p]; + std::string col_var_name = static_cast(j) < problem_.get_variable_names().size() + ? problem_.get_variable_names()[j] + : "C" + std::to_string(j); + if (v != f_t(0)) { + mps_file << " " << row_var_name << " " << col_var_name << " " << v << "\n"; + } + } + } + } + } + mps_file << "ENDATA\n"; mps_file.close(); } diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index dd1376621b..5689260a5b 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -956,12 +956,17 @@ TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) const auto& qcs = model.get_quadratic_constraints(); ASSERT_EQ(2u, qcs.size()); - ASSERT_EQ(3, model.get_n_constraints()); - ASSERT_EQ(3u, model.get_row_names().size()); + ASSERT_EQ(1, model.get_n_constraints()); + ASSERT_EQ(1u, model.get_row_names().size()); EXPECT_EQ("LIN0", model.get_row_names()[0]); - EXPECT_EQ("QC0", model.get_row_names()[1]); - EXPECT_EQ("QC1", model.get_row_names()[2]); EXPECT_EQ('L', model.get_row_types()[0]); + ASSERT_EQ(3, model.get_mps_declaration_constraint_row_count()); + ASSERT_EQ(3u, model.get_mps_all_constraint_row_names().size()); + EXPECT_EQ("LIN0", model.get_mps_all_constraint_row_names()[0]); + EXPECT_EQ("QC0", model.get_mps_all_constraint_row_names()[1]); + EXPECT_EQ("QC1", model.get_mps_all_constraint_row_names()[2]); + ASSERT_EQ(1u, model.get_linear_constraint_mps_indices().size()); + EXPECT_EQ(0, model.get_linear_constraint_mps_indices()[0]); // LIN0: 2*x1 + x2 ≤ 15 (linear row only; not duplicated in quadratic_constraints) EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), @@ -976,7 +981,7 @@ TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) EXPECT_EQ(0, A_idx[A_off[0] + 0]); EXPECT_EQ(1, A_idx[A_off[0] + 1]); - // QC0: x1 + x2 + xᵀQ₀x ≤ 5 (row index 1: OBJ 'N' rows are not counted in row_names) + // QC0: x1 + x2 + xᵀQ₀x ≤ 5 (MPS ROWS declaration index 1; OBJ 'N' rows are not counted) EXPECT_EQ(1, qcs[0].constraint_row_index); EXPECT_EQ("QC0", qcs[0].constraint_row_name); EXPECT_EQ('L', qcs[0].constraint_row_type); @@ -986,9 +991,6 @@ TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) EXPECT_EQ(0, qcs[0].linear_indices[0]); EXPECT_EQ(1, qcs[0].linear_indices[1]); EXPECT_DOUBLE_EQ(5.0, qcs[0].rhs_value); - EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), - model.get_constraint_lower_bounds()[qcs[0].constraint_row_index]); - EXPECT_DOUBLE_EQ(5.0, model.get_constraint_upper_bounds()[qcs[0].constraint_row_index]); EXPECT_FALSE(qcs[0].quadratic_values.empty()); // QC1: 3*x1 + x2 + xᵀQ₁x ≤ 10 @@ -999,9 +1001,6 @@ TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) EXPECT_EQ(3.0, qcs[1].linear_values[0]); EXPECT_EQ(1.0, qcs[1].linear_values[1]); EXPECT_DOUBLE_EQ(10.0, qcs[1].rhs_value); - EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), - model.get_constraint_lower_bounds()[qcs[1].constraint_row_index]); - EXPECT_DOUBLE_EQ(10.0, model.get_constraint_upper_bounds()[qcs[1].constraint_row_index]); } TEST(qps_parser, qcqp_p0033_mps_sections) @@ -1012,12 +1011,14 @@ TEST(qps_parser, qcqp_p0033_mps_sections) const auto model = parse_mps( cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps", false); - EXPECT_EQ(16, model.get_n_constraints()); + EXPECT_EQ(12, model.get_n_constraints()); EXPECT_EQ(33, model.get_n_variables()); - ASSERT_EQ(16u, model.get_row_types().size()); - ASSERT_EQ(16u, model.get_row_names().size()); + ASSERT_EQ(12u, model.get_row_types().size()); + ASSERT_EQ(12u, model.get_row_names().size()); + ASSERT_EQ(16, model.get_mps_declaration_constraint_row_count()); + ASSERT_EQ(16u, model.get_mps_all_constraint_row_names().size()); - const auto& rnames = model.get_row_names(); + const auto& rnames = model.get_mps_all_constraint_row_names(); auto qc1_it = std::find(rnames.begin(), rnames.end(), std::string("QC1")); ASSERT_NE(qc1_it, rnames.end()); const int qc1_row = static_cast(qc1_it - rnames.begin()); @@ -1201,6 +1202,46 @@ void compare_data_models(const mps_data_model_t& original, EXPECT_EQ(orig_Q_off[i], reload_Q_off[i]) << "Q offset mismatch at index " << i; } } + + EXPECT_EQ(original.has_quadratic_constraints(), reloaded.has_quadratic_constraints()); + if (original.has_quadratic_constraints() && reloaded.has_quadratic_constraints()) { + const auto& oqc = original.get_quadratic_constraints(); + const auto& rq = reloaded.get_quadratic_constraints(); + ASSERT_EQ(oqc.size(), rq.size()) << "Quadratic constraint count mismatch"; + for (size_t k = 0; k < oqc.size(); ++k) { + EXPECT_EQ(oqc[k].constraint_row_index, rq[k].constraint_row_index); + EXPECT_EQ(oqc[k].constraint_row_name, rq[k].constraint_row_name); + EXPECT_EQ(oqc[k].constraint_row_type, rq[k].constraint_row_type); + EXPECT_NEAR(oqc[k].rhs_value, rq[k].rhs_value, tol); + ASSERT_EQ(oqc[k].linear_values.size(), rq[k].linear_values.size()); + ASSERT_EQ(oqc[k].linear_indices.size(), rq[k].linear_indices.size()); + for (size_t i = 0; i < oqc[k].linear_values.size(); ++i) { + EXPECT_NEAR(oqc[k].linear_values[i], rq[k].linear_values[i], tol); + EXPECT_EQ(oqc[k].linear_indices[i], rq[k].linear_indices[i]); + } + ASSERT_EQ(oqc[k].quadratic_values.size(), rq[k].quadratic_values.size()); + ASSERT_EQ(oqc[k].quadratic_indices.size(), rq[k].quadratic_indices.size()); + ASSERT_EQ(oqc[k].quadratic_offsets.size(), rq[k].quadratic_offsets.size()); + for (size_t i = 0; i < oqc[k].quadratic_values.size(); ++i) { + EXPECT_NEAR(oqc[k].quadratic_values[i], rq[k].quadratic_values[i], tol); + } + for (size_t i = 0; i < oqc[k].quadratic_indices.size(); ++i) { + EXPECT_EQ(oqc[k].quadratic_indices[i], rq[k].quadratic_indices[i]); + } + for (size_t i = 0; i < oqc[k].quadratic_offsets.size(); ++i) { + EXPECT_EQ(oqc[k].quadratic_offsets[i], rq[k].quadratic_offsets[i]); + } + } + } + + EXPECT_EQ(original.get_mps_declaration_constraint_row_count(), + reloaded.get_mps_declaration_constraint_row_count()); + EXPECT_EQ(original.get_linear_constraint_mps_indices(), reloaded.get_linear_constraint_mps_indices()); + ASSERT_EQ(original.get_mps_all_constraint_row_names().size(), + reloaded.get_mps_all_constraint_row_names().size()); + for (size_t i = 0; i < original.get_mps_all_constraint_row_names().size(); ++i) { + EXPECT_EQ(original.get_mps_all_constraint_row_names()[i], reloaded.get_mps_all_constraint_row_names()[i]); + } } TEST(mps_roundtrip, linear_programming_basic) @@ -1311,4 +1352,27 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_2) std::filesystem::remove(temp_file); } +TEST(mps_roundtrip, qcqp_p0033_qc1) +{ + if (!file_exists("qcqp/p0033_qc1.mps")) { + GTEST_SKIP() << "Test file not found"; + } + + std::string input_file = + cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; + std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; + + auto original = parse_mps(input_file, false); + ASSERT_TRUE(original.has_quadratic_objective()); + ASSERT_TRUE(original.has_quadratic_constraints()); + + mps_writer_t writer(original); + writer.write(temp_file); + + auto reloaded = parse_mps(temp_file, false); + compare_data_models(original, reloaded); + + std::filesystem::remove(temp_file); +} + } // namespace cuopt::mps_parser diff --git a/cpp/src/pdlp/cpu_optimization_problem.cpp b/cpp/src/pdlp/cpu_optimization_problem.cpp index 406b0b6541..4bbe5ef545 100644 --- a/cpp/src/pdlp/cpu_optimization_problem.cpp +++ b/cpp/src/pdlp/cpu_optimization_problem.cpp @@ -133,6 +133,52 @@ void cpu_optimization_problem_t::set_quadratic_objective_matrix( std::copy(Q_offsets, Q_offsets + size_offsets, Q_offsets_.begin()); } +template +void cpu_optimization_problem_t::set_quadratic_constraints( + std::vector::mps_quadratic_constraint_t> + constraints) +{ + quadratic_constraints_ = std::move(constraints); +} + +template +void cpu_optimization_problem_t::set_linear_constraint_mps_indices(std::vector indices) +{ + linear_constraint_mps_indices_ = std::move(indices); +} + +template +void cpu_optimization_problem_t::set_mps_declaration_constraint_row_count(i_t count) +{ + mps_declaration_constraint_row_count_ = count; +} + +template +void cpu_optimization_problem_t::set_mps_all_constraint_row_names( + std::vector names) +{ + mps_all_constraint_row_names_ = std::move(names); +} + +template +i_t cpu_optimization_problem_t::get_mps_declaration_constraint_row_count() const +{ + return mps_declaration_constraint_row_count_; +} + +template +const std::vector& cpu_optimization_problem_t::get_linear_constraint_mps_indices() const +{ + return linear_constraint_mps_indices_; +} + +template +const std::vector& cpu_optimization_problem_t::get_mps_all_constraint_row_names() + const +{ + return mps_all_constraint_row_names_; +} + template void cpu_optimization_problem_t::set_variable_lower_bounds( const f_t* variable_lower_bounds, i_t size) @@ -494,6 +540,19 @@ bool cpu_optimization_problem_t::has_quadratic_objective() const return !Q_values_.empty(); } +template +const std::vector::mps_quadratic_constraint_t>& +cpu_optimization_problem_t::get_quadratic_constraints() const +{ + return quadratic_constraints_; +} + +template +bool cpu_optimization_problem_t::has_quadratic_constraints() const +{ + return !quadratic_constraints_.empty(); +} + // ============================================================================== // Host Getters (return references to CPU memory) // ============================================================================== @@ -621,6 +680,20 @@ cpu_optimization_problem_t::to_optimization_problem(raft::handle_t con Q_offsets_.size()); } + if (!quadratic_constraints_.empty()) { + gpu_problem->set_quadratic_constraints( + std::vector::mps_quadratic_constraint_t>( + quadratic_constraints_)); + } + + if (mps_declaration_constraint_row_count_ > 0) { + gpu_problem->set_linear_constraint_mps_indices( + std::vector(linear_constraint_mps_indices_)); + gpu_problem->set_mps_declaration_constraint_row_count(mps_declaration_constraint_row_count_); + gpu_problem->set_mps_all_constraint_row_names( + std::vector(mps_all_constraint_row_names_)); + } + // Set variable bounds if (!variable_lower_bounds_.empty()) { gpu_problem->set_variable_lower_bounds(variable_lower_bounds_.data(), @@ -740,6 +813,25 @@ void cpu_optimization_problem_t::write_to_mps(const std::string& mps_f false); } + if (has_quadratic_constraints()) { + data_model_view.set_quadratic_constraints( + std::vector::mps_quadratic_constraint_t>( + get_quadratic_constraints())); + } + + if (get_mps_declaration_constraint_row_count() > 0) { + data_model_view.set_mps_declaration_constraint_row_count(get_mps_declaration_constraint_row_count()); + if (!get_linear_constraint_mps_indices().empty()) { + data_model_view.set_linear_constraint_mps_indices(get_linear_constraint_mps_indices().data(), + static_cast( + get_linear_constraint_mps_indices().size())); + } + if (!get_mps_all_constraint_row_names().empty()) { + data_model_view.set_mps_all_constraint_row_names( + std::vector(get_mps_all_constraint_row_names())); + } + } + cuopt::mps_parser::write_mps(data_model_view, mps_file_path); } @@ -755,6 +847,10 @@ bool cpu_optimization_problem_t::is_equivalent( if (maximize_ != other.get_sense()) return false; if (n_vars_ != other.get_n_variables()) return false; if (n_constraints_ != other.get_n_constraints()) return false; + if (get_mps_declaration_constraint_row_count() != other.get_mps_declaration_constraint_row_count()) + return false; + if (get_linear_constraint_mps_indices() != other.get_linear_constraint_mps_indices()) return false; + if (get_mps_all_constraint_row_names() != other.get_mps_all_constraint_row_names()) return false; if (std::abs(objective_scaling_factor_ - other.get_objective_scaling_factor()) > 1e-9) return false; if (std::abs(objective_offset_ - other.get_objective_offset()) > 1e-9) return false; diff --git a/cpp/src/pdlp/optimization_problem.cu b/cpp/src/pdlp/optimization_problem.cu index 87ff9dab08..b3f71bd92c 100644 --- a/cpp/src/pdlp/optimization_problem.cu +++ b/cpp/src/pdlp/optimization_problem.cu @@ -97,7 +97,11 @@ optimization_problem_t::optimization_problem_t( problem_name_{other.get_problem_name()}, problem_category_{other.get_problem_category()}, var_names_{other.get_variable_names()}, - row_names_{other.get_row_names()} + row_names_{other.get_row_names()}, + quadratic_constraints_{other.get_quadratic_constraints()}, + linear_constraint_mps_indices_{other.get_linear_constraint_mps_indices()}, + mps_declaration_constraint_row_count_{other.get_mps_declaration_constraint_row_count()}, + mps_all_constraint_row_names_{other.get_mps_all_constraint_row_names()} { } @@ -197,6 +201,51 @@ void optimization_problem_t::set_quadratic_objective_matrix( // FIX ME:: check for positive semi definite matrix } +template +void optimization_problem_t::set_quadratic_constraints( + std::vector::mps_quadratic_constraint_t> + constraints) +{ + quadratic_constraints_ = std::move(constraints); +} + +template +void optimization_problem_t::set_linear_constraint_mps_indices(std::vector indices) +{ + linear_constraint_mps_indices_ = std::move(indices); +} + +template +void optimization_problem_t::set_mps_declaration_constraint_row_count(i_t count) +{ + mps_declaration_constraint_row_count_ = count; +} + +template +void optimization_problem_t::set_mps_all_constraint_row_names(std::vector names) +{ + mps_all_constraint_row_names_ = std::move(names); +} + +template +i_t optimization_problem_t::get_mps_declaration_constraint_row_count() const +{ + return mps_declaration_constraint_row_count_; +} + +template +const std::vector& optimization_problem_t::get_linear_constraint_mps_indices() const +{ + return linear_constraint_mps_indices_; +} + +template +const std::vector& optimization_problem_t::get_mps_all_constraint_row_names() + const +{ + return mps_all_constraint_row_names_; +} + template void optimization_problem_t::set_variable_lower_bounds(const f_t* variable_lower_bounds, i_t size) @@ -548,6 +597,19 @@ bool optimization_problem_t::has_quadratic_objective() const return !Q_values_.empty(); } +template +const std::vector::mps_quadratic_constraint_t>& +optimization_problem_t::get_quadratic_constraints() const +{ + return quadratic_constraints_; +} + +template +bool optimization_problem_t::has_quadratic_constraints() const +{ + return !quadratic_constraints_.empty(); +} + template raft::handle_t const* optimization_problem_t::get_handle_ptr() const noexcept { @@ -820,6 +882,25 @@ void optimization_problem_t::write_to_mps(const std::string& mps_file_ is_symmetrized); } + if (has_quadratic_constraints()) { + data_model_view.set_quadratic_constraints( + std::vector::mps_quadratic_constraint_t>( + get_quadratic_constraints())); + } + + if (get_mps_declaration_constraint_row_count() > 0) { + data_model_view.set_mps_declaration_constraint_row_count(get_mps_declaration_constraint_row_count()); + if (!get_linear_constraint_mps_indices().empty()) { + data_model_view.set_linear_constraint_mps_indices(get_linear_constraint_mps_indices().data(), + static_cast( + get_linear_constraint_mps_indices().size())); + } + if (!get_mps_all_constraint_row_names().empty()) { + data_model_view.set_mps_all_constraint_row_names( + std::vector(get_mps_all_constraint_row_names())); + } + } + cuopt::mps_parser::write_mps(data_model_view, mps_file_path); } @@ -1030,6 +1111,11 @@ bool optimization_problem_t::is_equivalent( if (maximize_ != other.maximize_) { return false; } if (n_vars_ != other.n_vars_) { return false; } if (n_constraints_ != other.n_constraints_) { return false; } + if (linear_constraint_mps_indices_ != other.linear_constraint_mps_indices_) { return false; } + if (mps_declaration_constraint_row_count_ != other.mps_declaration_constraint_row_count_) { + return false; + } + if (mps_all_constraint_row_names_ != other.mps_all_constraint_row_names_) { return false; } if (objective_scaling_factor_ != other.objective_scaling_factor_) { return false; } if (objective_offset_ != other.objective_offset_) { return false; } if (problem_category_ != other.problem_category_) { return false; } @@ -1174,6 +1260,10 @@ bool optimization_problem_t::is_equivalent( if (maximize_ != other.get_sense()) return false; if (n_vars_ != other.get_n_variables()) return false; if (n_constraints_ != other.get_n_constraints()) return false; + if (get_mps_declaration_constraint_row_count() != other.get_mps_declaration_constraint_row_count()) + return false; + if (get_linear_constraint_mps_indices() != other.get_linear_constraint_mps_indices()) return false; + if (get_mps_all_constraint_row_names() != other.get_mps_all_constraint_row_names()) return false; if (std::abs(objective_scaling_factor_ - other.get_objective_scaling_factor()) > 1e-9) return false; if (std::abs(objective_offset_ - other.get_objective_offset()) > 1e-9) return false; From 21b0f9a6d77d4982efe46c8d30ec9df02bdca65d Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 20 Apr 2026 07:31:36 -0700 Subject: [PATCH 10/68] Restrict quadratic constraints to be convex with type 'L' --- cpp/libmps_parser/include/mps_parser/mps_data_model.hpp | 7 ++++++- cpp/libmps_parser/src/mps_data_model.cpp | 6 ++++++ cpp/libmps_parser/src/mps_writer.cpp | 3 ++- cpp/libmps_parser/tests/mps_parser_test.cpp | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 2a578c36c5..a292bb5002 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -275,6 +275,7 @@ class mps_data_model_t { /** ROWS declaration index (among all constraint rows), not an index into the linear CSR. */ i_t constraint_row_index{}; std::string constraint_row_name{}; + /** MPS ROWS sense for this quadratic row; only 'L' (≤) is supported for convex QCQP at the moment. */ char constraint_row_type{}; std::vector linear_values{}; std::vector linear_indices{}; @@ -284,7 +285,11 @@ class mps_data_model_t { std::vector quadratic_offsets{}; }; - /** @brief Append one complete quadratic constraint (row + linear + rhs + quadratic Q). */ + /** + * @brief Append one complete quadratic constraint (row + linear + rhs + quadratic Q). + * @param constraint_row_type MPS ROWS type; must be 'L'. 'G' and 'E' quadratic rows are not + * supported. + */ void append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, char constraint_row_type, diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 523a975447..72ae524703 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -257,6 +257,12 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ mps_parser_expects( constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); + mps_parser_expects(constraint_row_type == 'L', + error_type_t::ValidationError, + "Quadratic constraint ROWS type must be 'L' (less-or-equal); got '%c'. " + "Only 'L' is supported for convex quadratic constraints.", + constraint_row_type); + mps_parser_expects(linear_nnz == linear_indices_nnz, error_type_t::ValidationError, "linear_values and linear_indices must have the same nnz count"); diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index 41771d7be6..a7d6734027 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -316,7 +316,8 @@ void mps_writer_t::write(const std::string& mps_file_path) ? qcs_by_decl_row[qc_idx] : nullptr; if (qc_match != nullptr) { - type = qc_match->constraint_row_type; + // Quadratic rows are supported only as MPS 'L' (≤); always emit that sense. + type = 'L'; } else { int const lj = linear_csr_row_for_mps_decl_index(linear_csr_row_for_mps_decl, k); if (lj >= 0) { diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index 5689260a5b..3704eac8db 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include From e6e1f427058750c6716964e5ffa102b69a4989c5 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 22 Apr 2026 04:47:45 -0700 Subject: [PATCH 11/68] Clean up for the MPS parser --- .../cpu_optimization_problem.hpp | 19 +- .../optimization_problem.hpp | 19 +- .../optimization_problem_interface.hpp | 67 +++--- .../optimization_problem_utils.hpp | 31 +-- .../include/mps_parser/data_model_view.hpp | 33 +-- .../include/mps_parser/mps_data_model.hpp | 29 +-- cpp/libmps_parser/src/data_model_view.cpp | 48 ----- cpp/libmps_parser/src/mps_data_model.cpp | 36 ---- cpp/libmps_parser/src/mps_parser.cpp | 19 +- cpp/libmps_parser/src/mps_writer.cpp | 196 ++++-------------- cpp/libmps_parser/tests/mps_parser_test.cpp | 26 +-- cpp/src/pdlp/cpu_optimization_problem.cpp | 76 +------ cpp/src/pdlp/optimization_problem.cu | 74 +------ 13 files changed, 125 insertions(+), 548 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp b/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp index 2865fc877f..48d61b9e0c 100644 --- a/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp @@ -41,7 +41,7 @@ class mip_solution_interface_t; template class cpu_optimization_problem_t : public optimization_problem_interface_t { public: - using typename optimization_problem_interface_t::mps_quadratic_constraint_t; + using typename optimization_problem_interface_t::quadratic_constraint_t; cpu_optimization_problem_t(); @@ -115,16 +115,9 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t& get_quadratic_objective_values() const override; bool has_quadratic_objective() const override; - void set_quadratic_constraints(std::vector constraints) override; + void set_quadratic_constraints(std::vector constraints) override; bool has_quadratic_constraints() const override; - const std::vector& get_quadratic_constraints() const override; - // Additional methods for MPS export if quadratic constraints are present - void set_linear_constraint_mps_indices(std::vector indices) override; - void set_mps_declaration_constraint_row_count(i_t count) override; - void set_mps_all_constraint_row_names(std::vector names) override; - i_t get_mps_declaration_constraint_row_count() const override; - const std::vector& get_linear_constraint_mps_indices() const override; - const std::vector& get_mps_all_constraint_row_names() const override; + const std::vector& get_quadratic_constraints() const override; // Host getters - these are the only supported getters for CPU implementation std::vector get_constraint_matrix_values_host() const override; @@ -198,11 +191,7 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t Q_indices_; std::vector Q_values_; - std::vector quadratic_constraints_{}; - - std::vector linear_constraint_mps_indices_{}; - i_t mps_declaration_constraint_row_count_{0}; - std::vector mps_all_constraint_row_names_{}; + std::vector quadratic_constraints_{}; std::vector variable_lower_bounds_; std::vector variable_upper_bounds_; diff --git a/cpp/include/cuopt/linear_programming/optimization_problem.hpp b/cpp/include/cuopt/linear_programming/optimization_problem.hpp index 27f5ac1168..c355fbde94 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem.hpp @@ -73,7 +73,7 @@ class optimization_problem_t : public optimization_problem_interface_t "'optimization_problem_t' accepts only floating point types for weights"); // nvcc does not always find base typedefs in derived class scope; inject explicitly. - using typename optimization_problem_interface_t::mps_quadratic_constraint_t; + using typename optimization_problem_interface_t::quadratic_constraint_t; /** * @brief A device-side view of the `optimization_problem_t` structure with @@ -199,7 +199,7 @@ class optimization_problem_t : public optimization_problem_interface_t i_t size_offsets, bool validate_positive_semi_definite = false) override; - void set_quadratic_constraints(std::vector constraints) override; + void set_quadratic_constraints(std::vector constraints) override; /** @copydoc optimization_problem_interface_t::set_variable_lower_bounds */ void set_variable_lower_bounds(const f_t* variable_lower_bounds, i_t size) override; @@ -264,17 +264,10 @@ class optimization_problem_t : public optimization_problem_interface_t const std::vector& get_quadratic_objective_offsets() const override; const std::vector& get_quadratic_objective_indices() const override; const std::vector& get_quadratic_objective_values() const override; - const std::vector& get_quadratic_constraints() const override; + const std::vector& get_quadratic_constraints() const override; bool has_quadratic_objective() const override; bool has_quadratic_constraints() const override; - void set_linear_constraint_mps_indices(std::vector indices) override; - void set_mps_declaration_constraint_row_count(i_t count) override; - void set_mps_all_constraint_row_names(std::vector names) override; - i_t get_mps_declaration_constraint_row_count() const override; - const std::vector& get_linear_constraint_mps_indices() const override; - const std::vector& get_mps_all_constraint_row_names() const override; - // ============================================================================ // Host getters // ============================================================================ @@ -391,11 +384,7 @@ class optimization_problem_t : public optimization_problem_interface_t std::vector Q_values_; /** QCQP: quadratic constraints **/ - std::vector quadratic_constraints_{}; - - std::vector linear_constraint_mps_indices_{}; - i_t mps_declaration_constraint_row_count_{0}; - std::vector mps_all_constraint_row_names_{}; + std::vector quadratic_constraints_{}; rmm::device_uvector variable_lower_bounds_; rmm::device_uvector variable_upper_bounds_; diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp index c4ee81d875..27a106037b 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp @@ -8,7 +8,6 @@ #pragma once #include -#include #include #include @@ -57,9 +56,18 @@ class optimization_problem_interface_t { static_assert(std::is_floating_point::value, "'optimization_problem_interface_t' accepts only floating point types for weights"); - /** Quadratic constraints as parsed/stored for MPS QCQP (QCMATRIX rows). */ - using mps_quadratic_constraint_t = - typename mps_parser::mps_data_model_t::quadratic_constraint_t; + /** Quadratic constraint bundle used by core optimization problem interfaces. */ + struct quadratic_constraint_t { + i_t constraint_row_index{}; + std::string constraint_row_name{}; + char constraint_row_type{}; + std::vector linear_values{}; + std::vector linear_indices{}; + f_t rhs_value{f_t(0)}; + std::vector quadratic_values{}; + std::vector quadratic_indices{}; + std::vector quadratic_offsets{}; + }; virtual ~optimization_problem_interface_t() = default; @@ -67,42 +75,37 @@ class optimization_problem_interface_t { * @brief Store quadratic constraints for MPS round-trip (linear + Q parts per QC row). * @note Default implementation ignores; GPU/CPU implementations persist for write_to_mps. */ - virtual void set_quadratic_constraints(std::vector constraints) + virtual void set_quadratic_constraints(std::vector constraints) { (void)constraints; } - - /** @brief Whether quadratic constraint metadata is present (for MPS export). */ - virtual bool has_quadratic_constraints() const { return false; } - - /** @brief Quadratic constraints for MPS export (empty if none). */ - virtual const std::vector& get_quadratic_constraints() const + template >> + void set_quadratic_constraints(const std::vector& constraints) { - static const std::vector k_empty{}; - return k_empty; + std::vector converted_constraints; + converted_constraints.reserve(constraints.size()); + for (const auto& qc : constraints) { + converted_constraints.push_back( + {static_cast(qc.constraint_row_index), + qc.constraint_row_name, + qc.constraint_row_type, + std::vector(qc.linear_values.begin(), qc.linear_values.end()), + std::vector(qc.linear_indices.begin(), qc.linear_indices.end()), + static_cast(qc.rhs_value), + std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end()), + std::vector(qc.quadratic_indices.begin(), qc.quadratic_indices.end()), + std::vector(qc.quadratic_offsets.begin(), qc.quadratic_offsets.end())}); + } + set_quadratic_constraints(std::move(converted_constraints)); } - /** - * @brief When QCMATRIX rows are omitted from the linear CSR, maps linear CSR row j to the MPS - * ROWS declaration index. Used for MPS export only. - */ - virtual void set_linear_constraint_mps_indices(std::vector indices) { (void)indices; } - - virtual void set_mps_declaration_constraint_row_count(i_t count) { (void)count; } - - virtual void set_mps_all_constraint_row_names(std::vector names) { (void)names; } - - virtual i_t get_mps_declaration_constraint_row_count() const { return 0; } - - virtual const std::vector& get_linear_constraint_mps_indices() const - { - static const std::vector k_empty{}; - return k_empty; - } + /** @brief Whether quadratic constraint metadata is present (for MPS export). */ + virtual bool has_quadratic_constraints() const = 0; - virtual const std::vector& get_mps_all_constraint_row_names() const + /** @brief Quadratic constraints for MPS export (empty if none). */ + virtual const std::vector& get_quadratic_constraints() const { - static const std::vector k_empty{}; + static const std::vector k_empty{}; return k_empty; } diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp index bd05b3c1b2..204556e053 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp @@ -111,22 +111,9 @@ void populate_from_mps_data_model(optimization_problem_interface_t* pr } // Handle quadratic constraints if present if (data_model.has_quadratic_constraints()) { - problem->set_quadratic_constraints( - std::vector::quadratic_constraint_t>( - data_model.get_quadratic_constraints())); + problem->set_quadratic_constraints(data_model.get_quadratic_constraints()); } - if (data_model.get_mps_declaration_constraint_row_count() > 0) { - problem->set_linear_constraint_mps_indices( - std::vector(data_model.get_linear_constraint_mps_indices())); - problem->set_mps_declaration_constraint_row_count(data_model.get_mps_declaration_constraint_row_count()); - problem->set_mps_all_constraint_row_names( - std::vector(data_model.get_mps_all_constraint_row_names())); - } else { - problem->set_linear_constraint_mps_indices({}); - problem->set_mps_declaration_constraint_row_count(0); - problem->set_mps_all_constraint_row_names({}); - } } /** @@ -285,20 +272,10 @@ void populate_from_data_model_view(optimization_problem_interface_t* p problem->set_row_names(data_model->get_row_names()); } - if (data_model->get_mps_declaration_constraint_row_count() > 0) { - const auto lmi = data_model->get_linear_constraint_mps_indices(); - if (lmi.size() > 0) { - problem->set_linear_constraint_mps_indices(std::vector( - lmi.data(), lmi.data() + static_cast(lmi.size()))); - } - problem->set_mps_declaration_constraint_row_count(data_model->get_mps_declaration_constraint_row_count()); - problem->set_mps_all_constraint_row_names( - std::vector(data_model->get_mps_all_constraint_row_names())); - } else { - problem->set_linear_constraint_mps_indices({}); - problem->set_mps_declaration_constraint_row_count(0); - problem->set_mps_all_constraint_row_names({}); + if (data_model->has_quadratic_constraints()) { + problem->set_quadratic_constraints(data_model->get_quadratic_constraints()); } + } } // namespace cuopt::linear_programming diff --git a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp index 9e2c0e112e..100492e2b0 100644 --- a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp +++ b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp @@ -421,22 +421,30 @@ class data_model_view_t { */ void set_quadratic_constraints( std::vector::quadratic_constraint_t> constraints); + template + void set_quadratic_constraints(const std::vector& constraints) + { + quadratic_constraints_.clear(); + quadratic_constraints_.reserve(constraints.size()); + for (const auto& qc : constraints) { + quadratic_constraints_.push_back( + {static_cast(qc.constraint_row_index), + qc.constraint_row_name, + qc.constraint_row_type, + std::vector(qc.linear_values.begin(), qc.linear_values.end()), + std::vector(qc.linear_indices.begin(), qc.linear_indices.end()), + static_cast(qc.rhs_value), + std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end()), + std::vector(qc.quadratic_indices.begin(), qc.quadratic_indices.end()), + std::vector(qc.quadratic_offsets.begin(), qc.quadratic_offsets.end())}); + } + } bool has_quadratic_constraints() const noexcept; const std::vector::quadratic_constraint_t>& get_quadratic_constraints() const noexcept; - void set_linear_constraint_mps_indices(const i_t* indices, i_t size); - span get_linear_constraint_mps_indices() const noexcept; - - void set_mps_declaration_constraint_row_count(i_t count); - i_t get_mps_declaration_constraint_row_count() const noexcept; - - void set_mps_all_constraint_row_names(std::vector names); - - const std::vector& get_mps_all_constraint_row_names() const noexcept; - private: bool maximize_{false}; span A_; @@ -468,11 +476,6 @@ class data_model_view_t { bool is_Q_symmetrized_{false}; std::vector::quadratic_constraint_t> quadratic_constraints_; - - span linear_mps_indices_{}; - std::vector linear_mps_indices_owned_{}; - i_t mps_declaration_constraint_row_count_{0}; - std::vector mps_all_constraint_row_names_{}; }; // class data_model_view_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index a292bb5002..69909fb3a6 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -353,25 +353,6 @@ class mps_data_model_t { bool has_quadratic_constraints() const noexcept; - /** - * @brief When quadratic constraints are present, CSR rows are linear-only; entry j is the MPS - * ROWS declaration index for linear CSR row j. - */ - void set_linear_constraint_mps_indices(std::vector indices); - - /** - * @brief ROWS names in declaration order (size == declaration row count). Used for MPS export - * when linear CSR excludes quadratic rows. - */ - void set_mps_all_constraint_row_names(std::vector names); - - /** @brief Total ROWS constraint count (linear + quadratic) when QC rows are separated; else 0. */ - void set_mps_declaration_constraint_row_count(i_t count); - - const std::vector& get_linear_constraint_mps_indices() const; - const std::vector& get_mps_all_constraint_row_names() const; - i_t get_mps_declaration_constraint_row_count() const; - /** whether to maximize or minimize the objective function */ bool maximize_; /** @@ -408,10 +389,7 @@ class mps_data_model_t { std::string problem_name_; /** names of each of the variables in the OP */ std::vector var_names_{}; - /** - * names of linear constraint rows only when QCMATRIX rows are separated; otherwise all constraint - * rows (same as MPS ROWS order excluding objective). - */ + /** names of linear constraint rows in exported MPS order. */ std::vector row_names_{}; /** number of variables */ i_t n_vars_{0}; @@ -433,11 +411,6 @@ class mps_data_model_t { /** One full quadratic constraint per QCMATRIX block, in order of appearance in the file */ std::vector quadratic_constraints_; - /** Maps linear CSR row j -> MPS ROWS declaration index; non-empty iff QC rows are split out. */ - std::vector linear_constraint_mps_indices_{}; - i_t mps_declaration_constraint_row_count_{0}; - std::vector mps_all_constraint_row_names_{}; - }; // class mps_data_model_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/src/data_model_view.cpp b/cpp/libmps_parser/src/data_model_view.cpp index be32eb3d3a..aacedfcadb 100644 --- a/cpp/libmps_parser/src/data_model_view.cpp +++ b/cpp/libmps_parser/src/data_model_view.cpp @@ -375,54 +375,6 @@ data_model_view_t::get_quadratic_constraints() const noexcept return quadratic_constraints_; } -template -void data_model_view_t::set_linear_constraint_mps_indices(const i_t* indices, i_t size) -{ - if (size != 0) { - mps_parser_expects( - indices != nullptr, error_type_t::ValidationError, "linear MPS indices cannot be null"); - } - linear_mps_indices_owned_.assign(indices, indices + size); - if (linear_mps_indices_owned_.empty()) { - linear_mps_indices_ = span{}; - } else { - linear_mps_indices_ = span(linear_mps_indices_owned_.data(), - static_cast(linear_mps_indices_owned_.size())); - } -} - -template -span data_model_view_t::get_linear_constraint_mps_indices() const noexcept -{ - return linear_mps_indices_; -} - -template -void data_model_view_t::set_mps_declaration_constraint_row_count(i_t count) -{ - mps_declaration_constraint_row_count_ = count; -} - -template -i_t data_model_view_t::get_mps_declaration_constraint_row_count() const noexcept -{ - return mps_declaration_constraint_row_count_; -} - -template -void data_model_view_t::set_mps_all_constraint_row_names(std::vector names) -{ - mps_all_constraint_row_names_ = std::move(names); -} - -template -const std::vector& data_model_view_t::get_mps_all_constraint_row_names() const - noexcept -{ - return mps_all_constraint_row_names_; -} - - // NOTE: Explicitly instantiate all types here in order to avoid linker error template class data_model_view_t; diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 72ae524703..1ecaf47b2c 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -220,24 +220,6 @@ void mps_data_model_t::set_quadratic_objective_matrix(const f_t* Q_val std::copy(Q_offsets, Q_offsets + size_offsets, Q_objective_offsets_.data()); } -template -void mps_data_model_t::set_linear_constraint_mps_indices(std::vector indices) -{ - linear_constraint_mps_indices_ = std::move(indices); -} - -template -void mps_data_model_t::set_mps_all_constraint_row_names(std::vector names) -{ - mps_all_constraint_row_names_ = std::move(names); -} - -template -void mps_data_model_t::set_mps_declaration_constraint_row_count(i_t count) -{ - mps_declaration_constraint_row_count_ = count; -} - template void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, @@ -551,24 +533,6 @@ std::vector& mps_data_model_t::get_quadratic_objective_offsets() return Q_objective_offsets_; } -template -const std::vector& mps_data_model_t::get_linear_constraint_mps_indices() const -{ - return linear_constraint_mps_indices_; -} - -template -const std::vector& mps_data_model_t::get_mps_all_constraint_row_names() const -{ - return mps_all_constraint_row_names_; -} - -template -i_t mps_data_model_t::get_mps_declaration_constraint_row_count() const -{ - return mps_declaration_constraint_row_count_; -} - template auto mps_data_model_t::get_quadratic_constraints() const -> const std::vector& diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 1d31f2a9d8..e77ed915fb 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -553,6 +553,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs). // Bundle row metadata, row-linear coefficients (from COLUMNS), rhs, and quadratic part together. constexpr f_t k_qcmatrix_value_scale = f_t(1); + const i_t linear_row_count = static_cast(row_types.size() - quadratic_row_ids.size()); + i_t quadratic_row_id = 0; for (const auto& block : qcmatrix_blocks_) { auto csr_result = build_csr_via_transpose( block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); @@ -567,7 +569,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) const i_t* linear_idx = linear_nnz > 0 ? A_indices[row_id].data() : nullptr; problem.append_quadratic_constraint( - row_id, + linear_row_count + quadratic_row_id, row_names[row_id], static_cast(row_types[row_id]), linear_val, @@ -581,20 +583,10 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) static_cast(csr_result.indices.size()), csr_result.offsets.data(), static_cast(csr_result.offsets.size())); - } - - std::vector linear_mps_indices{}; - linear_mps_indices.reserve(row_types.size()); - for (i_t i = 0; i < static_cast(row_types.size()); ++i) { - if (!quadratic_row_ids.count(i)) { linear_mps_indices.push_back(i); } + ++quadratic_row_id; } if (!quadratic_row_ids.empty()) { - problem.set_linear_constraint_mps_indices(std::move(linear_mps_indices)); - problem.set_mps_declaration_constraint_row_count(static_cast(row_names.size())); - problem.set_mps_all_constraint_row_names( - std::vector(row_names.begin(), row_names.end())); - std::vector linear_row_names{}; std::vector row_types_linear{}; linear_row_names.reserve(row_names.size()); @@ -608,9 +600,6 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_row_names(std::move(linear_row_names)); problem.set_row_types(row_types_linear.data(), static_cast(row_types_linear.size())); } else { - problem.set_linear_constraint_mps_indices({}); - problem.set_mps_declaration_constraint_row_count(0); - problem.set_mps_all_constraint_row_names({}); std::vector row_types_host(row_types.size()); for (size_t i = 0; i < row_types.size(); ++i) { row_types_host[i] = static_cast(row_types[i]); diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index a7d6734027..67346cdcb5 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -16,11 +16,9 @@ #include #include #include -#include #include #include #include -#include #include namespace cuopt::mps_parser { @@ -35,13 +33,6 @@ char linear_row_type_from_bounds(f_t cl, f_t cu) return 'L'; } -inline int linear_csr_row_for_mps_decl_index( - std::unordered_map const& linear_csr_row_for_mps_decl, size_t decl_k) -{ - auto const it = linear_csr_row_for_mps_decl.find(decl_k); - return it == linear_csr_row_for_mps_decl.end() ? -1 : it->second; -} - } // namespace template @@ -131,19 +122,6 @@ data_model_view_t mps_writer_t::create_view( model.get_quadratic_constraints())); } - if (model.get_mps_declaration_constraint_row_count() > 0) { - view.set_mps_declaration_constraint_row_count(model.get_mps_declaration_constraint_row_count()); - const auto& lmi = model.get_linear_constraint_mps_indices(); - if (!lmi.empty()) { - view.set_linear_constraint_mps_indices(lmi.data(), static_cast(lmi.size())); - } - const auto& all_names = model.get_mps_all_constraint_row_names(); - if (!all_names.empty()) { - view.set_mps_all_constraint_row_names( - std::vector(all_names.begin(), all_names.end())); - } - } - return view; } @@ -170,26 +148,8 @@ void mps_writer_t::write(const std::string& mps_file_path) n_constraints = problem_.get_constraint_bounds().size(); else n_constraints = problem_.get_constraint_lower_bounds().size(); - - const bool qc_rows_separated = - problem_.get_mps_declaration_constraint_row_count() > 0 && - problem_.get_linear_constraint_mps_indices().size() > 0 && - !problem_.get_mps_all_constraint_row_names().empty(); - - i_t n_mps_declaration_rows = n_constraints; - std::unordered_map linear_csr_row_for_mps_decl; - if (qc_rows_separated) { - n_mps_declaration_rows = problem_.get_mps_declaration_constraint_row_count(); - span const ltd = problem_.get_linear_constraint_mps_indices(); - linear_csr_row_for_mps_decl.reserve(static_cast(ltd.size())); - size_t const n_decl_sz = static_cast(n_mps_declaration_rows); - for (size_t j = 0; j < ltd.size(); ++j) { - i_t const decl_row = ltd[j]; - if (decl_row < 0) { continue; } - size_t const d = static_cast(decl_row); - if (d < n_decl_sz) { linear_csr_row_for_mps_decl.try_emplace(d, static_cast(j)); } - } - } + const auto& quadratic_constraints = problem_.get_quadratic_constraints(); + const i_t n_quadratic_constraints = static_cast(quadratic_constraints.size()); std::vector objective_coefficients(problem_.get_objective_coefficients().size()); std::vector constraint_lower_bounds(n_constraints); @@ -267,66 +227,23 @@ void mps_writer_t::write(const std::string& mps_file_path) if (problem_.get_sense()) { mps_file << "OBJSENSE\n MAXIMIZE\n"; } - // sort the quadratic constraints by the constraint row index, useful for both ROWS and RHS sections - using qc_t = typename mps_data_model_t::quadratic_constraint_t; - std::vector qcs_by_decl_row; - if (qc_rows_separated) { - std::vector const& qcs = problem_.get_quadratic_constraints(); - size_t const n_decl = static_cast(n_mps_declaration_rows); - qcs_by_decl_row.reserve(qcs.size()); - for (qc_t const& qc : qcs) { - size_t const idx = static_cast(qc.constraint_row_index); - if (idx < n_decl) { qcs_by_decl_row.push_back(&qc); } - } - std::stable_sort(qcs_by_decl_row.begin(), - qcs_by_decl_row.end(), - [](qc_t const* lhs, qc_t const* rhs) { - return lhs->constraint_row_index < rhs->constraint_row_index; - }); - } - // ROWS section mps_file << "ROWS\n"; mps_file << " N " << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) << "\n"; - if (!qc_rows_separated) { - for (size_t k = 0; k < (size_t)n_mps_declaration_rows; ++k) { - std::string row_name = k < problem_.get_row_names().size() ? problem_.get_row_names()[k] - : "R" + std::to_string(k); - char const type = - linear_row_type_from_bounds(constraint_lower_bounds[k], constraint_upper_bounds[k]); - mps_file << " " << type << " " << row_name << "\n"; - } - } else { - size_t const n_decl = static_cast(n_mps_declaration_rows); - const auto& alln = problem_.get_mps_all_constraint_row_names(); - size_t qc_idx = 0; - for (size_t k = 0; k < n_decl; ++k) { - std::string row_name = alln[k]; - char type = 'L'; - // find the quadratic constraint that corresponds to the current row - while (qc_idx < qcs_by_decl_row.size() && - static_cast(qcs_by_decl_row[qc_idx]->constraint_row_index) < k) { - ++qc_idx; - } - qc_t const* const qc_match = - (qc_idx < qcs_by_decl_row.size() && - static_cast(qcs_by_decl_row[qc_idx]->constraint_row_index) == k) - ? qcs_by_decl_row[qc_idx] - : nullptr; - if (qc_match != nullptr) { - // Quadratic rows are supported only as MPS 'L' (≤); always emit that sense. - type = 'L'; - } else { - int const lj = linear_csr_row_for_mps_decl_index(linear_csr_row_for_mps_decl, k); - if (lj >= 0) { - size_t const j = static_cast(lj); - type = linear_row_type_from_bounds(constraint_lower_bounds[j], constraint_upper_bounds[j]); - } - } - mps_file << " " << type << " " << row_name << "\n"; - } + for (size_t k = 0; k < static_cast(n_constraints); ++k) { + std::string row_name = + k < problem_.get_row_names().size() ? problem_.get_row_names()[k] : "R" + std::to_string(k); + char const type = linear_row_type_from_bounds(constraint_lower_bounds[k], constraint_upper_bounds[k]); + mps_file << " " << type << " " << row_name << "\n"; + } + for (size_t q = 0; q < quadratic_constraints.size(); ++q) { + const auto& qc = quadratic_constraints[q]; + std::string row_name = + qc.constraint_row_name.empty() ? "QC" + std::to_string(q) : qc.constraint_row_name; + // Quadratic rows are currently restricted to MPS 'L' (<=). + mps_file << " L " << row_name << "\n"; } // COLUMNS section @@ -340,8 +257,7 @@ void mps_writer_t::write(const std::string& mps_file_path) // iterate over the constraint matrix and add the nonzeros to the integral and continuous col_nnzs maps for (size_t csr_row = 0; csr_row < (size_t)n_constraints; csr_row++) { - const i_t row_id = - qc_rows_separated ? problem_.get_linear_constraint_mps_indices()[csr_row] : static_cast(csr_row); + const i_t row_id = static_cast(csr_row); for (size_t k = (size_t)constraint_matrix_offsets[csr_row]; k < (size_t)constraint_matrix_offsets[csr_row + 1]; k++) { @@ -357,8 +273,9 @@ void mps_writer_t::write(const std::string& mps_file_path) // Quadratic constraint rows omit linear coefficients from global A; add them from QC bundles. if (problem_.has_quadratic_constraints()) { - for (const auto& qc : problem_.get_quadratic_constraints()) { - const size_t row_id = static_cast(qc.constraint_row_index); + for (size_t q = 0; q < quadratic_constraints.size(); ++q) { + const auto& qc = quadratic_constraints[q]; + const size_t row_id = static_cast(n_constraints) + q; for (size_t t = 0; t < qc.linear_indices.size(); ++t) { size_t var = static_cast(qc.linear_indices[t]); f_t val = qc.linear_values[t]; @@ -405,11 +322,14 @@ void mps_writer_t::write(const std::string& mps_file_path) : "C" + std::to_string(var_id); for (auto& nnz : nnzs) { std::string row_name; - if (qc_rows_separated && - static_cast(nnz.first) < problem_.get_mps_all_constraint_row_names().size()) { - row_name = problem_.get_mps_all_constraint_row_names()[nnz.first]; - } else if (static_cast(nnz.first) < problem_.get_row_names().size()) { + if (static_cast(nnz.first) < problem_.get_row_names().size()) { row_name = problem_.get_row_names()[nnz.first]; + } else if (static_cast(nnz.first) < + static_cast(n_constraints) + quadratic_constraints.size()) { + const size_t q = static_cast(nnz.first) - static_cast(n_constraints); + row_name = quadratic_constraints[q].constraint_row_name.empty() + ? "QC" + std::to_string(q) + : quadratic_constraints[q].constraint_row_name; } else { row_name = "R" + std::to_string(nnz.first); } @@ -427,56 +347,28 @@ void mps_writer_t::write(const std::string& mps_file_path) // RHS section mps_file << "RHS\n"; - size_t qc_idx_rhs = 0; - for (size_t k = 0; k < (size_t)n_mps_declaration_rows; ++k) { - std::string row_name; + for (size_t k = 0; k < static_cast(n_constraints); ++k) { + std::string row_name = + k < problem_.get_row_names().size() ? problem_.get_row_names()[k] : "R" + std::to_string(k); f_t rhs{0}; - - if (!qc_rows_separated) { - row_name = k < problem_.get_row_names().size() ? problem_.get_row_names()[k] - : "R" + std::to_string(k); - if (constraint_bounds.size() > 0) - rhs = constraint_bounds[k]; - else if (std::isinf(constraint_lower_bounds[k])) { - rhs = constraint_upper_bounds[k]; - } else if (std::isinf(constraint_upper_bounds[k])) { - rhs = constraint_lower_bounds[k]; - } else { - rhs = constraint_lower_bounds[k]; - } + if (constraint_bounds.size() > 0) + rhs = constraint_bounds[k]; + else if (std::isinf(constraint_lower_bounds[k])) { + rhs = constraint_upper_bounds[k]; + } else if (std::isinf(constraint_upper_bounds[k])) { + rhs = constraint_lower_bounds[k]; } else { - const auto& alln = problem_.get_mps_all_constraint_row_names(); - row_name = alln[k]; - while (qc_idx_rhs < qcs_by_decl_row.size() && - static_cast(qcs_by_decl_row[qc_idx_rhs]->constraint_row_index) < k) { - ++qc_idx_rhs; - } - qc_t const* const qc_match = - (qc_idx_rhs < qcs_by_decl_row.size() && - static_cast(qcs_by_decl_row[qc_idx_rhs]->constraint_row_index) == k) - ? qcs_by_decl_row[qc_idx_rhs] - : nullptr; - if (qc_match != nullptr) { - rhs = qc_match->rhs_value; - } else { - int const lj = linear_csr_row_for_mps_decl_index(linear_csr_row_for_mps_decl, k); - mps_parser_expects(lj >= 0, - error_type_t::ValidationError, - "RHS row %zu has no linear or quadratic mapping", - k); - const size_t j = static_cast(lj); - if (constraint_bounds.size() > 0) - rhs = constraint_bounds[j]; - else if (std::isinf(constraint_lower_bounds[j])) { - rhs = constraint_upper_bounds[j]; - } else if (std::isinf(constraint_upper_bounds[j])) { - rhs = constraint_lower_bounds[j]; - } else { - rhs = constraint_lower_bounds[j]; - } - } + rhs = constraint_lower_bounds[k]; } - + if (std::isfinite(rhs) && rhs != 0.0) { + mps_file << " RHS1 " << row_name << " " << rhs << "\n"; + } + } + for (size_t q = 0; q < quadratic_constraints.size(); ++q) { + const auto& qc = quadratic_constraints[q]; + std::string row_name = + qc.constraint_row_name.empty() ? "QC" + std::to_string(q) : qc.constraint_row_name; + const f_t rhs = qc.rhs_value; if (std::isfinite(rhs) && rhs != 0.0) { mps_file << " RHS1 " << row_name << " " << rhs << "\n"; } diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index 3704eac8db..899aa069f9 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -961,13 +961,6 @@ TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) ASSERT_EQ(1u, model.get_row_names().size()); EXPECT_EQ("LIN0", model.get_row_names()[0]); EXPECT_EQ('L', model.get_row_types()[0]); - ASSERT_EQ(3, model.get_mps_declaration_constraint_row_count()); - ASSERT_EQ(3u, model.get_mps_all_constraint_row_names().size()); - EXPECT_EQ("LIN0", model.get_mps_all_constraint_row_names()[0]); - EXPECT_EQ("QC0", model.get_mps_all_constraint_row_names()[1]); - EXPECT_EQ("QC1", model.get_mps_all_constraint_row_names()[2]); - ASSERT_EQ(1u, model.get_linear_constraint_mps_indices().size()); - EXPECT_EQ(0, model.get_linear_constraint_mps_indices()[0]); // LIN0: 2*x1 + x2 ≤ 15 (linear row only; not duplicated in quadratic_constraints) EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), @@ -1016,19 +1009,10 @@ TEST(qps_parser, qcqp_p0033_mps_sections) EXPECT_EQ(33, model.get_n_variables()); ASSERT_EQ(12u, model.get_row_types().size()); ASSERT_EQ(12u, model.get_row_names().size()); - ASSERT_EQ(16, model.get_mps_declaration_constraint_row_count()); - ASSERT_EQ(16u, model.get_mps_all_constraint_row_names().size()); - const auto& rnames = model.get_mps_all_constraint_row_names(); - auto qc1_it = std::find(rnames.begin(), rnames.end(), std::string("QC1")); - ASSERT_NE(qc1_it, rnames.end()); - const int qc1_row = static_cast(qc1_it - rnames.begin()); - - std::vector coeff; - std::vector vars; const auto& qcs = model.get_quadratic_constraints(); ASSERT_EQ(4u, qcs.size()); - EXPECT_EQ(qc1_row, qcs[0].constraint_row_index); + EXPECT_EQ(12, qcs[0].constraint_row_index); ASSERT_EQ(1u, qcs[0].linear_values.size()); EXPECT_DOUBLE_EQ(1.0, qcs[0].linear_values[0]); @@ -1235,14 +1219,6 @@ void compare_data_models(const mps_data_model_t& original, } } - EXPECT_EQ(original.get_mps_declaration_constraint_row_count(), - reloaded.get_mps_declaration_constraint_row_count()); - EXPECT_EQ(original.get_linear_constraint_mps_indices(), reloaded.get_linear_constraint_mps_indices()); - ASSERT_EQ(original.get_mps_all_constraint_row_names().size(), - reloaded.get_mps_all_constraint_row_names().size()); - for (size_t i = 0; i < original.get_mps_all_constraint_row_names().size(); ++i) { - EXPECT_EQ(original.get_mps_all_constraint_row_names()[i], reloaded.get_mps_all_constraint_row_names()[i]); - } } TEST(mps_roundtrip, linear_programming_basic) diff --git a/cpp/src/pdlp/cpu_optimization_problem.cpp b/cpp/src/pdlp/cpu_optimization_problem.cpp index 4bbe5ef545..e4018a290a 100644 --- a/cpp/src/pdlp/cpu_optimization_problem.cpp +++ b/cpp/src/pdlp/cpu_optimization_problem.cpp @@ -135,50 +135,11 @@ void cpu_optimization_problem_t::set_quadratic_objective_matrix( template void cpu_optimization_problem_t::set_quadratic_constraints( - std::vector::mps_quadratic_constraint_t> - constraints) + std::vector::quadratic_constraint_t> constraints) { quadratic_constraints_ = std::move(constraints); } -template -void cpu_optimization_problem_t::set_linear_constraint_mps_indices(std::vector indices) -{ - linear_constraint_mps_indices_ = std::move(indices); -} - -template -void cpu_optimization_problem_t::set_mps_declaration_constraint_row_count(i_t count) -{ - mps_declaration_constraint_row_count_ = count; -} - -template -void cpu_optimization_problem_t::set_mps_all_constraint_row_names( - std::vector names) -{ - mps_all_constraint_row_names_ = std::move(names); -} - -template -i_t cpu_optimization_problem_t::get_mps_declaration_constraint_row_count() const -{ - return mps_declaration_constraint_row_count_; -} - -template -const std::vector& cpu_optimization_problem_t::get_linear_constraint_mps_indices() const -{ - return linear_constraint_mps_indices_; -} - -template -const std::vector& cpu_optimization_problem_t::get_mps_all_constraint_row_names() - const -{ - return mps_all_constraint_row_names_; -} - template void cpu_optimization_problem_t::set_variable_lower_bounds( const f_t* variable_lower_bounds, i_t size) @@ -541,7 +502,7 @@ bool cpu_optimization_problem_t::has_quadratic_objective() const } template -const std::vector::mps_quadratic_constraint_t>& +const std::vector::quadratic_constraint_t>& cpu_optimization_problem_t::get_quadratic_constraints() const { return quadratic_constraints_; @@ -682,18 +643,10 @@ cpu_optimization_problem_t::to_optimization_problem(raft::handle_t con if (!quadratic_constraints_.empty()) { gpu_problem->set_quadratic_constraints( - std::vector::mps_quadratic_constraint_t>( + std::vector::quadratic_constraint_t>( quadratic_constraints_)); } - if (mps_declaration_constraint_row_count_ > 0) { - gpu_problem->set_linear_constraint_mps_indices( - std::vector(linear_constraint_mps_indices_)); - gpu_problem->set_mps_declaration_constraint_row_count(mps_declaration_constraint_row_count_); - gpu_problem->set_mps_all_constraint_row_names( - std::vector(mps_all_constraint_row_names_)); - } - // Set variable bounds if (!variable_lower_bounds_.empty()) { gpu_problem->set_variable_lower_bounds(variable_lower_bounds_.data(), @@ -813,23 +766,8 @@ void cpu_optimization_problem_t::write_to_mps(const std::string& mps_f false); } - if (has_quadratic_constraints()) { - data_model_view.set_quadratic_constraints( - std::vector::mps_quadratic_constraint_t>( - get_quadratic_constraints())); - } - - if (get_mps_declaration_constraint_row_count() > 0) { - data_model_view.set_mps_declaration_constraint_row_count(get_mps_declaration_constraint_row_count()); - if (!get_linear_constraint_mps_indices().empty()) { - data_model_view.set_linear_constraint_mps_indices(get_linear_constraint_mps_indices().data(), - static_cast( - get_linear_constraint_mps_indices().size())); - } - if (!get_mps_all_constraint_row_names().empty()) { - data_model_view.set_mps_all_constraint_row_names( - std::vector(get_mps_all_constraint_row_names())); - } + if (!quadratic_constraints_.empty()) { + data_model_view.set_quadratic_constraints(quadratic_constraints_); } cuopt::mps_parser::write_mps(data_model_view, mps_file_path); @@ -847,10 +785,6 @@ bool cpu_optimization_problem_t::is_equivalent( if (maximize_ != other.get_sense()) return false; if (n_vars_ != other.get_n_variables()) return false; if (n_constraints_ != other.get_n_constraints()) return false; - if (get_mps_declaration_constraint_row_count() != other.get_mps_declaration_constraint_row_count()) - return false; - if (get_linear_constraint_mps_indices() != other.get_linear_constraint_mps_indices()) return false; - if (get_mps_all_constraint_row_names() != other.get_mps_all_constraint_row_names()) return false; if (std::abs(objective_scaling_factor_ - other.get_objective_scaling_factor()) > 1e-9) return false; if (std::abs(objective_offset_ - other.get_objective_offset()) > 1e-9) return false; diff --git a/cpp/src/pdlp/optimization_problem.cu b/cpp/src/pdlp/optimization_problem.cu index b3f71bd92c..634fd86c60 100644 --- a/cpp/src/pdlp/optimization_problem.cu +++ b/cpp/src/pdlp/optimization_problem.cu @@ -98,10 +98,7 @@ optimization_problem_t::optimization_problem_t( problem_category_{other.get_problem_category()}, var_names_{other.get_variable_names()}, row_names_{other.get_row_names()}, - quadratic_constraints_{other.get_quadratic_constraints()}, - linear_constraint_mps_indices_{other.get_linear_constraint_mps_indices()}, - mps_declaration_constraint_row_count_{other.get_mps_declaration_constraint_row_count()}, - mps_all_constraint_row_names_{other.get_mps_all_constraint_row_names()} + quadratic_constraints_{other.get_quadratic_constraints()} { } @@ -203,49 +200,12 @@ void optimization_problem_t::set_quadratic_objective_matrix( template void optimization_problem_t::set_quadratic_constraints( - std::vector::mps_quadratic_constraint_t> + std::vector::quadratic_constraint_t> constraints) { quadratic_constraints_ = std::move(constraints); } -template -void optimization_problem_t::set_linear_constraint_mps_indices(std::vector indices) -{ - linear_constraint_mps_indices_ = std::move(indices); -} - -template -void optimization_problem_t::set_mps_declaration_constraint_row_count(i_t count) -{ - mps_declaration_constraint_row_count_ = count; -} - -template -void optimization_problem_t::set_mps_all_constraint_row_names(std::vector names) -{ - mps_all_constraint_row_names_ = std::move(names); -} - -template -i_t optimization_problem_t::get_mps_declaration_constraint_row_count() const -{ - return mps_declaration_constraint_row_count_; -} - -template -const std::vector& optimization_problem_t::get_linear_constraint_mps_indices() const -{ - return linear_constraint_mps_indices_; -} - -template -const std::vector& optimization_problem_t::get_mps_all_constraint_row_names() - const -{ - return mps_all_constraint_row_names_; -} - template void optimization_problem_t::set_variable_lower_bounds(const f_t* variable_lower_bounds, i_t size) @@ -598,7 +558,7 @@ bool optimization_problem_t::has_quadratic_objective() const } template -const std::vector::mps_quadratic_constraint_t>& +const std::vector::quadratic_constraint_t>& optimization_problem_t::get_quadratic_constraints() const { return quadratic_constraints_; @@ -882,23 +842,8 @@ void optimization_problem_t::write_to_mps(const std::string& mps_file_ is_symmetrized); } - if (has_quadratic_constraints()) { - data_model_view.set_quadratic_constraints( - std::vector::mps_quadratic_constraint_t>( - get_quadratic_constraints())); - } - - if (get_mps_declaration_constraint_row_count() > 0) { - data_model_view.set_mps_declaration_constraint_row_count(get_mps_declaration_constraint_row_count()); - if (!get_linear_constraint_mps_indices().empty()) { - data_model_view.set_linear_constraint_mps_indices(get_linear_constraint_mps_indices().data(), - static_cast( - get_linear_constraint_mps_indices().size())); - } - if (!get_mps_all_constraint_row_names().empty()) { - data_model_view.set_mps_all_constraint_row_names( - std::vector(get_mps_all_constraint_row_names())); - } + if (!quadratic_constraints_.empty()) { + data_model_view.set_quadratic_constraints(quadratic_constraints_); } cuopt::mps_parser::write_mps(data_model_view, mps_file_path); @@ -1111,11 +1056,6 @@ bool optimization_problem_t::is_equivalent( if (maximize_ != other.maximize_) { return false; } if (n_vars_ != other.n_vars_) { return false; } if (n_constraints_ != other.n_constraints_) { return false; } - if (linear_constraint_mps_indices_ != other.linear_constraint_mps_indices_) { return false; } - if (mps_declaration_constraint_row_count_ != other.mps_declaration_constraint_row_count_) { - return false; - } - if (mps_all_constraint_row_names_ != other.mps_all_constraint_row_names_) { return false; } if (objective_scaling_factor_ != other.objective_scaling_factor_) { return false; } if (objective_offset_ != other.objective_offset_) { return false; } if (problem_category_ != other.problem_category_) { return false; } @@ -1260,10 +1200,6 @@ bool optimization_problem_t::is_equivalent( if (maximize_ != other.get_sense()) return false; if (n_vars_ != other.get_n_variables()) return false; if (n_constraints_ != other.get_n_constraints()) return false; - if (get_mps_declaration_constraint_row_count() != other.get_mps_declaration_constraint_row_count()) - return false; - if (get_linear_constraint_mps_indices() != other.get_linear_constraint_mps_indices()) return false; - if (get_mps_all_constraint_row_names() != other.get_mps_all_constraint_row_names()) return false; if (std::abs(objective_scaling_factor_ - other.get_objective_scaling_factor()) > 1e-9) return false; if (std::abs(objective_offset_ - other.get_objective_offset()) > 1e-9) return false; From 8ff6ce1fb78e1bcd0992ffd7904e32ec880cdf6c Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 22 Apr 2026 12:23:14 -0700 Subject: [PATCH 12/68] minor adjustment for parser --- cpp/libmps_parser/src/mps_parser.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index e77ed915fb..2921f9dc09 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #ifdef MPS_PARSER_WITH_BZIP2 #include @@ -271,11 +272,13 @@ ObjSenseType convert_to_obj_sense(const std::string& str) template void mps_parser_t::fill_problem(mps_data_model_t& problem) { - // count the row indices that are quadratic constraints + // Row indices that have QCMATRIX blocks (quadratic rows follow linear rows in ROWS under + // our MPS section rules; names are not required to be QC0..QCN) std::unordered_set quadratic_row_ids{}; for (const auto& block : qcmatrix_blocks_) { quadratic_row_ids.insert(block.constraint_row_id); } + const auto is_quadratic_row = [&quadratic_row_ids](i_t row) { return quadratic_row_ids.count(row); }; { std::vector h_offsets{}, h_indices{}; @@ -286,7 +289,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) for (i_t i = 0; i < (i_t)A_indices.size(); ++i) { // Quadratic constraint rows are omitted from the linear CSR; linear pieces live in each // quadratic_constraint_t bundle. - if (quadratic_row_ids.count(i)) { continue; } + if (is_quadratic_row(i)) { continue; } ++num_linear_rows; for (const auto& idx_itr : A_indices[i]) { h_indices.push_back(idx_itr); @@ -334,7 +337,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) std::vector b_compacted{}; b_compacted.reserve(b_values.size()); for (i_t i = 0; i < (i_t)b_values.size(); ++i) { - if (!quadratic_row_ids.count(i)) { b_compacted.push_back(b_values[i]); } + if (!is_quadratic_row(i)) { b_compacted.push_back(b_values[i]); } } problem.set_constraint_bounds(b_compacted.data(), static_cast(b_compacted.size())); problem.set_objective_coefficients(c_values.data(), c_values.size()); @@ -363,7 +366,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) std::vector h_constraint_lower_bounds{}; std::vector h_constraint_upper_bounds{}; for (i_t i = 0; i < (i_t)row_types.size(); ++i) { - if (quadratic_row_ids.count(i)) { continue; } + if (is_quadratic_row(i)) { continue; } if (row_types[i] == Equality) { h_constraint_lower_bounds.push_back(b_values[i]); h_constraint_upper_bounds.push_back(b_values[i]); @@ -510,6 +513,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) for (i_t row = 0; row < num_rows; ++row) { for (const auto& [col, val] : csr_data[row]) { + // While the mps format expects to optimize for 0.5 xT Q x, cuopt optimizes for xT Q xExpand commentComment on line L488 + // so we have to multiply the value by value_scale=0.5 to get the correct value. result.values.push_back(val * value_scale); result.indices.push_back(col); } @@ -553,8 +558,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs). // Bundle row metadata, row-linear coefficients (from COLUMNS), rhs, and quadratic part together. constexpr f_t k_qcmatrix_value_scale = f_t(1); - const i_t linear_row_count = static_cast(row_types.size() - quadratic_row_ids.size()); - i_t quadratic_row_id = 0; + const i_t linear_row_count = static_cast(row_types.size() - quadratic_row_ids.size()); + i_t quadratic_row_id = 0; for (const auto& block : qcmatrix_blocks_) { auto csr_result = build_csr_via_transpose( block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); @@ -592,7 +597,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) linear_row_names.reserve(row_names.size()); row_types_linear.reserve(row_names.size()); for (size_t i = 0; i < row_names.size(); ++i) { - if (!quadratic_row_ids.count(static_cast(i))) { + if (!is_quadratic_row(static_cast(i))) { linear_row_names.push_back(row_names[i]); row_types_linear.push_back(static_cast(row_types[i])); } From 6617f94396eb96eb8e938dfb5e073734f560a999 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 23 Apr 2026 07:55:21 -0700 Subject: [PATCH 13/68] Add bridging for mps model to user_problem model --- cpp/src/pdlp/solve.cu | 13 ++- cpp/src/pdlp/translate.hpp | 185 ++++++++++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 6 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 29a7f32db6..9087e732b9 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1378,12 +1378,17 @@ optimization_problem_solution_t solve_lp( // This needs to be called before pdlp is initialized init_handler(op_problem.get_handle_ptr()); - if (op_problem.has_quadratic_objective()) { - CUOPT_LOG_INFO("Problem has a quadratic objective. Using Barrier."); + if (op_problem.has_quadratic_objective() || op_problem.has_quadratic_constraints()) { + if (op_problem.has_quadratic_objective()) { + CUOPT_LOG_INFO("Problem has a quadratic objective. Using Barrier."); + } + if (op_problem.has_quadratic_constraints()) { + CUOPT_LOG_INFO("Problem has quadratic constraints. Using Barrier with SOC conversion."); + } settings.method = method_t::Barrier; settings.presolver = presolver_t::None; - // check for sense of the problem - if (op_problem.get_sense()) { + // Quadratic objective support is minimization-only. + if (op_problem.has_quadratic_objective() && op_problem.get_sense()) { CUOPT_LOG_ERROR("Quadratic problems must be minimized"); return optimization_problem_solution_t(pdlp_termination_status_t::NumericalError, op_problem.get_handle_ptr()->get_stream()); diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index b143a206d4..1628290c5a 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -7,6 +7,7 @@ #pragma once +#include #include #include @@ -34,8 +35,6 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( csr_A.j = std::vector(cuopt::host_copy(model.variables, handle_ptr->get_stream())); csr_A.row_start = std::vector(cuopt::host_copy(model.offsets, handle_ptr->get_stream())); - csr_A.to_compressed_col(user_problem.A); - user_problem.rhs.resize(m); user_problem.row_sense.resize(m); user_problem.range_rows.clear(); @@ -105,6 +104,188 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( user_problem.Q_indices = model.Q_indices; user_problem.Q_values = model.Q_values; + if (model.original_problem_ptr->has_quadratic_constraints()) { + const auto& qcs = model.original_problem_ptr->get_quadratic_constraints(); + cuopt_expects(!qcs.empty(), + error_type_t::ValidationError, + "Quadratic-constraint flag is set, but no constraints were provided"); + + const i_t original_rows = static_cast(user_problem.num_rows); + const f_t tol = f_t(1e-16); + + // SOC: Q is n×n diagonal CSR (offsets length n+1). Exactly q_n = nnz on the main diagonal, at + // q_n distinct variable indices: one −1 (head) and (q_n−1) +1 (tails). Lifting: q_n rows, each + // with −1 in one column; the **first** row must be the head (variable with Q = −1); order of + // remaining rows (+1 diagonals) is unconstrained (CSR row scan order). + + const i_t old_nnz = csr_A.row_start[original_rows]; + std::vector row_cone_dims{}; + row_cone_dims.reserve(qcs.size()); + + for (const auto& qc : qcs) { + cuopt_expects(qc.constraint_row_type == 'L', + error_type_t::ValidationError, + "Only <= quadratic constraints are supported for SOC conversion"); + cuopt_expects(qc.linear_values.empty(), + error_type_t::ValidationError, + "SOC conversion currently requires zero linear terms in quadratic constraints"); + cuopt_expects(qc.rhs_value < tol && qc.rhs_value > -tol, + error_type_t::ValidationError, + "SOC conversion currently requires rhs = 0 for quadratic constraints"); + + cuopt_expects(qc.quadratic_offsets.size() >= 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' has invalid CSR offsets (need at least 2 entries)", + qc.constraint_row_name.c_str()); + cuopt_expects( + qc.quadratic_values.size() == qc.quadratic_indices.size(), + error_type_t::ValidationError, + "Quadratic constraint '%s' quadratic_values and quadratic_indices length mismatch for CSR Q", + qc.constraint_row_name.c_str()); + + const i_t q_n = static_cast(qc.quadratic_values.size()); + cuopt_expects(q_n >= 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC must have at least 2 diagonal entries in Q (nnz " + "%d)", + qc.constraint_row_name.c_str(), + static_cast(q_n)); + + cuopt_expects( + qc.quadratic_offsets.size() == static_cast(n) + 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q must be n×n in CSR: expected %zu CSR row pointers (offsets " + "length n+1), got %zu (n = %d)", + qc.constraint_row_name.c_str(), + static_cast(n) + 1, + qc.quadratic_offsets.size(), + static_cast(n)); + cuopt_expects( + qc.quadratic_offsets[static_cast(n)] == q_n, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q last CSR offset %d must equal number of nonzeros (nnz) %d for " + "this diagonal Q", + qc.constraint_row_name.c_str(), + static_cast(qc.quadratic_offsets[static_cast(n)]), + static_cast(q_n)); + cuopt_expects( + qc.quadratic_offsets[0] == 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q CSR offsets[0] must be 0", + qc.constraint_row_name.c_str()); + + // Verify Q: n×n CSR, diagonal entries only, Lorentz pattern, then build the lift. + // Scan each row r: empty or one nnz on (r,r) with value -1 (head) or +1 (tail); + // tail order follows this scan; no requirement that diagonal indices be sorted. + i_t head = static_cast(-1); + i_t n_head_m = 0; + std::vector tail_row_vars{}; + tail_row_vars.reserve(static_cast(q_n - 1)); + + for (i_t r = 0; r < n; ++r) { + const i_t p_beg = qc.quadratic_offsets[static_cast(r)]; + const i_t p_end = qc.quadratic_offsets[static_cast(r + 1)]; + + if (p_beg == p_end) { continue; } + + cuopt_expects( + p_beg + 1 == p_end, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: expected at most one stored entry on the diagonal per " + "row (got end - beg = %d)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(p_end - p_beg)); + + const i_t col = qc.quadratic_indices[static_cast(p_beg)]; + const f_t v = qc.quadratic_values[static_cast(p_beg)]; + cuopt_expects( + col == r, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: only main diagonal (j,j) entries are allowed; got " + "column %d", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(col)); + + if (v > f_t(-1) - tol && v < f_t(-1) + tol) { + ++n_head_m; + head = r; + } else if (v > f_t(1) - tol && v < f_t(1) + tol) { + tail_row_vars.push_back(r); + } else { + cuopt_expects( + false, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: diagonal for SOC must be -1 (head) or +1 (tail); got " + "%g", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(v)); + } + } + cuopt_expects( + n_head_m == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected exactly one diagonal with value -1 (cone head), " + "found %d", + qc.constraint_row_name.c_str(), + static_cast(n_head_m)); + cuopt_expects( + static_cast(tail_row_vars.size()) == q_n - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonals with value +1 (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_n - 1), + tail_row_vars.size()); + cuopt_expects( + head >= 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: internal error (head index invalid)", + qc.constraint_row_name.c_str()); + + row_cone_dims.push_back(q_n); + dual_simplex::csr_matrix_t lift_block(q_n, n, q_n); + for (i_t t = 0; t <= q_n; ++t) { + lift_block.row_start[t] = t; + } + + // One lift row per cone component: -1 in column head, then -1 in each tail column + // (order matches tail_row_vars from the Q scan). + lift_block.j[0] = head; + lift_block.x[0] = f_t(-1); + for (i_t t = 0; t < q_n - 1; ++t) { + lift_block.j[static_cast(t) + 1U] = tail_row_vars[static_cast(t)]; + lift_block.x[static_cast(t) + 1U] = f_t(-1); + } + cuopt_expects(csr_A.append_rows(lift_block) == 0, + error_type_t::RuntimeError, + "Internal error while appending SOC lifting rows to CSR A"); + } + + // Update user_problem to include the new SOC rows + const i_t next_row = static_cast(csr_A.m); + const i_t lifted_rows = next_row - original_rows; + const i_t new_nnz = old_nnz + lifted_rows; + cuopt_expects(csr_A.row_start[next_row] == new_nnz, + error_type_t::RuntimeError, + "Internal error while building SOC lifting rows in CSR A"); + + user_problem.rhs.resize(next_row, f_t(0)); + user_problem.row_sense.resize(next_row, 'E'); + if (user_problem.row_names.size() == static_cast(original_rows)) { + for (i_t r = original_rows; r < next_row; ++r) { + user_problem.row_names.push_back("_CUOPT_soc_row_" + std::to_string(r - original_rows)); + } + } + user_problem.num_rows = next_row; + + user_problem.cone_row_start = original_rows; + user_problem.second_order_cone_row_dims = std::move(row_cone_dims); + } + + csr_A.to_compressed_col(user_problem.A); + return user_problem; } From 62380e49ca8f9827ee6020773aaa2e1d3891a243 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 23 Apr 2026 09:16:18 -0700 Subject: [PATCH 14/68] Update QCQP dataset for parser check and address minor issues --- .../optimization_problem_interface.hpp | 12 +- .../include/mps_parser/mps_data_model.hpp | 19 +- cpp/libmps_parser/src/mps_data_model.cpp | 65 ++---- cpp/libmps_parser/src/mps_parser.cpp | 19 +- cpp/libmps_parser/tests/mps_parser_test.cpp | 31 +-- datasets/qcqp/QC_Test_1.mps | 30 +++ datasets/qcqp/p0033_qc1.mps | 214 ++++++++++++++++++ 7 files changed, 282 insertions(+), 108 deletions(-) create mode 100644 datasets/qcqp/QC_Test_1.mps create mode 100644 datasets/qcqp/p0033_qc1.mps diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp index 27a106037b..eb0850ba44 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp @@ -73,12 +73,8 @@ class optimization_problem_interface_t { /** * @brief Store quadratic constraints for MPS round-trip (linear + Q parts per QC row). - * @note Default implementation ignores; GPU/CPU implementations persist for write_to_mps. */ - virtual void set_quadratic_constraints(std::vector constraints) - { - (void)constraints; - } + virtual void set_quadratic_constraints(std::vector constraints) = 0; template >> void set_quadratic_constraints(const std::vector& constraints) { @@ -103,11 +99,7 @@ class optimization_problem_interface_t { virtual bool has_quadratic_constraints() const = 0; /** @brief Quadratic constraints for MPS export (empty if none). */ - virtual const std::vector& get_quadratic_constraints() const - { - static const std::vector k_empty{}; - return k_empty; - } + virtual const std::vector& get_quadratic_constraints() const = 0; // ============================================================================ // Setters (accept both CPU and GPU pointers) diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 69909fb3a6..c4c8b56e28 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -287,23 +288,21 @@ class mps_data_model_t { /** * @brief Append one complete quadratic constraint (row + linear + rhs + quadratic Q). + * @param linear_values, linear_indices Same nnz; empty spans for a purely quadratic row (rare). + * @param quadratic_values, quadratic_indices CSR nnz; may be empty if Q is empty. + * @param quadratic_offsets CSR row starts; must be non-empty. * @param constraint_row_type MPS ROWS type; must be 'L'. 'G' and 'E' quadratic rows are not * supported. */ void append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, char constraint_row_type, - const f_t* linear_values, - i_t linear_nnz, - const i_t* linear_indices, - i_t linear_indices_nnz, + std::span linear_values, + std::span linear_indices, f_t rhs_value, - const f_t* quadratic_values, - i_t quadratic_size_values, - const i_t* quadratic_indices, - i_t quadratic_size_indices, - const i_t* quadratic_offsets, - i_t quadratic_size_offsets); + std::span quadratic_values, + std::span quadratic_indices, + std::span quadratic_offsets); const std::vector& get_quadratic_constraints() const; diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 1ecaf47b2c..f5db136a58 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -224,17 +224,12 @@ template void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, char constraint_row_type, - const f_t* linear_values, - i_t linear_nnz, - const i_t* linear_indices, - i_t linear_indices_nnz, + std::span linear_values, + std::span linear_indices, f_t rhs_value, - const f_t* quadratic_values, - i_t quadratic_size_values, - const i_t* quadratic_indices, - i_t quadratic_size_indices, - const i_t* quadratic_offsets, - i_t quadratic_size_offsets) + std::span quadratic_values, + std::span quadratic_indices, + std::span quadratic_offsets) { mps_parser_expects( constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); @@ -245,28 +240,13 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ "Only 'L' is supported for convex quadratic constraints.", constraint_row_type); - mps_parser_expects(linear_nnz == linear_indices_nnz, - error_type_t::ValidationError, - "linear_values and linear_indices must have the same nnz count"); - if (linear_nnz != 0) { - mps_parser_expects( - linear_values != nullptr && linear_indices != nullptr, - error_type_t::ValidationError, - "linear_values and linear_indices cannot be null when linear_nnz > 0"); - } - - if (quadratic_size_values != 0) { - mps_parser_expects( - quadratic_values != nullptr, error_type_t::ValidationError, "quadratic_values cannot be null"); - } mps_parser_expects( - quadratic_offsets != nullptr, error_type_t::ValidationError, "quadratic_offsets cannot be null"); - if (quadratic_size_indices != 0) { - mps_parser_expects( - quadratic_indices != nullptr, error_type_t::ValidationError, "quadratic_indices cannot be null"); - } + linear_values.size() == linear_indices.size(), + error_type_t::ValidationError, + "linear_values and linear_indices must have the same length"); + mps_parser_expects( - quadratic_size_offsets > 0, error_type_t::ValidationError, "quadratic_size_offsets cannot be empty"); + !quadratic_offsets.empty(), error_type_t::ValidationError, "quadratic_offsets cannot be empty"); quadratic_constraint_t qc; qc.constraint_row_index = constraint_row_index; @@ -274,26 +254,11 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ qc.constraint_row_type = constraint_row_type; qc.rhs_value = rhs_value; - qc.linear_values.resize(linear_nnz); - qc.linear_indices.resize(linear_nnz); - if (linear_nnz > 0) { - std::copy(linear_values, linear_values + linear_nnz, qc.linear_values.data()); - std::copy(linear_indices, linear_indices + linear_nnz, qc.linear_indices.data()); - } - - qc.quadratic_values.resize(quadratic_size_values); - if (quadratic_size_values > 0) { - std::copy( - quadratic_values, quadratic_values + quadratic_size_values, qc.quadratic_values.data()); - } - qc.quadratic_indices.resize(quadratic_size_indices); - if (quadratic_size_indices > 0) { - std::copy( - quadratic_indices, quadratic_indices + quadratic_size_indices, qc.quadratic_indices.data()); - } - qc.quadratic_offsets.resize(quadratic_size_offsets); - std::copy( - quadratic_offsets, quadratic_offsets + quadratic_size_offsets, qc.quadratic_offsets.data()); + qc.linear_values.assign(linear_values.begin(), linear_values.end()); + qc.linear_indices.assign(linear_indices.begin(), linear_indices.end()); + qc.quadratic_values.assign(quadratic_values.begin(), quadratic_values.end()); + qc.quadratic_indices.assign(quadratic_indices.begin(), quadratic_indices.end()); + qc.quadratic_offsets.assign(quadratic_offsets.begin(), quadratic_offsets.end()); quadratic_constraints_.push_back(std::move(qc)); } diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 2921f9dc09..b446db5009 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -569,25 +569,16 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) error_type_t::ValidationError, "QCMATRIX row index %d is out of range for constraints", static_cast(row_id)); - const i_t linear_nnz = static_cast(A_indices[row_id].size()); - const f_t* linear_val = linear_nnz > 0 ? A_values[row_id].data() : nullptr; - const i_t* linear_idx = linear_nnz > 0 ? A_indices[row_id].data() : nullptr; - problem.append_quadratic_constraint( linear_row_count + quadratic_row_id, row_names[row_id], static_cast(row_types[row_id]), - linear_val, - linear_nnz, - linear_idx, - linear_nnz, + A_values[row_id], + A_indices[row_id], b_values[row_id], - csr_result.values.data(), - static_cast(csr_result.values.size()), - csr_result.indices.data(), - static_cast(csr_result.indices.size()), - csr_result.offsets.data(), - static_cast(csr_result.offsets.size())); + csr_result.values, + csr_result.indices, + csr_result.offsets); ++quadratic_row_id; } diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index 899aa069f9..b8ddc243aa 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -883,20 +883,8 @@ TEST(qps_parser, qcmatrix_append_api) const std::vector qc0_offsets = {0, 2, 4}; const std::vector qc0_linear_values = {1.0, 1.0}; const std::vector qc0_linear_indices = {0, 1}; - model.append_quadratic_constraint(0, - "QC0", - 'L', - qc0_linear_values.data(), - qc0_linear_values.size(), - qc0_linear_indices.data(), - qc0_linear_indices.size(), - 5.0, - qc0_values.data(), - qc0_values.size(), - qc0_indices.data(), - qc0_indices.size(), - qc0_offsets.data(), - qc0_offsets.size()); + model.append_quadratic_constraint( + 0, "QC0", 'L', qc0_linear_values, qc0_linear_indices, 5.0, qc0_values, qc0_indices, qc0_offsets); // QC1: [[4, 1], [1, 6]] const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; @@ -907,17 +895,12 @@ TEST(qps_parser, qcmatrix_append_api) model.append_quadratic_constraint(1, "QC1", 'L', - qc1_linear_values.data(), - qc1_linear_values.size(), - qc1_linear_indices.data(), - qc1_linear_indices.size(), + qc1_linear_values, + qc1_linear_indices, 10.0, - qc1_values.data(), - qc1_values.size(), - qc1_indices.data(), - qc1_indices.size(), - qc1_offsets.data(), - qc1_offsets.size()); + qc1_values, + qc1_indices, + qc1_offsets); ASSERT_TRUE(model.has_quadratic_constraints()); const auto& qcs = model.get_quadratic_constraints(); diff --git a/datasets/qcqp/QC_Test_1.mps b/datasets/qcqp/QC_Test_1.mps new file mode 100644 index 0000000000..e791fdadd6 --- /dev/null +++ b/datasets/qcqp/QC_Test_1.mps @@ -0,0 +1,30 @@ +NAME QCTest +ROWS + N OBJ + L LIN0 + L QC0 + L QC1 +COLUMNS + VAR1 OBJ 0 + VAR1 LIN0 2 + VAR1 QC0 1 + VAR1 QC1 3 + VAR2 OBJ 0 + VAR2 LIN0 1 + VAR2 QC0 1 + VAR2 QC1 1 +RHS + RHS1 LIN0 15 + RHS1 QC0 5 + RHS1 QC1 10 +QCMATRIX QC0 + VAR1 VAR1 10 + VAR1 VAR2 2 + VAR2 VAR1 2 + VAR2 VAR2 2 +QCMATRIX QC1 + VAR1 VAR1 4 + VAR1 VAR2 1 + VAR2 VAR1 1 + VAR2 VAR2 6 +ENDATA diff --git a/datasets/qcqp/p0033_qc1.mps b/datasets/qcqp/p0033_qc1.mps new file mode 100644 index 0000000000..a06cec03c5 --- /dev/null +++ b/datasets/qcqp/p0033_qc1.mps @@ -0,0 +1,214 @@ +NAME p0033_qc1 +ROWS + N R100 + L R118 + L R119 + L R120 + L R121 + L R122 + L R123 + L R124 + L R125 + L R126 + L R127 + L R128 + L ZBESTROW + L QC1 + L QC2 + L QC3 + L QC4 +COLUMNS + C157 R100 171 + C157 R122 -300 + C157 R123 -300 + C158 R100 171 + C158 R126 -300 + C158 R127 -300 + C159 R100 171 + C159 R119 300 + C159 R120 -300 + C159 R121 -300 + C159 QC1 1 + C160 R100 171 + C160 R119 300 + C160 R120 -300 + C160 R121 -300 + C161 R100 163 + C161 R119 285 + C161 R120 -285 + C161 R124 -285 + C161 R125 -285 + C162 R100 162 + C162 R119 285 + C162 R120 -285 + C162 R122 -285 + C162 R123 -285 + C163 R100 163 + C163 R128 -285 + C164 R100 69 + C164 R119 265 + C164 R120 -265 + C164 R124 -265 + C164 R125 -265 + C165 R100 69 + C165 R119 265 + C165 R120 -265 + C165 R122 -265 + C165 R123 -265 + C166 R100 183 + C166 R118 -230 + C167 R100 183 + C167 R124 -230 + C167 R125 -230 + C168 R100 183 + C168 R119 230 + C168 R120 -230 + C168 R125 -230 + C169 R100 183 + C169 R119 230 + C169 R120 -230 + C169 R123 -230 + C170 R100 49 + C170 R119 190 + C170 R120 -190 + C170 R122 -190 + C170 R123 -190 + C171 R100 183 + C172 R100 258 + C172 R118 -200 + C173 R100 517 + C173 R118 -400 + C174 R100 250 + C174 R126 -200 + C174 R127 -200 + C175 R100 500 + C175 R126 -400 + C175 R127 -400 + C176 R100 250 + C176 R127 -200 + C177 R100 500 + C177 R127 -400 + C178 R100 159 + C178 R119 200 + C178 R120 -200 + C178 R124 -200 + C178 R125 -200 + C179 R100 318 + C179 R119 400 + C179 R120 -400 + C179 R124 -400 + C179 R125 -400 + C180 R100 159 + C180 R119 200 + C180 R120 -200 + C180 R125 -200 + C181 R100 318 + C181 R119 400 + C181 R120 -400 + C181 R125 -400 + C182 R100 159 + C182 R119 200 + C182 R120 -200 + C182 R122 -200 + C182 R123 -200 + C183 R100 318 + C183 R119 400 + C183 R120 -400 + C183 R122 -400 + C183 R123 -400 + C184 R100 159 + C184 R119 200 + C184 R120 -200 + C184 R123 -200 + C185 R100 318 + C185 R119 400 + C185 R120 -400 + C185 R123 -400 + C186 R100 114 + C186 R119 200 + C186 R120 -200 + C186 R121 -200 + C187 R100 228 + C187 R119 400 + C187 R120 -400 + C187 R121 -400 + C188 R100 159 + C188 R128 -200 + C189 R100 318 + C189 R128 -400 +RHS + rhs R118 -5 + rhs R119 2700 + rhs R120 -2600 + rhs R121 -100 + rhs R122 -900 + rhs R123 -1656 + rhs R124 -335 + rhs R125 -1026 + rhs R126 -5 + rhs R127 -500 + rhs R128 -270 + rhs QC1 1 + rhs QC2 2 + rhs QC3 1 + rhs QC4 1 +BOUNDS + UP bnd C157 1 + UP bnd C158 1 + UP bnd C159 1 + UP bnd C160 1 + UP bnd C161 1 + UP bnd C162 1 + UP bnd C163 1 + UP bnd C164 1 + UP bnd C165 1 + UP bnd C166 1 + UP bnd C167 1 + UP bnd C168 1 + UP bnd C169 1 + UP bnd C170 1 + UP bnd C171 1 + UP bnd C172 1 + UP bnd C173 1 + UP bnd C174 1 + UP bnd C175 1 + UP bnd C176 1 + UP bnd C177 1 + UP bnd C178 1 + UP bnd C179 1 + UP bnd C180 1 + UP bnd C181 1 + UP bnd C182 1 + UP bnd C183 1 + UP bnd C184 1 + UP bnd C185 1 + UP bnd C186 1 + UP bnd C187 1 + UP bnd C188 1 + UP bnd C189 1 +QMATRIX + C158 C158 1 + C158 C189 0.5 + C189 C158 0.5 + C189 C189 1 +QCMATRIX QC1 + C157 C157 1 + C157 C158 0.5 + C158 C157 0.5 + C158 C158 1 + C159 C159 1 + C160 C160 1 +QCMATRIX QC2 + C161 C161 2 + C162 C162 2 + C163 C163 1 +QCMATRIX QC3 + C164 C164 1 + C165 C165 1 +QCMATRIX QC4 + C166 C166 1 + C167 C167 1 + C168 C168 1 + C169 C169 1 + C171 C171 1 +ENDATA \ No newline at end of file From 969ff06f0ec37049b78065e1ffd6d307eb9e7b68 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 23 Apr 2026 10:06:15 -0700 Subject: [PATCH 15/68] modified test check for the MPS parser --- cpp/libmps_parser/tests/mps_parser_test.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index b8ddc243aa..81a51b5dda 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -1320,7 +1320,9 @@ TEST(mps_roundtrip, qcqp_p0033_qc1) std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; - std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; + std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; + std::string temp_file_2 = "/tmp/mps_roundtrip_p0033_qc1_r2.mps"; + std::string temp_file_3 = "/tmp/mps_roundtrip_p0033_qc1_r3.mps"; auto original = parse_mps(input_file, false); ASSERT_TRUE(original.has_quadratic_objective()); @@ -1330,9 +1332,13 @@ TEST(mps_roundtrip, qcqp_p0033_qc1) writer.write(temp_file); auto reloaded = parse_mps(temp_file, false); - compare_data_models(original, reloaded); + mps_writer_t writer_r2(reloaded); + writer_r2.write(temp_file_2); + auto reloaded_2 = parse_mps(temp_file_2, false); + compare_data_models(reloaded, reloaded_2); std::filesystem::remove(temp_file); + std::filesystem::remove(temp_file_2); } } // namespace cuopt::mps_parser From 926633a03083bbfd9b121288770fd922f0cc839b Mon Sep 17 00:00:00 2001 From: Yuwen Chen <37250191+yuwenchen95@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:35:07 +0200 Subject: [PATCH 16/68] Remove unused temp_file_3 in mps_parser_test Removed unused temporary file variable in test. --- cpp/libmps_parser/tests/mps_parser_test.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index 81a51b5dda..48811cb218 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -1322,7 +1322,6 @@ TEST(mps_roundtrip, qcqp_p0033_qc1) cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; std::string temp_file_2 = "/tmp/mps_roundtrip_p0033_qc1_r2.mps"; - std::string temp_file_3 = "/tmp/mps_roundtrip_p0033_qc1_r3.mps"; auto original = parse_mps(input_file, false); ASSERT_TRUE(original.has_quadratic_objective()); From 6f2b1fb6654caaf8496ca97bf77b1e66b83fe04c Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 24 Apr 2026 01:05:40 -0700 Subject: [PATCH 17/68] undo std::span for CI tests --- .../include/mps_parser/mps_data_model.hpp | 20 ++++-- cpp/libmps_parser/src/mps_data_model.cpp | 65 ++++++++++++++----- cpp/libmps_parser/src/mps_parser.cpp | 15 +++-- cpp/libmps_parser/tests/mps_parser_test.cpp | 30 +++++++-- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index c4c8b56e28..88b3ed62a6 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -8,7 +8,6 @@ #pragma once #include -#include #include #include #include @@ -288,7 +287,9 @@ class mps_data_model_t { /** * @brief Append one complete quadratic constraint (row + linear + rhs + quadratic Q). - * @param linear_values, linear_indices Same nnz; empty spans for a purely quadratic row (rare). + * @note Pointer+size signature is kept for current CI/toolchain compatibility; `std::span` + * can be revisited later when compatibility constraints are lifted. + * @param linear_values, linear_indices Same nnz; can be empty for a purely quadratic row (rare). * @param quadratic_values, quadratic_indices CSR nnz; may be empty if Q is empty. * @param quadratic_offsets CSR row starts; must be non-empty. * @param constraint_row_type MPS ROWS type; must be 'L'. 'G' and 'E' quadratic rows are not @@ -297,12 +298,17 @@ class mps_data_model_t { void append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, char constraint_row_type, - std::span linear_values, - std::span linear_indices, + const f_t* linear_values, + i_t linear_nnz, + const i_t* linear_indices, + i_t linear_indices_nnz, f_t rhs_value, - std::span quadratic_values, - std::span quadratic_indices, - std::span quadratic_offsets); + const f_t* quadratic_values, + i_t quadratic_size_values, + const i_t* quadratic_indices, + i_t quadratic_size_indices, + const i_t* quadratic_offsets, + i_t quadratic_size_offsets); const std::vector& get_quadratic_constraints() const; diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index f5db136a58..1ecaf47b2c 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -224,12 +224,17 @@ template void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, char constraint_row_type, - std::span linear_values, - std::span linear_indices, + const f_t* linear_values, + i_t linear_nnz, + const i_t* linear_indices, + i_t linear_indices_nnz, f_t rhs_value, - std::span quadratic_values, - std::span quadratic_indices, - std::span quadratic_offsets) + const f_t* quadratic_values, + i_t quadratic_size_values, + const i_t* quadratic_indices, + i_t quadratic_size_indices, + const i_t* quadratic_offsets, + i_t quadratic_size_offsets) { mps_parser_expects( constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); @@ -240,13 +245,28 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ "Only 'L' is supported for convex quadratic constraints.", constraint_row_type); - mps_parser_expects( - linear_values.size() == linear_indices.size(), - error_type_t::ValidationError, - "linear_values and linear_indices must have the same length"); + mps_parser_expects(linear_nnz == linear_indices_nnz, + error_type_t::ValidationError, + "linear_values and linear_indices must have the same nnz count"); + if (linear_nnz != 0) { + mps_parser_expects( + linear_values != nullptr && linear_indices != nullptr, + error_type_t::ValidationError, + "linear_values and linear_indices cannot be null when linear_nnz > 0"); + } + if (quadratic_size_values != 0) { + mps_parser_expects( + quadratic_values != nullptr, error_type_t::ValidationError, "quadratic_values cannot be null"); + } + mps_parser_expects( + quadratic_offsets != nullptr, error_type_t::ValidationError, "quadratic_offsets cannot be null"); + if (quadratic_size_indices != 0) { + mps_parser_expects( + quadratic_indices != nullptr, error_type_t::ValidationError, "quadratic_indices cannot be null"); + } mps_parser_expects( - !quadratic_offsets.empty(), error_type_t::ValidationError, "quadratic_offsets cannot be empty"); + quadratic_size_offsets > 0, error_type_t::ValidationError, "quadratic_size_offsets cannot be empty"); quadratic_constraint_t qc; qc.constraint_row_index = constraint_row_index; @@ -254,11 +274,26 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ qc.constraint_row_type = constraint_row_type; qc.rhs_value = rhs_value; - qc.linear_values.assign(linear_values.begin(), linear_values.end()); - qc.linear_indices.assign(linear_indices.begin(), linear_indices.end()); - qc.quadratic_values.assign(quadratic_values.begin(), quadratic_values.end()); - qc.quadratic_indices.assign(quadratic_indices.begin(), quadratic_indices.end()); - qc.quadratic_offsets.assign(quadratic_offsets.begin(), quadratic_offsets.end()); + qc.linear_values.resize(linear_nnz); + qc.linear_indices.resize(linear_nnz); + if (linear_nnz > 0) { + std::copy(linear_values, linear_values + linear_nnz, qc.linear_values.data()); + std::copy(linear_indices, linear_indices + linear_nnz, qc.linear_indices.data()); + } + + qc.quadratic_values.resize(quadratic_size_values); + if (quadratic_size_values > 0) { + std::copy( + quadratic_values, quadratic_values + quadratic_size_values, qc.quadratic_values.data()); + } + qc.quadratic_indices.resize(quadratic_size_indices); + if (quadratic_size_indices > 0) { + std::copy( + quadratic_indices, quadratic_indices + quadratic_size_indices, qc.quadratic_indices.data()); + } + qc.quadratic_offsets.resize(quadratic_size_offsets); + std::copy( + quadratic_offsets, quadratic_offsets + quadratic_size_offsets, qc.quadratic_offsets.data()); quadratic_constraints_.push_back(std::move(qc)); } diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index b446db5009..5613e1aeda 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -573,12 +573,17 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) linear_row_count + quadratic_row_id, row_names[row_id], static_cast(row_types[row_id]), - A_values[row_id], - A_indices[row_id], + A_values[row_id].data(), + static_cast(A_values[row_id].size()), + A_indices[row_id].data(), + static_cast(A_indices[row_id].size()), b_values[row_id], - csr_result.values, - csr_result.indices, - csr_result.offsets); + csr_result.values.data(), + static_cast(csr_result.values.size()), + csr_result.indices.data(), + static_cast(csr_result.indices.size()), + csr_result.offsets.data(), + static_cast(csr_result.offsets.size())); ++quadratic_row_id; } diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index 48811cb218..907e0080d3 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -884,7 +884,20 @@ TEST(qps_parser, qcmatrix_append_api) const std::vector qc0_linear_values = {1.0, 1.0}; const std::vector qc0_linear_indices = {0, 1}; model.append_quadratic_constraint( - 0, "QC0", 'L', qc0_linear_values, qc0_linear_indices, 5.0, qc0_values, qc0_indices, qc0_offsets); + 0, + "QC0", + 'L', + qc0_linear_values.data(), + qc0_linear_values.size(), + qc0_linear_indices.data(), + qc0_linear_indices.size(), + 5.0, + qc0_values.data(), + qc0_values.size(), + qc0_indices.data(), + qc0_indices.size(), + qc0_offsets.data(), + qc0_offsets.size()); // QC1: [[4, 1], [1, 6]] const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; @@ -895,12 +908,17 @@ TEST(qps_parser, qcmatrix_append_api) model.append_quadratic_constraint(1, "QC1", 'L', - qc1_linear_values, - qc1_linear_indices, + qc1_linear_values.data(), + qc1_linear_values.size(), + qc1_linear_indices.data(), + qc1_linear_indices.size(), 10.0, - qc1_values, - qc1_indices, - qc1_offsets); + qc1_values.data(), + qc1_values.size(), + qc1_indices.data(), + qc1_indices.size(), + qc1_offsets.data(), + qc1_offsets.size()); ASSERT_TRUE(model.has_quadratic_constraints()); const auto& qcs = model.get_quadratic_constraints(); From 9defd60627f9afb4d7e0ecf7249ec42897345c29 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 24 Apr 2026 08:57:34 +0000 Subject: [PATCH 18/68] Apply pre-commit fixes Signed-off-by: yuwenchen95 --- .../optimization_problem_interface.hpp | 3 +- .../optimization_problem_utils.hpp | 2 - .../include/mps_parser/mps_data_model.hpp | 5 +- .../include/mps_parser/parser.hpp | 2 +- .../include/mps_parser/utilities/span.hpp | 2 +- cpp/libmps_parser/src/mps_data_model.cpp | 34 ++++--- cpp/libmps_parser/src/mps_parser.cpp | 93 ++++++++++--------- cpp/libmps_parser/src/mps_parser.hpp | 4 +- cpp/libmps_parser/src/mps_writer.cpp | 10 +- cpp/libmps_parser/tests/mps_parser_test.cpp | 51 +++++----- cpp/src/pdlp/cpu_optimization_problem.cpp | 3 +- 11 files changed, 106 insertions(+), 103 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp index eb0850ba44..8ffd38578b 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp @@ -75,7 +75,8 @@ class optimization_problem_interface_t { * @brief Store quadratic constraints for MPS round-trip (linear + Q parts per QC row). */ virtual void set_quadratic_constraints(std::vector constraints) = 0; - template >> + template >> void set_quadratic_constraints(const std::vector& constraints) { std::vector converted_constraints; diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp index 204556e053..e37ade9660 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp @@ -113,7 +113,6 @@ void populate_from_mps_data_model(optimization_problem_interface_t* pr if (data_model.has_quadratic_constraints()) { problem->set_quadratic_constraints(data_model.get_quadratic_constraints()); } - } /** @@ -275,7 +274,6 @@ void populate_from_data_model_view(optimization_problem_interface_t* p if (data_model->has_quadratic_constraints()) { problem->set_quadratic_constraints(data_model->get_quadratic_constraints()); } - } } // namespace cuopt::linear_programming diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 88b3ed62a6..4d9c4bd8f3 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -275,7 +275,8 @@ class mps_data_model_t { /** ROWS declaration index (among all constraint rows), not an index into the linear CSR. */ i_t constraint_row_index{}; std::string constraint_row_name{}; - /** MPS ROWS sense for this quadratic row; only 'L' (≤) is supported for convex QCQP at the moment. */ + /** MPS ROWS sense for this quadratic row; only 'L' (≤) is supported for convex QCQP at the + * moment. */ char constraint_row_type{}; std::vector linear_values{}; std::vector linear_indices{}; diff --git a/cpp/libmps_parser/include/mps_parser/parser.hpp b/cpp/libmps_parser/include/mps_parser/parser.hpp index 6578ffb4d5..94230a0d4c 100644 --- a/cpp/libmps_parser/include/mps_parser/parser.hpp +++ b/cpp/libmps_parser/include/mps_parser/parser.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/libmps_parser/include/mps_parser/utilities/span.hpp b/cpp/libmps_parser/include/mps_parser/utilities/span.hpp index 7ad4f25d4c..24a865de6a 100644 --- a/cpp/libmps_parser/include/mps_parser/utilities/span.hpp +++ b/cpp/libmps_parser/include/mps_parser/utilities/span.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 1ecaf47b2c..b9ae16dc03 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -236,8 +236,9 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ const i_t* quadratic_offsets, i_t quadratic_size_offsets) { - mps_parser_expects( - constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); + mps_parser_expects(constraint_row_index >= 0, + error_type_t::ValidationError, + "constraint_row_index must be non-negative"); mps_parser_expects(constraint_row_type == 'L', error_type_t::ValidationError, @@ -249,24 +250,27 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ error_type_t::ValidationError, "linear_values and linear_indices must have the same nnz count"); if (linear_nnz != 0) { - mps_parser_expects( - linear_values != nullptr && linear_indices != nullptr, - error_type_t::ValidationError, - "linear_values and linear_indices cannot be null when linear_nnz > 0"); + mps_parser_expects(linear_values != nullptr && linear_indices != nullptr, + error_type_t::ValidationError, + "linear_values and linear_indices cannot be null when linear_nnz > 0"); } if (quadratic_size_values != 0) { - mps_parser_expects( - quadratic_values != nullptr, error_type_t::ValidationError, "quadratic_values cannot be null"); + mps_parser_expects(quadratic_values != nullptr, + error_type_t::ValidationError, + "quadratic_values cannot be null"); } - mps_parser_expects( - quadratic_offsets != nullptr, error_type_t::ValidationError, "quadratic_offsets cannot be null"); + mps_parser_expects(quadratic_offsets != nullptr, + error_type_t::ValidationError, + "quadratic_offsets cannot be null"); if (quadratic_size_indices != 0) { - mps_parser_expects( - quadratic_indices != nullptr, error_type_t::ValidationError, "quadratic_indices cannot be null"); + mps_parser_expects(quadratic_indices != nullptr, + error_type_t::ValidationError, + "quadratic_indices cannot be null"); } - mps_parser_expects( - quadratic_size_offsets > 0, error_type_t::ValidationError, "quadratic_size_offsets cannot be empty"); + mps_parser_expects(quadratic_size_offsets > 0, + error_type_t::ValidationError, + "quadratic_size_offsets cannot be empty"); quadratic_constraint_t qc; qc.constraint_row_index = constraint_row_index; diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 5613e1aeda..7f4d742084 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -278,7 +278,9 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) for (const auto& block : qcmatrix_blocks_) { quadratic_row_ids.insert(block.constraint_row_id); } - const auto is_quadratic_row = [&quadratic_row_ids](i_t row) { return quadratic_row_ids.count(row); }; + const auto is_quadratic_row = [&quadratic_row_ids](i_t row) { + return quadratic_row_ids.count(row); + }; { std::vector h_offsets{}, h_indices{}; @@ -307,13 +309,12 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) h_offsets.data(), h_offsets.size()); - mps_parser_expects( - static_cast(num_linear_rows) + 1 == h_offsets.size(), - error_type_t::ValidationError, - "The row indexing vector for the constraint matrix was not constructed " - "successfully. Should be size %zu, but was size %zu", - static_cast(num_linear_rows) + 1, - h_offsets.size()); + mps_parser_expects(static_cast(num_linear_rows) + 1 == h_offsets.size(), + error_type_t::ValidationError, + "The row indexing vector for the constraint matrix was not constructed " + "successfully. Should be size %zu, but was size %zu", + static_cast(num_linear_rows) + 1, + h_offsets.size()); mps_parser_expects( h_indices.size() == h_values.size(), error_type_t::ValidationError, @@ -330,7 +331,6 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) "nonzero vector. Nonzero has size %zu but the last offset is %d.", h_values.size(), h_offsets[h_offsets.size() - 1]); - } // Set b & c (RHS entries for quadratic rows are stored only on quadratic_constraint_t) @@ -361,7 +361,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.get_variable_lower_bounds().size(), problem.get_variable_upper_bounds().size()); - // Determine the constraint bounds based on row types (quadratic rows use bundles only, not counted here) + // Determine the constraint bounds based on row types (quadratic rows use bundles only, not + // counted here) { std::vector h_constraint_lower_bounds{}; std::vector h_constraint_upper_bounds{}; @@ -463,11 +464,12 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_variable_types(std::move(var_types)); problem.set_maximize(maximize); - // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of O(nnz*log(nnz))) - // For QUADOBJ: handles upper triangular input by expanding to full symmetric matrix. - // - // @p value_scale: - // QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); + // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of + // O(nnz*log(nnz))) For QUADOBJ: handles upper triangular input by expanding to full symmetric + // matrix. + // + // @p value_scale: + // QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); // QCMATRIX uses 1.0 (symmetric Q defines xᵀQx directly in the constraint). auto build_csr_via_transpose = [](const std::vector>& entries, i_t num_rows, @@ -513,8 +515,9 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) for (i_t row = 0; row < num_rows; ++row) { for (const auto& [col, val] : csr_data[row]) { - // While the mps format expects to optimize for 0.5 xT Q x, cuopt optimizes for xT Q xExpand commentComment on line L488 - // so we have to multiply the value by value_scale=0.5 to get the correct value. + // While the mps format expects to optimize for 0.5 xT Q x, cuopt optimizes for xT Q xExpand + // commentComment on line L488 so we have to multiply the value by value_scale=0.5 to get + // the correct value. result.values.push_back(val * value_scale); result.indices.push_back(col); } @@ -529,8 +532,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) // Convert quadratic objective entries to CSR format using double transpose // QUADOBJ stores upper triangular elements, so we expand to full symmetric matrix constexpr f_t k_mps_quad_half_scale = f_t(0.5); // MPS ½ xᵀQx vs internal xᵀQx - auto csr_result = - build_csr_via_transpose(quadobj_entries, num_vars_for_quad, num_vars_for_quad, true, k_mps_quad_half_scale); + auto csr_result = build_csr_via_transpose( + quadobj_entries, num_vars_for_quad, num_vars_for_quad, true, k_mps_quad_half_scale); // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) problem.set_quadratic_objective_matrix(csr_result.values.data(), @@ -543,8 +546,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) // Convert quadratic objective entries to CSR format using double transpose // QMATRIX stores full symmetric matrix constexpr f_t k_mps_quad_half_scale = f_t(0.5); - auto csr_result = - build_csr_via_transpose(qmatrix_entries, num_vars_for_quad, num_vars_for_quad, false, k_mps_quad_half_scale); + auto csr_result = build_csr_via_transpose( + qmatrix_entries, num_vars_for_quad, num_vars_for_quad, false, k_mps_quad_half_scale); // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) problem.set_quadratic_objective_matrix(csr_result.values.data(), @@ -564,26 +567,24 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) auto csr_result = build_csr_via_transpose( block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); const i_t row_id = block.constraint_row_id; - mps_parser_expects( - row_id >= 0 && row_id < static_cast(row_types.size()), - error_type_t::ValidationError, - "QCMATRIX row index %d is out of range for constraints", - static_cast(row_id)); - problem.append_quadratic_constraint( - linear_row_count + quadratic_row_id, - row_names[row_id], - static_cast(row_types[row_id]), - A_values[row_id].data(), - static_cast(A_values[row_id].size()), - A_indices[row_id].data(), - static_cast(A_indices[row_id].size()), - b_values[row_id], - csr_result.values.data(), - static_cast(csr_result.values.size()), - csr_result.indices.data(), - static_cast(csr_result.indices.size()), - csr_result.offsets.data(), - static_cast(csr_result.offsets.size())); + mps_parser_expects(row_id >= 0 && row_id < static_cast(row_types.size()), + error_type_t::ValidationError, + "QCMATRIX row index %d is out of range for constraints", + static_cast(row_id)); + problem.append_quadratic_constraint(linear_row_count + quadratic_row_id, + row_names[row_id], + static_cast(row_types[row_id]), + A_values[row_id].data(), + static_cast(A_values[row_id].size()), + A_indices[row_id].data(), + static_cast(A_indices[row_id].size()), + b_values[row_id], + csr_result.values.data(), + static_cast(csr_result.values.size()), + csr_result.indices.data(), + static_cast(csr_result.indices.size()), + csr_result.offsets.data(), + static_cast(csr_result.offsets.size())); ++quadratic_row_id; } @@ -1420,7 +1421,7 @@ void mps_parser_t::parse_qcmatrix_header(std::string_view line) error_type_t::ValidationError, "QCMATRIX header line too short! line=%s", std::string(line).c_str()); - //fixed MPS: constraint name starts in column 12 (1-based) → 0-based index 11, 8 chars + // fixed MPS: constraint name starts in column 12 (1-based) → 0-based index 11, 8 chars row_name = std::string(trim(line.substr(11, 8))); } else { std::stringstream ss{std::string(line)}; @@ -1471,8 +1472,8 @@ void mps_parser_t::parse_qcmatrix_data(std::string_view line) i_t pos = 24; value = get_numerical_bound(line, pos); } else { - i_t pos = 0; - i_t end = 0; + i_t pos = 0; + i_t end = 0; const std::string_view var1_sv = get_next_string(line, pos, end); mps_parser_expects(!var1_sv.empty(), error_type_t::ValidationError, @@ -1529,8 +1530,8 @@ void mps_parser_t::parse_quad(std::string_view line, bool is_quadobj) i_t pos = 24; value = get_numerical_bound(line, pos); } else { - i_t pos = 0; - i_t end = 0; + i_t pos = 0; + i_t end = 0; const std::string_view var1_sv = get_next_string(line, pos, end); mps_parser_expects(!var1_sv.empty(), error_type_t::ValidationError, diff --git a/cpp/libmps_parser/src/mps_parser.hpp b/cpp/libmps_parser/src/mps_parser.hpp index d73cfdd8b3..6e56d4bce3 100644 --- a/cpp/libmps_parser/src/mps_parser.hpp +++ b/cpp/libmps_parser/src/mps_parser.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -12,8 +12,8 @@ #include #include #include -#include #include +#include #include #include diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index 67346cdcb5..c5cbd4b3f9 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -235,7 +235,8 @@ void mps_writer_t::write(const std::string& mps_file_path) for (size_t k = 0; k < static_cast(n_constraints); ++k) { std::string row_name = k < problem_.get_row_names().size() ? problem_.get_row_names()[k] : "R" + std::to_string(k); - char const type = linear_row_type_from_bounds(constraint_lower_bounds[k], constraint_upper_bounds[k]); + char const type = + linear_row_type_from_bounds(constraint_lower_bounds[k], constraint_upper_bounds[k]); mps_file << " " << type << " " << row_name << "\n"; } for (size_t q = 0; q < quadratic_constraints.size(); ++q) { @@ -255,7 +256,8 @@ void mps_writer_t::write(const std::string& mps_file_path) std::map>> integral_col_nnzs; std::map>> continuous_col_nnzs; - // iterate over the constraint matrix and add the nonzeros to the integral and continuous col_nnzs maps + // iterate over the constraint matrix and add the nonzeros to the integral and continuous col_nnzs + // maps for (size_t csr_row = 0; csr_row < (size_t)n_constraints; csr_row++) { const i_t row_id = static_cast(csr_row); for (size_t k = (size_t)constraint_matrix_offsets[csr_row]; @@ -498,8 +500,8 @@ void mps_writer_t::write(const std::string& mps_file_path) ? problem_.get_variable_names()[i] : "C" + std::to_string(i); for (i_t p = qc.quadratic_offsets[i]; p < qc.quadratic_offsets[i + 1]; ++p) { - i_t j = qc.quadratic_indices[p]; - f_t v = qc.quadratic_values[p]; + i_t j = qc.quadratic_indices[p]; + f_t v = qc.quadratic_values[p]; std::string col_var_name = static_cast(j) < problem_.get_variable_names().size() ? problem_.get_variable_names()[j] : "C" + std::to_string(j); diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index 907e0080d3..669a8c5e05 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -16,8 +16,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -878,31 +878,30 @@ TEST(qps_parser, qcmatrix_append_api) EXPECT_EQ(0.0, default_qcm.rhs_value); // QC0: [[10, 2], [2, 2]] - const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; - const std::vector qc0_indices = {0, 1, 0, 1}; - const std::vector qc0_offsets = {0, 2, 4}; + const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; + const std::vector qc0_indices = {0, 1, 0, 1}; + const std::vector qc0_offsets = {0, 2, 4}; const std::vector qc0_linear_values = {1.0, 1.0}; const std::vector qc0_linear_indices = {0, 1}; - model.append_quadratic_constraint( - 0, - "QC0", - 'L', - qc0_linear_values.data(), - qc0_linear_values.size(), - qc0_linear_indices.data(), - qc0_linear_indices.size(), - 5.0, - qc0_values.data(), - qc0_values.size(), - qc0_indices.data(), - qc0_indices.size(), - qc0_offsets.data(), - qc0_offsets.size()); + model.append_quadratic_constraint(0, + "QC0", + 'L', + qc0_linear_values.data(), + qc0_linear_values.size(), + qc0_linear_indices.data(), + qc0_linear_indices.size(), + 5.0, + qc0_values.data(), + qc0_values.size(), + qc0_indices.data(), + qc0_indices.size(), + qc0_offsets.data(), + qc0_offsets.size()); // QC1: [[4, 1], [1, 6]] - const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; - const std::vector qc1_indices = {0, 1, 0, 1}; - const std::vector qc1_offsets = {0, 2, 4}; + const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; + const std::vector qc1_indices = {0, 1, 0, 1}; + const std::vector qc1_offsets = {0, 2, 4}; const std::vector qc1_linear_values = {3.0, 1.0}; const std::vector qc1_linear_indices = {0, 1}; model.append_quadratic_constraint(1, @@ -1219,7 +1218,6 @@ void compare_data_models(const mps_data_model_t& original, } } } - } TEST(mps_roundtrip, linear_programming_basic) @@ -1332,12 +1330,9 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_2) TEST(mps_roundtrip, qcqp_p0033_qc1) { - if (!file_exists("qcqp/p0033_qc1.mps")) { - GTEST_SKIP() << "Test file not found"; - } + if (!file_exists("qcqp/p0033_qc1.mps")) { GTEST_SKIP() << "Test file not found"; } - std::string input_file = - cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; + std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; std::string temp_file_2 = "/tmp/mps_roundtrip_p0033_qc1_r2.mps"; diff --git a/cpp/src/pdlp/cpu_optimization_problem.cpp b/cpp/src/pdlp/cpu_optimization_problem.cpp index e4018a290a..de1f74ed47 100644 --- a/cpp/src/pdlp/cpu_optimization_problem.cpp +++ b/cpp/src/pdlp/cpu_optimization_problem.cpp @@ -135,7 +135,8 @@ void cpu_optimization_problem_t::set_quadratic_objective_matrix( template void cpu_optimization_problem_t::set_quadratic_constraints( - std::vector::quadratic_constraint_t> constraints) + std::vector::quadratic_constraint_t> + constraints) { quadratic_constraints_ = std::move(constraints); } From 0a31a443b6ad600e73149e3ca54b528f8e7f3285 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 24 Apr 2026 06:10:41 -0700 Subject: [PATCH 19/68] minior correction for bound check and update dataset for qcqp --- cpp/src/pdlp/translate.hpp | 77 ++++++++++++++++++++------------------ datasets/qcqp/socp1.mps | 23 ++++++++++++ datasets/qcqp/socp2.mps | 26 +++++++++++++ datasets/qcqp/socp3.mps | 22 +++++++++++ datasets/qcqp/socp4.mps | 33 ++++++++++++++++ 5 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 datasets/qcqp/socp1.mps create mode 100644 datasets/qcqp/socp2.mps create mode 100644 datasets/qcqp/socp3.mps create mode 100644 datasets/qcqp/socp4.mps diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 1628290c5a..4e5c0be991 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -111,7 +111,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( "Quadratic-constraint flag is set, but no constraints were provided"); const i_t original_rows = static_cast(user_problem.num_rows); - const f_t tol = f_t(1e-16); + // Use a practical tolerance for text-parsed MPS numeric values. + const f_t tol = std::numeric_limits::epsilon() * 2; // SOC: Q is n×n diagonal CSR (offsets length n+1). Exactly q_n = nnz on the main diagonal, at // q_n distinct variable indices: one −1 (head) and (q_n−1) +1 (tails). Lifting: q_n rows, each @@ -137,11 +138,11 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( error_type_t::ValidationError, "Quadratic constraint '%s' has invalid CSR offsets (need at least 2 entries)", qc.constraint_row_name.c_str()); - cuopt_expects( - qc.quadratic_values.size() == qc.quadratic_indices.size(), - error_type_t::ValidationError, - "Quadratic constraint '%s' quadratic_values and quadratic_indices length mismatch for CSR Q", - qc.constraint_row_name.c_str()); + cuopt_expects(qc.quadratic_values.size() == qc.quadratic_indices.size(), + error_type_t::ValidationError, + "Quadratic constraint '%s' quadratic_values and quadratic_indices length " + "mismatch for CSR Q", + qc.constraint_row_name.c_str()); const i_t q_n = static_cast(qc.quadratic_values.size()); cuopt_expects(q_n >= 2, @@ -168,11 +169,10 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( qc.constraint_row_name.c_str(), static_cast(qc.quadratic_offsets[static_cast(n)]), static_cast(q_n)); - cuopt_expects( - qc.quadratic_offsets[0] == 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q CSR offsets[0] must be 0", - qc.constraint_row_name.c_str()); + cuopt_expects(qc.quadratic_offsets[0] == 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q CSR offsets[0] must be 0", + qc.constraint_row_name.c_str()); // Verify Q: n×n CSR, diagonal entries only, Lorentz pattern, then build the lift. // Scan each row r: empty or one nnz on (r,r) with value -1 (head) or +1 (tail); @@ -188,14 +188,14 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( if (p_beg == p_end) { continue; } - cuopt_expects( - p_beg + 1 == p_end, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: expected at most one stored entry on the diagonal per " - "row (got end - beg = %d)", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(p_end - p_beg)); + cuopt_expects(p_beg + 1 == p_end, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: expected at most one stored entry on " + "the diagonal per " + "row (got end - beg = %d)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(p_end - p_beg)); const i_t col = qc.quadratic_indices[static_cast(p_beg)]; const f_t v = qc.quadratic_values[static_cast(p_beg)]; @@ -208,20 +208,24 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( static_cast(r), static_cast(col)); - if (v > f_t(-1) - tol && v < f_t(-1) + tol) { + const f_t neg_one_delta = v + f_t(1); + const f_t pos_one_delta = v - f_t(1); + const bool is_neg_one = (neg_one_delta >= -tol && neg_one_delta <= tol); + const bool is_pos_one = (pos_one_delta >= -tol && pos_one_delta <= tol); + if (is_neg_one) { ++n_head_m; head = r; - } else if (v > f_t(1) - tol && v < f_t(1) + tol) { + } else if (is_pos_one) { tail_row_vars.push_back(r); } else { - cuopt_expects( - false, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: diagonal for SOC must be -1 (head) or +1 (tail); got " - "%g", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(v)); + cuopt_expects(false, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: diagonal for SOC must be -1 (head) or " + "+1 (tail); got " + "%.17g", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(v)); } } cuopt_expects( @@ -238,11 +242,10 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( qc.constraint_row_name.c_str(), static_cast(q_n - 1), tail_row_vars.size()); - cuopt_expects( - head >= 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: internal error (head index invalid)", - qc.constraint_row_name.c_str()); + cuopt_expects(head >= 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: internal error (head index invalid)", + qc.constraint_row_name.c_str()); row_cone_dims.push_back(q_n); dual_simplex::csr_matrix_t lift_block(q_n, n, q_n); @@ -250,8 +253,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( lift_block.row_start[t] = t; } - // One lift row per cone component: -1 in column head, then -1 in each tail column - // (order matches tail_row_vars from the Q scan). + // One lift row per cone component: -1 in column head, then -1 in each tail column (since our + // slack variable is done by + s form) (order matches tail_row_vars from the Q scan). lift_block.j[0] = head; lift_block.x[0] = f_t(-1); for (i_t t = 0; t < q_n - 1; ++t) { @@ -280,7 +283,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( } user_problem.num_rows = next_row; - user_problem.cone_row_start = original_rows; + user_problem.cone_row_start = original_rows; user_problem.second_order_cone_row_dims = std::move(row_cone_dims); } diff --git a/datasets/qcqp/socp1.mps b/datasets/qcqp/socp1.mps new file mode 100644 index 0000000000..3e479054d0 --- /dev/null +++ b/datasets/qcqp/socp1.mps @@ -0,0 +1,23 @@ +NAME +ROWS + N OBJ + L c2 + E c1 +COLUMNS + y OBJ -1 + z OBJ -1 + x c1 1 +RHS + rhs c2 0 + rhs c1 1 +RANGES +BOUNDS + FR bounds y + FR bounds z + LO bounds x 0 + PL bounds x +QCMATRIX c2 + y y 1 + z z 1 + x x -1 +ENDATA diff --git a/datasets/qcqp/socp2.mps b/datasets/qcqp/socp2.mps new file mode 100644 index 0000000000..6542d1d4f8 --- /dev/null +++ b/datasets/qcqp/socp2.mps @@ -0,0 +1,26 @@ +NAME +ROWS + N OBJ + L c2 + G c1 + E c3 +COLUMNS + x OBJ 1 + y c1 1 + tau c3 1 +RHS + rhs c2 0 + rhs c1 0.7071067811865476 + rhs c3 1 +RANGES +BOUNDS + FR bounds x + LO bounds y 0 + PL bounds y + LO bounds tau 0 + PL bounds tau +QCMATRIX c2 + x x 1 + y y 1 + tau tau -1 +ENDATA diff --git a/datasets/qcqp/socp3.mps b/datasets/qcqp/socp3.mps new file mode 100644 index 0000000000..ae92220042 --- /dev/null +++ b/datasets/qcqp/socp3.mps @@ -0,0 +1,22 @@ +NAME +ROWS + N OBJ + L c2 + L c3 + G c1 +COLUMNS + y c1 1 + x c2 1 +RHS + rhs c2 1 + rhs c3 0 + rhs c1 2 +RANGES +BOUNDS + FR bounds y + LO bounds x 0 + PL bounds x +QCMATRIX c3 + y y 1 + x x -1 +ENDATA diff --git a/datasets/qcqp/socp4.mps b/datasets/qcqp/socp4.mps new file mode 100644 index 0000000000..584725963f --- /dev/null +++ b/datasets/qcqp/socp4.mps @@ -0,0 +1,33 @@ +NAME +ROWS + N OBJ + L c2 + E c1a + E c1b + E c1c +COLUMNS + x2 c1b 1 + x2 OBJ -2 + x3 c1c 1 + x3 OBJ -1 + x1 c1a 1 + x4 c1b -1 + x5 c1c -1 +RHS + rhs c2 0 + rhs c1a 1 + rhs c1b 0 + rhs c1c 0 +RANGES +BOUNDS + FR bounds x2 + FR bounds x3 + LO bounds x1 0 + PL bounds x1 + FR bounds x4 + FR bounds x5 +QCMATRIX c2 + x1 x1 -1 + x4 x4 1 + x5 x5 1 +ENDATA From d17221348fd4708d4da86d6138fbbef78ae231fc Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 24 Apr 2026 11:25:16 -0700 Subject: [PATCH 20/68] Print # of quadratic constraints and cones --- cpp/src/barrier/barrier.cu | 3 +++ cpp/src/pdlp/solve.cu | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index 09a7616cdf..cc0b391731 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -3818,6 +3818,9 @@ lp_status_t barrier_solver_t::solve(f_t start_time, if (lp.Q.n > 0) { settings.log.printf("Quadratic objective matrix: %d nonzeros\n", lp.Q.row_start[lp.Q.n]); } + if (lp.second_order_cone_dims.size() > 0) { + settings.log.printf("Second-order cones: %d\n", static_cast(lp.second_order_cone_dims.size())); + } settings.log.printf("\n"); // Compute the number of free variables diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 68dbe4bda8..585bfe4476 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1383,7 +1383,7 @@ optimization_problem_solution_t solve_lp( CUOPT_LOG_INFO("Problem has a quadratic objective. Using Barrier."); } if (op_problem.has_quadratic_constraints()) { - CUOPT_LOG_INFO("Problem has quadratic constraints. Using Barrier with SOC conversion."); + CUOPT_LOG_INFO("Problem has %d quadratic constraints. Using Barrier with SOC conversion.", static_cast(op_problem.get_quadratic_constraints().size())); } settings.method = method_t::Barrier; settings.presolver = presolver_t::None; From f20d132cf2ce798495ba39baa162b220aa939d92 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 24 Apr 2026 17:57:31 -0700 Subject: [PATCH 21/68] Add support for handling free variables in QPs with the augmented system --- cpp/src/barrier/barrier.cu | 234 +++++++++++++++++++++++++----- cpp/src/dual_simplex/presolve.cpp | 13 +- cpp/src/dual_simplex/presolve.hpp | 3 + 3 files changed, 209 insertions(+), 41 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index 778038db1f..4ccd5a0445 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -165,6 +165,8 @@ class iteration_data_t { d_augmented_diagonal_indices_(0, lp.handle_ptr->get_stream()), use_augmented(false), has_factorization(false), + n_free_vars(0), + d_is_free_(0, lp.handle_ptr->get_stream()), num_factorizations(0), has_solve_info(false), settings_(settings), @@ -287,6 +289,11 @@ class iteration_data_t { f_t estimated_nz_AAT = 0.0; std::vector dense_columns_unordered; + if (has_Q) { + // QPs always use the augmented system; skip dense column detection + use_augmented = true; + n_dense_columns = 0; + } else { f_t start_column_density = tic(); // Ignore Q matrix for now find_dense_columns( @@ -320,6 +327,7 @@ class iteration_data_t { n_dense_columns = 0; use_augmented = !Q_diagonal; } + } // end !has_Q if (use_augmented) { settings.log.printf("Linear system : augmented\n"); @@ -1538,6 +1546,8 @@ class iteration_data_t { bool use_augmented; i_t symbolic_status; + i_t n_free_vars{0}; + rmm::device_uvector d_is_free_; // 1 if variable is free (QP only), 0 otherwise std::unique_ptr> chol; @@ -1908,6 +1918,12 @@ int barrier_solver_t::initial_point(iteration_data_t& data) } } } + // Free variables have z = 0 (no complementarity condition) + if (data.n_free_vars > 0) { + for (i_t j : presolve_info.free_variable_indices) { + data.z[j] = 0.0; + } + } } else if (use_augmented) { dense_vector_t dual_rhs(lp.num_cols + lp.num_rows); dual_rhs.set_scalar(0.0); @@ -1970,8 +1986,25 @@ int barrier_solver_t::initial_point(iteration_data_t& data) vector_norm2(data.dual_residual)); #endif // Make sure (w, x, v, z) > 0 + // Save free variable x values before forcing positive (they can be negative) + std::vector free_x_save; + if (data.n_free_vars > 0) { + free_x_save.resize(data.n_free_vars); + for (i_t k = 0; k < data.n_free_vars; ++k) { + free_x_save[k] = data.x[presolve_info.free_variable_indices[k]]; + } + } data.w.ensure_positive(epsilon_adjust); data.x.ensure_positive(epsilon_adjust); + + // For native free variables (QP): restore x values and set z = 0 + if (data.n_free_vars > 0) { + for (i_t k = 0; k < data.n_free_vars; ++k) { + i_t j = presolve_info.free_variable_indices[k]; + data.x[j] = free_x_save[k]; + data.z[j] = 0.0; + } + } #ifdef PRINT_INFO settings.log.printf("min v %e min z %e\n", data.v.minimum(), data.z.minimum()); #endif @@ -2166,6 +2199,29 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t& x, const rmm::device_uvector& dx) { + // For x-sized vectors with free variables, skip free vars in ratio test + const bool has_free = data.n_free_vars > 0 && static_cast(x.size()) == lp.num_cols; + + if (has_free) { + auto is_free_ptr = data.d_is_free_.data(); + auto ratio_test_free = [is_free_ptr] HD(const thrust::tuple t) { + const f_t dx_val = thrust::get<0>(t); + const f_t x_val = thrust::get<1>(t); + const i_t is_free = thrust::get<2>(t); + if (is_free) return f_t(1.0); + if (dx_val < f_t(0.0)) return -x_val / dx_val; + return f_t(1.0); + }; + + return data.transform_reduce_helper_.transform_reduce( + thrust::make_zip_iterator(dx.data(), x.data(), is_free_ptr), + thrust::minimum(), + ratio_test_free, + f_t(1.0), + x.size(), + stream_view_); + } + return data.transform_reduce_helper_.transform_reduce( thrust::make_zip_iterator(dx.data(), x.data()), thrust::minimum(), @@ -2248,11 +2304,37 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t{}, - stream_view_.value()); + // For native free variables (QP): use Q diagonal if available, otherwise a static regularizer + if (data.n_free_vars > 0) { + constexpr f_t free_var_reg = 1e-7; + if (data.Q.n > 0 && data.Q_diagonal) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), + data.d_is_free_.data(), data.d_Q_diag_.data()), + data.d_diag_.data(), + data.d_diag_.size(), + [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_free, f_t q_jj) { + if (!is_free) return z_j / x_j; + return (q_jj > f_t(0)) ? f_t(0) : free_var_reg; + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), data.d_is_free_.data()), + data.d_diag_.data(), + data.d_diag_.size(), + [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_free) { + return is_free ? free_var_reg : (z_j / x_j); + }, + stream_view_.value()); + } + } else { + cub::DeviceTransform::Transform(cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data()), + data.d_diag_.data(), + data.d_diag_.size(), + cuda::std::divides<>{}, + stream_view_.value()); + } RAFT_CHECK_CUDA(stream_view_); // diag = z ./ x + E * (v ./ w) * E' @@ -2358,20 +2440,40 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t thrust::tuple { - const f_t tmp = tmp3 + -(complementarity_xz_rhs / x) + dual_rhs; - return {tmp, inv_diag * tmp}; - }, - stream_view_.value()); + // For free variables, skip the complementarity_xz_rhs / x term + if (data.n_free_vars > 0) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_inv_diag.data(), + data.d_tmp3_.data(), + data.d_complementarity_xz_rhs_.data(), + data.d_x_.data(), + data.d_dual_rhs_.data(), + data.d_is_free_.data()), + thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + lp.num_cols, + [] HD(f_t inv_diag, f_t tmp3, f_t complementarity_xz_rhs, f_t x, f_t dual_rhs, i_t is_free) + -> thrust::tuple { + const f_t xz_term = is_free ? f_t(0) : (complementarity_xz_rhs / x); + const f_t tmp = tmp3 - xz_term + dual_rhs; + return {tmp, inv_diag * tmp}; + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_inv_diag.data(), + data.d_tmp3_.data(), + data.d_complementarity_xz_rhs_.data(), + data.d_x_.data(), + data.d_dual_rhs_.data()), + thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + lp.num_cols, + [] HD(f_t inv_diag, f_t tmp3, f_t complementarity_xz_rhs, f_t x, f_t dual_rhs) + -> thrust::tuple { + const f_t tmp = tmp3 + -(complementarity_xz_rhs / x) + dual_rhs; + return {tmp, inv_diag * tmp}; + }, + stream_view_.value()); + } RAFT_CHECK_CUDA(stream_view_); raft::copy(data.d_r1_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); raft::copy(data.d_r1_prime_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); @@ -2656,17 +2758,33 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_complementarity_xz_rhs_.data(), + data.d_z_.data(), + data.d_dx_.data(), + data.d_x_.data(), + data.d_is_free_.data()), + data.d_dz_.data(), + data.d_dz_.size(), + [] HD(f_t complementarity_xz_rhs, f_t z, f_t dx, f_t x, i_t is_free) { + return is_free ? f_t(0) : ((complementarity_xz_rhs - z * dx) / x); + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_complementarity_xz_rhs_.data(), + data.d_z_.data(), + data.d_dx_.data(), + data.d_x_.data()), + data.d_dz_.data(), + data.d_dz_.size(), + [] HD(f_t complementarity_xz_rhs, f_t z, f_t dx, f_t x) { + return (complementarity_xz_rhs - z * dx) / x; + }, + stream_view_.value()); + } RAFT_CHECK_CUDA(stream_view_); raft::copy(dz.data(), data.d_dz_.data(), data.d_dz_.size(), stream_view_); } @@ -2956,9 +3074,9 @@ void barrier_solver_t::compute_target_mu( stream_view_); complementarity_aff_sum = complementarity_xz_aff_sum + complementarity_wv_aff_sum; - - mu_aff = (complementarity_aff_sum) / - (static_cast(data.x.size()) + static_cast(data.n_upper_bounds)); + f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); + mu_denom -= static_cast(data.n_free_vars); + mu_aff = complementarity_aff_sum / mu_denom; sigma = std::max(0.0, std::min(1.0, std::pow(mu_aff / mu, 3.0))); new_mu = sigma * mu_aff; } @@ -2968,12 +3086,34 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data { raft::common::nvtx::range fun_scope("Barrier: compute_cc_rhs"); - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_dx_aff_.data(), data.d_dz_aff_.data()), - data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_rhs_.size(), - [new_mu] HD(f_t dx_aff, f_t dz_aff) { return -(dx_aff * dz_aff) + new_mu; }, - stream_view_.value()); + auto fill_linear_cc_rhs = [&](raft::device_span out, + raft::device_span dx_aff, + raft::device_span dz_aff) { + if (out.empty()) return; + if (data.n_free_vars > 0) { + auto is_free_ptr = data.d_is_free_.data(); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(dx_aff.data(), dz_aff.data(), is_free_ptr), + out.data(), + out.size(), + [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val, i_t is_free) { + return is_free ? f_t(0) : (-(dx_aff_val * dz_aff_val) + new_mu); + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(dx_aff.data(), dz_aff.data()), + out.data(), + out.size(), + [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val) { + return -(dx_aff_val * dz_aff_val) + new_mu; + }, + stream_view_.value()); + } + }; + fill_linear_cc_rhs(cuopt::make_span(data.d_complementarity_xz_rhs_), + cuopt::make_span(data.d_dx_aff_), + cuopt::make_span(data.d_dz_aff_)); RAFT_CHECK_CUDA(stream_view_); cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_dw_aff_.data(), data.d_dv_aff_.data()), @@ -3160,13 +3300,16 @@ void barrier_solver_t::compute_mu(iteration_data_t& data, f_ { raft::common::nvtx::range fun_scope("Barrier: compute_mu"); + f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); + mu_denom -= static_cast(data.n_free_vars); // free vars don't contribute to mu + mu = (data.sum_reduce_helper_.sum(data.d_complementarity_xz_residual_.begin(), data.d_complementarity_xz_residual_.size(), stream_view_) + data.sum_reduce_helper_.sum(data.d_complementarity_wv_residual_.begin(), data.d_complementarity_wv_residual_.size(), stream_view_)) / - (static_cast(data.x.size()) + static_cast(data.n_upper_bounds)); + mu_denom; } template @@ -3445,6 +3588,17 @@ lp_status_t barrier_solver_t::solve(f_t start_time, if (lp.Q.n > 0) { create_Q(lp, Q); } iteration_data_t data(lp, num_upper_bounds, Q, settings); + // Set up native free variable tracking for QPs + if (!presolve_info.free_variable_indices.empty()) { + data.n_free_vars = presolve_info.free_variable_indices.size(); + std::vector is_free_host(n, 0); + for (i_t j : presolve_info.free_variable_indices) { + is_free_host[j] = 1; + } + data.d_is_free_.resize(n, stream_view_); + raft::copy(data.d_is_free_.data(), is_free_host.data(), n, stream_view_); + settings.log.printf("Native free variables (QP) : %d\n", data.n_free_vars); + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Barrier solver halted\n"); return lp_status_t::CONCURRENT_LIMIT; diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index c5ef847106..88f6865013 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -1139,7 +1139,18 @@ i_t presolve(const lp_problem_t& original, problem.Q.check_matrix("Before free variable expansion"); - if (settings.barrier_presolve && free_variables > 0) { + // For QPs using the augmented system, keep free variables as-is rather than + // splitting x = v - w. The barrier solver handles them natively with a + // static regularizer on the diagonal instead of z/x complementarity terms. + if (settings.barrier_presolve && free_variables > 0 && problem.Q.n > 0) { + presolve_info.free_variable_indices.clear(); + for (i_t j = 0; j < problem.num_cols; j++) { + if (problem.lower[j] == -inf && problem.upper[j] == inf) { + presolve_info.free_variable_indices.push_back(j); + } + } + settings.log.printf("Keeping %d free variables for QP augmented system\n", free_variables); + } else if (settings.barrier_presolve && free_variables > 0) { // We have a variable x_j: with -inf < x_j < inf // we create new variables v and w with 0 <= v, w and x_j = v - w // Constraints diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index d0e2d52812..2caf5673e0 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -153,6 +153,9 @@ struct presolve_info_t { // Variables that were negated to handle -inf < x_j <= u_j std::vector negated_variables; + + // Free variable indices for QP augmented system (not split, handled natively) + std::vector free_variable_indices; }; template From 462a2ccc0e0ac3a4959c707b2be969725b1c44dc Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 27 Apr 2026 05:32:45 -0700 Subject: [PATCH 22/68] fix issues in CI tests --- cpp/libmps_parser/src/mps_parser.cpp | 8 ++++++-- cpp/tests/mip/cuts_test.cu | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index f416bc37fb..21d2c945cc 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -600,14 +600,18 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) } } problem.set_row_names(std::move(linear_row_names)); - problem.set_row_types(row_types_linear.data(), static_cast(row_types_linear.size())); + if (!row_types_linear.empty()) { + problem.set_row_types(row_types_linear.data(), static_cast(row_types_linear.size())); + } } else { std::vector row_types_host(row_types.size()); for (size_t i = 0; i < row_types.size(); ++i) { row_types_host[i] = static_cast(row_types[i]); } problem.set_row_names(std::move(row_names)); - problem.set_row_types(row_types_host.data(), static_cast(row_types_host.size())); + if (!row_types_host.empty()) { + problem.set_row_types(row_types_host.data(), static_cast(row_types_host.size())); + } } } diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 33b5457dfe..d1f2b96c40 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -721,8 +721,10 @@ mps_parser::mps_data_model_t append_literal_cut_prefix_to_lp_model( std::vector matrix_values = base_lp_model.get_constraint_matrix_values(); std::vector matrix_indices = base_lp_model.get_constraint_matrix_indices(); std::vector matrix_offsets = base_lp_model.get_constraint_matrix_offsets(); + std::vector constraint_rhs = base_lp_model.get_constraint_bounds(); std::vector constraint_lbs = base_lp_model.get_constraint_lower_bounds(); std::vector constraint_ubs = base_lp_model.get_constraint_upper_bounds(); + std::vector row_types = base_lp_model.get_row_types(); std::vector row_names = base_lp_model.get_row_names(); if (matrix_offsets.empty()) { matrix_offsets.push_back(0); } @@ -768,8 +770,14 @@ mps_parser::mps_data_model_t append_literal_cut_prefix_to_lp_model( matrix_values.push_back(coeff); } matrix_offsets.push_back(static_cast(matrix_indices.size())); + // Keep RHS / ROWS metadata aligned with appended bounds. + // Literal cut is lhs >= rhs, so row type is 'G'. + if (!constraint_rhs.empty()) { + constraint_rhs.push_back(static_cast(num_complements - 1)); + } constraint_lbs.push_back(static_cast(num_complements - 1)); constraint_ubs.push_back(std::numeric_limits::infinity()); + if (!row_types.empty()) { row_types.push_back('G'); } row_names.push_back("literal_cut_" + std::to_string(cut_idx)); } @@ -779,8 +787,12 @@ mps_parser::mps_data_model_t append_literal_cut_prefix_to_lp_model( matrix_indices.size(), matrix_offsets.data(), matrix_offsets.size()); + if (!constraint_rhs.empty()) { + model_with_cuts.set_constraint_bounds(constraint_rhs.data(), constraint_rhs.size()); + } model_with_cuts.set_constraint_lower_bounds(constraint_lbs.data(), constraint_lbs.size()); model_with_cuts.set_constraint_upper_bounds(constraint_ubs.data(), constraint_ubs.size()); + if (!row_types.empty()) { model_with_cuts.set_row_types(row_types.data(), row_types.size()); } model_with_cuts.set_row_names(row_names); return model_with_cuts; } From d408b74ce6c0d61a73bfb668abec1f8396acfd45 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 27 Apr 2026 10:51:09 -0700 Subject: [PATCH 23/68] fix bugs for indexing linear and quadratic constraints in mps_writer --- cpp/libmps_parser/src/mps_writer.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index c5cbd4b3f9..b112b53476 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -324,8 +324,11 @@ void mps_writer_t::write(const std::string& mps_file_path) : "C" + std::to_string(var_id); for (auto& nnz : nnzs) { std::string row_name; - if (static_cast(nnz.first) < problem_.get_row_names().size()) { - row_name = problem_.get_row_names()[nnz.first]; + if (static_cast(nnz.first) < static_cast(n_constraints)) { + // Linear rows: do not use row-name count here—names are optional; row id is 0..m-1. + row_name = static_cast(nnz.first) < problem_.get_row_names().size() + ? problem_.get_row_names()[nnz.first] + : "R" + std::to_string(nnz.first); } else if (static_cast(nnz.first) < static_cast(n_constraints) + quadratic_constraints.size()) { const size_t q = static_cast(nnz.first) - static_cast(n_constraints); From 1c98f5e00ff202d92daae96bc4c96547eafdfe68 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 27 Apr 2026 15:02:01 -0700 Subject: [PATCH 24/68] fix bugs in validate_constraint_bounds for cuopt_server --- .../linear_programming/data_validation.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py b/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py index e5a714add7..cb6f213cf3 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py @@ -37,23 +37,21 @@ def validate_constraint_bounds(constraint_bounds): False, "Either Row types or upper and lower bounds must be provided", ) - else: - if any( - [ - row_type not in ["E", "G", "L"] - for row_type in constraint_bounds.types - ] - ): - return ( - False, - "Row types must be E, L or G", - ) - elif not is_empty(constraint_bounds.types): - return ( - False, - "Both row types and upper and lower bounds can not be provided", - ) - elif not len(constraint_bounds.upper_bounds) == len( + if any( + [ + row_type not in ["E", "G", "L"] + for row_type in constraint_bounds.types + ] + ): + return ( + False, + "Row types must be E, L or G", + ) + return (True, "Valid constraint bounds") + + # Both upper and lower are present; they take priority over row types. Redundant + # "types" may still be present (e.g. MPS toDict). See ConstraintBounds docstring. + if not len(constraint_bounds.upper_bounds) == len( constraint_bounds.lower_bounds ): return ( From 4befacf768627f1ffb92d6d3fe16aab2f8546827 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Tue, 28 Apr 2026 00:45:39 -0700 Subject: [PATCH 25/68] precommit check correction --- .../cuopt_server/utils/linear_programming/data_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py b/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py index cb6f213cf3..ca0f8eb1f5 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/data_validation.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import numpy as np From f70e45d2ca1d79a6874379d5b8ce8bc3df7b508b Mon Sep 17 00:00:00 2001 From: Yan Zaretskiy Date: Tue, 28 Apr 2026 09:34:26 -0700 Subject: [PATCH 26/68] Refresh SOCP barrier kernels Replace the old second_order_cone wrapper with the new kernel and reduction headers, migrate barrier.cu to the new cone_data_t API, and keep cone topology handling behind barrier-local helpers. Update the dual simplex SOC kernel tests for the refreshed kernels. Signed-off-by: Yan Zaretskiy --- cpp/src/barrier/barrier.cu | 320 ++-- cpp/src/barrier/second_order_cone.cuh | 957 ----------- cpp/src/barrier/second_order_cone_kernels.cuh | 1004 ++++++++++++ .../barrier/second_order_cone_reduction.cuh | 261 +++ cpp/tests/dual_simplex/CMakeLists.txt | 2 +- .../unit_tests/second_order_cone_kernels.cu | 625 ++++++++ .../unit_tests/second_order_cone_test.cu | 1416 ----------------- 7 files changed, 2065 insertions(+), 2520 deletions(-) delete mode 100644 cpp/src/barrier/second_order_cone.cuh create mode 100644 cpp/src/barrier/second_order_cone_kernels.cuh create mode 100644 cpp/src/barrier/second_order_cone_reduction.cuh create mode 100644 cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu delete mode 100644 cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index cc0b391731..475879c8fd 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include @@ -36,6 +36,7 @@ #include #include +#include #include #include @@ -276,14 +277,15 @@ class iteration_data_t { "cone variables exceed problem dimension"); cuopt_assert(cone_var_start_ + total_cone_dim == lp.num_cols, "barrier expects [linear | cone] layout"); - cones_.emplace(static_cast(lp.second_order_cone_dims.size()), - lp.second_order_cone_dims, - raft::device_span{}, - raft::device_span{}, - stream_view_); + cones_.emplace( + std::span(lp.second_order_cone_dims.data(), lp.second_order_cone_dims.size()), + raft::device_span{}, + raft::device_span{}, + stream_view_); + cuopt_assert(cone_count() > 0, "second-order cone topology must contain at least one cone"); + cuopt_assert(cone_entry_count() == total_cone_dim, "second-order cone entry count mismatch"); } - i_t linear_xz_rhs_size = lp.num_cols; - if (cones_.has_value() && cones_->K > 0) { linear_xz_rhs_size -= cones_->m_c; } + const i_t linear_xz_rhs_size = linear_xz_size(lp.num_cols); d_complementarity_xz_rhs_.resize(linear_xz_rhs_size, stream_view_); // Allocating GPU flag data for Form ADAT @@ -349,7 +351,7 @@ class iteration_data_t { use_augmented = !Q_diagonal; } - if (cones_.has_value() && !use_augmented) { + if (has_cones() && !use_augmented) { n_dense_columns = 0; use_augmented = true; } @@ -451,6 +453,51 @@ class iteration_data_t { } } + bool has_cones() const { return cones_.has_value(); } + + cone_data_t& cones() + { + cuopt_assert(cones_.has_value(), "second-order cone data is not initialized"); + return *cones_; + } + + const cone_data_t& cones() const + { + cuopt_assert(cones_.has_value(), "second-order cone data is not initialized"); + return *cones_; + } + + i_t cone_count() const { return has_cones() ? cones_->n_cones : i_t(0); } + + i_t cone_entry_count() const + { + return has_cones() ? static_cast(cones_->n_cone_entries) : i_t(0); + } + + i_t cone_start() const { return cone_var_start_; } + + i_t cone_end() const { return cone_start() + cone_entry_count(); } + + i_t linear_xz_size(std::size_t full_xz_size) const + { + return has_cones() ? cone_start() : static_cast(full_xz_size); + } + + bool is_cone_variable(i_t variable) const + { + return has_cones() && variable >= cone_start() && variable < cone_end(); + } + + f_t complementarity_degree(std::size_t num_primal_variables, i_t num_upper_bounds) const + { + f_t degree = static_cast(num_primal_variables) + static_cast(num_upper_bounds); + if (has_cones()) { + degree -= static_cast(cone_entry_count()); + degree += static_cast(cone_count()); + } + return degree; + } + void form_augmented(bool first_call = false) { i_t n = A.n; @@ -461,21 +508,23 @@ class iteration_data_t { const f_t dual_perturb = 0.0; const f_t primal_perturb = 1e-6; - const bool has_cones = cones_.has_value() && cones_->K > 0; - const i_t m_c = has_cones ? cones_->m_c : 0; - i_t total_block_nnz = 0; - - std::vector cone_offsets_host; - std::vector cone_block_offsets_host; - if (has_cones) { - cone_offsets_host.resize(cones_->K + 1); - cone_block_offsets_host.resize(cones_->K + 1); - raft::copy( - cone_offsets_host.data(), cones_->cone_offsets.data(), cones_->K + 1, stream_view_); - raft::copy( - cone_block_offsets_host.data(), cones_->block_offsets.data(), cones_->K + 1, stream_view_); + const bool has_soc = has_cones(); + const i_t m_c = cone_entry_count(); + i_t total_block_nnz = 0; + + std::vector cone_offsets_host; + std::vector cone_block_offsets_host; + if (has_soc) { + const i_t n_cones = cone_count(); + cone_offsets_host.resize(n_cones + 1); + cone_block_offsets_host.resize(n_cones + 1); + raft::copy(cone_offsets_host.data(), cones().cone_offsets.data(), n_cones + 1, stream_view_); handle_ptr->sync_stream(); - total_block_nnz = cone_block_offsets_host[cones_->K]; + for (i_t k = 0; k < n_cones; ++k) { + const auto q_k = cone_offsets_host[k + 1] - cone_offsets_host[k]; + cone_block_offsets_host[k + 1] = cone_block_offsets_host[k] + q_k * q_k; + } + total_block_nnz = static_cast(cone_block_offsets_host[n_cones]); } if (first_call) { @@ -490,19 +539,21 @@ class iteration_data_t { for (i_t i = 0; i < n; i++) { augmented_CSR.row_start[i] = q; - const bool is_cone_row = has_cones && i >= cone_var_start_ && i < cone_var_start_ + m_c; + const bool is_cone_row = is_cone_variable(i); if (is_cone_row) { // Determine which cone this variable belongs to and its local row - i_t local_idx = i - cone_var_start_; + i_t local_idx = i - cone_start(); i_t k = 0; - while (k + 1 < cones_->K && cone_offsets_host[k + 1] <= local_idx) { + while (k + 1 < cone_count() && + cone_offsets_host[k + 1] <= static_cast(local_idx)) { k++; } - i_t local_r = local_idx - cone_offsets_host[k]; - i_t q_k = cone_offsets_host[k + 1] - cone_offsets_host[k]; - i_t cone_col_start = cone_var_start_ + cone_offsets_host[k]; - i_t block_base = cone_block_offsets_host[k] + local_r * q_k; + i_t local_r = + static_cast(static_cast(local_idx) - cone_offsets_host[k]); + i_t q_k = static_cast(cone_offsets_host[k + 1] - cone_offsets_host[k]); + i_t cone_col_start = cone_start() + static_cast(cone_offsets_host[k]); + i_t block_base = static_cast(cone_block_offsets_host[k]) + local_r * q_k; // Merge-join: Q entries (sorted) with dense cone block columns (contiguous) i_t qp = (nnzQ > 0) ? Q.col_start[i] : 0; @@ -518,19 +569,20 @@ class iteration_data_t { // Dense cone block, absorbing any Q entries that fall inside for (i_t c = 0; c < q_k; c++) { - i_t col = cone_col_start + c; - f_t q_val = (c == local_r) ? dual_perturb : f_t(0); + i_t col = cone_col_start + c; + f_t q_contrib = f_t(0); + f_t initial_val = (c == local_r) ? f_t(1) : f_t(0); if (qp < q_end && Q.i[qp] == col) { - q_val += Q.x[qp]; + q_contrib = Q.x[qp]; qp++; } cone_csr_indices_host[block_base + c] = q; - cone_Q_values_host[block_base + c] = q_val; + cone_Q_values_host[block_base + c] = q_contrib; if (col == i) { augmented_diagonal_indices[i] = q; } augmented_CSR.j[q] = col; - augmented_CSR.x[q++] = f_t(0); + augmented_CSR.x[q++] = initial_val - q_contrib; } // Q entries after cone block @@ -606,7 +658,7 @@ class iteration_data_t { augmented_diagonal_indices.size(), handle_ptr->get_stream()); - if (has_cones) { + if (has_soc) { d_cone_csr_indices_.resize(total_block_nnz, handle_ptr->get_stream()); raft::copy(d_cone_csr_indices_.data(), cone_csr_indices_host.data(), @@ -658,8 +710,8 @@ class iteration_data_t { }); RAFT_CHECK_CUDA(handle_ptr->get_stream()); - if (has_cones) { - scatter_hinv2_into_augmented(*cones_, + if (has_soc) { + scatter_hinv2_into_augmented(cones(), device_augmented.x, d_cone_csr_indices_, d_cone_Q_values_, @@ -1542,9 +1594,9 @@ class iteration_data_t { f_t beta, rmm::device_uvector& y) { - const i_t m = A.m; - const i_t n = A.n; - const bool has_cones = cones_.has_value() && cones_->K > 0; + const i_t m = A.m; + const i_t n = A.n; + const bool has_soc = has_cones(); rmm::device_uvector d_x1(n, handle_ptr->get_stream()); rmm::device_uvector d_x2(m, handle_ptr->get_stream()); @@ -1563,9 +1615,9 @@ class iteration_data_t { // diag.pairwise_product(x1, r1); // r1 <- D * x_1 pairwise_multiply(d_x1.data(), d_diag_.data(), d_r1.data(), n, stream_view_); - if (has_cones) { - thrust::fill_n( - rmm::exec_policy(stream_view_), d_r1.begin() + cone_var_start_, cones_->m_c, f_t(0)); + if (has_soc) { + const i_t m_c = cone_entry_count(); + thrust::fill_n(rmm::exec_policy(stream_view_), d_r1.begin() + cone_start(), m_c, f_t(0)); } // r1 <- Q x1 + D x1 @@ -1573,13 +1625,12 @@ class iteration_data_t { // matrix_vector_multiply(Q, 1.0, x1, 1.0, r1); cusparse_Q_view_.spmv(1.0, d_x1, 1.0, d_r1); } - if (has_cones) { - accumulate_cone_hinv2_matvec( - raft::device_span(d_x1.data() + cone_var_start_, cones_->m_c), - *cones_, - d_cone_hinv2_dx_, - raft::device_span(d_r1.data() + cone_var_start_, cones_->m_c), - stream_view_); + if (has_soc) { + const i_t m_c = cone_entry_count(); + accumulate_cone_hinv2_matvec(raft::device_span(d_x1.data() + cone_start(), m_c), + cones(), + raft::device_span(d_r1.data() + cone_start(), m_c), + stream_view_); RAFT_CHECK_CUDA(stream_view_); } @@ -1892,7 +1943,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: initial_point"); const bool use_augmented = data.use_augmented; - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const bool has_soc = data.has_cones(); // Perform a numerical factorization i_t status; @@ -2052,9 +2103,9 @@ int barrier_solver_t::initial_point(iteration_data_t& data) } } // Now handle the case with no upper bounds (skip cone variables) - const i_t cone_end = has_cones ? data.cone_var_start_ + data.cones_->m_c : 0; + const i_t cone_end = data.cone_end(); for (i_t j = 0; j < lp.num_cols; j++) { - if (has_cones && j >= data.cone_var_start_ && j < cone_end) continue; + if (has_soc && j >= data.cone_start() && j < cone_end) continue; if (lp.upper[j] == inf) { if (c[j] > epsilon_adjust) { data.z[j] = c[j]; @@ -2083,8 +2134,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.v.multiply_scalar(-1.0); data.v.ensure_positive(epsilon_adjust); - data.z.ensure_positive_skip_range( - epsilon_adjust, data.cone_var_start_, has_cones ? data.cones_->m_c : 0); + data.z.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); } else { // First compute rhs = A*Dinv*c dense_vector_t rhs(lp.num_rows); @@ -2108,8 +2158,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.gather_upper_bounds(data.z, data.v); data.v.multiply_scalar(-1.0); data.v.ensure_positive(epsilon_adjust); - data.z.ensure_positive_skip_range( - epsilon_adjust, data.cone_var_start_, has_cones ? data.cones_->m_c : 0); + data.z.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); } // Verify A'*y + z - E*v - Q*x = c @@ -2128,12 +2177,11 @@ int barrier_solver_t::initial_point(iteration_data_t& data) #endif // Make sure (w, x, v, z) > 0; skip cone vars — handled by shift_into_interior below data.w.ensure_positive(epsilon_adjust); - data.x.ensure_positive_skip_range( - epsilon_adjust, data.cone_var_start_, has_cones ? data.cones_->m_c : 0); + data.x.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); - if (has_cones) { + if (has_soc) { const auto& dims = lp.second_order_cone_dims; - i_t cs = data.cone_var_start_; + i_t cs = data.cone_start(); i_t off = 0; for (i_t k = 0; k < static_cast(dims.size()); ++k) { @@ -2336,24 +2384,19 @@ void barrier_solver_t::gpu_compute_residual_norms(const rmm::device_uv primal_residual_norm = std::max(device_vector_norm_inf(data.d_primal_residual_, stream_view_), device_vector_norm_inf(data.d_bound_residual_, stream_view_)); - dual_residual_norm = device_vector_norm_inf(data.d_dual_residual_, stream_view_); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; - const i_t linear_xz_size = - has_cones ? data.cone_var_start_ : static_cast(data.d_complementarity_xz_residual_.size()); + dual_residual_norm = device_vector_norm_inf(data.d_dual_residual_, stream_view_); + const bool has_soc = data.has_cones(); + const i_t linear_xz_size = data.linear_xz_size(data.d_complementarity_xz_residual_.size()); auto linear_xz_span = raft::device_span(data.d_complementarity_xz_residual_.data(), linear_xz_size); complementarity_residual_norm = std::max(device_vector_norm_inf(linear_xz_span, stream_view_), device_vector_norm_inf(data.d_complementarity_wv_residual_, stream_view_)); - if (has_cones) { + if (has_soc) { f_t cone_complementarity_norm = f_t(0); - auto cone_dot = data.cones_->scratch.hinv2_tail_dot(); - segmented_sum(data.d_complementarity_xz_residual_.data() + data.cone_var_start_, - cuopt::make_span(data.cones_->cone_offsets), - data.cones_->K, - cone_dot, - data.cones_->scratch.segmented_reduce_workspace, - stream_view_); + auto cone_dot = data.cones().scratch.template get_slot<0>(); + data.cones().segmented_sum( + data.d_complementarity_xz_residual_.data() + data.cone_start(), cone_dot, stream_view_); cone_complementarity_norm = thrust::reduce(rmm::exec_policy(stream_view_), cone_dot.begin(), cone_dot.end(), @@ -2369,9 +2412,8 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t& x, const rmm::device_uvector& dx) { - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; - const bool skip_cone_range = - has_cones && static_cast(x.size()) >= data.cone_var_start_ + data.cones_->m_c; + const bool has_soc = data.has_cones(); + const bool skip_cone_range = has_soc && static_cast(x.size()) >= data.cone_end(); auto ratio_test = [] HD(const thrust::tuple t) { const f_t dx = thrust::get<0>(t); @@ -2381,8 +2423,8 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_tm_c; + i_t cs = data.cone_start(); + i_t mc = data.cone_entry_count(); f_t alpha = f_t(1); if (cs > 0) { @@ -2432,10 +2474,10 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_tK > 0; - const i_t m_c = has_cones ? data.cones_->m_c : 0; - const i_t cone_var_start = data.cone_var_start_; - const i_t linear_size = has_cones ? cone_var_start : lp.num_cols; + const bool has_soc = data.has_cones(); + const i_t m_c = data.cone_entry_count(); + const i_t cone_var_start = data.cone_start(); + const i_t linear_size = data.linear_xz_size(lp.num_cols); auto fill_linear_target = [&](raft::device_span target, raft::device_span xz_rhs, @@ -2506,10 +2548,11 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_ts = raft::device_span(data.d_x_.data() + cone_var_start, m_c); - data.cones_->lambda = raft::device_span(data.d_z_.data() + cone_var_start, m_c); - launch_nt_scaling(*data.cones_, stream_view_); + if (has_soc) { + auto& cones = data.cones(); + cones.x = raft::device_span(data.d_x_.data() + cone_var_start, m_c); + cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + launch_nt_scaling(cones, stream_view_); cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); fill_linear_target( @@ -2522,13 +2565,19 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_dx_aff_.data() + cone_var_start, m_c), - *data.cones_, + raft::device_span(data.d_dz_aff_.data() + cone_var_start, m_c), + cones, data.cone_sigma_mu_, cone_target, - stream_view_, - f_t(-1)); + stream_view_); } else { - compute_affine_cone_rhs_term(*data.cones_, cone_target, stream_view_, f_t(-1)); + cub::DeviceTransform::Transform( + cones.z.data(), + cone_target.data(), + m_c, + [] HD(f_t z_val) { return -z_val; }, + stream_view_.value()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); } RAFT_CHECK_CUDA(stream_view_); } else { @@ -2558,7 +2607,7 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t(data.d_dx_.data() + cone_var_start, m_c), - *data.cones_, + data.cones(), raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c), - data.d_cone_hinv2_dx_, raft::device_span(data.d_dz_.data() + cone_var_start, m_c), stream_view_); @@ -3179,8 +3227,7 @@ template void barrier_solver_t::compute_affine_rhs(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: compute_affine_rhs"); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; - const i_t linear_size = has_cones ? data.cone_var_start_ : lp.num_cols; + const i_t linear_size = data.linear_xz_size(lp.num_cols); data.primal_rhs = data.primal_residual; data.bound_rhs = data.bound_residual; @@ -3221,7 +3268,7 @@ void barrier_solver_t::compute_target_mu( iteration_data_t& data, f_t mu, f_t& mu_aff, f_t& sigma, f_t& new_mu) { raft::common::nvtx::range fun_scope("Barrier: compute_target_mu"); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const bool has_soc = data.has_cones(); f_t complementarity_aff_sum = 0.0; // TMP no copy and data should always be on the GPU @@ -3240,14 +3287,12 @@ void barrier_solver_t::compute_target_mu( f_t step_dual_aff = std::min(gpu_max_step_to_boundary(data, data.d_v_, data.d_dv_aff_), gpu_max_step_to_boundary(data, data.d_z_, data.d_dz_aff_)); - if (has_cones) { - i_t cs = data.cone_var_start_; - i_t mc = data.cones_->m_c; + if (has_soc) { + i_t cs = data.cone_start(); + i_t mc = data.cone_entry_count(); auto [cone_p, cone_d] = - compute_cone_step_length(*data.cones_, - raft::device_span(data.d_x_.data() + cs, mc), + compute_cone_step_length(data.cones(), raft::device_span(data.d_dx_aff_.data() + cs, mc), - raft::device_span(data.d_z_.data() + cs, mc), raft::device_span(data.d_dz_aff_.data() + cs, mc), std::min(step_primal_aff, step_dual_aff), stream_view_); @@ -3256,7 +3301,7 @@ void barrier_solver_t::compute_target_mu( step_dual_aff = step_primal_aff; } - if (data.Q.n > 0 || has_cones) { + if (data.Q.n > 0 || has_soc) { step_primal_aff = step_dual_aff = std::min(step_primal_aff, step_dual_aff); } @@ -3309,22 +3354,18 @@ void barrier_solver_t::compute_target_mu( stream_view_); complementarity_aff_sum = complementarity_xz_aff_sum + complementarity_wv_aff_sum; - f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); - if (has_cones) { - mu_denom -= static_cast(data.cones_->m_c); - mu_denom += static_cast(data.cones_->K); - } - mu_aff = complementarity_aff_sum / mu_denom; - sigma = std::max(0.0, std::min(1.0, std::pow(mu_aff / mu, 3.0))); - new_mu = sigma * mu; + f_t mu_denom = data.complementarity_degree(data.x.size(), data.n_upper_bounds); + mu_aff = complementarity_aff_sum / mu_denom; + sigma = std::max(0.0, std::min(1.0, std::pow(mu_aff / mu, 3.0))); + new_mu = sigma * mu; } template void barrier_solver_t::compute_cc_rhs(iteration_data_t& data, f_t& new_mu) { raft::common::nvtx::range fun_scope("Barrier: compute_cc_rhs"); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; - const i_t linear_size = has_cones ? data.cone_var_start_ : lp.num_cols; + const bool has_soc = data.has_cones(); + const i_t linear_size = data.linear_xz_size(lp.num_cols); auto fill_linear_cc_rhs = [&](raft::device_span out, raft::device_span dx_aff, @@ -3352,8 +3393,8 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data data.primal_rhs.set_scalar(0.0); data.bound_rhs.set_scalar(0.0); data.dual_rhs.set_scalar(0.0); - data.cone_combined_step_ = has_cones; - data.cone_sigma_mu_ = has_cones ? new_mu : f_t(0); + data.cone_combined_step_ = has_soc; + data.cone_sigma_mu_ = has_soc ? new_mu : f_t(0); } template @@ -3426,7 +3467,7 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ f_t& step_dual) { raft::common::nvtx::range fun_scope("Barrier: compute_primal_dual_step_length"); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; + const bool has_soc = data.has_cones(); f_t max_step_primal = 0.0; f_t max_step_dual = 0.0; @@ -3435,14 +3476,12 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ max_step_dual = std::min(gpu_max_step_to_boundary(data, data.d_v_, data.d_dv_), gpu_max_step_to_boundary(data, data.d_z_, data.d_dz_)); - if (has_cones) { - i_t cs = data.cone_var_start_; - i_t mc = data.cones_->m_c; + if (has_soc) { + i_t cs = data.cone_start(); + i_t mc = data.cone_entry_count(); auto [cone_primal, cone_dual] = - compute_cone_step_length(*data.cones_, - raft::device_span(data.d_x_.data() + cs, mc), + compute_cone_step_length(data.cones(), raft::device_span(data.d_dx_.data() + cs, mc), - raft::device_span(data.d_z_.data() + cs, mc), raft::device_span(data.d_dz_.data() + cs, mc), f_t(1), stream_view_); @@ -3453,7 +3492,7 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ step_primal = step_scale * max_step_primal; step_dual = step_scale * max_step_dual; - if (data.Q.n > 0 || has_cones) { step_primal = step_dual = std::min(step_primal, step_dual); } + if (data.Q.n > 0 || has_soc) { step_primal = step_dual = std::min(step_primal, step_dual); } } template @@ -3543,13 +3582,7 @@ template void barrier_solver_t::compute_mu(iteration_data_t& data, f_t& mu) { raft::common::nvtx::range fun_scope("Barrier: compute_mu"); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; - - f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); - if (has_cones) { - mu_denom -= static_cast(data.cones_->m_c); - mu_denom += static_cast(data.cones_->K); - } + f_t mu_denom = data.complementarity_degree(data.x.size(), data.n_upper_bounds); mu = (data.sum_reduce_helper_.sum(data.d_complementarity_xz_residual_.begin(), data.d_complementarity_xz_residual_.size(), @@ -3819,7 +3852,8 @@ lp_status_t barrier_solver_t::solve(f_t start_time, settings.log.printf("Quadratic objective matrix: %d nonzeros\n", lp.Q.row_start[lp.Q.n]); } if (lp.second_order_cone_dims.size() > 0) { - settings.log.printf("Second-order cones: %d\n", static_cast(lp.second_order_cone_dims.size())); + settings.log.printf("Second-order cones: %d\n", + static_cast(lp.second_order_cone_dims.size())); } settings.log.printf("\n"); @@ -3881,18 +3915,17 @@ lp_status_t barrier_solver_t::solve(f_t start_time, f_t primal_residual_norm = std::max(vector_norm_inf(data.primal_residual, stream_view_), vector_norm_inf(data.bound_residual, stream_view_)); - f_t dual_residual_norm = vector_norm_inf(data.dual_residual, stream_view_); - const bool has_cones = data.cones_.has_value() && data.cones_->K > 0; - const i_t linear_xz_size = - has_cones ? data.cone_var_start_ : static_cast(data.complementarity_xz_residual.size()); + f_t dual_residual_norm = vector_norm_inf(data.dual_residual, stream_view_); + const bool has_soc = data.has_cones(); + const i_t linear_xz_size = data.linear_xz_size(data.complementarity_xz_residual.size()); auto linear_xz_span = raft::host_span(data.complementarity_xz_residual.data(), linear_xz_size); f_t complementarity_residual_norm = std::max(vector_norm_inf(linear_xz_span, stream_view_), vector_norm_inf(data.complementarity_wv_residual, stream_view_)); - if (has_cones) { + if (has_soc) { f_t cone_complementarity_norm = f_t(0); - i_t off = data.cone_var_start_; + i_t off = data.cone_start(); for (auto q_k : lp.second_order_cone_dims) { f_t cone_dot = f_t(0); for (i_t j = 0; j < q_k; ++j) { @@ -3904,11 +3937,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, complementarity_residual_norm = std::max(complementarity_residual_norm, cone_complementarity_norm); } - f_t mu_denom = static_cast(n) + static_cast(num_upper_bounds); - if (data.cones_.has_value() && data.cones_->K > 0) { - mu_denom -= static_cast(data.cones_->m_c); - mu_denom += static_cast(data.cones_->K); - } + f_t mu_denom = data.complementarity_degree(n, num_upper_bounds); f_t mu = (data.complementarity_xz_residual.sum() + data.complementarity_wv_residual.sum()) / mu_denom; @@ -3960,8 +3989,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, (duality_gap_abs < settings.barrier_relative_complementarity_tol || duality_gap_rel < settings.barrier_relative_complementarity_tol); - const i_t linear_xz_rhs_size = - has_cones ? data.cone_var_start_ : static_cast(data.complementarity_xz_rhs.size()); + const i_t linear_xz_rhs_size = data.linear_xz_size(data.complementarity_xz_rhs.size()); data.d_complementarity_xz_residual_.resize(data.complementarity_xz_residual.size(), stream_view_); diff --git a/cpp/src/barrier/second_order_cone.cuh b/cpp/src/barrier/second_order_cone.cuh deleted file mode 100644 index 3487707f12..0000000000 --- a/cpp/src/barrier/second_order_cone.cuh +++ /dev/null @@ -1,957 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#pragma once - -#include -#include - -#include - -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include -#include - -namespace cuopt::linear_programming::dual_simplex { - -// --------------------------------------------------------------------------- -// Flat cone kernels: segmented reductions compute per-cone scalars, then a -// single elementwise launch applies the result across all packed cone entries. -// This keeps the cone math vectorized instead of one block per cone. -// --------------------------------------------------------------------------- - -constexpr int flat_block_dim = 256; - -template -__global__ void apply_hinv2_write_kernel(raft::device_span v, - raft::device_span out, - raft::device_span w_bar, - raft::device_span inv_eta, - raft::device_span tail_dot, - raft::device_span cone_offsets, - raft::device_span element_cone_ids, - f_t output_scale) -{ - i_t flat_idx = static_cast(blockIdx.x * blockDim.x + threadIdx.x); - if (flat_idx >= static_cast(out.size())) return; - - i_t cone = element_cone_ids[flat_idx]; - i_t cone_off = cone_offsets[cone]; - i_t local_idx = flat_idx - cone_off; - - f_t ie_sq = inv_eta[cone] * inv_eta[cone]; - f_t u_tv = w_bar[cone_off] * v[cone_off] - tail_dot[cone]; - f_t coeff = f_t(2) * u_tv * ie_sq; - int sign = (local_idx == 0) * 2 - 1; - f_t value = coeff * w_bar[flat_idx] - ie_sq * v[flat_idx]; - out[flat_idx] = output_scale * value * sign; -} - -template -struct corrector_raw_t { - f_t zeta; - f_t xi; - f_t psi; -}; - -template -struct corrector_raw_sum_t { - HD corrector_raw_t operator()(const corrector_raw_t& lhs, - const corrector_raw_t& rhs) const - { - return {lhs.zeta + rhs.zeta, lhs.xi + rhs.xi, lhs.psi + rhs.psi}; - } -}; - -template -struct cone_scratch_t { - i_t K; - rmm::device_uvector> corrector_raw; // [K] {zeta, xi, psi} - rmm::device_uvector scalar_slots; // [6 * K] reusable K-length scalar scratch slots - rmm::device_uvector step_alpha_primal; - rmm::device_uvector step_alpha_dual; - rmm::device_uvector segmented_reduce_workspace; - - cone_scratch_t(i_t K_in, rmm::cuda_stream_view stream) - : K(K_in), - corrector_raw(K_in, stream), - scalar_slots(6 * K_in, stream), - step_alpha_primal(K_in, stream), - step_alpha_dual(K_in, stream), - segmented_reduce_workspace(0, stream) - { - } - - raft::device_span hinv2_tail_dot() { return slot_span(0); } - raft::device_span step_s_du1_sq() { return slot_span(0); } - raft::device_span step_s_u1du1() { return slot_span(1); } - raft::device_span step_s_u1_sq() { return slot_span(2); } - raft::device_span step_l_du1_sq() { return slot_span(3); } - raft::device_span step_l_u1du1() { return slot_span(4); } - raft::device_span step_l_u1_sq() { return slot_span(5); } - - raft::device_span nt_s1_sq() { return slot_span(0); } - raft::device_span nt_l1_sq() { return slot_span(1); } - raft::device_span nt_sl() { return slot_span(2); } - - raft::device_span step_alpha_primal_span() { return cuopt::make_span(step_alpha_primal); } - raft::device_span step_alpha_dual_span() { return cuopt::make_span(step_alpha_dual); } - - private: - raft::device_span slot_span(i_t slot) - { - return raft::device_span(scalar_slots.data() + slot * K, K); - } -}; - -template -__global__ void fused_corrector_write_kernel(raft::device_span s, - raft::device_span lambda, - raft::device_span dx_aff, - raft::device_span omega, - raft::device_span w_bar, - raft::device_span inv_eta, - raft::device_span inv_1pw0, - raft::device_span rho, - raft::device_span> raw, - raft::device_span out, - raft::device_span cone_offsets, - raft::device_span element_cone_ids, - f_t sigma_mu, - f_t output_scale) -{ - i_t flat_idx = static_cast(blockIdx.x * blockDim.x + threadIdx.x); - if (flat_idx >= static_cast(out.size())) return; - - i_t cone = element_cone_ids[flat_idx]; - i_t cone_off = cone_offsets[cone]; - i_t local_idx = flat_idx - cone_off; - f_t ie = inv_eta[cone]; - f_t ipw = inv_1pw0[cone]; - f_t w0 = w_bar[cone_off]; - f_t omega0 = omega[cone_off]; - f_t dx_a0 = dx_aff[cone_off]; - auto raw_vals = raw[cone]; - f_t coeff_a = -dx_a0 + raw_vals.zeta * ipw; - f_t dx0 = (w0 * dx_a0 - raw_vals.zeta) * ie; - f_t dz0 = -omega0 - dx0; - f_t w_sq_sum = max(f_t(0), w0 * w0 - f_t(1)); - f_t w_omega_sum = f_t(0.5) * (ie * s[cone_off] - lambda[cone_off] / ie); - f_t omega_sq_sum = max(f_t(0), omega0 * omega0 - rho[cone]); - f_t omega_dx_sum = ie * (raw_vals.xi + coeff_a * w_omega_sum); - f_t dx_sq_sum = - ie * ie * (raw_vals.psi + f_t(2) * coeff_a * raw_vals.zeta + coeff_a * coeff_a * w_sq_sum); - f_t r_K_0 = (omega0 * omega0 + omega_sq_sum) + (dx0 * dz0 - omega_dx_sum - dx_sq_sum) - sigma_mu; - f_t nu = (f_t(2) * omega0 - dx0) * omega_sq_sum - (omega0 + f_t(2) * dx0) * omega_dx_sum; - f_t inv_rho = f_t(1) / rho[cone]; - f_t corr0 = (omega0 * r_K_0 - nu) * inv_rho; - f_t inv_omega0 = f_t(1) / omega0; - f_t c_inv = (nu * inv_omega0 - r_K_0) * inv_rho; - f_t p1 = c_inv + f_t(2) - dx0 * inv_omega0; - f_t p2 = -(f_t(1) + f_t(2) * dx0 * inv_omega0); - f_t w_dx_sum = ie * (raw_vals.zeta + coeff_a * w_sq_sum); - f_t zeta2 = p1 * w_omega_sum + p2 * w_dx_sum; - f_t coeff_c = -corr0 + zeta2 * ipw; - - if (local_idx == 0) { - out[flat_idx] = output_scale * ((w0 * corr0 - zeta2) * ie); - return; - } - - f_t dx_j = (dx_aff[flat_idx] + coeff_a * w_bar[flat_idx]) * ie; - f_t corr_j = p1 * omega[flat_idx] + p2 * dx_j; - out[flat_idx] = output_scale * ((corr_j + coeff_c * w_bar[flat_idx]) * ie); -} - -// --------------------------------------------------------------------------- -// Flattened NT scaling / step-length kernels. -// All follow the same pattern: segmented reduction to per-cone scalars, then -// flat or scalar kernels to write the packed cone outputs. -// --------------------------------------------------------------------------- - -template -__global__ void nt_scaling_scalar_kernel(raft::device_span s, - raft::device_span lambda, - raft::device_span cone_offsets, - raft::device_span s1_sq, - raft::device_span l1_sq, - raft::device_span sl, - raft::device_span inv_eta, - raft::device_span inv_1pw0, - raft::device_span w_bar, - raft::device_span omega, - raft::device_span rho, - i_t K) -{ - i_t cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - f_t s0 = s[off]; - f_t l0 = lambda[off]; - - f_t s_J = sqrt(max(f_t(0), s0 * s0 - s1_sq[cone])); - f_t l_J = sqrt(max(f_t(0), l0 * l0 - l1_sq[cone])); - f_t inv_s_J = f_t(1) / s_J; - f_t inv_l_J = f_t(1) / l_J; - f_t rho_val = s_J * l_J; - f_t inv_eta_v = sqrt(l_J / s_J); - f_t scale = sqrt(rho_val); - - f_t s_dot_l = (s0 * l0 + sl[cone]) * inv_s_J * inv_l_J; - f_t gamma = sqrt(max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); - f_t inv_2g = f_t(1) / (f_t(2) * gamma); - f_t sb0 = s0 * inv_s_J; - f_t lb0 = l0 * inv_l_J; - - f_t w0 = (sb0 + lb0) * inv_2g; - inv_eta[cone] = inv_eta_v; - inv_1pw0[cone] = f_t(1) / (f_t(1) + w0); - w_bar[off] = w0; - omega[off] = gamma * scale; - rho[cone] = rho_val; -} - -template -__global__ void nt_scaling_tail_kernel(raft::device_span s, - raft::device_span lambda, - raft::device_span inv_eta, - raft::device_span rho, - raft::device_span w_bar, - raft::device_span omega, - raft::device_span cone_offsets, - raft::device_span element_cone_ids) -{ - i_t flat_idx = static_cast(blockIdx.x * blockDim.x + threadIdx.x); - if (flat_idx >= static_cast(w_bar.size())) return; - - i_t cone = element_cone_ids[flat_idx]; - i_t cone_off = cone_offsets[cone]; - if (flat_idx == cone_off) return; - - f_t s0 = s[cone_off]; - f_t l0 = lambda[cone_off]; - f_t inv_eta_val = inv_eta[cone]; - f_t rho_val = rho[cone]; - f_t scale = sqrt(rho_val); - - f_t s_J = scale / inv_eta_val; - f_t l_J = scale * inv_eta_val; - f_t inv_s_J = f_t(1) / s_J; - f_t inv_l_J = f_t(1) / l_J; - - f_t gamma = omega[cone_off] / scale; - f_t inv_2g = f_t(1) / (f_t(2) * gamma); - f_t sb0 = s0 * inv_s_J; - f_t lb0 = l0 * inv_l_J; - f_t D = sb0 + lb0 + f_t(2) * gamma; - f_t inv_D = f_t(1) / D; - f_t c_s = (gamma + sb0) * inv_D; - f_t c_l = (gamma + lb0) * inv_D; - - f_t w_from_s = inv_2g * inv_s_J; - f_t w_from_lambda = -inv_2g * inv_l_J; - f_t omega_s_coeff = scale * c_l * inv_s_J; - f_t omega_lambda_coeff = scale * c_s * inv_l_J; - - f_t sj = s[flat_idx]; - f_t lj = lambda[flat_idx]; - w_bar[flat_idx] = w_from_s * sj + w_from_lambda * lj; - omega[flat_idx] = omega_s_coeff * sj + omega_lambda_coeff * lj; -} - -template -DI f_t cone_step_length_from_scalars(f_t u0, f_t du0, f_t du1_sq, f_t u1du1, f_t c, f_t alpha_max) -{ - f_t a = du0 * du0 - du1_sq; - f_t b = u0 * du0 - u1du1; - f_t disc = b * b - a * c; - f_t alpha = alpha_max; - - if (du0 < f_t(0)) { alpha = min(alpha, -u0 / du0); } - if ((a > f_t(0) && b > f_t(0)) || disc < f_t(0)) { - return alpha; - } else if (a == f_t(0)) { - if (b < f_t(0)) { alpha = min(alpha, c / (f_t(-2) * b)); } - } else if (c == f_t(0)) { - alpha = (a >= f_t(0)) ? alpha : f_t(0); - } else { - f_t t = -(b + copysign(sqrt(disc), b)); - f_t r1 = c / t; - f_t r2 = t / a; - if (r1 < f_t(0)) { r1 = alpha; } - if (r2 < f_t(0)) { r2 = alpha; } - alpha = min(alpha, min(r1, r2)); - } - return alpha; -} - -template -__global__ void step_length_pair_kernel(raft::device_span s, - raft::device_span ds, - raft::device_span lambda, - raft::device_span dlambda, - raft::device_span alpha_primal, - raft::device_span alpha_dual, - raft::device_span s_du1_sq, - raft::device_span s_u1du1, - raft::device_span s_u1_sq, - raft::device_span l_du1_sq, - raft::device_span l_u1du1, - raft::device_span l_u1_sq, - raft::device_span cone_offsets, - f_t alpha_max, - i_t K) -{ - i_t cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); - if (cone >= K) return; - - i_t off = cone_offsets[cone]; - f_t s_c = max(f_t(0), s[off] * s[off] - s_u1_sq[cone]); - f_t l_c = max(f_t(0), lambda[off] * lambda[off] - l_u1_sq[cone]); - - alpha_primal[cone] = cone_step_length_from_scalars( - s[off], ds[off], s_du1_sq[cone], s_u1du1[cone], s_c, alpha_max); - alpha_dual[cone] = cone_step_length_from_scalars( - lambda[off], dlambda[off], l_du1_sq[cone], l_u1du1[cone], l_c, alpha_max); -} - -/** - * Device storage for second-order cone topology, NT scaling, and iterate views. - * - * Flat arrays are packed by cone: elements [cone_offsets[i], cone_offsets[i+1]) - * belong to cone i, which has dimension cone_dims[i]. - * - * Primal/dual iterates (s, lambda) are non-owning spans, pre-sliced by the - * caller to cover the cone portion of the global x/z vectors. The caller - * must keep the underlying memory alive. - * - * Only persistent cone state lives here. Reusable per-iteration workspace sits - * under `scratch`, which keeps the mutating temporary buffers out of the - * persistent NT state. - */ -template -struct cone_data_t { - // --- Topology (set once at construction) --- - i_t K; // number of second-order cones - i_t m_c; // total cone dimension = sum of cone_dims - - rmm::device_uvector cone_offsets; // [K+1] prefix sums of cone_dims - rmm::device_uvector cone_dims; // [K] dimension q_i of each cone - rmm::device_uvector block_offsets; // [K+1] prefix sums of q_i^2 (for dense block build) - rmm::device_uvector block_entry_cone_ids; // [sum q_i^2] owning cone id for each block entry - - // --- Primal/dual cone iterates (non-owning views, set by caller) --- - raft::device_span s; // [m_c] cone slack: s_i in int(Q^{q_i}) - raft::device_span lambda; // [m_c] cone dual: lambda_i in int(Q^{q_i}) - - // --- NT scaling state (recomputed each iteration from s, lambda) --- - rmm::device_uvector - inv_eta; // [K] 1/eta_i where eta_i = (||s_i||_J / ||lambda_i||_J)^{1/2} - rmm::device_uvector inv_1pw0; // [K] cached 1/(1 + wbar_0_i) - rmm::device_uvector w_bar; // [m_c] NT scaling direction, unit J-norm, packed by cone - rmm::device_uvector omega; // [m_c] scaled variable omega_i = H_i^{-1} s_i, packed by cone - rmm::device_uvector rho; // [K] ||omega_i||^2_J = ||s_i||_J * ||lambda_i||_J - rmm::device_uvector element_cone_ids; // [m_c] owning cone id for each packed entry - cone_scratch_t scratch; - - cone_data_t(i_t K_in, - const std::vector& dims, - raft::device_span s_in, - raft::device_span lambda_in, - rmm::cuda_stream_view stream) - : K(K_in), - m_c(std::accumulate(dims.begin(), dims.end(), i_t(0))), - cone_offsets(K_in + 1, stream), - cone_dims(K_in, stream), - block_offsets(K_in + 1, stream), - block_entry_cone_ids( - std::accumulate( - dims.begin(), dims.end(), i_t(0), [](i_t acc, i_t q) { return acc + q * q; }), - stream), - s(s_in), - lambda(lambda_in), - inv_eta(K_in, stream), - inv_1pw0(K_in, stream), - w_bar(m_c, stream), - omega(m_c, stream), - rho(K_in, stream), - element_cone_ids(m_c, stream), - scratch(K_in, stream) - { - std::vector offsets(K + 1, 0); - std::vector blk_offsets(K + 1, 0); - std::vector cone_ids(m_c, 0); - std::vector block_cone_ids(block_entry_cone_ids.size(), 0); - - for (i_t i = 0; i < K; ++i) { - offsets[i + 1] = offsets[i] + dims[i]; - blk_offsets[i + 1] = blk_offsets[i] + dims[i] * dims[i]; - std::fill(cone_ids.begin() + offsets[i], cone_ids.begin() + offsets[i + 1], i); - std::fill( - block_cone_ids.begin() + blk_offsets[i], block_cone_ids.begin() + blk_offsets[i + 1], i); - } - - auto init_device_vec = [&](auto& d_vec, const auto& h_vec) { - if (!h_vec.empty()) { - d_vec.resize(h_vec.size(), stream); - raft::copy(d_vec.data(), h_vec.data(), h_vec.size(), stream); - } - }; - - raft::copy(cone_offsets.data(), offsets.data(), K + 1, stream); - raft::copy(cone_dims.data(), dims.data(), K, stream); - raft::copy(block_offsets.data(), blk_offsets.data(), K + 1, stream); - init_device_vec(block_entry_cone_ids, block_cone_ids); - init_device_vec(element_cone_ids, cone_ids); - } -}; - -template -void segmented_sum(InputIt input, - raft::device_span cone_offsets, - i_t K, - raft::device_span out, - rmm::device_uvector& workspace, - rmm::cuda_stream_view stream) -{ - if (K == 0) return; - cuopt_assert(static_cast(out.size()) == K, "segmented_sum output must match cone count"); - - std::size_t temp_storage_bytes = 0; - cub::DeviceSegmentedReduce::Sum(nullptr, - temp_storage_bytes, - input, - out.data(), - K, - cone_offsets.data(), - cone_offsets.data() + 1, - stream.value()); - if (workspace.size() < temp_storage_bytes) { workspace.resize(temp_storage_bytes, stream); } - cub::DeviceSegmentedReduce::Sum(workspace.data(), - temp_storage_bytes, - input, - out.data(), - K, - cone_offsets.data(), - cone_offsets.data() + 1, - stream.value()); - RAFT_CUDA_TRY(cudaPeekAtLastError()); -} - -template -void segmented_reduce(InputIt input, - raft::device_span cone_offsets, - i_t K, - rmm::device_uvector& out, - rmm::device_uvector& workspace, - ReduceOp reduce_op, - t_t initial_value, - rmm::cuda_stream_view stream) -{ - out.resize(K, stream); - if (K == 0) return; - - std::size_t temp_storage_bytes = 0; - cub::DeviceSegmentedReduce::Reduce(nullptr, - temp_storage_bytes, - input, - out.data(), - K, - cone_offsets.data(), - cone_offsets.data() + 1, - reduce_op, - initial_value, - stream.value()); - if (workspace.size() < temp_storage_bytes) { workspace.resize(temp_storage_bytes, stream); } - cub::DeviceSegmentedReduce::Reduce(workspace.data(), - temp_storage_bytes, - input, - out.data(), - K, - cone_offsets.data(), - cone_offsets.data() + 1, - reduce_op, - initial_value, - stream.value()); - RAFT_CUDA_TRY(cudaPeekAtLastError()); -} - -template -void apply_hinv2(raft::device_span v, - raft::device_span out, - raft::device_span w_bar, - raft::device_span inv_eta, - raft::device_span cone_offsets, - raft::device_span element_cone_ids, - raft::device_span tail_dot, - rmm::device_uvector& workspace, - i_t K, - rmm::cuda_stream_view stream, - f_t output_scale = f_t(1)) -{ - if (K == 0) return; - - auto span_v = v; - auto span_w_bar = w_bar; - auto span_cone_offsets = cone_offsets; - auto span_element_cone_ids = element_cone_ids; - auto tail_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [span_v, span_w_bar, span_cone_offsets, span_element_cone_ids] HD(i_t idx) { - i_t cone = span_element_cone_ids[idx]; - i_t cone_off = span_cone_offsets[cone]; - return (idx == cone_off) ? f_t(0) : span_w_bar[idx] * span_v[idx]; - }); - segmented_sum(tail_terms, cone_offsets, K, tail_dot, workspace, stream); - - i_t grid_dim = (static_cast(out.size()) + flat_block_dim - 1) / flat_block_dim; - apply_hinv2_write_kernel<<>>( - v, out, w_bar, inv_eta, tail_dot, cone_offsets, element_cone_ids, output_scale); - RAFT_CUDA_TRY(cudaPeekAtLastError()); -} - -template -void apply_hinv2(raft::device_span v, - raft::device_span out, - cone_data_t& cones, - rmm::cuda_stream_view stream, - f_t output_scale = f_t(1)) -{ - apply_hinv2(v, - out, - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.cone_offsets), - cuopt::make_span(cones.element_cone_ids), - cones.scratch.hinv2_tail_dot(), - cones.scratch.segmented_reduce_workspace, - cones.K, - stream, - output_scale); -} - -template -void compute_affine_cone_rhs_term(cone_data_t& cones, - raft::device_span out, - rmm::cuda_stream_view stream, - f_t output_scale = f_t(1)) -{ - cuopt_assert(static_cast(out.size()) == cones.m_c, "cone rhs span must match cone size"); - if (cones.K == 0) return; - - apply_hinv2(cones.s, out, cones, stream, output_scale); -} - -template -void compute_combined_cone_rhs_term(raft::device_span dx_aff, - cone_data_t& cones, - f_t sigma_mu, - raft::device_span out, - rmm::cuda_stream_view stream, - f_t output_scale = f_t(1)) -{ - cuopt_assert(static_cast(out.size()) == cones.m_c, "cone rhs span must match cone size"); - if (cones.K == 0) return; - - auto span_dx_aff = dx_aff; - auto span_w_bar = cuopt::make_span(cones.w_bar); - auto span_omega = cuopt::make_span(cones.omega); - auto span_cone_offsets = cuopt::make_span(cones.cone_offsets); - auto span_element_cone_id = cuopt::make_span(cones.element_cone_ids); - - auto raw_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [span_dx_aff, span_w_bar, span_omega, span_cone_offsets, span_element_cone_id] HD(i_t idx) { - i_t cone = span_element_cone_id[idx]; - i_t cone_off = span_cone_offsets[cone]; - if (idx == cone_off) { return corrector_raw_t{f_t(0), f_t(0), f_t(0)}; } - f_t dx_aff_j = span_dx_aff[idx]; - return corrector_raw_t{ - span_w_bar[idx] * dx_aff_j, span_omega[idx] * dx_aff_j, dx_aff_j * dx_aff_j}; - }); - segmented_reduce>(raw_terms, - cuopt::make_span(cones.cone_offsets), - cones.K, - cones.scratch.corrector_raw, - cones.scratch.segmented_reduce_workspace, - corrector_raw_sum_t{}, - corrector_raw_t{f_t(0), f_t(0), f_t(0)}, - stream); - - i_t grid_dim = (cones.m_c + flat_block_dim - 1) / flat_block_dim; - fused_corrector_write_kernel - <<>>(cones.s, - cones.lambda, - dx_aff, - cuopt::make_span(cones.omega), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.inv_1pw0), - cuopt::make_span(cones.rho), - cuopt::make_span(cones.scratch.corrector_raw), - out, - cuopt::make_span(cones.cone_offsets), - cuopt::make_span(cones.element_cone_ids), - sigma_mu, - output_scale); - RAFT_CUDA_TRY(cudaPeekAtLastError()); -} - -template -void recover_cone_dz_from_target(raft::device_span dx, - cone_data_t& cones, - raft::device_span cone_target, - rmm::device_uvector& hinv2_dx, - raft::device_span dz, - rmm::cuda_stream_view stream) -{ - hinv2_dx.resize(cones.m_c, stream); - if (cones.K == 0) return; - - apply_hinv2(dx, cuopt::make_span(hinv2_dx), cones, stream); - - auto span_target = cone_target; - auto span_hinv2 = cuopt::make_span(hinv2_dx); - auto span_dz = dz; - thrust::for_each_n(rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - cones.m_c, - [span_target, span_hinv2, span_dz] __device__(i_t j) { - span_dz[j] = span_target[j] - span_hinv2[j]; - }); -} - -template -void accumulate_cone_hinv2_matvec(raft::device_span x, - cone_data_t& cones, - rmm::device_uvector& hinv2_x, - raft::device_span out, - rmm::cuda_stream_view stream) -{ - hinv2_x.resize(cones.m_c, stream); - if (cones.K == 0) return; - - apply_hinv2(x, cuopt::make_span(hinv2_x), cones, stream); - - auto span_hinv2 = cuopt::make_span(hinv2_x); - auto span_out = out; - thrust::for_each_n(rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - cones.m_c, - [span_hinv2, span_out] __device__(i_t j) { span_out[j] += span_hinv2[j]; }); -} - -// --------------------------------------------------------------------------- -// Compute flat H^{-2} cone-block entries and scatter them into the augmented -// CSR value array. -// -// The caller provides one flat entry per dense cone-block element: -// - `csr_indices[e]` gives the destination slot in `augmented_x` -// - `q_values[e]` stores any pre-merged Q contribution for that slot -// -// For each flat entry we load its precomputed owning cone id, recover local -// (r, c) coordinates, evaluate H_k^{-2}(r, c), and write -// -(H_k^{-2}(r, c) + q_values[e]) -// into `augmented_x[csr_indices[e]]`. -// --------------------------------------------------------------------------- -template -__global__ void scatter_hinv2_into_augmented_kernel( - raft::device_span augmented_x, - raft::device_span csr_indices, - raft::device_span q_values, - raft::device_span w_bar, - raft::device_span inv_eta, - raft::device_span cone_offsets, - raft::device_span block_offsets, - raft::device_span block_entry_cone_ids) -{ - i_t e = static_cast(blockIdx.x * blockDim.x + threadIdx.x); - if (e >= static_cast(csr_indices.size())) return; - - i_t cone = block_entry_cone_ids[e]; - i_t off = cone_offsets[cone]; - i_t q = cone_offsets[cone + 1] - off; - i_t blk_off = block_offsets[cone]; - i_t local = e - blk_off; - i_t r = local / q; - i_t c = local % q; - - f_t ie_sq = inv_eta[cone] * inv_eta[cone]; - f_t w0 = w_bar[off]; - f_t u_r = (r == 0) ? w0 : -w_bar[off + r]; - f_t u_c = (c == 0) ? w0 : -w_bar[off + c]; - f_t val = f_t(2) * u_r * ie_sq * u_c; - f_t diag_correction = (r == 0) ? -ie_sq : ie_sq; - if (r == c) { val += diag_correction; } - - augmented_x[csr_indices[e]] = -val - q_values[e]; -} - -template -void scatter_hinv2_into_augmented(const cone_data_t& cones, - rmm::device_uvector& augmented_x, - const rmm::device_uvector& csr_indices, - const rmm::device_uvector& q_values, - rmm::cuda_stream_view stream) -{ - i_t count = static_cast(csr_indices.size()); - if (count == 0) return; - - cuopt_assert(count == static_cast(cones.block_entry_cone_ids.size()), - "scatter expects one flat entry per cone-block coefficient"); - - i_t grid_dim = (count + flat_block_dim - 1) / flat_block_dim; - scatter_hinv2_into_augmented_kernel - <<>>(cuopt::make_span(augmented_x), - cuopt::make_span(csr_indices), - cuopt::make_span(q_values), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.cone_offsets), - cuopt::make_span(cones.block_offsets), - cuopt::make_span(cones.block_entry_cone_ids)); - RAFT_CUDA_TRY(cudaPeekAtLastError()); -} - -// --------------------------------------------------------------------------- -// Compute per-cone step lengths, then reduce them to the global maximum -// feasible primal/dual step. -// --------------------------------------------------------------------------- -template -void compute_cone_step_length_per_cone(cone_data_t& cones, - raft::device_span x_K, - raft::device_span dx_K, - raft::device_span z_K, - raft::device_span dz_K, - raft::device_span alpha_primal, - raft::device_span alpha_dual, - f_t alpha_max, - rmm::cuda_stream_view stream) -{ - cuopt_assert(static_cast(alpha_primal.size()) == cones.K && - static_cast(alpha_dual.size()) == cones.K, - "step-length outputs must match cone count"); - if (cones.K == 0) return; - - auto span_offsets = cuopt::make_span(cones.cone_offsets); - auto span_elem = cuopt::make_span(cones.element_cone_ids); - - auto s_du1_sq = cones.scratch.step_s_du1_sq(); - auto s_u1du1 = cones.scratch.step_s_u1du1(); - auto s_u1_sq = cones.scratch.step_s_u1_sq(); - auto l_du1_sq = cones.scratch.step_l_du1_sq(); - auto l_u1du1 = cones.scratch.step_l_u1du1(); - auto l_u1_sq = cones.scratch.step_l_u1_sq(); - - auto span_x_K = x_K; - auto span_dx_K = dx_K; - auto span_z_K = z_K; - auto span_dz_K = dz_K; - - auto s_du1_sq_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), [span_dx_K, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_dx_K[idx] * span_dx_K[idx]; - }); - segmented_sum(s_du1_sq_terms, - span_offsets, - cones.K, - s_du1_sq, - cones.scratch.segmented_reduce_workspace, - stream); - - auto s_u1du1_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [span_x_K, span_dx_K, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_x_K[idx] * span_dx_K[idx]; - }); - segmented_sum(s_u1du1_terms, - span_offsets, - cones.K, - s_u1du1, - cones.scratch.segmented_reduce_workspace, - stream); - - auto s_u1_sq_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), [span_x_K, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_x_K[idx] * span_x_K[idx]; - }); - segmented_sum(s_u1_sq_terms, - span_offsets, - cones.K, - s_u1_sq, - cones.scratch.segmented_reduce_workspace, - stream); - - auto l_du1_sq_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), [span_dz_K, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_dz_K[idx] * span_dz_K[idx]; - }); - segmented_sum(l_du1_sq_terms, - span_offsets, - cones.K, - l_du1_sq, - cones.scratch.segmented_reduce_workspace, - stream); - - auto l_u1du1_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [span_z_K, span_dz_K, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_z_K[idx] * span_dz_K[idx]; - }); - segmented_sum(l_u1du1_terms, - span_offsets, - cones.K, - l_u1du1, - cones.scratch.segmented_reduce_workspace, - stream); - - auto l_u1_sq_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), [span_z_K, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_z_K[idx] * span_z_K[idx]; - }); - segmented_sum(l_u1_sq_terms, - span_offsets, - cones.K, - l_u1_sq, - cones.scratch.segmented_reduce_workspace, - stream); - - i_t grid_dim = (cones.K + flat_block_dim - 1) / flat_block_dim; - step_length_pair_kernel<<>>(x_K, - dx_K, - z_K, - dz_K, - alpha_primal, - alpha_dual, - s_du1_sq, - s_u1du1, - s_u1_sq, - l_du1_sq, - l_u1du1, - l_u1_sq, - span_offsets, - alpha_max, - cones.K); - RAFT_CUDA_TRY(cudaPeekAtLastError()); -} - -template -std::pair compute_cone_step_length(cone_data_t& cones, - raft::device_span x_K, - raft::device_span dx_K, - raft::device_span z_K, - raft::device_span dz_K, - f_t alpha_max, - rmm::cuda_stream_view stream) -{ - if (cones.K == 0) return {alpha_max, alpha_max}; - - auto alpha_primal = cones.scratch.step_alpha_primal_span(); - auto alpha_dual = cones.scratch.step_alpha_dual_span(); - - compute_cone_step_length_per_cone( - cones, x_K, dx_K, z_K, dz_K, alpha_primal, alpha_dual, alpha_max, stream); - - f_t primal = thrust::reduce(rmm::exec_policy(stream), - alpha_primal.begin(), - alpha_primal.end(), - alpha_max, - thrust::minimum()); - f_t dual = thrust::reduce(rmm::exec_policy(stream), - alpha_dual.begin(), - alpha_dual.end(), - alpha_max, - thrust::minimum()); - return {primal, dual}; -} - -template -void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) -{ - if (cones.K == 0) return; - - auto nt_s1_sq = cones.scratch.nt_s1_sq(); - auto nt_l1_sq = cones.scratch.nt_l1_sq(); - auto nt_sl = cones.scratch.nt_sl(); - - auto span_s = cones.s; - auto span_lambda = cones.lambda; - auto span_offsets = cuopt::make_span(cones.cone_offsets); - auto span_elem = cuopt::make_span(cones.element_cone_ids); - - auto s1_sq_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), [span_s, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_s[idx] * span_s[idx]; - }); - segmented_sum( - s1_sq_terms, span_offsets, cones.K, nt_s1_sq, cones.scratch.segmented_reduce_workspace, stream); - - auto l1_sq_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), [span_lambda, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_lambda[idx] * span_lambda[idx]; - }); - segmented_sum( - l1_sq_terms, span_offsets, cones.K, nt_l1_sq, cones.scratch.segmented_reduce_workspace, stream); - - auto sl_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [span_s, span_lambda, span_offsets, span_elem] HD(i_t idx) { - i_t cone = span_elem[idx]; - return (idx == span_offsets[cone]) ? f_t(0) : span_s[idx] * span_lambda[idx]; - }); - segmented_sum( - sl_terms, span_offsets, cones.K, nt_sl, cones.scratch.segmented_reduce_workspace, stream); - - i_t scalar_grid_dim = (cones.K + flat_block_dim - 1) / flat_block_dim; - nt_scaling_scalar_kernel - <<>>(cones.s, - cones.lambda, - span_offsets, - nt_s1_sq, - nt_l1_sq, - nt_sl, - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.inv_1pw0), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.omega), - cuopt::make_span(cones.rho), - cones.K); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - - i_t grid_dim = (cones.m_c + flat_block_dim - 1) / flat_block_dim; - nt_scaling_tail_kernel - <<>>(cones.s, - cones.lambda, - cuopt::make_span(cones.inv_eta), - cuopt::make_span(cones.rho), - cuopt::make_span(cones.w_bar), - cuopt::make_span(cones.omega), - span_offsets, - span_elem); - RAFT_CUDA_TRY(cudaPeekAtLastError()); -} - -} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/barrier/second_order_cone_kernels.cuh b/cpp/src/barrier/second_order_cone_kernels.cuh new file mode 100644 index 0000000000..42568fcc9b --- /dev/null +++ b/cpp/src/barrier/second_order_cone_kernels.cuh @@ -0,0 +1,1004 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// ============================================================================= +// SOC (second-order cone) kernels for the cuOpt barrier solver. +// +// x_soc : cone primal block +// z_soc : cone dual block +// W, W^{-1} : Nesterov-Todd scaling matrix and inverse. W is symmetric for +// SOC, so W^{-T} = W^{-1} +// H : W^{-1} W^{-T} = W^{-2}, the cone KKT block added to the +// primal-reduced system +// eta : sqrt(x_J / z_J), where x_J = sqrt(det_J(x_soc)) +// w : NT scaling direction with det_J(w) = 1 and +// w[head] = sqrt(1 + ||w_tail||^2) +// +// Cone vectors are packed flat: +// entries [cone_offsets[i], cone_offsets[i + 1]) belong to cone i. +// ============================================================================= + +namespace cuopt::linear_programming::dual_simplex { + +inline constexpr int soc_block_size = 256; + +/** + * Reusable device workspace for second-order cone kernels. + * + * The scratch object owns only temporary storage. Kernels may reuse the scalar + * slots and `temp_cone` sequentially inside a higher-level operation, but no + * persistent NT scaling or iterate state is stored here. + */ +template +struct cone_scratch_t { + i_t n_cones; // number of SOC blocks + std::size_t n_cone_entries; // total packed cone dimension + + rmm::device_uvector slots; // [n_slots * n_cones] + + // Per-cone step candidates before the final min reduction. + rmm::device_uvector step_alpha_primal; // [n_cones] + rmm::device_uvector step_alpha_dual; // [n_cones] + + // TODO: Consider moving this out to the barrier layer when we wire it in + rmm::device_uvector temp_cone; // [n_cone_entries] + + cone_scratch_t(i_t n_cones_in, std::size_t n_cone_entries_in, rmm::cuda_stream_view stream) + : n_cones(n_cones_in), + n_cone_entries(n_cone_entries_in), + slots(0, stream), + step_alpha_primal(0, stream), + step_alpha_dual(0, stream), + temp_cone(0, stream) + { + const auto n_cones_size = static_cast(n_cones); + + slots.resize(n_cones_size * static_cast(n_slots), stream); + step_alpha_primal.resize(n_cones_size, stream); + step_alpha_dual.resize(n_cones_size, stream); + temp_cone.resize(n_cone_entries, stream); + } + + template + raft::device_span get_slot() const + { + static_assert(slot_idx >= 0 && slot_idx < n_slots, "scratch slot index out of range"); + const auto n_cones_size = static_cast(n_cones); + const auto begin = static_cast(slot_idx) * n_cones_size; + const auto end = begin + n_cones_size; + return cuopt::make_span(slots, begin, end); + } + + template + raft::device_span get_slot() + { + const auto const_slot = static_cast(*this).template get_slot(); + return raft::device_span(const_cast(const_slot.data()), const_slot.size()); + } +}; + +struct to_size_t_t { + template + HD std::size_t operator()(value_t value) const + { + return value; + } +}; + +template +HD f_t cone_step_length_from_scalars( + f_t u0, f_t du0, f_t du_tail_sq, f_t u_tail_du_tail, f_t u_tail_sq, f_t alpha_max) +{ + const auto a = du0 * du0 - du_tail_sq; + const auto b = u0 * du0 - u_tail_du_tail; + const auto c_raw = u0 * u0 - u_tail_sq; + const auto c = c_raw > 0 ? c_raw : 0; + const auto disc = b * b - a * c; + auto alpha = alpha_max; + + if (du0 < 0) { alpha = cuda::std::min(alpha, -u0 / du0); } + + if ((a > 0 && b > 0) || disc < 0) { return alpha; } + + if (a == 0) { + if (b < 0) { alpha = cuda::std::min(alpha, c / (-2 * b)); } + } else if (c == 0) { + alpha = a >= 0 ? alpha : 0; + } else { + const auto t = -(b + copysign(sqrt(disc), b)); + auto r1 = c / t; + auto r2 = t / a; + if (r1 < 0) { r1 = alpha; } + if (r2 < 0) { r2 = alpha; } + alpha = cuda::std::min(alpha, cuda::std::min(r1, r2)); + } + + return alpha; +} + +template +__global__ void __launch_bounds__(soc_block_size) + step_length_single_kernel(raft::device_span u, + raft::device_span du, + raft::device_span alpha, + raft::device_span du_tail_sq, + raft::device_span u_tail_du_tail, + raft::device_span u_tail_sq, + raft::device_span cone_offsets, + f_t alpha_max, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + const auto off = cone_offsets[cone]; + alpha[cone] = cone_step_length_from_scalars( + u[off], du[off], du_tail_sq[cone], u_tail_du_tail[cone], u_tail_sq[cone], alpha_max); +} + +/** + * Device storage for second-order cone topology, NT scaling, and iterate views. + * + * Flat arrays are packed by cone: entries + * [cone_offsets[i], cone_offsets[i + 1]) belong to cone i, whose dimension is + * cone_dimensions[i]. + * + * The primal/dual cone vectors are non-owning spans over the SOC slice of the + * solver's global x/z vectors. The caller must keep the underlying storage + * alive for the lifetime of this object. + */ +template +struct cone_data_t { + // Topology. This is immutable after construction. + i_t n_cones; // number of SOC blocks + std::size_t n_cone_entries; // total packed cone dimension = sum(cone_dimensions) + + rmm::device_uvector cone_offsets; // [n_cones + 1], prefix sum of dimensions + rmm::device_uvector cone_dimensions; // [n_cones], dimension q_i of each cone + // Owning cone per entry for upcoming flat per-entry SOC kernels. + rmm::device_uvector element_cone_ids; // [n_cone_entries] + segmented_sum_t segmented_sum; + + // Non-owning iterate views over the cone portion of x/z. + raft::device_span x; // [n_cone_entries], SOC primal block + raft::device_span z; // [n_cone_entries], SOC dual block + + // Persistent Nesterov-Todd scaling state, recomputed from x/z each iteration. + rmm::device_uvector eta; // [n_cones], sqrt(|x|_J / |z|_J) + rmm::device_uvector w; // [n_cone_entries], unit-J-norm NT direction + + cone_scratch_t scratch; + + cone_data_t(std::span cone_dimensions_host, + raft::device_span x_in, + raft::device_span z_in, + rmm::cuda_stream_view stream) + : n_cones(cone_dimensions_host.size()), + n_cone_entries( + std::reduce(cone_dimensions_host.begin(), cone_dimensions_host.end(), std::size_t{0})), + cone_offsets(n_cones + 1, stream), + cone_dimensions(n_cones, stream), + element_cone_ids(n_cone_entries, stream), + segmented_sum(cone_dimensions_host, cuopt::make_span(cone_offsets), stream), + x(x_in), + z(z_in), + eta(n_cones, stream), + w(n_cone_entries, stream), + scratch(n_cones, n_cone_entries, stream) + { + raft::copy(cone_dimensions.data(), cone_dimensions_host.data(), n_cones, stream); + cone_offsets.set_element_to_zero_async(0, stream); + auto policy = rmm::exec_policy(stream); + + auto cone_dimensions_as_offsets = + thrust::make_transform_iterator(cone_dimensions.begin(), to_size_t_t{}); + thrust::inclusive_scan(policy, + cone_dimensions_as_offsets, + cone_dimensions_as_offsets + n_cones, + cone_offsets.begin() + 1, + cuda::std::plus{}); + + thrust::upper_bound(policy, + cone_offsets.begin() + 1, + cone_offsets.end(), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(n_cone_entries), + element_cone_ids.begin()); + segmented_sum.template prepare_workspace(stream); + } +}; + +template +__global__ void __launch_bounds__(soc_block_size) + nt_finalize_scaling_scalars_kernel(raft::device_span x, + raft::device_span z, + raft::device_span x_scale, + raft::device_span z_scale, + raft::device_span eta, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + const auto off = cone_offsets[cone]; + const auto x_tail_norm = sqrt(x_scale[cone]); + const auto z_tail_norm = sqrt(z_scale[cone]); + const auto x_det = (x[off] - x_tail_norm) * (x[off] + x_tail_norm); + const auto z_det = (z[off] - z_tail_norm) * (z[off] + z_tail_norm); + + x_scale[cone] = sqrt(x_det); + z_scale[cone] = sqrt(z_det); + eta[cone] = sqrt(x_scale[cone] / z_scale[cone]); +} + +template +__global__ void __launch_bounds__(soc_block_size) + nt_finalize_w_scale_kernel(raft::device_span normalized_dot, i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + normalized_dot[cone] = sqrt(2 + 2 * normalized_dot[cone]); +} + +/** + * Write normalized w_tail directly: + * + * w_tail = (x_tail / x_scale - z_tail / z_scale) / w_scale. + * + * The head is zeroed temporarily and overwritten after reducing + * ||w_tail||^2. + */ +template +__global__ void __launch_bounds__(soc_block_size) + nt_write_w_tail_kernel(raft::device_span x, + raft::device_span z, + raft::device_span x_scale, + raft::device_span z_scale, + raft::device_span w_scale, + raft::device_span w, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= w.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + if (idx == cone_off) { + w[idx] = 0; + return; + } + + w[idx] = (x[idx] / x_scale[cone] - z[idx] / z_scale[cone]) / w_scale[cone]; +} + +template +__global__ void __launch_bounds__(soc_block_size) + nt_finalize_head_kernel(raft::device_span w, + raft::device_span normalized_tail_sq, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + w[cone_offsets[cone]] = sqrt(1 + normalized_tail_sq[cone]); +} + +/** + * Build Nesterov-Todd scaling for packed SOC blocks. + * + * Given interior cone primal/dual blocks x and z: + * + * x_scale = sqrt(det_J(x)), z_scale = sqrt(det_J(z)) + * eta = sqrt(x_scale / z_scale) + * w_scale = sqrt(2 + 2 * dot(x / x_scale, z / z_scale)) + * w_tail = (x_tail / x_scale - z_tail / z_scale) / w_scale + * w_0 = sqrt(1 + ||w_tail||^2) to re-impose det_J(w) = 1 + * + * Scratch slots: + * 0: ||x_tail||^2 -> x_scale + * 1: ||z_tail||^2 -> z_scale + * 2: dot(x / x_scale, z / z_scale) -> w_scale -> ||w_tail||^2 + */ +template +void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) +{ + auto x_scale = cones.scratch.template get_slot<0>(); + auto z_scale = cones.scratch.template get_slot<1>(); + auto w_scale = cones.scratch.template get_slot<2>(); + + const auto span_x = cones.x; + const auto span_z = cones.z; + const auto cone_offsets = cuopt::make_span(cones.cone_offsets); + const auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + + auto x_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_x, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : span_x[idx] * span_x[idx]; + }); + cones.segmented_sum(x_tail_sq_terms, x_scale, stream); + + auto z_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_z, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : span_z[idx] * span_z[idx]; + }); + cones.segmented_sum(z_tail_sq_terms, z_scale, stream); + + const auto cone_grid_dim = + raft::ceildiv(static_cast(cones.n_cones), soc_block_size); + nt_finalize_scaling_scalars_kernel + <<>>( + cones.x, cones.z, x_scale, z_scale, cuopt::make_span(cones.eta), cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + const auto element_grid_dim = raft::ceildiv(cones.n_cone_entries, soc_block_size); + auto normalized_dot_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_x, span_z, x_scale, z_scale, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return span_x[idx] * span_z[idx] / (x_scale[cone] * z_scale[cone]); + }); + cones.segmented_sum(normalized_dot_terms, w_scale, stream); + + nt_finalize_w_scale_kernel + <<>>(w_scale, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + auto w = cuopt::make_span(cones.w); + nt_write_w_tail_kernel<<>>( + cones.x, cones.z, x_scale, z_scale, w_scale, w, cone_offsets, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + // Reduce ||w_tail||^2 per cone. The head entries are zero, so the same + // flat iterator can feed the segmented reduction. + auto normalized_tail_terms = + thrust::make_transform_iterator(thrust::make_counting_iterator(0), + [cone_offsets, element_cone_ids, w] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * w[idx]; + }); + cones.segmented_sum(normalized_tail_terms, w_scale, stream); + + nt_finalize_head_kernel<<>>( + cuopt::make_span(cones.w), w_scale, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +template +__global__ void __launch_bounds__(soc_block_size) + apply_w_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto w0 = w[cone_off]; + const auto zeta = tail_dot[cone]; + const auto inv_1pw0 = 1 / (1 + w0); + const auto v0 = v[cone_off]; + + if (local_idx == 0) { + out[idx] = eta[cone] * (w0 * v0 + zeta); + return; + } + + const auto coeff = v0 + zeta * inv_1pw0; + out[idx] = eta[cone] * (v[idx] + coeff * w[idx]); +} + +template +__global__ void __launch_bounds__(soc_block_size) + apply_w_inv_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto w0 = w[cone_off]; + const auto zeta = tail_dot[cone]; + const auto inv_1pw0 = 1 / (1 + w0); + const auto v0 = v[cone_off]; + const auto inv_eta = 1 / eta[cone]; + + if (local_idx == 0) { + out[idx] = inv_eta * (w0 * v0 - zeta); + return; + } + + const auto coeff = -v0 + zeta * inv_1pw0; + out[idx] = inv_eta * (v[idx] + coeff * w[idx]); +} + +template +__global__ void __launch_bounds__(soc_block_size) + apply_hinv2_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + raft::device_span bias, + f_t output_scale, + f_t bias_scale) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto inv_eta_sq = 1 / (eta[cone] * eta[cone]); + const auto rho = w[cone_off] * v[cone_off] - tail_dot[cone]; + const auto coeff = 2 * rho * inv_eta_sq; + const int sign = (local_idx == 0) * 2 - 1; + const auto value = coeff * w[idx] - inv_eta_sq * v[idx]; + const auto h_value = output_scale * value * sign; + + out[idx] = bias.empty() ? h_value : bias_scale * bias[idx] + h_value; +} + +template +__global__ void __launch_bounds__(soc_block_size) + gather_cone_heads_kernel(raft::device_span values, + raft::device_span heads, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + heads[cone] = values[cone_offsets[cone]]; +} + +/** + * Build the Mehrotra corrector shift: + * + * d = (W^{-1} dx_aff) o (W dz_aff) - sigma_mu e. + * + * On entry, `scaled_dx` is W^{-1} dx_aff and `scaled_dz` is W dz_aff. The + * cone head uses the full dot product, and tail entries use the SOC Jordan + * product: + * + * d_0 = - sigma_mu + * d_tail = scaled_dx_0 * scaled_dz_tail + scaled_dz_0 * scaled_dx_tail. + */ +template +__global__ void __launch_bounds__(soc_block_size) + combined_cone_shift_write_kernel(raft::device_span shift, + raft::device_span scaled_dx, + raft::device_span scaled_dz, + raft::device_span full_dot, + raft::device_span scaled_dx_head, + raft::device_span scaled_dz_head, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + f_t sigma_mu) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= shift.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + if (local_idx == 0) { + shift[idx] = full_dot[cone] - sigma_mu; + return; + } + + shift[idx] = scaled_dx_head[cone] * scaled_dz[idx] + scaled_dz_head[cone] * scaled_dx[idx]; +} + +/** + * Per-cone scalar stage for p = lambda \ d: + * + * p_0 = (lambda_0 d_0 - ) / det_J(lambda) + * inv_lambda_0 = 1 / lambda_0. + * + * A second flat kernel writes `-p`, which lets the final W^{-1} call produce + * q = -W^{-1} p without adding an output-scale argument to W^{-1}. + */ +template +__global__ void __launch_bounds__(soc_block_size) + jordan_divide_by_lambda_scalar_kernel(raft::device_span shift, + raft::device_span nt_point, + raft::device_span lambda_tail_dot, + raft::device_span lambda_tail_sq, + raft::device_span p0, + raft::device_span inv_lambda0, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + const auto cone_off = cone_offsets[cone]; + const auto lambda0 = nt_point[cone_off]; + const auto lambda_tail_norm = sqrt(lambda_tail_sq[cone]); + const auto det_lambda = (lambda0 - lambda_tail_norm) * (lambda0 + lambda_tail_norm); + + p0[cone] = (lambda0 * shift[cone_off] - lambda_tail_dot[cone]) / det_lambda; + inv_lambda0[cone] = 1 / lambda0; +} + +template +__global__ void __launch_bounds__(soc_block_size) + jordan_divide_by_lambda_write_kernel(raft::device_span shift, + raft::device_span nt_point, + raft::device_span p0, + raft::device_span inv_lambda0, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + raft::device_span out) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + if (local_idx == 0) { + out[idx] = -p0[cone]; + return; + } + + out[idx] = (p0[cone] * nt_point[idx] - shift[idx]) * inv_lambda0[cone]; +} + +/** + * Apply the Nesterov-Todd scaling matrix: out = W v. + * + * For each cone: + * zeta = + * (Wv)_0 = eta * (w_0 v_0 + zeta) + * (Wv)_tail = eta * (v_tail + (v_0 + zeta / (1 + w_0)) w_tail) + */ +template +void apply_w(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream) +{ + auto w = cuopt::make_span(cones.w); + auto eta = cuopt::make_span(cones.eta); + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto tail_dot = cones.scratch.template get_slot<0>(); + + auto tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [v, w, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * v[idx]; + }); + cones.segmented_sum(tail_terms, tail_dot, stream); + + const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); + apply_w_write_kernel<<>>( + v, out, w, eta, tail_dot, cone_offsets, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Apply the inverse Nesterov-Todd scaling matrix: + * out = W^{-1} v. + * + * For each cone, + * zeta = + * (W^{-1}v)_0 = eta^{-1} * (w_0 v_0 - zeta) + * (W^{-1}v)_tail = + * eta^{-1} * (v_tail + (-v_0 + zeta / (1 + w_0)) w_tail) + */ +template +void apply_w_inv(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream) +{ + auto w = cuopt::make_span(cones.w); + auto eta = cuopt::make_span(cones.eta); + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto tail_dot = cones.scratch.template get_slot<0>(); + + auto tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [v, w, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * v[idx]; + }); + cones.segmented_sum(tail_terms, tail_dot, stream); + + const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); + apply_w_inv_write_kernel<<>>( + v, out, w, eta, tail_dot, cone_offsets, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Apply the cone KKT block H = W^{-1} W^{-T} = W^{-2}. + * + * With zeta = and rho = w_0 v_0 - zeta: + * (Hv)_0 = eta^{-2} (2 w_0 rho - v_0) + * (Hv)_tail = eta^{-2} (v_tail - 2 w_tail rho) + */ +template +void apply_hinv2(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream, + f_t output_scale = 1, + raft::device_span bias = {}, + f_t bias_scale = 0) +{ + auto w = cuopt::make_span(cones.w); + auto eta = cuopt::make_span(cones.eta); + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto tail_dot = cones.scratch.template get_slot<0>(); + + auto tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [v, w, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * v[idx]; + }); + cones.segmented_sum(tail_terms, tail_dot, stream); + + const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); + apply_hinv2_write_kernel<<>>( + v, out, w, eta, tail_dot, cone_offsets, element_cone_ids, bias, output_scale, bias_scale); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Recover the SOC dual direction after the reduced KKT solve. + * + * The reduced solve gives `dx`; the cone equation supplies the target RHS. + * This function applies the cone block H = W^{-2} and writes: + * dz = cone_target - H dx. + */ +template +void recover_cone_dz_from_target(raft::device_span dx, + cone_data_t& cones, + raft::device_span cone_target, + raft::device_span dz, + rmm::cuda_stream_view stream) +{ + apply_hinv2(dx, dz, cones, stream, -1, cone_target, 1); +} + +/** + * Accumulate the SOC cone-block matvec into an existing output vector. + * + * Used by matrix-free products with the primal-reduced KKT block: + * out += H x, where H = W^{-2}. + */ +template +void accumulate_cone_hinv2_matvec(raft::device_span x, + cone_data_t& cones, + raft::device_span out, + rmm::cuda_stream_view stream) +{ + auto out_input = raft::device_span(out.data(), out.size()); + apply_hinv2(x, out, cones, stream, 1, out_input, 1); +} + +template +__global__ void __launch_bounds__(soc_block_size) + scatter_hinv2_into_augmented_kernel(raft::device_span augmented_x, + raft::device_span csr_indices, + raft::device_span q_values, + raft::device_span w, + raft::device_span eta, + raft::device_span cone_offsets, + raft::device_span block_offsets, + i_t n_cones) +{ + const auto e = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (e >= csr_indices.size()) { return; } + + i_t lo = 0; + i_t hi = n_cones; + while (lo < hi) { + const i_t mid = lo + (hi - lo) / 2; + if (block_offsets[mid + 1] <= e) { + lo = mid + 1; + } else { + hi = mid; + } + } + + const auto cone = lo; + const auto off = cone_offsets[cone]; + const auto q = cone_offsets[cone + 1] - off; + const auto blk_off = block_offsets[cone]; + const auto local = e - blk_off; + const auto r = local / q; + const auto c = local % q; + + const auto inv_eta_sq = 1 / (eta[cone] * eta[cone]); + const auto w0 = w[off]; + const auto u_r = (r == 0) ? w0 : -w[off + r]; + const auto u_c = (c == 0) ? w0 : -w[off + c]; + auto val = f_t{2} * u_r * inv_eta_sq * u_c; + const auto diag_correction = (r == 0) ? -inv_eta_sq : inv_eta_sq; + if (r == c) { val += diag_correction; } + + augmented_x[csr_indices[e]] = -val - q_values[e]; +} + +template +void scatter_hinv2_into_augmented(const cone_data_t& cones, + rmm::device_uvector& augmented_x, + const rmm::device_uvector& csr_indices, + const rmm::device_uvector& q_values, + rmm::cuda_stream_view stream) +{ + const auto count = csr_indices.size(); + if (count == 0) { return; } + cuopt_assert(count == q_values.size(), "cone CSR index and Q-value arrays must match"); + + rmm::device_uvector block_offsets(cones.n_cones + 1, stream); + block_offsets.set_element_to_zero_async(0, stream); + + auto block_sizes = thrust::make_transform_iterator( + cones.cone_dimensions.begin(), + [] HD(i_t q) -> std::size_t { return static_cast(q) * q; }); + thrust::inclusive_scan( + rmm::exec_policy(stream), block_sizes, block_sizes + cones.n_cones, block_offsets.begin() + 1); + + const auto grid = raft::ceildiv(count, soc_block_size); + scatter_hinv2_into_augmented_kernel + <<>>(cuopt::make_span(augmented_x), + cuopt::make_span(csr_indices), + cuopt::make_span(q_values), + cuopt::make_span(cones.w), + cuopt::make_span(cones.eta), + cuopt::make_span(cones.cone_offsets), + cuopt::make_span(block_offsets), + cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Compute the maximum primal and dual step lengths that keep SOC blocks + * feasible: + * + * x + alpha dx in Q, z + alpha dz in Q, alpha <= alpha_max. + * + * For one cone u + alpha du, feasibility is + * + * u_0 + alpha du_0 >= ||u_tail + alpha du_tail||. + * + * Squaring gives the quadratic + * + * c + 2 b alpha + a alpha^2 >= 0, + * + * where c = det_J(u), b = u_0 du_0 - , and + * a = det_J(du). The per-cone kernel below solves for the first boundary + * crossing, and the final reductions take the global minimum over cones. + */ +template +std::pair compute_cone_step_length(cone_data_t& cones, + raft::device_span dx, + raft::device_span dz, + f_t alpha_max, + rmm::cuda_stream_view stream) +{ + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto slot_0 = cones.scratch.template get_slot<0>(); + auto slot_1 = cones.scratch.template get_slot<1>(); + auto slot_2 = cones.scratch.template get_slot<2>(); + + auto run_pass = [&](raft::device_span u, + raft::device_span du, + raft::device_span alpha) { + auto du_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [du, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : du[idx] * du[idx]; + }); + cones.segmented_sum(du_tail_sq_terms, slot_0, stream); + + auto u_tail_du_tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [u, du, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : u[idx] * du[idx]; + }); + cones.segmented_sum(u_tail_du_tail_terms, slot_1, stream); + + auto u_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [u, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : u[idx] * u[idx]; + }); + cones.segmented_sum(u_tail_sq_terms, slot_2, stream); + + const auto grid_dim = + raft::ceildiv(static_cast(cones.n_cones), soc_block_size); + step_length_single_kernel<<>>( + u, du, alpha, slot_0, slot_1, slot_2, cone_offsets, alpha_max, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + }; + + auto alpha_primal = cuopt::make_span(cones.scratch.step_alpha_primal); + auto alpha_dual = cuopt::make_span(cones.scratch.step_alpha_dual); + + run_pass(cones.x, dx, alpha_primal); + run_pass(cones.z, dz, alpha_dual); + + const auto primal = thrust::reduce(rmm::exec_policy(stream), + alpha_primal.begin(), + alpha_primal.end(), + alpha_max, + thrust::minimum()); + const auto dual = thrust::reduce(rmm::exec_policy(stream), + alpha_dual.begin(), + alpha_dual.end(), + alpha_max, + thrust::minimum()); + + return {primal, dual}; +} + +/** + * Build the SOC corrector target for the reduced KKT solve. + * + * Mehrotra's corrector uses affine cone directions to form + * + * d = (W^{-1} dx_aff) o (W dz_aff) - sigma_mu e, + * + * where `o` is the SOC Jordan product and `e = (1, 0, ..., 0)` per cone. + * The reduced KKT solve needs the cone target + * + * q = -W^{-1} p, where p = lambda \ d and lambda = W z. + * + * On return, `out` holds `q`. Internally, `out` is reused for `W dz_aff` and + * then `d`; `scratch.temp_cone` is reused for `W^{-1} dx_aff`, then `lambda`, + * then `-p`. + */ +template +void compute_combined_cone_rhs_term(raft::device_span dx_aff, + raft::device_span dz_aff, + cone_data_t& cones, + f_t sigma_mu, + raft::device_span out, + rmm::cuda_stream_view stream) +{ + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + + auto scratch_cone = cuopt::make_span(cones.scratch.temp_cone); + auto scaled_dx = raft::device_span(scratch_cone.data(), scratch_cone.size()); + auto scaled_dz = raft::device_span(out.data(), out.size()); + auto slot_0 = cones.scratch.template get_slot<0>(); + auto slot_1 = cones.scratch.template get_slot<1>(); + auto slot_2 = cones.scratch.template get_slot<2>(); + + apply_w_inv(dx_aff, scratch_cone, cones, stream); + apply_w(dz_aff, out, cones, stream); + + auto full_product_terms = thrust::make_transform_iterator( + thrust::make_zip_iterator(scaled_dx.begin(), scaled_dz.begin()), + thrust::make_zip_function([] HD(f_t dx, f_t dz) -> f_t { return dx * dz; })); + cones.segmented_sum(full_product_terms, slot_0, stream); + + // `out` currently aliases W dz_aff and is about to be overwritten with d. + // Stage both head vectors first because every tail entry needs them. + const auto cone_grid_dim = + raft::ceildiv(static_cast(cones.n_cones), soc_block_size); + gather_cone_heads_kernel<<>>( + scaled_dx, slot_1, cone_offsets, cones.n_cones); + gather_cone_heads_kernel<<>>( + scaled_dz, slot_2, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + const auto element_grid_dim = raft::ceildiv(cones.n_cone_entries, soc_block_size); + combined_cone_shift_write_kernel + <<>>( + out, scaled_dx, scaled_dz, slot_0, slot_1, slot_2, cone_offsets, element_cone_ids, sigma_mu); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + // Form lambda = W z in scratch_cone. At this point W^{-1} dx_aff is dead. + apply_w(cones.z, scratch_cone, cones, stream); + + auto shift = raft::device_span(out.data(), out.size()); + auto nt_point = raft::device_span(scratch_cone.data(), scratch_cone.size()); + + auto lambda_tail_dot_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [shift, nt_point, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : nt_point[idx] * shift[idx]; + }); + cones.segmented_sum(lambda_tail_dot_terms, slot_0, stream); + + auto lambda_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [nt_point, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : nt_point[idx] * nt_point[idx]; + }); + cones.segmented_sum(lambda_tail_sq_terms, slot_1, stream); + + jordan_divide_by_lambda_scalar_kernel + <<>>( + shift, nt_point, slot_0, slot_1, slot_0, slot_1, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + jordan_divide_by_lambda_write_kernel + <<>>( + shift, nt_point, slot_0, slot_1, cone_offsets, element_cone_ids, scratch_cone); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + apply_w_inv(scratch_cone, out, cones, stream); +} + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/barrier/second_order_cone_reduction.cuh b/cpp/src/barrier/second_order_cone_reduction.cuh new file mode 100644 index 0000000000..ada9fcfb6a --- /dev/null +++ b/cpp/src/barrier/second_order_cone_reduction.cuh @@ -0,0 +1,261 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +__global__ void __launch_bounds__(warps_per_cta* raft::WarpSize) + warp_per_cone_reduce_kernel(InputIt input, + raft::device_span small_cone_ids, + raft::device_span cone_offsets, + OutputIt output, + value_t init); + +/** + * Segmented-sum dispatcher for packed second-order cone vectors. + * + * Cone dimensions are fixed for a solve, so the constructor partitions cone + * ids once by reduction strategy. Each call then reuses those partitions: + * small cones use one warp per cone, medium cones use CUB DeviceSegmentedReduce, + * and large cones use CUB DeviceReduce one cone at a time. The object owns the + * CUB workspace for those medium/large paths. Call `prepare_workspace` once + * before using a CUB-backed path. + */ +template +struct segmented_sum_t { + static_assert(warp_cone_dim > 0); + static_assert(large_cone_cutoff > warp_cone_dim); + + raft::device_span cone_offsets; + rmm::device_uvector small_cone_ids; // cone dimension <= warp_cone_dim + rmm::device_uvector medium_cone_ids; // warp_cone_dim < cone dimension <= large_cone_cutoff + + std::vector large_cone_offsets; + std::vector large_cone_ids; + std::vector large_cone_dimensions; + + // Maximum CUB temporary storage needed by prepared medium/large reductions. + std::size_t cub_workspace_bytes = 0; + rmm::device_buffer cub_workspace; + + private: + template + void prepare_workspace_for_type(rmm::cuda_stream_view stream) + { + auto input = thrust::make_constant_iterator(value_t{}); + auto output = thrust::make_discard_iterator(); + + if (!medium_cone_ids.is_empty()) { + const auto medium_begin_offsets = + thrust::make_permutation_iterator(cone_offsets.data(), medium_cone_ids.begin()); + const auto medium_end_offsets = + thrust::make_permutation_iterator(cone_offsets.data() + 1, medium_cone_ids.begin()); + + std::size_t temp_storage_bytes = 0; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Sum(nullptr, + temp_storage_bytes, + input, + output, + medium_cone_ids.size(), + medium_begin_offsets, + medium_end_offsets, + stream.value())); + cub_workspace_bytes = std::max(cub_workspace_bytes, temp_storage_bytes); + } + + for (std::size_t i = 0; i < large_cone_ids.size(); ++i) { + std::size_t temp_storage_bytes = 0; + RAFT_CUDA_TRY(cub::DeviceReduce::Sum(nullptr, + temp_storage_bytes, + input + large_cone_offsets[i], + output + large_cone_ids[i], + large_cone_dimensions[i], + stream.value())); + cub_workspace_bytes = std::max(cub_workspace_bytes, temp_storage_bytes); + } + + if (cub_workspace.size() < cub_workspace_bytes) { + cub_workspace.resize(cub_workspace_bytes, stream); + } + } + + public: + template + void prepare_workspace(rmm::cuda_stream_view stream) + { + prepare_workspace_for_type(stream); + (prepare_workspace_for_type(stream), ...); + } + + template + void operator()(InputIt input, OutputIt output, value_t init, rmm::cuda_stream_view stream) + { + if (!small_cone_ids.is_empty()) { + // Each warp reduces one small cone. `warps_per_cta` only controls how + // many independent cone reductions are packed into one CTA; the default + // of 8 gives a conventional 256-thread block. + const auto n_small = small_cone_ids.size(); + const auto grid = (n_small + warps_per_cta - 1) / warps_per_cta; + warp_per_cone_reduce_kernel + <<>>( + input, cuopt::make_span(small_cone_ids), cone_offsets, output, init); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + } + + if (!medium_cone_ids.is_empty()) { + cuopt_assert(cub_workspace_bytes > 0 && cub_workspace.size() >= cub_workspace_bytes, + "segmented_sum_t::prepare_workspace must be called before reducing medium or " + "large cones"); + + const auto medium_output = thrust::make_permutation_iterator(output, medium_cone_ids.begin()); + const auto medium_begin_offsets = + thrust::make_permutation_iterator(cone_offsets.data(), medium_cone_ids.begin()); + const auto medium_end_offsets = + thrust::make_permutation_iterator(cone_offsets.data() + 1, medium_cone_ids.begin()); + + std::size_t temp_storage_bytes = cub_workspace_bytes; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Sum(cub_workspace.data(), + temp_storage_bytes, + input, + medium_output, + medium_cone_ids.size(), + medium_begin_offsets, + medium_end_offsets, + stream.value())); + } + + if (!large_cone_ids.empty()) { + cuopt_assert(cub_workspace_bytes > 0 && cub_workspace.size() >= cub_workspace_bytes, + "segmented_sum_t::prepare_workspace must be called before reducing medium or " + "large cones"); + + for (std::size_t i = 0; i < large_cone_ids.size(); ++i) { + std::size_t temp_storage_bytes = cub_workspace_bytes; + RAFT_CUDA_TRY(cub::DeviceReduce::Sum(cub_workspace.data(), + temp_storage_bytes, + input + large_cone_offsets[i], + output + large_cone_ids[i], + large_cone_dimensions[i], + stream.value())); + } + } + } + + template + void operator()(InputIt input, raft::device_span output, rmm::cuda_stream_view stream) + { + operator()(input, output.data(), f_t{0}, stream); + } + + segmented_sum_t(std::span cone_dimensions_host, + raft::device_span cone_offsets_in, + rmm::cuda_stream_view stream) + : cone_offsets(cone_offsets_in), + small_cone_ids(0, stream), + medium_cone_ids(0, stream), + cub_workspace(0, stream) + { + std::vector small_cone_ids_host; + std::vector medium_cone_ids_host; + + std::size_t cone_offset = 0; + i_t cone = 0; + for (const auto cone_dimension : cone_dimensions_host) { + if (cone_dimension <= warp_cone_dim) { + small_cone_ids_host.push_back(cone); + } else if (cone_dimension <= large_cone_cutoff) { + medium_cone_ids_host.push_back(cone); + } else { + large_cone_ids.push_back(cone); + large_cone_offsets.push_back(cone_offset); + large_cone_dimensions.push_back(cone_dimension); + } + cone_offset += cone_dimension; + ++cone; + } + + bool need_sync = false; + if (!small_cone_ids_host.empty()) { + cuopt::device_copy(small_cone_ids, small_cone_ids_host, stream); + need_sync = true; + } + if (!medium_cone_ids_host.empty()) { + cuopt::device_copy(medium_cone_ids, medium_cone_ids_host, stream); + need_sync = true; + } + if (need_sync) { stream.synchronize(); } + } +}; + +template +__global__ void __launch_bounds__(warps_per_cta* raft::WarpSize) + warp_per_cone_reduce_kernel(InputIt input, + raft::device_span small_cone_ids, + raft::device_span cone_offsets, + OutputIt output, + value_t init) +{ + static_assert(warps_per_cta > 0); + static_assert(warps_per_cta * raft::WarpSize <= 1024); + + using warp_reduce_t = cub::WarpReduce; + __shared__ typename warp_reduce_t::TempStorage temp_storage[warps_per_cta]; + + const auto lane_id = raft::laneId(); + const auto warp_idx = threadIdx.x / raft::WarpSize; + const auto slot = blockIdx.x * warps_per_cta + warp_idx; + if (slot >= small_cone_ids.size()) { return; } + + const auto cone = small_cone_ids[slot]; + const auto off = cone_offsets[cone]; + const auto dim = cone_offsets[cone + 1] - off; + + auto sum = init; + for (std::size_t i = lane_id; i < dim; i += raft::WarpSize) { + sum = sum + input[off + i]; + } + + sum = warp_reduce_t(temp_storage[warp_idx]).Sum(sum); + if (lane_id == 0) { output[cone] = sum; } +} + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/tests/dual_simplex/CMakeLists.txt b/cpp/tests/dual_simplex/CMakeLists.txt index 1abeb62ded..f6dff93227 100644 --- a/cpp/tests/dual_simplex/CMakeLists.txt +++ b/cpp/tests/dual_simplex/CMakeLists.txt @@ -6,5 +6,5 @@ ConfigureTest(DUAL_SIMPLEX_TEST ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/solve.cpp ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/solve_barrier.cu - ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/second_order_cone_test.cu + ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/second_order_cone_kernels.cu ) diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu new file mode 100644 index 0000000000..b8e6eef487 --- /dev/null +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu @@ -0,0 +1,625 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +#include + +#include + +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex::test { + +double host_cone_step_length_from_scalars(double u0, + double du0, + double du_tail_sq, + double u_tail_du_tail, + double u_tail_sq, + double alpha_max) +{ + const auto a = du0 * du0 - du_tail_sq; + const auto b = u0 * du0 - u_tail_du_tail; + const auto c_raw = u0 * u0 - u_tail_sq; + const auto c = c_raw > 0.0 ? c_raw : 0.0; + const auto disc = b * b - a * c; + auto alpha = alpha_max; + + if (du0 < 0.0) { alpha = std::min(alpha, -u0 / du0); } + + if ((a > 0.0 && b > 0.0) || disc < 0.0) { return alpha; } + + if (a == 0.0) { + if (b < 0.0) { alpha = std::min(alpha, c / (-2.0 * b)); } + } else if (c == 0.0) { + alpha = a >= 0.0 ? alpha : 0.0; + } else { + const auto t = -(b + std::copysign(std::sqrt(disc), b)); + auto r1 = c / t; + auto r2 = t / a; + if (r1 < 0.0) { r1 = alpha; } + if (r2 < 0.0) { r2 = alpha; } + alpha = std::min(alpha, std::min(r1, r2)); + } + + return alpha; +} + +TEST(second_order_cone_kernels, topology_and_scratch_layout) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 2, 5}; + rmm::device_uvector x(10, stream); + rmm::device_uvector z(10, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + + EXPECT_EQ(cones.n_cones, std::size_t{3}); + EXPECT_EQ(cones.n_cone_entries, std::size_t{10}); + EXPECT_EQ(cones.x.data(), x.data()); + EXPECT_EQ(cones.z.data(), z.data()); + + EXPECT_EQ(cuopt::host_copy(cones.cone_offsets, stream), (std::vector{0, 3, 5, 10})); + EXPECT_EQ(cuopt::host_copy(cones.cone_dimensions, stream), cone_dimensions); + EXPECT_EQ(cuopt::host_copy(cones.element_cone_ids, stream), + (std::vector{0, 0, 0, 1, 1, 2, 2, 2, 2, 2})); + EXPECT_EQ(cuopt::host_copy(cones.segmented_sum.small_cone_ids, stream), + (std::vector{0, 1, 2})); + EXPECT_TRUE(cuopt::host_copy(cones.segmented_sum.medium_cone_ids, stream).empty()); + EXPECT_TRUE(cones.segmented_sum.large_cone_ids.empty()); + EXPECT_TRUE(cones.segmented_sum.large_cone_offsets.empty()); + EXPECT_TRUE(cones.segmented_sum.large_cone_dimensions.empty()); + + EXPECT_EQ(cones.eta.size(), 3); + EXPECT_EQ(cones.w.size(), 10); + + auto& scratch = cones.scratch; + EXPECT_EQ(scratch.n_cones, cones.n_cones); + EXPECT_EQ(scratch.n_cone_entries, cones.n_cone_entries); + EXPECT_EQ(scratch.slots.size(), 3 * cone_dimensions.size()); + EXPECT_EQ(scratch.step_alpha_primal.size(), cone_dimensions.size()); + EXPECT_EQ(scratch.step_alpha_dual.size(), cone_dimensions.size()); + EXPECT_EQ(scratch.temp_cone.size(), x.size()); + + EXPECT_EQ(scratch.get_slot<0>().size(), cone_dimensions.size()); + EXPECT_EQ(scratch.get_slot<1>().data(), scratch.get_slot<0>().data() + cones.n_cones); + EXPECT_EQ(scratch.get_slot<2>().data(), scratch.get_slot<1>().data() + cones.n_cones); +} + +TEST(second_order_cone_kernels, segmented_sum_uses_all_cone_size_buckets) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{65, 3, 66, 32769}; + rmm::device_uvector x(32903, stream); + rmm::device_uvector z(32903, stream); + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + + EXPECT_EQ(cuopt::host_copy(cones.segmented_sum.small_cone_ids, stream), (std::vector{1})); + EXPECT_EQ(cuopt::host_copy(cones.segmented_sum.medium_cone_ids, stream), + (std::vector{0, 2})); + EXPECT_EQ(cones.segmented_sum.large_cone_ids, (std::vector{3})); + EXPECT_EQ(cones.segmented_sum.large_cone_offsets, (std::vector{134})); + EXPECT_EQ(cones.segmented_sum.large_cone_dimensions, (std::vector{32769})); + + std::vector values_host(cones.n_cone_entries, 1.0); + rmm::device_uvector values(values_host.size(), stream); + rmm::device_uvector sums(cone_dimensions.size(), stream); + raft::copy(values.data(), values_host.data(), values_host.size(), stream); + + EXPECT_GT(cones.segmented_sum.cub_workspace_bytes, 0); + const auto workspace_size = cones.segmented_sum.cub_workspace.size(); + EXPECT_GT(workspace_size, 0); + + cones.segmented_sum(values.data(), cuopt::make_span(sums), stream); + + EXPECT_EQ(cuopt::host_copy(sums, stream), (std::vector{65.0, 3.0, 66.0, 32769.0})); + EXPECT_EQ(cones.segmented_sum.cub_workspace.size(), workspace_size); +} + +TEST(second_order_cone_kernels, nt_scaling_matches_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + x_host[offset] = 100.0 + static_cast(cone); + z_host[offset] = 80.0 + static_cast(cone); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + x_host[offset + local_idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[offset + local_idx] = 0.0015 * static_cast((local_idx % 7) + 1); + } + offset += static_cast(dim); + } + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + const auto workspace_size = cones.segmented_sum.cub_workspace.size(); + EXPECT_GT(workspace_size, 0); + + launch_nt_scaling(cones, stream); + EXPECT_EQ(cones.segmented_sum.cub_workspace.size(), workspace_size); + + auto eta_host = cuopt::host_copy(cones.eta, stream); + auto w_host = cuopt::host_copy(cones.w, stream); + + std::vector expected_eta(cone_dimensions.size()); + std::vector expected_w(n_cone_entries); + + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double x_tail_sq = 0.0; + double z_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + x_tail_sq += x_host[idx] * x_host[idx]; + z_tail_sq += z_host[idx] * z_host[idx]; + } + + const auto x_tail_norm = std::sqrt(x_tail_sq); + const auto z_tail_norm = std::sqrt(z_tail_sq); + const auto x_det = (x_host[offset] - x_tail_norm) * (x_host[offset] + x_tail_norm); + const auto z_det = (z_host[offset] - z_tail_norm) * (z_host[offset] + z_tail_norm); + ASSERT_GT(x_det, 0.0) << "cone " << cone; + ASSERT_GT(z_det, 0.0) << "cone " << cone; + + const auto x_scale = std::sqrt(x_det); + const auto z_scale = std::sqrt(z_det); + + expected_eta[cone] = std::sqrt(x_scale / z_scale); + + double normalized_xz_dot = 0.0; + for (int local_idx = 0; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + normalized_xz_dot += x_host[idx] * z_host[idx] / (x_scale * z_scale); + } + const auto w_det = 2.0 + 2.0 * normalized_xz_dot; + ASSERT_GT(w_det, 0.0) << "cone " << cone; + const auto w_scale = std::sqrt(w_det); + + expected_w[offset] = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + expected_w[idx] = (x_host[idx] / x_scale - z_host[idx] / z_scale) / w_scale; + } + + double normalized_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + normalized_tail_sq += expected_w[idx] * expected_w[idx]; + } + expected_w[offset] = std::sqrt(1.0 + normalized_tail_sq); + + offset += static_cast(dim); + } + + for (std::size_t i = 0; i < expected_eta.size(); ++i) { + EXPECT_NEAR(eta_host[i], expected_eta[i], 1e-10) << "cone " << i; + } + + for (std::size_t i = 0; i < expected_w.size(); ++i) { + EXPECT_NEAR(w_host[i], expected_w[i], 1e-10) << "entry " << i; + } + + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + tail_sq += w_host[idx] * w_host[idx]; + } + + EXPECT_NEAR(w_host[offset] * w_host[offset] - tail_sq, 1.0, 1e-10) << "cone " << cone; + offset += static_cast(dim); + } +} + +TEST(second_order_cone_kernels, cone_step_length_matches_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::vector dx_host(n_cone_entries); + std::vector dz_host(n_cone_entries); + + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + x_host[offset] = 12.0 + static_cast(cone); + z_host[offset] = 14.0 + static_cast(cone); + dx_host[offset] = (cone == 0) ? -30.0 : 0.2; + dz_host[offset] = (cone == 1) ? -25.0 : 0.15; + + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + + x_host[idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[idx] = 0.0015 * static_cast((local_idx % 7) + 1); + dx_host[idx] = 0.02 * static_cast((local_idx % 5) - 2); + dz_host[idx] = -0.015 * static_cast((local_idx % 7) - 3); + } + + offset += static_cast(dim); + } + + auto compute_expected_step = [&](std::vector const& u, + std::vector const& du, + double alpha_max, + std::vector& per_cone_alpha) { + auto global_alpha = alpha_max; + std::size_t off = 0; + + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double du_tail_sq = 0.0; + double u_tail_du_tail = 0.0; + double u_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + du_tail_sq += du[idx] * du[idx]; + u_tail_du_tail += u[idx] * du[idx]; + u_tail_sq += u[idx] * u[idx]; + } + + per_cone_alpha[cone] = host_cone_step_length_from_scalars( + u[off], du[off], du_tail_sq, u_tail_du_tail, u_tail_sq, alpha_max); + global_alpha = std::min(global_alpha, per_cone_alpha[cone]); + + off += static_cast(dim); + } + + return global_alpha; + }; + + constexpr double alpha_max = 0.99; + std::vector expected_primal_per_cone(cone_dimensions.size()); + std::vector expected_dual_per_cone(cone_dimensions.size()); + const auto expected_primal = + compute_expected_step(x_host, dx_host, alpha_max, expected_primal_per_cone); + const auto expected_dual = + compute_expected_step(z_host, dz_host, alpha_max, expected_dual_per_cone); + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + auto dx = cuopt::device_copy(dx_host, stream); + auto dz = cuopt::device_copy(dz_host, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + const auto [step_primal, step_dual] = + compute_cone_step_length(cones, + raft::device_span(dx.data(), dx.size()), + raft::device_span(dz.data(), dz.size()), + alpha_max, + stream); + + EXPECT_LT(expected_primal, alpha_max); + EXPECT_LT(expected_dual, alpha_max); + EXPECT_NEAR(step_primal, expected_primal, 1e-12); + EXPECT_NEAR(step_dual, expected_dual, 1e-12); + + const auto primal_per_cone = cuopt::host_copy(cones.scratch.step_alpha_primal, stream); + const auto dual_per_cone = cuopt::host_copy(cones.scratch.step_alpha_dual, stream); + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + EXPECT_NEAR(primal_per_cone[cone], expected_primal_per_cone[cone], 1e-12) + << "primal cone " << cone; + EXPECT_NEAR(dual_per_cone[cone], expected_dual_per_cone[cone], 1e-12) << "dual cone " << cone; + } +} + +TEST(second_order_cone_kernels, scaling_operators_match_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::vector v_host(n_cone_entries); + std::vector cone_target_host(n_cone_entries); + std::vector accum_initial_host(n_cone_entries); + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + x_host[offset] = 100.0 + static_cast(cone); + z_host[offset] = 80.0 + static_cast(cone); + v_host[offset] = 0.75 + 0.1 * static_cast(cone); + cone_target_host[offset] = 0.4 + 0.03 * static_cast(cone); + accum_initial_host[offset] = -0.2 + 0.02 * static_cast(cone); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + x_host[idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[idx] = 0.0015 * static_cast((local_idx % 7) + 1); + v_host[idx] = 0.002 * static_cast((local_idx % 11) - 5); + cone_target_host[idx] = 0.003 * static_cast((local_idx % 13) - 6); + accum_initial_host[idx] = 0.004 * static_cast((local_idx % 17) - 8); + } + offset += static_cast(dim); + } + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + auto v = cuopt::device_copy(v_host, stream); + auto cone_target = cuopt::device_copy(cone_target_host, stream); + auto accum = cuopt::device_copy(accum_initial_host, stream); + rmm::device_uvector w_out(n_cone_entries, stream); + rmm::device_uvector w_inv_out(n_cone_entries, stream); + rmm::device_uvector h_out(n_cone_entries, stream); + rmm::device_uvector w_inv_tmp(n_cone_entries, stream); + rmm::device_uvector h_from_w_inv(n_cone_entries, stream); + rmm::device_uvector recovered_dz(n_cone_entries, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + launch_nt_scaling(cones, stream); + + auto v_span = raft::device_span(v.data(), v.size()); + apply_w(v_span, cuopt::make_span(w_out), cones, stream); + apply_w_inv(v_span, cuopt::make_span(w_inv_out), cones, stream); + apply_hinv2(v_span, cuopt::make_span(h_out), cones, stream); + recover_cone_dz_from_target( + v_span, + cones, + raft::device_span(cone_target.data(), cone_target.size()), + cuopt::make_span(recovered_dz), + stream); + accumulate_cone_hinv2_matvec(v_span, cones, cuopt::make_span(accum), stream); + apply_w_inv(v_span, cuopt::make_span(w_inv_tmp), cones, stream); + apply_w_inv(raft::device_span(w_inv_tmp.data(), w_inv_tmp.size()), + cuopt::make_span(h_from_w_inv), + cones, + stream); + + auto eta_host = cuopt::host_copy(cones.eta, stream); + auto w_host = cuopt::host_copy(cones.w, stream); + auto w_out_host = cuopt::host_copy(w_out, stream); + auto w_inv_out_host = cuopt::host_copy(w_inv_out, stream); + auto h_out_host = cuopt::host_copy(h_out, stream); + auto h_identity_host = cuopt::host_copy(h_from_w_inv, stream); + auto recovered_dz_host = cuopt::host_copy(recovered_dz, stream); + auto accum_host = cuopt::host_copy(accum, stream); + + std::vector expected_w(n_cone_entries); + std::vector expected_w_inv(n_cone_entries); + std::vector expected_h(n_cone_entries); + std::vector expected_h_unscaled(n_cone_entries); + + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto w0 = w_host[offset]; + const auto v0 = v_host[offset]; + const auto eta = eta_host[cone]; + + double tail_dot = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + tail_dot += w_host[idx] * v_host[idx]; + } + + expected_w[offset] = eta * (w0 * v0 + tail_dot); + expected_w_inv[offset] = (w0 * v0 - tail_dot) / eta; + + const auto rho = w0 * v0 - tail_dot; + expected_h_unscaled[offset] = (2.0 * w0 * rho - v0) / (eta * eta); + expected_h[offset] = expected_h_unscaled[offset]; + + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + + expected_w[idx] = eta * (v_host[idx] + (v0 + tail_dot / (1.0 + w0)) * w_host[idx]); + expected_w_inv[idx] = (v_host[idx] + (-v0 + tail_dot / (1.0 + w0)) * w_host[idx]) / eta; + expected_h_unscaled[idx] = (v_host[idx] - 2.0 * w_host[idx] * rho) / (eta * eta); + expected_h[idx] = expected_h_unscaled[idx]; + } + + offset += static_cast(dim); + } + + for (std::size_t i = 0; i < n_cone_entries; ++i) { + EXPECT_NEAR(w_out_host[i], expected_w[i], 1e-9) << "W entry " << i; + EXPECT_NEAR(w_inv_out_host[i], expected_w_inv[i], 1e-9) << "W inverse entry " << i; + EXPECT_NEAR(h_out_host[i], expected_h[i], 1e-9) << "H entry " << i; + EXPECT_NEAR(h_out_host[i], h_identity_host[i], 1e-9) << "H identity entry " << i; + EXPECT_NEAR(recovered_dz_host[i], cone_target_host[i] - expected_h_unscaled[i], 1e-9) + << "recovered dz entry " << i; + EXPECT_NEAR(accum_host[i], accum_initial_host[i] + expected_h_unscaled[i], 1e-9) + << "accumulated H entry " << i; + } +} + +TEST(second_order_cone_kernels, combined_cone_rhs_matches_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::vector dx_aff_host(n_cone_entries); + std::vector dz_aff_host(n_cone_entries); + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + x_host[offset] = 120.0 + static_cast(cone); + z_host[offset] = 90.0 + static_cast(cone); + dx_aff_host[offset] = 0.25 + 0.05 * static_cast(cone); + dz_aff_host[offset] = -0.3 + 0.04 * static_cast(cone); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + x_host[idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[idx] = 0.0015 * static_cast((local_idx % 7) + 1); + dx_aff_host[idx] = 0.002 * static_cast((local_idx % 11) - 5); + dz_aff_host[idx] = 0.001 * static_cast((local_idx % 13) - 6); + } + offset += static_cast(dim); + } + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + auto dx_aff = cuopt::device_copy(dx_aff_host, stream); + auto dz_aff = cuopt::device_copy(dz_aff_host, stream); + rmm::device_uvector out(n_cone_entries, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + launch_nt_scaling(cones, stream); + + constexpr double sigma_mu = 0.37; + compute_combined_cone_rhs_term(raft::device_span(dx_aff.data(), dx_aff.size()), + raft::device_span(dz_aff.data(), dz_aff.size()), + cones, + sigma_mu, + cuopt::make_span(out), + stream); + + auto eta_host = cuopt::host_copy(cones.eta, stream); + auto w_host = cuopt::host_copy(cones.w, stream); + auto out_host = cuopt::host_copy(out, stream); + + auto apply_w_ref = [&](std::vector const& v) { + std::vector result(n_cone_entries); + std::size_t off = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto w0 = w_host[off]; + const auto v0 = v[off]; + + double tail_dot = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + tail_dot += w_host[idx] * v[idx]; + } + + result[off] = eta_host[cone] * (w0 * v0 + tail_dot); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + result[idx] = eta_host[cone] * (v[idx] + (v0 + tail_dot / (1.0 + w0)) * w_host[idx]); + } + + off += static_cast(dim); + } + return result; + }; + + auto apply_w_inv_ref = [&](std::vector const& v) { + std::vector result(n_cone_entries); + std::size_t off = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto w0 = w_host[off]; + const auto v0 = v[off]; + + double tail_dot = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + tail_dot += w_host[idx] * v[idx]; + } + + result[off] = (w0 * v0 - tail_dot) / eta_host[cone]; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + result[idx] = (v[idx] + (-v0 + tail_dot / (1.0 + w0)) * w_host[idx]) / eta_host[cone]; + } + + off += static_cast(dim); + } + return result; + }; + + auto scaled_dx = apply_w_inv_ref(dx_aff_host); + auto scaled_dz = apply_w_ref(dz_aff_host); + auto nt_point = apply_w_ref(z_host); + + std::vector shift(n_cone_entries); + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double head_dot = 0.0; + for (int local_idx = 0; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + head_dot += scaled_dx[idx] * scaled_dz[idx]; + } + + shift[offset] = head_dot - sigma_mu; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + shift[idx] = scaled_dx[offset] * scaled_dz[idx] + scaled_dz[offset] * scaled_dx[idx]; + } + + offset += static_cast(dim); + } + + std::vector minus_p(n_cone_entries); + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto lambda0 = nt_point[offset]; + + double lambda_tail_dot = 0.0; + double lambda_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + lambda_tail_dot += nt_point[idx] * shift[idx]; + lambda_tail_sq += nt_point[idx] * nt_point[idx]; + } + + const auto lambda_tail_norm = std::sqrt(lambda_tail_sq); + const auto det_lambda = (lambda0 - lambda_tail_norm) * (lambda0 + lambda_tail_norm); + ASSERT_GT(lambda0, 0.0) << "cone " << cone; + ASSERT_GT(det_lambda, 0.0) << "cone " << cone; + + const auto p_head = (lambda0 * shift[offset] - lambda_tail_dot) / det_lambda; + minus_p[offset] = -p_head; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + minus_p[idx] = (p_head * nt_point[idx] - shift[idx]) / lambda0; + } + + offset += static_cast(dim); + } + + auto expected = apply_w_inv_ref(minus_p); + for (std::size_t i = 0; i < n_cone_entries; ++i) { + EXPECT_NEAR(out_host[i], expected[i], 1e-8) << "entry " << i; + } +} + +} // namespace cuopt::linear_programming::dual_simplex::test diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu deleted file mode 100644 index b3e8974e8b..0000000000 --- a/cpp/tests/dual_simplex/unit_tests/second_order_cone_test.cu +++ /dev/null @@ -1,1416 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include - -#include -#include - -#include - -#include -#include -#include -#include - -namespace cuopt::linear_programming::dual_simplex::test { -namespace { - -template -auto build_offsets(const std::vector& dims) -> std::vector -{ - std::vector offsets(dims.size() + 1, 0); - for (std::size_t i = 0; i < dims.size(); ++i) { - offsets[i + 1] = offsets[i] + dims[i]; - } - return offsets; -} - -template -auto pack_cones(const std::vector>& cones) -> std::vector -{ - std::size_t total_size = 0; - for (const auto& cone : cones) { - total_size += cone.size(); - } - - std::vector packed; - packed.reserve(total_size); - for (const auto& cone : cones) { - packed.insert(packed.end(), cone.begin(), cone.end()); - } - return packed; -} - -template -auto slice_cone(const std::vector& packed, const std::vector& offsets, i_t cone) - -> std::vector -{ - auto begin = packed.begin() + offsets[cone]; - auto end = packed.begin() + offsets[cone + 1]; - return std::vector(begin, end); -} - -template -auto j_norm_sq(const std::vector& u) -> f_t -{ - if (u.empty()) { return f_t(0); } - - f_t tail_sq = f_t(0); - for (std::size_t j = 1; j < u.size(); ++j) { - tail_sq += u[j] * u[j]; - } - return u[0] * u[0] - tail_sq; -} - -template -auto tail_norm(const std::vector& u) -> f_t -{ - f_t tail_sq = f_t(0); - for (std::size_t j = 1; j < u.size(); ++j) { - tail_sq += u[j] * u[j]; - } - return std::sqrt(tail_sq); -} - -template -auto ref_apply_hinv_single(const std::vector& z, - const std::vector& w_bar, - f_t inv_eta, - f_t inv_1pw0) -> std::vector -{ - std::vector out(z.size(), f_t(0)); - if (z.empty()) { return out; } - - f_t zeta = f_t(0); - for (std::size_t j = 1; j < z.size(); ++j) { - zeta += w_bar[j] * z[j]; - } - - f_t coeff = -z[0] + zeta * inv_1pw0; - out[0] = (w_bar[0] * z[0] - zeta) * inv_eta; - for (std::size_t j = 1; j < z.size(); ++j) { - out[j] = (z[j] + coeff * w_bar[j]) * inv_eta; - } - return out; -} - -template -auto ref_apply_H_single(const std::vector& z, - const std::vector& w_bar, - f_t eta, - f_t inv_1pw0) -> std::vector -{ - std::vector out(z.size(), f_t(0)); - if (z.empty()) { return out; } - - f_t zeta = f_t(0); - for (std::size_t j = 1; j < z.size(); ++j) { - zeta += w_bar[j] * z[j]; - } - - f_t coeff = z[0] + zeta * inv_1pw0; - out[0] = (w_bar[0] * z[0] + zeta) * eta; - for (std::size_t j = 1; j < z.size(); ++j) { - out[j] = (z[j] + coeff * w_bar[j]) * eta; - } - return out; -} - -template -auto ref_build_hinv2_block_single(const std::vector& w_bar, f_t inv_eta) -> std::vector -{ - std::size_t q = w_bar.size(); - std::vector block(q * q, f_t(0)); - f_t ie_sq = inv_eta * inv_eta; - - for (std::size_t r = 0; r < q; ++r) { - f_t u_r = (r == 0) ? w_bar[0] : -w_bar[r]; - for (std::size_t c = 0; c < q; ++c) { - f_t u_c = (c == 0) ? w_bar[0] : -w_bar[c]; - f_t j_rc = (r == c) ? ((r == 0) ? f_t(1) : f_t(-1)) : f_t(0); - block[r * q + c] = ie_sq * (f_t(2) * u_r * u_c - j_rc); - } - } - return block; -} - -template -auto ref_apply_hinv2_single(const std::vector& v, const std::vector& w_bar, f_t inv_eta) - -> std::vector -{ - std::vector out(v.size(), f_t(0)); - if (v.empty()) { return out; } - - f_t uTv = w_bar[0] * v[0]; - for (std::size_t j = 1; j < v.size(); ++j) { - uTv -= w_bar[j] * v[j]; - } - - f_t ie_sq = inv_eta * inv_eta; - out[0] = ie_sq * (f_t(2) * w_bar[0] * uTv - v[0]); - for (std::size_t j = 1; j < v.size(); ++j) { - out[j] = ie_sq * (-f_t(2) * w_bar[j] * uTv + v[j]); - } - return out; -} - -template -struct nt_scaling_reference_t { - f_t eta{}; - f_t inv_eta{}; - f_t inv_1pw0{}; - f_t rho{}; - std::vector w_bar; - std::vector omega; -}; - -template -auto ref_nt_scaling_single(const std::vector& s, const std::vector& lambda) - -> nt_scaling_reference_t -{ - EXPECT_EQ(s.size(), lambda.size()); - EXPECT_FALSE(s.empty()); - - f_t s_j_norm_sq = j_norm_sq(s); - f_t l_j_norm_sq = j_norm_sq(lambda); - EXPECT_GT(s_j_norm_sq, f_t(0)); - EXPECT_GT(l_j_norm_sq, f_t(0)); - - f_t s_j_norm = std::sqrt(s_j_norm_sq); - f_t l_j_norm = std::sqrt(l_j_norm_sq); - f_t inv_s_j_norm = f_t(1) / s_j_norm; - f_t inv_l_j_norm = f_t(1) / l_j_norm; - - f_t dot_bar = (s[0] * lambda[0]) * inv_s_j_norm * inv_l_j_norm; - for (std::size_t j = 1; j < s.size(); ++j) { - dot_bar += (s[j] * lambda[j]) * inv_s_j_norm * inv_l_j_norm; - } - - f_t gamma = std::sqrt(std::max(f_t(0), (f_t(1) + dot_bar) * f_t(0.5))); - f_t inv_2g = f_t(1) / (f_t(2) * gamma); - - nt_scaling_reference_t ref{}; - ref.eta = std::sqrt(s_j_norm / l_j_norm); - ref.inv_eta = f_t(1) / ref.eta; - ref.rho = s_j_norm * l_j_norm; - ref.w_bar.assign(s.size(), f_t(0)); - - f_t w1_sq = f_t(0); - for (std::size_t j = 1; j < s.size(); ++j) { - ref.w_bar[j] = inv_2g * (s[j] * inv_s_j_norm - lambda[j] * inv_l_j_norm); - w1_sq += ref.w_bar[j] * ref.w_bar[j]; - } - - // Match the kernel's numerical cleanup path for w_bar[0]. - ref.w_bar[0] = std::sqrt(f_t(1) + w1_sq); - ref.inv_1pw0 = f_t(1) / (f_t(1) + ref.w_bar[0]); - ref.omega = ref_apply_hinv_single(s, ref.w_bar, ref.inv_eta, ref.inv_1pw0); - return ref; -} - -template -auto ref_step_length_single(const std::vector& u, const std::vector& du, f_t alpha_max) - -> f_t -{ - EXPECT_EQ(u.size(), du.size()); - EXPECT_FALSE(u.empty()); - - f_t du1_sq = f_t(0); - f_t u1du1 = f_t(0); - f_t u1_sq = f_t(0); - for (std::size_t j = 1; j < u.size(); ++j) { - du1_sq += du[j] * du[j]; - u1du1 += u[j] * du[j]; - u1_sq += u[j] * u[j]; - } - - f_t a = du[0] * du[0] - du1_sq; - f_t b = u[0] * du[0] - u1du1; - f_t c = std::max(f_t(0), u[0] * u[0] - u1_sq); - f_t disc = b * b - a * c; - - f_t alpha = alpha_max; - if (du[0] < f_t(0)) { alpha = std::min(alpha, -u[0] / du[0]); } - - if ((a > f_t(0) && b > f_t(0)) || disc < f_t(0)) { - // No positive root (parabola stays non-negative for alpha > 0). - } else if (a < f_t(0)) { - alpha = std::min(alpha, (b + std::sqrt(std::max(f_t(0), disc))) / (-a)); - } else if (a == f_t(0)) { - if (b < f_t(0)) { alpha = std::min(alpha, c / (f_t(-2) * b)); } - } else if (c == f_t(0)) { - alpha = (a >= f_t(0)) ? alpha : f_t(0); - } else if (b < f_t(0) && disc > f_t(0)) { - alpha = std::min(alpha, (-b - std::sqrt(disc)) / a); - } - - return alpha; -} - -template -auto ref_jordan_product_single(const std::vector& a, const std::vector& b) - -> std::vector -{ - EXPECT_EQ(a.size(), b.size()); - std::vector out(a.size(), f_t(0)); - if (a.empty()) { return out; } - - f_t dot = f_t(0); - for (std::size_t j = 0; j < a.size(); ++j) { - dot += a[j] * b[j]; - } - out[0] = dot; - for (std::size_t j = 1; j < a.size(); ++j) { - out[j] = a[0] * b[j] + b[0] * a[j]; - } - return out; -} - -template -auto ref_inverse_jordan_product_single(const std::vector& omega, - const std::vector& r, - f_t rho_val) -> std::vector -{ - EXPECT_EQ(omega.size(), r.size()); - std::vector out(omega.size(), f_t(0)); - if (omega.empty()) { return out; } - - f_t nu = f_t(0); - for (std::size_t j = 1; j < omega.size(); ++j) { - nu += omega[j] * r[j]; - } - - f_t inv_rho = f_t(1) / rho_val; - f_t omega_0 = omega[0]; - out[0] = (omega_0 * r[0] - nu) * inv_rho; - - f_t c_omega = ((nu / omega_0) - r[0]) * inv_rho; - f_t c_r = f_t(1) / omega_0; - for (std::size_t j = 1; j < omega.size(); ++j) { - out[j] = c_omega * omega[j] + c_r * r[j]; - } - return out; -} - -template -auto ref_fused_corrector_single(const std::vector& dx_aff, - const std::vector& omega, - const std::vector& w_bar, - f_t inv_eta, - f_t inv_1pw0, - f_t rho_val, - f_t sigma_mu) -> std::vector -{ - auto dx = ref_apply_hinv_single(dx_aff, w_bar, inv_eta, inv_1pw0); - - std::vector dz(dx.size()); - for (std::size_t j = 0; j < dx.size(); ++j) { - dz[j] = -omega[j] - dx[j]; - } - - auto r_K_1 = ref_jordan_product_single(omega, omega); - auto r_K_2 = ref_jordan_product_single(dx, dz); - - std::vector r_K(dx.size()); - for (std::size_t j = 0; j < dx.size(); ++j) { - r_K[j] = r_K_1[j] + r_K_2[j]; - } - r_K[0] -= sigma_mu; - - auto corr = ref_inverse_jordan_product_single(omega, r_K, rho_val); - return ref_apply_hinv_single(corr, w_bar, inv_eta, inv_1pw0); -} - -template -auto make_patterned_cone(int q, f_t head, f_t scale) -> std::vector -{ - std::vector cone(q, f_t(0)); - cone[0] = head; - for (int j = 1; j < q; ++j) { - f_t sign = (j % 2 == 0) ? f_t(1) : f_t(-1); - cone[j] = sign * scale * static_cast((j % 7) + 1); - } - return cone; -} - -} // namespace - -class second_order_cone_test : public ::testing::Test { - protected: - using i_t = int; - using f_t = double; - static constexpr int dim = 256; - - raft::handle_t handle_; - rmm::cuda_stream_view stream_ = handle_.get_stream(); - - template - auto make_device_vector(const std::vector& host) -> rmm::device_uvector - { - rmm::device_uvector device(host.size(), stream_); - if (!host.empty()) { raft::copy(device.data(), host.data(), host.size(), stream_); } - sync(); - return device; - } - - template - auto copy_to_host(const rmm::device_uvector& device) -> std::vector - { - std::vector host(device.size()); - if (!host.empty()) { raft::copy(host.data(), device.data(), host.size(), stream_); } - sync(); - return host; - } - - template - void copy_to_device(rmm::device_uvector& device, const std::vector& host) - { - ASSERT_EQ(device.size(), host.size()); - if (!host.empty()) { raft::copy(device.data(), host.data(), host.size(), stream_); } - sync(); - } - - void sync() { RAFT_CUDA_TRY(cudaStreamSynchronize(stream_.value())); } - - template - void expect_vector_near(const std::vector& actual, - const std::vector& expected, - t_t atol, - t_t rtol, - const char* label) - { - ASSERT_EQ(actual.size(), expected.size()) << label << " size mismatch"; - for (std::size_t i = 0; i < actual.size(); ++i) { - EXPECT_NEAR(actual[i], expected[i], atol + rtol * std::abs(expected[i])) - << label << "[" << i << "]"; - } - } - - void launch_step_length(rmm::device_uvector& s, - rmm::device_uvector& ds, - rmm::device_uvector& lambda, - rmm::device_uvector& dlambda, - rmm::device_uvector& alpha, - const rmm::device_uvector& cone_offsets, - i_t k, - f_t alpha_max) - { - auto h_offsets = copy_to_host(cone_offsets); - std::vector dims(k, 0); - for (i_t cone = 0; cone < k; ++cone) { - dims[cone] = h_offsets[cone + 1] - h_offsets[cone]; - } - cone_data_t cones(k, dims, cuopt::make_span(s), cuopt::make_span(lambda), stream_); - rmm::device_uvector alpha_dual(k, stream_); - launch_nt_scaling(cones, stream_); - compute_cone_step_length_per_cone(cones, - cuopt::make_span(s), - cuopt::make_span(ds), - cuopt::make_span(lambda), - cuopt::make_span(dlambda), - cuopt::make_span(alpha), - cuopt::make_span(alpha_dual), - alpha_max, - stream_); - sync(); - auto h_primal = copy_to_host(alpha); - auto h_dual = copy_to_host(alpha_dual); - for (i_t i = 0; i < k; ++i) { - h_primal[i] = std::min(h_primal[i], h_dual[i]); - } - copy_to_device(alpha, h_primal); - } - - void launch_apply_hinv2(const rmm::device_uvector& v, - rmm::device_uvector& out, - const rmm::device_uvector& w_bar, - const rmm::device_uvector& inv_eta, - const rmm::device_uvector& cone_offsets, - i_t k) - { - auto h_offsets = copy_to_host(cone_offsets); - std::vector h_element_cone_ids(v.size(), 0); - for (i_t cone = 0; cone < k; ++cone) { - std::fill(h_element_cone_ids.begin() + h_offsets[cone], - h_element_cone_ids.begin() + h_offsets[cone + 1], - cone); - } - auto d_element_cone_ids = make_device_vector(h_element_cone_ids); - rmm::device_uvector tail_dot(k, stream_); - rmm::device_uvector workspace(0, stream_); - apply_hinv2(cuopt::make_span(v), - cuopt::make_span(out), - cuopt::make_span(w_bar), - cuopt::make_span(inv_eta), - cuopt::make_span(cone_offsets), - cuopt::make_span(d_element_cone_ids), - cuopt::make_span(tail_dot), - workspace, - k, - stream_); - sync(); - } - - void launch_fused_corrector(const rmm::device_uvector& dx_aff, - cone_data_t& cones, - f_t sigma_mu, - rmm::device_uvector& out) - { - out.resize(cones.m_c, stream_); - compute_combined_cone_rhs_term( - cuopt::make_span(dx_aff), cones, sigma_mu, cuopt::make_span(out), stream_); - sync(); - } - - void launch_affine_cone_rhs(cone_data_t& cones, rmm::device_uvector& out) - { - out.resize(cones.m_c, stream_); - compute_affine_cone_rhs_term(cones, cuopt::make_span(out), stream_); - sync(); - } - - void launch_recover_cone_dz_from_target(const rmm::device_uvector& dx, - cone_data_t& cones, - const rmm::device_uvector& cone_target, - rmm::device_uvector& hinv2_dx, - rmm::device_uvector& dz) - { - recover_cone_dz_from_target(cuopt::make_span(dx), - cones, - cuopt::make_span(cone_target), - hinv2_dx, - cuopt::make_span(dz), - stream_); - sync(); - } - - void launch_accumulate_cone_hinv2(const rmm::device_uvector& x, - cone_data_t& cones, - rmm::device_uvector& hinv2_x, - rmm::device_uvector& out) - { - accumulate_cone_hinv2_matvec( - cuopt::make_span(x), cones, hinv2_x, cuopt::make_span(out), stream_); - sync(); - } - - void launch_cone_block_scatter(const cone_data_t& cones, - rmm::device_uvector& aug_x, - const rmm::device_uvector& csr_indices, - const rmm::device_uvector& q_values) - { - scatter_hinv2_into_augmented(cones, aug_x, csr_indices, q_values, stream_); - sync(); - } -}; - -TEST_F(second_order_cone_test, cone_data_topology_and_flat_index_maps) -{ - std::vector dims{1, 2, 3, 4}; - cone_data_t cones(static_cast(dims.size()), dims, {}, {}, stream_); - - auto expected_offsets = build_offsets(dims); - auto actual_offsets = copy_to_host(cones.cone_offsets); - auto actual_dims = copy_to_host(cones.cone_dims); - auto element_cone_ids = copy_to_host(cones.element_cone_ids); - auto block_cone_ids = copy_to_host(cones.block_entry_cone_ids); - - std::vector expected_element_cone_ids; - for (i_t cone = 0; cone < static_cast(dims.size()); ++cone) { - expected_element_cone_ids.insert(expected_element_cone_ids.end(), dims[cone], cone); - } - - std::vector expected_block_cone_ids; - for (i_t cone = 0; cone < static_cast(dims.size()); ++cone) { - expected_block_cone_ids.insert(expected_block_cone_ids.end(), dims[cone] * dims[cone], cone); - } - - EXPECT_EQ(cones.K, static_cast(dims.size())); - EXPECT_EQ(cones.m_c, expected_offsets.back()); - EXPECT_EQ(actual_offsets, expected_offsets); - EXPECT_EQ(actual_dims, dims); - EXPECT_EQ(element_cone_ids, expected_element_cone_ids); - EXPECT_EQ(block_cone_ids, expected_block_cone_ids); -} - -TEST_F(second_order_cone_test, cone_data_reuses_named_scratch_slots) -{ - std::vector> s_cones{{5.0, 1.0, 1.0}, {6.0, 1.0, -0.5, 0.25, 0.1}}; - std::vector> ds_cones{{-0.5, 0.1, 0.1}, {-0.2, 0.05, 0.03, -0.02, 0.01}}; - std::vector> lambda_cones{{5.0, 1.0, 1.0}, {4.0, 0.2, 0.3, -0.1, 0.05}}; - std::vector> dlambda_cones{{-0.5, 0.1, 0.1}, {-0.1, 0.02, -0.03, 0.01, -0.01}}; - std::vector dims{3, 5}; - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_ds = make_device_vector(pack_cones(ds_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - auto d_dlambda = make_device_vector(pack_cones(dlambda_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - - EXPECT_EQ(cones.scratch.step_s_du1_sq().size(), dims.size()); - EXPECT_EQ(cones.scratch.step_s_u1du1().size(), dims.size()); - EXPECT_EQ(cones.scratch.step_l_du1_sq().size(), dims.size()); - EXPECT_EQ(cones.scratch.step_l_u1du1().size(), dims.size()); - EXPECT_EQ(cones.scratch.hinv2_tail_dot().size(), dims.size()); - EXPECT_EQ(cones.scratch.step_s_u1_sq().size(), dims.size()); - EXPECT_EQ(cones.scratch.step_l_u1_sq().size(), dims.size()); - EXPECT_EQ(cones.scratch.nt_s1_sq().size(), dims.size()); - EXPECT_EQ(cones.scratch.nt_l1_sq().size(), dims.size()); - EXPECT_EQ(cones.scratch.nt_sl().size(), dims.size()); - EXPECT_EQ(cones.scratch.step_alpha_primal_span().size(), dims.size()); - EXPECT_EQ(cones.scratch.step_alpha_dual_span().size(), dims.size()); - - auto s_du1_ptr = cones.scratch.step_s_du1_sq().data(); - auto s_u1du1_ptr = cones.scratch.step_s_u1du1().data(); - auto l_du1_ptr = cones.scratch.step_l_du1_sq().data(); - auto l_u1du1_ptr = cones.scratch.step_l_u1du1().data(); - auto hinv2_ptr = cones.scratch.hinv2_tail_dot().data(); - auto s_u1_sq_ptr = cones.scratch.step_s_u1_sq().data(); - auto l_u1_sq_ptr = cones.scratch.step_l_u1_sq().data(); - auto nt_s1_ptr = cones.scratch.nt_s1_sq().data(); - auto nt_l1_ptr = cones.scratch.nt_l1_sq().data(); - auto nt_sl_ptr = cones.scratch.nt_sl().data(); - auto alpha_p_ptr = cones.scratch.step_alpha_primal_span().data(); - auto alpha_d_ptr = cones.scratch.step_alpha_dual_span().data(); - - EXPECT_EQ(hinv2_ptr, s_du1_ptr); - EXPECT_EQ(s_du1_ptr, nt_s1_ptr); - EXPECT_EQ(s_u1du1_ptr, nt_l1_ptr); - EXPECT_EQ(s_u1_sq_ptr, nt_sl_ptr); - - compute_cone_step_length_per_cone(cones, - cuopt::make_span(d_s), - cuopt::make_span(d_ds), - cuopt::make_span(d_lambda), - cuopt::make_span(d_dlambda), - cones.scratch.step_alpha_primal_span(), - cones.scratch.step_alpha_dual_span(), - f_t(10.0), - stream_); - sync(); - - EXPECT_EQ(s_du1_ptr, cones.scratch.step_s_du1_sq().data()); - EXPECT_EQ(s_u1du1_ptr, cones.scratch.step_s_u1du1().data()); - EXPECT_EQ(l_du1_ptr, cones.scratch.step_l_du1_sq().data()); - EXPECT_EQ(l_u1du1_ptr, cones.scratch.step_l_u1du1().data()); - EXPECT_EQ(hinv2_ptr, cones.scratch.hinv2_tail_dot().data()); - EXPECT_EQ(s_u1_sq_ptr, cones.scratch.step_s_u1_sq().data()); - EXPECT_EQ(l_u1_sq_ptr, cones.scratch.step_l_u1_sq().data()); - EXPECT_EQ(nt_s1_ptr, cones.scratch.nt_s1_sq().data()); - EXPECT_EQ(nt_l1_ptr, cones.scratch.nt_l1_sq().data()); - EXPECT_EQ(nt_sl_ptr, cones.scratch.nt_sl().data()); - EXPECT_EQ(alpha_p_ptr, cones.scratch.step_alpha_primal_span().data()); - EXPECT_EQ(alpha_d_ptr, cones.scratch.step_alpha_dual_span().data()); -} - -TEST_F(second_order_cone_test, nt_scaling_matches_reference_for_small_cone) -{ - // Fixed small-cone fixture validated against the host-side NT formulas. - std::vector> s_cones{{1.5, 0.3, 0.4}}; - std::vector> lambda_cones{{2.0, 0.5, 0.5}}; - std::vector dims{3}; - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - auto inv_eta = copy_to_host(cones.inv_eta); - auto inv_1pw0 = copy_to_host(cones.inv_1pw0); - auto rho = copy_to_host(cones.rho); - auto w_bar = copy_to_host(cones.w_bar); - auto omega = copy_to_host(cones.omega); - - auto ref = ref_nt_scaling_single(s_cones[0], lambda_cones[0]); - - EXPECT_NEAR(inv_eta[0], ref.inv_eta, 1e-12); - EXPECT_NEAR(inv_1pw0[0], ref.inv_1pw0, 1e-12); - EXPECT_NEAR(rho[0], ref.rho, 1e-12); - expect_vector_near(w_bar, ref.w_bar, 1e-12, 1e-10, "w_bar"); - expect_vector_near(omega, ref.omega, 1e-12, 1e-10, "omega"); - - EXPECT_NEAR(j_norm_sq(w_bar), f_t(1), 1e-12); - EXPECT_NEAR(j_norm_sq(omega), rho[0], 1e-12); - - auto omega_from_apply_hinv = ref_apply_hinv_single(s_cones[0], w_bar, inv_eta[0], inv_1pw0[0]); - expect_vector_near(omega, omega_from_apply_hinv, 1e-12, 1e-10, "omega_consistency"); -} - -TEST_F(second_order_cone_test, nt_scaling_matches_reference_across_bucket_sizes) -{ - std::vector> s_cones{ - {2.0}, make_patterned_cone(33, 4.0, 0.01), make_patterned_cone(2049, 5.0, 0.001)}; - std::vector> lambda_cones{ - {0.5}, make_patterned_cone(33, 3.0, 0.0075), make_patterned_cone(2049, 4.0, 0.00075)}; - std::vector dims{1, 33, 2049}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - - launch_nt_scaling(cones, stream_); - - auto inv_eta = copy_to_host(cones.inv_eta); - auto inv_1pw0 = copy_to_host(cones.inv_1pw0); - auto rho = copy_to_host(cones.rho); - auto w_bar = copy_to_host(cones.w_bar); - auto omega = copy_to_host(cones.omega); - - for (i_t cone = 0; cone < static_cast(dims.size()); ++cone) { - auto ref = ref_nt_scaling_single(s_cones[cone], lambda_cones[cone]); - - EXPECT_NEAR(inv_eta[cone], ref.inv_eta, 1e-10) << "cone " << cone; - EXPECT_NEAR(inv_1pw0[cone], ref.inv_1pw0, 1e-10) << "cone " << cone; - EXPECT_NEAR(rho[cone], ref.rho, 1e-10) << "cone " << cone; - - auto actual_w_bar = slice_cone(w_bar, offsets, cone); - auto actual_omega = slice_cone(omega, offsets, cone); - expect_vector_near(actual_w_bar, ref.w_bar, 1e-10, 1e-8, "w_bar"); - expect_vector_near(actual_omega, ref.omega, 1e-10, 1e-8, "omega"); - - EXPECT_NEAR(j_norm_sq(actual_w_bar), f_t(1), 1e-10) << "cone " << cone; - EXPECT_NEAR(j_norm_sq(actual_omega), rho[cone], 1e-10) << "cone " << cone; - } -} - -TEST_F(second_order_cone_test, nt_scaling_omega_equals_H_times_lambda) -{ - std::vector> s_cones{{5.0, 1.0, -1.0, 0.5, 0.3}}; - std::vector> lambda_cones{{4.0, 0.5, 1.0, -0.3, 0.2}}; - std::vector dims{5}; - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - auto inv_eta = copy_to_host(cones.inv_eta); - auto inv_1pw0 = copy_to_host(cones.inv_1pw0); - auto w_bar = copy_to_host(cones.w_bar); - auto omega = copy_to_host(cones.omega); - - // NT symmetry: omega should equal both H^{-1}s and H*lambda. - auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, f_t(1) / inv_eta[0], inv_1pw0[0]); - expect_vector_near(omega, H_lambda, 1e-10, 1e-8, "omega_vs_H_lambda"); -} - -TEST_F(second_order_cone_test, nt_scaling_tail_identities_match_heads) -{ - std::vector> s_cones{{5.0, 1.0, -1.0, 0.5, 0.3}}; - std::vector> lambda_cones{{4.0, 0.5, 1.0, -0.3, 0.2}}; - std::vector dims{5}; - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - auto inv_eta = copy_to_host(cones.inv_eta); - auto rho = copy_to_host(cones.rho); - auto w_bar = copy_to_host(cones.w_bar); - auto omega = copy_to_host(cones.omega); - - f_t s_J = std::sqrt(j_norm_sq(s_cones[0])); - f_t l_J = std::sqrt(j_norm_sq(lambda_cones[0])); - f_t s_dot_raw = s_cones[0][0] * lambda_cones[0][0]; - for (std::size_t j = 1; j < s_cones[0].size(); ++j) { - s_dot_raw += s_cones[0][j] * lambda_cones[0][j]; - } - f_t s_dot_l = s_dot_raw / (s_J * l_J); - f_t gamma = std::sqrt(std::max(f_t(0), (f_t(1) + s_dot_l) * f_t(0.5))); - f_t w0_from_heads = (s_cones[0][0] / s_J + lambda_cones[0][0] / l_J) / (f_t(2) * gamma); - - f_t omega_tail_sq = f_t(0); - f_t w_omega_tail = f_t(0); - for (std::size_t j = 1; j < omega.size(); ++j) { - omega_tail_sq += omega[j] * omega[j]; - w_omega_tail += w_bar[j] * omega[j]; - } - - EXPECT_NEAR(omega_tail_sq, omega[0] * omega[0] - rho[0], 1e-10) - << "||omega_1||^2 should be derived from omega_0 and rho"; - EXPECT_NEAR(w_bar[0], w0_from_heads, 1e-10) - << "w_bar_0 should be derived directly from normalized cone heads"; - - f_t derived_w_omega = f_t(0.5) * (inv_eta[0] * s_cones[0][0] - lambda_cones[0][0] / inv_eta[0]); - EXPECT_NEAR(w_omega_tail, derived_w_omega, 1e-10) - << "w_bar_1^T omega_1 should be derived from cone heads"; -} - -TEST_F(second_order_cone_test, nt_scaling_near_boundary_is_stable) -{ - // s and lambda barely inside the cone: ||tail||^2 ≈ head^2. - std::vector> s_cones{{1.00002, 0.6, 0.8, 1e-4, -2e-4}}; - std::vector> lambda_cones{{1.000015, 0.8, 0.6, -3e-5, 2e-5}}; - std::vector dims{5}; - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - auto inv_eta = copy_to_host(cones.inv_eta); - auto inv_1pw0 = copy_to_host(cones.inv_1pw0); - auto w_bar = copy_to_host(cones.w_bar); - auto omega = copy_to_host(cones.omega); - - f_t eta_val = f_t(1) / inv_eta[0]; - - EXPECT_NEAR(j_norm_sq(w_bar), f_t(1), 1e-8) << "w_bar J-norm not 1 near boundary"; - EXPECT_GT(w_bar[0], tail_norm(w_bar)) << "w_bar not interior near boundary"; - - // Round-trip: H(omega) should equal s. - auto H_omega = ref_apply_H_single(omega, w_bar, eta_val, inv_1pw0[0]); - expect_vector_near(H_omega, pack_cones(s_cones), 1e-8, 1e-6, "H_omega_vs_s_near_boundary"); - - // Symmetry: omega should also equal H*lambda. - auto H_lambda = ref_apply_H_single(lambda_cones[0], w_bar, eta_val, inv_1pw0[0]); - expect_vector_near(omega, H_lambda, 1e-8, 1e-6, "omega_vs_H_lambda_near_boundary"); -} - -TEST_F(second_order_cone_test, step_length_matches_reference_and_handles_q1) -{ - std::vector dims{1, 3}; - auto offsets = build_offsets(dims); - - std::vector> s_cones{{2.0}, {5.0, 1.0, 1.0}}; - std::vector> ds_cones{{-3.0}, {-0.5, 0.1, 0.1}}; - std::vector> lambda_cones{{5.0}, {5.0, 1.0, 1.0}}; - std::vector> dlambda_cones{{1.0}, {-0.5, 0.1, 0.1}}; - f_t alpha_max = 10.0; - - auto s = make_device_vector(pack_cones(s_cones)); - auto ds = make_device_vector(pack_cones(ds_cones)); - auto lambda = make_device_vector(pack_cones(lambda_cones)); - auto dlambda = make_device_vector(pack_cones(dlambda_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector alpha(dims.size(), stream_); - - launch_step_length( - s, ds, lambda, dlambda, alpha, d_offsets, static_cast(dims.size()), alpha_max); - - auto actual_alpha = copy_to_host(alpha); - std::vector expected_alpha(dims.size(), alpha_max); - for (std::size_t cone = 0; cone < dims.size(); ++cone) { - expected_alpha[cone] = - std::min(ref_step_length_single(s_cones[cone], ds_cones[cone], alpha_max), - ref_step_length_single(lambda_cones[cone], dlambda_cones[cone], alpha_max)); - } - - expect_vector_near(actual_alpha, expected_alpha, 1e-12, 1e-10, "step_length"); - EXPECT_NEAR(actual_alpha[0], 2.0 / 3.0, 1e-12); - EXPECT_NEAR(actual_alpha[1], 5.5903758157691508, 1e-10); -} - -TEST_F(second_order_cone_test, step_length_matches_reference_for_large_cone) -{ - std::vector dims{513}; - auto offsets = build_offsets(dims); - - std::vector> s_cones{{make_patterned_cone(dims[0], 5.0, 0.01)}}; - std::vector> ds_cones{{make_patterned_cone(dims[0], -0.25, 0.002)}}; - std::vector> lambda_cones{{make_patterned_cone(dims[0], 6.0, 0.009)}}; - std::vector> dlambda_cones{{make_patterned_cone(dims[0], -0.15, 0.0015)}}; - f_t alpha_max = 20.0; - - auto s = make_device_vector(pack_cones(s_cones)); - auto ds = make_device_vector(pack_cones(ds_cones)); - auto lambda = make_device_vector(pack_cones(lambda_cones)); - auto dlambda = make_device_vector(pack_cones(dlambda_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector alpha(dims.size(), stream_); - - launch_step_length( - s, ds, lambda, dlambda, alpha, d_offsets, static_cast(dims.size()), alpha_max); - - auto actual_alpha = copy_to_host(alpha); - std::vector expected_alpha(dims.size(), alpha_max); - for (std::size_t cone = 0; cone < dims.size(); ++cone) { - expected_alpha[cone] = - std::min(ref_step_length_single(s_cones[cone], ds_cones[cone], alpha_max), - ref_step_length_single(lambda_cones[cone], dlambda_cones[cone], alpha_max)); - } - - expect_vector_near(actual_alpha, expected_alpha, 1e-12, 1e-10, "step_length_large"); - EXPECT_GT(actual_alpha[0], 0.0); - EXPECT_LT(actual_alpha[0], alpha_max); -} - -TEST_F(second_order_cone_test, step_length_boundary_c_zero_returns_zero) -{ - std::vector dims{3}; - auto offsets = build_offsets(dims); - - // Boundary point: c = u^T J u = 1^2 - 1^2 - 0^2 = 0. - // Direction: a = du^T J du = 1^2 - 1^2 - 1^2 = -1 < 0. - // The step length is 0 in this case because the direction leaves the cone - // immediately. - std::vector> s_cones{{1.0, 1.0, 0.0}}; - std::vector> ds_cones{{1.0, 1.0, 1.0}}; - std::vector> lambda_cones{{1.0, 1.0, 0.0}}; - std::vector> dlambda_cones{{1.0, 1.0, 1.0}}; - f_t alpha_max = 10.0; - - auto s = make_device_vector(pack_cones(s_cones)); - auto ds = make_device_vector(pack_cones(ds_cones)); - auto lambda = make_device_vector(pack_cones(lambda_cones)); - auto dlambda = make_device_vector(pack_cones(dlambda_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector alpha(dims.size(), stream_); - - launch_step_length( - s, ds, lambda, dlambda, alpha, d_offsets, static_cast(dims.size()), alpha_max); - - auto actual_alpha = copy_to_host(alpha); - ASSERT_EQ(actual_alpha.size(), 1); - EXPECT_EQ(actual_alpha[0], 0.0); -} - -TEST_F(second_order_cone_test, step_length_degenerate_a_zero) -{ - std::vector dims{2}; - auto offsets = build_offsets(dims); - - // u=(2,0), du=(-1,1): a = du_0^2 - du_1^2 = 1 - 1 = 0 (degenerate quadratic). - // Linear constraint: alpha <= 2. Degenerate branch: alpha = c/(-2b) = 4/2 = 2. - // But the linear constraint also gives alpha <= 2, so result is min(2, 2) = 2... - // Actually b = u0*du0 - u1*du1 = 2*(-1) - 0 = -2, c = u0^2 - u1^2 = 4. - // Degenerate: alpha = c/(-2b) = 4/4 = 1. And linear: alpha <= -u0/du0 = 2. - // So alpha = 1. - std::vector> s_cones{{2.0, 0.0}}; - std::vector> ds_cones{{-1.0, 1.0}}; - std::vector> lambda_cones{{5.0, 0.0}}; - std::vector> dlambda_cones{{0.0, 0.0}}; - - auto s = make_device_vector(pack_cones(s_cones)); - auto ds = make_device_vector(pack_cones(ds_cones)); - auto lambda = make_device_vector(pack_cones(lambda_cones)); - auto dlambda = make_device_vector(pack_cones(dlambda_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector alpha(1, stream_); - - launch_step_length(s, ds, lambda, dlambda, alpha, d_offsets, 1, 10.0); - - auto actual = copy_to_host(alpha); - EXPECT_NEAR(actual[0], 1.0, 1e-14); -} - -TEST_F(second_order_cone_test, step_length_safe_direction_returns_alpha_max) -{ - std::vector dims{3}; - auto offsets = build_offsets(dims); - - // Interior point with direction along the identity element — stays in cone forever. - std::vector> s_cones{{10.0, 0.0, 0.0}}; - std::vector> ds_cones{{1.0, 0.0, 0.0}}; - std::vector> lambda_cones{{10.0, 0.0, 0.0}}; - std::vector> dlambda_cones{{0.0, 0.1, 0.0}}; - - auto s = make_device_vector(pack_cones(s_cones)); - auto ds = make_device_vector(pack_cones(ds_cones)); - auto lambda = make_device_vector(pack_cones(lambda_cones)); - auto dlambda = make_device_vector(pack_cones(dlambda_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector alpha(1, stream_); - - launch_step_length(s, ds, lambda, dlambda, alpha, d_offsets, 1, 1.0); - - auto actual = copy_to_host(alpha); - EXPECT_DOUBLE_EQ(actual[0], 1.0); -} - -TEST_F(second_order_cone_test, step_length_boundary_tightness) -{ - std::vector dims{5}; - auto offsets = build_offsets(dims); - - std::vector> s_cones{{4.0, 1.0, -1.0, 0.5, 0.3}}; - std::vector> ds_cones{{-2.0, 1.0, 0.5, -0.3, 0.1}}; - std::vector> lambda_cones{{5.0, 0.5, 1.0, -0.3, 0.2}}; - std::vector> dlambda_cones{{-1.0, 2.0, 1.0, -0.5, 0.4}}; - - auto s = make_device_vector(pack_cones(s_cones)); - auto ds = make_device_vector(pack_cones(ds_cones)); - auto lambda = make_device_vector(pack_cones(lambda_cones)); - auto dlambda = make_device_vector(pack_cones(dlambda_cones)); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector alpha(1, stream_); - - launch_step_length(s, ds, lambda, dlambda, alpha, d_offsets, 1, 100.0); - - auto a = copy_to_host(alpha)[0]; - ASSERT_GT(a, 0.0); - - // At alpha, at least one of (s, lambda) should be on the cone boundary. - auto s_bnd = s_cones[0]; - auto l_bnd = lambda_cones[0]; - for (std::size_t j = 0; j < s_bnd.size(); ++j) { - s_bnd[j] += a * ds_cones[0][j]; - l_bnd[j] += a * dlambda_cones[0][j]; - } - f_t res_s = j_norm_sq(s_bnd); - f_t res_l = j_norm_sq(l_bnd); - EXPECT_GE(res_s, -1e-10) << "s left the cone"; - EXPECT_GE(res_l, -1e-10) << "lambda left the cone"; - EXPECT_NEAR(std::min(res_s, res_l), 0.0, 1e-10) << "neither hit the boundary"; - - // At (1 − ε) α, both should be strictly interior. - f_t a_int = a * (1.0 - 1e-8); - auto s_int = s_cones[0]; - auto l_int = lambda_cones[0]; - for (std::size_t j = 0; j < s_int.size(); ++j) { - s_int[j] += a_int * ds_cones[0][j]; - l_int[j] += a_int * dlambda_cones[0][j]; - } - EXPECT_GT(j_norm_sq(s_int), 0.0) << "s not interior at (1-eps)*alpha"; - EXPECT_GT(j_norm_sq(l_int), 0.0) << "lambda not interior at (1-eps)*alpha"; -} - -TEST_F(second_order_cone_test, apply_hinv2_matches_reference_for_packed_cones) -{ - std::vector dims{1, 3, 5}; - auto offsets = build_offsets(dims); - - std::vector> v_cones{{3.0}, {2.0, -1.0, 0.5}, {1.0, 0.25, -0.75, 0.5, -0.125}}; - std::vector> w_bar_cones{ - {1.0}, {0.0, 0.15, -0.05}, {0.0, 0.10, -0.20, 0.05, 0.15}}; - std::vector inv_eta_host{0.5, 1.25, 0.75}; - - for (std::size_t cone = 0; cone < w_bar_cones.size(); ++cone) { - f_t w1_sq = f_t(0); - for (std::size_t j = 1; j < w_bar_cones[cone].size(); ++j) { - w1_sq += w_bar_cones[cone][j] * w_bar_cones[cone][j]; - } - w_bar_cones[cone][0] = std::sqrt(f_t(1) + w1_sq); - } - - auto v = make_device_vector(pack_cones(v_cones)); - auto w_bar = make_device_vector(pack_cones(w_bar_cones)); - auto inv_eta = make_device_vector(inv_eta_host); - auto d_offsets = make_device_vector(offsets); - rmm::device_uvector out(v.size(), stream_); - - launch_apply_hinv2(v, out, w_bar, inv_eta, d_offsets, static_cast(dims.size())); - - auto actual = copy_to_host(out); - auto expected = pack_cones(std::vector>{ - ref_apply_hinv2_single(v_cones[0], w_bar_cones[0], inv_eta_host[0]), - ref_apply_hinv2_single(v_cones[1], w_bar_cones[1], inv_eta_host[1]), - ref_apply_hinv2_single(v_cones[2], w_bar_cones[2], inv_eta_host[2])}); - - expect_vector_near(actual, expected, 1e-12, 1e-10, "apply_hinv2"); -} - -TEST_F(second_order_cone_test, affine_cone_rhs_matches_hinv2_of_primal) -{ - std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; - std::vector dims{3, 5}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - launch_nt_scaling(cones, stream_); - - rmm::device_uvector d_out(cones.m_c, stream_); - launch_affine_cone_rhs(cones, d_out); - - auto actual = copy_to_host(d_out); - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - - for (i_t c = 0; c < static_cast(dims.size()); ++c) { - auto ref = ref_apply_hinv2_single(s_cones[c], slice_cone(w_bar_host, offsets, c), inv_eta_h[c]); - auto act = slice_cone(actual, offsets, c); - expect_vector_near(act, ref, 1e-10, 1e-8, "affine_cone_rhs"); - } -} - -TEST_F(second_order_cone_test, accumulate_cone_hinv2_matvec_matches_reference) -{ - std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; - std::vector> x_cones{{0.3, -0.1, 0.2}, {-0.5, 0.2, 0.1, -0.3, 0.15}}; - std::vector> base_cones{{1.0, 2.0, 3.0}, {0.5, -0.5, 0.25, -0.25, 0.75}}; - std::vector dims{3, 5}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - auto d_x = make_device_vector(pack_cones(x_cones)); - auto d_out = make_device_vector(pack_cones(base_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - launch_nt_scaling(cones, stream_); - - rmm::device_uvector d_hinv2_x(cones.m_c, stream_); - launch_accumulate_cone_hinv2(d_x, cones, d_hinv2_x, d_out); - - auto actual = copy_to_host(d_out); - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - - for (i_t c = 0; c < static_cast(dims.size()); ++c) { - auto ref_hinv2 = - ref_apply_hinv2_single(x_cones[c], slice_cone(w_bar_host, offsets, c), inv_eta_h[c]); - auto actual_c = slice_cone(actual, offsets, c); - std::vector ref(actual_c.size()); - for (i_t j = 0; j < static_cast(ref.size()); ++j) { - ref[j] = base_cones[c][j] + ref_hinv2[j]; - } - expect_vector_near(actual_c, ref, 1e-10, 1e-8, "accumulate_cone_hinv2"); - } -} - -TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_matches_reference_with_nt_scaling) -{ - std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; - std::vector dims{3, 5}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - - launch_nt_scaling(cones, stream_); - - auto block_offsets_host = copy_to_host(cones.block_offsets); - i_t total_blk = dims[0] * dims[0] + dims[1] * dims[1]; - std::vector q_vals(total_blk, f_t(0)); - std::vector csr_indices(total_blk); - constexpr i_t aug_offset = 2; - for (i_t e = 0; e < total_blk; ++e) { - csr_indices[e] = aug_offset + (total_blk - 1 - e); - } - auto d_csr_indices = make_device_vector(csr_indices); - auto d_q_values = make_device_vector(q_vals); - rmm::device_uvector d_aug_x(total_blk + aug_offset, stream_); - RAFT_CUDA_TRY( - cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * (total_blk + aug_offset), stream_)); - launch_cone_block_scatter(cones, d_aug_x, d_csr_indices, d_q_values); - - auto actual = copy_to_host(d_aug_x); - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - - for (i_t e = 0; e < aug_offset; ++e) { - EXPECT_EQ(actual[e], f_t(0)) << "untouched prefix entry " << e; - } - - i_t blk_off = 0; - for (i_t c = 0; c < static_cast(dims.size()); ++c) { - auto w_c = slice_cone(w_bar_host, offsets, c); - auto ref = ref_build_hinv2_block_single(w_c, inv_eta_h[c]); - i_t blk_sz = dims[c] * dims[c]; - for (i_t e = 0; e < blk_sz; ++e) { - EXPECT_NEAR(actual[csr_indices[blk_off + e]], -ref[e], 1e-10 + 1e-8 * std::abs(ref[e])) - << "cone " << c << " entry " << e; - } - blk_off += blk_sz; - } -} - -TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_matvec_matches_apply_hinv2) -{ - std::vector> s_cones{{5.0, 1.0, -1.0, 0.5, 0.3}}; - std::vector> lambda_cones{{4.0, 0.5, 1.0, -0.3, 0.2}}; - std::vector dims{5}; - i_t q = dims[0]; - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - i_t total_blk = q * q; - std::vector csr_indices(total_blk); - std::iota(csr_indices.begin(), csr_indices.end(), 0); - std::vector q_vals(total_blk, f_t(0)); - auto d_csr_indices = make_device_vector(csr_indices); - auto d_q_values = make_device_vector(q_vals); - rmm::device_uvector d_aug_x(total_blk, stream_); - RAFT_CUDA_TRY(cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * total_blk, stream_)); - launch_cone_block_scatter(cones, d_aug_x, d_csr_indices, d_q_values); - - auto scattered = copy_to_host(d_aug_x); - std::vector block(total_blk); - for (i_t e = 0; e < total_blk; ++e) { - block[e] = -scattered[e]; - } - - std::vector> test_vectors{ - {1.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0, 0.0}, {0.3, -0.1, 0.2, -0.5, 0.15}}; - - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - - for (const auto& v : test_vectors) { - // Host mat-vec: y = block * v - std::vector y(q, f_t(0)); - for (i_t r = 0; r < q; ++r) { - for (i_t c = 0; c < q; ++c) { - y[r] += block[r * q + c] * v[c]; - } - } - - auto ref = ref_apply_hinv2_single(v, w_bar_host, inv_eta_h[0]); - expect_vector_near(y, ref, 1e-10, 1e-8, "block_matvec_vs_apply"); - } -} - -TEST_F(second_order_cone_test, scatter_hinv2_into_augmented_large_cone) -{ - std::vector dims{513}; - - auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); - auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); - - auto d_s = make_device_vector(s_cone); - auto d_lambda = make_device_vector(lambda_cone); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - i_t total_blk = dims[0] * dims[0]; - std::vector csr_indices(total_blk); - std::iota(csr_indices.begin(), csr_indices.end(), 0); - std::vector q_vals(total_blk, f_t(0)); - auto d_csr_indices = make_device_vector(csr_indices); - auto d_q_values = make_device_vector(q_vals); - rmm::device_uvector d_aug_x(total_blk, stream_); - RAFT_CUDA_TRY(cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * total_blk, stream_)); - launch_cone_block_scatter(cones, d_aug_x, d_csr_indices, d_q_values); - - auto scattered = copy_to_host(d_aug_x); - std::vector block(total_blk); - for (i_t e = 0; e < total_blk; ++e) { - block[e] = -scattered[e]; - } - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - - // Spot-check: block * e_0 should match apply_Hinv2(e_0) - i_t q = dims[0]; - std::vector col0(q); - for (i_t r = 0; r < q; ++r) { - col0[r] = block[r * q]; - } - std::vector e0(q, f_t(0)); - e0[0] = f_t(1); - auto ref = ref_apply_hinv2_single(e0, w_bar_host, inv_eta_h[0]); - expect_vector_near(col0, ref, 1e-8, 1e-6, "hinv2_block_col0_large"); - - // Symmetry check: block[r][c] == block[c][r] - for (i_t r = 0; r < std::min(q, i_t(50)); ++r) { - for (i_t c = r + 1; c < std::min(q, i_t(50)); ++c) { - EXPECT_NEAR(block[r * q + c], block[c * q + r], 1e-10) - << "asymmetry at (" << r << "," << c << ")"; - } - } -} - -TEST_F(second_order_cone_test, fused_corrector_matches_reference_with_nt_scaling) -{ - std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; - std::vector dims{3, 5}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - - launch_nt_scaling(cones, stream_); - - std::vector> dx_aff_cones{{0.3, -0.1, 0.2}, {-0.5, 0.2, 0.1, -0.3, 0.15}}; - f_t sigma_mu = 0.1; - - auto d_dx_aff = make_device_vector(pack_cones(dx_aff_cones)); - rmm::device_uvector d_out(cones.omega.size(), stream_); - - launch_fused_corrector(d_dx_aff, cones, sigma_mu, d_out); - - auto actual = copy_to_host(d_out); - auto omega_host = copy_to_host(cones.omega); - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - auto inv_1pw0_h = copy_to_host(cones.inv_1pw0); - auto rho_h = copy_to_host(cones.rho); - - for (i_t c = 0; c < static_cast(dims.size()); ++c) { - auto ref = ref_fused_corrector_single(dx_aff_cones[c], - slice_cone(omega_host, offsets, c), - slice_cone(w_bar_host, offsets, c), - inv_eta_h[c], - inv_1pw0_h[c], - rho_h[c], - sigma_mu); - auto act = slice_cone(actual, offsets, c); - expect_vector_near(act, ref, 1e-10, 1e-8, "fused_corrector"); - } -} - -TEST_F(second_order_cone_test, fused_corrector_strided_loop_for_large_cone) -{ - std::vector dims{513}; - auto offsets = build_offsets(dims); - - auto s_cone = make_patterned_cone(dims[0], 5.0, 0.005); - auto lambda_cone = make_patterned_cone(dims[0], 4.0, 0.004); - - auto d_s = make_device_vector(s_cone); - auto d_lambda = make_device_vector(lambda_cone); - cone_data_t cones(1, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - - launch_nt_scaling(cones, stream_); - - auto dx_aff_cone = make_patterned_cone(dims[0], 0.5, 0.003); - f_t sigma_mu = 0.25; - - auto d_dx_aff = make_device_vector(dx_aff_cone); - rmm::device_uvector d_out(cones.omega.size(), stream_); - - launch_fused_corrector(d_dx_aff, cones, sigma_mu, d_out); - - auto actual = copy_to_host(d_out); - auto omega_host = copy_to_host(cones.omega); - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - auto inv_1pw0_h = copy_to_host(cones.inv_1pw0); - auto rho_h = copy_to_host(cones.rho); - - auto ref = ref_fused_corrector_single( - dx_aff_cone, omega_host, w_bar_host, inv_eta_h[0], inv_1pw0_h[0], rho_h[0], sigma_mu); - expect_vector_near(actual, ref, 1e-8, 1e-6, "fused_corrector_large"); -} - -TEST_F(second_order_cone_test, cone_block_scatter_with_q_overlap) -{ - std::vector> s_cones{{3.0, 0.5, -0.3}}; - std::vector> lambda_cones{{2.0, -0.2, 0.4}}; - std::vector dims{3}; - i_t K = 1; - i_t q_k = 3; - i_t total_block_nnz = q_k * q_k; - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - cone_data_t cones(K, dims, cuopt::make_span(d_s), cuopt::make_span(d_lambda), stream_); - launch_nt_scaling(cones, stream_); - - f_t dual_perturb = 1e-6; - std::vector q_vals(total_block_nnz, f_t(0)); - q_vals[0] = 0.5 + dual_perturb; - q_vals[4] = 0.3 + dual_perturb; - q_vals[8] = 0.1 + dual_perturb; - q_vals[1] = 0.05; - q_vals[3] = 0.05; - - std::vector cone_csr_indices(total_block_nnz); - std::iota(cone_csr_indices.begin(), cone_csr_indices.end(), 0); - auto d_cone_csr_indices = make_device_vector(cone_csr_indices); - auto d_cone_Q_values = make_device_vector(q_vals); - - rmm::device_uvector d_aug_x(total_block_nnz, stream_); - RAFT_CUDA_TRY(cudaMemsetAsync(d_aug_x.data(), 0, sizeof(f_t) * total_block_nnz, stream_)); - - launch_cone_block_scatter(cones, d_aug_x, d_cone_csr_indices, d_cone_Q_values); - - auto actual = copy_to_host(d_aug_x); - auto w_bar_h = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - auto ref_block = ref_build_hinv2_block_single(w_bar_h, inv_eta_h[0]); - - for (i_t e = 0; e < total_block_nnz; ++e) { - f_t expected = -ref_block[e] - q_vals[e]; - EXPECT_NEAR(actual[e], expected, 1e-10 + 1e-8 * std::abs(expected)) - << "entry " << e << " (Q overlap test)"; - } -} - -TEST_F(second_order_cone_test, recover_cone_dz_from_target_matches_reference) -{ - std::vector> s_cones{{2.0, 0.5, 0.25}, {3.0, 0.25, -0.5, 0.75, -0.25}}; - std::vector> lambda_cones{{1.5, -0.25, 0.1}, {2.5, -0.1, 0.3, -0.2, 0.15}}; - std::vector> dx_cones{{0.3, -0.1, 0.2}, {-0.5, 0.2, 0.1, -0.3, 0.15}}; - std::vector dims{3, 5}; - auto offsets = build_offsets(dims); - - auto d_s = make_device_vector(pack_cones(s_cones)); - auto d_lambda = make_device_vector(pack_cones(lambda_cones)); - auto d_dx = make_device_vector(pack_cones(dx_cones)); - cone_data_t cones(static_cast(dims.size()), - dims, - cuopt::make_span(d_s), - cuopt::make_span(d_lambda), - stream_); - launch_nt_scaling(cones, stream_); - - rmm::device_uvector d_rhs(cones.m_c, stream_); - launch_affine_cone_rhs(cones, d_rhs); - auto rhs_actual = copy_to_host(d_rhs); - - std::vector target_host(rhs_actual.size(), f_t(0)); - for (std::size_t j = 0; j < rhs_actual.size(); ++j) { - target_host[j] = -rhs_actual[j]; - } - auto d_target = make_device_vector(target_host); - - rmm::device_uvector d_hinv2_dx(cones.m_c, stream_); - rmm::device_uvector d_dz(cones.m_c, stream_); - launch_recover_cone_dz_from_target(d_dx, cones, d_target, d_hinv2_dx, d_dz); - - auto actual = copy_to_host(d_dz); - auto w_bar_host = copy_to_host(cones.w_bar); - auto inv_eta_h = copy_to_host(cones.inv_eta); - - for (i_t c = 0; c < static_cast(dims.size()); ++c) { - auto ref_hinv2 = - ref_apply_hinv2_single(dx_cones[c], slice_cone(w_bar_host, offsets, c), inv_eta_h[c]); - auto rhs_c = slice_cone(rhs_actual, offsets, c); - auto act = slice_cone(actual, offsets, c); - std::vector ref(act.size()); - for (i_t j = 0; j < static_cast(ref.size()); ++j) { - ref[j] = -rhs_c[j] - ref_hinv2[j]; - } - expect_vector_near(act, ref, 1e-10, 1e-8, "recover_cone_dz_from_target"); - } -} - -} // namespace cuopt::linear_programming::dual_simplex::test From a20ac270a29893f3fdc5051e5d6ada96d6de8b96 Mon Sep 17 00:00:00 2001 From: Yan Zaretskiy Date: Tue, 28 Apr 2026 12:22:58 -0700 Subject: [PATCH 27/68] Support direct MPS SOC conversion --- cpp/src/barrier/barrier.cu | 5 +- cpp/src/dual_simplex/presolve.cpp | 66 --- cpp/src/dual_simplex/solve.cpp | 45 -- cpp/src/dual_simplex/user_problem.hpp | 2 - cpp/src/pdlp/translate.hpp | 184 ++++++-- .../dual_simplex/unit_tests/solve_barrier.cu | 402 ------------------ 6 files changed, 147 insertions(+), 557 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index 475879c8fd..bdb571ec32 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -2626,8 +2626,9 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0 && data.Q_diagonal) { + // In ADAT mode, diagonal Q is folded into D. In augmented mode, Q is an explicit block in the + // KKT matrix, so adding it here would double count the quadratic objective. + if (!use_augmented && data.Q.n > 0 && data.Q_diagonal) { cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_Q_diag_.data(), data.d_diag_.data()), data.d_diag_.data(), diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 7adee4525a..ca98dc9da5 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -15,7 +15,6 @@ #include #include -#include namespace cuopt::linear_programming::dual_simplex { @@ -25,68 +24,6 @@ static i_t linear_var_count(const lp_problem_t& problem) return problem.second_order_cone_dims.empty() ? problem.num_cols : problem.cone_var_start; } -template -static void lift_second_order_cone_rows(const user_problem_t& user_problem, - std::vector& row_sense, - lp_problem_t& problem) -{ - if (user_problem.second_order_cone_row_dims.empty()) { return; } - - auto& dims = user_problem.second_order_cone_row_dims; - const i_t lifted_row_count = std::accumulate(dims.begin(), dims.end(), i_t{0}); - const i_t cone_row_start = user_problem.cone_row_start; - - const i_t old_num_cols = problem.num_cols; - const i_t new_num_cols = old_num_cols + lifted_row_count; - const i_t old_nnz = problem.A.col_start[old_num_cols]; - const i_t new_nnz = old_nnz + lifted_row_count; - - auto old_A = problem.A; - csc_matrix_t lifted_A(problem.num_rows, new_num_cols, new_nnz); - - i_t nz = 0; - for (i_t j = 0; j < old_num_cols; ++j) { - lifted_A.col_start[j] = nz; - for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { - lifted_A.i[nz] = old_A.i[p]; - lifted_A.x[nz] = old_A.x[p]; - ++nz; - } - } - - for (i_t offset = 0; offset < lifted_row_count; ++offset) { - const i_t j = old_num_cols + offset; - const i_t row = cone_row_start + offset; - lifted_A.col_start[j] = nz; - lifted_A.i[nz] = row; - lifted_A.x[nz] = 1.0; - row_sense[row] = 'E'; - ++nz; - } - lifted_A.col_start[new_num_cols] = nz; - assert(nz == new_nnz); - - std::vector objective(new_num_cols, 0.0); - std::vector lower(new_num_cols, 0.0); - std::vector upper(new_num_cols, inf); - for (i_t j = 0; j < old_num_cols; ++j) { - objective[j] = problem.objective[j]; - lower[j] = problem.lower[j]; - upper[j] = problem.upper[j]; - } - - problem.A = lifted_A; - problem.A.n = new_num_cols; - problem.objective = objective; - problem.lower = lower; - problem.upper = upper; - problem.num_cols = new_num_cols; - if (problem.second_order_cone_dims.empty()) { problem.cone_var_start = old_num_cols; } - problem.second_order_cone_dims.insert(problem.second_order_cone_dims.end(), - user_problem.second_order_cone_row_dims.begin(), - user_problem.second_order_cone_row_dims.end()); -} - template i_t remove_empty_cols(lp_problem_t& problem, i_t& num_empty_cols, @@ -759,9 +696,6 @@ void convert_user_problem(const user_problem_t& user_problem, // Make a copy of row_sense so we can modify it std::vector row_sense = user_problem.row_sense; - if (settings.barrier && !user_problem.second_order_cone_row_dims.empty()) { - lift_second_order_cone_rows(user_problem, row_sense, problem); - } // The original problem can have constraints in the form // a_i^T x >= b, a_i^T x <= b, and a_i^T x == b diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 7d18d192eb..3755931a12 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -37,48 +37,6 @@ namespace cuopt::linear_programming::dual_simplex { namespace { -template -bool validate_second_order_cone_row_metadata(const user_problem_t& user_problem, - const simplex_solver_settings_t& settings) -{ - if (user_problem.second_order_cone_row_dims.empty()) { return true; } - - i_t lifted_row_count = 0; - for (auto q_k : user_problem.second_order_cone_row_dims) { - if (q_k < 0) { - settings.log.printf("Error: second-order cone row dimensions must be nonnegative\n"); - return false; - } - lifted_row_count += q_k; - } - - if (user_problem.cone_row_start < 0) { - settings.log.printf("Error: cone_row_start must be nonnegative\n"); - return false; - } - - const i_t cone_row_end = user_problem.cone_row_start + lifted_row_count; - if (cone_row_end > user_problem.num_rows) { - settings.log.printf("Error: second-order cone row block exceeds the number of rows\n"); - return false; - } - - if (user_problem.num_range_rows > static_cast(user_problem.range_rows.size())) { - settings.log.printf("Error: range row metadata is inconsistent\n"); - return false; - } - - for (i_t k = 0; k < user_problem.num_range_rows; ++k) { - const i_t row = user_problem.range_rows[k]; - if (row >= user_problem.cone_row_start && row < cone_row_end) { - settings.log.printf("Error: range rows cannot intersect the second-order cone row block\n"); - return false; - } - } - - return true; -} - template void write_matlab(const std::string& filename, const dual_simplex::lp_problem_t& lp) { @@ -429,9 +387,6 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us simplex_solver_settings_t barrier_settings = settings; barrier_settings.barrier_presolve = true; dualize_info_t dualize_info; - if (!validate_second_order_cone_row_metadata(user_problem, settings)) { - return lp_status_t::NUMERICAL_ISSUES; - } convert_user_problem(user_problem, barrier_settings, original_lp, new_slacks, dualize_info); if (!validate_barrier_cone_layout(original_lp, settings)) { return lp_status_t::NUMERICAL_ISSUES; diff --git a/cpp/src/dual_simplex/user_problem.hpp b/cpp/src/dual_simplex/user_problem.hpp index db62891185..8b0588064c 100644 --- a/cpp/src/dual_simplex/user_problem.hpp +++ b/cpp/src/dual_simplex/user_problem.hpp @@ -54,8 +54,6 @@ struct user_problem_t { std::vector Q_values; i_t cone_var_start{0}; std::vector second_order_cone_dims; - i_t cone_row_start{0}; - std::vector second_order_cone_row_dims; }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 4e5c0be991..1b39dffcaa 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -15,6 +15,10 @@ #include +#include +#include +#include + namespace cuopt::linear_programming { template @@ -110,18 +114,18 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( error_type_t::ValidationError, "Quadratic-constraint flag is set, but no constraints were provided"); - const i_t original_rows = static_cast(user_problem.num_rows); // Use a practical tolerance for text-parsed MPS numeric values. const f_t tol = std::numeric_limits::epsilon() * 2; - // SOC: Q is n×n diagonal CSR (offsets length n+1). Exactly q_n = nnz on the main diagonal, at - // q_n distinct variable indices: one −1 (head) and (q_n−1) +1 (tails). Lifting: q_n rows, each - // with −1 in one column; the **first** row must be the head (variable with Q = −1); order of - // remaining rows (+1 diagonals) is unconstrained (CSR row scan order). - - const i_t old_nnz = csr_A.row_start[original_rows]; - std::vector row_cone_dims{}; - row_cone_dims.reserve(qcs.size()); + // SOC conversion accepts only diagonal Lorentz-form QCMATRIX rows: + // -x_head^2 + sum_i x_tail_i^2 <= 0. + // The barrier consumes SOCs as trailing variable blocks [head, tails...], so we validate all + // QCMATRIX blocks first, then apply a single column permutation to the linear model. + std::vector> cone_vars; + std::vector cone_dims; + std::vector is_cone_var(static_cast(n), 0); + cone_vars.reserve(qcs.size()); + cone_dims.reserve(qcs.size()); for (const auto& qc : qcs) { cuopt_expects(qc.constraint_row_type == 'L', @@ -155,7 +159,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( cuopt_expects( qc.quadratic_offsets.size() == static_cast(n) + 1, error_type_t::ValidationError, - "Quadratic constraint '%s' Q must be n×n in CSR: expected %zu CSR row pointers (offsets " + "Quadratic constraint '%s' Q must be n by n in CSR: expected %zu CSR row pointers (offsets " "length n+1), got %zu (n = %d)", qc.constraint_row_name.c_str(), static_cast(n) + 1, @@ -174,7 +178,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( "Quadratic constraint '%s' Q CSR offsets[0] must be 0", qc.constraint_row_name.c_str()); - // Verify Q: n×n CSR, diagonal entries only, Lorentz pattern, then build the lift. + // Verify Q: n by n CSR, diagonal entries only, Lorentz pattern. // Scan each row r: empty or one nnz on (r,r) with value -1 (head) or +1 (tail); // tail order follows this scan; no requirement that diagonal indices be sorted. i_t head = static_cast(-1); @@ -185,6 +189,13 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( for (i_t r = 0; r < n; ++r) { const i_t p_beg = qc.quadratic_offsets[static_cast(r)]; const i_t p_end = qc.quadratic_offsets[static_cast(r + 1)]; + cuopt_expects(p_beg >= 0 && p_beg <= p_end && p_end <= q_n, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d has invalid CSR offsets [%d, %d)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(p_beg), + static_cast(p_end)); if (p_beg == p_end) { continue; } @@ -247,44 +258,137 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( "Quadratic constraint '%s' SOC Q: internal error (head index invalid)", qc.constraint_row_name.c_str()); - row_cone_dims.push_back(q_n); - dual_simplex::csr_matrix_t lift_block(q_n, n, q_n); - for (i_t t = 0; t <= q_n; ++t) { - lift_block.row_start[t] = t; + std::vector cone; + cone.reserve(static_cast(q_n)); + cone.push_back(head); + cone.insert(cone.end(), tail_row_vars.begin(), tail_row_vars.end()); + for (const i_t var : cone) { + cuopt_expects(!is_cone_var[static_cast(var)], + error_type_t::ValidationError, + "Variable %d appears in more than one SOC QCMATRIX block; overlapping cones " + "are not supported", + static_cast(var)); + is_cone_var[static_cast(var)] = 1; } + cone_dims.push_back(q_n); + cone_vars.push_back(std::move(cone)); + } - // One lift row per cone component: -1 in column head, then -1 in each tail column (since our - // slack variable is done by + s form) (order matches tail_row_vars from the Q scan). - lift_block.j[0] = head; - lift_block.x[0] = f_t(-1); - for (i_t t = 0; t < q_n - 1; ++t) { - lift_block.j[static_cast(t) + 1U] = tail_row_vars[static_cast(t)]; - lift_block.x[static_cast(t) + 1U] = f_t(-1); + std::vector old_to_new(static_cast(n), i_t{-1}); + std::vector new_to_old; + new_to_old.reserve(static_cast(n)); + for (i_t j = 0; j < n; ++j) { + if (is_cone_var[static_cast(j)]) { continue; } + old_to_new[static_cast(j)] = static_cast(new_to_old.size()); + new_to_old.push_back(j); + } + const i_t cone_var_start = static_cast(new_to_old.size()); + for (const auto& cone : cone_vars) { + for (const i_t old_j : cone) { + old_to_new[static_cast(old_j)] = static_cast(new_to_old.size()); + new_to_old.push_back(old_j); } - cuopt_expects(csr_A.append_rows(lift_block) == 0, - error_type_t::RuntimeError, - "Internal error while appending SOC lifting rows to CSR A"); } - - // Update user_problem to include the new SOC rows - const i_t next_row = static_cast(csr_A.m); - const i_t lifted_rows = next_row - original_rows; - const i_t new_nnz = old_nnz + lifted_rows; - cuopt_expects(csr_A.row_start[next_row] == new_nnz, + cuopt_expects(static_cast(new_to_old.size()) == n, error_type_t::RuntimeError, - "Internal error while building SOC lifting rows in CSR A"); + "Internal error while building SOC variable permutation"); + + for (i_t row = 0; row < csr_A.m; ++row) { + for (i_t p = csr_A.row_start[static_cast(row)]; + p < csr_A.row_start[static_cast(row + 1)]; + ++p) { + const i_t old_j = csr_A.j[static_cast(p)]; + cuopt_expects(old_j >= 0 && old_j < n, + error_type_t::ValidationError, + "Linear constraint matrix column index %d is outside [0, %d)", + static_cast(old_j), + static_cast(n)); + csr_A.j[static_cast(p)] = old_to_new[static_cast(old_j)]; + } + } - user_problem.rhs.resize(next_row, f_t(0)); - user_problem.row_sense.resize(next_row, 'E'); - if (user_problem.row_names.size() == static_cast(original_rows)) { - for (i_t r = original_rows; r < next_row; ++r) { - user_problem.row_names.push_back("_CUOPT_soc_row_" + std::to_string(r - original_rows)); + auto permute_dense_by_old_to_new = [&](auto& values, const char* name) { + if (values.empty()) { return; } + using value_t = typename std::decay_t::value_type; + cuopt_expects(values.size() == static_cast(n), + error_type_t::ValidationError, + "%s length %zu does not match number of variables %d", + name, + values.size(), + static_cast(n)); + std::vector permuted(values.size()); + for (i_t old_j = 0; old_j < n; ++old_j) { + permuted[static_cast(old_to_new[static_cast(old_j)])] = + std::move(values[static_cast(old_j)]); } + values = std::move(permuted); + }; + + permute_dense_by_old_to_new(user_problem.objective, "objective"); + permute_dense_by_old_to_new(user_problem.lower, "lower bounds"); + permute_dense_by_old_to_new(user_problem.upper, "upper bounds"); + permute_dense_by_old_to_new(user_problem.var_types, "variable types"); + permute_dense_by_old_to_new(user_problem.col_names, "column names"); + + if (!user_problem.Q_values.empty()) { + cuopt_expects(user_problem.Q_indices.size() == user_problem.Q_values.size(), + error_type_t::ValidationError, + "Quadratic objective indices and values length mismatch"); + cuopt_expects(user_problem.Q_offsets.size() == static_cast(n) + 1, + error_type_t::ValidationError, + "Quadratic objective CSR offsets length must be n+1 when SOC QCMATRIX " + "conversion permutes variables"); + cuopt_expects(user_problem.Q_offsets[0] == 0, + error_type_t::ValidationError, + "Quadratic objective CSR offsets[0] must be 0"); + cuopt_expects(user_problem.Q_offsets[static_cast(n)] == + static_cast(user_problem.Q_values.size()), + error_type_t::ValidationError, + "Quadratic objective CSR last offset must equal number of nonzeros"); + + std::vector q_offsets(static_cast(n) + 1, 0); + for (i_t old_row = 0; old_row < n; ++old_row) { + const i_t p_beg = user_problem.Q_offsets[static_cast(old_row)]; + const i_t p_end = user_problem.Q_offsets[static_cast(old_row + 1)]; + cuopt_expects( + p_beg >= 0 && p_beg <= p_end && p_end <= static_cast(user_problem.Q_values.size()), + error_type_t::ValidationError, + "Quadratic objective CSR offsets are invalid at row %d", + static_cast(old_row)); + const i_t new_row = old_to_new[static_cast(old_row)]; + q_offsets[static_cast(new_row + 1)] = p_end - p_beg; + } + for (i_t row = 0; row < n; ++row) { + q_offsets[static_cast(row + 1)] += q_offsets[static_cast(row)]; + } + + std::vector q_indices(user_problem.Q_indices.size()); + std::vector q_values(user_problem.Q_values.size()); + auto q_write = q_offsets; + for (i_t old_row = 0; old_row < n; ++old_row) { + const i_t new_row = old_to_new[static_cast(old_row)]; + for (i_t p = user_problem.Q_offsets[static_cast(old_row)]; + p < user_problem.Q_offsets[static_cast(old_row + 1)]; + ++p) { + const i_t old_col = user_problem.Q_indices[static_cast(p)]; + cuopt_expects(old_col >= 0 && old_col < n, + error_type_t::ValidationError, + "Quadratic objective column index %d is outside [0, %d)", + static_cast(old_col), + static_cast(n)); + const i_t dst = q_write[static_cast(new_row)]++; + q_indices[static_cast(dst)] = old_to_new[static_cast(old_col)]; + q_values[static_cast(dst)] = user_problem.Q_values[static_cast(p)]; + } + } + + user_problem.Q_offsets = std::move(q_offsets); + user_problem.Q_indices = std::move(q_indices); + user_problem.Q_values = std::move(q_values); } - user_problem.num_rows = next_row; - user_problem.cone_row_start = original_rows; - user_problem.second_order_cone_row_dims = std::move(row_cone_dims); + user_problem.cone_var_start = cone_var_start; + user_problem.second_order_cone_dims = std::move(cone_dims); } csr_A.to_compressed_col(user_problem.A); diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index 1b4a866221..d630bfac18 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -32,72 +32,6 @@ static void init_handler(const raft::handle_t* handle_ptr) handle_ptr->get_cusparse_handle(), CUSPARSE_POINTER_MODE_DEVICE, handle_ptr->get_stream())); } -template -static void populate_basic_qp_socp_problem(user_problem_t& user_problem, - bool explicit_cone_variables) -{ - constexpr i_t num_rows = 9; - constexpr f_t p00 = static_cast(1.4652521089139698); - constexpr f_t p01 = static_cast(0.6137176286085666); - constexpr f_t p02 = static_cast(-1.1527861771130112); - constexpr f_t p11 = static_cast(2.219109946678485); - constexpr f_t p12 = static_cast(-1.4400420548730628); - constexpr f_t p22 = static_cast(1.6014483534926371); - - user_problem.num_rows = num_rows; - user_problem.rhs = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0}; - user_problem.row_sense = {'L', 'L', 'L', 'L', 'L', 'L', 'E', 'E', 'E'}; - user_problem.num_range_rows = 0; - - if (explicit_cone_variables) { - user_problem.num_cols = 6; - user_problem.objective = {0.1, -2.0, 1.0, 0.0, 0.0, 0.0}; - - user_problem.A.m = num_rows; - user_problem.A.n = user_problem.num_cols; - user_problem.A.nz_max = 12; - user_problem.A.reallocate(12); - user_problem.A.col_start = {0, 3, 6, 9, 10, 11, 12}; - user_problem.A.i = {0, 3, 6, 1, 4, 7, 2, 5, 8, 6, 7, 8}; - user_problem.A.x = {2.0, -2.0, 1.0, 2.0, -2.0, 1.0, 2.0, -2.0, 1.0, 1.0, 1.0, 1.0}; - - user_problem.lower = {-inf, -inf, -inf, 0.0, 0.0, 0.0}; - user_problem.upper.assign(user_problem.num_cols, inf); - - user_problem.Q_offsets = {0, 3, 6, 9, 9, 9, 9}; - user_problem.Q_indices = {0, 1, 2, 0, 1, 2, 0, 1, 2}; - user_problem.Q_values = {p00, p01, p02, p01, p11, p12, p02, p12, p22}; - - user_problem.cone_var_start = 3; - user_problem.second_order_cone_dims = {3}; - user_problem.problem_name = "basic_qp_socp_explicit_cone"; - } else { - user_problem.num_cols = 3; - user_problem.objective = {0.1, -2.0, 1.0}; - - user_problem.A.m = num_rows; - user_problem.A.n = user_problem.num_cols; - user_problem.A.nz_max = 9; - user_problem.A.reallocate(9); - user_problem.A.col_start = {0, 3, 6, 9}; - user_problem.A.i = {0, 3, 6, 1, 4, 7, 2, 5, 8}; - user_problem.A.x = {2.0, -2.0, 1.0, 2.0, -2.0, 1.0, 2.0, -2.0, 1.0}; - - user_problem.lower.assign(user_problem.num_cols, -inf); - user_problem.upper.assign(user_problem.num_cols, inf); - - user_problem.Q_offsets = {0, 3, 6, 9}; - user_problem.Q_indices = {0, 1, 2, 0, 1, 2, 0, 1, 2}; - user_problem.Q_values = {p00, p01, p02, p01, p11, p12, p02, p12, p22}; - - user_problem.cone_row_start = 6; - user_problem.second_order_cone_row_dims = {3}; - user_problem.problem_name = "basic_qp_socp_row_cone"; - } - - user_problem.var_types.assign(user_problem.num_cols, variable_type_t::CONTINUOUS); -} - TEST(barrier, chess_set) { namespace dual_simplex = cuopt::linear_programming::dual_simplex; @@ -299,225 +233,6 @@ TEST(barrier, cone_metadata_reindexed_when_slack_is_inserted_before_cones) EXPECT_EQ(barrier_lp.cone_var_start, 2); } -TEST(barrier, row_cone_block_is_lifted_into_trailing_cone_variables) -{ - raft::handle_t handle{}; - init_handler(&handle); - - using namespace cuopt::linear_programming::dual_simplex; - user_problem_t user_problem(&handle); - - constexpr int m = 3; - constexpr int n = 2; - constexpr int nz = 4; - - user_problem.num_rows = m; - user_problem.num_cols = n; - user_problem.objective = {0.0, 0.0}; - - user_problem.A.m = m; - user_problem.A.n = n; - user_problem.A.nz_max = nz; - user_problem.A.reallocate(nz); - user_problem.A.col_start = {0, 2, 4}; - user_problem.A.i[0] = 0; - user_problem.A.x[0] = 1.0; - user_problem.A.i[1] = 2; - user_problem.A.x[1] = 1.0; - user_problem.A.i[2] = 1; - user_problem.A.x[2] = -1.0; - user_problem.A.i[3] = 2; - user_problem.A.x[3] = 2.0; - - user_problem.rhs = {3.0, 1.0, 4.0}; - user_problem.row_sense = {'E', 'E', 'E'}; - user_problem.lower.assign(n, 0.0); - user_problem.upper.assign(n, inf); - user_problem.num_range_rows = 0; - user_problem.cone_row_start = 0; - user_problem.second_order_cone_row_dims = {3}; - - simplex_solver_settings_t settings; - settings.barrier = true; - settings.barrier_presolve = true; - settings.dualize = 0; - settings.scale_columns = false; - - std::vector new_slacks; - dualize_info_t dualize_info; - lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); - convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); - - EXPECT_TRUE(new_slacks.empty()); - EXPECT_EQ(original_lp.num_cols, 5); - EXPECT_EQ(original_lp.cone_var_start, 2); - EXPECT_EQ(original_lp.second_order_cone_dims, std::vector({3})); - - for (int j = 2; j < 5; ++j) { - EXPECT_EQ(original_lp.A.col_start[j + 1] - original_lp.A.col_start[j], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[j]], j - 2); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[j]], 1.0); - EXPECT_EQ(original_lp.objective[j], 0.0); - EXPECT_EQ(original_lp.lower[j], 0.0); - EXPECT_EQ(original_lp.upper[j], inf); - } -} - -TEST(barrier, row_cone_block_and_scalar_inequality_order_as_linear_slack_then_cone) -{ - raft::handle_t handle{}; - init_handler(&handle); - - using namespace cuopt::linear_programming::dual_simplex; - user_problem_t user_problem(&handle); - - constexpr int m = 4; - constexpr int n = 1; - constexpr int nz = 2; - - user_problem.num_rows = m; - user_problem.num_cols = n; - user_problem.objective = {1.0}; - - user_problem.A.m = m; - user_problem.A.n = n; - user_problem.A.nz_max = nz; - user_problem.A.reallocate(nz); - user_problem.A.col_start = {0, 2}; - user_problem.A.i[0] = 0; - user_problem.A.x[0] = 1.0; - user_problem.A.i[1] = 1; - user_problem.A.x[1] = -1.0; - - user_problem.rhs = {2.0, 0.0, 1.0, 0.0}; - user_problem.row_sense = {'L', 'E', 'E', 'E'}; - user_problem.lower = {0.0}; - user_problem.upper = {inf}; - user_problem.num_range_rows = 0; - user_problem.cone_row_start = 1; - user_problem.second_order_cone_row_dims = {3}; - - simplex_solver_settings_t settings; - settings.barrier = true; - settings.barrier_presolve = true; - settings.dualize = 0; - settings.scale_columns = false; - - std::vector new_slacks; - dualize_info_t dualize_info; - lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); - convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); - - ASSERT_EQ(new_slacks.size(), 1); - EXPECT_EQ(new_slacks[0], 1); - EXPECT_EQ(original_lp.num_cols, 5); - EXPECT_EQ(original_lp.cone_var_start, 2); - EXPECT_EQ(original_lp.second_order_cone_dims, std::vector({3})); - - EXPECT_EQ(original_lp.A.col_start[1] - original_lp.A.col_start[0], 2); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[0]], 0); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[0]], 1.0); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[0] + 1], 1); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[0] + 1], -1.0); - - EXPECT_EQ(original_lp.A.col_start[2] - original_lp.A.col_start[1], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[1]], 0); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[1]], 1.0); - - for (int j = 2; j < 5; ++j) { - EXPECT_EQ(original_lp.A.col_start[j + 1] - original_lp.A.col_start[j], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[j]], j - 1); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[j]], 1.0); - EXPECT_EQ(original_lp.objective[j], 0.0); - EXPECT_EQ(original_lp.lower[j], 0.0); - EXPECT_EQ(original_lp.upper[j], inf); - } -} - -TEST(barrier, explicit_and_lifted_cones_stay_contiguous_after_scalar_slack_insertion) -{ - raft::handle_t handle{}; - init_handler(&handle); - - using namespace cuopt::linear_programming::dual_simplex; - user_problem_t user_problem(&handle); - - constexpr int m = 4; - constexpr int n = 4; - constexpr int nz = 5; - - user_problem.num_rows = m; - user_problem.num_cols = n; - user_problem.objective = {0.0, 0.0, 0.0, 0.0}; - - user_problem.A.m = m; - user_problem.A.n = n; - user_problem.A.nz_max = nz; - user_problem.A.reallocate(nz); - user_problem.A.col_start = {0, 2, 3, 4, 5}; - user_problem.A.i[0] = 0; - user_problem.A.x[0] = 1.0; - user_problem.A.i[1] = 1; - user_problem.A.x[1] = -1.0; - user_problem.A.i[2] = 1; - user_problem.A.x[2] = 1.0; - user_problem.A.i[3] = 2; - user_problem.A.x[3] = 2.0; - user_problem.A.i[4] = 3; - user_problem.A.x[4] = -3.0; - - user_problem.rhs = {2.0, 0.0, 0.0, 0.0}; - user_problem.row_sense = {'L', 'E', 'E', 'E'}; - user_problem.lower.assign(n, 0.0); - user_problem.upper.assign(n, inf); - user_problem.num_range_rows = 0; - user_problem.cone_var_start = 1; - user_problem.second_order_cone_dims = {3}; - user_problem.cone_row_start = 1; - user_problem.second_order_cone_row_dims = {3}; - user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); - - simplex_solver_settings_t settings; - settings.barrier = true; - settings.barrier_presolve = true; - settings.dualize = 0; - settings.scale_columns = false; - - std::vector new_slacks; - dualize_info_t dualize_info; - lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); - convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); - - ASSERT_EQ(new_slacks.size(), 1); - EXPECT_EQ(new_slacks[0], 1); - EXPECT_EQ(original_lp.num_cols, 8); - EXPECT_EQ(original_lp.cone_var_start, 2); - EXPECT_EQ(original_lp.second_order_cone_dims, std::vector({3, 3})); - - EXPECT_EQ(original_lp.A.col_start[2] - original_lp.A.col_start[1], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[1]], 0); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[1]], 1.0); - - EXPECT_EQ(original_lp.A.col_start[3] - original_lp.A.col_start[2], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[2]], 1); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[2]], 1.0); - EXPECT_EQ(original_lp.A.col_start[4] - original_lp.A.col_start[3], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[3]], 2); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[3]], 2.0); - EXPECT_EQ(original_lp.A.col_start[5] - original_lp.A.col_start[4], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[4]], 3); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[4]], -3.0); - - for (int j = 5; j < 8; ++j) { - EXPECT_EQ(original_lp.A.col_start[j + 1] - original_lp.A.col_start[j], 1); - EXPECT_EQ(original_lp.A.i[original_lp.A.col_start[j]], j - 4); - EXPECT_EQ(original_lp.A.x[original_lp.A.col_start[j]], 1.0); - EXPECT_EQ(original_lp.objective[j], 0.0); - EXPECT_EQ(original_lp.lower[j], 0.0); - EXPECT_EQ(original_lp.upper[j], inf); - } -} - TEST(barrier, presolve_reindexes_cone_start_after_empty_column_removal) { raft::handle_t handle{}; @@ -779,123 +494,6 @@ TEST(barrier, socp_min_x0_subject_to_norm_constraint) EXPECT_NEAR(std::abs(solution.x[2]), 0.0, 1e-4); } -TEST(barrier, socp_min_x_subject_to_row_cone_metadata) -{ - // minimize x - // subject to -x + s_0 = 0 - // s_1 = 1 - // s_2 = 0 - // (s_0, s_1, s_2) in Q^3 - // - // Optimal: x* = 1, obj* = 1. - - raft::handle_t handle{}; - init_handler(&handle); - - using namespace cuopt::linear_programming::dual_simplex; - user_problem_t user_problem(&handle); - - constexpr int m = 3; - constexpr int n = 1; - constexpr int nz = 1; - - user_problem.num_rows = m; - user_problem.num_cols = n; - user_problem.objective = {1.0}; - - user_problem.A.m = m; - user_problem.A.n = n; - user_problem.A.nz_max = nz; - user_problem.A.reallocate(nz); - user_problem.A.col_start = {0, 1}; - user_problem.A.i[0] = 0; - user_problem.A.x[0] = -1.0; - - user_problem.rhs = {0.0, 1.0, 0.0}; - user_problem.row_sense = {'E', 'E', 'E'}; - user_problem.lower = {0.0}; - user_problem.upper = {inf}; - user_problem.num_range_rows = 0; - user_problem.problem_name = "socp_row_cone_metadata"; - user_problem.cone_row_start = 0; - user_problem.second_order_cone_row_dims = {3}; - user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); - - simplex_solver_settings_t settings; - settings.barrier = true; - settings.barrier_presolve = false; - settings.dualize = 0; - settings.scale_columns = false; - - lp_solution_t solution(m, n); - auto status = solve_linear_program_with_barrier(user_problem, settings, solution); - - EXPECT_EQ(status, lp_status_t::OPTIMAL); - EXPECT_NEAR(solution.objective, 1.0, 1e-4); - EXPECT_NEAR(solution.x[0], 1.0, 1e-4); -} - -TEST(barrier, basic_qp_socp_row_cone_matches_reference_solution) -{ - raft::handle_t handle{}; - init_handler(&handle); - - using namespace cuopt::linear_programming::dual_simplex; - user_problem_t user_problem(&handle); - populate_basic_qp_socp_problem(user_problem, false); - - simplex_solver_settings_t settings; - settings.barrier = true; - settings.barrier_presolve = false; - settings.dualize = 0; - settings.scale_columns = false; - - lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); - auto status = solve_linear_program_with_barrier(user_problem, settings, solution); - - EXPECT_EQ(status, lp_status_t::OPTIMAL); - EXPECT_NEAR(solution.x[0], -0.5, 1e-3); - EXPECT_NEAR(solution.x[1], 0.435603, 1e-3); - EXPECT_NEAR(solution.x[2], -0.245459, 1e-3); - EXPECT_NEAR(solution.objective, -0.84590, 1e-3); -} - -TEST(barrier, basic_qp_socp_row_cone_matches_explicit_cone_formulation) -{ - raft::handle_t handle{}; - init_handler(&handle); - - using namespace cuopt::linear_programming::dual_simplex; - user_problem_t row_cone_problem(&handle); - user_problem_t explicit_cone_problem(&handle); - populate_basic_qp_socp_problem(row_cone_problem, false); - populate_basic_qp_socp_problem(explicit_cone_problem, true); - - simplex_solver_settings_t settings; - settings.barrier = true; - settings.barrier_presolve = false; - settings.dualize = 0; - settings.scale_columns = false; - - lp_solution_t row_cone_solution(row_cone_problem.num_rows, - row_cone_problem.num_cols); - lp_solution_t explicit_cone_solution(explicit_cone_problem.num_rows, - explicit_cone_problem.num_cols); - - auto row_cone_status = - solve_linear_program_with_barrier(row_cone_problem, settings, row_cone_solution); - auto explicit_status = - solve_linear_program_with_barrier(explicit_cone_problem, settings, explicit_cone_solution); - - EXPECT_EQ(row_cone_status, lp_status_t::OPTIMAL); - EXPECT_EQ(explicit_status, lp_status_t::OPTIMAL); - EXPECT_NEAR(row_cone_solution.objective, explicit_cone_solution.objective, 1e-4); - EXPECT_NEAR(row_cone_solution.objective, -0.84590, 1e-3); - for (int j = 0; j < 3; ++j) { - EXPECT_NEAR(row_cone_solution.x[j], explicit_cone_solution.x[j], 1e-4); - } -} - TEST(barrier, mixed_linear_and_soc_block) { // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. From 883bf6360a1ea749dc4d60e565164ccf2b8bb492 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 30 Apr 2026 02:53:59 -0700 Subject: [PATCH 28/68] add support for rotated second-order cone --- cpp/src/pdlp/translate.hpp | 382 +++++++++++++++++++++++++++++-------- 1 file changed, 300 insertions(+), 82 deletions(-) diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 1b39dffcaa..02fb7ad641 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -12,11 +12,14 @@ #include #include +#include #include +#include #include #include +#include #include namespace cuopt::linear_programming { @@ -117,15 +120,30 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( // Use a practical tolerance for text-parsed MPS numeric values. const f_t tol = std::numeric_limits::epsilon() * 2; - // SOC conversion accepts only diagonal Lorentz-form QCMATRIX rows: - // -x_head^2 + sum_i x_tail_i^2 <= 0. + // SOC conversion accepts: + // 1) diagonal Lorentz-form QCMATRIX rows: + // -x_head^2 + sum_i x_tail_i^2 <= 0 + // 2) canonical rotated SOC rows: + // -2*x_head0*x_head1 + sum_i x_tail_i^2 <= 0 + // represented in MPS QCMATRIX as symmetric off-diagonals (-1,-1). // The barrier consumes SOCs as trailing variable blocks [head, tails...], so we validate all - // QCMATRIX blocks first, then apply a single column permutation to the linear model. + // QCMATRIX blocks first, convert rotated cones via slack variables in standard SOC coordinates, + // then apply a single column permutation to the linear model. + struct rotated_soc_t { + i_t head0{}; + i_t head1{}; + std::vector tails{}; + }; + std::vector> cone_vars; std::vector cone_dims; + std::vector cone_is_rotated; + std::vector rotated_cones; std::vector is_cone_var(static_cast(n), 0); cone_vars.reserve(qcs.size()); cone_dims.reserve(qcs.size()); + cone_is_rotated.reserve(qcs.size()); + rotated_cones.reserve(qcs.size()); for (const auto& qc : qcs) { cuopt_expects(qc.constraint_row_type == 'L', @@ -148,13 +166,12 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( "mismatch for CSR Q", qc.constraint_row_name.c_str()); - const i_t q_n = static_cast(qc.quadratic_values.size()); - cuopt_expects(q_n >= 2, + const i_t q_nnz = static_cast(qc.quadratic_values.size()); + cuopt_expects(q_nnz >= 2, error_type_t::ValidationError, - "Quadratic constraint '%s' SOC must have at least 2 diagonal entries in Q (nnz " - "%d)", + "Quadratic constraint '%s' SOC must have at least 2 entries in Q (nnz %d)", qc.constraint_row_name.c_str(), - static_cast(q_n)); + static_cast(q_nnz)); cuopt_expects( qc.quadratic_offsets.size() == static_cast(n) + 1, @@ -166,30 +183,31 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( qc.quadratic_offsets.size(), static_cast(n)); cuopt_expects( - qc.quadratic_offsets[static_cast(n)] == q_n, + qc.quadratic_offsets[static_cast(n)] == q_nnz, error_type_t::ValidationError, - "Quadratic constraint '%s' Q last CSR offset %d must equal number of nonzeros (nnz) %d for " - "this diagonal Q", + "Quadratic constraint '%s' Q last CSR offset %d must equal number of nonzeros (nnz) %d", qc.constraint_row_name.c_str(), static_cast(qc.quadratic_offsets[static_cast(n)]), - static_cast(q_n)); + static_cast(q_nnz)); cuopt_expects(qc.quadratic_offsets[0] == 0, error_type_t::ValidationError, "Quadratic constraint '%s' Q CSR offsets[0] must be 0", qc.constraint_row_name.c_str()); - // Verify Q: n by n CSR, diagonal entries only, Lorentz pattern. - // Scan each row r: empty or one nnz on (r,r) with value -1 (head) or +1 (tail); - // tail order follows this scan; no requirement that diagonal indices be sorted. - i_t head = static_cast(-1); - i_t n_head_m = 0; - std::vector tail_row_vars{}; - tail_row_vars.reserve(static_cast(q_n - 1)); + // Verify Q as either: + // - standard SOC diagonal Lorentz form, or + // - canonical rotated SOC with one symmetric (-1,-1) off-diagonal pair. + i_t head = static_cast(-1); + i_t n_head_m = 0; + std::vector tail_vars{}; + std::vector> offdiag_pairs{}; + tail_vars.reserve(static_cast(q_nnz)); + offdiag_pairs.reserve(2); for (i_t r = 0; r < n; ++r) { const i_t p_beg = qc.quadratic_offsets[static_cast(r)]; const i_t p_end = qc.quadratic_offsets[static_cast(r + 1)]; - cuopt_expects(p_beg >= 0 && p_beg <= p_end && p_end <= q_n, + cuopt_expects(p_beg >= 0 && p_beg <= p_end && p_end <= q_nnz, error_type_t::ValidationError, "Quadratic constraint '%s' Q row %d has invalid CSR offsets [%d, %d)", qc.constraint_row_name.c_str(), @@ -201,8 +219,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( cuopt_expects(p_beg + 1 == p_end, error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: expected at most one stored entry on " - "the diagonal per " + "Quadratic constraint '%s' Q row %d: expected at most one stored entry per " "row (got end - beg = %d)", qc.constraint_row_name.c_str(), static_cast(r), @@ -210,58 +227,107 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( const i_t col = qc.quadratic_indices[static_cast(p_beg)]; const f_t v = qc.quadratic_values[static_cast(p_beg)]; - cuopt_expects( - col == r, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: only main diagonal (j,j) entries are allowed; got " - "column %d", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(col)); - const f_t neg_one_delta = v + f_t(1); const f_t pos_one_delta = v - f_t(1); const bool is_neg_one = (neg_one_delta >= -tol && neg_one_delta <= tol); const bool is_pos_one = (pos_one_delta >= -tol && pos_one_delta <= tol); - if (is_neg_one) { - ++n_head_m; - head = r; - } else if (is_pos_one) { - tail_row_vars.push_back(r); + + if (col == r) { + if (is_neg_one) { + ++n_head_m; + head = r; + } else if (is_pos_one) { + tail_vars.push_back(r); + } else { + cuopt_expects(false, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: diagonal for SOC must be -1 (head) " + "or +1 (tail); got %.17g", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(v)); + } } else { - cuopt_expects(false, + cuopt_expects(is_neg_one, error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: diagonal for SOC must be -1 (head) or " - "+1 (tail); got " - "%.17g", + "Quadratic constraint '%s' Q row %d: off-diagonal entries are only " + "supported for rotated SOC as value -1; got %.17g at column %d", qc.constraint_row_name.c_str(), static_cast(r), - static_cast(v)); + static_cast(v), + static_cast(col)); + offdiag_pairs.emplace_back(r, col); } } - cuopt_expects( - n_head_m == 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected exactly one diagonal with value -1 (cone head), " - "found %d", - qc.constraint_row_name.c_str(), - static_cast(n_head_m)); - cuopt_expects( - static_cast(tail_row_vars.size()) == q_n - 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected %d diagonals with value +1 (tails), found %zu", - qc.constraint_row_name.c_str(), - static_cast(q_n - 1), - tail_row_vars.size()); - cuopt_expects(head >= 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: internal error (head index invalid)", - qc.constraint_row_name.c_str()); - std::vector cone; - cone.reserve(static_cast(q_n)); - cone.push_back(head); - cone.insert(cone.end(), tail_row_vars.begin(), tail_row_vars.end()); + if (offdiag_pairs.empty()) { + cuopt_expects( + n_head_m == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected exactly one diagonal with value -1 (cone " + "head), found %d", + qc.constraint_row_name.c_str(), + static_cast(n_head_m)); + cuopt_expects( + static_cast(tail_vars.size()) == q_nnz - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonals with value +1 (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 1), + tail_vars.size()); + cuopt_expects(head >= 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: internal error (head index invalid)", + qc.constraint_row_name.c_str()); + + cone.reserve(static_cast(q_nnz)); + cone.push_back(head); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + } else { + cuopt_expects( + n_head_m == 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q cannot contain diagonal -1 entries; found %d", + qc.constraint_row_name.c_str(), + static_cast(n_head_m)); + cuopt_expects( + offdiag_pairs.size() == 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must contain exactly one symmetric off-diagonal " + "pair (-1,-1); found %zu off-diagonal entries", + qc.constraint_row_name.c_str(), + offdiag_pairs.size()); + + const i_t a = offdiag_pairs[0].first; + const i_t b = offdiag_pairs[0].second; + cuopt_expects(a != b, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal pair must use distinct " + "variables", + qc.constraint_row_name.c_str()); + cuopt_expects(offdiag_pairs[1].first == b && offdiag_pairs[1].second == a, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must have symmetric entries (a,b) " + "and (b,a) with value -1", + qc.constraint_row_name.c_str()); + cuopt_expects(static_cast(tail_vars.size()) == q_nnz - 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q: expected %d diagonal +1 entries " + "(tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 2), + tail_vars.size()); + cuopt_expects(q_nnz >= 3, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must have at least 1 tail entry", + qc.constraint_row_name.c_str()); + + cone.reserve(static_cast(q_nnz)); + cone.push_back(a); + cone.push_back(b); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + rotated_cones.push_back(rotated_soc_t{a, b, tail_vars}); + } for (const i_t var : cone) { cuopt_expects(!is_cone_var[static_cast(var)], error_type_t::ValidationError, @@ -270,14 +336,165 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( static_cast(var)); is_cone_var[static_cast(var)] = 1; } - cone_dims.push_back(q_n); + cone_dims.push_back(static_cast(cone.size())); cone_vars.push_back(std::move(cone)); + cone_is_rotated.push_back(offdiag_pairs.empty() ? static_cast(0) : static_cast(1)); + } + + i_t n_prob = static_cast(n); + + if (!rotated_cones.empty()) { + cuopt_expects(user_problem.Q_values.empty(), + error_type_t::ValidationError, + "Rotated SOC conversion is currently not supported when the objective has " + "quadratic terms"); + + const f_t inf = std::numeric_limits::infinity(); + const f_t inv_sqrt_2 = f_t(1) / std::sqrt(f_t(2)); + + for (const auto& rc : rotated_cones) { + cuopt_expects(user_problem.var_types[static_cast(rc.head0)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS && + user_problem.var_types[static_cast(rc.head1)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC head variables must be continuous"); + for (const i_t t : rc.tails) { + cuopt_expects(user_problem.var_types[static_cast(t)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC tail variables must be continuous"); + } + } + + auto is_inf = [](f_t x) { return std::isinf(static_cast(x)); }; + for (const auto& rc : rotated_cones) { + const f_t l0 = user_problem.lower[static_cast(rc.head0)]; + const f_t u0 = user_problem.upper[static_cast(rc.head0)]; + const f_t l1 = user_problem.lower[static_cast(rc.head1)]; + const f_t u1 = user_problem.upper[static_cast(rc.head1)]; + cuopt_expects(l0 >= -tol && l0 <= tol && is_inf(u0) && u0 > 0, + error_type_t::ValidationError, + "Rotated SOC head variable %d must have bounds [0, +inf) for conversion", + static_cast(rc.head0)); + cuopt_expects(l1 >= -tol && l1 <= tol && is_inf(u1) && u1 > 0, + error_type_t::ValidationError, + "Rotated SOC head variable %d must have bounds [0, +inf) for conversion", + static_cast(rc.head1)); + for (const i_t t : rc.tails) { + const f_t lt = user_problem.lower[static_cast(t)]; + const f_t ut = user_problem.upper[static_cast(t)]; + cuopt_expects(is_inf(lt) && lt < 0 && is_inf(ut) && ut > 0, + error_type_t::ValidationError, + "Rotated SOC tail variable %d must be free [-inf, +inf) for conversion", + static_cast(t)); + } + } + + // Lift each rotated cone into standard SOC coordinates with two slacks: + // s0 = (x_h0 + x_h1) / sqrt(2), s1 = (x_h0 - x_h1) / sqrt(2) + // so that 2*x_h0*x_h1 >= sum tails^2 becomes s0^2 >= s1^2 + sum tails^2. + // Only the rotated heads are replaced by slacks; tails remain unchanged. + i_t n_slack_total = 0; + for (size_t ci = 0; ci < cone_is_rotated.size(); ++ci) { + if (cone_is_rotated[ci]) { n_slack_total += 2; } + } + + const i_t n_old = n_prob; + n_prob = static_cast(n_old + n_slack_total); + + user_problem.objective.resize(static_cast(n_prob), f_t(0)); + user_problem.lower.resize(static_cast(n_prob), -inf); + user_problem.upper.resize(static_cast(n_prob), inf); + user_problem.var_types.resize(static_cast(n_prob), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_prob)); + for (i_t j = n_old; j < n_prob; ++j) { + user_problem.col_names[static_cast(j)] = + "_CUOPT_rsoc_slack_" + std::to_string(static_cast(j - n_old)); + } + } + + is_cone_var.resize(static_cast(n_prob), 0); + + const i_t m_old = csr_A.m; + user_problem.rhs.resize(static_cast(m_old + n_slack_total)); + user_problem.row_sense.resize(static_cast(m_old + n_slack_total)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_old + n_slack_total)); + for (i_t r = m_old; r < m_old + n_slack_total; ++r) { + user_problem.row_names[static_cast(r)] = + "_CUOPT_rsoc_lift_" + std::to_string(static_cast(r - m_old)); + } + } + + csr_A.n = n_prob; + + dual_simplex::sparse_vector_t eq_row; + size_t ri = 0; + i_t slack_base = n_old; + i_t row_idx = m_old; + + for (size_t ci = 0; ci < cone_vars.size(); ++ci) { + if (!cone_is_rotated[ci]) { continue; } + const auto& rc = rotated_cones[ri++]; + const i_t dim = cone_dims[ci]; + std::vector new_cone; + new_cone.reserve(static_cast(dim)); + new_cone.push_back(slack_base); + new_cone.push_back(slack_base + 1); + new_cone.insert(new_cone.end(), rc.tails.begin(), rc.tails.end()); + cone_vars[ci] = std::move(new_cone); + + is_cone_var[static_cast(rc.head0)] = 0; + is_cone_var[static_cast(rc.head1)] = 0; + is_cone_var[static_cast(slack_base)] = 1; + is_cone_var[static_cast(slack_base + 1)] = 1; + + // s_0 - inv_sqrt_2 * x_h0 - inv_sqrt_2 * x_h1 = 0 + eq_row.n = n_prob; + eq_row.i = {rc.head0, rc.head1, slack_base}; + eq_row.x = {-inv_sqrt_2, -inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + // s_1 - inv_sqrt_2 * x_h0 + inv_sqrt_2 * x_h1 = 0 + eq_row.i = {rc.head0, rc.head1, slack_base + 1}; + eq_row.x = {-inv_sqrt_2, inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + slack_base += 2; + } + + cuopt_expects(ri == rotated_cones.size(), + error_type_t::RuntimeError, + "Internal error: rotated SOC cone metadata mismatch"); + cuopt_expects(slack_base == n_prob, + error_type_t::RuntimeError, + "Internal error: slack variable count mismatch"); + cuopt_expects(row_idx == m_old + n_slack_total, + error_type_t::RuntimeError, + "Internal error: rotated SOC equality row count mismatch"); + cuopt_expects(csr_A.m == m_old + n_slack_total, + error_type_t::RuntimeError, + "Internal error: CSR row count after rotated SOC lift"); + + user_problem.num_rows = csr_A.m; + user_problem.num_cols = n_prob; } - std::vector old_to_new(static_cast(n), i_t{-1}); + std::vector old_to_new(static_cast(n_prob), i_t{-1}); std::vector new_to_old; - new_to_old.reserve(static_cast(n)); - for (i_t j = 0; j < n; ++j) { + new_to_old.reserve(static_cast(n_prob)); + for (i_t j = 0; j < n_prob; ++j) { if (is_cone_var[static_cast(j)]) { continue; } old_to_new[static_cast(j)] = static_cast(new_to_old.size()); new_to_old.push_back(j); @@ -289,7 +506,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( new_to_old.push_back(old_j); } } - cuopt_expects(static_cast(new_to_old.size()) == n, + cuopt_expects(static_cast(new_to_old.size()) == n_prob, error_type_t::RuntimeError, "Internal error while building SOC variable permutation"); @@ -298,11 +515,11 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( p < csr_A.row_start[static_cast(row + 1)]; ++p) { const i_t old_j = csr_A.j[static_cast(p)]; - cuopt_expects(old_j >= 0 && old_j < n, + cuopt_expects(old_j >= 0 && old_j < n_prob, error_type_t::ValidationError, "Linear constraint matrix column index %d is outside [0, %d)", static_cast(old_j), - static_cast(n)); + static_cast(n_prob)); csr_A.j[static_cast(p)] = old_to_new[static_cast(old_j)]; } } @@ -310,14 +527,14 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( auto permute_dense_by_old_to_new = [&](auto& values, const char* name) { if (values.empty()) { return; } using value_t = typename std::decay_t::value_type; - cuopt_expects(values.size() == static_cast(n), + cuopt_expects(values.size() == static_cast(n_prob), error_type_t::ValidationError, "%s length %zu does not match number of variables %d", name, values.size(), - static_cast(n)); + static_cast(n_prob)); std::vector permuted(values.size()); - for (i_t old_j = 0; old_j < n; ++old_j) { + for (i_t old_j = 0; old_j < n_prob; ++old_j) { permuted[static_cast(old_to_new[static_cast(old_j)])] = std::move(values[static_cast(old_j)]); } @@ -331,23 +548,24 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( permute_dense_by_old_to_new(user_problem.col_names, "column names"); if (!user_problem.Q_values.empty()) { + const i_t n_model = static_cast(n); cuopt_expects(user_problem.Q_indices.size() == user_problem.Q_values.size(), error_type_t::ValidationError, "Quadratic objective indices and values length mismatch"); - cuopt_expects(user_problem.Q_offsets.size() == static_cast(n) + 1, + cuopt_expects(user_problem.Q_offsets.size() == static_cast(n_model) + 1, error_type_t::ValidationError, "Quadratic objective CSR offsets length must be n+1 when SOC QCMATRIX " "conversion permutes variables"); cuopt_expects(user_problem.Q_offsets[0] == 0, error_type_t::ValidationError, "Quadratic objective CSR offsets[0] must be 0"); - cuopt_expects(user_problem.Q_offsets[static_cast(n)] == + cuopt_expects(user_problem.Q_offsets[static_cast(n_model)] == static_cast(user_problem.Q_values.size()), error_type_t::ValidationError, "Quadratic objective CSR last offset must equal number of nonzeros"); - std::vector q_offsets(static_cast(n) + 1, 0); - for (i_t old_row = 0; old_row < n; ++old_row) { + std::vector q_offsets(static_cast(n_prob) + 1, 0); + for (i_t old_row = 0; old_row < n_model; ++old_row) { const i_t p_beg = user_problem.Q_offsets[static_cast(old_row)]; const i_t p_end = user_problem.Q_offsets[static_cast(old_row + 1)]; cuopt_expects( @@ -358,24 +576,24 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( const i_t new_row = old_to_new[static_cast(old_row)]; q_offsets[static_cast(new_row + 1)] = p_end - p_beg; } - for (i_t row = 0; row < n; ++row) { + for (i_t row = 0; row < n_prob; ++row) { q_offsets[static_cast(row + 1)] += q_offsets[static_cast(row)]; } - std::vector q_indices(user_problem.Q_indices.size()); + std::vector q_indices(user_problem.Q_values.size()); std::vector q_values(user_problem.Q_values.size()); auto q_write = q_offsets; - for (i_t old_row = 0; old_row < n; ++old_row) { + for (i_t old_row = 0; old_row < n_model; ++old_row) { const i_t new_row = old_to_new[static_cast(old_row)]; for (i_t p = user_problem.Q_offsets[static_cast(old_row)]; p < user_problem.Q_offsets[static_cast(old_row + 1)]; ++p) { const i_t old_col = user_problem.Q_indices[static_cast(p)]; - cuopt_expects(old_col >= 0 && old_col < n, + cuopt_expects(old_col >= 0 && old_col < n_model, error_type_t::ValidationError, "Quadratic objective column index %d is outside [0, %d)", static_cast(old_col), - static_cast(n)); + static_cast(n_model)); const i_t dst = q_write[static_cast(new_row)]++; q_indices[static_cast(dst)] = old_to_new[static_cast(old_col)]; q_values[static_cast(dst)] = user_problem.Q_values[static_cast(p)]; From 1c1bde20b034d0c648ed815565d5516cff05cd8d Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 1 May 2026 06:41:22 -0700 Subject: [PATCH 29/68] Force SOCP to be unit initialization --- cpp/src/barrier/barrier.cu | 75 ++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index bdb571ec32..d552778e87 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -528,7 +528,7 @@ class iteration_data_t { } if (first_call) { - i_t new_nnz = 2 * nnzA + n + m + nnzQ + total_block_nnz; + i_t new_nnz = 2 * nnzA + n + m + nnzQ + total_block_nnz; // conservantive estimate of nnz csr_matrix_t augmented_CSR(n + m, n + m, new_nnz); std::vector augmented_diagonal_indices(n + m, -1); std::vector cone_csr_indices_host(total_block_nnz, -1); @@ -571,7 +571,7 @@ class iteration_data_t { for (i_t c = 0; c < q_k; c++) { i_t col = cone_col_start + c; f_t q_contrib = f_t(0); - f_t initial_val = (c == local_r) ? f_t(1) : f_t(0); + f_t initial_val = (c == local_r) ? f_t(-dual_perturb) : f_t(0); // diagonal entry of the cone block column if (qp < q_end && Q.i[qp] == col) { q_contrib = Q.x[qp]; @@ -2070,6 +2070,35 @@ int barrier_solver_t::initial_point(iteration_data_t& data) dense_vector_t dual_res(lp.num_cols); float64_t epsilon_adjust = 10.0; + + // Set the initial point for the SOC case separately + if (has_soc) { + const i_t cs = data.cone_start(); + // Nonnegative primal / dual (linear orthant): x_j, z_j <- 1 + for (i_t j = 0; j < cs; ++j) { + data.x[j] = 1.0; + data.z[j] = 1.0; + } + // Primal and dual SOC blocks: (t, x_bar) <- (1, 0, ..., 0) + i_t off = 0; + for (i_t k = 0; k < static_cast(lp.second_order_cone_dims.size()); ++k) { + i_t q_k = lp.second_order_cone_dims[k]; + data.x[cs + off] = 1.0; + data.z[cs + off] = 1.0; + for (i_t j = 1; j < q_k; ++j) { + data.x[cs + off + j] = 0.0; + data.z[cs + off + j] = 0.0; + } + off += q_k; + } + data.y.set_scalar(0.0); + if (data.n_upper_bounds > 0) { + data.w.set_scalar(1.0); + data.v.set_scalar(1.0); + } + return 0; + } + if (settings.barrier_dual_initial_point == -1 || settings.barrier_dual_initial_point == 0) { // Use the dual starting point suggested by the paper // On Implementing Mehrotra’s Predictor–Corrector Interior-Point Method for Linear Programming @@ -2179,27 +2208,27 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.w.ensure_positive(epsilon_adjust); data.x.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); - if (has_soc) { - const auto& dims = lp.second_order_cone_dims; - i_t cs = data.cone_start(); - i_t off = 0; - - for (i_t k = 0; k < static_cast(dims.size()); ++k) { - i_t q_k = dims[k]; - auto shift_into_interior = [&](auto& vec) { - f_t tail_sq = f_t(0); - for (i_t j = 1; j < q_k; ++j) { - f_t v = vec[cs + off + j]; - tail_sq += v * v; - } - f_t gap = std::sqrt(tail_sq) - vec[cs + off]; - if (gap >= f_t(0)) { vec[cs + off] += f_t(1) + gap; } - }; - shift_into_interior(data.x); - shift_into_interior(data.z); - off += q_k; - } - } + // if (has_soc) { + // const auto& dims = lp.second_order_cone_dims; + // i_t cs = data.cone_start(); + // i_t off = 0; + + // for (i_t k = 0; k < static_cast(dims.size()); ++k) { + // i_t q_k = dims[k]; + // auto shift_into_interior = [&](auto& vec) { + // f_t tail_sq = f_t(0); + // for (i_t j = 1; j < q_k; ++j) { + // f_t v = vec[cs + off + j]; + // tail_sq += v * v; + // } + // f_t gap = std::sqrt(tail_sq) - vec[cs + off]; + // if (gap >= f_t(0)) { vec[cs + off] += f_t(1) + gap; } + // }; + // shift_into_interior(data.x); + // shift_into_interior(data.z); + // off += q_k; + // } + // } #ifdef PRINT_INFO settings.log.printf("min v %e min z %e\n", data.v.minimum(), data.z.minimum()); From dbf9fa0e9db8e20b137a75c4469afe612ea09634 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 6 May 2026 10:41:25 -0700 Subject: [PATCH 30/68] Update scaling computation for SOCP, beneficial for sparse exploitation --- cpp/src/barrier/barrier.cu | 120 +++-- cpp/src/barrier/second_order_cone_kernels.cuh | 412 +++++++++++------- cpp/src/pdlp/solve.cu | 3 +- cpp/src/pdlp/translate.hpp | 26 +- .../unit_tests/second_order_cone_kernels.cu | 47 +- 5 files changed, 360 insertions(+), 248 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index d552778e87..200cf6c7c3 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -216,7 +216,7 @@ class iteration_data_t { d_complementarity_wv_rhs_(0, lp.handle_ptr->get_stream()), d_dual_rhs_(lp.num_cols, lp.handle_ptr->get_stream()), d_complementarity_target_(lp.num_cols, lp.handle_ptr->get_stream()), - d_cone_hinv2_dx_(0, lp.handle_ptr->get_stream()), + d_cone_hessian_dx_(0, lp.handle_ptr->get_stream()), d_Q_diag_(0, lp.handle_ptr->get_stream()), d_Qx_(Qin.m, lp.handle_ptr->get_stream()), restrict_u_(0), @@ -505,8 +505,8 @@ class iteration_data_t { i_t nnzA = A.col_start[n]; i_t nnzQ = Q.n > 0 ? Q.col_start[n] : 0; i_t factorization_size = n + m; - const f_t dual_perturb = 0.0; - const f_t primal_perturb = 1e-6; + const f_t dual_perturb = 1e-8; + const f_t primal_perturb = 1e-8; const bool has_soc = has_cones(); const i_t m_c = cone_entry_count(); @@ -514,21 +514,24 @@ class iteration_data_t { std::vector cone_offsets_host; std::vector cone_block_offsets_host; - if (has_soc) { - const i_t n_cones = cone_count(); - cone_offsets_host.resize(n_cones + 1); - cone_block_offsets_host.resize(n_cones + 1); - raft::copy(cone_offsets_host.data(), cones().cone_offsets.data(), n_cones + 1, stream_view_); - handle_ptr->sync_stream(); - for (i_t k = 0; k < n_cones; ++k) { - const auto q_k = cone_offsets_host[k + 1] - cone_offsets_host[k]; - cone_block_offsets_host[k + 1] = cone_block_offsets_host[k] + q_k * q_k; - } - total_block_nnz = static_cast(cone_block_offsets_host[n_cones]); - } if (first_call) { - i_t new_nnz = 2 * nnzA + n + m + nnzQ + total_block_nnz; // conservantive estimate of nnz + // Initialize the cone block offsets + if (has_soc) { + const i_t n_cones = cone_count(); + cone_offsets_host.resize(n_cones + 1); + cone_block_offsets_host.resize(n_cones + 1); + raft::copy( + cone_offsets_host.data(), cones().cone_offsets.data(), n_cones + 1, stream_view_); + handle_ptr->sync_stream(); + for (i_t k = 0; k < n_cones; ++k) { + const auto q_k = cone_offsets_host[k + 1] - cone_offsets_host[k]; + cone_block_offsets_host[k + 1] = cone_block_offsets_host[k] + q_k * q_k; + } + total_block_nnz = static_cast(cone_block_offsets_host[n_cones]); + } + + i_t new_nnz = 2 * nnzA + n + m + nnzQ + total_block_nnz; // conservative estimate of nnz csr_matrix_t augmented_CSR(n + m, n + m, new_nnz); std::vector augmented_diagonal_indices(n + m, -1); std::vector cone_csr_indices_host(total_block_nnz, -1); @@ -571,7 +574,8 @@ class iteration_data_t { for (i_t c = 0; c < q_k; c++) { i_t col = cone_col_start + c; f_t q_contrib = f_t(0); - f_t initial_val = (c == local_r) ? f_t(-dual_perturb) : f_t(0); // diagonal entry of the cone block column + f_t initial_val = (c == local_r) ? f_t(-dual_perturb) + : f_t(0); // diagonal entry of the cone block column if (qp < q_end && Q.i[qp] == col) { q_contrib = Q.x[qp]; @@ -686,6 +690,7 @@ class iteration_data_t { cuopt_assert(error.norm1() <= 1e-2, "|| Aug - Aug^T ||_1 > 1e-2"); #endif } else { + // Update the augmented matrix with the new diagonal values for primal and dual variables thrust::for_each_n(rmm::exec_policy(handle_ptr->get_stream()), thrust::make_counting_iterator(0), i_t(n), @@ -710,12 +715,15 @@ class iteration_data_t { }); RAFT_CHECK_CUDA(handle_ptr->get_stream()); + // Update the augmented matrix with the new diagonal blocks for soc constraints + // Overwrites the diagonal updates for primal variables of soc constraints if (has_soc) { - scatter_hinv2_into_augmented(cones(), - device_augmented.x, - d_cone_csr_indices_, - d_cone_Q_values_, - handle_ptr->get_stream()); + scatter_hessian_into_augmented(cones(), + device_augmented.x, + d_cone_csr_indices_, + d_cone_Q_values_, + handle_ptr->get_stream(), + dual_perturb); RAFT_CHECK_CUDA(handle_ptr->get_stream()); } } @@ -1618,21 +1626,19 @@ class iteration_data_t { if (has_soc) { const i_t m_c = cone_entry_count(); thrust::fill_n(rmm::exec_policy(stream_view_), d_r1.begin() + cone_start(), m_c, f_t(0)); + // r1 <- D * x_1 + H x_1 (cone Hessian block H = S^T S; accumulate_cone_hessian_matvec) + accumulate_cone_hessian_matvec(raft::device_span(d_x1.data() + cone_start(), m_c), + cones(), + raft::device_span(d_r1.data() + cone_start(), m_c), + stream_view_); + RAFT_CHECK_CUDA(stream_view_); } - // r1 <- Q x1 + D x1 + // r1 <- Q x1 + D x1 + H x1 (cone: same H as above) if (Q.n > 0) { // matrix_vector_multiply(Q, 1.0, x1, 1.0, r1); cusparse_Q_view_.spmv(1.0, d_x1, 1.0, d_r1); } - if (has_soc) { - const i_t m_c = cone_entry_count(); - accumulate_cone_hinv2_matvec(raft::device_span(d_x1.data() + cone_start(), m_c), - cones(), - raft::device_span(d_r1.data() + cone_start(), m_c), - stream_view_); - RAFT_CHECK_CUDA(stream_view_); - } // y1 <- - alpha * r1 + beta * y1 // y1.axpy(-alpha, r1, beta); @@ -1832,7 +1838,7 @@ class iteration_data_t { rmm::device_uvector d_complementarity_wv_rhs_; rmm::device_uvector d_dual_rhs_; rmm::device_uvector d_complementarity_target_; - rmm::device_uvector d_cone_hinv2_dx_; + rmm::device_uvector d_cone_hessian_dx_; rmm::device_uvector d_Q_diag_; rmm::device_uvector d_Qx_; @@ -2082,7 +2088,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) // Primal and dual SOC blocks: (t, x_bar) <- (1, 0, ..., 0) i_t off = 0; for (i_t k = 0; k < static_cast(lp.second_order_cone_dims.size()); ++k) { - i_t q_k = lp.second_order_cone_dims[k]; + i_t q_k = lp.second_order_cone_dims[k]; data.x[cs + off] = 1.0; data.z[cs + off] = 1.0; for (i_t j = 1; j < q_k; ++j) { @@ -2208,6 +2214,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.w.ensure_positive(epsilon_adjust); data.x.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); + // TODO: may be used for other heuristics // if (has_soc) { // const auto& dims = lp.second_order_cone_dims; // i_t cs = data.cone_start(); @@ -2466,6 +2473,7 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t(x.size()) - tail_start; if (tail_size > 0) { @@ -2577,11 +2585,14 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_x_.data() + cone_var_start, m_c); cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); - launch_nt_scaling(cones, stream_view_); + + if (!data.cone_combined_step_) { launch_nt_scaling(cones, stream_view_); } cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); fill_linear_target( @@ -2618,28 +2629,37 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t{}, stream_view_.value()); RAFT_CHECK_CUDA(stream_view_); + // TODO: need to double check this + // Set cone diagonal part to be 1 if (has_soc) { thrust::fill_n( rmm::exec_policy(stream_view_), data.d_diag_.begin() + cone_var_start, m_c, f_t(1)); } + // diag = z ./ x + E * (v ./ w) * E' if (data.n_upper_bounds > 0) { @@ -2769,6 +2789,7 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t d_augmented_rhs(lp.num_cols + lp.num_rows, stream_view_); raft::copy(d_augmented_rhs.data(), data.d_r1_.data(), lp.num_cols, stream_view_); raft::copy( @@ -3950,9 +3971,11 @@ lp_status_t barrier_solver_t::solve(f_t start_time, const i_t linear_xz_size = data.linear_xz_size(data.complementarity_xz_residual.size()); auto linear_xz_span = raft::host_span(data.complementarity_xz_residual.data(), linear_xz_size); + // compute complementarity residual norm for nonnegativity constraints f_t complementarity_residual_norm = std::max(vector_norm_inf(linear_xz_span, stream_view_), vector_norm_inf(data.complementarity_wv_residual, stream_view_)); + // compute complementarity residual norm for second-order cones if (has_soc) { f_t cone_complementarity_norm = f_t(0); i_t off = data.cone_start(); @@ -3994,6 +4017,11 @@ lp_status_t barrier_solver_t::solve(f_t start_time, f_t dual_objective = data.b.inner_product(data.y) - data.restrict_u_.inner_product(data.v) - quad_objective; + f_t duality_gap_abs = std::abs(primal_objective - dual_objective); + f_t duality_gap_rel = + duality_gap_abs / + std::max(f_t(1), std::min(std::abs(primal_objective), std::abs(dual_objective))); + i_t iter = 0; settings.log.printf("\n"); settings.log.printf( @@ -4010,10 +4038,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, relative_complementarity_residual, elapsed_time); - f_t duality_gap_abs = std::abs(primal_objective - dual_objective); - f_t duality_gap_rel = - duality_gap_abs / - std::max(f_t(1), std::min(std::abs(primal_objective), std::abs(dual_objective))); + // TODO: should polish termination condition bool converged = primal_residual_norm < settings.barrier_relative_feasibility_tol && dual_residual_norm < settings.barrier_relative_optimality_tol && (duality_gap_abs < settings.barrier_relative_complementarity_tol || @@ -4025,6 +4050,8 @@ lp_status_t barrier_solver_t::solve(f_t start_time, stream_view_); data.d_complementarity_wv_residual_.resize(data.complementarity_wv_residual.size(), stream_view_); + + // Resize the complementarity residual for x-z pairs (only nonnegativity constraints) data.d_complementarity_xz_rhs_.resize(linear_xz_rhs_size, stream_view_); data.d_complementarity_wv_rhs_.resize(data.complementarity_wv_rhs.size(), stream_view_); raft::copy(data.d_complementarity_xz_residual_.data(), @@ -4068,6 +4095,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, compute_affine_rhs(data); f_t max_affine_residual = 0.0; + // Update NT-scaling in gpu_compute_search_direction i_t status = gpu_compute_search_direction( data, data.dw_aff, data.dx_aff, data.dy_aff, data.dv_aff, data.dz_aff, max_affine_residual); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { diff --git a/cpp/src/barrier/second_order_cone_kernels.cuh b/cpp/src/barrier/second_order_cone_kernels.cuh index 42568fcc9b..045f68430f 100644 --- a/cpp/src/barrier/second_order_cone_kernels.cuh +++ b/cpp/src/barrier/second_order_cone_kernels.cuh @@ -41,10 +41,10 @@ // x_soc : cone primal block // z_soc : cone dual block // W, W^{-1} : Nesterov-Todd scaling matrix and inverse. W is symmetric for -// SOC, so W^{-T} = W^{-1} -// H : W^{-1} W^{-T} = W^{-2}, the cone KKT block added to the +// SOC, so W^{T} = W +// H : S^{T} S = S^{2}, the cone KKT block added to the // primal-reduced system -// eta : sqrt(x_J / z_J), where x_J = sqrt(det_J(x_soc)) +// eta : sqrt(z_J / x_J), where x_J = sqrt(det_J(x_soc)) // w : NT scaling direction with det_J(w) = 1 and // w[head] = sqrt(1 + ||w_tail||^2) // @@ -130,12 +130,12 @@ HD f_t cone_step_length_from_scalars( const auto disc = b * b - a * c; auto alpha = alpha_max; - if (du0 < 0) { alpha = cuda::std::min(alpha, -u0 / du0); } + if (u0 >= 0 && du0 < 0) { alpha = cuda::std::min(alpha, -u0 / du0); } if ((a > 0 && b > 0) || disc < 0) { return alpha; } if (a == 0) { - if (b < 0) { alpha = cuda::std::min(alpha, c / (-2 * b)); } + return alpha; } else if (c == 0) { alpha = a >= 0 ? alpha : 0; } else { @@ -198,8 +198,9 @@ struct cone_data_t { raft::device_span z; // [n_cone_entries], SOC dual block // Persistent Nesterov-Todd scaling state, recomputed from x/z each iteration. - rmm::device_uvector eta; // [n_cones], sqrt(|x|_J / |z|_J) - rmm::device_uvector w; // [n_cone_entries], unit-J-norm NT direction + rmm::device_uvector eta; // [n_cones], sqrt(|z|_J / |x|_J) + rmm::device_uvector w; // [n_cone_entries], unit-J-norm NT direction + rmm::device_uvector lambda; // [n_cone_entries], NT point lambda = W^{-T} z cone_scratch_t scratch; @@ -218,6 +219,7 @@ struct cone_data_t { z(z_in), eta(n_cones, stream), w(n_cone_entries, stream), + lambda(n_cone_entries, stream), scratch(n_cones, n_cone_entries, stream) { raft::copy(cone_dimensions.data(), cone_dimensions_host.data(), n_cones, stream); @@ -263,37 +265,42 @@ __global__ void __launch_bounds__(soc_block_size) x_scale[cone] = sqrt(x_det); z_scale[cone] = sqrt(z_det); - eta[cone] = sqrt(x_scale[cone] / z_scale[cone]); + eta[cone] = sqrt(z_scale[cone] / x_scale[cone]); } template __global__ void __launch_bounds__(soc_block_size) - nt_finalize_w_scale_kernel(raft::device_span normalized_dot, i_t n_cones) + nt_finalize_w_scale_kernel(raft::device_span w, + raft::device_span tail_sq, + raft::device_span w_scale, + raft::device_span cone_offsets, + i_t n_cones) { const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); if (cone >= n_cones) { return; } - normalized_dot[cone] = sqrt(2 + 2 * normalized_dot[cone]); + const auto cone_off = cone_offsets[cone]; + const auto head = w[cone_off]; + const auto tail_norm = sqrt(tail_sq[cone]); + const auto residual = (head - tail_norm) * (head + tail_norm); + w_scale[cone] = sqrt(residual); } /** - * Write normalized w_tail directly: - * - * w_tail = (x_tail / x_scale - z_tail / z_scale) / w_scale. + * Write unnormalized w: * - * The head is zeroed temporarily and overwritten after reducing - * ||w_tail||^2. + * w_0 = z_0 / z_scale + x_0 / x_scale + * w_tail = z_tail / z_scale - x_tail / x_scale. */ template __global__ void __launch_bounds__(soc_block_size) - nt_write_w_tail_kernel(raft::device_span x, - raft::device_span z, - raft::device_span x_scale, - raft::device_span z_scale, - raft::device_span w_scale, - raft::device_span w, - raft::device_span cone_offsets, - raft::device_span element_cone_ids) + nt_write_w_kernel(raft::device_span x, + raft::device_span z, + raft::device_span x_scale, + raft::device_span z_scale, + raft::device_span w, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) { const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; if (idx >= w.size()) { return; } @@ -301,11 +308,24 @@ __global__ void __launch_bounds__(soc_block_size) const auto cone = element_cone_ids[idx]; const auto cone_off = cone_offsets[cone]; if (idx == cone_off) { - w[idx] = 0; + w[idx] = z[idx] / z_scale[cone] + x[idx] / x_scale[cone]; return; } - w[idx] = (x[idx] / x_scale[cone] - z[idx] / z_scale[cone]) / w_scale[cone]; + w[idx] = z[idx] / z_scale[cone] - x[idx] / x_scale[cone]; +} + +template +__global__ void __launch_bounds__(soc_block_size) + nt_normalize_w_kernel(raft::device_span w, + raft::device_span w_scale, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= w.size()) { return; } + + const auto cone = element_cone_ids[idx]; + w[idx] /= w_scale[cone]; } template @@ -321,21 +341,62 @@ __global__ void __launch_bounds__(soc_block_size) w[cone_offsets[cone]] = sqrt(1 + normalized_tail_sq[cone]); } +template +__global__ void __launch_bounds__(soc_block_size) + nt_write_lambda_kernel(raft::device_span x, + raft::device_span z, + raft::device_span x_scale, + raft::device_span z_scale, + raft::device_span w_scale, + raft::device_span lambda, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= lambda.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto x_scale_cone = x_scale[cone]; + const auto z_scale_cone = z_scale[cone]; + const auto gamma = static_cast(0.5) * w_scale[cone]; + const auto head_scale = sqrt(x_scale_cone * z_scale_cone); + + if (local_idx == 0) { + lambda[idx] = gamma * head_scale; + return; + } + + const auto x_head = x[cone_off]; + const auto z_head = z[cone_off]; + const auto denom = z_head / z_scale_cone + x_head / x_scale_cone + static_cast(2) * gamma; + const auto coeff_z = (gamma + x_head / x_scale_cone) / z_scale_cone; + const auto coeff_x = (gamma + z_head / z_scale_cone) / x_scale_cone; + + const auto lambda_tail = (coeff_z * z[idx] + coeff_x * x[idx]) / denom; + lambda[idx] = lambda_tail * head_scale; +} + /** * Build Nesterov-Todd scaling for packed SOC blocks. * * Given interior cone primal/dual blocks x and z: * + * det_J(x) = x_0^2 - ||x_tail||^2 + * det_J(z) = z_0^2 - ||z_tail||^2 * x_scale = sqrt(det_J(x)), z_scale = sqrt(det_J(z)) - * eta = sqrt(x_scale / z_scale) - * w_scale = sqrt(2 + 2 * dot(x / x_scale, z / z_scale)) - * w_tail = (x_tail / x_scale - z_tail / z_scale) / w_scale + * eta = sqrt(z_scale / x_scale) + * w_tmp_0 = z_0 / z_scale + x_0 / x_scale + * w_tmp_tail = z_tail / z_scale - x_tail / x_scale + * w_scale = sqrt(det_J(w_tmp)) + * w = w_tmp / w_scale * w_0 = sqrt(1 + ||w_tail||^2) to re-impose det_J(w) = 1 * * Scratch slots: * 0: ||x_tail||^2 -> x_scale * 1: ||z_tail||^2 -> z_scale - * 2: dot(x / x_scale, z / z_scale) -> w_scale -> ||w_tail||^2 */ template void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) @@ -373,25 +434,41 @@ void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view strea RAFT_CUDA_TRY(cudaPeekAtLastError()); const auto element_grid_dim = raft::ceildiv(cones.n_cone_entries, soc_block_size); - auto normalized_dot_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [span_x, span_z, x_scale, z_scale, element_cone_ids] HD(std::size_t idx) -> f_t { - const auto cone = element_cone_ids[idx]; - return span_x[idx] * span_z[idx] / (x_scale[cone] * z_scale[cone]); - }); - cones.segmented_sum(normalized_dot_terms, w_scale, stream); - nt_finalize_w_scale_kernel - <<>>(w_scale, cones.n_cones); + auto w = cuopt::make_span(cones.w); + nt_write_w_kernel<<>>( + cones.x, cones.z, x_scale, z_scale, w, cone_offsets, element_cone_ids); RAFT_CUDA_TRY(cudaPeekAtLastError()); - auto w = cuopt::make_span(cones.w); - nt_write_w_tail_kernel<<>>( - cones.x, cones.z, x_scale, z_scale, w_scale, w, cone_offsets, element_cone_ids); + auto unnormalized_tail_sq_terms = + thrust::make_transform_iterator(thrust::make_counting_iterator(0), + [cone_offsets, element_cone_ids, w] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * w[idx]; + }); + cones.segmented_sum(unnormalized_tail_sq_terms, w_scale, stream); + + nt_finalize_w_scale_kernel<<>>( + w, w_scale, w_scale, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + nt_normalize_w_kernel + <<>>(w, w_scale, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + // Persist lambda while w_scale still stores sqrt(det_J(w_tmp)). + nt_write_lambda_kernel + <<>>(cones.x, + cones.z, + x_scale, + z_scale, + w_scale, + cuopt::make_span(cones.lambda), + cone_offsets, + element_cone_ids); RAFT_CUDA_TRY(cudaPeekAtLastError()); - // Reduce ||w_tail||^2 per cone. The head entries are zero, so the same - // flat iterator can feed the segmented reduction. + // w_scale is overwritten from here auto normalized_tail_terms = thrust::make_transform_iterator(thrust::make_counting_iterator(0), [cone_offsets, element_cone_ids, w] HD(std::size_t idx) -> f_t { @@ -407,13 +484,13 @@ void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view strea template __global__ void __launch_bounds__(soc_block_size) - apply_w_write_kernel(raft::device_span v, - raft::device_span out, - raft::device_span w, - raft::device_span eta, - raft::device_span tail_dot, - raft::device_span cone_offsets, - raft::device_span element_cone_ids) + apply_w_inv_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) { const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; if (idx >= out.size()) { return; } @@ -422,29 +499,29 @@ __global__ void __launch_bounds__(soc_block_size) const auto cone_off = cone_offsets[cone]; const auto local_idx = idx - cone_off; - const auto w0 = w[cone_off]; - const auto zeta = tail_dot[cone]; - const auto inv_1pw0 = 1 / (1 + w0); - const auto v0 = v[cone_off]; + const auto w0 = w[cone_off]; + const auto zeta = tail_dot[cone]; + const auto v0 = v[cone_off]; + const auto inv_eta = f_t(1) / eta[cone]; if (local_idx == 0) { - out[idx] = eta[cone] * (w0 * v0 + zeta); + out[idx] = inv_eta * (w0 * v0 - zeta); return; } - const auto coeff = v0 + zeta * inv_1pw0; - out[idx] = eta[cone] * (v[idx] + coeff * w[idx]); + const auto coeff = -v0 + zeta / (f_t(1) + w0); + out[idx] = inv_eta * (v[idx] + coeff * w[idx]); } template __global__ void __launch_bounds__(soc_block_size) - apply_w_inv_write_kernel(raft::device_span v, - raft::device_span out, - raft::device_span w, - raft::device_span eta, - raft::device_span tail_dot, - raft::device_span cone_offsets, - raft::device_span element_cone_ids) + apply_w_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) { const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; if (idx >= out.size()) { return; } @@ -455,31 +532,30 @@ __global__ void __launch_bounds__(soc_block_size) const auto w0 = w[cone_off]; const auto zeta = tail_dot[cone]; - const auto inv_1pw0 = 1 / (1 + w0); const auto v0 = v[cone_off]; - const auto inv_eta = 1 / eta[cone]; + const auto cone_eta = eta[cone]; if (local_idx == 0) { - out[idx] = inv_eta * (w0 * v0 - zeta); + out[idx] = cone_eta * (w0 * v0 + zeta); return; } - const auto coeff = -v0 + zeta * inv_1pw0; - out[idx] = inv_eta * (v[idx] + coeff * w[idx]); + const auto coeff = v0 + zeta / (f_t(1) + w0); + out[idx] = cone_eta * (v[idx] + coeff * w[idx]); } template __global__ void __launch_bounds__(soc_block_size) - apply_hinv2_write_kernel(raft::device_span v, - raft::device_span out, - raft::device_span w, - raft::device_span eta, - raft::device_span tail_dot, - raft::device_span cone_offsets, - raft::device_span element_cone_ids, - raft::device_span bias, - f_t output_scale, - f_t bias_scale) + apply_hessian_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span wv_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + raft::device_span bias, + f_t output_scale, + f_t bias_scale) { const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; if (idx >= out.size()) { return; } @@ -488,12 +564,11 @@ __global__ void __launch_bounds__(soc_block_size) const auto cone_off = cone_offsets[cone]; const auto local_idx = idx - cone_off; - const auto inv_eta_sq = 1 / (eta[cone] * eta[cone]); - const auto rho = w[cone_off] * v[cone_off] - tail_dot[cone]; - const auto coeff = 2 * rho * inv_eta_sq; - const int sign = (local_idx == 0) * 2 - 1; - const auto value = coeff * w[idx] - inv_eta_sq * v[idx]; - const auto h_value = output_scale * value * sign; + const auto eta_sq = (eta[cone] * eta[cone]); + const auto coeff = 2 * wv_dot[cone] * eta_sq; + const int sign = (local_idx == 0) * 2 - 1; + const auto value = coeff * w[idx] - eta_sq * v[idx] * sign; + const auto h_value = output_scale * value; out[idx] = bias.empty() ? h_value : bias_scale * bias[idx] + h_value; } @@ -514,9 +589,9 @@ __global__ void __launch_bounds__(soc_block_size) /** * Build the Mehrotra corrector shift: * - * d = (W^{-1} dx_aff) o (W dz_aff) - sigma_mu e. + * d = (W dx_aff) o (W^{-T} dz_aff) - sigma_mu e. * - * On entry, `scaled_dx` is W^{-1} dx_aff and `scaled_dz` is W dz_aff. The + * On entry, `scaled_dx` is W dx_aff and `scaled_dz` is W^{-T} dz_aff. The * cone head uses the full dot product, and tail entries use the SOC Jordan * product: * @@ -578,6 +653,7 @@ __global__ void __launch_bounds__(soc_block_size) const auto lambda_tail_norm = sqrt(lambda_tail_sq[cone]); const auto det_lambda = (lambda0 - lambda_tail_norm) * (lambda0 + lambda_tail_norm); + // repurpose the heads in lambda_tail_dot, lambda_tail_sq for each cone p0[cone] = (lambda0 * shift[cone_off] - lambda_tail_dot[cone]) / det_lambda; inv_lambda0[cone] = 1 / lambda0; } @@ -608,18 +684,18 @@ __global__ void __launch_bounds__(soc_block_size) } /** - * Apply the Nesterov-Todd scaling matrix: out = W v. + * Apply the Nesterov-Todd scaling matrix: out = W^{-1} v. * * For each cone: * zeta = - * (Wv)_0 = eta * (w_0 v_0 + zeta) - * (Wv)_tail = eta * (v_tail + (v_0 + zeta / (1 + w_0)) w_tail) + * (W^{-1}v)_0 = inv_eta * (w_0 v_0 - zeta) + * (W^{-1}v)_tail = inv_eta * (v_tail + (-v_0 + zeta / (1 + w_0)) w_tail) */ template -void apply_w(raft::device_span v, - raft::device_span out, - cone_data_t& cones, - rmm::cuda_stream_view stream) +void apply_w_inv(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream) { auto w = cuopt::make_span(cones.w); auto eta = cuopt::make_span(cones.eta); @@ -636,26 +712,26 @@ void apply_w(raft::device_span v, cones.segmented_sum(tail_terms, tail_dot, stream); const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); - apply_w_write_kernel<<>>( + apply_w_inv_write_kernel<<>>( v, out, w, eta, tail_dot, cone_offsets, element_cone_ids); RAFT_CUDA_TRY(cudaPeekAtLastError()); } /** - * Apply the inverse Nesterov-Todd scaling matrix: - * out = W^{-1} v. + * Apply the multiplication of Nesterov-Todd scaling matrix: + * out = W v. * * For each cone, * zeta = - * (W^{-1}v)_0 = eta^{-1} * (w_0 v_0 - zeta) - * (W^{-1}v)_tail = - * eta^{-1} * (v_tail + (-v_0 + zeta / (1 + w_0)) w_tail) + * (W * v)_0 = eta * (w_0 v_0 + zeta) + * (W * v)_tail = + * eta * (v_tail + (v_0 + zeta / (1 + w_0)) w_tail) */ template -void apply_w_inv(raft::device_span v, - raft::device_span out, - cone_data_t& cones, - rmm::cuda_stream_view stream) +void apply_w(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream) { auto w = cuopt::make_span(cones.w); auto eta = cuopt::make_span(cones.eta); @@ -672,44 +748,41 @@ void apply_w_inv(raft::device_span v, cones.segmented_sum(tail_terms, tail_dot, stream); const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); - apply_w_inv_write_kernel<<>>( + apply_w_write_kernel<<>>( v, out, w, eta, tail_dot, cone_offsets, element_cone_ids); RAFT_CUDA_TRY(cudaPeekAtLastError()); } /** - * Apply the cone KKT block H = W^{-1} W^{-T} = W^{-2}. + * Apply the cone KKT block H = S^T S = S^2. * - * With zeta = and rho = w_0 v_0 - zeta: - * (Hv)_0 = eta^{-2} (2 w_0 rho - v_0) - * (Hv)_tail = eta^{-2} (v_tail - 2 w_tail rho) + * With rho = : + * (Hv)_0 = eta^{2} (2 w_0 rho - v_0) + * (Hv)_tail = eta^{2} (2 w_tail rho + v_tail) */ template -void apply_hinv2(raft::device_span v, - raft::device_span out, - cone_data_t& cones, - rmm::cuda_stream_view stream, - f_t output_scale = 1, - raft::device_span bias = {}, - f_t bias_scale = 0) +void apply_hessian(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream, + f_t output_scale = 1, + raft::device_span bias = {}, + f_t bias_scale = 0) { auto w = cuopt::make_span(cones.w); auto eta = cuopt::make_span(cones.eta); auto cone_offsets = cuopt::make_span(cones.cone_offsets); auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); - auto tail_dot = cones.scratch.template get_slot<0>(); + auto wv_dot = cones.scratch.template get_slot<0>(); - auto tail_terms = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [v, w, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { - const auto cone = element_cone_ids[idx]; - return idx == cone_offsets[cone] ? 0 : w[idx] * v[idx]; - }); - cones.segmented_sum(tail_terms, tail_dot, stream); + auto wv_terms = + thrust::make_transform_iterator(thrust::make_counting_iterator(0), + [v, w] HD(std::size_t idx) -> f_t { return w[idx] * v[idx]; }); + cones.segmented_sum(wv_terms, wv_dot, stream); const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); - apply_hinv2_write_kernel<<>>( - v, out, w, eta, tail_dot, cone_offsets, element_cone_ids, bias, output_scale, bias_scale); + apply_hessian_write_kernel<<>>( + v, out, w, eta, wv_dot, cone_offsets, element_cone_ids, bias, output_scale, bias_scale); RAFT_CUDA_TRY(cudaPeekAtLastError()); } @@ -717,7 +790,7 @@ void apply_hinv2(raft::device_span v, * Recover the SOC dual direction after the reduced KKT solve. * * The reduced solve gives `dx`; the cone equation supplies the target RHS. - * This function applies the cone block H = W^{-2} and writes: + * This function applies the cone block H = S^2 and writes: * dz = cone_target - H dx. */ template @@ -727,35 +800,36 @@ void recover_cone_dz_from_target(raft::device_span dx, raft::device_span dz, rmm::cuda_stream_view stream) { - apply_hinv2(dx, dz, cones, stream, -1, cone_target, 1); + apply_hessian(dx, dz, cones, stream, -1, cone_target, 1); } /** * Accumulate the SOC cone-block matvec into an existing output vector. * * Used by matrix-free products with the primal-reduced KKT block: - * out += H x, where H = W^{-2}. + * out += H x, where H = S^2. */ template -void accumulate_cone_hinv2_matvec(raft::device_span x, - cone_data_t& cones, - raft::device_span out, - rmm::cuda_stream_view stream) +void accumulate_cone_hessian_matvec(raft::device_span x, + cone_data_t& cones, + raft::device_span out, + rmm::cuda_stream_view stream) { auto out_input = raft::device_span(out.data(), out.size()); - apply_hinv2(x, out, cones, stream, 1, out_input, 1); + apply_hessian(x, out, cones, stream, 1, out_input, 1); } template __global__ void __launch_bounds__(soc_block_size) - scatter_hinv2_into_augmented_kernel(raft::device_span augmented_x, - raft::device_span csr_indices, - raft::device_span q_values, - raft::device_span w, - raft::device_span eta, - raft::device_span cone_offsets, - raft::device_span block_offsets, - i_t n_cones) + scatter_hessian_into_augmented_kernel(raft::device_span augmented_x, + raft::device_span csr_indices, + raft::device_span q_values, + raft::device_span w, + raft::device_span eta, + raft::device_span cone_offsets, + raft::device_span block_offsets, + i_t n_cones, + f_t dual_perturb_value) { const auto e = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; if (e >= csr_indices.size()) { return; } @@ -779,28 +853,32 @@ __global__ void __launch_bounds__(soc_block_size) const auto r = local / q; const auto c = local % q; - const auto inv_eta_sq = 1 / (eta[cone] * eta[cone]); + const auto eta_sq = eta[cone] * eta[cone]; const auto w0 = w[off]; - const auto u_r = (r == 0) ? w0 : -w[off + r]; - const auto u_c = (c == 0) ? w0 : -w[off + c]; - auto val = f_t{2} * u_r * inv_eta_sq * u_c; - const auto diag_correction = (r == 0) ? -inv_eta_sq : inv_eta_sq; + const auto u_r = (r == 0) ? w0 : w[off + r]; + const auto u_c = (c == 0) ? w0 : w[off + c]; + auto val = f_t{2} * u_r * eta_sq * u_c; + const auto diag_correction = (r == 0) ? -eta_sq : eta_sq; if (r == c) { val += diag_correction; } augmented_x[csr_indices[e]] = -val - q_values[e]; } template -void scatter_hinv2_into_augmented(const cone_data_t& cones, - rmm::device_uvector& augmented_x, - const rmm::device_uvector& csr_indices, - const rmm::device_uvector& q_values, - rmm::cuda_stream_view stream) +void scatter_hessian_into_augmented(const cone_data_t& cones, + rmm::device_uvector& augmented_x, + const rmm::device_uvector& csr_indices, + const rmm::device_uvector& q_values, + rmm::cuda_stream_view stream, + f_t dual_perturb_value) { const auto count = csr_indices.size(); if (count == 0) { return; } cuopt_assert(count == q_values.size(), "cone CSR index and Q-value arrays must match"); + // TODO: This offset calculation should be done in the barrier layer, + // because it is already done in the barrier layer for the augmented system, see + // cone_block_offsets_host. rmm::device_uvector block_offsets(cones.n_cones + 1, stream); block_offsets.set_element_to_zero_async(0, stream); @@ -810,8 +888,9 @@ void scatter_hinv2_into_augmented(const cone_data_t& cones, thrust::inclusive_scan( rmm::exec_policy(stream), block_sizes, block_sizes + cones.n_cones, block_offsets.begin() + 1); + // TODO: use dual_perturb_value for regularization const auto grid = raft::ceildiv(count, soc_block_size); - scatter_hinv2_into_augmented_kernel + scatter_hessian_into_augmented_kernel <<>>(cuopt::make_span(augmented_x), cuopt::make_span(csr_indices), cuopt::make_span(q_values), @@ -819,7 +898,8 @@ void scatter_hinv2_into_augmented(const cone_data_t& cones, cuopt::make_span(cones.eta), cuopt::make_span(cones.cone_offsets), cuopt::make_span(block_offsets), - cones.n_cones); + cones.n_cones, + dual_perturb_value); RAFT_CUDA_TRY(cudaPeekAtLastError()); } @@ -913,16 +993,15 @@ std::pair compute_cone_step_length(cone_data_t& cones, * * Mehrotra's corrector uses affine cone directions to form * - * d = (W^{-1} dx_aff) o (W dz_aff) - sigma_mu e, + * d = (W dx_aff) o (W^{-T} dz_aff) - sigma_mu e, * * where `o` is the SOC Jordan product and `e = (1, 0, ..., 0)` per cone. * The reduced KKT solve needs the cone target * - * q = -W^{-1} p, where p = lambda \ d and lambda = W z. + * q = -W * p, where p = lambda \ d and lambda = W^{-T} z. * - * On return, `out` holds `q`. Internally, `out` is reused for `W dz_aff` and - * then `d`; `scratch.temp_cone` is reused for `W^{-1} dx_aff`, then `lambda`, - * then `-p`. + * On return, `out` holds `q`. Internally, `out` is reused for `W^{-T} dz_aff` and + * then `d`; `scratch.temp_cone` is reused for `W dx_aff`, then `-p`. */ template void compute_combined_cone_rhs_term(raft::device_span dx_aff, @@ -942,15 +1021,15 @@ void compute_combined_cone_rhs_term(raft::device_span dx_aff, auto slot_1 = cones.scratch.template get_slot<1>(); auto slot_2 = cones.scratch.template get_slot<2>(); - apply_w_inv(dx_aff, scratch_cone, cones, stream); - apply_w(dz_aff, out, cones, stream); + apply_w(dx_aff, scratch_cone, cones, stream); + apply_w_inv(dz_aff, out, cones, stream); auto full_product_terms = thrust::make_transform_iterator( thrust::make_zip_iterator(scaled_dx.begin(), scaled_dz.begin()), thrust::make_zip_function([] HD(f_t dx, f_t dz) -> f_t { return dx * dz; })); cones.segmented_sum(full_product_terms, slot_0, stream); - // `out` currently aliases W dz_aff and is about to be overwritten with d. + // `out` currently aliases W^{-T} dz_aff and is about to be overwritten with d. // Stage both head vectors first because every tail entry needs them. const auto cone_grid_dim = raft::ceildiv(static_cast(cones.n_cones), soc_block_size); @@ -966,12 +1045,10 @@ void compute_combined_cone_rhs_term(raft::device_span dx_aff, out, scaled_dx, scaled_dz, slot_0, slot_1, slot_2, cone_offsets, element_cone_ids, sigma_mu); RAFT_CUDA_TRY(cudaPeekAtLastError()); - // Form lambda = W z in scratch_cone. At this point W^{-1} dx_aff is dead. - apply_w(cones.z, scratch_cone, cones, stream); - auto shift = raft::device_span(out.data(), out.size()); - auto nt_point = raft::device_span(scratch_cone.data(), scratch_cone.size()); + auto nt_point = raft::device_span(cones.lambda.data(), cones.lambda.size()); + // compute W *(-(\lambda inv_circ shift)) auto lambda_tail_dot_terms = thrust::make_transform_iterator( thrust::make_counting_iterator(0), [shift, nt_point, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { @@ -993,12 +1070,13 @@ void compute_combined_cone_rhs_term(raft::device_span dx_aff, shift, nt_point, slot_0, slot_1, slot_0, slot_1, cone_offsets, cones.n_cones); RAFT_CUDA_TRY(cudaPeekAtLastError()); + // Note that we implicitly multiply by -1 here since we are writing -p. jordan_divide_by_lambda_write_kernel <<>>( shift, nt_point, slot_0, slot_1, cone_offsets, element_cone_ids, scratch_cone); RAFT_CUDA_TRY(cudaPeekAtLastError()); - apply_w_inv(scratch_cone, out, cones, stream); + apply_w(scratch_cone, out, cones, stream); } } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 585bfe4476..3a30645b9e 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1383,7 +1383,8 @@ optimization_problem_solution_t solve_lp( CUOPT_LOG_INFO("Problem has a quadratic objective. Using Barrier."); } if (op_problem.has_quadratic_constraints()) { - CUOPT_LOG_INFO("Problem has %d quadratic constraints. Using Barrier with SOC conversion.", static_cast(op_problem.get_quadratic_constraints().size())); + CUOPT_LOG_INFO("Problem has %d quadratic constraints. Using Barrier with SOC conversion.", + static_cast(op_problem.get_quadratic_constraints().size())); } settings.method = method_t::Barrier; settings.presolver = presolver_t::None; diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 02fb7ad641..a5807f102c 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -197,8 +197,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( // Verify Q as either: // - standard SOC diagonal Lorentz form, or // - canonical rotated SOC with one symmetric (-1,-1) off-diagonal pair. - i_t head = static_cast(-1); - i_t n_head_m = 0; + i_t head = static_cast(-1); + i_t n_head_m = 0; std::vector tail_vars{}; std::vector> offdiag_pairs{}; tail_vars.reserve(static_cast(q_nnz)); @@ -225,8 +225,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( static_cast(r), static_cast(p_end - p_beg)); - const i_t col = qc.quadratic_indices[static_cast(p_beg)]; - const f_t v = qc.quadratic_values[static_cast(p_beg)]; + const i_t col = qc.quadratic_indices[static_cast(p_beg)]; + const f_t v = qc.quadratic_values[static_cast(p_beg)]; const f_t neg_one_delta = v + f_t(1); const f_t pos_one_delta = v - f_t(1); const bool is_neg_one = (neg_one_delta >= -tol && neg_one_delta <= tol); @@ -338,7 +338,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( } cone_dims.push_back(static_cast(cone.size())); cone_vars.push_back(std::move(cone)); - cone_is_rotated.push_back(offdiag_pairs.empty() ? static_cast(0) : static_cast(1)); + cone_is_rotated.push_back(offdiag_pairs.empty() ? static_cast(0) + : static_cast(1)); } i_t n_prob = static_cast(n); @@ -354,7 +355,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( for (const auto& rc : rotated_cones) { cuopt_expects(user_problem.var_types[static_cast(rc.head0)] == - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS && + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS && user_problem.var_types[static_cast(rc.head1)] == cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, error_type_t::ValidationError, @@ -406,8 +407,9 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( user_problem.objective.resize(static_cast(n_prob), f_t(0)); user_problem.lower.resize(static_cast(n_prob), -inf); user_problem.upper.resize(static_cast(n_prob), inf); - user_problem.var_types.resize(static_cast(n_prob), - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + user_problem.var_types.resize( + static_cast(n_prob), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); if (!user_problem.col_names.empty()) { user_problem.col_names.resize(static_cast(n_prob)); for (i_t j = n_old; j < n_prob; ++j) { @@ -432,14 +434,14 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( csr_A.n = n_prob; dual_simplex::sparse_vector_t eq_row; - size_t ri = 0; + size_t ri = 0; i_t slack_base = n_old; i_t row_idx = m_old; for (size_t ci = 0; ci < cone_vars.size(); ++ci) { if (!cone_is_rotated[ci]) { continue; } const auto& rc = rotated_cones[ri++]; - const i_t dim = cone_dims[ci]; + const i_t dim = cone_dims[ci]; std::vector new_cone; new_cone.reserve(static_cast(dim)); new_cone.push_back(slack_base); @@ -447,8 +449,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( new_cone.insert(new_cone.end(), rc.tails.begin(), rc.tails.end()); cone_vars[ci] = std::move(new_cone); - is_cone_var[static_cast(rc.head0)] = 0; - is_cone_var[static_cast(rc.head1)] = 0; + is_cone_var[static_cast(rc.head0)] = 0; + is_cone_var[static_cast(rc.head1)] = 0; is_cone_var[static_cast(slack_base)] = 1; is_cone_var[static_cast(slack_base + 1)] = 1; diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu index b8e6eef487..a29e840deb 100644 --- a/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu @@ -18,6 +18,7 @@ namespace cuopt::linear_programming::dual_simplex::test { +// Mirrors `cone_step_length_from_scalars` in second_order_cone_kernels.cuh (device). double host_cone_step_length_from_scalars(double u0, double du0, double du_tail_sq, @@ -32,12 +33,12 @@ double host_cone_step_length_from_scalars(double u0, const auto disc = b * b - a * c; auto alpha = alpha_max; - if (du0 < 0.0) { alpha = std::min(alpha, -u0 / du0); } + if (u0 >= 0.0 && du0 < 0.0) { alpha = std::min(alpha, -u0 / du0); } if ((a > 0.0 && b > 0.0) || disc < 0.0) { return alpha; } if (a == 0.0) { - if (b < 0.0) { alpha = std::min(alpha, c / (-2.0 * b)); } + return alpha; } else if (c == 0.0) { alpha = a >= 0.0 ? alpha : 0.0; } else { @@ -186,7 +187,7 @@ TEST(second_order_cone_kernels, nt_scaling_matches_host_reference) const auto x_scale = std::sqrt(x_det); const auto z_scale = std::sqrt(z_det); - expected_eta[cone] = std::sqrt(x_scale / z_scale); + expected_eta[cone] = std::sqrt(z_scale / x_scale); double normalized_xz_dot = 0.0; for (int local_idx = 0; local_idx < dim; ++local_idx) { @@ -200,7 +201,7 @@ TEST(second_order_cone_kernels, nt_scaling_matches_host_reference) expected_w[offset] = 0.0; for (int local_idx = 1; local_idx < dim; ++local_idx) { const auto idx = offset + local_idx; - expected_w[idx] = (x_host[idx] / x_scale - z_host[idx] / z_scale) / w_scale; + expected_w[idx] = (z_host[idx] / z_scale - x_host[idx] / x_scale) / w_scale; } double normalized_tail_sq = 0.0; @@ -379,8 +380,9 @@ TEST(second_order_cone_kernels, scaling_operators_match_host_reference) rmm::device_uvector w_out(n_cone_entries, stream); rmm::device_uvector w_inv_out(n_cone_entries, stream); rmm::device_uvector h_out(n_cone_entries, stream); - rmm::device_uvector w_inv_tmp(n_cone_entries, stream); - rmm::device_uvector h_from_w_inv(n_cone_entries, stream); + rmm::device_uvector w_tmp(n_cone_entries, stream); + // apply_w then apply_w on same v: should match apply_hessian (H = W^2 for symmetric NT W). + rmm::device_uvector w_squared_v(n_cone_entries, stream); rmm::device_uvector recovered_dz(n_cone_entries, stream); cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); @@ -389,26 +391,26 @@ TEST(second_order_cone_kernels, scaling_operators_match_host_reference) auto v_span = raft::device_span(v.data(), v.size()); apply_w(v_span, cuopt::make_span(w_out), cones, stream); apply_w_inv(v_span, cuopt::make_span(w_inv_out), cones, stream); - apply_hinv2(v_span, cuopt::make_span(h_out), cones, stream); + apply_hessian(v_span, cuopt::make_span(h_out), cones, stream); recover_cone_dz_from_target( v_span, cones, raft::device_span(cone_target.data(), cone_target.size()), cuopt::make_span(recovered_dz), stream); - accumulate_cone_hinv2_matvec(v_span, cones, cuopt::make_span(accum), stream); - apply_w_inv(v_span, cuopt::make_span(w_inv_tmp), cones, stream); - apply_w_inv(raft::device_span(w_inv_tmp.data(), w_inv_tmp.size()), - cuopt::make_span(h_from_w_inv), - cones, - stream); + accumulate_cone_hessian_matvec(v_span, cones, cuopt::make_span(accum), stream); + apply_w(v_span, cuopt::make_span(w_tmp), cones, stream); + apply_w(raft::device_span(w_tmp.data(), w_tmp.size()), + cuopt::make_span(w_squared_v), + cones, + stream); auto eta_host = cuopt::host_copy(cones.eta, stream); auto w_host = cuopt::host_copy(cones.w, stream); auto w_out_host = cuopt::host_copy(w_out, stream); auto w_inv_out_host = cuopt::host_copy(w_inv_out, stream); auto h_out_host = cuopt::host_copy(h_out, stream); - auto h_identity_host = cuopt::host_copy(h_from_w_inv, stream); + auto w_squared_v_host = cuopt::host_copy(w_squared_v, stream); auto recovered_dz_host = cuopt::host_copy(recovered_dz, stream); auto accum_host = cuopt::host_copy(accum, stream); @@ -433,8 +435,8 @@ TEST(second_order_cone_kernels, scaling_operators_match_host_reference) expected_w[offset] = eta * (w0 * v0 + tail_dot); expected_w_inv[offset] = (w0 * v0 - tail_dot) / eta; - const auto rho = w0 * v0 - tail_dot; - expected_h_unscaled[offset] = (2.0 * w0 * rho - v0) / (eta * eta); + const auto rho = w0 * v0 + tail_dot; + expected_h_unscaled[offset] = (eta * eta) * (2.0 * w0 * rho - v0); expected_h[offset] = expected_h_unscaled[offset]; for (int local_idx = 1; local_idx < dim; ++local_idx) { @@ -442,7 +444,7 @@ TEST(second_order_cone_kernels, scaling_operators_match_host_reference) expected_w[idx] = eta * (v_host[idx] + (v0 + tail_dot / (1.0 + w0)) * w_host[idx]); expected_w_inv[idx] = (v_host[idx] + (-v0 + tail_dot / (1.0 + w0)) * w_host[idx]) / eta; - expected_h_unscaled[idx] = (v_host[idx] - 2.0 * w_host[idx] * rho) / (eta * eta); + expected_h_unscaled[idx] = (eta * eta) * (2.0 * w_host[idx] * rho + v_host[idx]); expected_h[idx] = expected_h_unscaled[idx]; } @@ -453,7 +455,7 @@ TEST(second_order_cone_kernels, scaling_operators_match_host_reference) EXPECT_NEAR(w_out_host[i], expected_w[i], 1e-9) << "W entry " << i; EXPECT_NEAR(w_inv_out_host[i], expected_w_inv[i], 1e-9) << "W inverse entry " << i; EXPECT_NEAR(h_out_host[i], expected_h[i], 1e-9) << "H entry " << i; - EXPECT_NEAR(h_out_host[i], h_identity_host[i], 1e-9) << "H identity entry " << i; + EXPECT_NEAR(h_out_host[i], w_squared_v_host[i], 1e-9) << "W^2 v vs H v entry " << i; EXPECT_NEAR(recovered_dz_host[i], cone_target_host[i] - expected_h_unscaled[i], 1e-9) << "recovered dz entry " << i; EXPECT_NEAR(accum_host[i], accum_initial_host[i] + expected_h_unscaled[i], 1e-9) @@ -563,9 +565,10 @@ TEST(second_order_cone_kernels, combined_cone_rhs_matches_host_reference) return result; }; - auto scaled_dx = apply_w_inv_ref(dx_aff_host); - auto scaled_dz = apply_w_ref(dz_aff_host); - auto nt_point = apply_w_ref(z_host); + // Same order as compute_combined_cone_rhs_term: apply_w(dx_aff), apply_w_inv(dz_aff). + auto scaled_dx = apply_w_ref(dx_aff_host); + auto scaled_dz = apply_w_inv_ref(dz_aff_host); + auto nt_point = apply_w_inv_ref(z_host); std::vector shift(n_cone_entries); offset = 0; @@ -616,7 +619,7 @@ TEST(second_order_cone_kernels, combined_cone_rhs_matches_host_reference) offset += static_cast(dim); } - auto expected = apply_w_inv_ref(minus_p); + auto expected = apply_w_ref(minus_p); for (std::size_t i = 0; i < n_cone_entries; ++i) { EXPECT_NEAR(out_host[i], expected[i], 1e-8) << "entry " << i; } From a9f82e98182e37e5afcea3bf3e2c94952471bdf6 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 7 May 2026 09:30:20 -0700 Subject: [PATCH 31/68] uadd support for mflexible uniform coefficients in quadratic cost & linear constraint cost in quadratic constraints --- cpp/src/pdlp/translate.hpp | 692 +++++++++++++++++++++++++++++-------- 1 file changed, 552 insertions(+), 140 deletions(-) diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index a5807f102c..d3a1755983 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -16,8 +16,10 @@ #include +#include #include #include +#include #include #include #include @@ -122,10 +124,17 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( // SOC conversion accepts: // 1) diagonal Lorentz-form QCMATRIX rows: - // -x_head^2 + sum_i x_tail_i^2 <= 0 - // 2) canonical rotated SOC rows: - // -2*x_head0*x_head1 + sum_i x_tail_i^2 <= 0 - // represented in MPS QCMATRIX as symmetric off-diagonals (-1,-1). + // -s*x_head^2 + sum_i s*x_tail_i^2 <= 0 (any common s > 0; divide by s to normalize) + // 2) rotated SOC rows: + // -2*d*x_head0*x_head1 + sum_i s*x_tail_i^2 <= 0 (d>0, s>0; canonical d=s) + // symmetric Q off-diagonals (-d,-d) give x^T Q x cross term -2*d*x0*x1, i.e. a*x0*x1 + // in the inequality 2*d*x0*x1 >= s*||tail||^2 with a = 2*d. Lift uses sqrt(d/s) on heads. + // 3) quadratic rows with linear part: + // sum_i s*x_tail_i^2 + a^T x <= 0 + // represented as diagonal +s QCMATRIX entries plus linear terms in COLUMNS. + // We introduce an auxiliary t = -(1/s)*a^T x so the row becomes: + // sum_i x_tail_i^2 - t <= 0 + // then lift it as rotated SOC with implicit second head fixed at 1/2. // The barrier consumes SOCs as trailing variable blocks [head, tails...], so we validate all // QCMATRIX blocks first, convert rotated cones via slack variables in standard SOC coordinates, // then apply a single column permutation to the linear model. @@ -133,28 +142,45 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( i_t head0{}; i_t head1{}; std::vector tails{}; + bool head1_is_constant_half{false}; + /// For two-head rotated SOC: sqrt(d/s) where Q_off = -d and tail diagonals +s (canonical 1). + f_t head_lift_sqrt_ratio{f_t(1)}; }; + // This is the index of the auxiliary variable for the linear part of the quadratic constraint. + std::vector qc_affine_heads(qcs.size(), static_cast(-1)); + i_t n_affine_linear_aux = 0; + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + if (!qcs[qc_i].linear_values.empty()) { + qc_affine_heads[qc_i] = static_cast(n + n_affine_linear_aux); + ++n_affine_linear_aux; + } + } + + const i_t n_with_affine_aux = static_cast(n + n_affine_linear_aux); std::vector> cone_vars; std::vector cone_dims; std::vector cone_is_rotated; std::vector rotated_cones; - std::vector is_cone_var(static_cast(n), 0); + std::vector is_cone_var(static_cast(n_with_affine_aux), 0); cone_vars.reserve(qcs.size()); cone_dims.reserve(qcs.size()); cone_is_rotated.reserve(qcs.size()); rotated_cones.reserve(qcs.size()); + std::vector qc_soc_uniform_scale(qcs.size(), f_t(1)); - for (const auto& qc : qcs) { + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const auto& qc = qcs[qc_i]; cuopt_expects(qc.constraint_row_type == 'L', error_type_t::ValidationError, "Only <= quadratic constraints are supported for SOC conversion"); - cuopt_expects(qc.linear_values.empty(), - error_type_t::ValidationError, - "SOC conversion currently requires zero linear terms in quadratic constraints"); cuopt_expects(qc.rhs_value < tol && qc.rhs_value > -tol, error_type_t::ValidationError, "SOC conversion currently requires rhs = 0 for quadratic constraints"); + cuopt_expects(qc.linear_values.size() == qc.linear_indices.size(), + error_type_t::ValidationError, + "Quadratic constraint '%s' linear_values and linear_indices length mismatch", + qc.constraint_row_name.c_str()); cuopt_expects(qc.quadratic_offsets.size() >= 2, error_type_t::ValidationError, @@ -167,9 +193,9 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( qc.constraint_row_name.c_str()); const i_t q_nnz = static_cast(qc.quadratic_values.size()); - cuopt_expects(q_nnz >= 2, + cuopt_expects(q_nnz >= 1, error_type_t::ValidationError, - "Quadratic constraint '%s' SOC must have at least 2 entries in Q (nnz %d)", + "Quadratic constraint '%s' SOC must have at least 1 entry in Q (nnz %d)", qc.constraint_row_name.c_str(), static_cast(q_nnz)); @@ -193,17 +219,44 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( error_type_t::ValidationError, "Quadratic constraint '%s' Q CSR offsets[0] must be 0", qc.constraint_row_name.c_str()); + // This is the index of the auxiliary variable for the linear part of the quadratic constraint. + const i_t affine_head = qc_affine_heads[qc_i]; + const bool has_linear_part = affine_head >= 0; + if (has_linear_part) { + size_t nonzero_terms = 0; + for (size_t p = 0; p < qc.linear_values.size(); ++p) { + const i_t idx = qc.linear_indices[p]; + const f_t v = qc.linear_values[p]; + cuopt_expects(idx >= 0 && idx < n, + error_type_t::ValidationError, + "Quadratic constraint '%s' linear index %d is outside [0, %d)", + qc.constraint_row_name.c_str(), + static_cast(idx), + static_cast(n)); + if (v > -tol && v < tol) { continue; } + ++nonzero_terms; + } + cuopt_expects(nonzero_terms > 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' has linear section but all linear coefficients are " + "zero", + qc.constraint_row_name.c_str()); + } // Verify Q as either: - // - standard SOC diagonal Lorentz form, or - // - canonical rotated SOC with one symmetric (-1,-1) off-diagonal pair. - i_t head = static_cast(-1); - i_t n_head_m = 0; - std::vector tail_vars{}; - std::vector> offdiag_pairs{}; - tail_vars.reserve(static_cast(q_nnz)); - offdiag_pairs.reserve(2); - + // - standard SOC: one diagonal -s (head), tail diagonals +s for a common s > 0, + // - rotated SOC: symmetric (-s,-s) off-diagonal pair on the two heads, tails +s, + // - affine SOC: tail diagonals +s and linear terms (no Q off-diagonals). + // Feasibility is unchanged after dividing the quadratic row by s; affine rows also scale + // linear coefficients when forming the auxiliary t = -(1/s) a^T x. + + auto approx_eq_scaled = [&](f_t a, f_t b) { + const f_t scale = std::max({f_t(1), std::abs(a), std::abs(b)}); + return std::abs(a - b) <= tol * scale; + }; + + std::vector> q_entries; + q_entries.reserve(static_cast(q_nnz)); for (i_t r = 0; r < n; ++r) { const i_t p_beg = qc.quadratic_offsets[static_cast(r)]; const i_t p_end = qc.quadratic_offsets[static_cast(r + 1)]; @@ -225,94 +278,266 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( static_cast(r), static_cast(p_end - p_beg)); - const i_t col = qc.quadratic_indices[static_cast(p_beg)]; - const f_t v = qc.quadratic_values[static_cast(p_beg)]; - const f_t neg_one_delta = v + f_t(1); - const f_t pos_one_delta = v - f_t(1); - const bool is_neg_one = (neg_one_delta >= -tol && neg_one_delta <= tol); - const bool is_pos_one = (pos_one_delta >= -tol && pos_one_delta <= tol); - - if (col == r) { - if (is_neg_one) { - ++n_head_m; - head = r; - } else if (is_pos_one) { - tail_vars.push_back(r); + const i_t col = qc.quadratic_indices[static_cast(p_beg)]; + const f_t v = qc.quadratic_values[static_cast(p_beg)]; + q_entries.emplace_back(r, col, v); + } + cuopt_expects(static_cast(q_entries.size()) == q_nnz, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row nnz mismatch (expected %d stored entries, got " + "%zu)", + qc.constraint_row_name.c_str(), + static_cast(q_nnz), + q_entries.size()); + + std::vector> pos_diag_rows; + std::vector> neg_diag_rows; + std::vector> offdiag_entries; + pos_diag_rows.reserve(q_entries.size()); + neg_diag_rows.reserve(1); + offdiag_entries.reserve(4); + + for (const auto& [r, c, v] : q_entries) { + if (r == c) { + if (v > tol) { + pos_diag_rows.emplace_back(r, v); + } else if (v < -tol) { + neg_diag_rows.emplace_back(r, v); } else { cuopt_expects(false, error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: diagonal for SOC must be -1 (head) " - "or +1 (tail); got %.17g", + "Quadratic constraint '%s' Q row %d: diagonal SOC entry is near zero " + "(%.17g)", qc.constraint_row_name.c_str(), static_cast(r), static_cast(v)); } } else { - cuopt_expects(is_neg_one, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: off-diagonal entries are only " - "supported for rotated SOC as value -1; got %.17g at column %d", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(v), - static_cast(col)); - offdiag_pairs.emplace_back(r, col); + offdiag_entries.emplace_back(r, c, v); } } - std::vector cone; - if (offdiag_pairs.empty()) { - cuopt_expects( - n_head_m == 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected exactly one diagonal with value -1 (cone " - "head), found %d", - qc.constraint_row_name.c_str(), - static_cast(n_head_m)); - cuopt_expects( - static_cast(tail_vars.size()) == q_nnz - 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected %d diagonals with value +1 (tails), found %zu", - qc.constraint_row_name.c_str(), - static_cast(q_nnz - 1), - tail_vars.size()); - cuopt_expects(head >= 0, + + std::vector tail_vars; + tail_vars.reserve(pos_diag_rows.size()); + for (const auto& pr : pos_diag_rows) { + tail_vars.push_back(pr.first); + } + + f_t uniform_s = f_t(0); + bool have_uniform_s = false; + auto note_positive_s = [&](f_t v) { + cuopt_expects(v > tol, error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: internal error (head index invalid)", - qc.constraint_row_name.c_str()); + "Quadratic constraint '%s' SOC Q: expected strictly positive diagonal tail " + "coefficient, got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v)); + if (!have_uniform_s) { + uniform_s = v; + have_uniform_s = true; + } else { + cuopt_expects( + approx_eq_scaled(v, uniform_s), + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: all positive diagonal coefficients must match; got " + "%.17g vs %.17g", + qc.constraint_row_name.c_str(), + static_cast(v), + static_cast(uniform_s)); + } + }; - cone.reserve(static_cast(q_nnz)); - cone.push_back(head); - cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + std::vector cone; + i_t cone_dim = 0; + char is_rotated = 0; + i_t head = static_cast(-1); + + if (offdiag_entries.empty()) { + if (!has_linear_part) { + if (pos_diag_rows.empty()) { + cuopt_expects(neg_diag_rows.size() == 1 && q_nnz == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected tail diagonals +s with head -s, " + "or a single head row with q_nnz=1", + qc.constraint_row_name.c_str()); + const f_t neg_v = neg_diag_rows[0].second; + cuopt_expects(neg_v < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: cone head diagonal must be negative " + "(%.17g)", + qc.constraint_row_name.c_str(), + static_cast(neg_v)); + uniform_s = -neg_v; + have_uniform_s = true; + head = neg_diag_rows[0].first; + cuopt_expects( + static_cast(tail_vars.size()) == q_nnz - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 1), + tail_vars.size()); + cone.reserve(1); + cone.push_back(head); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(0); + } else { + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: could not infer uniform positive scale s", + qc.constraint_row_name.c_str()); + cuopt_expects( + neg_diag_rows.size() == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected exactly one diagonal -s (cone head) for " + "%zu tail entries, found %zu negative diagonals", + qc.constraint_row_name.c_str(), + tail_vars.size(), + neg_diag_rows.size()); + cuopt_expects( + static_cast(tail_vars.size()) == q_nnz - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 1), + tail_vars.size()); + const f_t neg_v = neg_diag_rows[0].second; + cuopt_expects( + approx_eq_scaled(neg_v, -uniform_s), + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: cone head diagonal must be -s with the same s as " + "positive tail diagonals; head %.17g vs -s = %.17g", + qc.constraint_row_name.c_str(), + static_cast(neg_v), + static_cast(-uniform_s)); + head = neg_diag_rows[0].first; + cone.reserve(static_cast(q_nnz)); + cone.push_back(head); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(0); + } + } else { + cuopt_expects(neg_diag_rows.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms cannot contain negative diagonal " + "Q entries", + qc.constraint_row_name.c_str()); + cuopt_expects(affine_head >= 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' internal error: affine SOC head index invalid", + qc.constraint_row_name.c_str()); + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms must have at least one " + "diagonal +s term in Q", + qc.constraint_row_name.c_str()); + cuopt_expects(!tail_vars.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms must have at least one " + "diagonal +s term in Q", + qc.constraint_row_name.c_str()); + for (const i_t tail : tail_vars) { + cuopt_expects( + tail != affine_head, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms requires the linear head variable to be " + "distinct from quadratic diagonal variables", + qc.constraint_row_name.c_str()); + } + + cone.reserve(tail_vars.size() + 1); + cone.push_back(affine_head); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(tail_vars.size() + 2); + is_rotated = static_cast(1); + rotated_cones.push_back( + rotated_soc_t{affine_head, static_cast(-1), tail_vars, true, f_t(1)}); + } } else { + cuopt_expects(!has_linear_part, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms cannot include rotated-SOC " + "off-diagonal entries", + qc.constraint_row_name.c_str()); + cuopt_expects(neg_diag_rows.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q cannot contain diagonal head " + "entries; found %zu negative diagonals", + qc.constraint_row_name.c_str(), + neg_diag_rows.size()); + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q: could not infer uniform scale s", + qc.constraint_row_name.c_str()); cuopt_expects( - n_head_m == 0, + offdiag_entries.size() == 2, error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q cannot contain diagonal -1 entries; found %d", + "Quadratic constraint '%s' rotated SOC Q must contain exactly one symmetric off-diagonal " + "pair (-d,-d); found %zu off-diagonal entries", qc.constraint_row_name.c_str(), - static_cast(n_head_m)); + offdiag_entries.size()); + + const i_t a = std::get<0>(offdiag_entries[0]); + const i_t b = std::get<1>(offdiag_entries[0]); + const f_t v0 = std::get<2>(offdiag_entries[0]); cuopt_expects( - offdiag_pairs.size() == 2, + v0 < -tol, error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q must contain exactly one symmetric off-diagonal " - "pair (-1,-1); found %zu off-diagonal entries", + "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", qc.constraint_row_name.c_str(), - offdiag_pairs.size()); - - const i_t a = offdiag_pairs[0].first; - const i_t b = offdiag_pairs[0].second; + static_cast(v0)); cuopt_expects(a != b, error_type_t::ValidationError, "Quadratic constraint '%s' rotated SOC Q off-diagonal pair must use distinct " "variables", qc.constraint_row_name.c_str()); - cuopt_expects(offdiag_pairs[1].first == b && offdiag_pairs[1].second == a, + cuopt_expects(std::get<0>(offdiag_entries[1]) == b && std::get<1>(offdiag_entries[1]) == a, error_type_t::ValidationError, "Quadratic constraint '%s' rotated SOC Q must have symmetric entries (a,b) " - "and (b,a) with value -1", + "and (b,a) with the same value", qc.constraint_row_name.c_str()); + const f_t v1 = std::get<2>(offdiag_entries[1]); + cuopt_expects( + v1 < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v1)); + cuopt_expects( + approx_eq_scaled(v0, v1), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q symmetric off-diagonals must match; got %.17g " + "and %.17g", + qc.constraint_row_name.c_str(), + static_cast(v0), + static_cast(v1)); + const f_t cross_d = -v0; + cuopt_expects( + cross_d > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q cross coefficient d = -Q_off must be positive", + qc.constraint_row_name.c_str()); + const f_t head_lift_sqrt_ratio = std::sqrt(cross_d / uniform_s); + cuopt_expects(std::isfinite(static_cast(head_lift_sqrt_ratio)), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q head lift ratio sqrt(d/s) is not " + "finite (d=%.17g, s=%.17g)", + qc.constraint_row_name.c_str(), + static_cast(cross_d), + static_cast(uniform_s)); cuopt_expects(static_cast(tail_vars.size()) == q_nnz - 2, error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q: expected %d diagonal +1 entries " + "Quadratic constraint '%s' rotated SOC Q: expected %d diagonal +s entries " "(tails), found %zu", qc.constraint_row_name.c_str(), static_cast(q_nnz - 2), @@ -326,24 +551,106 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( cone.push_back(a); cone.push_back(b); cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); - rotated_cones.push_back(rotated_soc_t{a, b, tail_vars}); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(1); + rotated_cones.push_back(rotated_soc_t{a, b, tail_vars, false, head_lift_sqrt_ratio}); } + + cuopt_expects( + have_uniform_s && uniform_s > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: uniform scale s must be positive (got %.17g)", + qc.constraint_row_name.c_str(), + static_cast(uniform_s)); + qc_soc_uniform_scale[qc_i] = uniform_s; + for (const i_t var : cone) { - cuopt_expects(!is_cone_var[static_cast(var)], + cuopt_expects(var >= 0 && var < static_cast(is_cone_var.size()), error_type_t::ValidationError, - "Variable %d appears in more than one SOC QCMATRIX block; overlapping cones " - "are not supported", - static_cast(var)); - is_cone_var[static_cast(var)] = 1; + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(is_cone_var.size())); } - cone_dims.push_back(static_cast(cone.size())); + cone_dims.push_back(cone_dim); cone_vars.push_back(std::move(cone)); - cone_is_rotated.push_back(offdiag_pairs.empty() ? static_cast(0) - : static_cast(1)); + cone_is_rotated.push_back(is_rotated); + } + // Add affine linear auxiliary variables and linking rows. + if (n_affine_linear_aux > 0) { + const f_t inf = std::numeric_limits::infinity(); + const i_t n_old = static_cast(n); + const i_t n_aug = n_with_affine_aux; + const i_t m_old = csr_A.m; + const i_t m_aug = static_cast(m_old + n_affine_linear_aux); + i_t row_write_cursor = m_old; + + user_problem.objective.resize(static_cast(n_aug), f_t(0)); + user_problem.lower.resize(static_cast(n_aug), -inf); + user_problem.upper.resize(static_cast(n_aug), inf); + user_problem.var_types.resize( + static_cast(n_aug), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_aug)); + } + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const i_t aux_j = qc_affine_heads[qc_i]; + if (aux_j < 0) { continue; } + user_problem.lower[static_cast(aux_j)] = f_t(0); + user_problem.upper[static_cast(aux_j)] = inf; + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(aux_j)] = + "_CUOPT_qc_linear_aux_" + std::to_string(static_cast(aux_j - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_aug)); + user_problem.row_sense.resize(static_cast(m_aug)); + if (!user_problem.row_names.empty()) { user_problem.row_names.resize(static_cast(m_aug)); } + + csr_A.n = n_aug; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_aug; + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const i_t aux_j = qc_affine_heads[qc_i]; + if (aux_j < 0) { continue; } + const auto& qc = qcs[qc_i]; + eq_row.i.clear(); + eq_row.x.clear(); + // Define auxiliary as t = -(1/s) a^T x so QC linear part matches normalized cone row. + const f_t inv_s = f_t(1) / qc_soc_uniform_scale[qc_i]; + eq_row.i.push_back(aux_j); + eq_row.x.push_back(f_t(1)); + for (size_t p = 0; p < qc.linear_values.size(); ++p) { + const f_t v = qc.linear_values[p]; + if (v > -tol && v < tol) { continue; } + eq_row.i.push_back(qc.linear_indices[p]); + eq_row.x.push_back(v * inv_s); + } + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_write_cursor)] = 'E'; + user_problem.rhs[static_cast(row_write_cursor)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_write_cursor)] = + "_CUOPT_qc_linear_link_" + qc.constraint_row_name; + } + ++row_write_cursor; + } + + cuopt_expects(row_write_cursor == m_aug, + error_type_t::RuntimeError, + "Internal error: affine QC linking row count mismatch"); + cuopt_expects(csr_A.m == m_aug, + error_type_t::RuntimeError, + "Internal error: CSR row count after affine QC linking"); } - i_t n_prob = static_cast(n); + i_t n_prob = n_with_affine_aux; + // Convert rotated SOC cones to standard SOC cones. if (!rotated_cones.empty()) { cuopt_expects(user_problem.Q_values.empty(), error_type_t::ValidationError, @@ -352,50 +659,31 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( const f_t inf = std::numeric_limits::infinity(); const f_t inv_sqrt_2 = f_t(1) / std::sqrt(f_t(2)); + const f_t half = f_t(0.5); for (const auto& rc : rotated_cones) { cuopt_expects(user_problem.var_types[static_cast(rc.head0)] == - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS && - user_problem.var_types[static_cast(rc.head1)] == - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, error_type_t::ValidationError, "Rotated SOC head variables must be continuous"); - for (const i_t t : rc.tails) { - cuopt_expects(user_problem.var_types[static_cast(t)] == + if (!rc.head1_is_constant_half) { + cuopt_expects(user_problem.var_types[static_cast(rc.head1)] == cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, error_type_t::ValidationError, - "Rotated SOC tail variables must be continuous"); + "Rotated SOC head variables must be continuous"); } - } - - auto is_inf = [](f_t x) { return std::isinf(static_cast(x)); }; - for (const auto& rc : rotated_cones) { - const f_t l0 = user_problem.lower[static_cast(rc.head0)]; - const f_t u0 = user_problem.upper[static_cast(rc.head0)]; - const f_t l1 = user_problem.lower[static_cast(rc.head1)]; - const f_t u1 = user_problem.upper[static_cast(rc.head1)]; - cuopt_expects(l0 >= -tol && l0 <= tol && is_inf(u0) && u0 > 0, - error_type_t::ValidationError, - "Rotated SOC head variable %d must have bounds [0, +inf) for conversion", - static_cast(rc.head0)); - cuopt_expects(l1 >= -tol && l1 <= tol && is_inf(u1) && u1 > 0, - error_type_t::ValidationError, - "Rotated SOC head variable %d must have bounds [0, +inf) for conversion", - static_cast(rc.head1)); for (const i_t t : rc.tails) { - const f_t lt = user_problem.lower[static_cast(t)]; - const f_t ut = user_problem.upper[static_cast(t)]; - cuopt_expects(is_inf(lt) && lt < 0 && is_inf(ut) && ut > 0, + cuopt_expects(user_problem.var_types[static_cast(t)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, error_type_t::ValidationError, - "Rotated SOC tail variable %d must be free [-inf, +inf) for conversion", - static_cast(t)); + "Rotated SOC tail variables must be continuous"); } } // Lift each rotated cone into standard SOC coordinates with two slacks: - // s0 = (x_h0 + x_h1) / sqrt(2), s1 = (x_h0 - x_h1) / sqrt(2) - // so that 2*x_h0*x_h1 >= sum tails^2 becomes s0^2 >= s1^2 + sum tails^2. - // Only the rotated heads are replaced by slacks; tails remain unchanged. + // With x_i' = sqrt(d/s)*x_hi, canonical s0 = (x_0'+x_1')/sqrt(2), s1 = (x_0'-x_1')/sqrt(2) + // so 2*d*x_h0*x_h1 >= s*sum tail^2 <=> 2*x_0'*x_1' >= sum (x_tail)^2 => s0^2 >= s1^2 + ... + // Only the rotated heads are replaced by slacks; tails stay as original variables. i_t n_slack_total = 0; for (size_t ci = 0; ci < cone_is_rotated.size(); ++ci) { if (cone_is_rotated[ci]) { n_slack_total += 2; } @@ -449,29 +737,55 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( new_cone.insert(new_cone.end(), rc.tails.begin(), rc.tails.end()); cone_vars[ci] = std::move(new_cone); - is_cone_var[static_cast(rc.head0)] = 0; - is_cone_var[static_cast(rc.head1)] = 0; is_cone_var[static_cast(slack_base)] = 1; is_cone_var[static_cast(slack_base + 1)] = 1; - // s_0 - inv_sqrt_2 * x_h0 - inv_sqrt_2 * x_h1 = 0 eq_row.n = n_prob; - eq_row.i = {rc.head0, rc.head1, slack_base}; - eq_row.x = {-inv_sqrt_2, -inv_sqrt_2, f_t(1)}; - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_idx)] = 'E'; - user_problem.rhs[static_cast(row_idx)] = f_t(0); - ++row_idx; - - // s_1 - inv_sqrt_2 * x_h0 + inv_sqrt_2 * x_h1 = 0 - eq_row.i = {rc.head0, rc.head1, slack_base + 1}; - eq_row.x = {-inv_sqrt_2, inv_sqrt_2, f_t(1)}; - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_idx)] = 'E'; - user_problem.rhs[static_cast(row_idx)] = f_t(0); - ++row_idx; + // If the second head is not constant half, we need to lift it. + if (!rc.head1_is_constant_half) { + const f_t h = inv_sqrt_2 * rc.head_lift_sqrt_ratio; + // s_0 - h * x_h0 - h * x_h1 = 0 (h = inv_sqrt_2 * sqrt(d/s)) + eq_row.i = {rc.head0, rc.head1, slack_base}; + eq_row.x = {-h, -h, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + // s_1 - h * x_h0 + h * x_h1 = 0 + eq_row.i = {rc.head0, rc.head1, slack_base + 1}; + eq_row.x = {-h, h, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + is_cone_var[static_cast(rc.head0)] = 0; + is_cone_var[static_cast(rc.head1)] = 0; + } else { + // One head is constant half, so we can lift it directly. + // s_0 - inv_sqrt_2 * x_h0 = inv_sqrt_2 * (1/2) + eq_row.i = {rc.head0, slack_base}; + eq_row.x = {-inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = inv_sqrt_2 * half; + ++row_idx; + + // s_1 - inv_sqrt_2 * x_h0 = -inv_sqrt_2 * (1/2) + eq_row.i = {rc.head0, slack_base + 1}; + eq_row.x = {-inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = -inv_sqrt_2 * half; + ++row_idx; + + is_cone_var[static_cast(rc.head0)] = 0; + } slack_base += 2; } @@ -489,8 +803,104 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( error_type_t::RuntimeError, "Internal error: CSR row count after rotated SOC lift"); - user_problem.num_rows = csr_A.m; - user_problem.num_cols = n_prob; + } + + // If a variable appears in multiple cones, create per-cone aliases and add linking rows + // alias - original = 0 so cone variable blocks are disjoint. + { + std::vector first_owner(static_cast(n_prob), static_cast(-1)); + std::vector> cone_alias_pairs; // (alias, original) + + for (size_t ci = 0; ci < cone_vars.size(); ++ci) { + auto& cone = cone_vars[ci]; + for (auto& var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(n_prob)); + const auto idx = static_cast(var); + if (first_owner[idx] == static_cast(-1)) { + first_owner[idx] = static_cast(ci); + continue; + } + if (first_owner[idx] != static_cast(ci)) { + const i_t alias = static_cast(n_prob + cone_alias_pairs.size()); + cone_alias_pairs.emplace_back(alias, var); + var = alias; + } + } + } + + if (!cone_alias_pairs.empty()) { + const i_t n_old = n_prob; + const i_t n_new = static_cast(n_old + cone_alias_pairs.size()); + const i_t m_old = csr_A.m; + const i_t m_new = static_cast(m_old + cone_alias_pairs.size()); + + user_problem.objective.resize(static_cast(n_new), f_t(0)); + user_problem.lower.resize(static_cast(n_new), -std::numeric_limits::infinity()); + user_problem.upper.resize(static_cast(n_new), std::numeric_limits::infinity()); + user_problem.var_types.resize( + static_cast(n_new), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { user_problem.col_names.resize(static_cast(n_new)); } + + for (const auto& [alias, original] : cone_alias_pairs) { + user_problem.lower[static_cast(alias)] = user_problem.lower[static_cast(original)]; + user_problem.upper[static_cast(alias)] = user_problem.upper[static_cast(original)]; + user_problem.var_types[static_cast(alias)] = + user_problem.var_types[static_cast(original)]; + // Keep objective unchanged: alias coefficient stays zero and alias==original links values. + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(alias)] = + "_CUOPT_cone_alias_" + std::to_string(static_cast(alias - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_new)); + user_problem.row_sense.resize(static_cast(m_new)); + if (!user_problem.row_names.empty()) { user_problem.row_names.resize(static_cast(m_new)); } + + csr_A.n = n_new; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_new; + i_t row_idx = m_old; + for (const auto& [alias, original] : cone_alias_pairs) { + eq_row.i = {alias, original}; + eq_row.x = {f_t(1), f_t(-1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_idx)] = + "_CUOPT_cone_alias_link_" + std::to_string(static_cast(row_idx - m_old)); + } + ++row_idx; + } + + cuopt_expects(row_idx == m_new, + error_type_t::RuntimeError, + "Internal error: cone alias linking row count mismatch"); + cuopt_expects(csr_A.m == m_new, + error_type_t::RuntimeError, + "Internal error: CSR row count after cone alias linking"); + + n_prob = n_new; + } + } + + is_cone_var.assign(static_cast(n_prob), 0); + for (const auto& cone : cone_vars) { + for (const i_t var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d) after cone aliasing", + static_cast(var), + static_cast(n_prob)); + is_cone_var[static_cast(var)] = 1; + } } std::vector old_to_new(static_cast(n_prob), i_t{-1}); @@ -609,6 +1019,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( user_problem.cone_var_start = cone_var_start; user_problem.second_order_cone_dims = std::move(cone_dims); + user_problem.num_rows = csr_A.m; + user_problem.num_cols = n_prob; } csr_A.to_compressed_col(user_problem.A); From 7e5bec7197424c09ba5e12f7b713b0b32cb563ac Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 8 May 2026 05:55:35 -0700 Subject: [PATCH 32/68] code clean up for socp logic --- .../cuopt/linear_programming/constants.h | 56 +++---- .../pdlp/solver_settings.hpp | 3 + cpp/src/barrier/barrier.cu | 144 ++++++++---------- cpp/src/dual_simplex/presolve.cpp | 4 +- cpp/src/math_optimization/solver_settings.cu | 3 + cpp/src/pdlp/solve.cu | 31 ++-- cpp/src/pdlp/translate.hpp | 95 +++++++----- 7 files changed, 173 insertions(+), 163 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 06eacb3408..c04fcc5bac 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -20,32 +20,36 @@ #define CUOPT_INSTANTIATE_INT64 0 /* @brief LP/MIP parameter string constants */ -#define CUOPT_ABSOLUTE_DUAL_TOLERANCE "absolute_dual_tolerance" -#define CUOPT_RELATIVE_DUAL_TOLERANCE "relative_dual_tolerance" -#define CUOPT_ABSOLUTE_PRIMAL_TOLERANCE "absolute_primal_tolerance" -#define CUOPT_RELATIVE_PRIMAL_TOLERANCE "relative_primal_tolerance" -#define CUOPT_ABSOLUTE_GAP_TOLERANCE "absolute_gap_tolerance" -#define CUOPT_RELATIVE_GAP_TOLERANCE "relative_gap_tolerance" -#define CUOPT_INFEASIBILITY_DETECTION "infeasibility_detection" -#define CUOPT_STRICT_INFEASIBILITY "strict_infeasibility" -#define CUOPT_PRIMAL_INFEASIBLE_TOLERANCE "primal_infeasible_tolerance" -#define CUOPT_DUAL_INFEASIBLE_TOLERANCE "dual_infeasible_tolerance" -#define CUOPT_ITERATION_LIMIT "iteration_limit" -#define CUOPT_TIME_LIMIT "time_limit" -#define CUOPT_WORK_LIMIT "work_limit" -#define CUOPT_PDLP_SOLVER_MODE "pdlp_solver_mode" -#define CUOPT_METHOD "method" -#define CUOPT_PER_CONSTRAINT_RESIDUAL "per_constraint_residual" -#define CUOPT_SAVE_BEST_PRIMAL_SO_FAR "save_best_primal_so_far" -#define CUOPT_FIRST_PRIMAL_FEASIBLE "first_primal_feasible" -#define CUOPT_LOG_FILE "log_file" -#define CUOPT_LOG_TO_CONSOLE "log_to_console" -#define CUOPT_CROSSOVER "crossover" -#define CUOPT_FOLDING "folding" -#define CUOPT_AUGMENTED "augmented" -#define CUOPT_DUALIZE "dualize" -#define CUOPT_ORDERING "ordering" -#define CUOPT_BARRIER_DUAL_INITIAL_POINT "barrier_dual_initial_point" +#define CUOPT_ABSOLUTE_DUAL_TOLERANCE "absolute_dual_tolerance" +#define CUOPT_RELATIVE_DUAL_TOLERANCE "relative_dual_tolerance" +#define CUOPT_ABSOLUTE_PRIMAL_TOLERANCE "absolute_primal_tolerance" +#define CUOPT_RELATIVE_PRIMAL_TOLERANCE "relative_primal_tolerance" +#define CUOPT_ABSOLUTE_GAP_TOLERANCE "absolute_gap_tolerance" +#define CUOPT_RELATIVE_GAP_TOLERANCE "relative_gap_tolerance" +#define CUOPT_INFEASIBILITY_DETECTION "infeasibility_detection" +#define CUOPT_STRICT_INFEASIBILITY "strict_infeasibility" +#define CUOPT_PRIMAL_INFEASIBLE_TOLERANCE "primal_infeasible_tolerance" +#define CUOPT_DUAL_INFEASIBLE_TOLERANCE "dual_infeasible_tolerance" +#define CUOPT_ITERATION_LIMIT "iteration_limit" +#define CUOPT_TIME_LIMIT "time_limit" +#define CUOPT_WORK_LIMIT "work_limit" +#define CUOPT_PDLP_SOLVER_MODE "pdlp_solver_mode" +#define CUOPT_METHOD "method" +#define CUOPT_PER_CONSTRAINT_RESIDUAL "per_constraint_residual" +#define CUOPT_SAVE_BEST_PRIMAL_SO_FAR "save_best_primal_so_far" +#define CUOPT_FIRST_PRIMAL_FEASIBLE "first_primal_feasible" +#define CUOPT_LOG_FILE "log_file" +#define CUOPT_LOG_TO_CONSOLE "log_to_console" +#define CUOPT_CROSSOVER "crossover" +#define CUOPT_FOLDING "folding" +#define CUOPT_AUGMENTED "augmented" +#define CUOPT_DUALIZE "dualize" +#define CUOPT_ORDERING "ordering" +#define CUOPT_BARRIER_DUAL_INITIAL_POINT "barrier_dual_initial_point" +#define CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE "barrier_relative_feasibility_tolerance" +#define CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE "barrier_relative_optimality_tolerance" +#define CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE \ + "barrier_relative_complementarity_tolerance" #define CUOPT_ELIMINATE_DENSE_COLUMNS "eliminate_dense_columns" #define CUOPT_CUDSS_DETERMINISTIC "cudss_deterministic" #define CUOPT_PRESOLVE "presolve" diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index bcf5a736f0..8955eb94f1 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -279,6 +279,9 @@ class pdlp_solver_settings_t { i_t dualize{-1}; i_t ordering{-1}; i_t barrier_dual_initial_point{-1}; + f_t barrier_relative_feasibility_tolerance{1.0e-8}; + f_t barrier_relative_optimality_tolerance{1.0e-8}; + f_t barrier_relative_complementarity_tolerance{1.0e-8}; bool eliminate_dense_columns{true}; pdlp_precision_t pdlp_precision{pdlp_precision_t::DefaultPrecision}; bool save_best_primal_so_far{false}; diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index 21224c4508..6f91ae2945 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -319,7 +319,8 @@ class iteration_data_t { f_t estimated_nz_AAT = 0.0; std::vector dense_columns_unordered; - if (has_Q || has_cones()) { + const bool has_soc = has_cones(); + if (has_Q || has_soc) { // QP and SOCP always use the augmented KKT; skip dense-column / ADAT heuristics. use_augmented = true; n_dense_columns = 0; @@ -487,10 +488,11 @@ class iteration_data_t { f_t complementarity_degree(std::size_t num_primal_variables, i_t num_upper_bounds) const { + const bool has_soc = has_cones(); f_t degree = static_cast(num_primal_variables) + static_cast(num_upper_bounds); // Native QP free variables (linear only): no x·z complementarity in the barrier degree. degree -= static_cast(n_free_vars); - if (has_cones()) { + if (has_soc) { degree -= static_cast(cone_entry_count()); degree += static_cast(cone_count()); } @@ -1951,6 +1953,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) raft::common::nvtx::range fun_scope("Barrier: initial_point"); const bool use_augmented = data.use_augmented; const bool has_soc = data.has_cones(); + const bool has_free = data.n_free_vars > 0; // Perform a numerical factorization i_t status; @@ -2088,7 +2091,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) } // Native free variables: no finite lower barrier — dual slack z_j stays at 0 (matches non-SOC // branch and barrier algebra). - if (data.n_free_vars > 0) { + if (has_free) { for (i_t j : presolve_info.free_variable_indices) { if (j < cs) { data.z[j] = 0.0; } } @@ -2158,7 +2161,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) } } // Free variables have z = 0 (no complementarity condition) - if (data.n_free_vars > 0) { + if (has_free) { for (i_t j : presolve_info.free_variable_indices) { data.z[j] = 0.0; } @@ -2227,22 +2230,22 @@ int barrier_solver_t::initial_point(iteration_data_t& data) // Make sure (w, x, v, z) > 0. Cone blocks are not nonnegative orthant variables — skip them for // box positivity; NT scaling keeps cone iterates feasible separately. std::vector free_x_save; - if (data.n_free_vars > 0) { + if (has_free) { free_x_save.resize(data.n_free_vars); for (i_t k = 0; k < data.n_free_vars; ++k) { free_x_save[k] = data.x[presolve_info.free_variable_indices[k]]; } } data.w.ensure_positive(epsilon_adjust); - if (data.has_cones()) { + if (has_soc) { data.x.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); } else { data.x.ensure_positive(epsilon_adjust); } // Native free variables (QP/SOCP): restore possibly negative x and set z = 0 - if (data.n_free_vars > 0) { + if (has_free) { for (i_t k = 0; k < data.n_free_vars; ++k) { - i_t j = presolve_info.free_variable_indices[k]; + i_t j = presolve_info.free_variable_indices[k]; data.x[j] = free_x_save[k]; data.z[j] = 0.0; } @@ -2458,22 +2461,14 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t& x, const rmm::device_uvector& dx) { - const bool has_soc = data.has_cones(); - const bool skip_cone_range = has_soc && static_cast(x.size()) >= data.cone_end(); - const bool has_free = - data.n_free_vars > 0 && static_cast(x.size()) == lp.num_cols; - - auto ratio_test = [] HD(const thrust::tuple t) { - const f_t dx_val = thrust::get<0>(t); - const f_t x_val = thrust::get<1>(t); - if (dx_val < f_t(0.0)) return -x_val / dx_val; - return f_t(1.0); - }; + const bool has_soc = data.has_cones() && static_cast(x.size()) >= data.cone_end(); + const bool has_free = data.n_free_vars > 0 && static_cast(x.size()) == lp.num_cols; auto reduce_segment = [&](i_t start, i_t len) -> f_t { if (len <= 0) { return f_t(1); } if (has_free) { auto is_free_ptr = data.d_is_free_.data() + start; + // step size computation for nonnegative variables with free variables auto ratio_test_free = [is_free_ptr] HD(const thrust::tuple t) { const f_t dx_val = thrust::get<0>(t); const f_t x_val = thrust::get<1>(t); @@ -2490,6 +2485,13 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t t) { + const f_t dx = thrust::get<0>(t); + const f_t x = thrust::get<1>(t); + if (dx < f_t(0.0)) return -x / dx; + return f_t(1.0); + }; return data.transform_reduce_helper_.transform_reduce( thrust::make_zip_iterator(dx.data() + start, x.data() + start), thrust::minimum(), @@ -2499,7 +2501,8 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t::gpu_compute_search_direction(iteration_data_t 0; const i_t m_c = data.cone_entry_count(); const i_t cone_var_start = data.cone_start(); const i_t linear_size = data.linear_xz_size(lp.num_cols); @@ -2660,18 +2664,14 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_diag_.size()); - if (data.n_free_vars > 0) { + if (has_free) { constexpr f_t free_var_reg = 1e-7; if (data.Q.n > 0 && data.Q_diagonal) { cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_z_.data(), - data.d_x_.data(), - data.d_is_free_.data(), - data.d_Q_diag_.data()), + cuda::std::make_tuple( + data.d_z_.data(), data.d_x_.data(), data.d_is_free_.data(), data.d_Q_diag_.data()), data.d_diag_.data(), - diag_linear_len, + linear_size, [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_free, f_t q_jj) { if (!is_free) return z_j / x_j; return (q_jj > f_t(0)) ? f_t(0) : free_var_reg; @@ -2681,7 +2681,7 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t{}, stream_view_.value()); } @@ -2809,7 +2809,7 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0) { + if (has_free) { cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_inv_diag.data(), data.d_tmp3_.data(), @@ -3123,6 +3123,8 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_dz_.size()); + if (has_soc) { recover_cone_dz_from_target( raft::device_span(data.d_dx_.data() + cone_var_start, m_c), @@ -3130,49 +3132,30 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_complementarity_target_.data() + cone_var_start, m_c), raft::device_span(data.d_dz_.data() + cone_var_start, m_c), stream_view_); + } - if (cone_var_start > 0) { - if (data.n_free_vars > 0) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_complementarity_target_.data(), - data.d_z_.data(), - data.d_dx_.data(), - data.d_x_.data(), - data.d_is_free_.data()), - data.d_dz_.data(), - cone_var_start, - [] HD(f_t target, f_t z, f_t dx, f_t x, i_t is_free) { - return is_free ? f_t(0) : (target - (z * dx) / x); - }, - stream_view_.value()); - } else { - recover_linear_dz( - raft::device_span(data.d_complementarity_target_.data(), cone_var_start), - raft::device_span(data.d_z_.data(), cone_var_start), - raft::device_span(data.d_dx_.data(), cone_var_start), - raft::device_span(data.d_x_.data(), cone_var_start), - raft::device_span(data.d_dz_.data(), cone_var_start)); - } + if (linear_dz_size > 0) { + if (has_free) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_complementarity_target_.data(), + data.d_z_.data(), + data.d_dx_.data(), + data.d_x_.data(), + data.d_is_free_.data()), + data.d_dz_.data(), + linear_dz_size, + [] HD(f_t target, f_t z, f_t dx, f_t x, i_t is_free) { + return is_free ? f_t(0) : (target - (z * dx) / x); + }, + stream_view_.value()); + } else { + recover_linear_dz( + raft::device_span(data.d_complementarity_target_.data(), linear_dz_size), + raft::device_span(data.d_z_.data(), linear_dz_size), + raft::device_span(data.d_dx_.data(), linear_dz_size), + raft::device_span(data.d_x_.data(), linear_dz_size), + raft::device_span(data.d_dz_.data(), linear_dz_size)); } - } else if (data.n_free_vars > 0) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_complementarity_xz_rhs_.data(), - data.d_z_.data(), - data.d_dx_.data(), - data.d_x_.data(), - data.d_is_free_.data()), - data.d_dz_.data(), - data.d_dz_.size(), - [] HD(f_t complementarity_xz_rhs, f_t z, f_t dx, f_t x, i_t is_free) { - return is_free ? f_t(0) : ((complementarity_xz_rhs - z * dx) / x); - }, - stream_view_.value()); - } else { - recover_linear_dz(cuopt::make_span(data.d_complementarity_target_), - cuopt::make_span(data.d_z_), - cuopt::make_span(data.d_dx_), - cuopt::make_span(data.d_x_), - cuopt::make_span(data.d_dz_)); } RAFT_CHECK_CUDA(stream_view_); raft::copy(dz.data(), data.d_dz_.data(), data.d_dz_.size(), stream_view_); @@ -3507,13 +3490,14 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data { raft::common::nvtx::range fun_scope("Barrier: compute_cc_rhs"); const bool has_soc = data.has_cones(); + const bool has_free = data.n_free_vars > 0; const i_t linear_size = data.linear_xz_size(lp.num_cols); auto fill_linear_cc_rhs = [&](raft::device_span out, raft::device_span dx_aff, raft::device_span dz_aff) { if (out.empty()) return; - if (data.n_free_vars > 0) { + if (has_free) { auto is_free_ptr = data.d_is_free_.data(); cub::DeviceTransform::Transform( cuda::std::make_tuple(dx_aff.data(), dz_aff.data(), is_free_ptr), @@ -3528,9 +3512,7 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data cuda::std::make_tuple(dx_aff.data(), dz_aff.data()), out.data(), out.size(), - [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val) { - return -(dx_aff_val * dz_aff_val) + new_mu; - }, + [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val) { return -(dx_aff_val * dz_aff_val) + new_mu; }, stream_view_.value()); } }; @@ -3741,8 +3723,8 @@ void barrier_solver_t::compute_mu(iteration_data_t& data, f_ const f_t mu_denom = data.complementarity_degree(data.x.size(), data.n_upper_bounds); mu = (data.sum_reduce_helper_.sum(data.d_complementarity_xz_residual_.begin(), - data.d_complementarity_xz_residual_.size(), - stream_view_) + + data.d_complementarity_xz_residual_.size(), + stream_view_) + data.sum_reduce_helper_.sum(data.d_complementarity_wv_residual_.begin(), data.d_complementarity_wv_residual_.size(), stream_view_)) / @@ -4154,9 +4136,9 @@ lp_status_t barrier_solver_t::solve(f_t start_time, relative_complementarity_residual, elapsed_time); - // TODO: should polish termination condition + // TODO: should polish termination condition, bool converged = primal_residual_norm < settings.barrier_relative_feasibility_tol && - dual_residual_norm < settings.barrier_relative_optimality_tol && + dual_residual_norm < settings.barrier_relative_feasibility_tol && (duality_gap_abs < settings.barrier_relative_complementarity_tol || duality_gap_rel < settings.barrier_relative_complementarity_tol); @@ -4363,7 +4345,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, elapsed_time); bool primal_feasible = relative_primal_residual < settings.barrier_relative_feasibility_tol; - bool dual_feasible = relative_dual_residual < settings.barrier_relative_optimality_tol; + bool dual_feasible = relative_dual_residual < settings.barrier_relative_feasibility_tol; bool small_gap = relative_complementarity_residual < settings.barrier_relative_complementarity_tol; diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 7e2421e2ad..8e692a2194 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -1272,8 +1272,8 @@ i_t presolve(const lp_problem_t& original, presolve_info.free_variable_indices.push_back(j); } } - settings.log.printf( - "Keeping %d free variables for augmented-system barrier (QP/SOCP)\n", free_variables); + settings.log.printf("Keeping %d free variables for augmented-system barrier (QP/SOCP)\n", + free_variables); } else if (settings.barrier_presolve && free_variables > 0) { // We have a variable x_j: with -inf < x_j < inf // we create new variables v and w with 0 <= v, w and x_j = v - w diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 39bcdeb8f6..1f6aafb34a 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -98,6 +98,9 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_INTEGRALITY_TOLERANCE, &mip_settings.tolerances.integrality_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-5)}, {CUOPT_MIP_ABSOLUTE_GAP, &mip_settings.tolerances.absolute_mip_gap, f_t(0.0), std::numeric_limits::infinity(), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, {CUOPT_MIP_RELATIVE_GAP, &mip_settings.tolerances.relative_mip_gap, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE, &pdlp_settings.barrier_relative_feasibility_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-8)}, + {CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE, &pdlp_settings.barrier_relative_optimality_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-8)}, + {CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE, &pdlp_settings.barrier_relative_complementarity_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-8)}, {CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, &pdlp_settings.tolerances.primal_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, {CUOPT_DUAL_INFEASIBLE_TOLERANCE, &pdlp_settings.tolerances.dual_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, {CUOPT_MIP_CUT_CHANGE_THRESHOLD, &mip_settings.cut_change_threshold, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0)}, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 3a30645b9e..478b618a78 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -438,19 +438,24 @@ run_barrier(dual_simplex::user_problem_t& user_problem, f_t norm_rhs = dual_simplex::vector_norm2(user_problem.rhs); dual_simplex::simplex_solver_settings_t barrier_settings; - barrier_settings.num_gpus = settings.num_gpus; - barrier_settings.time_limit = settings.time_limit; - barrier_settings.iteration_limit = settings.iteration_limit; - barrier_settings.concurrent_halt = settings.concurrent_halt; - barrier_settings.folding = settings.folding; - barrier_settings.augmented = settings.augmented; - barrier_settings.dualize = settings.dualize; - barrier_settings.ordering = settings.ordering; - barrier_settings.barrier_dual_initial_point = settings.barrier_dual_initial_point; - barrier_settings.barrier = true; - barrier_settings.crossover = settings.crossover; - barrier_settings.eliminate_dense_columns = settings.eliminate_dense_columns; - barrier_settings.cudss_deterministic = settings.cudss_deterministic; + barrier_settings.num_gpus = settings.num_gpus; + barrier_settings.time_limit = settings.time_limit; + barrier_settings.iteration_limit = settings.iteration_limit; + barrier_settings.concurrent_halt = settings.concurrent_halt; + barrier_settings.folding = settings.folding; + barrier_settings.augmented = settings.augmented; + barrier_settings.dualize = settings.dualize; + barrier_settings.ordering = settings.ordering; + barrier_settings.barrier_dual_initial_point = settings.barrier_dual_initial_point; + barrier_settings.barrier = true; + barrier_settings.crossover = settings.crossover; + barrier_settings.eliminate_dense_columns = settings.eliminate_dense_columns; + barrier_settings.cudss_deterministic = settings.cudss_deterministic; + barrier_settings.barrier_relative_feasibility_tol = + settings.barrier_relative_feasibility_tolerance; + barrier_settings.barrier_relative_optimality_tol = settings.barrier_relative_optimality_tolerance; + barrier_settings.barrier_relative_complementarity_tol = + settings.barrier_relative_complementarity_tolerance; barrier_settings.barrier_relaxed_feasibility_tol = settings.tolerances.relative_primal_tolerance; barrier_settings.barrier_relaxed_optimality_tol = settings.tolerances.relative_dual_tolerance; barrier_settings.barrier_relaxed_complementarity_tol = settings.tolerances.relative_gap_tolerance; diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index d3a1755983..4e880a3947 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -219,7 +219,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( error_type_t::ValidationError, "Quadratic constraint '%s' Q CSR offsets[0] must be 0", qc.constraint_row_name.c_str()); - // This is the index of the auxiliary variable for the linear part of the quadratic constraint. + // This is the index of the auxiliary variable for the linear part of the quadratic + // constraint. const i_t affine_head = qc_affine_heads[qc_i]; const bool has_linear_part = affine_head >= 0; if (has_linear_part) { @@ -236,11 +237,12 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( if (v > -tol && v < tol) { continue; } ++nonzero_terms; } - cuopt_expects(nonzero_terms > 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' has linear section but all linear coefficients are " - "zero", - qc.constraint_row_name.c_str()); + cuopt_expects( + nonzero_terms > 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' has linear section but all linear coefficients are " + "zero", + qc.constraint_row_name.c_str()); } // Verify Q as either: @@ -323,8 +325,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( tail_vars.push_back(pr.first); } - f_t uniform_s = f_t(0); - bool have_uniform_s = false; + f_t uniform_s = f_t(0); + bool have_uniform_s = false; auto note_positive_s = [&](f_t v) { cuopt_expects(v > tol, error_type_t::ValidationError, @@ -333,7 +335,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( qc.constraint_row_name.c_str(), static_cast(v)); if (!have_uniform_s) { - uniform_s = v; + uniform_s = v; have_uniform_s = true; } else { cuopt_expects( @@ -355,11 +357,12 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( if (offdiag_entries.empty()) { if (!has_linear_part) { if (pos_diag_rows.empty()) { - cuopt_expects(neg_diag_rows.size() == 1 && q_nnz == 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected tail diagonals +s with head -s, " - "or a single head row with q_nnz=1", - qc.constraint_row_name.c_str()); + cuopt_expects( + neg_diag_rows.size() == 1 && q_nnz == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected tail diagonals +s with head -s, " + "or a single head row with q_nnz=1", + qc.constraint_row_name.c_str()); const f_t neg_v = neg_diag_rows[0].second; cuopt_expects(neg_v < -tol, error_type_t::ValidationError, @@ -367,7 +370,7 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( "(%.17g)", qc.constraint_row_name.c_str(), static_cast(neg_v)); - uniform_s = -neg_v; + uniform_s = -neg_v; have_uniform_s = true; head = neg_diag_rows[0].first; cuopt_expects( @@ -385,10 +388,11 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( for (const auto& pr : pos_diag_rows) { note_positive_s(pr.second); } - cuopt_expects(have_uniform_s, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: could not infer uniform positive scale s", - qc.constraint_row_name.c_str()); + cuopt_expects( + have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: could not infer uniform positive scale s", + qc.constraint_row_name.c_str()); cuopt_expects( neg_diag_rows.size() == 1, error_type_t::ValidationError, @@ -421,11 +425,12 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( is_rotated = static_cast(0); } } else { - cuopt_expects(neg_diag_rows.empty(), - error_type_t::ValidationError, - "Quadratic constraint '%s' with linear terms cannot contain negative diagonal " - "Q entries", - qc.constraint_row_name.c_str()); + cuopt_expects( + neg_diag_rows.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms cannot contain negative diagonal " + "Q entries", + qc.constraint_row_name.c_str()); cuopt_expects(affine_head >= 0, error_type_t::ValidationError, "Quadratic constraint '%s' internal error: affine SOC head index invalid", @@ -556,12 +561,11 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( rotated_cones.push_back(rotated_soc_t{a, b, tail_vars, false, head_lift_sqrt_ratio}); } - cuopt_expects( - have_uniform_s && uniform_s > tol, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: uniform scale s must be positive (got %.17g)", - qc.constraint_row_name.c_str(), - static_cast(uniform_s)); + cuopt_expects(have_uniform_s && uniform_s > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: uniform scale s must be positive (got %.17g)", + qc.constraint_row_name.c_str(), + static_cast(uniform_s)); qc_soc_uniform_scale[qc_i] = uniform_s; for (const i_t var : cone) { @@ -607,7 +611,9 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( user_problem.rhs.resize(static_cast(m_aug)); user_problem.row_sense.resize(static_cast(m_aug)); - if (!user_problem.row_names.empty()) { user_problem.row_names.resize(static_cast(m_aug)); } + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_aug)); + } csr_A.n = n_aug; dual_simplex::sparse_vector_t eq_row; @@ -682,8 +688,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( // Lift each rotated cone into standard SOC coordinates with two slacks: // With x_i' = sqrt(d/s)*x_hi, canonical s0 = (x_0'+x_1')/sqrt(2), s1 = (x_0'-x_1')/sqrt(2) - // so 2*d*x_h0*x_h1 >= s*sum tail^2 <=> 2*x_0'*x_1' >= sum (x_tail)^2 => s0^2 >= s1^2 + ... - // Only the rotated heads are replaced by slacks; tails stay as original variables. + // so 2*d*x_h0*x_h1 >= s*sum tail^2 <=> 2*x_0'*x_1' >= sum (x_tail)^2 => s0^2 >= s1^2 + + // ... Only the rotated heads are replaced by slacks; tails stay as original variables. i_t n_slack_total = 0; for (size_t ci = 0; ci < cone_is_rotated.size(); ++ci) { if (cone_is_rotated[ci]) { n_slack_total += 2; } @@ -802,7 +808,6 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( cuopt_expects(csr_A.m == m_old + n_slack_total, error_type_t::RuntimeError, "Internal error: CSR row count after rotated SOC lift"); - } // If a variable appears in multiple cones, create per-cone aliases and add linking rows @@ -839,19 +844,25 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( const i_t m_new = static_cast(m_old + cone_alias_pairs.size()); user_problem.objective.resize(static_cast(n_new), f_t(0)); - user_problem.lower.resize(static_cast(n_new), -std::numeric_limits::infinity()); + user_problem.lower.resize(static_cast(n_new), + -std::numeric_limits::infinity()); user_problem.upper.resize(static_cast(n_new), std::numeric_limits::infinity()); user_problem.var_types.resize( static_cast(n_new), cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); - if (!user_problem.col_names.empty()) { user_problem.col_names.resize(static_cast(n_new)); } + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_new)); + } for (const auto& [alias, original] : cone_alias_pairs) { - user_problem.lower[static_cast(alias)] = user_problem.lower[static_cast(original)]; - user_problem.upper[static_cast(alias)] = user_problem.upper[static_cast(original)]; + user_problem.lower[static_cast(alias)] = + user_problem.lower[static_cast(original)]; + user_problem.upper[static_cast(alias)] = + user_problem.upper[static_cast(original)]; user_problem.var_types[static_cast(alias)] = user_problem.var_types[static_cast(original)]; - // Keep objective unchanged: alias coefficient stays zero and alias==original links values. + // Keep objective unchanged: alias coefficient stays zero and alias==original links + // values. if (!user_problem.col_names.empty()) { user_problem.col_names[static_cast(alias)] = "_CUOPT_cone_alias_" + std::to_string(static_cast(alias - n_old)); @@ -860,11 +871,13 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( user_problem.rhs.resize(static_cast(m_new)); user_problem.row_sense.resize(static_cast(m_new)); - if (!user_problem.row_names.empty()) { user_problem.row_names.resize(static_cast(m_new)); } + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_new)); + } csr_A.n = n_new; dual_simplex::sparse_vector_t eq_row; - eq_row.n = n_new; + eq_row.n = n_new; i_t row_idx = m_old; for (const auto& [alias, original] : cone_alias_pairs) { eq_row.i = {alias, original}; From 36d3373590193d87b4996d1c11b61422f810ad3b Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 11 May 2026 04:37:40 -0700 Subject: [PATCH 33/68] clean up translation for quadratic constraints --- cpp/src/barrier/translate_soc.hpp | 960 ++++++++++++++++++++++++++++++ cpp/src/pdlp/translate.hpp | 926 +--------------------------- 2 files changed, 964 insertions(+), 922 deletions(-) create mode 100644 cpp/src/barrier/translate_soc.hpp diff --git a/cpp/src/barrier/translate_soc.hpp b/cpp/src/barrier/translate_soc.hpp new file mode 100644 index 0000000000..cd7481850e --- /dev/null +++ b/cpp/src/barrier/translate_soc.hpp @@ -0,0 +1,960 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +/** + * @brief Expand QCMATRIX second-order cone (and rotated / affine variants) into the + * canonical slack form expected by the simplex/PDLP path: extra variables, equality + * rows, optional cone aliases, column permutation, and `user_problem` cone metadata. + * + * Preconditions: `csr_A` and `user_problem` already reflect the linear model for `n` variables + * and original rows; this routine augments dimensions and CSR row storage in place. + */ +template +void apply_soc_qcmatrix_conversion_for_simplex( + int n, + const std::vector::quadratic_constraint_t>& + qcs, + dual_simplex::csr_matrix_t& csr_A, + dual_simplex::user_problem_t& user_problem) +{ + cuopt_expects(!qcs.empty(), + error_type_t::ValidationError, + "Quadratic-constraint flag is set, but no constraints were provided"); + + // Use a practical tolerance for text-parsed MPS numeric values. + const f_t tol = std::numeric_limits::epsilon() * 2; + + // SOC conversion accepts: + // 1) diagonal Lorentz-form QCMATRIX rows: + // -s*x_head^2 + sum_i s*x_tail_i^2 <= 0 (any common s > 0; divide by s to normalize) + // 2) rotated SOC rows: + // -2*d*x_head0*x_head1 + sum_i s*x_tail_i^2 <= 0 (d>0, s>0; canonical d=s) + // symmetric Q off-diagonals (-d,-d) give x^T Q x cross term -2*d*x0*x1, i.e. a*x0*x1 + // in the inequality 2*d*x0*x1 >= s*||tail||^2 with a = 2*d. Lift uses sqrt(d/s) on heads. + // 3) quadratic rows with linear part: + // sum_i s*x_tail_i^2 + a^T x <= 0 + // represented as diagonal +s QCMATRIX entries plus linear terms in COLUMNS. + // We introduce an auxiliary t = -(1/s)*a^T x so the row becomes: + // sum_i x_tail_i^2 - t <= 0 + // then lift it as rotated SOC with implicit second head fixed at 1/2. + // The barrier consumes SOCs as trailing variable blocks [head, tails...], so we validate all + // QCMATRIX blocks first, convert rotated cones via slack variables in standard SOC coordinates, + // then apply a single column permutation to the linear model. + struct rotated_soc_t { + i_t head0{}; + i_t head1{}; + std::vector tails{}; + bool head1_is_constant_half{false}; + /// For two-head rotated SOC: sqrt(d/s) where Q_off = -d and tail diagonals +s (canonical 1). + f_t head_lift_sqrt_ratio{f_t(1)}; + }; + // This is the index of the auxiliary variable for the linear part of the quadratic constraint. + std::vector qc_affine_heads(qcs.size(), static_cast(-1)); + i_t n_affine_linear_aux = 0; + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + if (!qcs[qc_i].linear_values.empty()) { + qc_affine_heads[qc_i] = static_cast(n + n_affine_linear_aux); + ++n_affine_linear_aux; + } + } + + const i_t n_with_affine_aux = static_cast(n + n_affine_linear_aux); + + std::vector> cone_vars; + std::vector cone_dims; + std::vector cone_is_rotated; + std::vector rotated_cones; + std::vector is_cone_var(static_cast(n_with_affine_aux), 0); + cone_vars.reserve(qcs.size()); + cone_dims.reserve(qcs.size()); + cone_is_rotated.reserve(qcs.size()); + rotated_cones.reserve(qcs.size()); + std::vector qc_soc_uniform_scale(qcs.size(), f_t(1)); + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const auto& qc = qcs[qc_i]; + cuopt_expects(qc.constraint_row_type == 'L', + error_type_t::ValidationError, + "Only <= quadratic constraints are supported for SOC conversion"); + cuopt_expects(qc.rhs_value < tol && qc.rhs_value > -tol, + error_type_t::ValidationError, + "SOC conversion currently requires rhs = 0 for quadratic constraints"); + cuopt_expects(qc.linear_values.size() == qc.linear_indices.size(), + error_type_t::ValidationError, + "Quadratic constraint '%s' linear_values and linear_indices length mismatch", + qc.constraint_row_name.c_str()); + + cuopt_expects(qc.quadratic_offsets.size() >= 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' has invalid CSR offsets (need at least 2 entries)", + qc.constraint_row_name.c_str()); + cuopt_expects(qc.quadratic_values.size() == qc.quadratic_indices.size(), + error_type_t::ValidationError, + "Quadratic constraint '%s' quadratic_values and quadratic_indices length " + "mismatch for CSR Q", + qc.constraint_row_name.c_str()); + + const i_t q_nnz = static_cast(qc.quadratic_values.size()); + cuopt_expects(q_nnz >= 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC must have at least 1 entry in Q (nnz %d)", + qc.constraint_row_name.c_str(), + static_cast(q_nnz)); + + cuopt_expects( + qc.quadratic_offsets.size() == static_cast(n) + 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q must be n by n in CSR: expected %zu CSR row pointers (offsets " + "length n+1), got %zu (n = %d)", + qc.constraint_row_name.c_str(), + static_cast(n) + 1, + qc.quadratic_offsets.size(), + static_cast(n)); + cuopt_expects( + qc.quadratic_offsets[static_cast(n)] == q_nnz, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q last CSR offset %d must equal number of nonzeros (nnz) %d", + qc.constraint_row_name.c_str(), + static_cast(qc.quadratic_offsets[static_cast(n)]), + static_cast(q_nnz)); + cuopt_expects(qc.quadratic_offsets[0] == 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q CSR offsets[0] must be 0", + qc.constraint_row_name.c_str()); + // This is the index of the auxiliary variable for the linear part of the quadratic + // constraint. + const i_t affine_head = qc_affine_heads[qc_i]; + const bool has_linear_part = affine_head >= 0; + if (has_linear_part) { + size_t nonzero_terms = 0; + for (size_t p = 0; p < qc.linear_values.size(); ++p) { + const i_t idx = qc.linear_indices[p]; + const f_t v = qc.linear_values[p]; + cuopt_expects(idx >= 0 && idx < n, + error_type_t::ValidationError, + "Quadratic constraint '%s' linear index %d is outside [0, %d)", + qc.constraint_row_name.c_str(), + static_cast(idx), + static_cast(n)); + if (v > -tol && v < tol) { continue; } + ++nonzero_terms; + } + cuopt_expects(nonzero_terms > 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' has linear section but all linear coefficients are " + "zero", + qc.constraint_row_name.c_str()); + } + + // Verify Q as either: + // - standard SOC: one diagonal -s (head), tail diagonals +s for a common s > 0, + // - rotated SOC: symmetric (-s,-s) off-diagonal pair on the two heads, tails +s, + // - affine SOC: tail diagonals +s and linear terms (no Q off-diagonals). + // Feasibility is unchanged after dividing the quadratic row by s; affine rows also scale + // linear coefficients when forming the auxiliary t = -(1/s) a^T x. + + auto approx_eq_scaled = [&](f_t a, f_t b) { + const f_t scale = std::max({f_t(1), std::abs(a), std::abs(b)}); + return std::abs(a - b) <= tol * scale; + }; + + std::vector> q_entries; + q_entries.reserve(static_cast(q_nnz)); + for (i_t r = 0; r < n; ++r) { + const i_t p_beg = qc.quadratic_offsets[static_cast(r)]; + const i_t p_end = qc.quadratic_offsets[static_cast(r + 1)]; + cuopt_expects(p_beg >= 0 && p_beg <= p_end && p_end <= q_nnz, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d has invalid CSR offsets [%d, %d)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(p_beg), + static_cast(p_end)); + + if (p_beg == p_end) { continue; } + + cuopt_expects(p_beg + 1 == p_end, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: expected at most one stored entry per " + "row (got end - beg = %d)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(p_end - p_beg)); + + const i_t col = qc.quadratic_indices[static_cast(p_beg)]; + const f_t v = qc.quadratic_values[static_cast(p_beg)]; + q_entries.emplace_back(r, col, v); + } + cuopt_expects(static_cast(q_entries.size()) == q_nnz, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row nnz mismatch (expected %d stored entries, got " + "%zu)", + qc.constraint_row_name.c_str(), + static_cast(q_nnz), + q_entries.size()); + + std::vector> pos_diag_rows; + std::vector> neg_diag_rows; + std::vector> offdiag_entries; + pos_diag_rows.reserve(q_entries.size()); + neg_diag_rows.reserve(1); + offdiag_entries.reserve(4); + + for (const auto& [r, c, v] : q_entries) { + if (r == c) { + if (v > tol) { + pos_diag_rows.emplace_back(r, v); + } else if (v < -tol) { + neg_diag_rows.emplace_back(r, v); + } else { + cuopt_expects(false, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: diagonal SOC entry is near zero " + "(%.17g)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(v)); + } + } else { + offdiag_entries.emplace_back(r, c, v); + } + } + + std::vector tail_vars; + tail_vars.reserve(pos_diag_rows.size()); + for (const auto& pr : pos_diag_rows) { + tail_vars.push_back(pr.first); + } + + f_t uniform_s = f_t(0); + bool have_uniform_s = false; + auto note_positive_s = [&](f_t v) { + cuopt_expects(v > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected strictly positive diagonal tail " + "coefficient, got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v)); + if (!have_uniform_s) { + uniform_s = v; + have_uniform_s = true; + } else { + cuopt_expects( + approx_eq_scaled(v, uniform_s), + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: all positive diagonal coefficients must match; got " + "%.17g vs %.17g", + qc.constraint_row_name.c_str(), + static_cast(v), + static_cast(uniform_s)); + } + }; + + std::vector cone; + i_t cone_dim = 0; + char is_rotated = 0; + i_t head = static_cast(-1); + + if (offdiag_entries.empty()) { + if (!has_linear_part) { + if (pos_diag_rows.empty()) { + cuopt_expects(neg_diag_rows.size() == 1 && q_nnz == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected tail diagonals +s with head -s, " + "or a single head row with q_nnz=1", + qc.constraint_row_name.c_str()); + const f_t neg_v = neg_diag_rows[0].second; + cuopt_expects(neg_v < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: cone head diagonal must be negative " + "(%.17g)", + qc.constraint_row_name.c_str(), + static_cast(neg_v)); + uniform_s = -neg_v; + have_uniform_s = true; + head = neg_diag_rows[0].first; + cuopt_expects( + static_cast(tail_vars.size()) == q_nnz - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 1), + tail_vars.size()); + cone.reserve(1); + cone.push_back(head); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(0); + } else { + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: could not infer uniform positive scale s", + qc.constraint_row_name.c_str()); + cuopt_expects( + neg_diag_rows.size() == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected exactly one diagonal -s (cone head) for " + "%zu tail entries, found %zu negative diagonals", + qc.constraint_row_name.c_str(), + tail_vars.size(), + neg_diag_rows.size()); + cuopt_expects( + static_cast(tail_vars.size()) == q_nnz - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 1), + tail_vars.size()); + const f_t neg_v = neg_diag_rows[0].second; + cuopt_expects( + approx_eq_scaled(neg_v, -uniform_s), + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: cone head diagonal must be -s with the same s as " + "positive tail diagonals; head %.17g vs -s = %.17g", + qc.constraint_row_name.c_str(), + static_cast(neg_v), + static_cast(-uniform_s)); + head = neg_diag_rows[0].first; + cone.reserve(static_cast(q_nnz)); + cone.push_back(head); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(0); + } + } else { + cuopt_expects( + neg_diag_rows.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms cannot contain negative diagonal " + "Q entries", + qc.constraint_row_name.c_str()); + cuopt_expects(affine_head >= 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' internal error: affine SOC head index invalid", + qc.constraint_row_name.c_str()); + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms must have at least one " + "diagonal +s term in Q", + qc.constraint_row_name.c_str()); + cuopt_expects(!tail_vars.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms must have at least one " + "diagonal +s term in Q", + qc.constraint_row_name.c_str()); + for (const i_t tail : tail_vars) { + cuopt_expects( + tail != affine_head, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms requires the linear head variable to be " + "distinct from quadratic diagonal variables", + qc.constraint_row_name.c_str()); + } + + cone.reserve(tail_vars.size() + 1); + cone.push_back(affine_head); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(tail_vars.size() + 2); + is_rotated = static_cast(1); + rotated_cones.push_back( + rotated_soc_t{affine_head, static_cast(-1), tail_vars, true, f_t(1)}); + } + } else { + cuopt_expects(!has_linear_part, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms cannot include rotated-SOC " + "off-diagonal entries", + qc.constraint_row_name.c_str()); + cuopt_expects(neg_diag_rows.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q cannot contain diagonal head " + "entries; found %zu negative diagonals", + qc.constraint_row_name.c_str(), + neg_diag_rows.size()); + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q: could not infer uniform scale s", + qc.constraint_row_name.c_str()); + cuopt_expects( + offdiag_entries.size() == 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must contain exactly one symmetric off-diagonal " + "pair (-d,-d); found %zu off-diagonal entries", + qc.constraint_row_name.c_str(), + offdiag_entries.size()); + + const i_t a = std::get<0>(offdiag_entries[0]); + const i_t b = std::get<1>(offdiag_entries[0]); + const f_t v0 = std::get<2>(offdiag_entries[0]); + cuopt_expects( + v0 < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v0)); + cuopt_expects(a != b, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal pair must use distinct " + "variables", + qc.constraint_row_name.c_str()); + cuopt_expects(std::get<0>(offdiag_entries[1]) == b && std::get<1>(offdiag_entries[1]) == a, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must have symmetric entries (a,b) " + "and (b,a) with the same value", + qc.constraint_row_name.c_str()); + const f_t v1 = std::get<2>(offdiag_entries[1]); + cuopt_expects( + v1 < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v1)); + cuopt_expects( + approx_eq_scaled(v0, v1), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q symmetric off-diagonals must match; got %.17g " + "and %.17g", + qc.constraint_row_name.c_str(), + static_cast(v0), + static_cast(v1)); + const f_t cross_d = -v0; + cuopt_expects( + cross_d > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q cross coefficient d = -Q_off must be positive", + qc.constraint_row_name.c_str()); + const f_t head_lift_sqrt_ratio = std::sqrt(cross_d / uniform_s); + cuopt_expects(std::isfinite(static_cast(head_lift_sqrt_ratio)), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q head lift ratio sqrt(d/s) is not " + "finite (d=%.17g, s=%.17g)", + qc.constraint_row_name.c_str(), + static_cast(cross_d), + static_cast(uniform_s)); + cuopt_expects(static_cast(tail_vars.size()) == q_nnz - 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q: expected %d diagonal +s entries " + "(tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 2), + tail_vars.size()); + cuopt_expects(q_nnz >= 3, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must have at least 1 tail entry", + qc.constraint_row_name.c_str()); + + cone.reserve(static_cast(q_nnz)); + cone.push_back(a); + cone.push_back(b); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(1); + rotated_cones.push_back(rotated_soc_t{a, b, tail_vars, false, head_lift_sqrt_ratio}); + } + + cuopt_expects(have_uniform_s && uniform_s > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: uniform scale s must be positive (got %.17g)", + qc.constraint_row_name.c_str(), + static_cast(uniform_s)); + qc_soc_uniform_scale[qc_i] = uniform_s; + + for (const i_t var : cone) { + cuopt_expects(var >= 0 && var < static_cast(is_cone_var.size()), + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(is_cone_var.size())); + } + cone_dims.push_back(cone_dim); + cone_vars.push_back(std::move(cone)); + cone_is_rotated.push_back(is_rotated); + } + // Add affine linear auxiliary variables and linking rows. + if (n_affine_linear_aux > 0) { + const f_t inf = std::numeric_limits::infinity(); + const i_t n_old = static_cast(n); + const i_t n_aug = n_with_affine_aux; + const i_t m_old = csr_A.m; + const i_t m_aug = static_cast(m_old + n_affine_linear_aux); + i_t row_write_cursor = m_old; + + user_problem.objective.resize(static_cast(n_aug), f_t(0)); + user_problem.lower.resize(static_cast(n_aug), -inf); + user_problem.upper.resize(static_cast(n_aug), inf); + user_problem.var_types.resize( + static_cast(n_aug), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_aug)); + } + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const i_t aux_j = qc_affine_heads[qc_i]; + if (aux_j < 0) { continue; } + user_problem.lower[static_cast(aux_j)] = f_t(0); + user_problem.upper[static_cast(aux_j)] = inf; + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(aux_j)] = + "_CUOPT_qc_linear_aux_" + std::to_string(static_cast(aux_j - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_aug)); + user_problem.row_sense.resize(static_cast(m_aug)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_aug)); + } + + csr_A.n = n_aug; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_aug; + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const i_t aux_j = qc_affine_heads[qc_i]; + if (aux_j < 0) { continue; } + const auto& qc = qcs[qc_i]; + eq_row.i.clear(); + eq_row.x.clear(); + // Define auxiliary as t = -(1/s) a^T x so QC linear part matches normalized cone row. + const f_t inv_s = f_t(1) / qc_soc_uniform_scale[qc_i]; + eq_row.i.push_back(aux_j); + eq_row.x.push_back(f_t(1)); + for (size_t p = 0; p < qc.linear_values.size(); ++p) { + const f_t v = qc.linear_values[p]; + if (v > -tol && v < tol) { continue; } + eq_row.i.push_back(qc.linear_indices[p]); + eq_row.x.push_back(v * inv_s); + } + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_write_cursor)] = 'E'; + user_problem.rhs[static_cast(row_write_cursor)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_write_cursor)] = + "_CUOPT_qc_linear_link_" + qc.constraint_row_name; + } + ++row_write_cursor; + } + + cuopt_expects(row_write_cursor == m_aug, + error_type_t::RuntimeError, + "Internal error: affine QC linking row count mismatch"); + cuopt_expects(csr_A.m == m_aug, + error_type_t::RuntimeError, + "Internal error: CSR row count after affine QC linking"); + } + + i_t n_prob = n_with_affine_aux; + + // Convert rotated SOC cones to standard SOC cones. + if (!rotated_cones.empty()) { + cuopt_expects(user_problem.Q_values.empty(), + error_type_t::ValidationError, + "Rotated SOC conversion is currently not supported when the objective has " + "quadratic terms"); + + const f_t inf = std::numeric_limits::infinity(); + const f_t inv_sqrt_2 = f_t(1) / std::sqrt(f_t(2)); + const f_t half = f_t(0.5); + + for (const auto& rc : rotated_cones) { + cuopt_expects(user_problem.var_types[static_cast(rc.head0)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC head variables must be continuous"); + if (!rc.head1_is_constant_half) { + cuopt_expects(user_problem.var_types[static_cast(rc.head1)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC head variables must be continuous"); + } + for (const i_t t : rc.tails) { + cuopt_expects(user_problem.var_types[static_cast(t)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC tail variables must be continuous"); + } + } + + // Lift each rotated cone into standard SOC coordinates with two slacks: + // With x_i' = sqrt(d/s)*x_hi, canonical s0 = (x_0'+x_1')/sqrt(2), s1 = (x_0'-x_1')/sqrt(2) + // so 2*d*x_h0*x_h1 >= s*sum tail^2 <=> 2*x_0'*x_1' >= sum (x_tail)^2 => s0^2 >= s1^2 + + // ... Only the rotated heads are replaced by slacks; tails stay as original variables. + i_t n_slack_total = 0; + for (size_t ci = 0; ci < cone_is_rotated.size(); ++ci) { + if (cone_is_rotated[ci]) { n_slack_total += 2; } + } + + const i_t n_old = n_prob; + n_prob = static_cast(n_old + n_slack_total); + + user_problem.objective.resize(static_cast(n_prob), f_t(0)); + user_problem.lower.resize(static_cast(n_prob), -inf); + user_problem.upper.resize(static_cast(n_prob), inf); + user_problem.var_types.resize( + static_cast(n_prob), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_prob)); + for (i_t j = n_old; j < n_prob; ++j) { + user_problem.col_names[static_cast(j)] = + "_CUOPT_rsoc_slack_" + std::to_string(static_cast(j - n_old)); + } + } + + is_cone_var.resize(static_cast(n_prob), 0); + + const i_t m_old = csr_A.m; + user_problem.rhs.resize(static_cast(m_old + n_slack_total)); + user_problem.row_sense.resize(static_cast(m_old + n_slack_total)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_old + n_slack_total)); + for (i_t r = m_old; r < m_old + n_slack_total; ++r) { + user_problem.row_names[static_cast(r)] = + "_CUOPT_rsoc_lift_" + std::to_string(static_cast(r - m_old)); + } + } + + csr_A.n = n_prob; + + dual_simplex::sparse_vector_t eq_row; + size_t ri = 0; + i_t slack_base = n_old; + i_t row_idx = m_old; + + for (size_t ci = 0; ci < cone_vars.size(); ++ci) { + if (!cone_is_rotated[ci]) { continue; } + const auto& rc = rotated_cones[ri++]; + const i_t dim = cone_dims[ci]; + std::vector new_cone; + new_cone.reserve(static_cast(dim)); + new_cone.push_back(slack_base); + new_cone.push_back(slack_base + 1); + new_cone.insert(new_cone.end(), rc.tails.begin(), rc.tails.end()); + cone_vars[ci] = std::move(new_cone); + + is_cone_var[static_cast(slack_base)] = 1; + is_cone_var[static_cast(slack_base + 1)] = 1; + + eq_row.n = n_prob; + // If the second head is not constant half, we need to lift it. + if (!rc.head1_is_constant_half) { + const f_t h = inv_sqrt_2 * rc.head_lift_sqrt_ratio; + // s_0 - h * x_h0 - h * x_h1 = 0 (h = inv_sqrt_2 * sqrt(d/s)) + eq_row.i = {rc.head0, rc.head1, slack_base}; + eq_row.x = {-h, -h, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + // s_1 - h * x_h0 + h * x_h1 = 0 + eq_row.i = {rc.head0, rc.head1, slack_base + 1}; + eq_row.x = {-h, h, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + is_cone_var[static_cast(rc.head0)] = 0; + is_cone_var[static_cast(rc.head1)] = 0; + } else { + // One head is constant half, so we can lift it directly. + // s_0 - inv_sqrt_2 * x_h0 = inv_sqrt_2 * (1/2) + eq_row.i = {rc.head0, slack_base}; + eq_row.x = {-inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = inv_sqrt_2 * half; + ++row_idx; + + // s_1 - inv_sqrt_2 * x_h0 = -inv_sqrt_2 * (1/2) + eq_row.i = {rc.head0, slack_base + 1}; + eq_row.x = {-inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = -inv_sqrt_2 * half; + ++row_idx; + + is_cone_var[static_cast(rc.head0)] = 0; + } + + slack_base += 2; + } + + cuopt_expects(ri == rotated_cones.size(), + error_type_t::RuntimeError, + "Internal error: rotated SOC cone metadata mismatch"); + cuopt_expects(slack_base == n_prob, + error_type_t::RuntimeError, + "Internal error: slack variable count mismatch"); + cuopt_expects(row_idx == m_old + n_slack_total, + error_type_t::RuntimeError, + "Internal error: rotated SOC equality row count mismatch"); + cuopt_expects(csr_A.m == m_old + n_slack_total, + error_type_t::RuntimeError, + "Internal error: CSR row count after rotated SOC lift"); + } + + // If a variable appears in multiple cones, create per-cone aliases and add linking rows + // alias - original = 0 so cone variable blocks are disjoint. + { + std::vector first_owner(static_cast(n_prob), static_cast(-1)); + std::vector> cone_alias_pairs; // (alias, original) + + for (size_t ci = 0; ci < cone_vars.size(); ++ci) { + auto& cone = cone_vars[ci]; + for (auto& var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(n_prob)); + const auto idx = static_cast(var); + if (first_owner[idx] == static_cast(-1)) { + first_owner[idx] = static_cast(ci); + continue; + } + if (first_owner[idx] != static_cast(ci)) { + const i_t alias = static_cast(n_prob + cone_alias_pairs.size()); + cone_alias_pairs.emplace_back(alias, var); + var = alias; + } + } + } + + if (!cone_alias_pairs.empty()) { + const i_t n_old = n_prob; + const i_t n_new = static_cast(n_old + cone_alias_pairs.size()); + const i_t m_old = csr_A.m; + const i_t m_new = static_cast(m_old + cone_alias_pairs.size()); + + user_problem.objective.resize(static_cast(n_new), f_t(0)); + user_problem.lower.resize(static_cast(n_new), -std::numeric_limits::infinity()); + user_problem.upper.resize(static_cast(n_new), std::numeric_limits::infinity()); + user_problem.var_types.resize( + static_cast(n_new), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_new)); + } + + for (const auto& [alias, original] : cone_alias_pairs) { + user_problem.lower[static_cast(alias)] = + user_problem.lower[static_cast(original)]; + user_problem.upper[static_cast(alias)] = + user_problem.upper[static_cast(original)]; + user_problem.var_types[static_cast(alias)] = + user_problem.var_types[static_cast(original)]; + // Keep objective unchanged: alias coefficient stays zero and alias==original links + // values. + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(alias)] = + "_CUOPT_cone_alias_" + std::to_string(static_cast(alias - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_new)); + user_problem.row_sense.resize(static_cast(m_new)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_new)); + } + + csr_A.n = n_new; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_new; + i_t row_idx = m_old; + for (const auto& [alias, original] : cone_alias_pairs) { + eq_row.i = {alias, original}; + eq_row.x = {f_t(1), f_t(-1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_idx)] = + "_CUOPT_cone_alias_link_" + std::to_string(static_cast(row_idx - m_old)); + } + ++row_idx; + } + + cuopt_expects(row_idx == m_new, + error_type_t::RuntimeError, + "Internal error: cone alias linking row count mismatch"); + cuopt_expects(csr_A.m == m_new, + error_type_t::RuntimeError, + "Internal error: CSR row count after cone alias linking"); + + n_prob = n_new; + } + } + + is_cone_var.assign(static_cast(n_prob), 0); + for (const auto& cone : cone_vars) { + for (const i_t var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d) after cone aliasing", + static_cast(var), + static_cast(n_prob)); + is_cone_var[static_cast(var)] = 1; + } + } + + std::vector old_to_new(static_cast(n_prob), i_t{-1}); + std::vector new_to_old; + new_to_old.reserve(static_cast(n_prob)); + for (i_t j = 0; j < n_prob; ++j) { + if (is_cone_var[static_cast(j)]) { continue; } + old_to_new[static_cast(j)] = static_cast(new_to_old.size()); + new_to_old.push_back(j); + } + const i_t cone_var_start = static_cast(new_to_old.size()); + for (const auto& cone : cone_vars) { + for (const i_t old_j : cone) { + old_to_new[static_cast(old_j)] = static_cast(new_to_old.size()); + new_to_old.push_back(old_j); + } + } + cuopt_expects(static_cast(new_to_old.size()) == n_prob, + error_type_t::RuntimeError, + "Internal error while building SOC variable permutation"); + + for (i_t row = 0; row < csr_A.m; ++row) { + for (i_t p = csr_A.row_start[static_cast(row)]; + p < csr_A.row_start[static_cast(row + 1)]; + ++p) { + const i_t old_j = csr_A.j[static_cast(p)]; + cuopt_expects(old_j >= 0 && old_j < n_prob, + error_type_t::ValidationError, + "Linear constraint matrix column index %d is outside [0, %d)", + static_cast(old_j), + static_cast(n_prob)); + csr_A.j[static_cast(p)] = old_to_new[static_cast(old_j)]; + } + } + + auto permute_dense_by_old_to_new = [&](auto& values, const char* name) { + if (values.empty()) { return; } + using value_t = typename std::decay_t::value_type; + cuopt_expects(values.size() == static_cast(n_prob), + error_type_t::ValidationError, + "%s length %zu does not match number of variables %d", + name, + values.size(), + static_cast(n_prob)); + std::vector permuted(values.size()); + for (i_t old_j = 0; old_j < n_prob; ++old_j) { + permuted[static_cast(old_to_new[static_cast(old_j)])] = + std::move(values[static_cast(old_j)]); + } + values = std::move(permuted); + }; + + permute_dense_by_old_to_new(user_problem.objective, "objective"); + permute_dense_by_old_to_new(user_problem.lower, "lower bounds"); + permute_dense_by_old_to_new(user_problem.upper, "upper bounds"); + permute_dense_by_old_to_new(user_problem.var_types, "variable types"); + permute_dense_by_old_to_new(user_problem.col_names, "column names"); + + if (!user_problem.Q_values.empty()) { + const i_t n_model = static_cast(n); + cuopt_expects(user_problem.Q_indices.size() == user_problem.Q_values.size(), + error_type_t::ValidationError, + "Quadratic objective indices and values length mismatch"); + cuopt_expects(user_problem.Q_offsets.size() == static_cast(n_model) + 1, + error_type_t::ValidationError, + "Quadratic objective CSR offsets length must be n+1 when SOC QCMATRIX " + "conversion permutes variables"); + cuopt_expects(user_problem.Q_offsets[0] == 0, + error_type_t::ValidationError, + "Quadratic objective CSR offsets[0] must be 0"); + cuopt_expects(user_problem.Q_offsets[static_cast(n_model)] == + static_cast(user_problem.Q_values.size()), + error_type_t::ValidationError, + "Quadratic objective CSR last offset must equal number of nonzeros"); + + std::vector q_offsets(static_cast(n_prob) + 1, 0); + for (i_t old_row = 0; old_row < n_model; ++old_row) { + const i_t p_beg = user_problem.Q_offsets[static_cast(old_row)]; + const i_t p_end = user_problem.Q_offsets[static_cast(old_row + 1)]; + cuopt_expects( + p_beg >= 0 && p_beg <= p_end && p_end <= static_cast(user_problem.Q_values.size()), + error_type_t::ValidationError, + "Quadratic objective CSR offsets are invalid at row %d", + static_cast(old_row)); + const i_t new_row = old_to_new[static_cast(old_row)]; + q_offsets[static_cast(new_row + 1)] = p_end - p_beg; + } + for (i_t row = 0; row < n_prob; ++row) { + q_offsets[static_cast(row + 1)] += q_offsets[static_cast(row)]; + } + + std::vector q_indices(user_problem.Q_values.size()); + std::vector q_values(user_problem.Q_values.size()); + auto q_write = q_offsets; + for (i_t old_row = 0; old_row < n_model; ++old_row) { + const i_t new_row = old_to_new[static_cast(old_row)]; + for (i_t p = user_problem.Q_offsets[static_cast(old_row)]; + p < user_problem.Q_offsets[static_cast(old_row + 1)]; + ++p) { + const i_t old_col = user_problem.Q_indices[static_cast(p)]; + cuopt_expects(old_col >= 0 && old_col < n_model, + error_type_t::ValidationError, + "Quadratic objective column index %d is outside [0, %d)", + static_cast(old_col), + static_cast(n_model)); + const i_t dst = q_write[static_cast(new_row)]++; + q_indices[static_cast(dst)] = old_to_new[static_cast(old_col)]; + q_values[static_cast(dst)] = user_problem.Q_values[static_cast(p)]; + } + } + + user_problem.Q_offsets = std::move(q_offsets); + user_problem.Q_indices = std::move(q_indices); + user_problem.Q_values = std::move(q_values); + } + + user_problem.cone_var_start = cone_var_start; + user_problem.second_order_cone_dims = std::move(cone_dims); + user_problem.num_rows = csr_A.m; + user_problem.num_cols = n_prob; +} + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 4e880a3947..146ceb6c95 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -14,13 +14,13 @@ #include #include +#include + #include #include -#include #include #include -#include #include #include @@ -114,926 +114,8 @@ static dual_simplex::user_problem_t cuopt_problem_to_simplex_problem( user_problem.Q_values = model.Q_values; if (model.original_problem_ptr->has_quadratic_constraints()) { - const auto& qcs = model.original_problem_ptr->get_quadratic_constraints(); - cuopt_expects(!qcs.empty(), - error_type_t::ValidationError, - "Quadratic-constraint flag is set, but no constraints were provided"); - - // Use a practical tolerance for text-parsed MPS numeric values. - const f_t tol = std::numeric_limits::epsilon() * 2; - - // SOC conversion accepts: - // 1) diagonal Lorentz-form QCMATRIX rows: - // -s*x_head^2 + sum_i s*x_tail_i^2 <= 0 (any common s > 0; divide by s to normalize) - // 2) rotated SOC rows: - // -2*d*x_head0*x_head1 + sum_i s*x_tail_i^2 <= 0 (d>0, s>0; canonical d=s) - // symmetric Q off-diagonals (-d,-d) give x^T Q x cross term -2*d*x0*x1, i.e. a*x0*x1 - // in the inequality 2*d*x0*x1 >= s*||tail||^2 with a = 2*d. Lift uses sqrt(d/s) on heads. - // 3) quadratic rows with linear part: - // sum_i s*x_tail_i^2 + a^T x <= 0 - // represented as diagonal +s QCMATRIX entries plus linear terms in COLUMNS. - // We introduce an auxiliary t = -(1/s)*a^T x so the row becomes: - // sum_i x_tail_i^2 - t <= 0 - // then lift it as rotated SOC with implicit second head fixed at 1/2. - // The barrier consumes SOCs as trailing variable blocks [head, tails...], so we validate all - // QCMATRIX blocks first, convert rotated cones via slack variables in standard SOC coordinates, - // then apply a single column permutation to the linear model. - struct rotated_soc_t { - i_t head0{}; - i_t head1{}; - std::vector tails{}; - bool head1_is_constant_half{false}; - /// For two-head rotated SOC: sqrt(d/s) where Q_off = -d and tail diagonals +s (canonical 1). - f_t head_lift_sqrt_ratio{f_t(1)}; - }; - // This is the index of the auxiliary variable for the linear part of the quadratic constraint. - std::vector qc_affine_heads(qcs.size(), static_cast(-1)); - i_t n_affine_linear_aux = 0; - for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { - if (!qcs[qc_i].linear_values.empty()) { - qc_affine_heads[qc_i] = static_cast(n + n_affine_linear_aux); - ++n_affine_linear_aux; - } - } - - const i_t n_with_affine_aux = static_cast(n + n_affine_linear_aux); - - std::vector> cone_vars; - std::vector cone_dims; - std::vector cone_is_rotated; - std::vector rotated_cones; - std::vector is_cone_var(static_cast(n_with_affine_aux), 0); - cone_vars.reserve(qcs.size()); - cone_dims.reserve(qcs.size()); - cone_is_rotated.reserve(qcs.size()); - rotated_cones.reserve(qcs.size()); - std::vector qc_soc_uniform_scale(qcs.size(), f_t(1)); - - for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { - const auto& qc = qcs[qc_i]; - cuopt_expects(qc.constraint_row_type == 'L', - error_type_t::ValidationError, - "Only <= quadratic constraints are supported for SOC conversion"); - cuopt_expects(qc.rhs_value < tol && qc.rhs_value > -tol, - error_type_t::ValidationError, - "SOC conversion currently requires rhs = 0 for quadratic constraints"); - cuopt_expects(qc.linear_values.size() == qc.linear_indices.size(), - error_type_t::ValidationError, - "Quadratic constraint '%s' linear_values and linear_indices length mismatch", - qc.constraint_row_name.c_str()); - - cuopt_expects(qc.quadratic_offsets.size() >= 2, - error_type_t::ValidationError, - "Quadratic constraint '%s' has invalid CSR offsets (need at least 2 entries)", - qc.constraint_row_name.c_str()); - cuopt_expects(qc.quadratic_values.size() == qc.quadratic_indices.size(), - error_type_t::ValidationError, - "Quadratic constraint '%s' quadratic_values and quadratic_indices length " - "mismatch for CSR Q", - qc.constraint_row_name.c_str()); - - const i_t q_nnz = static_cast(qc.quadratic_values.size()); - cuopt_expects(q_nnz >= 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC must have at least 1 entry in Q (nnz %d)", - qc.constraint_row_name.c_str(), - static_cast(q_nnz)); - - cuopt_expects( - qc.quadratic_offsets.size() == static_cast(n) + 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q must be n by n in CSR: expected %zu CSR row pointers (offsets " - "length n+1), got %zu (n = %d)", - qc.constraint_row_name.c_str(), - static_cast(n) + 1, - qc.quadratic_offsets.size(), - static_cast(n)); - cuopt_expects( - qc.quadratic_offsets[static_cast(n)] == q_nnz, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q last CSR offset %d must equal number of nonzeros (nnz) %d", - qc.constraint_row_name.c_str(), - static_cast(qc.quadratic_offsets[static_cast(n)]), - static_cast(q_nnz)); - cuopt_expects(qc.quadratic_offsets[0] == 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q CSR offsets[0] must be 0", - qc.constraint_row_name.c_str()); - // This is the index of the auxiliary variable for the linear part of the quadratic - // constraint. - const i_t affine_head = qc_affine_heads[qc_i]; - const bool has_linear_part = affine_head >= 0; - if (has_linear_part) { - size_t nonzero_terms = 0; - for (size_t p = 0; p < qc.linear_values.size(); ++p) { - const i_t idx = qc.linear_indices[p]; - const f_t v = qc.linear_values[p]; - cuopt_expects(idx >= 0 && idx < n, - error_type_t::ValidationError, - "Quadratic constraint '%s' linear index %d is outside [0, %d)", - qc.constraint_row_name.c_str(), - static_cast(idx), - static_cast(n)); - if (v > -tol && v < tol) { continue; } - ++nonzero_terms; - } - cuopt_expects( - nonzero_terms > 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' has linear section but all linear coefficients are " - "zero", - qc.constraint_row_name.c_str()); - } - - // Verify Q as either: - // - standard SOC: one diagonal -s (head), tail diagonals +s for a common s > 0, - // - rotated SOC: symmetric (-s,-s) off-diagonal pair on the two heads, tails +s, - // - affine SOC: tail diagonals +s and linear terms (no Q off-diagonals). - // Feasibility is unchanged after dividing the quadratic row by s; affine rows also scale - // linear coefficients when forming the auxiliary t = -(1/s) a^T x. - - auto approx_eq_scaled = [&](f_t a, f_t b) { - const f_t scale = std::max({f_t(1), std::abs(a), std::abs(b)}); - return std::abs(a - b) <= tol * scale; - }; - - std::vector> q_entries; - q_entries.reserve(static_cast(q_nnz)); - for (i_t r = 0; r < n; ++r) { - const i_t p_beg = qc.quadratic_offsets[static_cast(r)]; - const i_t p_end = qc.quadratic_offsets[static_cast(r + 1)]; - cuopt_expects(p_beg >= 0 && p_beg <= p_end && p_end <= q_nnz, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d has invalid CSR offsets [%d, %d)", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(p_beg), - static_cast(p_end)); - - if (p_beg == p_end) { continue; } - - cuopt_expects(p_beg + 1 == p_end, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: expected at most one stored entry per " - "row (got end - beg = %d)", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(p_end - p_beg)); - - const i_t col = qc.quadratic_indices[static_cast(p_beg)]; - const f_t v = qc.quadratic_values[static_cast(p_beg)]; - q_entries.emplace_back(r, col, v); - } - cuopt_expects(static_cast(q_entries.size()) == q_nnz, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row nnz mismatch (expected %d stored entries, got " - "%zu)", - qc.constraint_row_name.c_str(), - static_cast(q_nnz), - q_entries.size()); - - std::vector> pos_diag_rows; - std::vector> neg_diag_rows; - std::vector> offdiag_entries; - pos_diag_rows.reserve(q_entries.size()); - neg_diag_rows.reserve(1); - offdiag_entries.reserve(4); - - for (const auto& [r, c, v] : q_entries) { - if (r == c) { - if (v > tol) { - pos_diag_rows.emplace_back(r, v); - } else if (v < -tol) { - neg_diag_rows.emplace_back(r, v); - } else { - cuopt_expects(false, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: diagonal SOC entry is near zero " - "(%.17g)", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(v)); - } - } else { - offdiag_entries.emplace_back(r, c, v); - } - } - - std::vector tail_vars; - tail_vars.reserve(pos_diag_rows.size()); - for (const auto& pr : pos_diag_rows) { - tail_vars.push_back(pr.first); - } - - f_t uniform_s = f_t(0); - bool have_uniform_s = false; - auto note_positive_s = [&](f_t v) { - cuopt_expects(v > tol, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected strictly positive diagonal tail " - "coefficient, got %.17g", - qc.constraint_row_name.c_str(), - static_cast(v)); - if (!have_uniform_s) { - uniform_s = v; - have_uniform_s = true; - } else { - cuopt_expects( - approx_eq_scaled(v, uniform_s), - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: all positive diagonal coefficients must match; got " - "%.17g vs %.17g", - qc.constraint_row_name.c_str(), - static_cast(v), - static_cast(uniform_s)); - } - }; - - std::vector cone; - i_t cone_dim = 0; - char is_rotated = 0; - i_t head = static_cast(-1); - - if (offdiag_entries.empty()) { - if (!has_linear_part) { - if (pos_diag_rows.empty()) { - cuopt_expects( - neg_diag_rows.size() == 1 && q_nnz == 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected tail diagonals +s with head -s, " - "or a single head row with q_nnz=1", - qc.constraint_row_name.c_str()); - const f_t neg_v = neg_diag_rows[0].second; - cuopt_expects(neg_v < -tol, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: cone head diagonal must be negative " - "(%.17g)", - qc.constraint_row_name.c_str(), - static_cast(neg_v)); - uniform_s = -neg_v; - have_uniform_s = true; - head = neg_diag_rows[0].first; - cuopt_expects( - static_cast(tail_vars.size()) == q_nnz - 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", - qc.constraint_row_name.c_str(), - static_cast(q_nnz - 1), - tail_vars.size()); - cone.reserve(1); - cone.push_back(head); - cone_dim = static_cast(cone.size()); - is_rotated = static_cast(0); - } else { - for (const auto& pr : pos_diag_rows) { - note_positive_s(pr.second); - } - cuopt_expects( - have_uniform_s, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: could not infer uniform positive scale s", - qc.constraint_row_name.c_str()); - cuopt_expects( - neg_diag_rows.size() == 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected exactly one diagonal -s (cone head) for " - "%zu tail entries, found %zu negative diagonals", - qc.constraint_row_name.c_str(), - tail_vars.size(), - neg_diag_rows.size()); - cuopt_expects( - static_cast(tail_vars.size()) == q_nnz - 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", - qc.constraint_row_name.c_str(), - static_cast(q_nnz - 1), - tail_vars.size()); - const f_t neg_v = neg_diag_rows[0].second; - cuopt_expects( - approx_eq_scaled(neg_v, -uniform_s), - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: cone head diagonal must be -s with the same s as " - "positive tail diagonals; head %.17g vs -s = %.17g", - qc.constraint_row_name.c_str(), - static_cast(neg_v), - static_cast(-uniform_s)); - head = neg_diag_rows[0].first; - cone.reserve(static_cast(q_nnz)); - cone.push_back(head); - cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); - cone_dim = static_cast(cone.size()); - is_rotated = static_cast(0); - } - } else { - cuopt_expects( - neg_diag_rows.empty(), - error_type_t::ValidationError, - "Quadratic constraint '%s' with linear terms cannot contain negative diagonal " - "Q entries", - qc.constraint_row_name.c_str()); - cuopt_expects(affine_head >= 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' internal error: affine SOC head index invalid", - qc.constraint_row_name.c_str()); - for (const auto& pr : pos_diag_rows) { - note_positive_s(pr.second); - } - cuopt_expects(have_uniform_s, - error_type_t::ValidationError, - "Quadratic constraint '%s' with linear terms must have at least one " - "diagonal +s term in Q", - qc.constraint_row_name.c_str()); - cuopt_expects(!tail_vars.empty(), - error_type_t::ValidationError, - "Quadratic constraint '%s' with linear terms must have at least one " - "diagonal +s term in Q", - qc.constraint_row_name.c_str()); - for (const i_t tail : tail_vars) { - cuopt_expects( - tail != affine_head, - error_type_t::ValidationError, - "Quadratic constraint '%s' with linear terms requires the linear head variable to be " - "distinct from quadratic diagonal variables", - qc.constraint_row_name.c_str()); - } - - cone.reserve(tail_vars.size() + 1); - cone.push_back(affine_head); - cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); - cone_dim = static_cast(tail_vars.size() + 2); - is_rotated = static_cast(1); - rotated_cones.push_back( - rotated_soc_t{affine_head, static_cast(-1), tail_vars, true, f_t(1)}); - } - } else { - cuopt_expects(!has_linear_part, - error_type_t::ValidationError, - "Quadratic constraint '%s' with linear terms cannot include rotated-SOC " - "off-diagonal entries", - qc.constraint_row_name.c_str()); - cuopt_expects(neg_diag_rows.empty(), - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q cannot contain diagonal head " - "entries; found %zu negative diagonals", - qc.constraint_row_name.c_str(), - neg_diag_rows.size()); - for (const auto& pr : pos_diag_rows) { - note_positive_s(pr.second); - } - cuopt_expects(have_uniform_s, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q: could not infer uniform scale s", - qc.constraint_row_name.c_str()); - cuopt_expects( - offdiag_entries.size() == 2, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q must contain exactly one symmetric off-diagonal " - "pair (-d,-d); found %zu off-diagonal entries", - qc.constraint_row_name.c_str(), - offdiag_entries.size()); - - const i_t a = std::get<0>(offdiag_entries[0]); - const i_t b = std::get<1>(offdiag_entries[0]); - const f_t v0 = std::get<2>(offdiag_entries[0]); - cuopt_expects( - v0 < -tol, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", - qc.constraint_row_name.c_str(), - static_cast(v0)); - cuopt_expects(a != b, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q off-diagonal pair must use distinct " - "variables", - qc.constraint_row_name.c_str()); - cuopt_expects(std::get<0>(offdiag_entries[1]) == b && std::get<1>(offdiag_entries[1]) == a, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q must have symmetric entries (a,b) " - "and (b,a) with the same value", - qc.constraint_row_name.c_str()); - const f_t v1 = std::get<2>(offdiag_entries[1]); - cuopt_expects( - v1 < -tol, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", - qc.constraint_row_name.c_str(), - static_cast(v1)); - cuopt_expects( - approx_eq_scaled(v0, v1), - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q symmetric off-diagonals must match; got %.17g " - "and %.17g", - qc.constraint_row_name.c_str(), - static_cast(v0), - static_cast(v1)); - const f_t cross_d = -v0; - cuopt_expects( - cross_d > tol, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q cross coefficient d = -Q_off must be positive", - qc.constraint_row_name.c_str()); - const f_t head_lift_sqrt_ratio = std::sqrt(cross_d / uniform_s); - cuopt_expects(std::isfinite(static_cast(head_lift_sqrt_ratio)), - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q head lift ratio sqrt(d/s) is not " - "finite (d=%.17g, s=%.17g)", - qc.constraint_row_name.c_str(), - static_cast(cross_d), - static_cast(uniform_s)); - cuopt_expects(static_cast(tail_vars.size()) == q_nnz - 2, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q: expected %d diagonal +s entries " - "(tails), found %zu", - qc.constraint_row_name.c_str(), - static_cast(q_nnz - 2), - tail_vars.size()); - cuopt_expects(q_nnz >= 3, - error_type_t::ValidationError, - "Quadratic constraint '%s' rotated SOC Q must have at least 1 tail entry", - qc.constraint_row_name.c_str()); - - cone.reserve(static_cast(q_nnz)); - cone.push_back(a); - cone.push_back(b); - cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); - cone_dim = static_cast(cone.size()); - is_rotated = static_cast(1); - rotated_cones.push_back(rotated_soc_t{a, b, tail_vars, false, head_lift_sqrt_ratio}); - } - - cuopt_expects(have_uniform_s && uniform_s > tol, - error_type_t::ValidationError, - "Quadratic constraint '%s' SOC Q: uniform scale s must be positive (got %.17g)", - qc.constraint_row_name.c_str(), - static_cast(uniform_s)); - qc_soc_uniform_scale[qc_i] = uniform_s; - - for (const i_t var : cone) { - cuopt_expects(var >= 0 && var < static_cast(is_cone_var.size()), - error_type_t::ValidationError, - "SOC variable index %d is outside [0, %d)", - static_cast(var), - static_cast(is_cone_var.size())); - } - cone_dims.push_back(cone_dim); - cone_vars.push_back(std::move(cone)); - cone_is_rotated.push_back(is_rotated); - } - // Add affine linear auxiliary variables and linking rows. - if (n_affine_linear_aux > 0) { - const f_t inf = std::numeric_limits::infinity(); - const i_t n_old = static_cast(n); - const i_t n_aug = n_with_affine_aux; - const i_t m_old = csr_A.m; - const i_t m_aug = static_cast(m_old + n_affine_linear_aux); - i_t row_write_cursor = m_old; - - user_problem.objective.resize(static_cast(n_aug), f_t(0)); - user_problem.lower.resize(static_cast(n_aug), -inf); - user_problem.upper.resize(static_cast(n_aug), inf); - user_problem.var_types.resize( - static_cast(n_aug), - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); - if (!user_problem.col_names.empty()) { - user_problem.col_names.resize(static_cast(n_aug)); - } - - for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { - const i_t aux_j = qc_affine_heads[qc_i]; - if (aux_j < 0) { continue; } - user_problem.lower[static_cast(aux_j)] = f_t(0); - user_problem.upper[static_cast(aux_j)] = inf; - if (!user_problem.col_names.empty()) { - user_problem.col_names[static_cast(aux_j)] = - "_CUOPT_qc_linear_aux_" + std::to_string(static_cast(aux_j - n_old)); - } - } - - user_problem.rhs.resize(static_cast(m_aug)); - user_problem.row_sense.resize(static_cast(m_aug)); - if (!user_problem.row_names.empty()) { - user_problem.row_names.resize(static_cast(m_aug)); - } - - csr_A.n = n_aug; - dual_simplex::sparse_vector_t eq_row; - eq_row.n = n_aug; - - for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { - const i_t aux_j = qc_affine_heads[qc_i]; - if (aux_j < 0) { continue; } - const auto& qc = qcs[qc_i]; - eq_row.i.clear(); - eq_row.x.clear(); - // Define auxiliary as t = -(1/s) a^T x so QC linear part matches normalized cone row. - const f_t inv_s = f_t(1) / qc_soc_uniform_scale[qc_i]; - eq_row.i.push_back(aux_j); - eq_row.x.push_back(f_t(1)); - for (size_t p = 0; p < qc.linear_values.size(); ++p) { - const f_t v = qc.linear_values[p]; - if (v > -tol && v < tol) { continue; } - eq_row.i.push_back(qc.linear_indices[p]); - eq_row.x.push_back(v * inv_s); - } - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_write_cursor)] = 'E'; - user_problem.rhs[static_cast(row_write_cursor)] = f_t(0); - if (!user_problem.row_names.empty()) { - user_problem.row_names[static_cast(row_write_cursor)] = - "_CUOPT_qc_linear_link_" + qc.constraint_row_name; - } - ++row_write_cursor; - } - - cuopt_expects(row_write_cursor == m_aug, - error_type_t::RuntimeError, - "Internal error: affine QC linking row count mismatch"); - cuopt_expects(csr_A.m == m_aug, - error_type_t::RuntimeError, - "Internal error: CSR row count after affine QC linking"); - } - - i_t n_prob = n_with_affine_aux; - - // Convert rotated SOC cones to standard SOC cones. - if (!rotated_cones.empty()) { - cuopt_expects(user_problem.Q_values.empty(), - error_type_t::ValidationError, - "Rotated SOC conversion is currently not supported when the objective has " - "quadratic terms"); - - const f_t inf = std::numeric_limits::infinity(); - const f_t inv_sqrt_2 = f_t(1) / std::sqrt(f_t(2)); - const f_t half = f_t(0.5); - - for (const auto& rc : rotated_cones) { - cuopt_expects(user_problem.var_types[static_cast(rc.head0)] == - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, - error_type_t::ValidationError, - "Rotated SOC head variables must be continuous"); - if (!rc.head1_is_constant_half) { - cuopt_expects(user_problem.var_types[static_cast(rc.head1)] == - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, - error_type_t::ValidationError, - "Rotated SOC head variables must be continuous"); - } - for (const i_t t : rc.tails) { - cuopt_expects(user_problem.var_types[static_cast(t)] == - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, - error_type_t::ValidationError, - "Rotated SOC tail variables must be continuous"); - } - } - - // Lift each rotated cone into standard SOC coordinates with two slacks: - // With x_i' = sqrt(d/s)*x_hi, canonical s0 = (x_0'+x_1')/sqrt(2), s1 = (x_0'-x_1')/sqrt(2) - // so 2*d*x_h0*x_h1 >= s*sum tail^2 <=> 2*x_0'*x_1' >= sum (x_tail)^2 => s0^2 >= s1^2 + - // ... Only the rotated heads are replaced by slacks; tails stay as original variables. - i_t n_slack_total = 0; - for (size_t ci = 0; ci < cone_is_rotated.size(); ++ci) { - if (cone_is_rotated[ci]) { n_slack_total += 2; } - } - - const i_t n_old = n_prob; - n_prob = static_cast(n_old + n_slack_total); - - user_problem.objective.resize(static_cast(n_prob), f_t(0)); - user_problem.lower.resize(static_cast(n_prob), -inf); - user_problem.upper.resize(static_cast(n_prob), inf); - user_problem.var_types.resize( - static_cast(n_prob), - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); - if (!user_problem.col_names.empty()) { - user_problem.col_names.resize(static_cast(n_prob)); - for (i_t j = n_old; j < n_prob; ++j) { - user_problem.col_names[static_cast(j)] = - "_CUOPT_rsoc_slack_" + std::to_string(static_cast(j - n_old)); - } - } - - is_cone_var.resize(static_cast(n_prob), 0); - - const i_t m_old = csr_A.m; - user_problem.rhs.resize(static_cast(m_old + n_slack_total)); - user_problem.row_sense.resize(static_cast(m_old + n_slack_total)); - if (!user_problem.row_names.empty()) { - user_problem.row_names.resize(static_cast(m_old + n_slack_total)); - for (i_t r = m_old; r < m_old + n_slack_total; ++r) { - user_problem.row_names[static_cast(r)] = - "_CUOPT_rsoc_lift_" + std::to_string(static_cast(r - m_old)); - } - } - - csr_A.n = n_prob; - - dual_simplex::sparse_vector_t eq_row; - size_t ri = 0; - i_t slack_base = n_old; - i_t row_idx = m_old; - - for (size_t ci = 0; ci < cone_vars.size(); ++ci) { - if (!cone_is_rotated[ci]) { continue; } - const auto& rc = rotated_cones[ri++]; - const i_t dim = cone_dims[ci]; - std::vector new_cone; - new_cone.reserve(static_cast(dim)); - new_cone.push_back(slack_base); - new_cone.push_back(slack_base + 1); - new_cone.insert(new_cone.end(), rc.tails.begin(), rc.tails.end()); - cone_vars[ci] = std::move(new_cone); - - is_cone_var[static_cast(slack_base)] = 1; - is_cone_var[static_cast(slack_base + 1)] = 1; - - eq_row.n = n_prob; - // If the second head is not constant half, we need to lift it. - if (!rc.head1_is_constant_half) { - const f_t h = inv_sqrt_2 * rc.head_lift_sqrt_ratio; - // s_0 - h * x_h0 - h * x_h1 = 0 (h = inv_sqrt_2 * sqrt(d/s)) - eq_row.i = {rc.head0, rc.head1, slack_base}; - eq_row.x = {-h, -h, f_t(1)}; - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_idx)] = 'E'; - user_problem.rhs[static_cast(row_idx)] = f_t(0); - ++row_idx; - - // s_1 - h * x_h0 + h * x_h1 = 0 - eq_row.i = {rc.head0, rc.head1, slack_base + 1}; - eq_row.x = {-h, h, f_t(1)}; - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_idx)] = 'E'; - user_problem.rhs[static_cast(row_idx)] = f_t(0); - ++row_idx; - - is_cone_var[static_cast(rc.head0)] = 0; - is_cone_var[static_cast(rc.head1)] = 0; - } else { - // One head is constant half, so we can lift it directly. - // s_0 - inv_sqrt_2 * x_h0 = inv_sqrt_2 * (1/2) - eq_row.i = {rc.head0, slack_base}; - eq_row.x = {-inv_sqrt_2, f_t(1)}; - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_idx)] = 'E'; - user_problem.rhs[static_cast(row_idx)] = inv_sqrt_2 * half; - ++row_idx; - - // s_1 - inv_sqrt_2 * x_h0 = -inv_sqrt_2 * (1/2) - eq_row.i = {rc.head0, slack_base + 1}; - eq_row.x = {-inv_sqrt_2, f_t(1)}; - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_idx)] = 'E'; - user_problem.rhs[static_cast(row_idx)] = -inv_sqrt_2 * half; - ++row_idx; - - is_cone_var[static_cast(rc.head0)] = 0; - } - - slack_base += 2; - } - - cuopt_expects(ri == rotated_cones.size(), - error_type_t::RuntimeError, - "Internal error: rotated SOC cone metadata mismatch"); - cuopt_expects(slack_base == n_prob, - error_type_t::RuntimeError, - "Internal error: slack variable count mismatch"); - cuopt_expects(row_idx == m_old + n_slack_total, - error_type_t::RuntimeError, - "Internal error: rotated SOC equality row count mismatch"); - cuopt_expects(csr_A.m == m_old + n_slack_total, - error_type_t::RuntimeError, - "Internal error: CSR row count after rotated SOC lift"); - } - - // If a variable appears in multiple cones, create per-cone aliases and add linking rows - // alias - original = 0 so cone variable blocks are disjoint. - { - std::vector first_owner(static_cast(n_prob), static_cast(-1)); - std::vector> cone_alias_pairs; // (alias, original) - - for (size_t ci = 0; ci < cone_vars.size(); ++ci) { - auto& cone = cone_vars[ci]; - for (auto& var : cone) { - cuopt_expects(var >= 0 && var < n_prob, - error_type_t::ValidationError, - "SOC variable index %d is outside [0, %d)", - static_cast(var), - static_cast(n_prob)); - const auto idx = static_cast(var); - if (first_owner[idx] == static_cast(-1)) { - first_owner[idx] = static_cast(ci); - continue; - } - if (first_owner[idx] != static_cast(ci)) { - const i_t alias = static_cast(n_prob + cone_alias_pairs.size()); - cone_alias_pairs.emplace_back(alias, var); - var = alias; - } - } - } - - if (!cone_alias_pairs.empty()) { - const i_t n_old = n_prob; - const i_t n_new = static_cast(n_old + cone_alias_pairs.size()); - const i_t m_old = csr_A.m; - const i_t m_new = static_cast(m_old + cone_alias_pairs.size()); - - user_problem.objective.resize(static_cast(n_new), f_t(0)); - user_problem.lower.resize(static_cast(n_new), - -std::numeric_limits::infinity()); - user_problem.upper.resize(static_cast(n_new), std::numeric_limits::infinity()); - user_problem.var_types.resize( - static_cast(n_new), - cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); - if (!user_problem.col_names.empty()) { - user_problem.col_names.resize(static_cast(n_new)); - } - - for (const auto& [alias, original] : cone_alias_pairs) { - user_problem.lower[static_cast(alias)] = - user_problem.lower[static_cast(original)]; - user_problem.upper[static_cast(alias)] = - user_problem.upper[static_cast(original)]; - user_problem.var_types[static_cast(alias)] = - user_problem.var_types[static_cast(original)]; - // Keep objective unchanged: alias coefficient stays zero and alias==original links - // values. - if (!user_problem.col_names.empty()) { - user_problem.col_names[static_cast(alias)] = - "_CUOPT_cone_alias_" + std::to_string(static_cast(alias - n_old)); - } - } - - user_problem.rhs.resize(static_cast(m_new)); - user_problem.row_sense.resize(static_cast(m_new)); - if (!user_problem.row_names.empty()) { - user_problem.row_names.resize(static_cast(m_new)); - } - - csr_A.n = n_new; - dual_simplex::sparse_vector_t eq_row; - eq_row.n = n_new; - i_t row_idx = m_old; - for (const auto& [alias, original] : cone_alias_pairs) { - eq_row.i = {alias, original}; - eq_row.x = {f_t(1), f_t(-1)}; - eq_row.sort(); - csr_A.append_row(eq_row); - user_problem.row_sense[static_cast(row_idx)] = 'E'; - user_problem.rhs[static_cast(row_idx)] = f_t(0); - if (!user_problem.row_names.empty()) { - user_problem.row_names[static_cast(row_idx)] = - "_CUOPT_cone_alias_link_" + std::to_string(static_cast(row_idx - m_old)); - } - ++row_idx; - } - - cuopt_expects(row_idx == m_new, - error_type_t::RuntimeError, - "Internal error: cone alias linking row count mismatch"); - cuopt_expects(csr_A.m == m_new, - error_type_t::RuntimeError, - "Internal error: CSR row count after cone alias linking"); - - n_prob = n_new; - } - } - - is_cone_var.assign(static_cast(n_prob), 0); - for (const auto& cone : cone_vars) { - for (const i_t var : cone) { - cuopt_expects(var >= 0 && var < n_prob, - error_type_t::ValidationError, - "SOC variable index %d is outside [0, %d) after cone aliasing", - static_cast(var), - static_cast(n_prob)); - is_cone_var[static_cast(var)] = 1; - } - } - - std::vector old_to_new(static_cast(n_prob), i_t{-1}); - std::vector new_to_old; - new_to_old.reserve(static_cast(n_prob)); - for (i_t j = 0; j < n_prob; ++j) { - if (is_cone_var[static_cast(j)]) { continue; } - old_to_new[static_cast(j)] = static_cast(new_to_old.size()); - new_to_old.push_back(j); - } - const i_t cone_var_start = static_cast(new_to_old.size()); - for (const auto& cone : cone_vars) { - for (const i_t old_j : cone) { - old_to_new[static_cast(old_j)] = static_cast(new_to_old.size()); - new_to_old.push_back(old_j); - } - } - cuopt_expects(static_cast(new_to_old.size()) == n_prob, - error_type_t::RuntimeError, - "Internal error while building SOC variable permutation"); - - for (i_t row = 0; row < csr_A.m; ++row) { - for (i_t p = csr_A.row_start[static_cast(row)]; - p < csr_A.row_start[static_cast(row + 1)]; - ++p) { - const i_t old_j = csr_A.j[static_cast(p)]; - cuopt_expects(old_j >= 0 && old_j < n_prob, - error_type_t::ValidationError, - "Linear constraint matrix column index %d is outside [0, %d)", - static_cast(old_j), - static_cast(n_prob)); - csr_A.j[static_cast(p)] = old_to_new[static_cast(old_j)]; - } - } - - auto permute_dense_by_old_to_new = [&](auto& values, const char* name) { - if (values.empty()) { return; } - using value_t = typename std::decay_t::value_type; - cuopt_expects(values.size() == static_cast(n_prob), - error_type_t::ValidationError, - "%s length %zu does not match number of variables %d", - name, - values.size(), - static_cast(n_prob)); - std::vector permuted(values.size()); - for (i_t old_j = 0; old_j < n_prob; ++old_j) { - permuted[static_cast(old_to_new[static_cast(old_j)])] = - std::move(values[static_cast(old_j)]); - } - values = std::move(permuted); - }; - - permute_dense_by_old_to_new(user_problem.objective, "objective"); - permute_dense_by_old_to_new(user_problem.lower, "lower bounds"); - permute_dense_by_old_to_new(user_problem.upper, "upper bounds"); - permute_dense_by_old_to_new(user_problem.var_types, "variable types"); - permute_dense_by_old_to_new(user_problem.col_names, "column names"); - - if (!user_problem.Q_values.empty()) { - const i_t n_model = static_cast(n); - cuopt_expects(user_problem.Q_indices.size() == user_problem.Q_values.size(), - error_type_t::ValidationError, - "Quadratic objective indices and values length mismatch"); - cuopt_expects(user_problem.Q_offsets.size() == static_cast(n_model) + 1, - error_type_t::ValidationError, - "Quadratic objective CSR offsets length must be n+1 when SOC QCMATRIX " - "conversion permutes variables"); - cuopt_expects(user_problem.Q_offsets[0] == 0, - error_type_t::ValidationError, - "Quadratic objective CSR offsets[0] must be 0"); - cuopt_expects(user_problem.Q_offsets[static_cast(n_model)] == - static_cast(user_problem.Q_values.size()), - error_type_t::ValidationError, - "Quadratic objective CSR last offset must equal number of nonzeros"); - - std::vector q_offsets(static_cast(n_prob) + 1, 0); - for (i_t old_row = 0; old_row < n_model; ++old_row) { - const i_t p_beg = user_problem.Q_offsets[static_cast(old_row)]; - const i_t p_end = user_problem.Q_offsets[static_cast(old_row + 1)]; - cuopt_expects( - p_beg >= 0 && p_beg <= p_end && p_end <= static_cast(user_problem.Q_values.size()), - error_type_t::ValidationError, - "Quadratic objective CSR offsets are invalid at row %d", - static_cast(old_row)); - const i_t new_row = old_to_new[static_cast(old_row)]; - q_offsets[static_cast(new_row + 1)] = p_end - p_beg; - } - for (i_t row = 0; row < n_prob; ++row) { - q_offsets[static_cast(row + 1)] += q_offsets[static_cast(row)]; - } - - std::vector q_indices(user_problem.Q_values.size()); - std::vector q_values(user_problem.Q_values.size()); - auto q_write = q_offsets; - for (i_t old_row = 0; old_row < n_model; ++old_row) { - const i_t new_row = old_to_new[static_cast(old_row)]; - for (i_t p = user_problem.Q_offsets[static_cast(old_row)]; - p < user_problem.Q_offsets[static_cast(old_row + 1)]; - ++p) { - const i_t old_col = user_problem.Q_indices[static_cast(p)]; - cuopt_expects(old_col >= 0 && old_col < n_model, - error_type_t::ValidationError, - "Quadratic objective column index %d is outside [0, %d)", - static_cast(old_col), - static_cast(n_model)); - const i_t dst = q_write[static_cast(new_row)]++; - q_indices[static_cast(dst)] = old_to_new[static_cast(old_col)]; - q_values[static_cast(dst)] = user_problem.Q_values[static_cast(p)]; - } - } - - user_problem.Q_offsets = std::move(q_offsets); - user_problem.Q_indices = std::move(q_indices); - user_problem.Q_values = std::move(q_values); - } - - user_problem.cone_var_start = cone_var_start; - user_problem.second_order_cone_dims = std::move(cone_dims); - user_problem.num_rows = csr_A.m; - user_problem.num_cols = n_prob; + detail::apply_soc_qcmatrix_conversion_for_simplex( + n, model.original_problem_ptr->get_quadratic_constraints(), csr_A, user_problem); } csr_A.to_compressed_col(user_problem.A); From 006edb77ffd8a5fa0d1fc055c1df97ea50e58090 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 11 May 2026 07:04:45 -0700 Subject: [PATCH 34/68] more efficient problem setup from MPS data --- cpp/libmps_parser/src/mps_parser.cpp | 379 ++++++++++++++++++++------- 1 file changed, 284 insertions(+), 95 deletions(-) diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index e438046041..7938a7d82d 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include #ifdef MPS_PARSER_WITH_BZIP2 @@ -203,6 +205,231 @@ std::vector zlib_file_to_string(const std::string& file) } // end namespace #endif // MPS_PARSER_WITH_ZLIB +namespace { + +/** @brief ceil(log2(x)) for x >= 1; used for sparse-vs-dense CSR heuristic only. */ +inline size_t ceil_log2_size_t(size_t x) +{ + size_t b = 0; + size_t y = (x <= 1) ? 0 : (x - 1); + while (y > 0) { + y >>= 1; + ++b; + } + return (b == 0) ? 1 : b; +} + +/** + * @brief Reusable scratch for converting (row,col,value) triples to CSR via CSC (double transpose). + * + * Avoids `std::vector>` per column/row, which is allocation-heavy for large Q + * blocks (e.g. many QCMATRIX sections in portfolio models). + */ +template +struct double_transpose_scratch_t { + std::vector col_nnz{}; + std::vector col_off{}; + std::vector col_wr{}; + std::vector csc_rows{}; + std::vector csc_vals{}; + std::vector row_nnz{}; + std::vector row_off{}; + std::vector row_wr{}; + /** Indices 0..nnz-1 for sort-by-(row,col) sparse CSR path (no symmetrization). */ + std::vector sort_perm{}; +}; + +/** + * @brief CSR from triples without full O(num_cols) column sweeps: count per row, sort by (row,col), + * scatter. Same numeric result ordering as the dense double-transpose path for non-symmetrized + * inputs when column order within a row is the stable sort order for equal keys (see std::sort). + * + * Used for QCMATRIX / QMATRIX when nnz is small enough vs matrix dimension that sorting beats + * scanning every column index twice. + */ +template +void triples_to_csr_sparse_sorted_flat( + const std::vector>& entries, + i_t num_rows, + i_t num_cols, + f_t value_scale, + double_transpose_scratch_t& scratch, + std::vector& out_values, + std::vector& out_indices, + std::vector& out_offsets) +{ + static_cast(num_cols); // square Q paths pass num_cols == num_rows + const size_t nnz = entries.size(); + const size_t nr = static_cast(num_rows); + + scratch.row_nnz.assign(nr, 0); + for (const auto& e : entries) { + ++scratch.row_nnz[static_cast(std::get<0>(e))]; + } + + scratch.row_off.resize(nr + 1); + scratch.row_off[0] = 0; + for (size_t r = 0; r < nr; ++r) { + scratch.row_off[r + 1] = scratch.row_off[r] + scratch.row_nnz[r]; + } + const size_t csr_nnz = static_cast(scratch.row_off[nr]); + + out_values.resize(csr_nnz); + out_indices.resize(csr_nnz); + + scratch.sort_perm.resize(nnz); + std::iota(scratch.sort_perm.begin(), scratch.sort_perm.end(), size_t{0}); + std::stable_sort(scratch.sort_perm.begin(), scratch.sort_perm.end(), [&](size_t a, size_t b) { + const auto& ta = entries[a]; + const auto& tb = entries[b]; + const i_t ra = std::get<0>(ta); + const i_t rb = std::get<0>(tb); + if (ra != rb) { return ra < rb; } + return std::get<1>(ta) < std::get<1>(tb); + }); + + scratch.row_wr.resize(nr); + std::copy(scratch.row_off.begin(), scratch.row_off.begin() + nr, scratch.row_wr.begin()); + + for (size_t k = 0; k < nnz; ++k) { + const size_t ix = scratch.sort_perm[k]; + const auto& e = entries[ix]; + const i_t row = std::get<0>(e); + const i_t col = std::get<1>(e); + const f_t val = std::get<2>(e); + const size_t sr = static_cast(row); + const i_t w = scratch.row_wr[sr]++; + out_indices[static_cast(w)] = col; + out_values[static_cast(w)] = val * value_scale; + } + + out_offsets = std::move(scratch.row_off); +} + +/** + * @brief Build CSR from coordinate triples using the same double-transpose semantics as the + * previous nested-vector implementation: CSC column-major buckets, then CSR with columns sorted + * within each row (column index ascending). + * + * @param symmetrize_upper_triangular If true (QUADOBJ), each off-diagonal (r,c) also adds (c,r). + */ +template +void triples_to_csr_double_transpose_flat( + const std::vector>& entries, + i_t num_rows, + i_t num_cols, + bool symmetrize_upper_triangular, + f_t value_scale, + double_transpose_scratch_t& scratch, + std::vector& out_values, + std::vector& out_indices, + std::vector& out_offsets) +{ + if (entries.empty()) { + out_values.clear(); + out_indices.clear(); + out_offsets.assign(static_cast(num_rows) + 1, 0); + return; + } + + const size_t nc = static_cast(num_cols); + const size_t nr = static_cast(num_rows); + const size_t nnz = entries.size(); + const size_t n_dim = std::max(nr, nc); + + // Without symmetrization (QCMATRIX, QMATRIX), avoid O(n) full-column sweeps when blocks are + // sparse: sort triples by (row,col) and scatter in one pass. QUADOBJ keeps the dense transpose + // path because symmetrization duplicates off-diagonal entries. + if (!symmetrize_upper_triangular) { + const size_t lg = ceil_log2_size_t(nnz); + // Roughly: sort cost ~ nnz·lg vs ~O(n) column iterations in dense path; 48 is a tunable fudge. + constexpr size_t k_sparse_vs_dense_heuristic = 48; + if (nnz * lg < k_sparse_vs_dense_heuristic * n_dim) { + triples_to_csr_sparse_sorted_flat( + entries, num_rows, num_cols, value_scale, scratch, out_values, out_indices, out_offsets); + return; + } + } + + scratch.col_nnz.assign(nc, 0); + for (const auto& entry : entries) { + const i_t r = std::get<0>(entry); + const i_t c = std::get<1>(entry); + scratch.col_nnz[static_cast(c)]++; + if (symmetrize_upper_triangular && r != c) { scratch.col_nnz[static_cast(r)]++; } + } + + scratch.col_off.resize(nc + 1); + scratch.col_off[0] = 0; + for (size_t c = 0; c < nc; ++c) { + scratch.col_off[c + 1] = scratch.col_off[c] + scratch.col_nnz[c]; + } + const i_t csc_nnz = scratch.col_off[nc]; + scratch.csc_rows.resize(static_cast(csc_nnz)); + scratch.csc_vals.resize(static_cast(csc_nnz)); + scratch.col_wr.resize(nc); + std::copy(scratch.col_off.begin(), scratch.col_off.begin() + nc, scratch.col_wr.begin()); + + for (const auto& entry : entries) { + const i_t r = std::get<0>(entry); + const i_t c = std::get<1>(entry); + const f_t v = std::get<2>(entry); + { + const size_t sc = static_cast(c); + const i_t p = scratch.col_wr[sc]++; + scratch.csc_rows[static_cast(p)] = r; + scratch.csc_vals[static_cast(p)] = v; + } + if (symmetrize_upper_triangular && r != c) { + const size_t sr = static_cast(r); + const i_t p = scratch.col_wr[sr]++; + scratch.csc_rows[static_cast(p)] = c; + scratch.csc_vals[static_cast(p)] = v; + } + } + + scratch.row_nnz.assign(nr, 0); + for (i_t cc = 0; cc < num_cols; ++cc) { + const size_t col_z = static_cast(cc); + const i_t lo = scratch.col_off[col_z]; + const i_t hi = scratch.col_off[col_z + 1]; + for (i_t t = lo; t < hi; ++t) { + const i_t row = scratch.csc_rows[static_cast(t)]; + scratch.row_nnz[static_cast(row)]++; + } + } + + scratch.row_off.resize(nr + 1); + scratch.row_off[0] = 0; + for (size_t r = 0; r < nr; ++r) { + scratch.row_off[r + 1] = scratch.row_off[r] + scratch.row_nnz[r]; + } + const size_t csr_nnz = static_cast(scratch.row_off[nr]); + + out_values.resize(csr_nnz); + out_indices.resize(csr_nnz); + scratch.row_wr.resize(nr); + std::copy(scratch.row_off.begin(), scratch.row_off.begin() + nr, scratch.row_wr.begin()); + + for (i_t cc = 0; cc < num_cols; ++cc) { + const size_t col_z = static_cast(cc); + const i_t lo = scratch.col_off[col_z]; + const i_t hi = scratch.col_off[col_z + 1]; + for (i_t t = lo; t < hi; ++t) { + const i_t row = scratch.csc_rows[static_cast(t)]; + const f_t val = scratch.csc_vals[static_cast(t)]; + const size_t sr = static_cast(row); + const i_t w = scratch.row_wr[sr]++; + out_indices[static_cast(w)] = cc; + out_values[static_cast(w)] = val * value_scale; + } + } + + out_offsets = std::move(scratch.row_off); +} + +} // namespace + namespace cuopt::mps_parser { template @@ -464,107 +691,71 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_variable_types(std::move(var_types)); problem.set_maximize(maximize); - // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of - // O(nnz*log(nnz))) For QUADOBJ: handles upper triangular input by expanding to full symmetric - // matrix. + // Build CSR from (row,col,value) triples via double transpose (CSC then CSR). Uses flat buffers + // plus reusable scratch to avoid per-column/per-row `std::vector` allocations — important for + // large Q blocks and many QCMATRIX sections. // - // @p value_scale: - // QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); - // QCMATRIX uses 1.0 (symmetric Q defines xᵀQx directly in the constraint). - auto build_csr_via_transpose = [](const std::vector>& entries, - i_t num_rows, - i_t num_cols, - bool symmetrize_upper_triangular, - f_t value_scale) { - struct CSRResult { - std::vector values; - std::vector indices; - std::vector offsets; - }; - - if (entries.empty()) { - CSRResult result; - result.offsets.resize(num_rows + 1, 0); - return result; - } - - // First transpose: build CSC format (entries sorted by column) - std::vector>> csc_data(num_cols); - for (const auto& entry : entries) { - i_t row = std::get<0>(entry); - i_t col = std::get<1>(entry); - f_t val = std::get<2>(entry); - - // For QUADOBJ (upper triangular), add both (row,col) and (col,row) if off-diagonal - csc_data[col].emplace_back(row, val); - if (symmetrize_upper_triangular && row != col) { csc_data[row].emplace_back(col, val); } - } - - // Second transpose: convert CSC to CSR (entries sorted by row, columns within rows sorted) - std::vector>> csr_data(num_rows); - for (i_t col = 0; col < num_cols; ++col) { - for (const auto& [row, val] : csc_data[col]) { - csr_data[row].emplace_back(col, val); - } - } - - // Build final CSR format - CSRResult result; - result.offsets.reserve(num_rows + 1); - result.offsets.push_back(0); - - for (i_t row = 0; row < num_rows; ++row) { - for (const auto& [col, val] : csr_data[row]) { - // MPS uses ½ xᵀQx in the file; cuOpt stores Q for xᵀQx, so scale by value_scale (0.5 for - // QUADOBJ/QMATRIX, 1.0 for QCMATRIX). - result.values.push_back(val * value_scale); - result.indices.push_back(col); - } - result.offsets.push_back(result.values.size()); - } - - return result; - }; + // value_scale: QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); QCMATRIX uses 1.0. + double_transpose_scratch_t triple_csr_scratch{}; + std::vector quad_csr_values{}; + std::vector quad_csr_indices{}; + std::vector quad_csr_offsets{}; // Process QUADOBJ data if present (upper triangular format) if (!quadobj_entries.empty()) { - // Convert quadratic objective entries to CSR format using double transpose - // QUADOBJ stores upper triangular elements, so we expand to full symmetric matrix constexpr f_t k_mps_quad_half_scale = f_t(0.5); // MPS ½ xᵀQx vs internal xᵀQx - auto csr_result = build_csr_via_transpose( - quadobj_entries, num_vars_for_quad, num_vars_for_quad, true, k_mps_quad_half_scale); - - // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) - problem.set_quadratic_objective_matrix(csr_result.values.data(), - csr_result.values.size(), - csr_result.indices.data(), - csr_result.indices.size(), - csr_result.offsets.data(), - csr_result.offsets.size()); + triples_to_csr_double_transpose_flat(quadobj_entries, + num_vars_for_quad, + num_vars_for_quad, + true, + k_mps_quad_half_scale, + triple_csr_scratch, + quad_csr_values, + quad_csr_indices, + quad_csr_offsets); + problem.set_quadratic_objective_matrix(quad_csr_values.data(), + static_cast(quad_csr_values.size()), + quad_csr_indices.data(), + static_cast(quad_csr_indices.size()), + quad_csr_offsets.data(), + static_cast(quad_csr_offsets.size())); } else if (!qmatrix_entries.empty()) { - // Convert quadratic objective entries to CSR format using double transpose - // QMATRIX stores full symmetric matrix constexpr f_t k_mps_quad_half_scale = f_t(0.5); - auto csr_result = build_csr_via_transpose( - qmatrix_entries, num_vars_for_quad, num_vars_for_quad, false, k_mps_quad_half_scale); - - // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) - problem.set_quadratic_objective_matrix(csr_result.values.data(), - csr_result.values.size(), - csr_result.indices.data(), - csr_result.indices.size(), - csr_result.offsets.data(), - csr_result.offsets.size()); + triples_to_csr_double_transpose_flat(qmatrix_entries, + num_vars_for_quad, + num_vars_for_quad, + false, + k_mps_quad_half_scale, + triple_csr_scratch, + quad_csr_values, + quad_csr_indices, + quad_csr_offsets); + problem.set_quadratic_objective_matrix(quad_csr_values.data(), + static_cast(quad_csr_values.size()), + quad_csr_indices.data(), + static_cast(quad_csr_indices.size()), + quad_csr_offsets.data(), + static_cast(quad_csr_offsets.size())); } // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs). // Bundle row metadata, row-linear coefficients (from COLUMNS), rhs, and quadratic part together. constexpr f_t k_qcmatrix_value_scale = f_t(1); const i_t linear_row_count = static_cast(row_types.size() - quadratic_row_ids.size()); - i_t quadratic_row_id = 0; + i_t quadratic_row_id = 0; + std::vector qc_csr_values{}; + std::vector qc_csr_indices{}; + std::vector qc_csr_offsets{}; for (const auto& block : qcmatrix_blocks_) { - auto csr_result = build_csr_via_transpose( - block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); + triples_to_csr_double_transpose_flat(block.entries, + num_vars_for_quad, + num_vars_for_quad, + false, + k_qcmatrix_value_scale, + triple_csr_scratch, + qc_csr_values, + qc_csr_indices, + qc_csr_offsets); const i_t row_id = block.constraint_row_id; mps_parser_expects(row_id >= 0 && row_id < static_cast(row_types.size()), error_type_t::ValidationError, @@ -578,12 +769,12 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) A_indices[row_id].data(), static_cast(A_indices[row_id].size()), b_values[row_id], - csr_result.values.data(), - static_cast(csr_result.values.size()), - csr_result.indices.data(), - static_cast(csr_result.indices.size()), - csr_result.offsets.data(), - static_cast(csr_result.offsets.size())); + qc_csr_values.data(), + static_cast(qc_csr_values.size()), + qc_csr_indices.data(), + static_cast(qc_csr_indices.size()), + qc_csr_offsets.data(), + static_cast(qc_csr_offsets.size())); ++quadratic_row_id; } @@ -931,9 +1122,7 @@ mps_parser_t::mps_parser_t(mps_data_model_t& problem, // raft::common::nvtx::range fun_scope("mps parser"); std::vector buf = file_to_string(file); - parse_string(buf.data()); - fill_problem(problem); } From 0779f675030532b4700794002f95cccb0e1c3fdd Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 11 May 2026 07:21:07 -0700 Subject: [PATCH 35/68] --amend --- cpp/libmps_parser/src/mps_parser.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index 7938a7d82d..dadb7d868e 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -741,8 +741,6 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs). // Bundle row metadata, row-linear coefficients (from COLUMNS), rhs, and quadratic part together. constexpr f_t k_qcmatrix_value_scale = f_t(1); - const i_t linear_row_count = static_cast(row_types.size() - quadratic_row_ids.size()); - i_t quadratic_row_id = 0; std::vector qc_csr_values{}; std::vector qc_csr_indices{}; std::vector qc_csr_offsets{}; @@ -761,7 +759,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) error_type_t::ValidationError, "QCMATRIX row index %d is out of range for constraints", static_cast(row_id)); - problem.append_quadratic_constraint(linear_row_count + quadratic_row_id, + problem.append_quadratic_constraint(row_id, row_names[row_id], static_cast(row_types[row_id]), A_values[row_id].data(), @@ -775,7 +773,6 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) static_cast(qc_csr_indices.size()), qc_csr_offsets.data(), static_cast(qc_csr_offsets.size())); - ++quadratic_row_id; } if (!quadratic_row_ids.empty()) { From f14861893fb1d374ed246c32d2d4160b19676850 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 13 May 2026 01:45:21 -0700 Subject: [PATCH 36/68] switch to COO format for quadratic constraint matrices assuming they are sparse --- .../optimization_problem_interface.hpp | 11 ++- .../include/mps_parser/data_model_view.hpp | 6 +- .../include/mps_parser/mps_data_model.hpp | 20 ++-- cpp/libmps_parser/src/mps_data_model.cpp | 72 +++++++++------ cpp/libmps_parser/src/mps_parser.cpp | 36 ++++---- cpp/libmps_parser/src/mps_writer.cpp | 23 +++-- cpp/libmps_parser/tests/mps_parser_test.cpp | 54 +++++------ cpp/src/barrier/translate_soc.hpp | 91 +++++++------------ 8 files changed, 147 insertions(+), 166 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp index 8ffd38578b..b0687e9f77 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp @@ -64,9 +64,10 @@ class optimization_problem_interface_t { std::vector linear_values{}; std::vector linear_indices{}; f_t rhs_value{f_t(0)}; + /** Q in COO: parallel arrays, same length. */ + std::vector quadratic_row_indices{}; + std::vector quadratic_col_indices{}; std::vector quadratic_values{}; - std::vector quadratic_indices{}; - std::vector quadratic_offsets{}; }; virtual ~optimization_problem_interface_t() = default; @@ -89,9 +90,9 @@ class optimization_problem_interface_t { std::vector(qc.linear_values.begin(), qc.linear_values.end()), std::vector(qc.linear_indices.begin(), qc.linear_indices.end()), static_cast(qc.rhs_value), - std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end()), - std::vector(qc.quadratic_indices.begin(), qc.quadratic_indices.end()), - std::vector(qc.quadratic_offsets.begin(), qc.quadratic_offsets.end())}); + std::vector(qc.quadratic_row_indices.begin(), qc.quadratic_row_indices.end()), + std::vector(qc.quadratic_col_indices.begin(), qc.quadratic_col_indices.end()), + std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end())}); } set_quadratic_constraints(std::move(converted_constraints)); } diff --git a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp index 100492e2b0..0de188ca44 100644 --- a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp +++ b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp @@ -434,9 +434,9 @@ class data_model_view_t { std::vector(qc.linear_values.begin(), qc.linear_values.end()), std::vector(qc.linear_indices.begin(), qc.linear_indices.end()), static_cast(qc.rhs_value), - std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end()), - std::vector(qc.quadratic_indices.begin(), qc.quadratic_indices.end()), - std::vector(qc.quadratic_offsets.begin(), qc.quadratic_offsets.end())}); + std::vector(qc.quadratic_row_indices.begin(), qc.quadratic_row_indices.end()), + std::vector(qc.quadratic_col_indices.begin(), qc.quadratic_col_indices.end()), + std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end())}); } } diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 4d9c4bd8f3..45341a9ad1 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -269,7 +269,7 @@ class mps_data_model_t { * - row identity and type (from ROWS), * - sparse linear coefficients (from COLUMNS), * - RHS value (from RHS), - * - quadratic matrix Q in CSR (from QCMATRIX). + * - quadratic matrix Q in COO (SoA: row, col, value) from QCMATRIX — one triplet per nonzero. */ struct quadratic_constraint_t { /** ROWS declaration index (among all constraint rows), not an index into the linear CSR. */ @@ -281,9 +281,10 @@ class mps_data_model_t { std::vector linear_values{}; std::vector linear_indices{}; f_t rhs_value{f_t(0)}; + /** Q nonzeros: parallel arrays, same length (COO / SoA). Sorted by (row, col) in append. */ + std::vector quadratic_row_indices{}; + std::vector quadratic_col_indices{}; std::vector quadratic_values{}; - std::vector quadratic_indices{}; - std::vector quadratic_offsets{}; }; /** @@ -291,8 +292,9 @@ class mps_data_model_t { * @note Pointer+size signature is kept for current CI/toolchain compatibility; `std::span` * can be revisited later when compatibility constraints are lifted. * @param linear_values, linear_indices Same nnz; can be empty for a purely quadratic row (rare). - * @param quadratic_values, quadratic_indices CSR nnz; may be empty if Q is empty. - * @param quadratic_offsets CSR row starts; must be non-empty. + * @param quadratic_nnz Number of Q triplets; if 0, Q is empty (pointers may be null). + * @param quadratic_values, quadratic_row_indices, quadratic_col_indices COO triplets (length + * quadratic_nnz). Stored sorted by (row, col). * @param constraint_row_type MPS ROWS type; must be 'L'. 'G' and 'E' quadratic rows are not * supported. */ @@ -304,12 +306,10 @@ class mps_data_model_t { const i_t* linear_indices, i_t linear_indices_nnz, f_t rhs_value, + i_t quadratic_nnz, const f_t* quadratic_values, - i_t quadratic_size_values, - const i_t* quadratic_indices, - i_t quadratic_size_indices, - const i_t* quadratic_offsets, - i_t quadratic_size_offsets); + const i_t* quadratic_row_indices, + const i_t* quadratic_col_indices); const std::vector& get_quadratic_constraints() const; diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index b9ae16dc03..44581062e3 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace cuopt::mps_parser { @@ -229,12 +230,10 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ const i_t* linear_indices, i_t linear_indices_nnz, f_t rhs_value, + i_t quadratic_nnz, const f_t* quadratic_values, - i_t quadratic_size_values, - const i_t* quadratic_indices, - i_t quadratic_size_indices, - const i_t* quadratic_offsets, - i_t quadratic_size_offsets) + const i_t* quadratic_row_indices, + const i_t* quadratic_col_indices) { mps_parser_expects(constraint_row_index >= 0, error_type_t::ValidationError, @@ -255,22 +254,15 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ "linear_values and linear_indices cannot be null when linear_nnz > 0"); } - if (quadratic_size_values != 0) { - mps_parser_expects(quadratic_values != nullptr, - error_type_t::ValidationError, - "quadratic_values cannot be null"); - } - mps_parser_expects(quadratic_offsets != nullptr, + mps_parser_expects(quadratic_nnz >= 0, error_type_t::ValidationError, - "quadratic_offsets cannot be null"); - if (quadratic_size_indices != 0) { - mps_parser_expects(quadratic_indices != nullptr, + "quadratic_nnz must be non-negative"); + if (quadratic_nnz > 0) { + mps_parser_expects(quadratic_values != nullptr && quadratic_row_indices != nullptr && + quadratic_col_indices != nullptr, error_type_t::ValidationError, - "quadratic_indices cannot be null"); + "Q COO pointers cannot be null when quadratic_nnz > 0"); } - mps_parser_expects(quadratic_size_offsets > 0, - error_type_t::ValidationError, - "quadratic_size_offsets cannot be empty"); quadratic_constraint_t qc; qc.constraint_row_index = constraint_row_index; @@ -285,19 +277,39 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ std::copy(linear_indices, linear_indices + linear_nnz, qc.linear_indices.data()); } - qc.quadratic_values.resize(quadratic_size_values); - if (quadratic_size_values > 0) { - std::copy( - quadratic_values, quadratic_values + quadratic_size_values, qc.quadratic_values.data()); + if (quadratic_nnz == 0) { + qc.quadratic_row_indices.clear(); + qc.quadratic_col_indices.clear(); + qc.quadratic_values.clear(); + } else { + std::vector wr(static_cast(quadratic_nnz)); + std::vector wc(static_cast(quadratic_nnz)); + std::vector wv(static_cast(quadratic_nnz)); + std::copy(quadratic_row_indices, + quadratic_row_indices + quadratic_nnz, + wr.begin()); + std::copy(quadratic_col_indices, + quadratic_col_indices + quadratic_nnz, + wc.begin()); + std::copy(quadratic_values, quadratic_values + quadratic_nnz, wv.begin()); + + std::vector perm(static_cast(quadratic_nnz)); + std::iota(perm.begin(), perm.end(), size_t{0}); + std::stable_sort(perm.begin(), perm.end(), [&](size_t a, size_t b) { + if (wr[a] != wr[b]) { return wr[a] < wr[b]; } + return wc[a] < wc[b]; + }); + + qc.quadratic_row_indices.resize(static_cast(quadratic_nnz)); + qc.quadratic_col_indices.resize(static_cast(quadratic_nnz)); + qc.quadratic_values.resize(static_cast(quadratic_nnz)); + for (i_t t = 0; t < quadratic_nnz; ++t) { + const size_t ix = perm[static_cast(t)]; + qc.quadratic_row_indices[static_cast(t)] = wr[ix]; + qc.quadratic_col_indices[static_cast(t)] = wc[ix]; + qc.quadratic_values[static_cast(t)] = wv[ix]; + } } - qc.quadratic_indices.resize(quadratic_size_indices); - if (quadratic_size_indices > 0) { - std::copy( - quadratic_indices, quadratic_indices + quadratic_size_indices, qc.quadratic_indices.data()); - } - qc.quadratic_offsets.resize(quadratic_size_offsets); - std::copy( - quadratic_offsets, quadratic_offsets + quadratic_size_offsets, qc.quadratic_offsets.data()); quadratic_constraints_.push_back(std::move(qc)); } diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index dadb7d868e..3d57b5c990 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -343,7 +343,7 @@ void triples_to_csr_double_transpose_flat( if (!symmetrize_upper_triangular) { const size_t lg = ceil_log2_size_t(nnz); // Roughly: sort cost ~ nnz·lg vs ~O(n) column iterations in dense path; 48 is a tunable fudge. - constexpr size_t k_sparse_vs_dense_heuristic = 48; + constexpr size_t k_sparse_vs_dense_heuristic = 96; if (nnz * lg < k_sparse_vs_dense_heuristic * n_dim) { triples_to_csr_sparse_sorted_flat( entries, num_rows, num_cols, value_scale, scratch, out_values, out_indices, out_offsets); @@ -739,26 +739,24 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) } // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs). - // Bundle row metadata, row-linear coefficients (from COLUMNS), rhs, and quadratic part together. + // Store Q in COO (row, col, value) per block — avoids O(n) CSR row-pointer materialization. constexpr f_t k_qcmatrix_value_scale = f_t(1); - std::vector qc_csr_values{}; - std::vector qc_csr_indices{}; - std::vector qc_csr_offsets{}; for (const auto& block : qcmatrix_blocks_) { - triples_to_csr_double_transpose_flat(block.entries, - num_vars_for_quad, - num_vars_for_quad, - false, - k_qcmatrix_value_scale, - triple_csr_scratch, - qc_csr_values, - qc_csr_indices, - qc_csr_offsets); const i_t row_id = block.constraint_row_id; mps_parser_expects(row_id >= 0 && row_id < static_cast(row_types.size()), error_type_t::ValidationError, "QCMATRIX row index %d is out of range for constraints", static_cast(row_id)); + const i_t nnz = static_cast(block.entries.size()); + std::vector qc_rows(static_cast(nnz)); + std::vector qc_cols(static_cast(nnz)); + std::vector qc_vals(static_cast(nnz)); + for (i_t e = 0; e < nnz; ++e) { + qc_rows[static_cast(e)] = std::get<0>(block.entries[static_cast(e)]); + qc_cols[static_cast(e)] = std::get<1>(block.entries[static_cast(e)]); + qc_vals[static_cast(e)] = + std::get<2>(block.entries[static_cast(e)]) * k_qcmatrix_value_scale; + } problem.append_quadratic_constraint(row_id, row_names[row_id], static_cast(row_types[row_id]), @@ -767,12 +765,10 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) A_indices[row_id].data(), static_cast(A_indices[row_id].size()), b_values[row_id], - qc_csr_values.data(), - static_cast(qc_csr_values.size()), - qc_csr_indices.data(), - static_cast(qc_csr_indices.size()), - qc_csr_offsets.data(), - static_cast(qc_csr_offsets.size())); + nnz, + qc_vals.data(), + qc_rows.data(), + qc_cols.data()); } if (!quadratic_row_ids.empty()) { diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index b112b53476..5ceece7d5e 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -497,20 +497,19 @@ void mps_writer_t::write(const std::string& mps_file_path) if (problem_.has_quadratic_constraints()) { for (const auto& qc : problem_.get_quadratic_constraints()) { mps_file << "QCMATRIX " << qc.constraint_row_name << "\n"; - const i_t n_quad_rows = static_cast(qc.quadratic_offsets.size()) - 1; - for (i_t i = 0; i < n_quad_rows; ++i) { + const i_t nnz = static_cast(qc.quadratic_values.size()); + for (i_t p = 0; p < nnz; ++p) { + const i_t i = qc.quadratic_row_indices[static_cast(p)]; + const i_t j = qc.quadratic_col_indices[static_cast(p)]; + f_t v = qc.quadratic_values[static_cast(p)]; std::string row_var_name = static_cast(i) < problem_.get_variable_names().size() - ? problem_.get_variable_names()[i] + ? problem_.get_variable_names()[static_cast(i)] : "C" + std::to_string(i); - for (i_t p = qc.quadratic_offsets[i]; p < qc.quadratic_offsets[i + 1]; ++p) { - i_t j = qc.quadratic_indices[p]; - f_t v = qc.quadratic_values[p]; - std::string col_var_name = static_cast(j) < problem_.get_variable_names().size() - ? problem_.get_variable_names()[j] - : "C" + std::to_string(j); - if (v != f_t(0)) { - mps_file << " " << row_var_name << " " << col_var_name << " " << v << "\n"; - } + std::string col_var_name = static_cast(j) < problem_.get_variable_names().size() + ? problem_.get_variable_names()[static_cast(j)] + : "C" + std::to_string(j); + if (v != f_t(0)) { + mps_file << " " << row_var_name << " " << col_var_name << " " << v << "\n"; } } } diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index 669a8c5e05..97fb5c5b4d 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -871,16 +871,16 @@ TEST(qps_parser, qcmatrix_append_api) model_t::quadratic_constraint_t default_qcm; EXPECT_EQ(0, default_qcm.constraint_row_index); EXPECT_TRUE(default_qcm.quadratic_values.empty()); - EXPECT_TRUE(default_qcm.quadratic_indices.empty()); - EXPECT_TRUE(default_qcm.quadratic_offsets.empty()); + EXPECT_TRUE(default_qcm.quadratic_row_indices.empty()); + EXPECT_TRUE(default_qcm.quadratic_col_indices.empty()); EXPECT_TRUE(default_qcm.linear_values.empty()); EXPECT_TRUE(default_qcm.linear_indices.empty()); EXPECT_EQ(0.0, default_qcm.rhs_value); - // QC0: [[10, 2], [2, 2]] - const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; - const std::vector qc0_indices = {0, 1, 0, 1}; - const std::vector qc0_offsets = {0, 2, 4}; + // QC0: [[10, 2], [2, 2]] in COO (row-major triplets) + const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; + const std::vector qc0_rows = {0, 0, 1, 1}; + const std::vector qc0_cols = {0, 1, 0, 1}; const std::vector qc0_linear_values = {1.0, 1.0}; const std::vector qc0_linear_indices = {0, 1}; model.append_quadratic_constraint(0, @@ -891,17 +891,15 @@ TEST(qps_parser, qcmatrix_append_api) qc0_linear_indices.data(), qc0_linear_indices.size(), 5.0, + static_cast(qc0_values.size()), qc0_values.data(), - qc0_values.size(), - qc0_indices.data(), - qc0_indices.size(), - qc0_offsets.data(), - qc0_offsets.size()); + qc0_rows.data(), + qc0_cols.data()); // QC1: [[4, 1], [1, 6]] - const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; - const std::vector qc1_indices = {0, 1, 0, 1}; - const std::vector qc1_offsets = {0, 2, 4}; + const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; + const std::vector qc1_rows = {0, 0, 1, 1}; + const std::vector qc1_cols = {0, 1, 0, 1}; const std::vector qc1_linear_values = {3.0, 1.0}; const std::vector qc1_linear_indices = {0, 1}; model.append_quadratic_constraint(1, @@ -912,12 +910,10 @@ TEST(qps_parser, qcmatrix_append_api) qc1_linear_indices.data(), qc1_linear_indices.size(), 10.0, + static_cast(qc1_values.size()), qc1_values.data(), - qc1_values.size(), - qc1_indices.data(), - qc1_indices.size(), - qc1_offsets.data(), - qc1_offsets.size()); + qc1_rows.data(), + qc1_cols.data()); ASSERT_TRUE(model.has_quadratic_constraints()); const auto& qcs = model.get_quadratic_constraints(); @@ -930,8 +926,8 @@ TEST(qps_parser, qcmatrix_append_api) EXPECT_EQ(qc0_linear_indices, qcs[0].linear_indices); EXPECT_EQ(5.0, qcs[0].rhs_value); EXPECT_EQ(qc0_values, qcs[0].quadratic_values); - EXPECT_EQ(qc0_indices, qcs[0].quadratic_indices); - EXPECT_EQ(qc0_offsets, qcs[0].quadratic_offsets); + EXPECT_EQ(qc0_rows, qcs[0].quadratic_row_indices); + EXPECT_EQ(qc0_cols, qcs[0].quadratic_col_indices); EXPECT_EQ(1, qcs[1].constraint_row_index); EXPECT_EQ("QC1", qcs[1].constraint_row_name); @@ -940,8 +936,8 @@ TEST(qps_parser, qcmatrix_append_api) EXPECT_EQ(qc1_linear_indices, qcs[1].linear_indices); EXPECT_EQ(10.0, qcs[1].rhs_value); EXPECT_EQ(qc1_values, qcs[1].quadratic_values); - EXPECT_EQ(qc1_indices, qcs[1].quadratic_indices); - EXPECT_EQ(qc1_offsets, qcs[1].quadratic_offsets); + EXPECT_EQ(qc1_rows, qcs[1].quadratic_row_indices); + EXPECT_EQ(qc1_cols, qcs[1].quadratic_col_indices); } // QCQP MPS: each quadratic constraint bundles row + linear + rhs + quadratic. @@ -1205,16 +1201,16 @@ void compare_data_models(const mps_data_model_t& original, EXPECT_EQ(oqc[k].linear_indices[i], rq[k].linear_indices[i]); } ASSERT_EQ(oqc[k].quadratic_values.size(), rq[k].quadratic_values.size()); - ASSERT_EQ(oqc[k].quadratic_indices.size(), rq[k].quadratic_indices.size()); - ASSERT_EQ(oqc[k].quadratic_offsets.size(), rq[k].quadratic_offsets.size()); + ASSERT_EQ(oqc[k].quadratic_row_indices.size(), rq[k].quadratic_row_indices.size()); + ASSERT_EQ(oqc[k].quadratic_col_indices.size(), rq[k].quadratic_col_indices.size()); for (size_t i = 0; i < oqc[k].quadratic_values.size(); ++i) { EXPECT_NEAR(oqc[k].quadratic_values[i], rq[k].quadratic_values[i], tol); } - for (size_t i = 0; i < oqc[k].quadratic_indices.size(); ++i) { - EXPECT_EQ(oqc[k].quadratic_indices[i], rq[k].quadratic_indices[i]); + for (size_t i = 0; i < oqc[k].quadratic_row_indices.size(); ++i) { + EXPECT_EQ(oqc[k].quadratic_row_indices[i], rq[k].quadratic_row_indices[i]); } - for (size_t i = 0; i < oqc[k].quadratic_offsets.size(); ++i) { - EXPECT_EQ(oqc[k].quadratic_offsets[i], rq[k].quadratic_offsets[i]); + for (size_t i = 0; i < oqc[k].quadratic_col_indices.size(); ++i) { + EXPECT_EQ(oqc[k].quadratic_col_indices[i], rq[k].quadratic_col_indices[i]); } } } diff --git a/cpp/src/barrier/translate_soc.hpp b/cpp/src/barrier/translate_soc.hpp index cd7481850e..4a31368752 100644 --- a/cpp/src/barrier/translate_soc.hpp +++ b/cpp/src/barrier/translate_soc.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -107,43 +108,18 @@ void apply_soc_qcmatrix_conversion_for_simplex( "Quadratic constraint '%s' linear_values and linear_indices length mismatch", qc.constraint_row_name.c_str()); - cuopt_expects(qc.quadratic_offsets.size() >= 2, - error_type_t::ValidationError, - "Quadratic constraint '%s' has invalid CSR offsets (need at least 2 entries)", - qc.constraint_row_name.c_str()); - cuopt_expects(qc.quadratic_values.size() == qc.quadratic_indices.size(), + const i_t q_nnz = static_cast(qc.quadratic_values.size()); + cuopt_expects(qc.quadratic_row_indices.size() == static_cast(q_nnz) && + qc.quadratic_col_indices.size() == static_cast(q_nnz), error_type_t::ValidationError, - "Quadratic constraint '%s' quadratic_values and quadratic_indices length " - "mismatch for CSR Q", + "Quadratic constraint '%s' Q COO row/col/value length mismatch", qc.constraint_row_name.c_str()); - - const i_t q_nnz = static_cast(qc.quadratic_values.size()); cuopt_expects(q_nnz >= 1, error_type_t::ValidationError, "Quadratic constraint '%s' SOC must have at least 1 entry in Q (nnz %d)", qc.constraint_row_name.c_str(), static_cast(q_nnz)); - cuopt_expects( - qc.quadratic_offsets.size() == static_cast(n) + 1, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q must be n by n in CSR: expected %zu CSR row pointers (offsets " - "length n+1), got %zu (n = %d)", - qc.constraint_row_name.c_str(), - static_cast(n) + 1, - qc.quadratic_offsets.size(), - static_cast(n)); - cuopt_expects( - qc.quadratic_offsets[static_cast(n)] == q_nnz, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q last CSR offset %d must equal number of nonzeros (nnz) %d", - qc.constraint_row_name.c_str(), - static_cast(qc.quadratic_offsets[static_cast(n)]), - static_cast(q_nnz)); - cuopt_expects(qc.quadratic_offsets[0] == 0, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q CSR offsets[0] must be 0", - qc.constraint_row_name.c_str()); // This is the index of the auxiliary variable for the linear part of the quadratic // constraint. const i_t affine_head = qc_affine_heads[qc_i]; @@ -181,40 +157,41 @@ void apply_soc_qcmatrix_conversion_for_simplex( return std::abs(a - b) <= tol * scale; }; + // Sort COO by (row, col); O(nnz log nnz). Enforce at most one stored entry per row (SOC CSR). + std::vector perm(static_cast(q_nnz)); + std::iota(perm.begin(), perm.end(), size_t{0}); + std::sort(perm.begin(), perm.end(), [&](size_t a, size_t b) { + const i_t ra = qc.quadratic_row_indices[a]; + const i_t rb = qc.quadratic_row_indices[b]; + if (ra != rb) { return ra < rb; } + return qc.quadratic_col_indices[a] < qc.quadratic_col_indices[b]; + }); + std::vector> q_entries; q_entries.reserve(static_cast(q_nnz)); - for (i_t r = 0; r < n; ++r) { - const i_t p_beg = qc.quadratic_offsets[static_cast(r)]; - const i_t p_end = qc.quadratic_offsets[static_cast(r + 1)]; - cuopt_expects(p_beg >= 0 && p_beg <= p_end && p_end <= q_nnz, + for (size_t t = 0; t < static_cast(q_nnz); ++t) { + const size_t ix = perm[t]; + const i_t r = qc.quadratic_row_indices[ix]; + const i_t c = qc.quadratic_col_indices[ix]; + const f_t v = qc.quadratic_values[ix]; + cuopt_expects(r >= 0 && r < n && c >= 0 && c < n, error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d has invalid CSR offsets [%d, %d)", + "Quadratic constraint '%s' Q entry (%d,%d) outside [0,%d)", qc.constraint_row_name.c_str(), static_cast(r), - static_cast(p_beg), - static_cast(p_end)); - - if (p_beg == p_end) { continue; } - - cuopt_expects(p_beg + 1 == p_end, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row %d: expected at most one stored entry per " - "row (got end - beg = %d)", - qc.constraint_row_name.c_str(), - static_cast(r), - static_cast(p_end - p_beg)); - - const i_t col = qc.quadratic_indices[static_cast(p_beg)]; - const f_t v = qc.quadratic_values[static_cast(p_beg)]; - q_entries.emplace_back(r, col, v); + static_cast(c), + static_cast(n)); + if (!q_entries.empty()) { + const i_t prev_r = std::get<0>(q_entries.back()); + cuopt_expects(r != prev_r, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: expected at most one stored entry per " + "row (CSR layout); duplicate or unsorted row in COO", + qc.constraint_row_name.c_str(), + static_cast(r)); + } + q_entries.emplace_back(r, c, v); } - cuopt_expects(static_cast(q_entries.size()) == q_nnz, - error_type_t::ValidationError, - "Quadratic constraint '%s' Q row nnz mismatch (expected %d stored entries, got " - "%zu)", - qc.constraint_row_name.c_str(), - static_cast(q_nnz), - q_entries.size()); std::vector> pos_diag_rows; std::vector> neg_diag_rows; From fc0609e25a8c48e042be24aa54454970e01ec19d Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 15 May 2026 17:47:08 -0700 Subject: [PATCH 37/68] Add Ruiz scaling for SOCP. Dynamic regularization for affine and centering (separately). Better initial point for SOCP --- cpp/src/barrier/barrier.cu | 88 +++++++--- cpp/src/barrier/barrier.hpp | 2 + cpp/src/dual_simplex/scaling.cpp | 158 ++++++++++++++++-- cpp/src/dual_simplex/scaling.hpp | 6 +- cpp/src/dual_simplex/solve.cpp | 21 ++- .../dual_simplex/unit_tests/solve_barrier.cu | 6 +- 6 files changed, 229 insertions(+), 52 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index 6f91ae2945..dc2d4bbdfb 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -506,8 +506,6 @@ class iteration_data_t { i_t nnzA = A.col_start[n]; i_t nnzQ = Q.n > 0 ? Q.col_start[n] : 0; i_t factorization_size = n + m; - const f_t dual_perturb = 1e-8; - const f_t primal_perturb = 1e-8; const bool has_soc = has_cones(); const i_t m_c = cone_entry_count(); @@ -1746,6 +1744,10 @@ class iteration_data_t { i_t n_free_vars{0}; rmm::device_uvector d_is_free_; // 1 if native free (QP/SOCP augmented), 0 otherwise + // Adaptive regularization for the augmented system + f_t dual_perturb{1e-8}; + f_t primal_perturb{1e-8}; + std::unique_ptr> chol; bool has_factorization; @@ -2081,27 +2083,35 @@ int barrier_solver_t::initial_point(iteration_data_t& data) dense_vector_t dual_res(lp.num_cols); float64_t epsilon_adjust = 10.0; - // Set the initial point for the SOC case separately + // Data-dependent initial point for SOCP, following SeDuMi (Sturm, 1999). + // mu = sqrt((1 + ||b||_inf) * (1 + ||c||_inf)) + // primal and dual: x = mu * e_K, z = mu * e_K + // where e_K is the identity of the symmetric cone: + // LP block: e = 1, SOC block: e = (sqrt(2), 0, ..., 0) if (has_soc) { - const i_t cs = data.cone_start(); - // Nonnegative primal / dual (linear orthant): x_j, z_j <- 1 + const i_t cs = data.cone_start(); + const f_t norm_b = vector_norm_inf(lp.rhs); + const f_t norm_c = vector_norm_inf(lp.objective); + const f_t mu = std::sqrt((1.0 + norm_b) * (1.0 + norm_c)); + const f_t sqrt2 = std::sqrt(2.0); + const f_t x_soc = mu * sqrt2; + const f_t z_soc = mu * sqrt2; + // Linear orthant for (i_t j = 0; j < cs; ++j) { - data.x[j] = 1.0; - data.z[j] = 1.0; + data.x[j] = mu; + data.z[j] = mu; } - // Native free variables: no finite lower barrier — dual slack z_j stays at 0 (matches non-SOC - // branch and barrier algebra). if (has_free) { for (i_t j : presolve_info.free_variable_indices) { if (j < cs) { data.z[j] = 0.0; } } } - // Primal and dual SOC blocks: (t, x_bar) <- (1, 0, ..., 0) + // SOC blocks i_t off = 0; for (i_t k = 0; k < static_cast(lp.second_order_cone_dims.size()); ++k) { i_t q_k = lp.second_order_cone_dims[k]; - data.x[cs + off] = 1.0; - data.z[cs + off] = 1.0; + data.x[cs + off] = x_soc; + data.z[cs + off] = z_soc; for (i_t j = 1; j < q_k; ++j) { data.x[cs + off + j] = 0.0; data.z[cs + off + j] = 0.0; @@ -2110,8 +2120,8 @@ int barrier_solver_t::initial_point(iteration_data_t& data) } data.y.set_scalar(0.0); if (data.n_upper_bounds > 0) { - data.w.set_scalar(1.0); - data.v.set_scalar(1.0); + data.w.set_scalar(mu); + data.v.set_scalar(mu); } return 0; } @@ -2522,6 +2532,8 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t& dy, pinned_dense_vector_t& dv, pinned_dense_vector_t& dz, + f_t& dual_perturb, + f_t& primal_perturb, f_t& max_residual) { raft::common::nvtx::range fun_scope("Barrier: compute_search_direction"); @@ -2749,6 +2761,8 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t 1e-2) { + f_t old_dp = dual_perturb; + dual_perturb = std::min(max_perturb, dual_perturb * 10.0); + primal_perturb = std::min(max_perturb, primal_perturb * 10.0); + settings.log.printf(" reg UP: %e -> %e (solve_err=%e)\n", old_dp, dual_perturb, solve_err); + } else if (solve_err < 1e-4) { + f_t old_dp = dual_perturb; + dual_perturb = std::max(min_perturb, dual_perturb / 10.0); + primal_perturb = std::max(min_perturb, primal_perturb / 10.0); + if (old_dp != dual_perturb) { + settings.log.printf(" reg DOWN: %e -> %e (solve_err=%e)\n", old_dp, dual_perturb, solve_err); + } + } + } + raft::copy(data.d_dx_.data(), d_augmented_soln.data(), lp.num_cols, stream_view_); raft::copy(data.d_dy_.data(), d_augmented_soln.data() + lp.num_cols, lp.num_rows, stream_view_); raft::copy(dx.data(), data.d_dx_.data(), lp.num_cols, stream_view_); @@ -4177,6 +4210,12 @@ lp_status_t barrier_solver_t::solve(f_t start_time, const i_t iteration_limit = settings.iteration_limit; + // Separate adaptive regularization for affine and centering steps + f_t affine_dual_perturb = 1e-8; + f_t affine_primal_perturb = 1e-8; + f_t centering_dual_perturb = 1e-8; + f_t centering_primal_perturb = 1e-8; + while (iter < iteration_limit) { raft::common::nvtx::range fun_scope("Barrier: iteration"); @@ -4189,13 +4228,14 @@ lp_status_t barrier_solver_t::solve(f_t start_time, return lp_status_t::CONCURRENT_LIMIT; } - // Compute the affine step + // Compute the affine step (use adaptive regularization — system is ill-conditioned near boundary) compute_affine_rhs(data); f_t max_affine_residual = 0.0; // Update NT-scaling in gpu_compute_search_direction i_t status = gpu_compute_search_direction( - data, data.dw_aff, data.dx_aff, data.dy_aff, data.dv_aff, data.dz_aff, max_affine_residual); + data, data.dw_aff, data.dx_aff, data.dy_aff, data.dv_aff, data.dz_aff, + affine_dual_perturb, affine_primal_perturb, max_affine_residual); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Barrier solver halted\n"); return lp_status_t::CONCURRENT_LIMIT; @@ -4231,10 +4271,12 @@ lp_status_t barrier_solver_t::solve(f_t start_time, compute_cc_rhs(data, new_mu); + // Centering step: use its own adaptive regularization f_t max_corrector_residual = 0.0; status = gpu_compute_search_direction( - data, data.dw, data.dx, data.dy, data.dv, data.dz, max_corrector_residual); + data, data.dw, data.dx, data.dy, data.dv, data.dz, + centering_dual_perturb, centering_primal_perturb, max_corrector_residual); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Barrier solver halted\n"); return lp_status_t::CONCURRENT_LIMIT; @@ -4293,15 +4335,13 @@ lp_status_t barrier_solver_t::solve(f_t start_time, relative_dual_residual < data.relative_dual_residual_save && relative_complementarity_residual < data.relative_complementarity_residual_save && primal_objective == primal_objective && dual_objective == dual_objective) { - settings.log.debug( - "Saving solution: feasibility %.2e (%.2e), optimality %.2e (%.2e), complementarity " - "%.2e (%.2e)\n", + settings.log.printf( + "Saving solution at iter %d: feasibility %.2e, optimality %.2e, complementarity " + "%.2e\n", + iter, relative_primal_residual, - primal_residual_norm, relative_dual_residual, - dual_residual_norm, - relative_complementarity_residual, - complementarity_residual_norm); + relative_complementarity_residual); data.w_save = data.w; data.x_save = data.x; data.y_save = data.y; diff --git a/cpp/src/barrier/barrier.hpp b/cpp/src/barrier/barrier.hpp index 014a46db35..69655bdb3a 100644 --- a/cpp/src/barrier/barrier.hpp +++ b/cpp/src/barrier/barrier.hpp @@ -110,6 +110,8 @@ class barrier_solver_t { pinned_dense_vector_t& dy, pinned_dense_vector_t& dv, pinned_dense_vector_t& dz, + f_t& dual_perturb, + f_t& primal_perturb, f_t& max_residual); private: diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 65dcb49bfe..6634fe4a54 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -16,19 +16,126 @@ template i_t column_scaling(const lp_problem_t& unscaled, const simplex_solver_settings_t& settings, lp_problem_t& scaled, - std::vector& column_scaling) + std::vector& col_scale, + std::vector& row_scale) { scaled = unscaled; i_t m = scaled.num_rows; i_t n = scaled.num_cols; - if (!settings.scale_columns || unscaled.Q.n > 0 || !unscaled.second_order_cone_dims.empty()) { + row_scale.assign(m, 1.0); + + // ========================================================================= + // Ruiz equilibration for SOCP (and QP) problems + // ========================================================================= + // For SOCP problems, apply Ruiz equilibration: alternating row and column + // infinity-norm scaling to bring the constraint matrix close to equilibrium. + // This dramatically improves the conditioning of the augmented KKT system. + if (!unscaled.second_order_cone_dims.empty() || unscaled.Q.n > 0) { + col_scale.assign(n, 1.0); + + // Build CSR view for row-wise access + csr_matrix_t Arow(0, 0, 0); + scaled.A.to_compressed_row(Arow); + + constexpr i_t max_ruiz_iterations = 10; + for (i_t iter = 0; iter < max_ruiz_iterations; ++iter) { + f_t max_deviation = 0.0; + + // --- Row scaling: scale each row by 1/sqrt(max|a_ij|) --- + std::vector r(m); + for (i_t i = 0; i < m; ++i) { + f_t row_max = 0.0; + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + f_t a = std::abs(Arow.x[p]); + if (a > row_max) row_max = a; + } + r[i] = row_max > 0 ? 1.0 / std::sqrt(row_max) : 1.0; + max_deviation = std::max(max_deviation, std::abs(row_max - 1.0)); + } + // Apply row scaling to CSC (scaled.A) and CSR (Arow) + for (i_t j = 0; j < n; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + scaled.A.x[p] *= r[scaled.A.i[p]]; + } + } + for (i_t i = 0; i < m; ++i) { + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + Arow.x[p] *= r[i]; + } + scaled.rhs[i] *= r[i]; + row_scale[i] *= r[i]; + } + + // --- Column scaling: scale each column by 1/sqrt(max|a_ij|) --- + std::vector c(n); + for (i_t j = 0; j < n; ++j) { + f_t col_max = 0.0; + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + f_t a = std::abs(scaled.A.x[p]); + if (a > col_max) col_max = a; + } + c[j] = col_max > 0 ? 1.0 / std::sqrt(col_max) : 1.0; + max_deviation = std::max(max_deviation, std::abs(col_max - 1.0)); + } + // Apply column scaling to CSC (scaled.A) and CSR (Arow) + for (i_t j = 0; j < n; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + scaled.A.x[p] *= c[j]; + } + } + for (i_t i = 0; i < m; ++i) { + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + Arow.x[p] *= c[Arow.j[p]]; + } + } + // Scale objective: c_scaled = C * c_original + for (i_t j = 0; j < n; ++j) { + scaled.objective[j] *= c[j]; + col_scale[j] *= c[j]; + } + // Scale bounds: l_scaled = l / C, u_scaled = u / C + for (i_t j = 0; j < n; ++j) { + if (scaled.lower[j] > -1e20) scaled.lower[j] /= c[j]; + if (scaled.upper[j] < 1e20) scaled.upper[j] /= c[j]; + } + + // Scale Q if present: Q_scaled = C * Q * C + if (scaled.Q.n > 0) { + for (i_t row = 0; row < scaled.Q.m; ++row) { + for (i_t p = scaled.Q.row_start[row]; p < scaled.Q.row_start[row + 1]; ++p) { + i_t col = scaled.Q.j[p]; + scaled.Q.x[p] *= c[row] * c[col]; + } + } + } + + // Check convergence: if all row/col maxima are close to 1, stop + if (max_deviation < 0.1) break; + } + + // Compute final coefficient range for logging + f_t a_min = std::numeric_limits::max(); + f_t a_max = 0; + for (i_t p = 0; p < scaled.A.col_start[n]; ++p) { + f_t a = std::abs(scaled.A.x[p]); + if (a > 0) { + a_min = std::min(a_min, a); + a_max = std::max(a_max, a); + } + } + settings.log.printf("Ruiz equilibration: coefficient range [%e, %e]\n", a_min, a_max); + + return 0; + } + + if (!settings.scale_columns) { settings.log.printf("Skipping column scaling\n"); - column_scaling.resize(n, 1.0); + col_scale.resize(n, 1.0); return 0; } - column_scaling.resize(n); + col_scale.resize(n); f_t max = 0; f_t min = std::numeric_limits::max(); @@ -40,30 +147,29 @@ i_t column_scaling(const lp_problem_t& unscaled, const f_t x = scaled.A.x[p]; sum += x * x; } - f_t col_norm_j = column_scaling[j] = sum > 0 ? std::sqrt(sum) : 1.0; - max = std::max(col_norm_j, max); - min = std::min(col_norm_j, min); + f_t col_norm_j = col_scale[j] = sum > 0 ? std::sqrt(sum) : 1.0; + max = std::max(col_norm_j, max); + min = std::min(col_norm_j, min); } settings.log.printf("Scaling matrix. Maximum column norm %e, minimum column norm %e\n", max, min); - // C(j, j) = 1/column_scaling(j) // scaled_A = unscaled_A * C for (i_t j = 0; j < n; ++j) { const i_t col_start = scaled.A.col_start[j]; const i_t col_end = scaled.A.col_start[j + 1]; for (i_t p = col_start; p < col_end; ++p) { - scaled.A.x[p] /= column_scaling[j]; + scaled.A.x[p] /= col_scale[j]; } } // scaled_obj = C*unscaled_obj for (i_t j = 0; j < n; ++j) { - scaled.objective[j] /= column_scaling[j]; + scaled.objective[j] /= col_scale[j]; } // scaled_lower = C^{-1} * unscaled_lower // scaled_upper = C^{-1} * unscaled_upper for (i_t j = 0; j < n; ++j) { - scaled.lower[j] *= column_scaling[j]; - scaled.upper[j] *= column_scaling[j]; + scaled.lower[j] *= col_scale[j]; + scaled.upper[j] *= col_scale[j]; } for (i_t i = 0; i < unscaled.Q.n; ++i) { @@ -72,25 +178,37 @@ i_t column_scaling(const lp_problem_t& unscaled, i_t row = i; for (i_t p = row_start; p < row_end; ++p) { i_t col = unscaled.Q.j[p]; - scaled.Q.x[p] = unscaled.Q.x[p] / (column_scaling[row] * column_scaling[col]); + scaled.Q.x[p] = unscaled.Q.x[p] / (col_scale[row] * col_scale[col]); } } return 0; } template -void unscale_solution(const std::vector& column_scaling, +void unscale_solution(const std::vector& col_scale, + const std::vector& row_scale, const std::vector& scaled_x, + const std::vector& scaled_y, const std::vector& scaled_z, std::vector& unscaled_x, + std::vector& unscaled_y, std::vector& unscaled_z) { const i_t n = scaled_x.size(); unscaled_x.resize(n); unscaled_z.resize(n); for (i_t j = 0; j < n; ++j) { - unscaled_x[j] = scaled_x[j] / column_scaling[j]; - unscaled_z[j] = scaled_z[j] * column_scaling[j]; + // x_unscaled = C * x_scaled (column scaling: x_scaled = C^{-1} * x) + unscaled_x[j] = scaled_x[j] * col_scale[j]; + // z_unscaled = C^{-1} * z_scaled (dual of column scaling) + unscaled_z[j] = scaled_z[j] / col_scale[j]; + } + + const i_t m = scaled_y.size(); + unscaled_y.resize(m); + for (i_t i = 0; i < m; ++i) { + // y_unscaled = R * y_scaled (row scaling: constraint was scaled by R) + unscaled_y[i] = scaled_y[i] * row_scale[i]; } } @@ -99,12 +217,16 @@ void unscale_solution(const std::vector& column_scaling, template int column_scaling(const lp_problem_t& unscaled, const simplex_solver_settings_t& settings, lp_problem_t& scaled, - std::vector& column_scaling); + std::vector& col_scale, + std::vector& row_scale); -template void unscale_solution(const std::vector& column_scaling, +template void unscale_solution(const std::vector& col_scale, + const std::vector& row_scale, const std::vector& scaled_x, + const std::vector& scaled_y, const std::vector& scaled_z, std::vector& unscaled_x, + std::vector& unscaled_y, std::vector& unscaled_z); #endif diff --git a/cpp/src/dual_simplex/scaling.hpp b/cpp/src/dual_simplex/scaling.hpp index 120660c765..6db0ac818c 100644 --- a/cpp/src/dual_simplex/scaling.hpp +++ b/cpp/src/dual_simplex/scaling.hpp @@ -20,13 +20,17 @@ template i_t column_scaling(const lp_problem_t& unscaled, const simplex_solver_settings_t& settings, lp_problem_t& scaled, - std::vector& column_scaling); + std::vector& column_scaling, + std::vector& row_scaling); template void unscale_solution(const std::vector& column_scaling, + const std::vector& row_scaling, const std::vector& scaled_x, + const std::vector& scaled_y, const std::vector& scaled_z, std::vector& unscaled_x, + std::vector& unscaled_y, std::vector& unscaled_z); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 3755931a12..856baef64e 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -206,9 +206,10 @@ lp_status_t solve_linear_program_with_advanced_basis( presolved_lp.num_cols, presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; + std::vector row_scales_simplex; { raft::common::nvtx::range scope_scaling("DualSimplex::scaling"); - column_scaling(presolved_lp, settings, lp, column_scales); + column_scaling(presolved_lp, settings, lp, column_scales, row_scales_simplex); } assert(presolved_lp.num_cols == lp.num_cols); lp_problem_t phase1_problem(original_lp.handle_ptr, 1, 1, 1); @@ -329,12 +330,15 @@ lp_status_t solve_linear_program_with_advanced_basis( } if (status == dual::status_t::OPTIMAL) { std::vector unscaled_x(lp.num_cols); + std::vector unscaled_y(lp.num_rows); std::vector unscaled_z(lp.num_cols); - unscale_solution(column_scales, solution.x, solution.z, unscaled_x, unscaled_z); + unscale_solution( + column_scales, row_scales_simplex, solution.x, solution.y, solution.z, + unscaled_x, unscaled_y, unscaled_z); uncrush_solution(presolve_info, settings, unscaled_x, - solution.y, + unscaled_y, unscaled_z, original_solution.x, original_solution.y, @@ -408,7 +412,8 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us presolved_lp.num_cols, presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; - column_scaling(presolved_lp, barrier_settings, barrier_lp, column_scales); + std::vector row_scales; + column_scaling(presolved_lp, barrier_settings, barrier_lp, column_scales, row_scales); // Solve using barrier lp_solution_t barrier_solution(barrier_lp.num_rows, barrier_lp.num_cols); @@ -452,9 +457,11 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us #endif // Unscale the solution std::vector unscaled_x(barrier_lp.num_cols); + std::vector unscaled_y(barrier_lp.num_rows); std::vector unscaled_z(barrier_lp.num_cols); unscale_solution( - column_scales, barrier_solution.x, barrier_solution.z, unscaled_x, unscaled_z); + column_scales, row_scales, barrier_solution.x, barrier_solution.y, barrier_solution.z, + unscaled_x, unscaled_y, unscaled_z); std::vector residual = presolved_lp.rhs; matrix_vector_multiply(presolved_lp.A, 1.0, unscaled_x, -1.0, residual); @@ -468,7 +475,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us unscaled_dual_residual[j] -= presolved_lp.objective[j]; } matrix_transpose_vector_multiply( - presolved_lp.A, 1.0, barrier_solution.y, 1.0, unscaled_dual_residual); + presolved_lp.A, 1.0, unscaled_y, 1.0, unscaled_dual_residual); f_t unscaled_dual_residual_norm = vector_norm_inf(unscaled_dual_residual); settings.log.printf( "Unscaled Dual infeasibility (abs/rel): %.2e/%.2e\n", @@ -480,7 +487,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us uncrush_solution(presolve_info, barrier_settings, unscaled_x, - barrier_solution.y, + unscaled_y, unscaled_z, lp_solution.x, lp_solution.y, diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index d630bfac18..2b121e035f 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -227,7 +227,8 @@ TEST(barrier, cone_metadata_reindexed_when_slack_is_inserted_before_cones) original_lp.num_cols, original_lp.A.col_start[original_lp.num_cols]); std::vector column_scales; - column_scaling(original_lp, settings, barrier_lp, column_scales); + std::vector row_scales; + column_scaling(original_lp, settings, barrier_lp, column_scales, row_scales); EXPECT_EQ(barrier_lp.second_order_cone_dims, user_problem.second_order_cone_dims); EXPECT_EQ(barrier_lp.cone_var_start, 2); @@ -294,7 +295,8 @@ TEST(barrier, presolve_reindexes_cone_start_after_empty_column_removal) presolved_lp.num_cols, presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; - ASSERT_EQ(column_scaling(presolved_lp, settings, barrier_lp, column_scales), 0); + std::vector row_scales; + ASSERT_EQ(column_scaling(presolved_lp, settings, barrier_lp, column_scales, row_scales), 0); EXPECT_EQ(barrier_lp.cone_var_start, 0); } From df6ff35f0ce28c0a1f2817ffb47b5f347ab32a44 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 15 May 2026 19:45:23 -0700 Subject: [PATCH 38/68] Conditionally apply ruiz scaling --- cpp/src/dual_simplex/scaling.cpp | 55 +++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 6634fe4a54..03a4925a67 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -31,10 +31,37 @@ i_t column_scaling(const lp_problem_t& unscaled, // For SOCP problems, apply Ruiz equilibration: alternating row and column // infinity-norm scaling to bring the constraint matrix close to equilibrium. // This dramatically improves the conditioning of the augmented KKT system. + // Ruiz equilibration for SOCP (and QP) problems, applied only when the + // constraint matrix has a large row-norm imbalance. if (!unscaled.second_order_cone_dims.empty() || unscaled.Q.n > 0) { col_scale.assign(n, 1.0); - // Build CSR view for row-wise access + // Decide whether Ruiz scaling is needed by checking row-norm imbalance. + // If max_row_norm / min_row_norm is small, the matrix is already well-conditioned + // and scaling can hurt (e.g. by amplifying tiny noise coefficients). + csr_matrix_t Arow_check(0, 0, 0); + scaled.A.to_compressed_row(Arow_check); + f_t max_row_norm = 0; + f_t min_row_norm = std::numeric_limits::max(); + for (i_t i = 0; i < m; ++i) { + f_t row_norm = 0; + for (i_t p = Arow_check.row_start[i]; p < Arow_check.row_start[i + 1]; ++p) { + f_t a = std::abs(Arow_check.x[p]); + if (a > row_norm) row_norm = a; + } + if (row_norm > 0) { + max_row_norm = std::max(max_row_norm, row_norm); + min_row_norm = std::min(min_row_norm, row_norm); + } + } + f_t row_norm_ratio = (min_row_norm > 0) ? max_row_norm / min_row_norm : 1.0; + + if (row_norm_ratio < 100.0) { + settings.log.printf("Skipping Ruiz equilibration (row norm ratio %.1f < 100)\n", row_norm_ratio); + return 0; + } + + // Apply Ruiz equilibration csr_matrix_t Arow(0, 0, 0); scaled.A.to_compressed_row(Arow); @@ -45,15 +72,14 @@ i_t column_scaling(const lp_problem_t& unscaled, // --- Row scaling: scale each row by 1/sqrt(max|a_ij|) --- std::vector r(m); for (i_t i = 0; i < m; ++i) { - f_t row_max = 0.0; + f_t rm = 0.0; for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { f_t a = std::abs(Arow.x[p]); - if (a > row_max) row_max = a; + if (a > rm) rm = a; } - r[i] = row_max > 0 ? 1.0 / std::sqrt(row_max) : 1.0; - max_deviation = std::max(max_deviation, std::abs(row_max - 1.0)); + r[i] = rm > 0 ? 1.0 / std::sqrt(rm) : 1.0; + max_deviation = std::max(max_deviation, std::abs(rm - 1.0)); } - // Apply row scaling to CSC (scaled.A) and CSR (Arow) for (i_t j = 0; j < n; ++j) { for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { scaled.A.x[p] *= r[scaled.A.i[p]]; @@ -70,15 +96,14 @@ i_t column_scaling(const lp_problem_t& unscaled, // --- Column scaling: scale each column by 1/sqrt(max|a_ij|) --- std::vector c(n); for (i_t j = 0; j < n; ++j) { - f_t col_max = 0.0; + f_t cm = 0.0; for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { f_t a = std::abs(scaled.A.x[p]); - if (a > col_max) col_max = a; + if (a > cm) cm = a; } - c[j] = col_max > 0 ? 1.0 / std::sqrt(col_max) : 1.0; - max_deviation = std::max(max_deviation, std::abs(col_max - 1.0)); + c[j] = cm > 0 ? 1.0 / std::sqrt(cm) : 1.0; + max_deviation = std::max(max_deviation, std::abs(cm - 1.0)); } - // Apply column scaling to CSC (scaled.A) and CSR (Arow) for (i_t j = 0; j < n; ++j) { for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { scaled.A.x[p] *= c[j]; @@ -89,18 +114,14 @@ i_t column_scaling(const lp_problem_t& unscaled, Arow.x[p] *= c[Arow.j[p]]; } } - // Scale objective: c_scaled = C * c_original for (i_t j = 0; j < n; ++j) { scaled.objective[j] *= c[j]; col_scale[j] *= c[j]; } - // Scale bounds: l_scaled = l / C, u_scaled = u / C for (i_t j = 0; j < n; ++j) { if (scaled.lower[j] > -1e20) scaled.lower[j] /= c[j]; if (scaled.upper[j] < 1e20) scaled.upper[j] /= c[j]; } - - // Scale Q if present: Q_scaled = C * Q * C if (scaled.Q.n > 0) { for (i_t row = 0; row < scaled.Q.m; ++row) { for (i_t p = scaled.Q.row_start[row]; p < scaled.Q.row_start[row + 1]; ++p) { @@ -109,12 +130,9 @@ i_t column_scaling(const lp_problem_t& unscaled, } } } - - // Check convergence: if all row/col maxima are close to 1, stop if (max_deviation < 0.1) break; } - // Compute final coefficient range for logging f_t a_min = std::numeric_limits::max(); f_t a_max = 0; for (i_t p = 0; p < scaled.A.col_start[n]; ++p) { @@ -125,7 +143,6 @@ i_t column_scaling(const lp_problem_t& unscaled, } } settings.log.printf("Ruiz equilibration: coefficient range [%e, %e]\n", a_min, a_max); - return 0; } From 3cfc7d5b7b4d752d083f51e4ef69bf682c9e7e57 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 15 May 2026 20:27:43 -0700 Subject: [PATCH 39/68] Be careful scaling cone variables --- cpp/src/dual_simplex/scaling.cpp | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 03a4925a67..6fc9544ed4 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -94,8 +94,12 @@ i_t column_scaling(const lp_problem_t& unscaled, } // --- Column scaling: scale each column by 1/sqrt(max|a_ij|) --- + // For cone variables, use a uniform scale per cone block to preserve SOC structure. std::vector c(n); - for (i_t j = 0; j < n; ++j) { + const i_t cone_start = unscaled.second_order_cone_dims.empty() ? n : unscaled.cone_var_start; + + // Linear columns: scale independently + for (i_t j = 0; j < cone_start; ++j) { f_t cm = 0.0; for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { f_t a = std::abs(scaled.A.x[p]); @@ -104,6 +108,26 @@ i_t column_scaling(const lp_problem_t& unscaled, c[j] = cm > 0 ? 1.0 / std::sqrt(cm) : 1.0; max_deviation = std::max(max_deviation, std::abs(cm - 1.0)); } + + // Cone columns: uniform scale per cone block + i_t cone_off = cone_start; + for (i_t k = 0; k < static_cast(unscaled.second_order_cone_dims.size()); ++k) { + i_t q_k = unscaled.second_order_cone_dims[k]; + // Find max column inf-norm across all columns in this cone + f_t cone_max = 0.0; + for (i_t j = cone_off; j < cone_off + q_k; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + f_t a = std::abs(scaled.A.x[p]); + if (a > cone_max) cone_max = a; + } + } + f_t cone_scale = cone_max > 0 ? 1.0 / std::sqrt(cone_max) : 1.0; + max_deviation = std::max(max_deviation, std::abs(cone_max - 1.0)); + for (i_t j = cone_off; j < cone_off + q_k; ++j) { + c[j] = cone_scale; + } + cone_off += q_k; + } for (i_t j = 0; j < n; ++j) { for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { scaled.A.x[p] *= c[j]; From 71b7a0f49584ca0af7ac2434a777ad78c20afa14 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Sat, 16 May 2026 11:58:54 -0700 Subject: [PATCH 40/68] add regularization for soc Hessian blocks --- cpp/src/barrier/barrier.cu | 10 +++++----- cpp/src/barrier/second_order_cone_kernels.cuh | 3 +-- cpp/src/dual_simplex/scaling.cpp | 3 +++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index dc2d4bbdfb..d59fdcc8bb 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -689,10 +689,12 @@ class iteration_data_t { cuopt_assert(error.norm1() <= 1e-2, "|| Aug - Aug^T ||_1 > 1e-2"); #endif } else { - // Update the augmented matrix with the new diagonal values for primal and dual variables + const i_t linear_n = has_soc ? cone_start() : n; + + // Primal diagonal: linear block includes dual_perturb; SOC block is filled by scatter below. thrust::for_each_n(rmm::exec_policy(handle_ptr->get_stream()), thrust::make_counting_iterator(0), - i_t(n), + linear_n, [span_x = cuopt::make_span(device_augmented.x), span_diag_indices = cuopt::make_span(d_augmented_diagonal_indices_), span_q_diag = cuopt::make_span(d_Q_diag_), @@ -702,8 +704,8 @@ class iteration_data_t { span_x[span_diag_indices[j]] = -q_diag - span_diag[j] - dual_perturb_value; }); - RAFT_CHECK_CUDA(handle_ptr->get_stream()); + thrust::for_each_n(rmm::exec_policy(handle_ptr->get_stream()), thrust::make_counting_iterator(n), i_t(m), @@ -714,8 +716,6 @@ class iteration_data_t { }); RAFT_CHECK_CUDA(handle_ptr->get_stream()); - // Update the augmented matrix with the new diagonal blocks for soc constraints - // Overwrites the diagonal updates for primal variables of soc constraints if (has_soc) { scatter_hessian_into_augmented(cones(), device_augmented.x, diff --git a/cpp/src/barrier/second_order_cone_kernels.cuh b/cpp/src/barrier/second_order_cone_kernels.cuh index 045f68430f..29c70601e1 100644 --- a/cpp/src/barrier/second_order_cone_kernels.cuh +++ b/cpp/src/barrier/second_order_cone_kernels.cuh @@ -859,7 +859,7 @@ __global__ void __launch_bounds__(soc_block_size) const auto u_c = (c == 0) ? w0 : w[off + c]; auto val = f_t{2} * u_r * eta_sq * u_c; const auto diag_correction = (r == 0) ? -eta_sq : eta_sq; - if (r == c) { val += diag_correction; } + if (r == c) { val += diag_correction + dual_perturb_value; } augmented_x[csr_indices[e]] = -val - q_values[e]; } @@ -888,7 +888,6 @@ void scatter_hessian_into_augmented(const cone_data_t& cones, thrust::inclusive_scan( rmm::exec_policy(stream), block_sizes, block_sizes + cones.n_cones, block_offsets.begin() + 1); - // TODO: use dual_perturb_value for regularization const auto grid = raft::ceildiv(count, soc_block_size); scatter_hessian_into_augmented_kernel <<>>(cuopt::make_span(augmented_x), diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 6fc9544ed4..cef7c1c403 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -142,6 +142,9 @@ i_t column_scaling(const lp_problem_t& unscaled, scaled.objective[j] *= c[j]; col_scale[j] *= c[j]; } + // Bounds use +/-inf for unbounded sides (see types.hpp). Use +/-1e20 as a practical + // sentinel: we do not expect finite bounds beyond this magnitude, and skipping scale + // on |bound| >= 1e20 avoids overflow when dividing very large limits by small c[j]. for (i_t j = 0; j < n; ++j) { if (scaled.lower[j] > -1e20) scaled.lower[j] /= c[j]; if (scaled.upper[j] < 1e20) scaled.upper[j] /= c[j]; From a033c2377ad23a1776eb641e54d2b57fb0380228 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Sun, 17 May 2026 03:02:12 -0700 Subject: [PATCH 41/68] update operations for column scaling that fixes the buggy output in unscale_solution, due to the addition of Ruiz scaling --- cpp/src/dual_simplex/scaling.cpp | 39 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 3ac6bfddbf..2db6c75b43 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -182,8 +182,8 @@ i_t column_scaling(const lp_problem_t& unscaled, col_scale.resize(n); - f_t max = 0; - f_t min = std::numeric_limits::max(); + f_t max_col_norm = 0; + f_t min_col_norm = std::numeric_limits::max(); for (i_t j = 0; j < n; ++j) { const i_t col_start = scaled.A.col_start[j]; const i_t col_end = scaled.A.col_start[j + 1]; @@ -192,38 +192,33 @@ i_t column_scaling(const lp_problem_t& unscaled, const f_t x = scaled.A.x[p]; sum += x * x; } - f_t col_norm_j = col_scale[j] = sum > 0 ? std::sqrt(sum) : 1.0; - max = std::max(col_norm_j, max); - min = std::min(col_norm_j, min); + const f_t col_norm = sum > 0 ? std::sqrt(sum) : 1.0; + col_scale[j] = f_t(1) / col_norm; + max_col_norm = std::max(col_norm, max_col_norm); + min_col_norm = std::min(col_norm, min_col_norm); } - settings.log.printf("Scaling matrix. Maximum column norm %e, minimum column norm %e\n", max, min); + settings.log.printf("Scaling matrix. Maximum column norm %e, minimum column norm %e\n", + max_col_norm, + min_col_norm); // scaled_A = unscaled_A * C for (i_t j = 0; j < n; ++j) { const i_t col_start = scaled.A.col_start[j]; const i_t col_end = scaled.A.col_start[j + 1]; for (i_t p = col_start; p < col_end; ++p) { - scaled.A.x[p] /= col_scale[j]; + scaled.A.x[p] *= col_scale[j]; } - } - // scaled_obj = C*unscaled_obj - for (i_t j = 0; j < n; ++j) { - scaled.objective[j] /= col_scale[j]; - } - // scaled_lower = C^{-1} * unscaled_lower - // scaled_upper = C^{-1} * unscaled_upper - for (i_t j = 0; j < n; ++j) { - scaled.lower[j] *= col_scale[j]; - scaled.upper[j] *= col_scale[j]; + scaled.objective[j] *= col_scale[j]; + if (scaled.lower[j] > -1e20) scaled.lower[j] /= col_scale[j]; + if (scaled.upper[j] < 1e20) scaled.upper[j] /= col_scale[j]; } for (i_t i = 0; i < unscaled.Q.n; ++i) { const i_t row_start = unscaled.Q.row_start[i]; const i_t row_end = unscaled.Q.row_start[i + 1]; - i_t row = i; for (i_t p = row_start; p < row_end; ++p) { - i_t col = unscaled.Q.j[p]; - scaled.Q.x[p] = unscaled.Q.x[p] / (col_scale[row] * col_scale[col]); + const i_t col = unscaled.Q.j[p]; + scaled.Q.x[p] = unscaled.Q.x[p] * col_scale[i] * col_scale[col]; } } return 0; @@ -243,9 +238,9 @@ void unscale_solution(const std::vector& col_scale, unscaled_x.resize(n); unscaled_z.resize(n); for (i_t j = 0; j < n; ++j) { - // x_unscaled = C * x_scaled (column scaling: x_scaled = C^{-1} * x) + // column_scaling multiplies A(:,j) and c_j by col_scale[j], divides bounds: + // x_orig = col_scale .* x_scaled, z_orig = z_scaled / col_scale. unscaled_x[j] = scaled_x[j] * col_scale[j]; - // z_unscaled = C^{-1} * z_scaled (dual of column scaling) unscaled_z[j] = scaled_z[j] / col_scale[j]; } From 54197563e11a80650730ddf0e094436ee77263ee Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Sun, 17 May 2026 03:03:47 -0700 Subject: [PATCH 42/68] update unit tests; tighting tolerance and update free variable test --- .../dual_simplex/unit_tests/solve_barrier.cu | 27 ++++++++++--------- cpp/tests/qp/unit_tests/no_constraints.cu | 2 ++ cpp/tests/qp/unit_tests/two_variable_test.cu | 1 + 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index de4dfc3bcc..a97bcd885e 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -300,8 +300,11 @@ TEST(barrier, presolve_reindexes_cone_start_after_empty_column_removal) EXPECT_EQ(barrier_lp.cone_var_start, 0); } -TEST(barrier, presolve_packs_free_variable_partner_before_cones) +TEST(barrier, presolve_keeps_native_free_variables_before_cones) { + // Layout: [x0, x1 | cone x2, x3, x4] with x0, x1 free and a 3-dimensional SOC block. + // SOCP barrier presolve keeps native free variables (no x = v - w split); cone_var_start + // and column count stay unchanged. raft::handle_t handle{}; init_handler(&handle); @@ -328,8 +331,6 @@ TEST(barrier, presolve_packs_free_variable_partner_before_cones) user_problem.rhs = {1.0}; user_problem.row_sense = {'E'}; - // Two free linear vars ensure the new implied-bound pass cannot fully - // eliminate the free-variable expansion path before the cone block. user_problem.lower = {-inf, -inf, 0.0, 0.0, 0.0}; user_problem.upper.assign(n, inf); user_problem.num_range_rows = 0; @@ -352,14 +353,17 @@ TEST(barrier, presolve_packs_free_variable_partner_before_cones) lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); ASSERT_EQ(presolve(original_lp, settings, presolved_lp, presolve_info), 0); - EXPECT_EQ(presolved_lp.num_cols, 7); - EXPECT_EQ(presolved_lp.cone_var_start, 4); + EXPECT_EQ(presolved_lp.num_cols, 5); + EXPECT_EQ(presolved_lp.cone_var_start, 2); EXPECT_EQ(presolved_lp.second_order_cone_dims, std::vector({3})); - ASSERT_EQ(presolve_info.free_variable_pairs.size(), 4); - EXPECT_EQ(presolve_info.free_variable_pairs[0], 0); - EXPECT_EQ(presolve_info.free_variable_pairs[1], 2); - EXPECT_EQ(presolve_info.free_variable_pairs[2], 1); - EXPECT_EQ(presolve_info.free_variable_pairs[3], 3); + EXPECT_TRUE(presolve_info.free_variable_pairs.empty()); + ASSERT_EQ(presolve_info.free_variable_indices.size(), 2); + EXPECT_EQ(presolve_info.free_variable_indices[0], 0); + EXPECT_EQ(presolve_info.free_variable_indices[1], 1); + EXPECT_EQ(presolved_lp.lower[0], -inf); + EXPECT_EQ(presolved_lp.lower[1], -inf); + EXPECT_EQ(presolved_lp.upper[0], inf); + EXPECT_EQ(presolved_lp.upper[1], inf); } TEST(barrier, uncrush_solution_removes_non_tail_free_variable_partner) @@ -868,8 +872,7 @@ TEST(barrier, free_linear_prefix_is_uncrushed_correctly_with_soc_block) // u = 1 // (t, u, v) in Q^3 // - // Presolve splits the free linear variable into a partner column before the - // cone block, so the returned user-space solution must uncrush back to + // Native free variable l is kept through presolve; end-to-end solve returns // l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. raft::handle_t handle{}; init_handler(&handle); diff --git a/cpp/tests/qp/unit_tests/no_constraints.cu b/cpp/tests/qp/unit_tests/no_constraints.cu index 75190b37fc..dc99a2ce5c 100644 --- a/cpp/tests/qp/unit_tests/no_constraints.cu +++ b/cpp/tests/qp/unit_tests/no_constraints.cu @@ -52,6 +52,8 @@ TEST(no_constraints_test, simple_test) op_problem.set_quadratic_objective_matrix(Q_values_host, 2, Q_indices_host, 2, Q_offsets_host, 3); auto settings = cuopt::linear_programming::pdlp_solver_settings_t(); + // Tighter than default: unconstrained QP needs small x at the origin, not just small objective. + settings.barrier_relative_complementarity_tolerance = 1e-12; auto solution = cuopt::linear_programming::solve_lp(op_problem, settings); EXPECT_EQ(solution.get_termination_status(), diff --git a/cpp/tests/qp/unit_tests/two_variable_test.cu b/cpp/tests/qp/unit_tests/two_variable_test.cu index aac618994a..f1fb4852ec 100644 --- a/cpp/tests/qp/unit_tests/two_variable_test.cu +++ b/cpp/tests/qp/unit_tests/two_variable_test.cu @@ -59,6 +59,7 @@ TEST(two_variable_test, simple_test) op_problem.set_quadratic_objective_matrix(Q_values_host, 2, Q_indices_host, 2, Q_offsets_host, 3); auto settings = cuopt::linear_programming::pdlp_solver_settings_t(); + settings.barrier_relative_complementarity_tolerance = 1e-12; auto solution = cuopt::linear_programming::solve_lp(op_problem, settings); EXPECT_EQ(solution.get_termination_status(), From 2e27d61167ee414f006bc7368155cd08bc78b770 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 03:20:32 -0700 Subject: [PATCH 43/68] add python API for second-order cone constraints --- .../data_model/data_model.pxd | 17 + .../data_model/data_model.py | 159 +++++++ .../data_model/data_model_wrapper.pxd | 4 + .../data_model/data_model_wrapper.pyx | 119 +++++- .../test_socp_barrier.py | 392 ++++++++++++++++++ 5 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd index 4a83f3a058..e38e630d3a 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd @@ -12,6 +12,21 @@ from libcpp.string cimport string from libcpp.vector cimport vector +cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuopt::linear_programming::io" nogil: # noqa + + cdef cppclass mps_data_model_t[i_t, f_t]: + cppclass quadratic_constraint_t: + int constraint_row_index + string constraint_row_name + char constraint_row_type + vector[double] linear_values + vector[int] linear_indices + double rhs_value + vector[int] quadratic_row_indices + vector[int] quadratic_col_indices + vector[double] quadratic_values + + cdef extern from "cuopt/linear_programming/io/data_model_view.hpp" namespace "cuopt::linear_programming::io" nogil: # noqa cdef cppclass data_model_view_t[i_t, f_t]: @@ -54,6 +69,8 @@ cdef extern from "cuopt/linear_programming/io/data_model_view.hpp" namespace "cu void set_row_names(const vector[string] row_names) except + void set_problem_name(const string problem_name) except + void set_objective_name(const string objective_name) except + + void set_quadratic_constraints( + vector[mps_data_model_t[i_t, f_t].quadratic_constraint_t] constraints) except + cdef extern from "cuopt/linear_programming/io/writer.hpp" namespace "cuopt::linear_programming::io" nogil: # noqa diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index 648809eac1..4522bd53c0 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -288,6 +288,165 @@ def set_quadratic_objective_matrix(self, Q_values, Q_indices, Q_offsets): """ super().set_quadratic_objective_matrix(Q_values, Q_indices, Q_offsets) + @catch_cuopt_exception + def clear_quadratic_constraints(self): + """ + Remove all quadratic (QCMATRIX) constraints from the model. + + Quadratic constraints are converted to second-order cone form internally + and solved with the barrier method. + """ + super().clear_quadratic_constraints() + + @catch_cuopt_exception + def add_quadratic_constraint( + self, + constraint_row_index, + constraint_row_name="", + linear_values=None, + linear_indices=None, + rhs_value=0.0, + quadratic_values=None, + quadratic_row_indices=None, + quadratic_col_indices=None, + ): + """ + Add one quadratic constraint in MPS QCMATRIX form (``<=`` row). + + Each constraint has a linear part (optional) and a quadratic part in COO + format. Call multiple times to add several QCMATRIX rows; each call must + use a distinct ``constraint_row_index``. + + Parameters + ---------- + constraint_row_index : int + ROWS declaration index for this quadratic row (among all rows). + constraint_row_name : str, optional + Optional row name (for MPS export). + linear_values, linear_indices : array-like, optional + Sparse linear coefficients on the same variable index space. + rhs_value : float, optional + Right-hand side of the quadratic row. + quadratic_values, quadratic_row_indices, quadratic_col_indices : array-like + COO triplets for the quadratic matrix Q in + ``linear^T x + x^T Q x <= rhs_value``. + + Notes + ----- + When any quadratic constraint is present, cuOpt selects the barrier + solver and converts QCMATRIX rows to second-order cones. + """ + super().add_quadratic_constraint( + constraint_row_index, + constraint_row_name, + linear_values, + linear_indices, + rhs_value, + quadratic_values, + quadratic_row_indices, + quadratic_col_indices, + ) + + @catch_cuopt_exception + def add_second_order_cone_constraint( + self, + variable_indices, + constraint_row_index, + constraint_row_name="", + ): + """ + Add a second-order cone constraint in QCMATRIX (Lorentz) form. + + Encodes ``sum_{i>0} x_i^2 - x_0^2 <= 0`` for + ``variable_indices = [x_0, x_1, ..., x_{k-1}]``, which is equivalent to + ``x_0 >= sqrt(sum_{i>0} x_i^2)`` when ``x_0 >= 0``. Call multiple times + to add several cones; each call must use a distinct ``constraint_row_index``. + + Parameters + ---------- + variable_indices : sequence of int + Variable indices ``[head, tail_1, ..., tail_{k-1}]`` with ``k >= 2``. + constraint_row_index : int + ROWS index for this quadratic constraint row. + constraint_row_name : str, optional + Optional row name. + + See Also + -------- + add_quadratic_constraint + """ + import numpy as np + + indices = [int(i) for i in variable_indices] + if len(indices) < 2: + raise ValueError("variable_indices must contain at least head and one tail index") + + tails = indices[1:] + q_values = [-1.0] + [1.0] * len(tails) + + self.add_quadratic_constraint( + constraint_row_index=constraint_row_index, + constraint_row_name=constraint_row_name, + rhs_value=0.0, + quadratic_values=np.array(q_values, dtype=np.float64), + quadratic_row_indices=np.array(indices, dtype=np.int32), + quadratic_col_indices=np.array(indices, dtype=np.int32), + ) + + @catch_cuopt_exception + def add_rotated_second_order_cone_constraint( + self, + variable_indices, + constraint_row_index, + constraint_row_name="", + ): + """ + Add a rotated second-order cone constraint in QCMATRIX form. + + Encodes ``-2 x_0 x_1 + sum_{i>1} x_i^2 <= 0`` for + ``variable_indices = [x_0, x_1, x_2, ..., x_{k-1}]`` with ``k >= 3``, which is + equivalent to ``2 x_0 x_1 >= sum_{i>1} x_i^2`` when ``x_0, x_1 >= 0``. Call + multiple times to add several cones; each call must use a distinct + ``constraint_row_index``. + + Parameters + ---------- + variable_indices : sequence of int + Variable indices ``[head_0, head_1, tail_1, ..., tail_{k-2}]``. + constraint_row_index : int + ROWS index for this quadratic constraint row. + constraint_row_name : str, optional + Optional row name. + + See Also + -------- + add_second_order_cone_constraint + add_quadratic_constraint + """ + import numpy as np + + indices = [int(i) for i in variable_indices] + if len(indices) < 3: + raise ValueError( + "variable_indices must contain two head indices and at least one tail index" + ) + + head0, head1 = indices[0], indices[1] + tails = indices[2:] + + q_rows = [head0, head1] + tails + q_cols = [head1, head0] + tails + q_values = [-1.0, -1.0] + [1.0] * len(tails) + + self.add_quadratic_constraint( + constraint_row_index=constraint_row_index, + constraint_row_name=constraint_row_name, + rhs_value=0.0, + quadratic_values=np.array(q_values, dtype=np.float64), + quadratic_row_indices=np.array(q_rows, dtype=np.int32), + quadratic_col_indices=np.array(q_cols, dtype=np.int32), + ) + @catch_cuopt_exception def set_variable_lower_bounds(self, variable_lower_bounds): """ diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd index 6c798a0f6a..9f9f5ba5cf 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd @@ -12,3 +12,7 @@ from libcpp.memory cimport unique_ptr cdef class DataModel: cdef unique_ptr[data_model_view_t[int, double]] c_data_model_view + + cdef void _set_cpp_quadratic_constraints( + self, data_model_view_t[int, double]* c_data_model_view + ) diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index 6d22b046f6..ba1ec051ef 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -7,7 +7,7 @@ # cython: embedsignature = True # cython: language_level = 3 -from .data_model cimport data_model_view_t, write_mps +from .data_model cimport data_model_view_t, mps_data_model_t, write_mps import warnings @@ -15,7 +15,9 @@ import numpy as np from libc.stdint cimport uintptr_t from libcpp.memory cimport unique_ptr +from libcpp.string cimport string from libcpp.utility cimport move +from libcpp.vector cimport vector def type_cast(np_obj, np_type, name): @@ -69,6 +71,70 @@ cdef class DataModel: self.variable_types = np.array([]) self.variable_names = np.array([]) self.row_names = np.array([]) + self.quadratic_constraints = [] + + def clear_quadratic_constraints(self): + self.quadratic_constraints = [] + + def add_quadratic_constraint( + self, + constraint_row_index, + constraint_row_name="", + linear_values=None, + linear_indices=None, + rhs_value=0.0, + quadratic_values=None, + quadratic_row_indices=None, + quadratic_col_indices=None, + ): + linear_values = ( + np.array([], dtype=np.float64) + if linear_values is None + else type_cast(linear_values, np.float64, "linear_values") + ) + linear_indices = ( + np.array([], dtype=np.int32) + if linear_indices is None + else type_cast(linear_indices, np.int32, "linear_indices") + ) + if linear_values.shape[0] != linear_indices.shape[0]: + raise ValueError("linear_values and linear_indices must have the same length") + quadratic_values = ( + np.array([], dtype=np.float64) + if quadratic_values is None + else type_cast(quadratic_values, np.float64, "quadratic_values") + ) + quadratic_row_indices = ( + np.array([], dtype=np.int32) + if quadratic_row_indices is None + else type_cast(quadratic_row_indices, np.int32, "quadratic_row_indices") + ) + quadratic_col_indices = ( + np.array([], dtype=np.int32) + if quadratic_col_indices is None + else type_cast(quadratic_col_indices, np.int32, "quadratic_col_indices") + ) + if not ( + quadratic_values.shape[0] + == quadratic_row_indices.shape[0] + == quadratic_col_indices.shape[0] + ): + raise ValueError( + "quadratic_values, quadratic_row_indices, and " + "quadratic_col_indices must have the same length" + ) + self.quadratic_constraints.append( + { + "constraint_row_index": int(constraint_row_index), + "constraint_row_name": str(constraint_row_name), + "linear_values": linear_values, + "linear_indices": linear_indices, + "rhs_value": float(rhs_value), + "quadratic_values": quadratic_values, + "quadratic_row_indices": quadratic_row_indices, + "quadratic_col_indices": quadratic_col_indices, + } + ) def set_maximize(self, maximize): self.maximize = maximize @@ -386,6 +452,57 @@ cdef class DataModel: self.get_initial_dual_solution().shape[0] ) + if self.quadratic_constraints: + self._set_cpp_quadratic_constraints(c_data_model_view) + + cdef void _set_cpp_quadratic_constraints( + self, data_model_view_t[int, double]* c_data_model_view + ): + cdef vector[mps_data_model_t[int, double].quadratic_constraint_t] constraints + cdef mps_data_model_t[int, double].quadratic_constraint_t qc + cdef dict item + cdef size_t i + cdef uintptr_t c_linear_values + cdef uintptr_t c_linear_indices + cdef uintptr_t c_quadratic_values + cdef uintptr_t c_quadratic_row_indices + cdef uintptr_t c_quadratic_col_indices + cdef size_t linear_nnz + cdef size_t quadratic_nnz + + for item in self.quadratic_constraints: + qc.constraint_row_index = item["constraint_row_index"] + qc.constraint_row_name = item["constraint_row_name"].encode("utf-8") + qc.constraint_row_type = ord("L") + qc.rhs_value = item["rhs_value"] + + linear_nnz = item["linear_values"].shape[0] + qc.linear_values.resize(linear_nnz) + qc.linear_indices.resize(linear_nnz) + if linear_nnz > 0: + c_linear_values = get_data_ptr(item["linear_values"]) + c_linear_indices = get_data_ptr(item["linear_indices"]) + for i in range(linear_nnz): + qc.linear_values[i] = (c_linear_values)[i] + qc.linear_indices[i] = (c_linear_indices)[i] + + quadratic_nnz = item["quadratic_values"].shape[0] + qc.quadratic_values.resize(quadratic_nnz) + qc.quadratic_row_indices.resize(quadratic_nnz) + qc.quadratic_col_indices.resize(quadratic_nnz) + if quadratic_nnz > 0: + c_quadratic_values = get_data_ptr(item["quadratic_values"]) + c_quadratic_row_indices = get_data_ptr(item["quadratic_row_indices"]) + c_quadratic_col_indices = get_data_ptr(item["quadratic_col_indices"]) + for i in range(quadratic_nnz): + qc.quadratic_values[i] = (c_quadratic_values)[i] + qc.quadratic_row_indices[i] = (c_quadratic_row_indices)[i] + qc.quadratic_col_indices[i] = (c_quadratic_col_indices)[i] + + constraints.push_back(qc) + + c_data_model_view.set_quadratic_constraints(constraints) + def writeMPS(self, user_problem_file): self.variable_types = type_cast( self.variable_types, "S1", "variable_types" diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py new file mode 100644 index 0000000000..aa8fa91f76 --- /dev/null +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py @@ -0,0 +1,392 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Tests for SOCP problems solved via the barrier method. + +Quadratic constraints (QCMATRIX form) are converted to second-order cones and +solved with the barrier interior-point method. +""" + +import numpy as np +import pytest + +from cuopt.linear_programming import data_model, solver, solver_settings +from cuopt.linear_programming.solver.solver_parameters import CUOPT_METHOD, CUOPT_PRESOLVE +from cuopt.linear_programming.solver_settings import SolverMethod + + +def _barrier_settings(): + settings = solver_settings.SolverSettings() + settings.set_parameter(CUOPT_METHOD, SolverMethod.Barrier) + settings.set_parameter(CUOPT_PRESOLVE, 0) + return settings + + +def _solve_socp(dm, settings=None): + if settings is None: + settings = _barrier_settings() + return solver.Solve(dm, settings) + + +def _lorentz_qcoo(variable_indices): + """COO triplets for sum_{i>0} x_i^2 - x_0^2 <= 0 on variable_indices.""" + indices = [int(i) for i in variable_indices] + q_values = [-1.0] + [1.0] * (len(indices) - 1) + return ( + np.array(q_values, dtype=np.float64), + np.array(indices, dtype=np.int32), + np.array(indices, dtype=np.int32), + ) + + +def _rotated_qcoo(variable_indices): + """COO triplets for -2 x_0 x_1 + sum_{i>1} x_i^2 <= 0 on variable_indices.""" + indices = [int(i) for i in variable_indices] + head0, head1 = indices[0], indices[1] + tails = indices[2:] + q_rows = [head0, head1] + tails + q_cols = [head1, head0] + tails + q_values = [-1.0, -1.0] + [1.0] * len(tails) + return ( + np.array(q_values, dtype=np.float64), + np.array(q_rows, dtype=np.int32), + np.array(q_cols, dtype=np.int32), + ) + + +def test_quadratic_constraint_api(): + dm = data_model.DataModel() + + dm.add_quadratic_constraint( + constraint_row_index=0, + constraint_row_name="qc0", + quadratic_values=np.array([-1.0, 1.0, 1.0], dtype=np.float64), + quadratic_row_indices=np.array([0, 1, 2], dtype=np.int32), + quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), + ) + dm.clear_quadratic_constraints() + dm.add_second_order_cone_constraint( + variable_indices=[0, 1, 2], + constraint_row_index=1, + ) + + +def test_rotated_second_order_cone_constraint_api(): + dm = data_model.DataModel() + dm.add_rotated_second_order_cone_constraint( + variable_indices=[0, 1, 2], + constraint_row_index=0, + ) + dm.clear_quadratic_constraints() + + qv, qr, qc = _rotated_qcoo([3, 4, 5, 6]) + dm.add_quadratic_constraint( + constraint_row_index=1, + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, + ) + + +def test_rotated_second_order_cone_min_tail(): + # minimize x2 + # subject to x0 = 1, x1 = 1 + # 2 x0 x1 >= x2^2 (rotated SOC on (x0, x1, x2)) + # + # Optimal: x* = (1, 1, 0), obj* = 0 + + dm = data_model.DataModel() + + A_values = np.array([1.0, 1.0], dtype=np.float64) + A_indices = np.array([0, 1], dtype=np.int32) + A_offsets = np.array([0, 1, 2], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([0.0, 0.0, 1.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + dm.add_rotated_second_order_cone_constraint( + variable_indices=[0, 1, 2], + constraint_row_index=2, + constraint_row_name="rsoc", + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(0.0, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[2]) == pytest.approx(0.0, abs=1e-3) + + +def test_multiple_rotated_second_order_cone_constraints(): + # Two disjoint rotated cones: + # 2 x0 x1 >= x2^2 with x0 = x1 = 1 + # 2 x3 x4 >= x5^2 with x3 = 2, x4 = 1 + # minimize x2 + x5 -> 0 + + dm = data_model.DataModel() + + A_values = np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float64) + A_indices = np.array([0, 1, 3, 4], dtype=np.int32) + A_offsets = np.array([0, 1, 2, 3, 4], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0, 1.0, 2.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0, 1.0, 2.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients( + np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float64) + ) + dm.set_variable_lower_bounds(np.zeros(6, dtype=np.float64)) + dm.set_variable_upper_bounds(np.full(6, np.inf, dtype=np.float64)) + + dm.add_rotated_second_order_cone_constraint( + variable_indices=[0, 1, 2], + constraint_row_index=4, + constraint_row_name="rsoc1", + ) + dm.add_rotated_second_order_cone_constraint( + variable_indices=[3, 4, 5], + constraint_row_index=5, + constraint_row_name="rsoc2", + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(0.0, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[2]) == pytest.approx(0.0, abs=1e-3) + assert x[3] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[4] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[5]) == pytest.approx(0.0, abs=1e-3) + + +def test_multiple_quadratic_constraints(): + """Two Lorentz QCMATRIX rows via add_quadratic_constraint (same problem as two SOCs).""" + # Variables [l1, l2 | t1, u1, v1 | t2, u2, v2]; see test_multiple_second_order_cone_constraints. + dm = data_model.DataModel() + + A_values = np.array([1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0], dtype=np.float64) + A_indices = np.array([0, 3, 1, 6, 0, 1, 0, 1], dtype=np.int32) + A_offsets = np.array([0, 2, 4, 6, 8], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients( + np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0], dtype=np.float64) + ) + dm.set_variable_lower_bounds(np.zeros(8, dtype=np.float64)) + dm.set_variable_upper_bounds(np.full(8, np.inf, dtype=np.float64)) + + qv0, qr0, qc0 = _lorentz_qcoo([2, 3, 4]) + dm.add_quadratic_constraint( + constraint_row_index=4, + constraint_row_name="soc1", + quadratic_values=qv0, + quadratic_row_indices=qr0, + quadratic_col_indices=qc0, + ) + + qv1, qr1, qc1 = _lorentz_qcoo([5, 6, 7]) + dm.add_quadratic_constraint( + constraint_row_index=5, + constraint_row_name="soc2", + quadratic_values=qv1, + quadratic_row_indices=qr1, + quadratic_col_indices=qc1, + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(3.0, rel=1e-3, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[2] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[3] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert abs(x[4]) == pytest.approx(0.0, abs=1e-3) + assert x[5] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[6] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[7]) == pytest.approx(0.0, abs=1e-3) + + +def test_multiple_second_order_cone_constraints(): + # Port of C++ mixed_linear_and_two_soc_blocks (solve_barrier.cu). + # + # Variables [l1, l2 | t1, u1, v1 | t2, u2, v2]; Lorentz cones on (t1,u1,v1) and (t2,u2,v2). + # + # minimize t1 + t2 + # subject to l1 - u1 = 0, l2 - u2 = 0, l1 + l2 = 3, l1 - l2 = 1 + # + # Optimal: l1*=2, l2*=1, t1*=2, u1*=2, v1*=0, t2*=1, u2*=1, v2*=0, obj*=3. + + dm = data_model.DataModel() + + A_values = np.array([1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0], dtype=np.float64) + A_indices = np.array([0, 3, 1, 6, 0, 1, 0, 1], dtype=np.int32) + A_offsets = np.array([0, 2, 4, 6, 8], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients( + np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0], dtype=np.float64) + ) + dm.set_variable_lower_bounds(np.zeros(8, dtype=np.float64)) + dm.set_variable_upper_bounds(np.full(8, np.inf, dtype=np.float64)) + + dm.add_second_order_cone_constraint( + variable_indices=[2, 3, 4], + constraint_row_index=4, + constraint_row_name="soc1", + ) + dm.add_second_order_cone_constraint( + variable_indices=[5, 6, 7], + constraint_row_index=5, + constraint_row_name="soc2", + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(3.0, rel=1e-3, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[2] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[3] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert abs(x[4]) == pytest.approx(0.0, abs=1e-3) + assert x[5] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[6] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[7]) == pytest.approx(0.0, abs=1e-3) + + +def test_socp_min_x0_norm_constraint(): + # minimize x0 + # subject to x1 = 1 + # (x0, x1, x2) in Lorentz cone (x0^2 >= x1^2 + x2^2, x0 >= 0) + # + # Optimal: x* = (1, 1, 0), obj* = 1 + + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + dm.add_second_order_cone_constraint( + variable_indices=[0, 1, 2], + constraint_row_index=1, + constraint_row_name="soc", + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[2]) == pytest.approx(0.0, abs=1e-3) + + +def test_socp_mixed_linear_and_cone_block(): + # Variables [l, t, u, v]; Lorentz cone on (t, u, v). + # + # minimize l + # subject to l - t = 0 + # u = 1 + # (t, u, v) in Lorentz cone + # + # Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + + dm = data_model.DataModel() + + A_values = np.array([1.0, -1.0, 1.0], dtype=np.float64) + A_indices = np.array([0, 1, 2], dtype=np.int32) + A_offsets = np.array([0, 2, 3], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([0.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([0.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients( + np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float64) + ) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf, np.inf], dtype=np.float64) + ) + + dm.add_second_order_cone_constraint( + variable_indices=[1, 2, 3], + constraint_row_index=2, + constraint_row_name="soc", + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[2] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[3]) == pytest.approx(0.0, abs=1e-3) + + +def test_socp_barrier_relative_tolerances(): + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + dm.add_second_order_cone_constraint( + variable_indices=[0, 1, 2], + constraint_row_index=1, + ) + + settings = _barrier_settings() + settings.set_parameter("barrier_relative_feasibility_tolerance", 1e-10) + settings.set_parameter("barrier_relative_optimality_tolerance", 1e-10) + settings.set_parameter("barrier_relative_complementarity_tolerance", 1e-10) + + solution = solver.Solve(dm, settings) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-4, abs=1e-4) From 9b96791958bf94283192bd852f54f203d27c8194 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 06:51:18 -0700 Subject: [PATCH 44/68] add duplicate for soc variables if there are also BOUND constraints on them --- cpp/src/barrier/translate_soc.hpp | 93 ++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/cpp/src/barrier/translate_soc.hpp b/cpp/src/barrier/translate_soc.hpp index 4a31368752..10d6b88297 100644 --- a/cpp/src/barrier/translate_soc.hpp +++ b/cpp/src/barrier/translate_soc.hpp @@ -753,10 +753,10 @@ void apply_soc_qcmatrix_conversion_for_simplex( } for (const auto& [alias, original] : cone_alias_pairs) { + // Cone copies are not box-constrained; linking rows tie them to the linear original. user_problem.lower[static_cast(alias)] = - user_problem.lower[static_cast(original)]; - user_problem.upper[static_cast(alias)] = - user_problem.upper[static_cast(original)]; + -std::numeric_limits::infinity(); + user_problem.upper[static_cast(alias)] = std::numeric_limits::infinity(); user_problem.var_types[static_cast(alias)] = user_problem.var_types[static_cast(original)]; // Keep objective unchanged: alias coefficient stays zero and alias==original links @@ -802,6 +802,93 @@ void apply_soc_qcmatrix_conversion_for_simplex( } } + // Box-bounded originals cannot sit in the cone block: introduce a free cone copy and + // alias - original = 0 so linear rows/objective keep the bounded variable. + { + const f_t neg_inf = -std::numeric_limits::infinity(); + const f_t pos_inf = std::numeric_limits::infinity(); + auto is_box_free = [&](i_t j) { + return user_problem.lower[static_cast(j)] == neg_inf && + user_problem.upper[static_cast(j)] == pos_inf; + }; + + std::vector> bound_split_pairs; // (cone_alias, linear_original) + + for (auto& cone : cone_vars) { + for (auto& var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(n_prob)); + if (!is_box_free(var)) { + const i_t alias = static_cast(n_prob + bound_split_pairs.size()); + bound_split_pairs.emplace_back(alias, var); + var = alias; + } + } + } + + if (!bound_split_pairs.empty()) { + const i_t n_old = n_prob; + const i_t n_new = static_cast(n_old + bound_split_pairs.size()); + const i_t m_old = csr_A.m; + const i_t m_new = static_cast(m_old + bound_split_pairs.size()); + + user_problem.objective.resize(static_cast(n_new), f_t(0)); + user_problem.lower.resize(static_cast(n_new), neg_inf); + user_problem.upper.resize(static_cast(n_new), pos_inf); + user_problem.var_types.resize( + static_cast(n_new), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_new)); + } + + for (const auto& [alias, original] : bound_split_pairs) { + user_problem.var_types[static_cast(alias)] = + user_problem.var_types[static_cast(original)]; + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(alias)] = + "_CUOPT_cone_bound_split_" + std::to_string(static_cast(alias - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_new)); + user_problem.row_sense.resize(static_cast(m_new)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_new)); + } + + csr_A.n = n_new; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_new; + i_t row_idx = m_old; + for (const auto& [alias, original] : bound_split_pairs) { + eq_row.i = {alias, original}; + eq_row.x = {f_t(1), f_t(-1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_idx)] = + "_CUOPT_cone_bound_split_link_" + std::to_string(static_cast(row_idx - m_old)); + } + ++row_idx; + } + + cuopt_expects(row_idx == m_new, + error_type_t::RuntimeError, + "Internal error: cone bound-split linking row count mismatch"); + cuopt_expects(csr_A.m == m_new, + error_type_t::RuntimeError, + "Internal error: CSR row count after cone bound-split linking"); + + n_prob = n_new; + } + } + is_cone_var.assign(static_cast(n_prob), 0); for (const auto& cone : cone_vars) { for (const i_t var : cone) { From 2c56e531e565d57b9eb2ada3979047dc5555c790 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 11:11:23 -0700 Subject: [PATCH 45/68] update for python API --- cpp/src/io/mps_writer.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cpp/src/io/mps_writer.cpp b/cpp/src/io/mps_writer.cpp index b06a6421e6..094534d88e 100644 --- a/cpp/src/io/mps_writer.cpp +++ b/cpp/src/io/mps_writer.cpp @@ -157,7 +157,15 @@ void mps_writer_t::write(const std::string& mps_file_path) std::vector constraint_bounds(problem_.get_constraint_bounds().size()); std::vector variable_lower_bounds(problem_.get_variable_lower_bounds().size()); std::vector variable_upper_bounds(problem_.get_variable_upper_bounds().size()); - std::vector variable_types(problem_.get_variable_types().size()); + // Default unset variable types to continuous ('C'); API models often omit set_variable_types. + std::vector variable_types(static_cast(n_variables), 'C'); + { + const auto& src_types = problem_.get_variable_types(); + const size_t n_copy = std::min(src_types.size(), variable_types.size()); + for (size_t j = 0; j < n_copy; ++j) { + variable_types[j] = src_types[j]; + } + } std::vector row_types(problem_.get_row_types().size()); std::vector constraint_matrix_offsets(problem_.get_constraint_matrix_offsets().size()); std::vector constraint_matrix_indices(problem_.get_constraint_matrix_indices().size()); @@ -178,9 +186,6 @@ void mps_writer_t::write(const std::string& mps_file_path) problem_.get_variable_upper_bounds().data(), problem_.get_variable_upper_bounds().data() + problem_.get_variable_upper_bounds().size(), variable_upper_bounds.data()); - std::copy(problem_.get_variable_types().data(), - problem_.get_variable_types().data() + problem_.get_variable_types().size(), - variable_types.data()); std::copy(problem_.get_row_types().data(), problem_.get_row_types().data() + problem_.get_row_types().size(), row_types.data()); From 73e11db116dc298be50a450049db00095ea9f5d0 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 11:12:00 -0700 Subject: [PATCH 46/68] --amend --- .../data_model/data_model.py | 140 +++------- .../data_model/data_model_wrapper.pyx | 23 +- .../linear_programming/mps_parser/parser.pxd | 12 + .../mps_parser/parser_wrapper.pyx | 48 +++- .../cuopt/cuopt/linear_programming/problem.py | 158 ++++++++++- .../test_socp_barrier.py | 245 +++++++++--------- .../test_socp_parser.py | 109 ++++++++ 7 files changed, 497 insertions(+), 238 deletions(-) create mode 100644 python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index 4522bd53c0..1490c6cfb7 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -4,6 +4,8 @@ import os import time +import numpy as np + from . import data_model_wrapper from .utilities import catch_cuopt_exception @@ -288,6 +290,15 @@ def set_quadratic_objective_matrix(self, Q_values, Q_indices, Q_offsets): """ super().set_quadratic_objective_matrix(Q_values, Q_indices, Q_offsets) + def get_quadratic_constraints(self): + """ + Return quadratic (QCMATRIX) constraints appended to this model. + + Each entry is a dict with keys including ``constraint_row_index``, + ``constraint_row_name``, ``constraint_row_type``, COO arrays, and ``rhs_value``. + """ + return self.quadratic_constraints + @catch_cuopt_exception def clear_quadratic_constraints(self): """ @@ -309,9 +320,10 @@ def add_quadratic_constraint( quadratic_values=None, quadratic_row_indices=None, quadratic_col_indices=None, + sense="L", ): """ - Add one quadratic constraint in MPS QCMATRIX form (``<=`` row). + Add one quadratic constraint in MPS QCMATRIX form. Each constraint has a linear part (optional) and a quadratic part in COO format. Call multiple times to add several QCMATRIX rows; each call must @@ -329,13 +341,36 @@ def add_quadratic_constraint( Right-hand side of the quadratic row. quadratic_values, quadratic_row_indices, quadratic_col_indices : array-like COO triplets for the quadratic matrix Q in - ``linear^T x + x^T Q x <= rhs_value``. + ``linear^T x + x^T Q x {sense} rhs_value``. + sense : str, optional + MPS row type: ``'L'`` (default, ``<=``) or ``'G'`` (``>=``), same values as + :meth:`set_row_types`. ``'G'`` is converted to ``'L'`` by negating coefficients and + rhs. Equality (``'E'``) is not supported. Notes ----- When any quadratic constraint is present, cuOpt selects the barrier solver and converts QCMATRIX rows to second-order cones. """ + if hasattr(sense, "value"): + sense = sense.value + if sense == "E": + raise ValueError( + "Equality quadratic constraints are not supported; use two inequality " + "constraints or build the model with Problem.addQuadraticConstraint." + ) + if sense not in ("L", "G"): + raise ValueError( + f"Invalid sense {sense!r}; use 'L' or 'G' like set_row_types " + "(equality 'E' is not supported)." + ) + if sense == "G": + if linear_values is not None: + linear_values = -np.asarray(linear_values, dtype=np.float64) + if quadratic_values is not None: + quadratic_values = -np.asarray(quadratic_values, dtype=np.float64) + rhs_value = -float(rhs_value) + sense = "L" super().add_quadratic_constraint( constraint_row_index, constraint_row_name, @@ -345,106 +380,7 @@ def add_quadratic_constraint( quadratic_values, quadratic_row_indices, quadratic_col_indices, - ) - - @catch_cuopt_exception - def add_second_order_cone_constraint( - self, - variable_indices, - constraint_row_index, - constraint_row_name="", - ): - """ - Add a second-order cone constraint in QCMATRIX (Lorentz) form. - - Encodes ``sum_{i>0} x_i^2 - x_0^2 <= 0`` for - ``variable_indices = [x_0, x_1, ..., x_{k-1}]``, which is equivalent to - ``x_0 >= sqrt(sum_{i>0} x_i^2)`` when ``x_0 >= 0``. Call multiple times - to add several cones; each call must use a distinct ``constraint_row_index``. - - Parameters - ---------- - variable_indices : sequence of int - Variable indices ``[head, tail_1, ..., tail_{k-1}]`` with ``k >= 2``. - constraint_row_index : int - ROWS index for this quadratic constraint row. - constraint_row_name : str, optional - Optional row name. - - See Also - -------- - add_quadratic_constraint - """ - import numpy as np - - indices = [int(i) for i in variable_indices] - if len(indices) < 2: - raise ValueError("variable_indices must contain at least head and one tail index") - - tails = indices[1:] - q_values = [-1.0] + [1.0] * len(tails) - - self.add_quadratic_constraint( - constraint_row_index=constraint_row_index, - constraint_row_name=constraint_row_name, - rhs_value=0.0, - quadratic_values=np.array(q_values, dtype=np.float64), - quadratic_row_indices=np.array(indices, dtype=np.int32), - quadratic_col_indices=np.array(indices, dtype=np.int32), - ) - - @catch_cuopt_exception - def add_rotated_second_order_cone_constraint( - self, - variable_indices, - constraint_row_index, - constraint_row_name="", - ): - """ - Add a rotated second-order cone constraint in QCMATRIX form. - - Encodes ``-2 x_0 x_1 + sum_{i>1} x_i^2 <= 0`` for - ``variable_indices = [x_0, x_1, x_2, ..., x_{k-1}]`` with ``k >= 3``, which is - equivalent to ``2 x_0 x_1 >= sum_{i>1} x_i^2`` when ``x_0, x_1 >= 0``. Call - multiple times to add several cones; each call must use a distinct - ``constraint_row_index``. - - Parameters - ---------- - variable_indices : sequence of int - Variable indices ``[head_0, head_1, tail_1, ..., tail_{k-2}]``. - constraint_row_index : int - ROWS index for this quadratic constraint row. - constraint_row_name : str, optional - Optional row name. - - See Also - -------- - add_second_order_cone_constraint - add_quadratic_constraint - """ - import numpy as np - - indices = [int(i) for i in variable_indices] - if len(indices) < 3: - raise ValueError( - "variable_indices must contain two head indices and at least one tail index" - ) - - head0, head1 = indices[0], indices[1] - tails = indices[2:] - - q_rows = [head0, head1] + tails - q_cols = [head1, head0] + tails - q_values = [-1.0, -1.0] + [1.0] * len(tails) - - self.add_quadratic_constraint( - constraint_row_index=constraint_row_index, - constraint_row_name=constraint_row_name, - rhs_value=0.0, - quadratic_values=np.array(q_values, dtype=np.float64), - quadratic_row_indices=np.array(q_rows, dtype=np.int32), - quadratic_col_indices=np.array(q_cols, dtype=np.int32), + constraint_row_type=sense, ) @catch_cuopt_exception diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index ba1ec051ef..ef1b962d59 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -21,6 +21,8 @@ from libcpp.vector cimport vector def type_cast(np_obj, np_type, name): + if not isinstance(np_obj, np.ndarray): + np_obj = np.asarray(np_obj) obj_type = np_obj.dtype if ((np.issubdtype(np_type, np.floating) and @@ -86,6 +88,7 @@ cdef class DataModel: quadratic_values=None, quadratic_row_indices=None, quadratic_col_indices=None, + constraint_row_type="L", ): linear_values = ( np.array([], dtype=np.float64) @@ -123,10 +126,18 @@ cdef class DataModel: "quadratic_values, quadratic_row_indices, and " "quadratic_col_indices must have the same length" ) + row_type = str(constraint_row_type) + if row_type != "L": + raise ValueError( + "constraint_row_type must be 'L' for quadratic constraints; " + "use DataModel.add_quadratic_constraint(sense='G') to normalize " + "'>=' rows." + ) self.quadratic_constraints.append( { "constraint_row_index": int(constraint_row_index), "constraint_row_name": str(constraint_row_name), + "constraint_row_type": row_type, "linear_values": linear_values, "linear_indices": linear_indices, "rhs_value": float(rhs_value), @@ -473,7 +484,7 @@ cdef class DataModel: for item in self.quadratic_constraints: qc.constraint_row_index = item["constraint_row_index"] qc.constraint_row_name = item["constraint_row_name"].encode("utf-8") - qc.constraint_row_type = ord("L") + qc.constraint_row_type = ord(item.get("constraint_row_type", "L")) qc.rhs_value = item["rhs_value"] linear_nnz = item["linear_values"].shape[0] @@ -504,9 +515,13 @@ cdef class DataModel: c_data_model_view.set_quadratic_constraints(constraints) def writeMPS(self, user_problem_file): - self.variable_types = type_cast( - self.variable_types, "S1", "variable_types" - ) + n_vars = self.get_variable_lower_bounds().shape[0] + if self.variable_types.shape[0] == 0 and n_vars > 0: + self.variable_types = np.array(["C"] * n_vars, dtype="S1") + else: + self.variable_types = type_cast( + self.variable_types, "S1", "variable_types" + ) self.set_data_model_view() write_mps(self.c_data_model_view.get()[0], user_problem_file.encode('utf-8')) diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd b/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd index b4875a0bca..26dd99a7f4 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd @@ -15,6 +15,17 @@ from libcpp.vector cimport vector cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuopt::linear_programming::io": # noqa cdef cppclass mps_data_model_t[i_t, f_t]: + cppclass quadratic_constraint_t: + int constraint_row_index + string constraint_row_name + char constraint_row_type + vector[double] linear_values + vector[int] linear_indices + double rhs_value + vector[int] quadratic_row_indices + vector[int] quadratic_col_indices + vector[double] quadratic_values + bool maximize_ vector[f_t] A_ vector[i_t] A_indices_ @@ -36,6 +47,7 @@ cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuo vector[char] row_types_ string objective_name_ string problem_name_ + const vector[quadratic_constraint_t]& get_quadratic_constraints() const cdef extern from "cuopt/linear_programming/io/utilities/cython_mps_parser.hpp" namespace "cuopt::cython": # noqa diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx index ffd6ac43f3..5998245202 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx @@ -16,7 +16,7 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move -from .parser cimport call_parse_mps +from .parser cimport call_parse_mps, mps_data_model_t import warnings @@ -143,4 +143,50 @@ def ParseMps(mps_file_path, fixed_mps_formats): data_model.set_objective_name(dm_ret.objective_name_.decode()) data_model.set_problem_name(dm_ret.problem_name_.decode()) + cdef size_t qi + cdef size_t n_qc = dm_ret.get_quadratic_constraints().size() + cdef mps_data_model_t[int, double].quadratic_constraint_t qc + cdef size_t linear_nnz, quadratic_nnz + cdef double[:] linear_values_view + cdef int[:] linear_indices_view + cdef double[:] quadratic_values_view + cdef int[:] quadratic_row_indices_view + cdef int[:] quadratic_col_indices_view + + for qi in range(n_qc): + qc = dm_ret.get_quadratic_constraints()[qi] + linear_nnz = qc.linear_values.size() + if linear_nnz > 0: + linear_values_view = qc.linear_values.data() + linear_values = np.asarray(linear_values_view).copy() + linear_indices_view = qc.linear_indices.data() + linear_indices = np.asarray(linear_indices_view).copy() + else: + linear_values = None + linear_indices = None + + quadratic_nnz = qc.quadratic_values.size() + if quadratic_nnz > 0: + quadratic_values_view = qc.quadratic_values.data() + quadratic_values = np.asarray(quadratic_values_view).copy() + quadratic_row_indices_view = qc.quadratic_row_indices.data() + quadratic_row_indices = np.asarray(quadratic_row_indices_view).copy() + quadratic_col_indices_view = qc.quadratic_col_indices.data() + quadratic_col_indices = np.asarray(quadratic_col_indices_view).copy() + else: + quadratic_values = None + quadratic_row_indices = None + quadratic_col_indices = None + + data_model.add_quadratic_constraint( + qc.constraint_row_index, + qc.constraint_row_name.decode("utf-8"), + linear_values=linear_values, + linear_indices=linear_indices, + rhs_value=qc.rhs_value, + quadratic_values=quadratic_values, + quadratic_row_indices=quadratic_row_indices, + quadratic_col_indices=quadratic_col_indices, + ) + return data_model diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index f7f874fafd..957355abcc 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -253,8 +253,10 @@ def __mul__(self, other): qvars1 = [self] * len(other.vars) qvars2 = other.vars qcoeffs = other.coefficients - vars = [self] - coeffs = [other.constant] + vars, coeffs = [], [] + if other.constant != 0.0: + vars = [self] + coeffs = [other.constant] return QuadraticExpression( qvars1=qvars1, qvars2=qvars2, @@ -731,6 +733,9 @@ def __rsub__(self, other): # other - self -> other + self * -1.0 return other + self * -1.0 + def __neg__(self): + return self * -1.0 + def __imul__(self, other): # Compute expr *= constant match other: @@ -831,13 +836,105 @@ def __truediv__(self, other): ) def __le__(self, other): - raise Exception("Quadratic constraints not supported") + match other: + case int() | float(): + return QuadraticConstraint(self, LE, float(other)) + case Variable() | LinearExpression() | QuadraticExpression(): + return QuadraticConstraint(self - other, LE, 0.0) + case _: + raise ValueError( + "Can't compare QuadraticExpression with type %s" + % type(other).__name__ + ) def __ge__(self, other): - raise Exception("Quadratic constraints not supported") + match other: + case int() | float(): + return QuadraticConstraint(self, GE, float(other)) + case Variable() | LinearExpression() | QuadraticExpression(): + return QuadraticConstraint(self - other, GE, 0.0) + case _: + raise ValueError( + "Can't compare QuadraticExpression with type %s" + % type(other).__name__ + ) def __eq__(self, other): - raise Exception("Quadratic constraints not supported") + raise ValueError( + "Quadratic equality constraints are not supported; " + "use addQuadraticConstraint with QCMATRIX data or two inequality constraints." + ) + + +def _quadratic_expression_to_qcmatrix(expr, rhs): + """Build QCMATRIX COO data for ``expr <= rhs`` (constant moved to rhs).""" + rhs_value = float(rhs) - expr.constant + linear_indices = [] + linear_values = [] + for var, coeff in zip(expr.vars, expr.coefficients): + if coeff != 0.0: + linear_indices.append(var.index) + linear_values.append(coeff) + quadratic_row_indices = [var.index for var in expr.qvars1] + quadratic_col_indices = [var.index for var in expr.qvars2] + quadratic_values = list(expr.qcoefficients) + if expr.qmatrix is not None: + q_coo = expr.qmatrix.tocoo() + for row, col, value in zip(q_coo.row, q_coo.col, q_coo.data): + quadratic_row_indices.append(expr.qvars[row].index) + quadratic_col_indices.append(expr.qvars[col].index) + quadratic_values.append(value) + return ( + linear_values, + linear_indices, + quadratic_values, + quadratic_row_indices, + quadratic_col_indices, + rhs_value, + ) + + +class QuadraticConstraint: + """ + Quadratic constraint in QCMATRIX form. + + Created from :py:class:`QuadraticExpression` comparisons or + :py:meth:`Problem.addQuadraticConstraint`. Row sense (``<=`` / ``>=``) is + normalized when the problem is exported to :class:`~cuopt.linear_programming.data_model.DataModel`. + """ + + def __init__(self, expr=None, sense=LE, rhs=0.0, name="", **qcmatrix): + self.Sense = sense + self.ConstraintName = name + self.index = -1 + if expr is not None: + ( + linear_values, + linear_indices, + quadratic_values, + quadratic_row_indices, + quadratic_col_indices, + rhs_value, + ) = _quadratic_expression_to_qcmatrix(expr, rhs) + else: + linear_values = list(qcmatrix.get("linear_values", [])) + linear_indices = list(qcmatrix.get("linear_indices", [])) + quadratic_values = list(qcmatrix.get("quadratic_values", [])) + quadratic_row_indices = list(qcmatrix.get("quadratic_row_indices", [])) + quadratic_col_indices = list(qcmatrix.get("quadratic_col_indices", [])) + rhs_value = float(qcmatrix.get("rhs_value", 0.0)) + + self.linear_values = np.array(linear_values, dtype=np.float64) + self.linear_indices = np.array(linear_indices, dtype=np.int32) + self.quadratic_values = np.array(quadratic_values, dtype=np.float64) + self.quadratic_row_indices = np.array( + quadratic_row_indices, dtype=np.int32 + ) + self.quadratic_col_indices = np.array(quadratic_col_indices, dtype=np.int32) + self.rhs_value = rhs_value + + def getConstraintName(self): + return self.ConstraintName class LinearExpression: @@ -1311,6 +1408,7 @@ def __init__(self, model_name=""): self.Name = model_name self.vars = [] self.constrs = [] + self.quadratic_constrs = [] self.ObjSense = MINIMIZE self.ObjConstant = 0.0 self.Status = -1 @@ -1460,6 +1558,23 @@ def _to_data_model(self): dm.set_row_names(self.row_names) dm.set_problem_name(self.Name) + for i, qc in enumerate(self.quadratic_constrs): + row_index = len(self.constrs) + i + row_name = qc.ConstraintName + if row_name == "": + row_name = "Q" + str(qc.index) + dm.add_quadratic_constraint( + constraint_row_index=row_index, + constraint_row_name=row_name, + linear_values=qc.linear_values, + linear_indices=qc.linear_indices, + rhs_value=qc.rhs_value, + quadratic_values=qc.quadratic_values, + quadratic_row_indices=qc.quadratic_row_indices, + quadratic_col_indices=qc.quadratic_col_indices, + sense=qc.Sense, + ) + self.model = dm def update(self): @@ -1557,6 +1672,30 @@ def addConstraint(self, constr, name=""): raise ValueError("addConstraint requires a Constraint object") return constr + def addQuadraticConstraint(self, constr, name=""): + """ + Add a quadratic (QCMATRIX) constraint to the problem. + + Parameters + ---------- + constr : :py:class:`QuadraticConstraint` + Constructed using :py:class:`QuadraticExpression` comparisons. + name : str, optional + Name of the constraint. + """ + if self.solved: + self.reset_solved_values() + match constr: + case QuadraticConstraint(): + constr.index = len(self.quadratic_constrs) + constr.ConstraintName = name + self.quadratic_constrs.append(constr) + case _: + raise ValueError( + "addQuadraticConstraint requires a QuadraticConstraint object" + ) + return constr + def updateConstraint(self, constr, coeffs=[], rhs=None): """ Updates a previously added constraint. Values that can be updated are @@ -1821,7 +1960,13 @@ def NumVariables(self): @property def NumConstraints(self): # Returns number of contraints in the problem. - return len(self.constrs) + return len(self.constrs) + len(self.quadratic_constrs) + + def getQuadraticConstraints(self): + """ + Returns all quadratic constraints in the problem. + """ + return self.quadratic_constrs @property def NumNZs(self): @@ -1973,3 +2118,4 @@ def solve(self, settings=solver_settings.SolverSettings()): solution = solver.Solve(self.model, settings) # Post Solve self.populate_solution(solution) + return solution diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py index aa8fa91f76..3abbe5a49e 100644 --- a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py @@ -12,7 +12,14 @@ import pytest from cuopt.linear_programming import data_model, solver, solver_settings -from cuopt.linear_programming.solver.solver_parameters import CUOPT_METHOD, CUOPT_PRESOLVE +from cuopt.linear_programming.problem import Problem +from cuopt.linear_programming.solver.solver_parameters import ( + CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE, + CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE, + CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE, + CUOPT_METHOD, + CUOPT_PRESOLVE, +) from cuopt.linear_programming.solver_settings import SolverMethod @@ -40,6 +47,35 @@ def _lorentz_qcoo(variable_indices): ) +def _build_socp_min_x0_model(): + """min x0 s.t. x1=1 and Lorentz cone on (x0,x1,x2); optimal obj=1.""" + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + qv, qr, qc = _lorentz_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( + constraint_row_index=1, + constraint_row_name="soc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, + ) + return dm + + def _rotated_qcoo(variable_indices): """COO triplets for -2 x_0 x_1 + sum_{i>1} x_i^2 <= 0 on variable_indices.""" indices = [int(i) for i in variable_indices] @@ -65,28 +101,59 @@ def test_quadratic_constraint_api(): quadratic_row_indices=np.array([0, 1, 2], dtype=np.int32), quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), ) - dm.clear_quadratic_constraints() - dm.add_second_order_cone_constraint( - variable_indices=[0, 1, 2], + dm.add_quadratic_constraint( constraint_row_index=1, + constraint_row_name="qc_ge", + sense="G", + quadratic_values=np.array([1.0, -1.0, -1.0], dtype=np.float64), + quadratic_row_indices=np.array([0, 1, 2], dtype=np.int32), + quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), ) + qcs = dm.get_quadratic_constraints() + assert qcs[1]["constraint_row_type"] == "L" + assert qcs[1]["quadratic_values"][0] == pytest.approx(-1.0) + with pytest.raises(ValueError, match="Equality quadratic"): + dm.add_quadratic_constraint( + constraint_row_index=2, + sense="E", + quadratic_values=np.array([-1.0], dtype=np.float64), + quadratic_row_indices=np.array([0], dtype=np.int32), + quadratic_col_indices=np.array([0], dtype=np.int32), + ) + dm.clear_quadratic_constraints() +def test_quadratic_constraint_problem_solve(): + """Problem.addQuadraticConstraint + solve (Lorentz via expression).""" + prob = Problem() + x0 = prob.addVariable(lb=0.0, name="x0") + x1 = prob.addVariable(lb=0.0, name="x1") + x2 = prob.addVariable(lb=0.0, name="x2") + prob.addConstraint(x1 == 1.0) + prob.addQuadraticConstraint(-x0 * x0 + x1 * x1 + x2 * x2 <= 0, name="soc") + prob.setObjective(x0) -def test_rotated_second_order_cone_constraint_api(): - dm = data_model.DataModel() - dm.add_rotated_second_order_cone_constraint( - variable_indices=[0, 1, 2], - constraint_row_index=0, - ) - dm.clear_quadratic_constraints() + solution = prob.solve(_barrier_settings()) - qv, qr, qc = _rotated_qcoo([3, 4, 5, 6]) - dm.add_quadratic_constraint( - constraint_row_index=1, - quadratic_values=qv, - quadratic_row_indices=qr, - quadratic_col_indices=qc, - ) + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert len(prob.getQuadraticConstraints()) == 1 + + +def test_quadratic_constraint_problem_solve_ge_sense(): + """>= quadratic rows normalize via DataModel on export.""" + prob = Problem() + x0 = prob.addVariable(lb=0.0, name="x0") + x1 = prob.addVariable(lb=0.0, name="x1") + x2 = prob.addVariable(lb=0.0, name="x2") + prob.addConstraint(x1 == 1.0) + prob.addQuadraticConstraint(x0 * x0 - x1 * x1 - x2 * x2 >= 0, name="soc") + prob.setObjective(x0) + + solution = prob.solve(_barrier_settings()) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert prob.model.get_quadratic_constraints()[0]["constraint_row_type"] == "L" def test_rotated_second_order_cone_min_tail(): @@ -112,10 +179,13 @@ def test_rotated_second_order_cone_min_tail(): np.array([np.inf, np.inf, np.inf], dtype=np.float64) ) - dm.add_rotated_second_order_cone_constraint( - variable_indices=[0, 1, 2], + qv, qr, qc = _rotated_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( constraint_row_index=2, constraint_row_name="rsoc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, ) solution = _solve_socp(dm) @@ -150,15 +220,21 @@ def test_multiple_rotated_second_order_cone_constraints(): dm.set_variable_lower_bounds(np.zeros(6, dtype=np.float64)) dm.set_variable_upper_bounds(np.full(6, np.inf, dtype=np.float64)) - dm.add_rotated_second_order_cone_constraint( - variable_indices=[0, 1, 2], + qv0, qr0, qc0 = _rotated_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( constraint_row_index=4, constraint_row_name="rsoc1", + quadratic_values=qv0, + quadratic_row_indices=qr0, + quadratic_col_indices=qc0, ) - dm.add_rotated_second_order_cone_constraint( - variable_indices=[3, 4, 5], + qv1, qr1, qc1 = _rotated_qcoo([3, 4, 5]) + dm.add_quadratic_constraint( constraint_row_index=5, constraint_row_name="rsoc2", + quadratic_values=qv1, + quadratic_row_indices=qr1, + quadratic_col_indices=qc1, ) solution = _solve_socp(dm) @@ -175,8 +251,16 @@ def test_multiple_rotated_second_order_cone_constraints(): def test_multiple_quadratic_constraints(): - """Two Lorentz QCMATRIX rows via add_quadratic_constraint (same problem as two SOCs).""" - # Variables [l1, l2 | t1, u1, v1 | t2, u2, v2]; see test_multiple_second_order_cone_constraints. + """Port of C++ mixed_linear_and_two_soc_blocks (solve_barrier.cu). + + Two Lorentz QCMATRIX rows via add_quadratic_constraint. + Variables [l1, l2 | t1, u1, v1 | t2, u2, v2]; Lorentz cones on (t1,u1,v1) and (t2,u2,v2). + + minimize t1 + t2 + subject to l1 - u1 = 0, l2 - u2 = 0, l1 + l2 = 3, l1 - l2 = 1 + + Optimal: l1*=2, l2*=1, t1*=2, u1*=2, v1*=0, t2*=1, u2*=1, v2*=0, obj*=3. + """ dm = data_model.DataModel() A_values = np.array([1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0], dtype=np.float64) @@ -226,58 +310,6 @@ def test_multiple_quadratic_constraints(): assert abs(x[7]) == pytest.approx(0.0, abs=1e-3) -def test_multiple_second_order_cone_constraints(): - # Port of C++ mixed_linear_and_two_soc_blocks (solve_barrier.cu). - # - # Variables [l1, l2 | t1, u1, v1 | t2, u2, v2]; Lorentz cones on (t1,u1,v1) and (t2,u2,v2). - # - # minimize t1 + t2 - # subject to l1 - u1 = 0, l2 - u2 = 0, l1 + l2 = 3, l1 - l2 = 1 - # - # Optimal: l1*=2, l2*=1, t1*=2, u1*=2, v1*=0, t2*=1, u2*=1, v2*=0, obj*=3. - - dm = data_model.DataModel() - - A_values = np.array([1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0], dtype=np.float64) - A_indices = np.array([0, 3, 1, 6, 0, 1, 0, 1], dtype=np.int32) - A_offsets = np.array([0, 2, 4, 6, 8], dtype=np.int32) - dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) - - dm.set_constraint_lower_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) - dm.set_constraint_upper_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) - - dm.set_objective_coefficients( - np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0], dtype=np.float64) - ) - dm.set_variable_lower_bounds(np.zeros(8, dtype=np.float64)) - dm.set_variable_upper_bounds(np.full(8, np.inf, dtype=np.float64)) - - dm.add_second_order_cone_constraint( - variable_indices=[2, 3, 4], - constraint_row_index=4, - constraint_row_name="soc1", - ) - dm.add_second_order_cone_constraint( - variable_indices=[5, 6, 7], - constraint_row_index=5, - constraint_row_name="soc2", - ) - - solution = _solve_socp(dm) - - assert solution.get_termination_reason() == "Optimal" - assert solution.get_primal_objective() == pytest.approx(3.0, rel=1e-3, abs=1e-3) - x = solution.get_primal_solution() - assert x[0] == pytest.approx(2.0, rel=1e-3, abs=1e-3) - assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) - assert x[2] == pytest.approx(2.0, rel=1e-3, abs=1e-3) - assert x[3] == pytest.approx(2.0, rel=1e-3, abs=1e-3) - assert abs(x[4]) == pytest.approx(0.0, abs=1e-3) - assert x[5] == pytest.approx(1.0, rel=1e-3, abs=1e-3) - assert x[6] == pytest.approx(1.0, rel=1e-3, abs=1e-3) - assert abs(x[7]) == pytest.approx(0.0, abs=1e-3) - - def test_socp_min_x0_norm_constraint(): # minimize x0 # subject to x1 = 1 @@ -285,29 +317,7 @@ def test_socp_min_x0_norm_constraint(): # # Optimal: x* = (1, 1, 0), obj* = 1 - dm = data_model.DataModel() - - A_values = np.array([1.0], dtype=np.float64) - A_indices = np.array([1], dtype=np.int32) - A_offsets = np.array([0, 1], dtype=np.int32) - dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) - - dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) - dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) - - dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) - dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) - dm.set_variable_upper_bounds( - np.array([np.inf, np.inf, np.inf], dtype=np.float64) - ) - - dm.add_second_order_cone_constraint( - variable_indices=[0, 1, 2], - constraint_row_index=1, - constraint_row_name="soc", - ) - - solution = _solve_socp(dm) + solution = _solve_socp(_build_socp_min_x0_model()) assert solution.get_termination_reason() == "Optimal" assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) @@ -345,10 +355,13 @@ def test_socp_mixed_linear_and_cone_block(): np.array([np.inf, np.inf, np.inf, np.inf], dtype=np.float64) ) - dm.add_second_order_cone_constraint( - variable_indices=[1, 2, 3], + qv, qr, qc = _lorentz_qcoo([1, 2, 3]) + dm.add_quadratic_constraint( constraint_row_index=2, constraint_row_name="soc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, ) solution = _solve_socp(dm) @@ -363,30 +376,12 @@ def test_socp_mixed_linear_and_cone_block(): def test_socp_barrier_relative_tolerances(): - dm = data_model.DataModel() - - A_values = np.array([1.0], dtype=np.float64) - A_indices = np.array([1], dtype=np.int32) - A_offsets = np.array([0, 1], dtype=np.int32) - dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) - dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) - dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) - dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) - dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) - dm.set_variable_upper_bounds( - np.array([np.inf, np.inf, np.inf], dtype=np.float64) - ) - dm.add_second_order_cone_constraint( - variable_indices=[0, 1, 2], - constraint_row_index=1, - ) - settings = _barrier_settings() - settings.set_parameter("barrier_relative_feasibility_tolerance", 1e-10) - settings.set_parameter("barrier_relative_optimality_tolerance", 1e-10) - settings.set_parameter("barrier_relative_complementarity_tolerance", 1e-10) + settings.set_parameter(CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE, 1e-10) + settings.set_parameter(CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE, 1e-10) + settings.set_parameter(CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE, 1e-10) - solution = solver.Solve(dm, settings) + solution = solver.Solve(_build_socp_min_x0_model(), settings) assert solution.get_termination_reason() == "Optimal" assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-4, abs=1e-4) diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py new file mode 100644 index 0000000000..39f4065fd7 --- /dev/null +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py @@ -0,0 +1,109 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Tests for SOCP / QCMATRIX MPS I/O: writeMPS, ParseMps, and external dataset parsing. +""" + +import os + +import numpy as np +import pytest + +from cuopt.linear_programming import data_model, mps_parser, solver, solver_settings +from cuopt.linear_programming.solver.solver_parameters import ( + CUOPT_METHOD, + CUOPT_PRESOLVE, +) +from cuopt.linear_programming.solver_settings import SolverMethod + +RAPIDS_DATASET_ROOT_DIR = os.getenv("RAPIDS_DATASET_ROOT_DIR") +if RAPIDS_DATASET_ROOT_DIR is None: + RAPIDS_DATASET_ROOT_DIR = os.path.join(os.getcwd(), "datasets") + + +def _barrier_settings(): + settings = solver_settings.SolverSettings() + settings.set_parameter(CUOPT_METHOD, SolverMethod.Barrier) + settings.set_parameter(CUOPT_PRESOLVE, 0) + return settings + + +def _solve_socp(dm, settings=None): + if settings is None: + settings = _barrier_settings() + return solver.Solve(dm, settings) + + +def _lorentz_qcoo(variable_indices): + """COO triplets for sum_{i>0} x_i^2 - x_0^2 <= 0 on variable_indices.""" + indices = [int(i) for i in variable_indices] + q_values = [-1.0] + [1.0] * (len(indices) - 1) + return ( + np.array(q_values, dtype=np.float64), + np.array(indices, dtype=np.int32), + np.array(indices, dtype=np.int32), + ) + + +def _build_socp_min_x0_model(): + """min x0 s.t. x1=1 and Lorentz cone on (x0,x1,x2); optimal obj=1.""" + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + qv, qr, qc = _lorentz_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( + constraint_row_index=1, + constraint_row_name="soc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, + ) + return dm + + +def test_socp_mps_write_parse_solve(tmp_path): + """writeMPS -> ParseMps preserves QCMATRIX and barrier solve.""" + dm = _build_socp_min_x0_model() + mps_file = tmp_path / "socp_lorentz.mps" + dm.writeMPS(str(mps_file)) + + dm2 = mps_parser.ParseMps(str(mps_file)) + qcs = dm2.get_quadratic_constraints() + assert len(qcs) == 1 + assert qcs[0]["constraint_row_name"] == "soc" + assert qcs[0]["constraint_row_type"] == "L" + + solution = _solve_socp(dm2) + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + + +def test_parse_qcmatrix_dataset_mps(): + """Parse external QCQP MPS with QCMATRIX when dataset is available.""" + mps_path = os.path.join(RAPIDS_DATASET_ROOT_DIR, "qcqp", "QC_Test_1.mps") + if not os.path.exists(mps_path): + pytest.skip(f"{mps_path} not in dataset root") + + dm = mps_parser.ParseMps(mps_path) + qcs = dm.get_quadratic_constraints() + assert len(qcs) == 2 + assert qcs[0]["constraint_row_name"] == "QC0" + assert qcs[1]["constraint_row_name"] == "QC1" + for qc in qcs: + assert qc["constraint_row_type"] == "L" + assert qc["quadratic_row_indices"].shape == qc["quadratic_col_indices"].shape + assert qc["quadratic_values"].shape == qc["quadratic_row_indices"].shape From e840781e07b20485258fbb4c8b596197bfd021b8 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 11:29:03 -0700 Subject: [PATCH 47/68] shifting 'G' from python to 'L' C++ translation --- .../linear_programming/io/mps_data_model.hpp | 5 ++-- cpp/src/barrier/translate_soc.hpp | 26 ++++++++++++++++--- cpp/src/io/mps_data_model.cpp | 5 ++-- .../data_model/data_model.pxd | 1 + .../data_model/data_model.py | 12 +++------ .../data_model/data_model_wrapper.pyx | 11 +++++--- .../test_socp_barrier.py | 8 +++--- 7 files changed, 43 insertions(+), 25 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp b/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp index 4da19231f3..dee74a3a42 100644 --- a/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp +++ b/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp @@ -263,8 +263,9 @@ class mps_data_model_t { * @param linear_values, linear_indices Same nnz; can be empty for a purely quadratic row (rare). * @param quadratic_values, quadratic_row_indices, quadratic_col_indices COO triplets; same * length; may all be empty if Q is empty. Stored sorted by (row, col). - * @param constraint_row_type MPS ROWS type; must be 'L'. 'G' and 'E' quadratic rows are not - * supported. + * @param constraint_row_type MPS ROWS type: 'L' (<=) or 'G' (>=). Stored as given; 'G' rows are + * converted to '<=' form when building the SOCP for the barrier solver. Equality ('E') is + * not supported. */ void append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, diff --git a/cpp/src/barrier/translate_soc.hpp b/cpp/src/barrier/translate_soc.hpp index 10d6b88297..d3e805960a 100644 --- a/cpp/src/barrier/translate_soc.hpp +++ b/cpp/src/barrier/translate_soc.hpp @@ -25,6 +25,21 @@ namespace cuopt::linear_programming::detail { +/** Convert MPS >= ('G') quadratic row to <= ('L') form on a working copy for SOC conversion. */ +template +void normalize_quadratic_constraint_g_to_l(qc_t& qc) +{ + if (qc.constraint_row_type != 'G') { return; } + for (auto& v : qc.linear_values) { + v = -v; + } + for (auto& v : qc.quadratic_values) { + v = -v; + } + qc.rhs_value = -qc.rhs_value; + qc.constraint_row_type = 'L'; +} + /** * @brief Expand QCMATRIX second-order cone (and rotated / affine variants) into the * canonical slack form expected by the simplex/PDLP path: extra variables, equality @@ -96,10 +111,15 @@ void apply_soc_qcmatrix_conversion_for_simplex( std::vector qc_soc_uniform_scale(qcs.size(), f_t(1)); for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { - const auto& qc = qcs[qc_i]; - cuopt_expects(qc.constraint_row_type == 'L', + auto qc = qcs[qc_i]; + cuopt_expects(qc.constraint_row_type != 'E', + error_type_t::ValidationError, + "Equality quadratic constraints are not supported for SOC conversion"); + cuopt_expects(qc.constraint_row_type == 'L' || qc.constraint_row_type == 'G', error_type_t::ValidationError, - "Only <= quadratic constraints are supported for SOC conversion"); + "Quadratic constraint '%s' ROWS type must be 'L' (<=) or 'G' (>=)", + qc.constraint_row_name.c_str()); + normalize_quadratic_constraint_g_to_l(qc); cuopt_expects(qc.rhs_value < tol && qc.rhs_value > -tol, error_type_t::ValidationError, "SOC conversion currently requires rhs = 0 for quadratic constraints"); diff --git a/cpp/src/io/mps_data_model.cpp b/cpp/src/io/mps_data_model.cpp index 088a3045a2..0172918c62 100644 --- a/cpp/src/io/mps_data_model.cpp +++ b/cpp/src/io/mps_data_model.cpp @@ -155,10 +155,9 @@ void mps_data_model_t::append_quadratic_constraint( error_type_t::ValidationError, "constraint_row_index must be non-negative"); - mps_parser_expects(constraint_row_type == 'L', + mps_parser_expects(constraint_row_type == 'L' || constraint_row_type == 'G', error_type_t::ValidationError, - "Quadratic constraint ROWS type must be 'L' (less-or-equal); got '%c'. " - "Only 'L' is supported for convex quadratic constraints.", + "Quadratic constraint ROWS type must be 'L' (<=) or 'G' (>=); got '%c'.", constraint_row_type); mps_parser_expects(linear_values.size() == linear_indices.size(), diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd index e38e630d3a..b25ba76e3a 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd @@ -7,6 +7,7 @@ # cython: embedsignature = True # cython: language_level = 3 +from libc.stddef cimport size_t from libcpp cimport bool from libcpp.string cimport string from libcpp.vector cimport vector diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index 1490c6cfb7..b6a7bbf5d2 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -344,8 +344,9 @@ def add_quadratic_constraint( ``linear^T x + x^T Q x {sense} rhs_value``. sense : str, optional MPS row type: ``'L'`` (default, ``<=``) or ``'G'`` (``>=``), same values as - :meth:`set_row_types`. ``'G'`` is converted to ``'L'`` by negating coefficients and - rhs. Equality (``'E'``) is not supported. + :meth:`set_row_types`. Rows are stored with the given sense; ``'G'`` is converted to + ``'L'`` when the barrier solver builds second-order cones from QCMATRIX data. + Equality (``'E'``) is not supported. Notes ----- @@ -364,13 +365,6 @@ def add_quadratic_constraint( f"Invalid sense {sense!r}; use 'L' or 'G' like set_row_types " "(equality 'E' is not supported)." ) - if sense == "G": - if linear_values is not None: - linear_values = -np.asarray(linear_values, dtype=np.float64) - if quadratic_values is not None: - quadratic_values = -np.asarray(quadratic_values, dtype=np.float64) - rhs_value = -float(rhs_value) - sense = "L" super().add_quadratic_constraint( constraint_row_index, constraint_row_name, diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index ef1b962d59..94dda60cdd 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -127,11 +127,14 @@ cdef class DataModel: "quadratic_col_indices must have the same length" ) row_type = str(constraint_row_type) - if row_type != "L": + if row_type == "E": raise ValueError( - "constraint_row_type must be 'L' for quadratic constraints; " - "use DataModel.add_quadratic_constraint(sense='G') to normalize " - "'>=' rows." + "Equality quadratic constraints are not supported; use two inequality " + "constraints or build the model with Problem.addQuadraticConstraint." + ) + if row_type not in ("L", "G"): + raise ValueError( + f"Invalid constraint_row_type {row_type!r}; use 'L' or 'G' like set_row_types." ) self.quadratic_constraints.append( { diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py index 3abbe5a49e..cd5c7388e7 100644 --- a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py @@ -110,8 +110,8 @@ def test_quadratic_constraint_api(): quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), ) qcs = dm.get_quadratic_constraints() - assert qcs[1]["constraint_row_type"] == "L" - assert qcs[1]["quadratic_values"][0] == pytest.approx(-1.0) + assert qcs[1]["constraint_row_type"] == "G" + assert qcs[1]["quadratic_values"][0] == pytest.approx(1.0) with pytest.raises(ValueError, match="Equality quadratic"): dm.add_quadratic_constraint( constraint_row_index=2, @@ -140,7 +140,7 @@ def test_quadratic_constraint_problem_solve(): def test_quadratic_constraint_problem_solve_ge_sense(): - """>= quadratic rows normalize via DataModel on export.""" + """>= quadratic rows are converted to <= form when building SOCP for the barrier.""" prob = Problem() x0 = prob.addVariable(lb=0.0, name="x0") x1 = prob.addVariable(lb=0.0, name="x1") @@ -153,7 +153,7 @@ def test_quadratic_constraint_problem_solve_ge_sense(): assert solution.get_termination_reason() == "Optimal" assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) - assert prob.model.get_quadratic_constraints()[0]["constraint_row_type"] == "L" + assert prob.model.get_quadratic_constraints()[0]["constraint_row_type"] == "G" def test_rotated_second_order_cone_min_tail(): From 4175653973523a20d0d7cf588e71b71cfba3c6e4 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 12:11:57 -0700 Subject: [PATCH 48/68] Remove separate definition for quadrtic constraints --- .../data_model/data_model.py | 5 +- .../data_model/data_model_wrapper.pyx | 5 +- .../cuopt/cuopt/linear_programming/problem.py | 206 ++++++++---------- .../test_socp_barrier.py | 8 +- 4 files changed, 101 insertions(+), 123 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index b6a7bbf5d2..5e6fd6744d 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -356,10 +356,7 @@ def add_quadratic_constraint( if hasattr(sense, "value"): sense = sense.value if sense == "E": - raise ValueError( - "Equality quadratic constraints are not supported; use two inequality " - "constraints or build the model with Problem.addQuadraticConstraint." - ) + raise ValueError("Equality constraints are not supported.") if sense not in ("L", "G"): raise ValueError( f"Invalid sense {sense!r}; use 'L' or 'G' like set_row_types " diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index 94dda60cdd..562df50128 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -128,10 +128,7 @@ cdef class DataModel: ) row_type = str(constraint_row_type) if row_type == "E": - raise ValueError( - "Equality quadratic constraints are not supported; use two inequality " - "constraints or build the model with Problem.addQuadraticConstraint." - ) + raise ValueError("Equality constraints are not supported.") if row_type not in ("L", "G"): raise ValueError( f"Invalid constraint_row_type {row_type!r}; use 'L' or 'G' like set_row_types." diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 957355abcc..ee7f3b4755 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -282,6 +282,8 @@ def __le__(self, other): # var1 <= var2 -> var1 - var2 <= 0 expr = self - other return Constraint(expr, LE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, LE, 0.0) case _: raise ValueError("Unsupported operation") @@ -294,6 +296,8 @@ def __ge__(self, other): # var1 >= var2 -> var1 - var2 >= 0 expr = self - other return Constraint(expr, GE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, GE, 0.0) case _: raise ValueError("Unsupported operation") @@ -838,9 +842,9 @@ def __truediv__(self, other): def __le__(self, other): match other: case int() | float(): - return QuadraticConstraint(self, LE, float(other)) + return Constraint(self, LE, float(other)) case Variable() | LinearExpression() | QuadraticExpression(): - return QuadraticConstraint(self - other, LE, 0.0) + return Constraint(self - other, LE, 0.0) case _: raise ValueError( "Can't compare QuadraticExpression with type %s" @@ -850,9 +854,9 @@ def __le__(self, other): def __ge__(self, other): match other: case int() | float(): - return QuadraticConstraint(self, GE, float(other)) + return Constraint(self, GE, float(other)) case Variable() | LinearExpression() | QuadraticExpression(): - return QuadraticConstraint(self - other, GE, 0.0) + return Constraint(self - other, GE, 0.0) case _: raise ValueError( "Can't compare QuadraticExpression with type %s" @@ -860,10 +864,7 @@ def __ge__(self, other): ) def __eq__(self, other): - raise ValueError( - "Quadratic equality constraints are not supported; " - "use addQuadraticConstraint with QCMATRIX data or two inequality constraints." - ) + raise ValueError("Equality constraints are not supported.") def _quadratic_expression_to_qcmatrix(expr, rhs): @@ -894,49 +895,6 @@ def _quadratic_expression_to_qcmatrix(expr, rhs): ) -class QuadraticConstraint: - """ - Quadratic constraint in QCMATRIX form. - - Created from :py:class:`QuadraticExpression` comparisons or - :py:meth:`Problem.addQuadraticConstraint`. Row sense (``<=`` / ``>=``) is - normalized when the problem is exported to :class:`~cuopt.linear_programming.data_model.DataModel`. - """ - - def __init__(self, expr=None, sense=LE, rhs=0.0, name="", **qcmatrix): - self.Sense = sense - self.ConstraintName = name - self.index = -1 - if expr is not None: - ( - linear_values, - linear_indices, - quadratic_values, - quadratic_row_indices, - quadratic_col_indices, - rhs_value, - ) = _quadratic_expression_to_qcmatrix(expr, rhs) - else: - linear_values = list(qcmatrix.get("linear_values", [])) - linear_indices = list(qcmatrix.get("linear_indices", [])) - quadratic_values = list(qcmatrix.get("quadratic_values", [])) - quadratic_row_indices = list(qcmatrix.get("quadratic_row_indices", [])) - quadratic_col_indices = list(qcmatrix.get("quadratic_col_indices", [])) - rhs_value = float(qcmatrix.get("rhs_value", 0.0)) - - self.linear_values = np.array(linear_values, dtype=np.float64) - self.linear_indices = np.array(linear_indices, dtype=np.int32) - self.quadratic_values = np.array(quadratic_values, dtype=np.float64) - self.quadratic_row_indices = np.array( - quadratic_row_indices, dtype=np.int32 - ) - self.quadratic_col_indices = np.array(quadratic_col_indices, dtype=np.int32) - self.rhs_value = rhs_value - - def getConstraintName(self): - return self.ConstraintName - - class LinearExpression: """ LinearExpressions contain a set of variables, the coefficients @@ -1244,6 +1202,8 @@ def __le__(self, other): # expr1 <= expr2 -> expr1 - expr2 <= 0 expr = self - other return Constraint(expr, LE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, LE, 0.0) def __ge__(self, other): match other: @@ -1253,6 +1213,8 @@ def __ge__(self, other): # expr1 >= expr2 -> expr1 - expr2 >= 0 expr = self - other return Constraint(expr, GE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, GE, 0.0) def __eq__(self, other): match other: @@ -1266,16 +1228,15 @@ def __eq__(self, other): class Constraint: """ - cuOpt constraint object containing a linear expression, - the sense of the constraint, and the right-hand side of - the constraint. - Constraints are associated with a problem and can be - created using :py:meth:`Problem.addConstraint`. + cuOpt constraint object containing a linear or quadratic (QCMATRIX) + expression, the sense of the constraint, and the right-hand side. + Constraints are associated with a problem and can be created using + :py:meth:`Problem.addConstraint`. Parameters ---------- - expr : LinearExpression - Linear expression corresponding to a problem. + expr : LinearExpression or QuadraticExpression + Expression corresponding to the constraint. sense : enum Sense of the constraint. Either LE for <=, GE for >= or EQ for == . @@ -1291,7 +1252,9 @@ class Constraint: Sense : LE, GE or EQ Row sense. LE for <=, GE for >= or EQ for == . RHS : float - Constraint right-hand side value. + Constraint right-hand side value (linear rows). + is_quadratic : bool + True when the row is exported as a QCMATRIX quadratic constraint. Slack : float Computed LHS - RHS with current solution. DualValue : float @@ -1299,10 +1262,41 @@ class Constraint: """ def __init__(self, expr, sense, rhs, name=""): + self.index = -1 + self.Sense = sense + self.ConstraintName = name + self.DualValue = float("nan") + self.Slack = float("nan") + + if isinstance(expr, QuadraticExpression): + self.is_quadratic = True + ( + linear_values, + linear_indices, + quadratic_values, + quadratic_row_indices, + quadratic_col_indices, + rhs_value, + ) = _quadratic_expression_to_qcmatrix(expr, rhs) + self.linear_values = np.array(linear_values, dtype=np.float64) + self.linear_indices = np.array(linear_indices, dtype=np.int32) + self.quadratic_values = np.array(quadratic_values, dtype=np.float64) + self.quadratic_row_indices = np.array( + quadratic_row_indices, dtype=np.int32 + ) + self.quadratic_col_indices = np.array( + quadratic_col_indices, dtype=np.int32 + ) + self.rhs_value = rhs_value + self.RHS = rhs_value + self.vindex_coeff_dict = {} + self.vars = expr.vars + return + + self.is_quadratic = False self.vindex_coeff_dict = {} nz = len(expr) self.vars = expr.vars - self.index = -1 for i in range(nz): v_idx = expr.vars[i].index v_coeff = expr.coefficients[i] @@ -1311,11 +1305,7 @@ def __init__(self, expr, sense, rhs, name=""): if v_idx in self.vindex_coeff_dict else v_coeff ) - self.Sense = sense self.RHS = rhs - expr.getConstant() - self.ConstraintName = name - self.DualValue = float("nan") - self.Slack = float("nan") def __len__(self): return len(self.vindex_coeff_dict) @@ -1408,7 +1398,6 @@ def __init__(self, model_name=""): self.Name = model_name self.vars = [] self.constrs = [] - self.quadratic_constrs = [] self.ObjSense = MINIMIZE self.ObjConstant = 0.0 self.Status = -1 @@ -1496,6 +1485,8 @@ def _to_data_model(self): "values": [], } for constr in self.constrs: + if constr.is_quadratic: + continue csr_dict["column_indices"].extend( list(constr.vindex_coeff_dict.keys()) ) @@ -1515,6 +1506,8 @@ def _to_data_model(self): else: for constr in self.constrs: + if constr.is_quadratic: + continue self.rhs.append(constr.RHS) self.row_sense.append(constr.Sense) @@ -1558,22 +1551,26 @@ def _to_data_model(self): dm.set_row_names(self.row_names) dm.set_problem_name(self.Name) - for i, qc in enumerate(self.quadratic_constrs): - row_index = len(self.constrs) + i - row_name = qc.ConstraintName + linear_constr_count = sum(1 for c in self.constrs if not c.is_quadratic) + quad_index = 0 + for constr in self.constrs: + if not constr.is_quadratic: + continue + row_name = constr.ConstraintName if row_name == "": - row_name = "Q" + str(qc.index) + row_name = "Q" + str(constr.index) dm.add_quadratic_constraint( - constraint_row_index=row_index, + constraint_row_index=linear_constr_count + quad_index, constraint_row_name=row_name, - linear_values=qc.linear_values, - linear_indices=qc.linear_indices, - rhs_value=qc.rhs_value, - quadratic_values=qc.quadratic_values, - quadratic_row_indices=qc.quadratic_row_indices, - quadratic_col_indices=qc.quadratic_col_indices, - sense=qc.Sense, + linear_values=constr.linear_values, + linear_indices=constr.linear_indices, + rhs_value=constr.rhs_value, + quadratic_values=constr.quadratic_values, + quadratic_row_indices=constr.quadratic_row_indices, + quadratic_col_indices=constr.quadratic_col_indices, + sense=constr.Sense, ) + quad_index += 1 self.model = dm @@ -1641,15 +1638,15 @@ def addVariable( def addConstraint(self, constr, name=""): """ Adds a constraint to the problem defined by constraint object - and name. A constraint is generated using LinearExpression, - Sense and RHS. + and name. A constraint is generated using LinearExpression or + QuadraticExpression comparisons (``<=``, ``>=``, or ``==``). Parameters ---------- constr : :py:class:`Constraint` - Constructed using LinearExpressions (See Examples) + Constructed using expression comparisons (see Examples). name : string - Name of the variable. Optional. + Name of the constraint. Optional. Examples -------- @@ -1659,6 +1656,7 @@ def addConstraint(self, constr, name=""): >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y >>> problem.addConstraint(expr + x == 20, name="Constr2") + >>> problem.addConstraint(-x*x + y*y <= 0, name="soc") """ if self.solved: self.reset_solved_values() # Reset all solved values @@ -1672,30 +1670,6 @@ def addConstraint(self, constr, name=""): raise ValueError("addConstraint requires a Constraint object") return constr - def addQuadraticConstraint(self, constr, name=""): - """ - Add a quadratic (QCMATRIX) constraint to the problem. - - Parameters - ---------- - constr : :py:class:`QuadraticConstraint` - Constructed using :py:class:`QuadraticExpression` comparisons. - name : str, optional - Name of the constraint. - """ - if self.solved: - self.reset_solved_values() - match constr: - case QuadraticConstraint(): - constr.index = len(self.quadratic_constrs) - constr.ConstraintName = name - self.quadratic_constrs.append(constr) - case _: - raise ValueError( - "addQuadraticConstraint requires a QuadraticConstraint object" - ) - return constr - def updateConstraint(self, constr, coeffs=[], rhs=None): """ Updates a previously added constraint. Values that can be updated are @@ -1722,6 +1696,10 @@ def updateConstraint(self, constr, coeffs=[], rhs=None): """ self.reset_solved_values() if isinstance(constr, Constraint): + if constr.is_quadratic: + raise ValueError( + "updateConstraint applies to linear constraints only" + ) if isinstance(coeffs, dict): coeffs = coeffs.items() for var, coeff in coeffs: @@ -1960,13 +1938,13 @@ def NumVariables(self): @property def NumConstraints(self): # Returns number of contraints in the problem. - return len(self.constrs) + len(self.quadratic_constrs) + return len(self.constrs) def getQuadraticConstraints(self): """ - Returns all quadratic constraints in the problem. + Returns all quadratic (QCMATRIX) constraints in the problem. """ - return self.quadratic_constrs + return [c for c in self.constrs if c.is_quadratic] @property def NumNZs(self): @@ -2014,6 +1992,8 @@ def getCSR(self): return self.dict_to_object(self.constraint_csr_matrix) csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} for constr in self.constrs: + if constr.is_quadratic: + continue csr_dict["column_indices"].extend( list(constr.vindex_coeff_dict.keys()) ) @@ -2090,10 +2070,14 @@ def populate_solution(self, solution): dual_sol = None if not IsMIP: dual_sol = solution.get_dual_solution() - for i, constr in enumerate(self.constrs): - if dual_sol is not None and len(dual_sol) > 0: - constr.DualValue = dual_sol[i] + linear_row = 0 + for constr in self.constrs: + if constr.is_quadratic: + continue + if dual_sol is not None and len(dual_sol) > linear_row: + constr.DualValue = dual_sol[linear_row] constr.Slack = constr.compute_slack() + linear_row += 1 self.solved = True def solve(self, settings=solver_settings.SolverSettings()): diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py index cd5c7388e7..ec72e8de7c 100644 --- a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py @@ -112,7 +112,7 @@ def test_quadratic_constraint_api(): qcs = dm.get_quadratic_constraints() assert qcs[1]["constraint_row_type"] == "G" assert qcs[1]["quadratic_values"][0] == pytest.approx(1.0) - with pytest.raises(ValueError, match="Equality quadratic"): + with pytest.raises(ValueError, match="Equality constraints are not supported"): dm.add_quadratic_constraint( constraint_row_index=2, sense="E", @@ -123,13 +123,13 @@ def test_quadratic_constraint_api(): dm.clear_quadratic_constraints() def test_quadratic_constraint_problem_solve(): - """Problem.addQuadraticConstraint + solve (Lorentz via expression).""" + """Problem.addConstraint + solve (Lorentz via expression).""" prob = Problem() x0 = prob.addVariable(lb=0.0, name="x0") x1 = prob.addVariable(lb=0.0, name="x1") x2 = prob.addVariable(lb=0.0, name="x2") prob.addConstraint(x1 == 1.0) - prob.addQuadraticConstraint(-x0 * x0 + x1 * x1 + x2 * x2 <= 0, name="soc") + prob.addConstraint(-x0 * x0 + x1 * x1 + x2 * x2 <= 0, name="soc") prob.setObjective(x0) solution = prob.solve(_barrier_settings()) @@ -146,7 +146,7 @@ def test_quadratic_constraint_problem_solve_ge_sense(): x1 = prob.addVariable(lb=0.0, name="x1") x2 = prob.addVariable(lb=0.0, name="x2") prob.addConstraint(x1 == 1.0) - prob.addQuadraticConstraint(x0 * x0 - x1 * x1 - x2 * x2 >= 0, name="soc") + prob.addConstraint(x0 * x0 - x1 * x1 - x2 * x2 >= 0, name="soc") prob.setObjective(x0) solution = prob.solve(_barrier_settings()) From b9f0240276b45c34eafdf799f90b93de9058fabf Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 13:22:45 -0700 Subject: [PATCH 49/68] Fix a bug in Python's compute_slack when we have duplicate variable with different coefficients, e.g. 5*x + 7*x <= 18 --- python/cuopt/cuopt/linear_programming/problem.py | 9 ++++++--- .../cuopt/tests/linear_programming/test_python_API.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index ee7f3b4755..3130f9d64a 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1338,9 +1338,12 @@ def getCoefficient(self, var): def compute_slack(self): # Computes the constraint Slack in the current solution. - lhs = 0.0 - for var in self.vars: - lhs += var.Value * self.vindex_coeff_dict[var.index] + index_to_var = {var.index: var for var in self.vars} + lhs = sum( + index_to_var[v_idx].Value * coeff + for v_idx, coeff in self.vindex_coeff_dict.items() + ) + return self.RHS - lhs diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 467d714fee..096b518bb9 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -159,6 +159,16 @@ def test_model(): assert prob.ObjValue == pytest.approx(5 * x.Value + 3 * y.Value + 70) +def test_constraint_duplicate_terms_slack(): + """Merged coeffs in vindex_coeff_dict must not be double-counted in slack.""" + prob = Problem() + x = prob.addVariable() + c = prob.addConstraint(5 * x + 7 * x <= 18) + assert c.getCoefficient(x) == 12 + x.Value = 1.0 + assert c.compute_slack() == pytest.approx(6.0) + + def test_linear_expression(): prob = Problem() From 065bd1b575b5359a8ca4ac5e9f29fdd1cd598560 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Mon, 18 May 2026 13:57:05 -0700 Subject: [PATCH 50/68] Add support for duplicate quadratic terms with different coefficients in quadratic constraints, e.g. -4 * x0 * x0 + 3 * x0 * x0 + x1 * x1 + x2 * x2 <= 0 --- .../mps_parser/parser_wrapper.pyx | 1 + .../cuopt/cuopt/linear_programming/problem.py | 53 +++++++++++++++---- .../test_socp_barrier.py | 38 +++++++++++++ .../test_socp_parser.py | 38 +++++++++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx index 5998245202..65b10eaf7d 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx @@ -187,6 +187,7 @@ def ParseMps(mps_file_path, fixed_mps_formats): quadratic_values=quadratic_values, quadratic_row_indices=quadratic_row_indices, quadratic_col_indices=quadratic_col_indices, + sense=chr(qc.constraint_row_type), ) return data_model diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 3130f9d64a..1a64a39a52 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -317,8 +317,9 @@ def __eq__(self, other): class QuadraticExpression: """ QuadraticExpressions contain quadratic terms, linear terms, and a constant. - QuadraticExpressions can be used to create quadratic objectives in - the Problem. + Use them for quadratic objectives (``Problem.setObjective``) or quadratic + constraints via ``<=`` or ``>=`` comparisons passed to + :py:meth:`Problem.addConstraint` (equality is not supported). QuadraticExpressions can be added and subtracted with other QuadraticExpressions, LinearExpressions, and Variables, and can also be multiplied and divided by scalars. @@ -868,23 +869,55 @@ def __eq__(self, other): def _quadratic_expression_to_qcmatrix(expr, rhs): - """Build QCMATRIX COO data for ``expr <= rhs`` (constant moved to rhs).""" + """Build QCMATRIX COO data for a quadratic row ``expr`` sense ``rhs``. + + Used for both ``<=`` (``LE``) and ``>=`` (``GE``); row sense is stored on + ``Constraint.Sense``, not in this helper. The constant term is moved to + ``rhs_value``. + + Duplicate linear variable indices and duplicate Q (row, col) triplets are + merged by summing coefficients, matching linear ``Constraint`` behavior. + """ rhs_value = float(rhs) - expr.constant + + linear_coeff = {} + for var, coeff in zip(expr.vars, expr.coefficients): + if coeff == 0.0: + continue + idx = var.index + linear_coeff[idx] = linear_coeff.get(idx, 0.0) + coeff + linear_indices = [] linear_values = [] - for var, coeff in zip(expr.vars, expr.coefficients): + for idx in sorted(linear_coeff): + coeff = linear_coeff[idx] if coeff != 0.0: - linear_indices.append(var.index) + linear_indices.append(idx) linear_values.append(coeff) - quadratic_row_indices = [var.index for var in expr.qvars1] - quadratic_col_indices = [var.index for var in expr.qvars2] - quadratic_values = list(expr.qcoefficients) + + quad_coeff = {} + for var1, var2, coeff in zip(expr.qvars1, expr.qvars2, expr.qcoefficients): + if coeff == 0.0: + continue + key = (var1.index, var2.index) + quad_coeff[key] = quad_coeff.get(key, 0.0) + coeff if expr.qmatrix is not None: q_coo = expr.qmatrix.tocoo() for row, col, value in zip(q_coo.row, q_coo.col, q_coo.data): - quadratic_row_indices.append(expr.qvars[row].index) - quadratic_col_indices.append(expr.qvars[col].index) + if value == 0.0: + continue + key = (expr.qvars[row].index, expr.qvars[col].index) + quad_coeff[key] = quad_coeff.get(key, 0.0) + value + + quadratic_row_indices = [] + quadratic_col_indices = [] + quadratic_values = [] + for (row, col), value in sorted(quad_coeff.items()): + if value != 0.0: + quadratic_row_indices.append(row) + quadratic_col_indices.append(col) quadratic_values.append(value) + return ( linear_values, linear_indices, diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py index ec72e8de7c..64b94182e2 100644 --- a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py @@ -122,6 +122,26 @@ def test_quadratic_constraint_api(): ) dm.clear_quadratic_constraints() +def test_quadratic_constraint_duplicate_terms_merged(): + """Duplicate linear/Q terms in a quadratic constraint are summed on export.""" + prob = Problem() + x = prob.addVariable(lb=0.0) + y = prob.addVariable(lb=0.0) + c = prob.addConstraint(2 * x * x - y + 5 * x * x + 3 * y <= 18.0) + assert c.is_quadratic + assert c.rhs_value == pytest.approx(18.0) + linear = dict(zip(c.linear_indices.tolist(), c.linear_values.tolist())) + assert linear == {y.index: 2.0} + quad = list( + zip( + c.quadratic_row_indices.tolist(), + c.quadratic_col_indices.tolist(), + c.quadratic_values.tolist(), + ) + ) + assert quad == [(x.index, x.index, 7.0)] + + def test_quadratic_constraint_problem_solve(): """Problem.addConstraint + solve (Lorentz via expression).""" prob = Problem() @@ -139,6 +159,24 @@ def test_quadratic_constraint_problem_solve(): assert len(prob.getQuadraticConstraints()) == 1 +def test_quadratic_constraint_duplicate_terms_solve(): + """Duplicate Q terms merge to the same Lorentz row (-x0^2 + x1^2 + x2^2 <= 0).""" + prob = Problem() + x0 = prob.addVariable(lb=0.0, name="x0") + x1 = prob.addVariable(lb=0.0, name="x1") + x2 = prob.addVariable(lb=0.0, name="x2") + prob.addConstraint(x1 == 1.0) + prob.addConstraint( + -4 * x0 * x0 + 3 * x0 * x0 + x1 * x1 + x2 * x2 <= 0, name="soc" + ) + prob.setObjective(x0) + + solution = prob.solve(_barrier_settings()) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + + def test_quadratic_constraint_problem_solve_ge_sense(): """>= quadratic rows are converted to <= form when building SOCP for the barrier.""" prob = Problem() diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py index 39f4065fd7..4acb964c01 100644 --- a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py @@ -92,6 +92,44 @@ def test_socp_mps_write_parse_solve(tmp_path): assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) +def test_socp_mps_write_parse_preserves_ge_sense(tmp_path): + """writeMPS -> ParseMps preserves QCMATRIX row sense ('G' for >=).""" + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + # x0^2 - x1^2 - x2^2 >= 0 (MPS row type 'G') + dm.add_quadratic_constraint( + constraint_row_index=1, + constraint_row_name="soc_ge", + sense="G", + quadratic_values=np.array([1.0, -1.0, -1.0], dtype=np.float64), + quadratic_row_indices=np.array([0, 1, 2], dtype=np.int32), + quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), + ) + + mps_file = tmp_path / "socp_ge.mps" + dm.writeMPS(str(mps_file)) + + dm2 = mps_parser.ParseMps(str(mps_file)) + qcs = dm2.get_quadratic_constraints() + assert len(qcs) == 1 + assert qcs[0]["constraint_row_name"] == "soc_ge" + assert qcs[0]["constraint_row_type"] == "G" + + def test_parse_qcmatrix_dataset_mps(): """Parse external QCQP MPS with QCMATRIX when dataset is available.""" mps_path = os.path.join(RAPIDS_DATASET_ROOT_DIR, "qcqp", "QC_Test_1.mps") From 31329618247b0ef316d30bbdea578ea2f13e21b8 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Tue, 19 May 2026 02:43:32 -0700 Subject: [PATCH 51/68] fix a bug for the mps writer --- cpp/src/io/mps_writer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/io/mps_writer.cpp b/cpp/src/io/mps_writer.cpp index 094534d88e..dae7d75a9a 100644 --- a/cpp/src/io/mps_writer.cpp +++ b/cpp/src/io/mps_writer.cpp @@ -248,8 +248,8 @@ void mps_writer_t::write(const std::string& mps_file_path) const auto& qc = quadratic_constraints[q]; std::string row_name = qc.constraint_row_name.empty() ? "QC" + std::to_string(q) : qc.constraint_row_name; - // Quadratic rows are currently restricted to MPS 'L' (<=). - mps_file << " L " << row_name << "\n"; + char const type = qc.constraint_row_type; + mps_file << " " << type << " " << row_name << "\n"; } // COLUMNS section From f83c1b55ed3983b05cf6d3bac7bc5a68a7f63601 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Tue, 19 May 2026 11:31:00 -0700 Subject: [PATCH 52/68] correct multiplication operation in IR --- cpp/src/barrier/barrier.cu | 42 ++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index f9aa2c2426..f9a2e9e6ec 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -65,6 +65,29 @@ namespace cuopt::linear_programming::dual_simplex { cuda::std::make_tuple(a, b), out, size, cuda::std::multiplies<>{}, stream.value()); } +// out[i] = is_free[i] ? 0 : a[i] * b[i] +[[maybe_unused]] static void pairwise_multiply_skip_free( + float* a, float* b, int* is_free, float* out, int size, rmm::cuda_stream_view stream) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(a, b, is_free), + out, + size, + [] __host__ __device__(float x_j, float d_j, int free_j) { return free_j ? 0.f : x_j * d_j; }, + stream.value()); +} + +[[maybe_unused]] static void pairwise_multiply_skip_free( + double* a, double* b, int* is_free, double* out, int size, rmm::cuda_stream_view stream) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(a, b, is_free), + out, + size, + [] __host__ __device__(double x_j, double d_j, int free_j) { return free_j ? 0.0 : x_j * d_j; }, + stream.value()); +} + [[maybe_unused]] static void axpy( float alpha, float* x, float beta, float* y, float* out, int size, rmm::cuda_stream_view stream) { @@ -1638,14 +1661,21 @@ class iteration_data_t { // y1 <- alpha ( -D * x_1 + A^T x_2) + beta * y1 rmm::device_uvector d_r1(n, handle_ptr->get_stream()); + thrust::fill_n(rmm::exec_policy(stream_view_), d_r1.begin(), n, f_t(0)); + + // r1 <- D * x_1 on linear indices; barrier D is zero on native free variables + const i_t linear_n = has_soc ? cone_start() : n; + if (n_free_vars > 0) { + pairwise_multiply_skip_free( + d_x1.data(), d_diag_.data(), d_is_free_.data(), d_r1.data(), linear_n, stream_view_); + } else { + pairwise_multiply(d_x1.data(), d_diag_.data(), d_r1.data(), linear_n, stream_view_); + } + RAFT_CHECK_CUDA(stream_view_); - // diag.pairwise_product(x1, r1); - // r1 <- D * x_1 - pairwise_multiply(d_x1.data(), d_diag_.data(), d_r1.data(), n, stream_view_); + // r1 <- D * x_1 + H x_1 (cone Hessian block H = S^T S; accumulate_cone_hessian_matvec) if (has_soc) { const i_t m_c = cone_entry_count(); - thrust::fill_n(rmm::exec_policy(stream_view_), d_r1.begin() + cone_start(), m_c, f_t(0)); - // r1 <- D * x_1 + H x_1 (cone Hessian block H = S^T S; accumulate_cone_hessian_matvec) accumulate_cone_hessian_matvec(raft::device_span(d_x1.data() + cone_start(), m_c), cones(), raft::device_span(d_r1.data() + cone_start(), m_c), @@ -1660,7 +1690,7 @@ class iteration_data_t { } // y1 <- - alpha * r1 + beta * y1 - // y1.axpy(-alpha, r1, beta); + // flip the sign of r1 = (Q x1 + D x1 + H x1) axpy(-alpha, d_r1.data(), beta, d_y1.data(), d_y1.data(), n, stream_view_); // matrix_transpose_vector_multiply(A, alpha, x2, 1.0, y1); From 4f2a2c1352692feb2a5e1730b6698e6fd4e77168 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Tue, 19 May 2026 13:34:49 -0700 Subject: [PATCH 53/68] clarifying 'free' to be free variables in linear parts --- cpp/src/barrier/barrier.cu | 299 ++++++++++-------- cpp/src/dual_simplex/presolve.cpp | 9 +- cpp/src/dual_simplex/presolve.hpp | 2 +- .../dual_simplex/unit_tests/solve_barrier.cu | 6 +- 4 files changed, 174 insertions(+), 142 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index f9a2e9e6ec..f839317e2b 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -65,23 +65,23 @@ namespace cuopt::linear_programming::dual_simplex { cuda::std::make_tuple(a, b), out, size, cuda::std::multiplies<>{}, stream.value()); } -// out[i] = is_free[i] ? 0 : a[i] * b[i] -[[maybe_unused]] static void pairwise_multiply_skip_free( - float* a, float* b, int* is_free, float* out, int size, rmm::cuda_stream_view stream) +// out[i] = is_native_free_linear[i] ? 0 : a[i] * b[i] +[[maybe_unused]] static void pairwise_multiply_skip_native_free_linear( + float* a, float* b, int* is_native_free_linear, float* out, int size, rmm::cuda_stream_view stream) { cub::DeviceTransform::Transform( - cuda::std::make_tuple(a, b, is_free), + cuda::std::make_tuple(a, b, is_native_free_linear), out, size, [] __host__ __device__(float x_j, float d_j, int free_j) { return free_j ? 0.f : x_j * d_j; }, stream.value()); } -[[maybe_unused]] static void pairwise_multiply_skip_free( - double* a, double* b, int* is_free, double* out, int size, rmm::cuda_stream_view stream) +[[maybe_unused]] static void pairwise_multiply_skip_native_free_linear( + double* a, double* b, int* is_native_free_linear, double* out, int size, rmm::cuda_stream_view stream) { cub::DeviceTransform::Transform( - cuda::std::make_tuple(a, b, is_free), + cuda::std::make_tuple(a, b, is_native_free_linear), out, size, [] __host__ __device__(double x_j, double d_j, int free_j) { return free_j ? 0.0 : x_j * d_j; }, @@ -120,7 +120,7 @@ class iteration_data_t { public: iteration_data_t(const lp_problem_t& lp, i_t num_upper_bounds, - const std::vector& free_variable_indices, + const std::vector& native_free_linear_indices, const csc_matrix_t& Qin, const simplex_solver_settings_t& settings) : upper_bounds(num_upper_bounds), @@ -195,8 +195,8 @@ class iteration_data_t { d_cone_Q_values_(0, lp.handle_ptr->get_stream()), use_augmented(false), has_factorization(false), - n_free_vars(0), - d_is_free_(0, lp.handle_ptr->get_stream()), + n_native_free_linear(0), + d_is_native_free_linear_(0, lp.handle_ptr->get_stream()), num_factorizations(0), has_solve_info(false), settings_(settings), @@ -256,16 +256,16 @@ class iteration_data_t { { raft::common::nvtx::range fun_scope("Barrier: LP Data Creation"); - // Set up free variable tracking for QPs - if (!free_variable_indices.empty()) { - n_free_vars = free_variable_indices.size(); - std::vector is_free_host(lp.num_cols, 0); - for (i_t j : free_variable_indices) { - is_free_host[j] = 1; + // Set up native free linear variable tracking (linear columns only, j < cone_start). + if (!native_free_linear_indices.empty()) { + n_native_free_linear = native_free_linear_indices.size(); + std::vector is_native_free_linear_host(lp.num_cols, 0); + for (i_t j : native_free_linear_indices) { + is_native_free_linear_host[j] = 1; } - d_is_free_.resize(lp.num_cols, stream_view_); - raft::copy(d_is_free_.data(), is_free_host.data(), lp.num_cols, stream_view_); - settings.log.printf("Free variables (QP) : %d\n", n_free_vars); + d_is_native_free_linear_.resize(lp.num_cols, stream_view_); + raft::copy(d_is_native_free_linear_.data(), is_native_free_linear_host.data(), lp.num_cols, stream_view_); + settings.log.printf("Native free linear (QP): %d\n", n_native_free_linear); } bool has_Q = Q.x.size() > 0; @@ -534,7 +534,7 @@ class iteration_data_t { const bool has_soc = has_cones(); f_t degree = static_cast(num_primal_variables) + static_cast(num_upper_bounds); // Native QP free variables (linear only): no x·z complementarity in the barrier degree. - degree -= static_cast(n_free_vars); + degree -= static_cast(n_native_free_linear); if (has_soc) { degree -= static_cast(cone_entry_count()); degree += static_cast(cone_count()); @@ -735,6 +735,8 @@ class iteration_data_t { const i_t linear_n = has_soc ? cone_start() : n; // Primal diagonal: linear block includes dual_perturb; SOC block is filled by scatter below. + // Native free variables use barrier D = 0 in augmented_multiply; omit span_diag[j] here so + // the factorized matrix matches the matvec (only -q_diag - dual_perturb on the diagonal). thrust::for_each_n(rmm::exec_policy(handle_ptr->get_stream()), thrust::make_counting_iterator(0), linear_n, @@ -742,10 +744,11 @@ class iteration_data_t { span_diag_indices = cuopt::make_span(d_augmented_diagonal_indices_), span_q_diag = cuopt::make_span(d_Q_diag_), span_diag = cuopt::make_span(d_diag_), + span_is_native_free_linear = cuopt::make_span(d_is_native_free_linear_), dual_perturb_value = dual_perturb] __device__(i_t j) { - f_t q_diag = span_q_diag.size() > 0 ? span_q_diag[j] : 0.0; - span_x[span_diag_indices[j]] = - -q_diag - span_diag[j] - dual_perturb_value; + f_t q_diag = span_q_diag.size() > 0 ? span_q_diag[j] : 0.0; + const f_t d_j = (span_is_native_free_linear.size() > 0 && span_is_native_free_linear[j]) ? f_t(0) : span_diag[j]; + span_x[span_diag_indices[j]] = -q_diag - d_j - dual_perturb_value; }); RAFT_CHECK_CUDA(handle_ptr->get_stream()); @@ -1665,9 +1668,9 @@ class iteration_data_t { // r1 <- D * x_1 on linear indices; barrier D is zero on native free variables const i_t linear_n = has_soc ? cone_start() : n; - if (n_free_vars > 0) { - pairwise_multiply_skip_free( - d_x1.data(), d_diag_.data(), d_is_free_.data(), d_r1.data(), linear_n, stream_view_); + if (n_native_free_linear > 0) { + pairwise_multiply_skip_native_free_linear( + d_x1.data(), d_diag_.data(), d_is_native_free_linear_.data(), d_r1.data(), linear_n, stream_view_); } else { pairwise_multiply(d_x1.data(), d_diag_.data(), d_r1.data(), linear_n, stream_view_); } @@ -1790,8 +1793,8 @@ class iteration_data_t { bool use_augmented; i_t symbolic_status; - i_t n_free_vars{0}; - rmm::device_uvector d_is_free_; // 1 if native free (QP/SOCP augmented), 0 otherwise + i_t n_native_free_linear{0}; + rmm::device_uvector d_is_native_free_linear_; // 1 if native free linear (j < cone_start), else 0 // Adaptive regularization for the augmented system f_t dual_perturb{1e-8}; @@ -2004,7 +2007,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) raft::common::nvtx::range fun_scope("Barrier: initial_point"); const bool use_augmented = data.use_augmented; const bool has_soc = data.has_cones(); - const bool has_free = data.n_free_vars > 0; + const bool has_native_free_linear = data.n_native_free_linear > 0; // Perform a numerical factorization i_t status; @@ -2153,8 +2156,8 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.x[j] = mu; data.z[j] = mu; } - if (has_free) { - for (i_t j : presolve_info.free_variable_indices) { + if (has_native_free_linear) { + for (i_t j : presolve_info.native_free_linear_indices) { if (j < cs) { data.z[j] = 0.0; } } } @@ -2223,8 +2226,8 @@ int barrier_solver_t::initial_point(iteration_data_t& data) } } // Free variables have z = 0 (no complementarity condition) - if (has_free) { - for (i_t j : presolve_info.free_variable_indices) { + if (has_native_free_linear) { + for (i_t j : presolve_info.native_free_linear_indices) { data.z[j] = 0.0; } } @@ -2292,18 +2295,18 @@ int barrier_solver_t::initial_point(iteration_data_t& data) // Make sure (w, x, v, z) > 0. Cone blocks are not nonnegative orthant variables — skip them for // box positivity; NT scaling keeps cone iterates feasible separately. std::vector free_x_save; - if (has_free) { - free_x_save.resize(data.n_free_vars); - for (i_t k = 0; k < data.n_free_vars; ++k) { - free_x_save[k] = data.x[presolve_info.free_variable_indices[k]]; + if (has_native_free_linear) { + free_x_save.resize(data.n_native_free_linear); + for (i_t k = 0; k < data.n_native_free_linear; ++k) { + free_x_save[k] = data.x[presolve_info.native_free_linear_indices[k]]; } } data.w.ensure_positive(epsilon_adjust); if (has_soc) { data.x.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); - } else if (has_free) { + } else if (has_native_free_linear) { std::vector nonnegative_variables(data.x.size(), 1); - for (i_t j : presolve_info.free_variable_indices) { + for (i_t j : presolve_info.native_free_linear_indices) { nonnegative_variables[j] = 0; } data.x.ensure_positive(epsilon_adjust, nonnegative_variables); @@ -2311,9 +2314,9 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.x.ensure_positive(epsilon_adjust); } // Native free variables (QP/SOCP): restore possibly negative x and set z = 0 - if (has_free) { - for (i_t k = 0; k < data.n_free_vars; ++k) { - i_t j = presolve_info.free_variable_indices[k]; + if (has_native_free_linear) { + for (i_t k = 0; k < data.n_native_free_linear; ++k) { + i_t j = presolve_info.native_free_linear_indices[k]; data.x[j] = free_x_save[k]; data.z[j] = 0.0; } @@ -2530,23 +2533,23 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t& dx) { const bool has_soc = data.has_cones() && static_cast(x.size()) >= data.cone_end(); - const bool has_free = data.n_free_vars > 0 && static_cast(x.size()) == lp.num_cols; + const bool has_native_free_linear = data.n_native_free_linear > 0 && static_cast(x.size()) == lp.num_cols; auto reduce_segment = [&](i_t start, i_t len) -> f_t { if (len <= 0) { return f_t(1); } - if (has_free) { - auto is_free_ptr = data.d_is_free_.data() + start; + if (has_native_free_linear) { + auto native_free_linear_ptr = data.d_is_native_free_linear_.data() + start; // step size computation for nonnegative variables with free variables - auto ratio_test_free = [is_free_ptr] HD(const thrust::tuple t) { + auto ratio_test_free = [native_free_linear_ptr] HD(const thrust::tuple t) { const f_t dx_val = thrust::get<0>(t); const f_t x_val = thrust::get<1>(t); - const i_t is_free = thrust::get<2>(t); - if (is_free) return f_t(1.0); + const i_t is_native_free_linear = thrust::get<2>(t); + if (is_native_free_linear) return f_t(1.0); if (dx_val < f_t(0.0)) return -x_val / dx_val; return f_t(1.0); }; return data.transform_reduce_helper_.transform_reduce( - thrust::make_zip_iterator(dx.data() + start, x.data() + start, is_free_ptr), + thrust::make_zip_iterator(dx.data() + start, x.data() + start, native_free_linear_ptr), thrust::minimum(), ratio_test_free, f_t(1.0), @@ -2599,7 +2602,7 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0; + const bool has_native_free_linear = data.n_native_free_linear > 0; const i_t m_c = data.cone_entry_count(); const i_t cone_var_start = data.cone_start(); const i_t linear_size = data.linear_xz_size(lp.num_cols); @@ -2734,28 +2737,51 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0 && data.Q_diagonal) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple( - data.d_z_.data(), data.d_x_.data(), data.d_is_free_.data(), data.d_Q_diag_.data()), - data.d_diag_.data(), - linear_size, - [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_free, f_t q_jj) { - if (!is_free) return z_j / x_j; - return (q_jj > f_t(0)) ? f_t(0) : free_var_reg; - }, - stream_view_.value()); + if (has_native_free_linear) { + if (use_augmented) { + // Augmented KKT: D = 0 on native free vars (matrix regularization is dual_perturb only). + if (data.Q.n > 0 && data.Q_diagonal) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple( + data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data(), data.d_Q_diag_.data()), + data.d_diag_.data(), + linear_size, + [] HD(f_t z_j, f_t x_j, i_t is_native_free_linear, f_t /*q_jj*/) { + if (!is_native_free_linear) return z_j / x_j; + return f_t(0); + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data()), + data.d_diag_.data(), + linear_size, + [] HD(f_t z_j, f_t x_j, i_t is_native_free_linear) { return is_native_free_linear ? f_t(0) : (z_j / x_j); }, + stream_view_.value()); + } } else { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), data.d_is_free_.data()), - data.d_diag_.data(), - linear_size, - [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_free) { - return is_free ? free_var_reg : (z_j / x_j); - }, - stream_view_.value()); + constexpr f_t free_var_reg = 1e-7; + if (data.Q.n > 0 && data.Q_diagonal) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple( + data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data(), data.d_Q_diag_.data()), + data.d_diag_.data(), + linear_size, + [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_native_free_linear, f_t q_jj) { + if (!is_native_free_linear) return z_j / x_j; + return (q_jj > f_t(0)) ? f_t(0) : free_var_reg; + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data()), + data.d_diag_.data(), + linear_size, + [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_native_free_linear) { + return is_native_free_linear ? free_var_reg : (z_j / x_j); + }, + stream_view_.value()); + } } } else { cub::DeviceTransform::Transform(cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data()), @@ -2800,16 +2826,20 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t 0) { cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_inv_diag.data(), - data.d_tmp3_.data(), + cuda::std::make_tuple(data.d_bound_rhs_.data(), + data.d_v_.data(), + data.d_complementarity_wv_rhs_.data(), + data.d_w_.data()), + thrust::make_permutation_iterator(data.d_tmp3_.data(), data.d_upper_bounds_.data()), + data.n_upper_bounds, + [] HD(f_t bound_rhs, f_t v, f_t complementarity_wv_rhs, f_t w) { + return (complementarity_wv_rhs - v * bound_rhs) / w; + }, + stream_view_.value()); + RAFT_CHECK_CUDA(stream_view_); + } + if (has_native_free_linear) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_tmp3_.data(), data.d_complementarity_target_.data(), data.d_dual_rhs_.data(), - data.d_is_free_.data()), - thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + data.d_is_native_free_linear_.data()), + data.d_tmp3_.data(), lp.num_cols, - [] HD(f_t inv_diag, f_t tmp3, f_t target, f_t dual_rhs, i_t is_free) - -> thrust::tuple { - const f_t comp_term = is_free ? f_t(0) : target; - const f_t tmp = tmp3 + dual_rhs - comp_term; - return {tmp, inv_diag * tmp}; + [] HD(f_t tmp3, f_t target, f_t dual_rhs, i_t is_native_free_linear) { + const f_t comp_term = is_native_free_linear ? f_t(0) : target; + return tmp3 + dual_rhs - comp_term; }, stream_view_.value()); } else { cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_inv_diag.data(), - data.d_tmp3_.data(), - data.d_dual_rhs_.data(), - data.d_complementarity_target_.data()), - thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + cuda::std::make_tuple( + data.d_tmp3_.data(), data.d_dual_rhs_.data(), data.d_complementarity_target_.data()), + data.d_tmp3_.data(), lp.num_cols, - [] HD(f_t inv_diag, f_t tmp3, f_t dual_rhs, f_t target) -> thrust::tuple { - const f_t tmp = tmp3 + dual_rhs - target; - return {tmp, inv_diag * tmp}; - }, + [] HD(f_t tmp3, f_t dual_rhs, f_t target) { return tmp3 + dual_rhs - target; }, stream_view_.value()); } RAFT_CHECK_CUDA(stream_view_); raft::copy(data.d_r1_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); raft::copy(data.d_r1_prime_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); + } - // h <- A @ tmp4 .+ primal_rhs + if (!use_augmented) { + raft::common::nvtx::range fun_scope("Barrier: GPU compute H"); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_inv_diag.data(), data.d_tmp3_.data()), + data.d_tmp4_.data(), + lp.num_cols, + [] HD(f_t inv_diag, f_t tmp3) { return inv_diag * tmp3; }, + stream_view_.value()); + RAFT_CHECK_CUDA(stream_view_); data.cusparse_view_.spmv(1, data.cusparse_tmp4_, 1, data.cusparse_h_); } if (use_augmented) { raft::common::nvtx::range fun_scope("Barrier: GPU augmented solve"); - // r1 <- dual_rhs -complementarity_xz_rhs ./ x + E * ((complementarity_wv_rhs - v .* bound_rhs) - // ./ w) + // Augmented RHS [dx; dy]: primal block is d_r1_ (assembled above). + // linear j: dual_rhs[j] - complementarity_target[j] + // + E_j*((complementarity_wv_rhs - v.*bound_rhs)./w) (target = xz_rhs/x; free: 0) + // cone j: dual_rhs[j] - complementarity_target[j] (NT target: -z or combined centering term) + // Constraint block: primal_rhs. // TODO: d_augmented_rhs can be preallocated rmm::device_uvector d_augmented_rhs(lp.num_cols + lp.num_rows, stream_view_); @@ -3232,17 +3263,17 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0) { - if (has_free) { + if (has_native_free_linear) { cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_complementarity_target_.data(), data.d_z_.data(), data.d_dx_.data(), data.d_x_.data(), - data.d_is_free_.data()), + data.d_is_native_free_linear_.data()), data.d_dz_.data(), linear_dz_size, - [] HD(f_t target, f_t z, f_t dx, f_t x, i_t is_free) { - return is_free ? f_t(0) : (target - (z * dx) / x); + [] HD(f_t target, f_t z, f_t dx, f_t x, i_t is_native_free_linear) { + return is_native_free_linear ? f_t(0) : (target - (z * dx) / x); }, stream_view_.value()); } else { @@ -3586,21 +3617,21 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data { raft::common::nvtx::range fun_scope("Barrier: compute_cc_rhs"); const bool has_soc = data.has_cones(); - const bool has_free = data.n_free_vars > 0; + const bool has_native_free_linear = data.n_native_free_linear > 0; const i_t linear_size = data.linear_xz_size(lp.num_cols); auto fill_linear_cc_rhs = [&](raft::device_span out, raft::device_span dx_aff, raft::device_span dz_aff) { if (out.empty()) return; - if (has_free) { - auto is_free_ptr = data.d_is_free_.data(); + if (has_native_free_linear) { + auto native_free_linear_ptr = data.d_is_native_free_linear_.data(); cub::DeviceTransform::Transform( - cuda::std::make_tuple(dx_aff.data(), dz_aff.data(), is_free_ptr), + cuda::std::make_tuple(dx_aff.data(), dz_aff.data(), native_free_linear_ptr), out.data(), out.size(), - [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val, i_t is_free) { - return is_free ? f_t(0) : (-(dx_aff_val * dz_aff_val) + new_mu); + [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val, i_t is_native_free_linear) { + return is_native_free_linear ? f_t(0) : (-(dx_aff_val * dz_aff_val) + new_mu); }, stream_view_.value()); } else { @@ -4106,7 +4137,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t 0) { create_Q(lp, Q); } iteration_data_t data( - lp, num_upper_bounds, presolve_info.free_variable_indices, Q, settings); + lp, num_upper_bounds, presolve_info.native_free_linear_indices, Q, settings); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Barrier solver halted\n"); return lp_status_t::CONCURRENT_LIMIT; diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index d68ebd9a2d..e614504f67 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -1259,16 +1259,17 @@ i_t presolve(const lp_problem_t& original, problem.Q.n > 0 || !problem.second_order_cone_dims.empty(); if (settings.barrier_presolve && free_variables > 0 && keep_native_free_variables) { presolve_info.free_variable_pairs.clear(); - presolve_info.free_variable_indices.clear(); + presolve_info.native_free_linear_indices.clear(); // Only linear decision variables can be "native" free variables; cone/stack columns // are unbounded by construction and must not be counted here. for (i_t j = 0; j < linear_var_count(problem); j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { - presolve_info.free_variable_indices.push_back(j); + presolve_info.native_free_linear_indices.push_back(j); } } - settings.log.printf("Keeping %d free variables for augmented-system barrier (QP/SOCP)\n", - free_variables); + settings.log.printf( + "Keeping %d native free linear variables for augmented-system barrier (QP/SOCP)\n", + free_variables); } else if (settings.barrier_presolve && free_variables > 0) { // We have a variable x_j: with -inf < x_j < inf // we create new variables v and w with 0 <= v, w and x_j = v - w diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index c63a4d5076..a8299cd228 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -157,7 +157,7 @@ struct presolve_info_t { std::vector negated_variables; // Free variable indices for QP/SOCP augmented barrier (not split, handled natively) - std::vector free_variable_indices; + std::vector native_free_linear_indices; }; template diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index a97bcd885e..1d26ae523a 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -357,9 +357,9 @@ TEST(barrier, presolve_keeps_native_free_variables_before_cones) EXPECT_EQ(presolved_lp.cone_var_start, 2); EXPECT_EQ(presolved_lp.second_order_cone_dims, std::vector({3})); EXPECT_TRUE(presolve_info.free_variable_pairs.empty()); - ASSERT_EQ(presolve_info.free_variable_indices.size(), 2); - EXPECT_EQ(presolve_info.free_variable_indices[0], 0); - EXPECT_EQ(presolve_info.free_variable_indices[1], 1); + ASSERT_EQ(presolve_info.native_free_linear_indices.size(), 2); + EXPECT_EQ(presolve_info.native_free_linear_indices[0], 0); + EXPECT_EQ(presolve_info.native_free_linear_indices[1], 1); EXPECT_EQ(presolved_lp.lower[0], -inf); EXPECT_EQ(presolved_lp.lower[1], -inf); EXPECT_EQ(presolved_lp.upper[0], inf); From 52f5640f0c9fd538dc47bed8a7aa08136721d800 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Tue, 19 May 2026 13:41:53 -0700 Subject: [PATCH 54/68] --amend --- cpp/src/barrier/barrier.cu | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index f839317e2b..f36896281b 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -2739,26 +2739,16 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0 && data.Q_diagonal) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple( - data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data(), data.d_Q_diag_.data()), - data.d_diag_.data(), - linear_size, - [] HD(f_t z_j, f_t x_j, i_t is_native_free_linear, f_t /*q_jj*/) { - if (!is_native_free_linear) return z_j / x_j; - return f_t(0); - }, - stream_view_.value()); - } else { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data()), - data.d_diag_.data(), - linear_size, - [] HD(f_t z_j, f_t x_j, i_t is_native_free_linear) { return is_native_free_linear ? f_t(0) : (z_j / x_j); }, - stream_view_.value()); - } + // Augmented KKT: D = 0 on native free linear vars (Q curvature is in the KKT block, not D). + cub::DeviceTransform::Transform( + cuda::std::make_tuple( + data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data()), + data.d_diag_.data(), + linear_size, + [] HD(f_t z_j, f_t x_j, i_t is_native_free_linear) { + return is_native_free_linear ? f_t(0) : (z_j / x_j); + }, + stream_view_.value()); } else { constexpr f_t free_var_reg = 1e-7; if (data.Q.n > 0 && data.Q_diagonal) { From 5a5d6f20c2361313a668f8a01546b5c9d0a93f42 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 20 May 2026 02:29:47 -0700 Subject: [PATCH 55/68] move cone complimentarity computation into affine and corrector step --- cpp/src/barrier/barrier.cu | 366 +++++++++++------- cpp/src/barrier/second_order_cone_kernels.cuh | 2 +- 2 files changed, 222 insertions(+), 146 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index f36896281b..c8555cb83a 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -2607,115 +2607,42 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t target, - raft::device_span xz_rhs, - raft::device_span x) { - if (target.empty()) return; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(xz_rhs.data(), x.data()), - target.data(), - target.size(), - [] HD(f_t complementarity_xz_rhs, f_t x_val) { return complementarity_xz_rhs / x_val; }, - stream_view_.value()); - RAFT_CHECK_CUDA(stream_view_); - }; - - auto recover_linear_dz = [&](raft::device_span target, - raft::device_span z, - raft::device_span dx_span, - raft::device_span x, - raft::device_span dz_span) { + // Linear (orthant) block only; SOC uses recover_cone_dz_from_target. + auto recover_linear_orthant_dz = [&](raft::device_span target, + raft::device_span z, + raft::device_span dx_span, + raft::device_span x, + raft::device_span dz_span) { if (dz_span.empty()) return; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(target.data(), z.data(), dx_span.data(), x.data()), - dz_span.data(), - dz_span.size(), - [] HD(f_t target_val, f_t z_val, f_t dx_val, f_t x_val) { - return target_val - (z_val * dx_val) / x_val; - }, - stream_view_.value()); - RAFT_CHECK_CUDA(stream_view_); - }; - - { - raft::common::nvtx::range fun_scope("Barrier: GPU allocation and copies"); - - // TMP allocation and copy should happen only once where it's written in a first place - data.d_bound_rhs_.resize(data.bound_rhs.size(), stream_view_); - raft::copy( - data.d_bound_rhs_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream_view_); - data.d_x_.resize(data.x.size(), stream_view_); - raft::copy(data.d_x_.data(), data.x.data(), data.x.size(), stream_view_); - data.d_z_.resize(data.z.size(), stream_view_); - raft::copy(data.d_z_.data(), data.z.data(), data.z.size(), stream_view_); - data.d_w_.resize(data.w.size(), stream_view_); - raft::copy(data.d_w_.data(), data.w.data(), data.w.size(), stream_view_); - data.d_v_.resize(data.v.size(), stream_view_); - raft::copy(data.d_v_.data(), data.v.data(), data.v.size(), stream_view_); - data.d_upper_bounds_.resize(data.upper_bounds.size(), stream_view_); - raft::copy(data.d_upper_bounds_.data(), - data.upper_bounds.data(), - data.upper_bounds.size(), - stream_view_); - data.d_dy_.resize(dy.size(), stream_view_); - raft::copy(data.d_dy_.data(), dy.data(), dy.size(), stream_view_); - data.d_dx_.resize(dx.size(), stream_view_); - raft::copy(data.d_h_.data(), data.primal_rhs.data(), data.primal_rhs.size(), stream_view_); - raft::copy(data.d_dual_rhs_.data(), data.dual_rhs.data(), data.dual_rhs.size(), stream_view_); - data.d_dz_.resize(dz.size(), stream_view_); - data.d_dv_.resize(dv.size(), stream_view_); - data.d_dw_.resize(data.bound_rhs.size(), stream_view_); - raft::copy(data.d_dw_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream_view_); - data.d_dw_residual_.resize(data.n_upper_bounds, stream_view_); - data.d_wv_residual_.resize(data.d_complementarity_wv_rhs_.size(), stream_view_); - data.d_xz_residual_.resize(data.d_complementarity_xz_rhs_.size(), stream_view_); - data.d_primal_residual_.resize(lp.rhs.size(), stream_view_); - raft::copy(data.d_primal_residual_.data(), lp.rhs.data(), lp.rhs.size(), stream_view_); - data.d_bound_residual_.resize(data.bound_residual.size(), stream_view_); - data.d_upper_.resize(lp.upper.size(), stream_view_); - raft::copy(data.d_upper_.data(), lp.upper.data(), lp.upper.size(), stream_view_); - } - - // 1) Update NT-scaling in gpu_compute_search_direction - // 2) Compute the complementarity part for soc constraints - if (has_soc) { - auto& cones = data.cones(); - cones.x = raft::device_span(data.d_x_.data() + cone_var_start, m_c); - cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); - - if (!data.cone_combined_step_) { launch_nt_scaling(cones, stream_view_); } - - cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); - fill_linear_target( - raft::device_span(data.d_complementarity_target_.data(), linear_size), - raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), - raft::device_span(data.d_x_.data(), linear_size)); - - auto cone_target = - raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c); - if (data.cone_combined_step_) { - compute_combined_cone_rhs_term( - raft::device_span(data.d_dx_aff_.data() + cone_var_start, m_c), - raft::device_span(data.d_dz_aff_.data() + cone_var_start, m_c), - cones, - data.cone_sigma_mu_, - cone_target, - stream_view_); + if (has_native_free_linear) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple( + target.data(), z.data(), dx_span.data(), x.data(), data.d_is_native_free_linear_.data()), + dz_span.data(), + dz_span.size(), + [] HD(f_t target_val, f_t z_val, f_t dx_val, f_t x_val, i_t is_native_free_linear) { + if (is_native_free_linear) return f_t(0); + return target_val - (z_val * dx_val) / x_val; + }, + stream_view_.value()); } else { cub::DeviceTransform::Transform( - cones.z.data(), - cone_target.data(), - m_c, - [] HD(f_t z_val) { return -z_val; }, + cuda::std::make_tuple(target.data(), z.data(), dx_span.data(), x.data()), + dz_span.data(), + dz_span.size(), + [] HD(f_t target_val, f_t z_val, f_t dx_val, f_t x_val) { + return target_val - (z_val * dx_val) / x_val; + }, stream_view_.value()); - RAFT_CUDA_TRY(cudaPeekAtLastError()); } RAFT_CHECK_CUDA(stream_view_); - } else { - fill_linear_target(cuopt::make_span(data.d_complementarity_target_), - cuopt::make_span(data.d_complementarity_xz_rhs_), - cuopt::make_span(data.d_x_)); - } + }; + + copy_step_rhs_to_device(data, stream_view_); + data.d_dx_.resize(dx.size(), stream_view_); + data.d_dy_.resize(dy.size(), stream_view_); + data.d_dz_.resize(dz.size(), stream_view_); + data.d_dv_.resize(dv.size(), stream_view_); // Solves the linear system // @@ -2732,6 +2659,13 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(data.d_x_.data() + cone_var_start, m_c); + cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + launch_nt_scaling(cones, stream_view_); + } + max_residual = 0.0; { raft::common::nvtx::range fun_scope("Barrier: GPU diag, inv diag and sqrt inv diag formation"); @@ -3252,29 +3186,12 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0) { - if (has_native_free_linear) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_complementarity_target_.data(), - data.d_z_.data(), - data.d_dx_.data(), - data.d_x_.data(), - data.d_is_native_free_linear_.data()), - data.d_dz_.data(), - linear_dz_size, - [] HD(f_t target, f_t z, f_t dx, f_t x, i_t is_native_free_linear) { - return is_native_free_linear ? f_t(0) : (target - (z * dx) / x); - }, - stream_view_.value()); - } else { - recover_linear_dz( - raft::device_span(data.d_complementarity_target_.data(), linear_dz_size), - raft::device_span(data.d_z_.data(), linear_dz_size), - raft::device_span(data.d_dx_.data(), linear_dz_size), - raft::device_span(data.d_x_.data(), linear_dz_size), - raft::device_span(data.d_dz_.data(), linear_dz_size)); - } - } + recover_linear_orthant_dz( + raft::device_span(data.d_complementarity_target_.data(), linear_dz_size), + raft::device_span(data.d_z_.data(), linear_dz_size), + raft::device_span(data.d_dx_.data(), linear_dz_size), + raft::device_span(data.d_x_.data(), linear_dz_size), + raft::device_span(data.d_dz_.data(), linear_dz_size)); RAFT_CHECK_CUDA(stream_view_); raft::copy(dz.data(), data.d_dz_.data(), data.d_dz_.size(), stream_view_); } @@ -3465,11 +3382,151 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t +void copy_host_iterate_to_device(iteration_data_t& data, rmm::cuda_stream_view stream) +{ + raft::common::nvtx::range fun_scope("Barrier: copy_host_iterate_to_device"); + + data.d_x_.resize(data.x.size(), stream); + data.d_z_.resize(data.z.size(), stream); + raft::copy(data.d_x_.data(), data.x.data(), data.x.size(), stream); + raft::copy(data.d_z_.data(), data.z.data(), data.z.size(), stream); + + data.d_w_.resize(data.w.size(), stream); + data.d_v_.resize(data.v.size(), stream); + raft::copy(data.d_w_.data(), data.w.data(), data.w.size(), stream); + raft::copy(data.d_v_.data(), data.v.data(), data.v.size(), stream); + + data.d_y_.resize(data.y.size(), stream); + raft::copy(data.d_y_.data(), data.y.data(), data.y.size(), stream); + + data.d_upper_bounds_.resize(data.upper_bounds.size(), stream); + raft::copy(data.d_upper_bounds_.data(), + data.upper_bounds.data(), + data.upper_bounds.size(), + stream); +} + +// One-time static problem data (constant across barrier iterations). +template +void copy_static_problem_data_to_device(const lp_problem_t& lp, + iteration_data_t& data, + rmm::cuda_stream_view stream) +{ + raft::common::nvtx::range fun_scope("Barrier: copy_static_problem_data_to_device"); + + data.d_primal_residual_.resize(lp.rhs.size(), stream); + raft::copy(data.d_primal_residual_.data(), lp.rhs.data(), lp.rhs.size(), stream); + + data.d_upper_.resize(lp.upper.size(), stream); + raft::copy(data.d_upper_.data(), lp.upper.data(), lp.upper.size(), stream); + + data.d_bound_residual_.resize(data.bound_residual.size(), stream); + data.d_dw_residual_.resize(data.n_upper_bounds, stream); +} + +// Per Mehrotra step: affine/corrector RHS only (iterate stays on device from initial sync / next_iterate). +template +void copy_step_rhs_to_device(iteration_data_t& data, rmm::cuda_stream_view stream) +{ + raft::common::nvtx::range fun_scope("Barrier: copy_step_rhs_to_device"); + + data.d_bound_rhs_.resize(data.bound_rhs.size(), stream); + raft::copy( + data.d_bound_rhs_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream); + + raft::copy(data.d_h_.data(), data.primal_rhs.data(), data.primal_rhs.size(), stream); + raft::copy(data.d_dual_rhs_.data(), data.dual_rhs.data(), data.dual_rhs.size(), stream); + + data.d_dw_.resize(data.bound_rhs.size(), stream); + raft::copy(data.d_dw_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream); + + data.d_xz_residual_.resize(data.d_complementarity_xz_rhs_.size(), stream); + data.d_wv_residual_.resize(data.d_complementarity_wv_rhs_.size(), stream); +} + +template +void fill_linear_complementarity_target(iteration_data_t& data, + raft::device_span target, + raft::device_span xz_rhs, + raft::device_span x, + rmm::cuda_stream_view stream) +{ + if (target.empty()) return; + const bool has_native_free_linear = data.n_native_free_linear > 0; + if (has_native_free_linear) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(xz_rhs.data(), x.data(), data.d_is_native_free_linear_.data()), + target.data(), + target.size(), + [] HD(f_t complementarity_xz_rhs, f_t x_val, i_t is_native_free_linear) { + if (is_native_free_linear) return f_t(0); + return complementarity_xz_rhs / x_val; + }, + stream.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(xz_rhs.data(), x.data()), + target.data(), + target.size(), + [] HD(f_t complementarity_xz_rhs, f_t x_val) { return complementarity_xz_rhs / x_val; }, + stream.value()); + } + RAFT_CHECK_CUDA(stream); +} + +template +void fill_affine_cone_complementarity_target(iteration_data_t& data, + i_t cone_var_start, + i_t m_c, + rmm::cuda_stream_view stream) +{ + if (m_c == 0) return; + auto& cones = data.cones(); + cones.x = raft::device_span(data.d_x_.data() + cone_var_start, m_c); + cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + auto cone_target = + raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c); + cub::DeviceTransform::Transform( + cones.z.data(), + cone_target.data(), + m_c, + [] HD(f_t z_val) { return -z_val; }, + stream.value()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + RAFT_CHECK_CUDA(stream); +} + +template +void fill_corrector_cone_complementarity_target(iteration_data_t& data, + i_t cone_var_start, + i_t m_c, + f_t sigma_mu, + rmm::cuda_stream_view stream) +{ + if (m_c == 0) return; + auto& cones = data.cones(); + cones.x = raft::device_span(data.d_x_.data() + cone_var_start, m_c); + cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + auto cone_target = + raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c); + compute_combined_cone_rhs_term(raft::device_span(data.d_dx_aff_.data() + cone_var_start, m_c), + raft::device_span(data.d_dz_aff_.data() + cone_var_start, m_c), + cones, + sigma_mu, + cone_target, + stream); +} + template void barrier_solver_t::compute_affine_rhs(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: compute_affine_rhs"); + const bool has_soc = data.has_cones(); const i_t linear_size = data.linear_xz_size(lp.num_cols); + const i_t cone_var_start = data.cone_start(); + const i_t m_c = data.cone_entry_count(); data.primal_rhs = data.primal_residual; data.bound_rhs = data.bound_residual; @@ -3477,32 +3534,35 @@ void barrier_solver_t::compute_affine_rhs(iteration_data_t& data.cone_combined_step_ = false; data.cone_sigma_mu_ = f_t(0); - raft::copy(data.d_complementarity_wv_rhs_.data(), - data.d_complementarity_wv_residual_.data(), - data.d_complementarity_wv_residual_.size(), - stream_view_); - - auto negate_linear_rhs = [&](raft::device_span out, raft::device_span residual) { + auto negate_complementarity_rhs = [&](raft::device_span out, + raft::device_span residual) { if (out.empty()) return; cub::DeviceTransform::Transform( residual.data(), out.data(), out.size(), - [] HD(f_t xz_rhs) { return -xz_rhs; }, + [] HD(f_t rhs) { return -rhs; }, stream_view_.value()); }; - negate_linear_rhs( + // xz -> -x .* z + negate_complementarity_rhs( raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), raft::device_span(data.d_complementarity_xz_residual_.data(), linear_size)); - RAFT_CHECK_CUDA(stream_view_); // w.*v -> -w .* v - cub::DeviceTransform::Transform( - data.d_complementarity_wv_rhs_.data(), - data.d_complementarity_wv_rhs_.data(), - data.d_complementarity_wv_rhs_.size(), - [] HD(f_t wv_rhs) { return -wv_rhs; }, - stream_view_.value()); + negate_complementarity_rhs(cuopt::make_span(data.d_complementarity_wv_rhs_), + cuopt::make_span(data.d_complementarity_wv_residual_)); RAFT_CHECK_CUDA(stream_view_); + + fill_linear_complementarity_target( + data, + raft::device_span(data.d_complementarity_target_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_x_.data(), linear_size), + stream_view_); + if (has_soc) { + cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); + fill_affine_cone_complementarity_target(data, cone_var_start, m_c, stream_view_); + } } template @@ -3637,6 +3697,22 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data raft::device_span(data.d_dx_aff_.data(), linear_size), raft::device_span(data.d_dz_aff_.data(), linear_size)); RAFT_CHECK_CUDA(stream_view_); + + const i_t cone_var_start = data.cone_start(); + const i_t m_c = data.cone_entry_count(); + + fill_linear_complementarity_target( + data, + raft::device_span(data.d_complementarity_target_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_x_.data(), linear_size), + stream_view_); + if (has_soc) { + cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); + fill_corrector_cone_complementarity_target( + data, cone_var_start, m_c, new_mu, stream_view_); + } + cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_dw_aff_.data(), data.d_dv_aff_.data()), data.d_complementarity_wv_rhs_.data(), @@ -3656,10 +3732,7 @@ template void barrier_solver_t::compute_final_direction(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: compute_final_direction"); - // TODO Nicolas: Redundant copies - data.d_y_.resize(data.y.size(), stream_view_); data.d_dy_aff_.resize(data.dy_aff.size(), stream_view_); - raft::copy(data.d_y_.data(), data.y.data(), data.y.size(), stream_view_); raft::copy(data.d_dy_aff_.data(), data.dy_aff.data(), data.dy_aff.size(), stream_view_); #ifdef FINITE_CHECK @@ -4165,6 +4238,10 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t(data, stream_view_); + copy_static_problem_data_to_device(lp, data, stream_view_); + compute_residuals>(data.w, data.x, data.y, data.v, data.z, data); f_t primal_residual_norm = @@ -4306,7 +4383,6 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t compute_cone_step_length(cone_data_t& cones, * where `o` is the SOC Jordan product and `e = (1, 0, ..., 0)` per cone. * The reduced KKT solve needs the cone target * - * q = -W * p, where p = lambda \ d and lambda = W^{-T} z. + * q = W * (-p), where p = lambda \ d and lambda = W^{-T} z. * * On return, `out` holds `q`. Internally, `out` is reused for `W^{-T} dz_aff` and * then `d`; `scratch.temp_cone` is reused for `W dx_aff`, then `-p`. From 7572551572c36e8b0a138b57c8dea90780eb8e49 Mon Sep 17 00:00:00 2001 From: jolorunyomi Date: Wed, 20 May 2026 12:11:20 -0500 Subject: [PATCH 56/68] Revert "Prepare release/26.06" This reverts commit a6d9a5f0570d28e53a25b7f4227f58b6caf54a8a. --- .github/workflows/build.yaml | 26 ++++++++--------- .github/workflows/nightly-summary.yaml | 2 +- .github/workflows/pr.yaml | 28 +++++++++---------- .github/workflows/test.yaml | 10 +++---- .../trigger-breaking-change-alert.yaml | 2 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 84628a9cda..c2fcac85bc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,7 +53,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -69,7 +69,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -84,7 +84,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@main secrets: CONDA_RAPIDSAI_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_NIGHTLY_TOKEN }} CONDA_RAPIDSAI_TOKEN: ${{ secrets.CONDA_RAPIDSAI_TOKEN }} @@ -101,7 +101,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -119,7 +119,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} @@ -139,7 +139,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -156,7 +156,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} @@ -175,7 +175,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -195,7 +195,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} @@ -215,7 +215,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} node_type: "gpu-l4-latest-1" @@ -235,7 +235,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -256,7 +256,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} @@ -312,7 +312,7 @@ jobs: contents: read runs-on: linux-amd64-cpu4 container: - image: python:26.06-slim + image: python:3.14-slim steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/nightly-summary.yaml b/.github/workflows/nightly-summary.yaml index 079198aee7..96ffe144c2 100644 --- a/.github/workflows/nightly-summary.yaml +++ b/.github/workflows/nightly-summary.yaml @@ -60,7 +60,7 @@ jobs: contents: read runs-on: linux-amd64-cpu4 container: - image: python:26.06-slim + image: python:3.14-slim steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index a24b98580a..44cbcb0a90 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -36,7 +36,7 @@ jobs: - test-self-hosted-server permissions: contents: read - uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@main if: always() with: needs: ${{ toJSON(needs) }} @@ -125,7 +125,7 @@ jobs: contents: read packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/changed-files.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/changed-files.yaml@main with: files_yaml: | build_docs: @@ -377,7 +377,7 @@ jobs: checks: permissions: contents: read - uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@main with: enable_check_generated_files: false conda-cpp-build: @@ -394,7 +394,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@main with: build_type: pull-request script: ci/build_cpp.sh @@ -407,7 +407,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request @@ -433,7 +433,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@main with: build_type: pull-request script: ci/build_python.sh @@ -446,7 +446,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda with: run_codecov: false @@ -469,7 +469,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs with: build_type: pull-request @@ -489,7 +489,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: # build for every combination of arch and CUDA version, but only for the latest Python matrix_filter: ${{ needs.compute-matrix-filters.outputs.libcuopt_filter }} @@ -507,7 +507,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: pull-request script: ci/build_wheel_cuopt.sh @@ -522,7 +522,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels with: build_type: pull-request @@ -545,7 +545,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: pull-request script: ci/build_wheel_cuopt_server.sh @@ -564,7 +564,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: pull-request script: ci/build_wheel_cuopt_sh_client.sh @@ -582,7 +582,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels with: build_type: pull-request diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index eac85da368..8a39c587e4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,7 +35,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -57,7 +57,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@main with: run_codecov: false build_type: ${{ inputs.build_type }} @@ -80,7 +80,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -102,7 +102,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -125,7 +125,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/trigger-breaking-change-alert.yaml b/.github/workflows/trigger-breaking-change-alert.yaml index 930d174da4..0347178e38 100644 --- a/.github/workflows/trigger-breaking-change-alert.yaml +++ b/.github/workflows/trigger-breaking-change-alert.yaml @@ -19,7 +19,7 @@ permissions: {} jobs: trigger-notifier: if: contains(github.event.pull_request.labels.*.name, 'breaking') - uses: rapidsai/shared-workflows/.github/workflows/breaking-change-alert.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/breaking-change-alert.yaml@main secrets: NV_SLACK_BREAKING_CHANGE_ALERT: ${{ secrets.NV_SLACK_BREAKING_CHANGE_ALERT }} permissions: From 37814ed8c4c2aa23cc7dc028c62aed6366239652 Mon Sep 17 00:00:00 2001 From: Joseph <68436579+jolorunyomi@users.noreply.github.com> Date: Wed, 20 May 2026 15:20:33 -0500 Subject: [PATCH 57/68] Update to 26.08 (#1259) This PR updates the repository to version 26.08. This is part of the 26.06 release burndown process. --- .claude-plugin/marketplace.json | 2 +- .cursor-plugin/plugin.json | 2 +- .github/workflows/build.yaml | 2 +- .../workflows/build_test_publish_images.yaml | 2 +- .github/workflows/pr.yaml | 2 +- .github/workflows/test.yaml | 2 +- RAPIDS_BRANCH | 2 +- README.md | 2 +- VERSION | 2 +- .../all_cuda-129_arch-aarch64.yaml | 10 ++-- .../all_cuda-129_arch-x86_64.yaml | 10 ++-- .../all_cuda-132_arch-aarch64.yaml | 10 ++-- .../all_cuda-132_arch-x86_64.yaml | 10 ++-- dependencies.yaml | 50 +++++++++---------- docs/cuopt/source/versions1.json | 8 ++- gemini-extension.json | 2 +- helmchart/cuopt-server/Chart.yaml | 18 +++---- helmchart/cuopt-server/values.yaml | 2 +- python/cuopt/pyproject.toml | 14 +++--- python/cuopt_self_hosted/pyproject.toml | 2 +- python/cuopt_server/pyproject.toml | 2 +- python/libcuopt/pyproject.toml | 4 +- skills/cuopt-developer/SKILL.md | 2 +- skills/cuopt-install/SKILL.md | 2 +- .../SKILL.md | 2 +- .../SKILL.md | 2 +- .../SKILL.md | 2 +- skills/cuopt-routing-api-python/SKILL.md | 2 +- skills/cuopt-server-api-python/SKILL.md | 2 +- skills/cuopt-server-common/SKILL.md | 2 +- skills/cuopt-user-rules/SKILL.md | 2 +- .../SKILL.md | 2 +- skills/routing-formulation/SKILL.md | 2 +- skills/skill-evolution/SKILL.md | 2 +- 34 files changed, 94 insertions(+), 90 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6b17bbe882..d552a14cbf 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,7 +5,7 @@ }, "metadata": { "description": "Agent skills for NVIDIA cuOpt: routing (VRP, TSP, PDP), LP/MILP/QP, installation (user/developer), and REST server.", - "version": "26.06.00" + "version": "26.08.00" }, "plugins": [ { diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index e740506140..268393ba2a 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "nvidia-cuopt-skills", "description": "Agent skills for NVIDIA cuOpt: routing (VRP, TSP, PDP), LP/MILP/QP, installation (Python/C/developer), and REST server. Use when building or solving optimization with cuOpt.", - "version": "26.06.00", + "version": "26.08.00", "author": { "name": "NVIDIA" }, diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c2fcac85bc..f1d4123a6a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -225,7 +225,7 @@ jobs: arch: "amd64" file_to_upload: "docs/cuopt/build/html/" artifact-name: "cuopt_docs" - container_image: "rapidsai/ci-conda:26.06-latest" + container_image: "rapidsai/ci-conda:26.08-latest" script: "ci/build_docs.sh" wheel-build-cuopt-sh-client: permissions: diff --git a/.github/workflows/build_test_publish_images.yaml b/.github/workflows/build_test_publish_images.yaml index 9fd2fc1923..e1fa2efd15 100644 --- a/.github/workflows/build_test_publish_images.yaml +++ b/.github/workflows/build_test_publish_images.yaml @@ -42,7 +42,7 @@ jobs: permissions: contents: read container: - image: rapidsai/ci-conda:26.06-latest + image: rapidsai/ci-conda:26.08-latest outputs: MATRIX: ${{ steps.compute-matrix.outputs.MATRIX }} CUOPT_VER: ${{ steps.compute-cuopt-ver.outputs.CUOPT_VER }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 44cbcb0a90..827cf54578 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -477,7 +477,7 @@ jobs: arch: "amd64" file_to_upload: "docs/cuopt/build/html/" artifact-name: "cuopt_docs" - container_image: "rapidsai/ci-conda:26.06-latest" + container_image: "rapidsai/ci-conda:26.08-latest" script: "ci/build_docs.sh" wheel-build-libcuopt: needs: [compute-matrix-filters, changed-files] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8a39c587e4..01c1e2b0b3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -133,7 +133,7 @@ jobs: sha: ${{ inputs.sha }} node_type: "gpu-l4-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:26.06-latest" + container_image: "rapidsai/ci-conda:26.08-latest" script: ci/test_notebooks.sh nightly-summary: permissions: diff --git a/RAPIDS_BRANCH b/RAPIDS_BRANCH index 5a399d5c50..ba2906d066 100644 --- a/RAPIDS_BRANCH +++ b/RAPIDS_BRANCH @@ -1 +1 @@ -release/26.06 +main diff --git a/README.md b/README.md index 8c75ee7511..79d00e744e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # cuOpt - GPU-accelerated Optimization [![Build Status](https://github.com/NVIDIA/cuopt/actions/workflows/build.yaml/badge.svg)](https://github.com/NVIDIA/cuopt/actions/workflows/build.yaml) -[![Version](https://img.shields.io/badge/version-26.06.00-blue)](https://github.com/NVIDIA/cuopt/releases) +[![Version](https://img.shields.io/badge/version-26.08.00-blue)](https://github.com/NVIDIA/cuopt/releases) [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://docs.nvidia.com/cuopt/user-guide/latest/introduction.html) [![Docker Hub](https://img.shields.io/badge/docker-nvidia%2Fcuopt-blue?logo=docker)](https://hub.docker.com/r/nvidia/cuopt) [![Examples](https://img.shields.io/badge/examples-cuopt--examples-orange)](https://github.com/NVIDIA/cuopt-examples) diff --git a/VERSION b/VERSION index cdb610a24d..4e6864b4ca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -26.06.00 +26.08.00 diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 145850d4d8..2553f5be19 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=12.9.2,<13.0 - cuda-sanitizer-api - cuda-version=12.9 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -36,8 +36,8 @@ dependencies: - libcusparse-dev - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -54,7 +54,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -65,7 +65,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 293b49fbea..edda30f677 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=12.9.2,<13.0 - cuda-sanitizer-api - cuda-version=12.9 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -36,8 +36,8 @@ dependencies: - libcusparse-dev - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -54,7 +54,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -65,7 +65,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-132_arch-aarch64.yaml b/conda/environments/all_cuda-132_arch-aarch64.yaml index fa8844a1f9..78c869a342 100644 --- a/conda/environments/all_cuda-132_arch-aarch64.yaml +++ b/conda/environments/all_cuda-132_arch-aarch64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=13.0.1,<14.0 - cuda-sanitizer-api - cuda-version=13.2 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -36,8 +36,8 @@ dependencies: - libcusparse-dev - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -54,7 +54,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -65,7 +65,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-132_arch-x86_64.yaml b/conda/environments/all_cuda-132_arch-x86_64.yaml index a37d8718c0..ab946abd85 100644 --- a/conda/environments/all_cuda-132_arch-x86_64.yaml +++ b/conda/environments/all_cuda-132_arch-x86_64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=13.0.1,<14.0 - cuda-sanitizer-api - cuda-version=13.2 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -36,8 +36,8 @@ dependencies: - libcusparse-dev - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -54,7 +54,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -65,7 +65,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/dependencies.yaml b/dependencies.yaml index 4b647a7062..3414febbf1 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -283,7 +283,7 @@ dependencies: common: - output_types: [conda] packages: - - libcuopt-tests==26.6.*,>=0.0.0a0 + - libcuopt-tests==26.8.*,>=0.0.0a0 build_wheels: common: - output_types: [requirements, pyproject] @@ -393,12 +393,12 @@ dependencies: common: - output_types: [requirements, pyproject] packages: - - cuopt-cu13==26.6.*,>=0.0.0a0 + - cuopt-cu13==26.8.*,>=0.0.0a0 depends_on_libcuopt: common: - output_types: conda packages: - - &libcuopt_unsuffixed libcuopt==26.6.*,>=0.0.0a0 + - &libcuopt_unsuffixed libcuopt==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -411,18 +411,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - libcuopt-cu12==26.6.*,>=0.0.0a0 + - libcuopt-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - libcuopt-cu13==26.6.*,>=0.0.0a0 + - libcuopt-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*libcuopt_unsuffixed]} depends_on_cuopt: common: - output_types: conda packages: - - &cuopt_unsuffixed cuopt==26.6.*,>=0.0.0a0 + - &cuopt_unsuffixed cuopt==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -435,18 +435,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cuopt-cu12==26.6.*,>=0.0.0a0 + - cuopt-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - cuopt-cu13==26.6.*,>=0.0.0a0 + - cuopt-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*cuopt_unsuffixed]} depends_on_cuopt_server: common: - output_types: conda packages: - - &cuopt_server_unsuffixed cuopt-server==26.6.*,>=0.0.0a0 + - &cuopt_server_unsuffixed cuopt-server==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -459,18 +459,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cuopt-server-cu12==26.6.*,>=0.0.0a0 + - cuopt-server-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - cuopt-server-cu13==26.6.*,>=0.0.0a0 + - cuopt-server-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*cuopt_server_unsuffixed]} depends_on_cuopt_sh_client: common: - output_types: [conda, requirements, pyproject] packages: - - &cuopt_sh_client_unsuffixed cuopt-sh-client==26.6.*,>=0.0.0a0 + - &cuopt_sh_client_unsuffixed cuopt-sh-client==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -480,12 +480,12 @@ dependencies: common: - output_types: conda packages: - - libraft-headers==26.6.*,>=0.0.0a0 + - libraft-headers==26.8.*,>=0.0.0a0 depends_on_librmm: common: - output_types: conda packages: - - &librmm_unsuffixed librmm==26.6.*,>=0.0.0a0 + - &librmm_unsuffixed librmm==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -497,12 +497,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - librmm-cu12==26.6.*,>=0.0.0a0 + - librmm-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - librmm-cu13==26.6.*,>=0.0.0a0 + - librmm-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*librmm_unsuffixed]} depends_on_cupy: common: @@ -537,7 +537,7 @@ dependencies: common: - output_types: conda packages: - - &rmm_unsuffixed rmm==26.6.*,>=0.0.0a0 + - &rmm_unsuffixed rmm==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -549,12 +549,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - rmm-cu12==26.6.*,>=0.0.0a0 + - rmm-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - rmm-cu13==26.6.*,>=0.0.0a0 + - rmm-cu13==26.8.*,>=0.0.0a0 - matrix: packages: - *rmm_unsuffixed @@ -563,7 +563,7 @@ dependencies: common: - output_types: conda packages: - - &cudf_unsuffixed cudf==26.6.*,>=0.0.0a0 + - &cudf_unsuffixed cudf==26.8.*,>=0.0.0a0 - output_types: requirements packages: - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple @@ -574,12 +574,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cudf-cu12==26.6.*,>=0.0.0a0 + - cudf-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - cudf-cu13==26.6.*,>=0.0.0a0 + - cudf-cu13==26.8.*,>=0.0.0a0 - matrix: packages: - *cudf_unsuffixed @@ -588,7 +588,7 @@ dependencies: common: - output_types: conda packages: - - &pylibraft_unsuffixed pylibraft==26.6.*,>=0.0.0a0 + - &pylibraft_unsuffixed pylibraft==26.8.*,>=0.0.0a0 - output_types: requirements packages: - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple @@ -599,12 +599,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - pylibraft-cu12==26.6.*,>=0.0.0a0 + - pylibraft-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - pylibraft-cu13==26.6.*,>=0.0.0a0 + - pylibraft-cu13==26.8.*,>=0.0.0a0 - matrix: packages: - *pylibraft_unsuffixed diff --git a/docs/cuopt/source/versions1.json b/docs/cuopt/source/versions1.json index 507dfe57a4..dc92d1b660 100644 --- a/docs/cuopt/source/versions1.json +++ b/docs/cuopt/source/versions1.json @@ -1,10 +1,14 @@ [ { - "version": "26.06.00", - "url": "https://docs.nvidia.com/cuopt/user-guide/26.06.00/", + "version": "26.08.00", + "url": "https://docs.nvidia.com/cuopt/user-guide/26.08.00/", "name": "latest", "preferred": true }, + { + "version": "26.06.00", + "url": "https://docs.nvidia.com/cuopt/user-guide/26.06.00/" + }, { "version": "26.04.00", "url": "https://docs.nvidia.com/cuopt/user-guide/26.04.00/" diff --git a/gemini-extension.json b/gemini-extension.json index c5ef9883f8..48e8cb7ef7 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "nvidia-cuopt-skills", "description": "Agent skills for NVIDIA cuOpt optimization engine: routing, LP/MILP/QP, installation, and server.", - "version": "26.06.00", + "version": "26.08.00", "contextFileName": "AGENTS.md" } diff --git a/helmchart/cuopt-server/Chart.yaml b/helmchart/cuopt-server/Chart.yaml index 811ac067cb..5d638a1366 100644 --- a/helmchart/cuopt-server/Chart.yaml +++ b/helmchart/cuopt-server/Chart.yaml @@ -1,17 +1,17 @@ apiVersion: v2 -appVersion: 26.6.0 +appVersion: 26.8.0 description: A Helm chart for NVIDIA cuOpt Server with GPU support home: https://docs.nvidia.com/cuopt/user-guide/latest/resources.html keywords: -- nvidia -- cuopt -- optimization -- gpu + - nvidia + - cuopt + - optimization + - gpu maintainers: -- email: cuopt@nvidia.com - name: cuopt-maintainer + - email: cuopt@nvidia.com + name: cuopt-maintainer name: cuopt-server sources: -- https://docs.nvidia.com/cuopt/user-guide/latest/resources.html + - https://docs.nvidia.com/cuopt/user-guide/latest/resources.html type: application -version: 26.6.0 +version: 26.8.0 diff --git a/helmchart/cuopt-server/values.yaml b/helmchart/cuopt-server/values.yaml index 6adafea79e..1d744615fa 100644 --- a/helmchart/cuopt-server/values.yaml +++ b/helmchart/cuopt-server/values.yaml @@ -7,7 +7,7 @@ replicaCount: 1 image: repository: nvidia/cuopt pullPolicy: IfNotPresent - tag: "26.6.0-cuda12.9-py3.12" + tag: "26.8.0-cuda12.9-py3.12" imagePullSecrets: [] nameOverride: "" diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 4c1de77c81..d32557ca48 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -20,17 +20,17 @@ license = "Apache-2.0" requires-python = ">=3.11" dependencies = [ "cuda-python>=13.0.1,<14.0", - "cudf==26.6.*,>=0.0.0a0", + "cudf==26.8.*,>=0.0.0a0", "cupy-cuda13x>=13.6.0", - "libcuopt==26.6.*,>=0.0.0a0", + "libcuopt==26.8.*,>=0.0.0a0", "numba-cuda>=0.22.1", "numba>=0.60.0,<0.65.0", "numpy>=1.23.5,<3.0", "pandas>=2.0", - "pylibraft==26.6.*,>=0.0.0a0", + "pylibraft==26.8.*,>=0.0.0a0", "pyyaml>=6.0.0", "rapids-logger==0.2.*,>=0.0.0a0", - "rmm==26.6.*,>=0.0.0a0", + "rmm==26.8.*,>=0.0.0a0", "scipy>=1.14.1", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ @@ -103,9 +103,9 @@ requires = [ "cmake>=4.0", "cupy-cuda13x>=13.6.0", "cython>=3.0.3", - "libcuopt==26.6.*,>=0.0.0a0", + "libcuopt==26.8.*,>=0.0.0a0", "ninja", - "pylibraft==26.6.*,>=0.0.0a0", + "pylibraft==26.8.*,>=0.0.0a0", "rapids-logger==0.2.*,>=0.0.0a0", - "rmm==26.6.*,>=0.0.0a0", + "rmm==26.8.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. diff --git a/python/cuopt_self_hosted/pyproject.toml b/python/cuopt_self_hosted/pyproject.toml index 4de47de72e..74a665319b 100644 --- a/python/cuopt_self_hosted/pyproject.toml +++ b/python/cuopt_self_hosted/pyproject.toml @@ -40,7 +40,7 @@ test = [ "pytest<9.0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. mps = [ - "cuopt-cu13==26.6.*,>=0.0.0a0", + "cuopt-cu13==26.8.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.urls] diff --git a/python/cuopt_server/pyproject.toml b/python/cuopt_server/pyproject.toml index 4f9f141011..cd054a080e 100644 --- a/python/cuopt_server/pyproject.toml +++ b/python/cuopt_server/pyproject.toml @@ -21,7 +21,7 @@ license = "Apache-2.0" license-files = ["LICENSE"] requires-python = ">=3.11" dependencies = [ - "cuopt==26.6.*,>=0.0.0a0", + "cuopt==26.8.*,>=0.0.0a0", "cupy-cuda13x>=13.6.0", "fastapi", "jsonref==1.1.0", diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 0fe5dfb8df..b5190ec719 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ ] dependencies = [ "cuda-toolkit[cublas,cudart,curand,cusolver,cusparse,nvtx]==13.*", - "librmm==26.6.*,>=0.0.0a0", + "librmm==26.8.*,>=0.0.0a0", "nvidia-cudss-cu13", "nvidia-nvjitlink>=13.0,<14", "rapids-logger==0.2.*,>=0.0.0a0", @@ -76,7 +76,7 @@ dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=4.0", - "librmm==26.6.*,>=0.0.0a0", + "librmm==26.8.*,>=0.0.0a0", "ninja", "rapids-logger==0.2.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. diff --git a/skills/cuopt-developer/SKILL.md b/skills/cuopt-developer/SKILL.md index 34a2cf0502..2490a517ea 100644 --- a/skills/cuopt-developer/SKILL.md +++ b/skills/cuopt-developer/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-developer -version: "26.06.00" +version: "26.08.00" description: Modify, build, test, debug, and contribute to NVIDIA cuOpt (C++/CUDA, Python, server, CI). Use for solver internals, PRs, DCO, and code conventions. --- diff --git a/skills/cuopt-install/SKILL.md b/skills/cuopt-install/SKILL.md index d2aef94715..6220c341ce 100644 --- a/skills/cuopt-install/SKILL.md +++ b/skills/cuopt-install/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-install -version: "26.06.00" +version: "26.08.00" description: Install cuOpt for Python, C, or as a server (pip, conda, Docker) — system requirements, install commands, and verification. Use when the user wants to install or verify cuOpt for any user-facing interface. For building cuOpt from source or contributing to cuOpt, see cuopt-developer. --- diff --git a/skills/cuopt-numerical-optimization-api-c/SKILL.md b/skills/cuopt-numerical-optimization-api-c/SKILL.md index 46c7173787..eea2ab5c4d 100644 --- a/skills/cuopt-numerical-optimization-api-c/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-c/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-numerical-optimization-api-c -version: "26.06.00" +version: "26.08.00" description: LP, MILP, and QP (beta) with cuOpt — C API only. Use when the user is embedding LP, MILP, or QP in C/C++. --- diff --git a/skills/cuopt-numerical-optimization-api-cli/SKILL.md b/skills/cuopt-numerical-optimization-api-cli/SKILL.md index a9e4bcc3eb..dacd0a23a7 100644 --- a/skills/cuopt-numerical-optimization-api-cli/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-numerical-optimization-api-cli -version: "26.06.00" +version: "26.08.00" description: LP, MILP, and QP (beta) with cuOpt — CLI only (MPS files, cuopt_cli). Use when the user is solving LP, MILP, or QP from MPS via command line. --- diff --git a/skills/cuopt-numerical-optimization-api-python/SKILL.md b/skills/cuopt-numerical-optimization-api-python/SKILL.md index 35800ea2c6..44b3e00f30 100644 --- a/skills/cuopt-numerical-optimization-api-python/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-python/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-numerical-optimization-api-python -version: "26.06.00" +version: "26.08.00" description: Solve Linear Programming (LP), Mixed-Integer Linear Programming (MILP), and Quadratic Programming (QP, beta) with the Python API. Use when the user asks about optimization with linear or quadratic objectives, linear constraints, integer variables, scheduling, resource allocation, facility location, production planning, portfolio optimization, or least squares. --- diff --git a/skills/cuopt-routing-api-python/SKILL.md b/skills/cuopt-routing-api-python/SKILL.md index 5307776c96..a8bc2393a8 100644 --- a/skills/cuopt-routing-api-python/SKILL.md +++ b/skills/cuopt-routing-api-python/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-routing-api-python -version: "26.06.00" +version: "26.08.00" description: Vehicle routing (VRP, TSP, PDP) with cuOpt — Python API only. Use when the user is building or solving routing in Python. --- diff --git a/skills/cuopt-server-api-python/SKILL.md b/skills/cuopt-server-api-python/SKILL.md index ea3064efb5..fef3cbb056 100644 --- a/skills/cuopt-server-api-python/SKILL.md +++ b/skills/cuopt-server-api-python/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-server-api-python -version: "26.06.00" +version: "26.08.00" description: cuOpt REST server — start server, endpoints, Python/curl client examples. Use when the user is deploying or calling the REST API. --- diff --git a/skills/cuopt-server-common/SKILL.md b/skills/cuopt-server-common/SKILL.md index 62842bb5b4..3f86a96825 100644 --- a/skills/cuopt-server-common/SKILL.md +++ b/skills/cuopt-server-common/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-server-common -version: "26.06.00" +version: "26.08.00" description: cuOpt REST server — what it does and how requests flow. Domain concepts; no deploy or client code. --- diff --git a/skills/cuopt-user-rules/SKILL.md b/skills/cuopt-user-rules/SKILL.md index 02ebb3888c..47ce33a4e4 100644 --- a/skills/cuopt-user-rules/SKILL.md +++ b/skills/cuopt-user-rules/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-user-rules -version: "26.06.00" +version: "26.08.00" description: Base rules for end users calling NVIDIA cuOpt (routing/LP/MILP/QP/install/server). Not for cuOpt internals — use cuopt-developer for those. --- diff --git a/skills/numerical-optimization-formulation/SKILL.md b/skills/numerical-optimization-formulation/SKILL.md index d4f88f89bc..fa21ca6ae4 100644 --- a/skills/numerical-optimization-formulation/SKILL.md +++ b/skills/numerical-optimization-formulation/SKILL.md @@ -1,6 +1,6 @@ --- name: numerical-optimization-formulation -version: "26.06.00" +version: "26.08.00" description: Numerical optimization (LP, MILP, QP) — concepts, problem-text parsing, and formulation patterns. What LP, MILP, and QP are, required formulation questions, modeling elements, common patterns, and how to parse problem statements (parameters, constraints, decisions, objective). Domain concepts; no API or interface. --- diff --git a/skills/routing-formulation/SKILL.md b/skills/routing-formulation/SKILL.md index 293fbe940c..d755f9037d 100644 --- a/skills/routing-formulation/SKILL.md +++ b/skills/routing-formulation/SKILL.md @@ -1,6 +1,6 @@ --- name: routing-formulation -version: "26.06.00" +version: "26.08.00" description: Vehicle routing (VRP, TSP, PDP) — problem types and data requirements. Domain concepts; no API or interface. --- diff --git a/skills/skill-evolution/SKILL.md b/skills/skill-evolution/SKILL.md index 84c893bf02..8fb6b7bba0 100644 --- a/skills/skill-evolution/SKILL.md +++ b/skills/skill-evolution/SKILL.md @@ -1,6 +1,6 @@ --- name: skill-evolution -version: "26.06.00" +version: "26.08.00" description: After solving a non-trivial problem, detect generalizable learnings and propose skill updates so future interactions benefit automatically. Always active — applies to every interaction. --- From d8ffe5869f42a58b5fc16f7641974e8c6be2404d Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 20 May 2026 13:26:49 -0700 Subject: [PATCH 58/68] Fix bug on SOCPs with quadratic objectives. Fix misleading regularization for centering step --- cpp/src/barrier/barrier.cu | 32 ++++++++++++++++++-------------- cpp/src/pdlp/solve.cu | 5 ++++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index c8555cb83a..ec91f46ab6 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -2766,6 +2766,9 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t 1e-2) { @@ -4360,11 +4364,9 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t solve_lp( bool use_pdlp_solver_mode, bool is_batch_mode) { - if (op_problem.has_quadratic_objective()) { + // Pure QP (quadratic objective without quadratic constraints) takes the dedicated solve_qp + // path. When the problem also has quadratic constraints we MUST stay in solve_lp's main path so the + // SOC-aware translation runs. Since solve_qp currently silently drops quadratic constraints + if (op_problem.has_quadratic_objective() && !op_problem.has_quadratic_constraints()) { return solve_qp(op_problem, settings_const, problem_checking); } From d64a82b4cd4f3b1eb1edb0b45042df408166bb27 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 20 May 2026 13:37:33 -0700 Subject: [PATCH 59/68] add support of SOCP also in qp path --- cpp/src/pdlp/solve.cu | 4 ++++ cpp/src/pdlp/translate.hpp | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 3f5259744e..b341225e5d 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1754,6 +1754,10 @@ optimization_problem_solution_t solve_qp(optimization_problem_t(op_problem.get_quadratic_constraints().size())); + } if (settings.user_problem_file != "") { CUOPT_LOG_INFO("Writing user problem to file: %s", settings.user_problem_file.c_str()); op_problem.write_to_mps(settings.user_problem_file); diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 3d4fbcb520..2bfaa127ed 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -222,7 +222,6 @@ static dual_simplex::user_problem_t cuopt_optimization_problem_to_user csr_A.row_start.resize(1); csr_A.row_start[0] = 0; } - csr_A.to_compressed_col(user_problem.A); user_problem.rhs.resize(m); user_problem.row_sense.resize(m); @@ -302,6 +301,13 @@ static dual_simplex::user_problem_t cuopt_optimization_problem_to_user user_problem.Q_indices = model.get_quadratic_objective_indices(); user_problem.Q_values = model.get_quadratic_objective_values(); + if (model.has_quadratic_constraints()) { + detail::apply_soc_qcmatrix_conversion_for_simplex( + static_cast(n), model.get_quadratic_constraints(), csr_A, user_problem); + } + + csr_A.to_compressed_col(user_problem.A); + return user_problem; } From 0b4d8e32b0ed9f72b096be39b58a5c3e09a45bb5 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 20 May 2026 13:47:32 -0700 Subject: [PATCH 60/68] --amend --- cpp/src/pdlp/solve.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index b341225e5d..355a1c6e7f 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1800,7 +1800,7 @@ optimization_problem_solution_t solve_lp( // Pure QP (quadratic objective without quadratic constraints) takes the dedicated solve_qp // path. When the problem also has quadratic constraints we MUST stay in solve_lp's main path so the // SOC-aware translation runs. Since solve_qp currently silently drops quadratic constraints - if (op_problem.has_quadratic_objective() && !op_problem.has_quadratic_constraints()) { + if (op_problem.has_quadratic_objective()) { return solve_qp(op_problem, settings_const, problem_checking); } From 8babc7ca373d706a4eb1cafd1cb41abb4771ea11 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Wed, 20 May 2026 13:58:30 -0700 Subject: [PATCH 61/68] remove out-dated comment --- cpp/src/pdlp/solve.cu | 3 --- 1 file changed, 3 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 355a1c6e7f..a1a6e18c0d 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1797,9 +1797,6 @@ optimization_problem_solution_t solve_lp( bool use_pdlp_solver_mode, bool is_batch_mode) { - // Pure QP (quadratic objective without quadratic constraints) takes the dedicated solve_qp - // path. When the problem also has quadratic constraints we MUST stay in solve_lp's main path so the - // SOC-aware translation runs. Since solve_qp currently silently drops quadratic constraints if (op_problem.has_quadratic_objective()) { return solve_qp(op_problem, settings_const, problem_checking); } From 3eeccb84d227c9f24b5510716e43c595a6bb45ba Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 21 May 2026 04:17:51 -0700 Subject: [PATCH 62/68] disable barrier_solve but keep native free linear variables --- .../cuopt/linear_programming/constants.h | 2 + .../pdlp/solver_settings.hpp | 10 ++++ cpp/src/dual_simplex/presolve.cpp | 47 +++++++++------ .../dual_simplex/simplex_solver_settings.hpp | 4 +- cpp/src/dual_simplex/solve.cpp | 12 +++- cpp/src/math_optimization/solver_settings.cu | 2 + cpp/src/pdlp/solve.cu | 2 + .../dual_simplex/unit_tests/solve_barrier.cu | 59 +++++++++++++++++++ 8 files changed, 117 insertions(+), 21 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 503ae686b6..dc60cff036 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -55,6 +55,8 @@ #define CUOPT_ELIMINATE_DENSE_COLUMNS "eliminate_dense_columns" #define CUOPT_CUDSS_DETERMINISTIC "cudss_deterministic" #define CUOPT_PRESOLVE "presolve" +#define CUOPT_BARRIER_PRESOLVE "barrier_presolve" +#define CUOPT_BARRIER_NATIVE_FREE_LINEAR "barrier_native_free_linear" #define CUOPT_DUAL_POSTSOLVE "dual_postsolve" #define CUOPT_MIP_DETERMINISM_MODE "mip_determinism_mode" #define CUOPT_MIP_ABSOLUTE_TOLERANCE "mip_absolute_tolerance" diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index 3597b85016..fcd39f54ea 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -286,6 +286,16 @@ class pdlp_solver_settings_t { bool eliminate_dense_columns{true}; pdlp_precision_t pdlp_precision{pdlp_precision_t::DefaultPrecision}; bool barrier_iterative_refinement{true}; + /** + * Internal dual_simplex presolve (bound shifts, free-var bounding/split, folding) before barrier. + * Default true for LP/QP barrier. Ignored for SOCP (second-order cones): barrier presolve is off. + */ + bool barrier_presolve{true}; + /** + * Tag linear FR variables as native free for augmented SOCP/QP barrier (no x*z complementarity). + * Independent of @ref barrier_presolve. Always enabled for SOCP barrier solves. + */ + bool barrier_native_free_linear{true}; f_t barrier_step_scale{0.9}; bool save_best_primal_so_far{false}; /** diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index e614504f67..0950fc8c94 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -20,6 +20,7 @@ namespace cuopt::linear_programming::dual_simplex { template +/** Number of leading linear columns; SOCP cone variables occupy [linear_cols, num_cols). */ static i_t linear_var_count(const lp_problem_t& problem) { return problem.second_order_cone_dims.empty() ? problem.num_cols : problem.cone_var_start; @@ -922,8 +923,10 @@ void convert_user_problem(const user_problem_t& user_problem, problem.Q.x = user_problem.Q_values; } - // Add artifical variables - if (!settings.barrier_presolve) { + // Add artificial variables for LP equality rows when not using barrier presolve. + // SOCP problems must keep [linear | cone] column order; slacks for inequalities are + // inserted before the cone block in convert_less_than_to_equal — do not append here. + if (!settings.barrier_presolve && problem.second_order_cone_dims.empty()) { add_artifical_variables(problem, user_problem.range_rows, equality_rows, new_slacks); } } @@ -935,10 +938,11 @@ i_t presolve(const lp_problem_t& original, presolve_info_t& presolve_info) { problem = original; + const i_t linear_cols = linear_var_count(problem); std::vector row_sense(problem.num_rows, '='); - // Check for free variables + // Check for free variables (linear block only; cone columns are handled by the barrier SOC layout) i_t free_variables = 0; - for (i_t j = 0; j < linear_var_count(problem); j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } } @@ -949,7 +953,7 @@ i_t presolve(const lp_problem_t& original, std::vector row_marked(problem.num_rows, 0); current_free_variables.reserve(problem.num_cols); constraints_to_check.reserve(problem.num_rows); - for (i_t j = 0; j < linear_var_count(problem); j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { current_free_variables.push_back(j); const i_t col_start = problem.A.col_start[j]; @@ -983,6 +987,7 @@ i_t presolve(const lp_problem_t& original, f_t last_free_coeff_i = 0.0; for (i_t p = row_start; p < row_end; p++) { const i_t j = Arow.j[p]; + if (j >= linear_cols) { continue; } const f_t aij = Arow.x[p]; const f_t lower_j = problem.lower[j]; const f_t upper_j = problem.upper[j]; @@ -1094,9 +1099,9 @@ i_t presolve(const lp_problem_t& original, } // The original problem may have a variable without a lower bound // but a finite upper bound - // -inf < x_j <= u_j + // -inf < x_j <= u_j (linear variables only) i_t no_lower_bound = 0; - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] < inf) { no_lower_bound++; } } @@ -1107,7 +1112,7 @@ i_t presolve(const lp_problem_t& original, // Handle -inf < x_j <= u_j by substituting x'_j = -x_j, giving -u_j <= x'_j < inf if (settings.barrier_presolve && no_lower_bound > 0) { presolve_info.negated_variables.reserve(no_lower_bound); - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] < inf) { presolve_info.negated_variables.push_back(j); @@ -1143,9 +1148,9 @@ i_t presolve(const lp_problem_t& original, } // The original problem may have nonzero lower bounds - // 0 != l_j <= x_j <= u_j + // 0 != l_j <= x_j <= u_j (linear variables only) i_t nonzero_lower_bounds = 0; - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] != 0.0 && problem.lower[j] > -inf) { nonzero_lower_bounds++; } } if (settings.barrier_presolve && nonzero_lower_bounds > 0) { @@ -1168,7 +1173,7 @@ i_t presolve(const lp_problem_t& original, // so we get the constant term c_j * l_j std::vector lower_bounds_removed(problem.num_cols, false); - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] != 0.0 && problem.lower[j] > -inf) { lower_bounds_removed[j] = true; presolve_info.removed_lower_bounds[j] = problem.lower[j]; @@ -1177,7 +1182,7 @@ i_t presolve(const lp_problem_t& original, auto old_objective = problem.objective; if (problem.Q.n > 0) { - for (i_t row = 0; row < problem.num_cols; row++) { + for (i_t row = 0; row < linear_cols; row++) { i_t row_start = problem.Q.row_start[row]; i_t row_end = problem.Q.row_start[row + 1]; for (i_t p = row_start; p < row_end; p++) { @@ -1198,7 +1203,7 @@ i_t presolve(const lp_problem_t& original, } std::vector kahan_compensation(problem.num_rows, 0.0); - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (lower_bounds_removed[j]) { i_t col_start = problem.A.col_start[j]; i_t col_end = problem.A.col_start[j + 1]; @@ -1236,7 +1241,7 @@ i_t presolve(const lp_problem_t& original, // Check for empty cols i_t num_empty_cols = 0; { - for (i_t j = 0; j < linear_var_count(problem); ++j) { + for (i_t j = 0; j < linear_cols; ++j) { if ((problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { num_empty_cols++; } } } @@ -1247,7 +1252,7 @@ i_t presolve(const lp_problem_t& original, // Check for free variables (exclude cone variables — they are naturally unbounded) free_variables = 0; - for (i_t j = 0; j < linear_var_count(problem); j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } } problem.Q.check_matrix("Before free variable expansion"); @@ -1257,19 +1262,24 @@ i_t presolve(const lp_problem_t& original, // static regularizer on the diagonal instead of z/x complementarity terms. const bool keep_native_free_variables = problem.Q.n > 0 || !problem.second_order_cone_dims.empty(); - if (settings.barrier_presolve && free_variables > 0 && keep_native_free_variables) { + const bool register_native_free_linear = + (settings.barrier_native_free_linear || settings.barrier_presolve) && free_variables > 0 && + keep_native_free_variables; + if (register_native_free_linear) { presolve_info.free_variable_pairs.clear(); presolve_info.native_free_linear_indices.clear(); // Only linear decision variables can be "native" free variables; cone/stack columns // are unbounded by construction and must not be counted here. - for (i_t j = 0; j < linear_var_count(problem); j++) { + i_t native_free_count = 0; + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { presolve_info.native_free_linear_indices.push_back(j); + native_free_count++; } } settings.log.printf( "Keeping %d native free linear variables for augmented-system barrier (QP/SOCP)\n", - free_variables); + native_free_count); } else if (settings.barrier_presolve && free_variables > 0) { // We have a variable x_j: with -inf < x_j < inf // we create new variables v and w with 0 <= v, w and x_j = v - w @@ -1284,7 +1294,6 @@ i_t presolve(const lp_problem_t& original, // sum_{k != j} c_k x_k + c_j v - c_j w const i_t old_num_cols = problem.num_cols; - const i_t linear_cols = linear_var_count(problem); const i_t new_cone_start = problem.second_order_cone_dims.empty() ? 0 : linear_cols + free_variables; const i_t num_cols = old_num_cols + free_variables; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 454d977a21..ec5134358f 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -80,6 +80,7 @@ struct simplex_solver_settings_t { eliminate_singletons(true), print_presolve_stats(true), barrier_presolve(false), + barrier_native_free_linear(true), cudss_deterministic(false), deterministic(false), barrier(false), @@ -162,7 +163,8 @@ struct simplex_solver_settings_t { use_left_looking_lu; // true to use left looking LU factorization, false to use right looking bool eliminate_singletons; // true to eliminate singletons from the basis bool print_presolve_stats; // true to print presolve stats - bool barrier_presolve; // true to use barrier presolve + bool barrier_presolve; // bound shifts, free-var bounding/split, folding, etc. + bool barrier_native_free_linear; // register native free linear cols for augmented SOCP/QP barrier bool cudss_deterministic; // true to use cuDSS deterministic mode, false for non-deterministic bool barrier; // true to use barrier method, false to use dual simplex method bool deterministic; // true to use B&B deterministic mode, false to use non-deterministic mode diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 3abad00930..9808816e31 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -382,6 +382,16 @@ lp_status_t solve_linear_program_with_advanced_basis( return lp_status; } +/** SOCP barrier: skip bound-shift presolve; keep native free linear variables for augmented KKT. */ +template +void apply_barrier_socp_presolve_defaults(const user_problem_t& user_problem, + simplex_solver_settings_t& settings) +{ + if (user_problem.second_order_cone_dims.empty()) { return; } + settings.barrier_presolve = false; + settings.barrier_native_free_linear = true; +} + template lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, @@ -394,7 +404,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us // Convert the user problem to a linear program with only equality constraints std::vector new_slacks; simplex_solver_settings_t barrier_settings = settings; - barrier_settings.barrier_presolve = true; + apply_barrier_socp_presolve_defaults(user_problem, barrier_settings); dualize_info_t dualize_info; convert_user_problem(user_problem, barrier_settings, original_lp, new_slacks, dualize_info); if (!validate_barrier_cone_layout(original_lp, settings)) { diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index f57de9b8f1..ecf8a97ec7 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -177,6 +177,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_CUDSS_DETERMINISTIC, &pdlp_settings.cudss_deterministic, false}, {CUOPT_DUAL_POSTSOLVE, &pdlp_settings.dual_postsolve, true}, {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, + {CUOPT_BARRIER_PRESOLVE, &pdlp_settings.barrier_presolve, true}, + {CUOPT_BARRIER_NATIVE_FREE_LINEAR, &pdlp_settings.barrier_native_free_linear, true}, }; // String parameters string_parameters = { diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index a1a6e18c0d..621fe35dce 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -508,6 +508,8 @@ run_barrier(dual_simplex::user_problem_t& user_problem, barrier_settings.crossover = settings.crossover; barrier_settings.eliminate_dense_columns = settings.eliminate_dense_columns; barrier_settings.barrier_iterative_refinement = settings.barrier_iterative_refinement; + barrier_settings.barrier_presolve = settings.barrier_presolve; + barrier_settings.barrier_native_free_linear = settings.barrier_native_free_linear; barrier_settings.barrier_step_scale = settings.barrier_step_scale; barrier_settings.cudss_deterministic = settings.cudss_deterministic; barrier_settings.barrier_relative_feasibility_tol = diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index 1d26ae523a..a064b2b756 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -366,6 +366,65 @@ TEST(barrier, presolve_keeps_native_free_variables_before_cones) EXPECT_EQ(presolved_lp.upper[1], inf); } +TEST(barrier, native_free_linear_without_barrier_presolve) +{ + // barrier_presolve off skips bound shifts; barrier_native_free_linear still tags FR cols. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 5; + constexpr int nz = 5; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 1, 2, 3, 4, 5}; + for (int j = 0; j < n; ++j) { + user_problem.A.i[j] = 0; + user_problem.A.x[j] = 1.0; + } + + user_problem.rhs = {1.0}; + user_problem.row_sense = {'E'}; + user_problem.lower = {-inf, -inf, 0.0, 0.0, 0.0}; + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 2; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.barrier_presolve = false; + settings.barrier_native_free_linear = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + presolve_info_t presolve_info; + lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); + ASSERT_EQ(presolve(original_lp, settings, presolved_lp, presolve_info), 0); + + EXPECT_TRUE(presolve_info.removed_lower_bounds.empty()); + EXPECT_TRUE(presolve_info.negated_variables.empty()); + ASSERT_EQ(presolve_info.native_free_linear_indices.size(), 2); + EXPECT_EQ(presolve_info.native_free_linear_indices[0], 0); + EXPECT_EQ(presolve_info.native_free_linear_indices[1], 1); +} + TEST(barrier, uncrush_solution_removes_non_tail_free_variable_partner) { using namespace cuopt::linear_programming::dual_simplex; From 7773bda9c9fd30e3cbc96b555b4b53405a7b7206 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 21 May 2026 07:57:12 -0700 Subject: [PATCH 63/68] add nvtx ranges for profiling --- cpp/src/barrier/barrier.cu | 571 ++++++++++++++++++++----------------- solve_augmented_ldl.py | 151 ++++++++++ 2 files changed, 466 insertions(+), 256 deletions(-) create mode 100644 solve_augmented_ldl.py diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index ec91f46ab6..fd6f9dbc6e 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -256,209 +256,236 @@ class iteration_data_t { { raft::common::nvtx::range fun_scope("Barrier: LP Data Creation"); - // Set up native free linear variable tracking (linear columns only, j < cone_start). - if (!native_free_linear_indices.empty()) { - n_native_free_linear = native_free_linear_indices.size(); - std::vector is_native_free_linear_host(lp.num_cols, 0); - for (i_t j : native_free_linear_indices) { - is_native_free_linear_host[j] = 1; + { + raft::common::nvtx::range scope("Barrier: LP Data: native free linear"); + // Set up native free linear variable tracking (linear columns only, j < cone_start). + if (!native_free_linear_indices.empty()) { + n_native_free_linear = native_free_linear_indices.size(); + std::vector is_native_free_linear_host(lp.num_cols, 0); + for (i_t j : native_free_linear_indices) { + is_native_free_linear_host[j] = 1; + } + d_is_native_free_linear_.resize(lp.num_cols, stream_view_); + raft::copy( + d_is_native_free_linear_.data(), is_native_free_linear_host.data(), lp.num_cols, stream_view_); + settings.log.printf("Native free linear (QP): %d\n", n_native_free_linear); } - d_is_native_free_linear_.resize(lp.num_cols, stream_view_); - raft::copy(d_is_native_free_linear_.data(), is_native_free_linear_host.data(), lp.num_cols, stream_view_); - settings.log.printf("Native free linear (QP): %d\n", n_native_free_linear); } bool has_Q = Q.x.size() > 0; indefinite_Q = false; - if (has_Q) { - Qdiag.resize(lp.num_cols, 0.0); - - for (i_t j = 0; j < Q.n; j++) { - const i_t col_start = Q.col_start[j]; - const i_t col_end = Q.col_start[j + 1]; - for (i_t p = col_start; p < col_end; p++) { - const i_t i = Q.i[p]; - if (j == i) { - Qdiag[j] = Q.x[p]; - break; + { + raft::common::nvtx::range scope("Barrier: LP Data: Q setup"); + if (has_Q) { + Qdiag.resize(lp.num_cols, 0.0); + + for (i_t j = 0; j < Q.n; j++) { + const i_t col_start = Q.col_start[j]; + const i_t col_end = Q.col_start[j + 1]; + for (i_t p = col_start; p < col_end; p++) { + const i_t i = Q.i[p]; + if (j == i) { + Qdiag[j] = Q.x[p]; + break; + } } } - } - Q_diagonal = Q.is_diagonal(); + Q_diagonal = Q.is_diagonal(); - if (Q_diagonal) { - // Check to ensure that Q is positive semi-definite - for (i_t j = 0; j < lp.num_cols; j++) { - if (Qdiag[j] < 0.0) { - settings_.log.printf( - "Q is not positive semidefinite: Q(%d, %d) = %e\n", j, j, Qdiag[j]); - indefinite_Q = true; - return; + if (Q_diagonal) { + // Check to ensure that Q is positive semi-definite + for (i_t j = 0; j < lp.num_cols; j++) { + if (Qdiag[j] < 0.0) { + settings_.log.printf( + "Q is not positive semidefinite: Q(%d, %d) = %e\n", j, j, Qdiag[j]); + indefinite_Q = true; + return; + } } + } else if (settings.check_Q) { + // TODO: Check to ensure that Q is positive semi-definite + // This requires us to perform a Cholesky factorization. } - } else if (settings.check_Q) { - // TODO: Check to ensure that Q is positive semi-definite - // This requires us to perform a Cholesky factorization. - } - d_Q_diag_.resize(lp.num_cols, stream_view_); - raft::copy(d_Q_diag_.data(), Qdiag.data(), Qdiag.size(), stream_view_); + d_Q_diag_.resize(lp.num_cols, stream_view_); + raft::copy(d_Q_diag_.data(), Qdiag.data(), Qdiag.size(), stream_view_); + } } - if (!lp.second_order_cone_dims.empty()) { - cone_var_start_ = lp.cone_var_start; - i_t total_cone_dim = - std::accumulate(lp.second_order_cone_dims.begin(), lp.second_order_cone_dims.end(), i_t(0)); - cuopt_assert(cone_var_start_ >= 0, "cone_var_start must be nonnegative"); - cuopt_assert(cone_var_start_ + total_cone_dim <= lp.num_cols, - "cone variables exceed problem dimension"); - cuopt_assert(cone_var_start_ + total_cone_dim == lp.num_cols, - "barrier expects [linear | cone] layout"); - cones_.emplace( - std::span(lp.second_order_cone_dims.data(), lp.second_order_cone_dims.size()), - raft::device_span{}, - raft::device_span{}, - stream_view_); - cuopt_assert(cone_count() > 0, "second-order cone topology must contain at least one cone"); - cuopt_assert(cone_entry_count() == total_cone_dim, "second-order cone entry count mismatch"); - } - const i_t linear_xz_rhs_size = linear_xz_size(lp.num_cols); - d_complementarity_xz_rhs_.resize(linear_xz_rhs_size, stream_view_); - - // Allocating GPU flag data for Form ADAT - RAFT_CUDA_TRY(cub::DeviceSelect::Flagged( - nullptr, - flag_buffer_size, - d_inv_diag_prime.data(), // Not the actual input but just to allcoate the memory - thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), - d_inv_diag_prime.data(), - d_num_flag.data(), - inv_diag.size(), - stream_view_)); - - d_flag_buffer.resize(flag_buffer_size, stream_view_); - - // Create the upper bounds vector - n_upper_bounds = 0; - for (i_t j = 0; j < lp.num_cols; j++) { - if (lp.upper[j] < inf) { upper_bounds[n_upper_bounds++] = j; } - } - if (n_upper_bounds > 0) { - settings.log.printf("Upper bounds : %d\n", n_upper_bounds); + { + raft::common::nvtx::range scope("Barrier: LP Data: SOC setup"); + if (!lp.second_order_cone_dims.empty()) { + cone_var_start_ = lp.cone_var_start; + i_t total_cone_dim = + std::accumulate(lp.second_order_cone_dims.begin(), lp.second_order_cone_dims.end(), i_t(0)); + cuopt_assert(cone_var_start_ >= 0, "cone_var_start must be nonnegative"); + cuopt_assert(cone_var_start_ + total_cone_dim <= lp.num_cols, + "cone variables exceed problem dimension"); + cuopt_assert(cone_var_start_ + total_cone_dim == lp.num_cols, + "barrier expects [linear | cone] layout"); + cones_.emplace( + std::span(lp.second_order_cone_dims.data(), lp.second_order_cone_dims.size()), + raft::device_span{}, + raft::device_span{}, + stream_view_); + cuopt_assert(cone_count() > 0, "second-order cone topology must contain at least one cone"); + cuopt_assert(cone_entry_count() == total_cone_dim, "second-order cone entry count mismatch"); + } } - // Decide if we are going to use the augmented system or not - n_dense_columns = 0; - i_t n_dense_rows = 0; - i_t max_row_nz = 0; - f_t estimated_nz_AAT = 0.0; - std::vector dense_columns_unordered; + { + raft::common::nvtx::range scope("Barrier: LP Data: complementarity buffers"); + const i_t linear_xz_rhs_size = linear_xz_size(lp.num_cols); + d_complementarity_xz_rhs_.resize(linear_xz_rhs_size, stream_view_); - const bool has_soc = has_cones(); - if (has_Q || has_soc) { - // QP and SOCP always use the augmented KKT; skip dense-column / ADAT heuristics. - use_augmented = true; - n_dense_columns = 0; - } else { - f_t start_column_density = tic(); + // Allocating GPU flag data for Form ADAT + RAFT_CUDA_TRY(cub::DeviceSelect::Flagged( + nullptr, + flag_buffer_size, + d_inv_diag_prime.data(), // Not the actual input but just to allcoate the memory + thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), + d_inv_diag_prime.data(), + d_num_flag.data(), + inv_diag.size(), + stream_view_)); - // Do not look for dense columns if Q is not diagonal - if (!has_Q || Q_diagonal) { - find_dense_columns( - lp.A, settings, dense_columns_unordered, n_dense_rows, max_row_nz, estimated_nz_AAT); - } - if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } -#ifdef PRINT_INFO - for (i_t j : dense_columns_unordered) { - settings.log.printf("Dense column %6d\n", j); - } -#endif - float64_t column_density_time = toc(start_column_density); - if (!settings.eliminate_dense_columns) { dense_columns_unordered.clear(); } - n_dense_columns = static_cast(dense_columns_unordered.size()); - if (n_dense_columns > 0) { - settings.log.printf("Dense columns : %d\n", n_dense_columns); + d_flag_buffer.resize(flag_buffer_size, stream_view_); + } + + { + raft::common::nvtx::range scope("Barrier: LP Data: upper bounds"); + // Create the upper bounds vector + n_upper_bounds = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (lp.upper[j] < inf) { upper_bounds[n_upper_bounds++] = j; } } - if (n_dense_rows > 0) { - settings.log.printf("Dense rows : %d\n", n_dense_rows); + if (n_upper_bounds > 0) { + settings.log.printf("Upper bounds : %d\n", n_upper_bounds); } - settings.log.printf("Density estimator time : %.2fs\n", column_density_time); - if ((settings.augmented != 0) && - (n_dense_columns > 50 || n_dense_rows > 10 || - lp.A.m == 0 /* handle case with no constraints */ || - (max_row_nz > 5000 && estimated_nz_AAT > 1e10) || settings.augmented == 1)) { + } + + std::vector dense_columns_unordered; + { + raft::common::nvtx::range scope("Barrier: LP Data: dense columns and augmented"); + // Decide if we are going to use the augmented system or not + n_dense_columns = 0; + i_t n_dense_rows = 0; + i_t max_row_nz = 0; + f_t estimated_nz_AAT = 0.0; + + const bool has_soc = has_cones(); + if (has_Q || has_soc) { + // QP and SOCP always use the augmented KKT; skip dense-column / ADAT heuristics. use_augmented = true; n_dense_columns = 0; + } else { + f_t start_column_density = tic(); + + // Do not look for dense columns if Q is not diagonal + if (!has_Q || Q_diagonal) { + find_dense_columns( + lp.A, settings, dense_columns_unordered, n_dense_rows, max_row_nz, estimated_nz_AAT); + } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } +#ifdef PRINT_INFO + for (i_t j : dense_columns_unordered) { + settings.log.printf("Dense column %6d\n", j); + } +#endif + float64_t column_density_time = toc(start_column_density); + if (!settings.eliminate_dense_columns) { dense_columns_unordered.clear(); } + n_dense_columns = static_cast(dense_columns_unordered.size()); + if (n_dense_columns > 0) { + settings.log.printf("Dense columns : %d\n", n_dense_columns); + } + if (n_dense_rows > 0) { + settings.log.printf("Dense rows : %d\n", n_dense_rows); + } + settings.log.printf("Density estimator time : %.2fs\n", column_density_time); + if ((settings.augmented != 0) && + (n_dense_columns > 50 || n_dense_rows > 10 || + lp.A.m == 0 /* handle case with no constraints */ || + (max_row_nz > 5000 && estimated_nz_AAT > 1e10) || settings.augmented == 1)) { + use_augmented = true; + n_dense_columns = 0; + } } - } - if (use_augmented) { - settings.log.printf("Linear system : augmented\n"); - } else { - settings.log.printf("Linear system : ADAT\n"); + if (use_augmented) { + settings.log.printf("Linear system : augmented\n"); + } else { + settings.log.printf("Linear system : ADAT\n"); + } } - // D = I + EET - diag.set_scalar(1.0); - if (n_upper_bounds > 0) { - for (i_t k = 0; k < n_upper_bounds; k++) { - i_t j = upper_bounds[k]; - diag[j] = 2.0; + { + raft::common::nvtx::range scope("Barrier: LP Data: diag and inv_diag"); + // D = I + EET + diag.set_scalar(1.0); + if (n_upper_bounds > 0) { + for (i_t k = 0; k < n_upper_bounds; k++) { + i_t j = upper_bounds[k]; + diag[j] = 2.0; + } } - } - // D = I + EET + Q (if Q is diagonal) - if (has_Q && !use_augmented) { - // this means that Q is diagonal - for (i_t j = 0; j < Q.n; j++) { - diag[j] += Qdiag[j]; + // D = I + EET + Q (if Q is diagonal) + if (has_Q && !use_augmented) { + // this means that Q is diagonal + for (i_t j = 0; j < Q.n; j++) { + diag[j] += Qdiag[j]; + } } - } - inv_diag.set_scalar(1.0); - if (use_augmented) { diag.multiply_scalar(-1.0); } - if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { diag.inverse(inv_diag); } - // TMP diag and inv_diag should directly created and filled on the GPU - raft::copy(d_inv_diag.data(), inv_diag.data(), inv_diag.size(), stream_view_); - inv_sqrt_diag.set_scalar(1.0); - if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { inv_diag.sqrt(inv_sqrt_diag); } + inv_diag.set_scalar(1.0); + if (use_augmented) { diag.multiply_scalar(-1.0); } + if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { diag.inverse(inv_diag); } + // TMP diag and inv_diag should directly created and filled on the GPU + raft::copy(d_inv_diag.data(), inv_diag.data(), inv_diag.size(), stream_view_); + inv_sqrt_diag.set_scalar(1.0); + if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { inv_diag.sqrt(inv_sqrt_diag); } + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - // Copy A into AD - AD = lp.A; - if (!use_augmented && n_dense_columns > 0) { - cols_to_remove.resize(lp.num_cols, 0); - for (i_t k : dense_columns_unordered) { - cols_to_remove[k] = 1; - } - d_cols_to_remove.resize(cols_to_remove.size(), stream_view_); - raft::copy( - d_cols_to_remove.data(), cols_to_remove.data(), cols_to_remove.size(), stream_view_); - dense_columns.clear(); - dense_columns.reserve(n_dense_columns); - for (i_t j = 0; j < lp.num_cols; j++) { - if (cols_to_remove[j]) { dense_columns.push_back(j); } - } - AD.remove_columns(cols_to_remove); + { + raft::common::nvtx::range scope("Barrier: LP Data: AD matrix setup"); + // Copy A into AD + AD = lp.A; + if (!use_augmented && n_dense_columns > 0) { + cols_to_remove.resize(lp.num_cols, 0); + for (i_t k : dense_columns_unordered) { + cols_to_remove[k] = 1; + } + d_cols_to_remove.resize(cols_to_remove.size(), stream_view_); + raft::copy( + d_cols_to_remove.data(), cols_to_remove.data(), cols_to_remove.size(), stream_view_); + dense_columns.clear(); + dense_columns.reserve(n_dense_columns); + for (i_t j = 0; j < lp.num_cols; j++) { + if (cols_to_remove[j]) { dense_columns.push_back(j); } + } + AD.remove_columns(cols_to_remove); - sparse_mark.resize(lp.num_cols, 1); - for (i_t k : dense_columns) { - sparse_mark[k] = 0; - } + sparse_mark.resize(lp.num_cols, 1); + for (i_t k : dense_columns) { + sparse_mark[k] = 0; + } - A_dense.resize(AD.m, n_dense_columns); - i_t k = 0; - for (i_t j : dense_columns) { - A_dense.from_sparse(lp.A, j, k++); + A_dense.resize(AD.m, n_dense_columns); + i_t k = 0; + for (i_t j : dense_columns) { + A_dense.from_sparse(lp.A, j, k++); + } } - } - AD.transpose(AT); + AD.transpose(AT); + } // device_AD / device_A / ADAT path is only used when forming ADAT (!use_augmented). if (!use_augmented) { + raft::common::nvtx::range scope("Barrier: LP Data: device AD path"); device_AD.copy(AD, handle_ptr->get_stream()); d_original_A_values.resize(device_AD.x.size(), handle_ptr->get_stream()); raft::copy(d_original_A_values.data(), @@ -475,22 +502,34 @@ class iteration_data_t { } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - i_t factorization_size = use_augmented ? lp.num_rows + lp.num_cols : lp.num_rows; - chol = - std::make_unique>(handle_ptr, settings, factorization_size); - chol->set_positive_definite(false); + { + raft::common::nvtx::range scope("Barrier: LP Data: Cholesky init"); + i_t factorization_size = use_augmented ? lp.num_rows + lp.num_cols : lp.num_rows; + chol = std::make_unique>( + handle_ptr, settings, factorization_size); + chol->set_positive_definite(false); + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - // Perform symbolic analysis - symbolic_status = 0; - if (use_augmented) { - // Build the sparsity pattern of the augmented system - form_augmented(true); - if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - symbolic_status = chol->analyze(device_augmented); - } else { - form_adat(true); - if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - symbolic_status = chol->analyze(device_ADAT); + { + raft::common::nvtx::range scope("Barrier: LP Data: symbolic analysis"); + // Perform symbolic analysis + symbolic_status = 0; + if (use_augmented) { + { + raft::common::nvtx::range form_scope("Barrier: LP Data: form augmented"); + // Build the sparsity pattern of the augmented system + form_augmented(true); + } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } + symbolic_status = chol->analyze(device_augmented); + } else { + { + raft::common::nvtx::range form_scope("Barrier: LP Data: form ADAT"); + form_adat(true); + } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } + symbolic_status = chol->analyze(device_ADAT); + } } } @@ -558,8 +597,9 @@ class iteration_data_t { std::vector cone_block_offsets_host; if (first_call) { - // Initialize the cone block offsets if (has_soc) { + raft::common::nvtx::range scope("Barrier: augmented: SOC offsets"); + // Initialize the cone block offsets const i_t n_cones = cone_count(); cone_offsets_host.resize(n_cones + 1); cone_block_offsets_host.resize(n_cones + 1); @@ -581,7 +621,9 @@ class iteration_data_t { i_t q = 0; i_t off_diag_Qnz = 0; - for (i_t i = 0; i < n; i++) { + { + raft::common::nvtx::range scope("Barrier: augmented: host CSR build"); + for (i_t i = 0; i < n; i++) { augmented_CSR.row_start[i] = q; const bool is_cone_row = is_cone_variable(i); @@ -667,57 +709,61 @@ class iteration_data_t { // AT block, we can use A in csc directly const i_t col_beg = A.col_start[i]; const i_t col_end = A.col_start[i + 1]; - for (i_t p = col_beg; p < col_end; ++p) { - augmented_CSR.j[q] = A.i[p] + n; - augmented_CSR.x[q++] = A.x[p]; + for (i_t p = col_beg; p < col_end; ++p) { + augmented_CSR.j[q] = A.i[p] + n; + augmented_CSR.x[q++] = A.x[p]; + } } - } - for (i_t k = n; k < n + m; ++k) { - // A block, we can use AT in csc directly - augmented_CSR.row_start[k] = q; - const i_t l = k - n; - const i_t col_beg = AT.col_start[l]; - const i_t col_end = AT.col_start[l + 1]; - for (i_t p = col_beg; p < col_end; ++p) { - augmented_CSR.j[q] = AT.i[p]; - augmented_CSR.x[q++] = AT.x[p]; + for (i_t k = n; k < n + m; ++k) { + // A block, we can use AT in csc directly + augmented_CSR.row_start[k] = q; + const i_t l = k - n; + const i_t col_beg = AT.col_start[l]; + const i_t col_end = AT.col_start[l + 1]; + for (i_t p = col_beg; p < col_end; ++p) { + augmented_CSR.j[q] = AT.i[p]; + augmented_CSR.x[q++] = AT.x[p]; + } + augmented_diagonal_indices[k] = q; + augmented_CSR.j[q] = k; + augmented_CSR.x[q++] = primal_perturb; } - augmented_diagonal_indices[k] = q; - augmented_CSR.j[q] = k; - augmented_CSR.x[q++] = primal_perturb; + augmented_CSR.row_start[n + m] = q; + augmented_CSR.nz_max = q; + augmented_CSR.j.resize(q); + augmented_CSR.x.resize(q); + i_t expected_nnz = 2 * nnzA + (n - m_c) + total_block_nnz + m + off_diag_Qnz; + settings_.log.debug("augmented nz %d predicted %d\n", q, expected_nnz); + cuopt_assert(q == expected_nnz, "augmented nnz != predicted"); + cuopt_assert(A.col_start[n] == AT.col_start[m], "A nz != AT nz"); } - augmented_CSR.row_start[n + m] = q; - augmented_CSR.nz_max = q; - augmented_CSR.j.resize(q); - augmented_CSR.x.resize(q); - i_t expected_nnz = 2 * nnzA + (n - m_c) + total_block_nnz + m + off_diag_Qnz; - settings_.log.debug("augmented nz %d predicted %d\n", q, expected_nnz); - cuopt_assert(q == expected_nnz, "augmented nnz != predicted"); - cuopt_assert(A.col_start[n] == AT.col_start[m], "A nz != AT nz"); - - device_augmented.copy(augmented_CSR, handle_ptr->get_stream()); - d_augmented_diagonal_indices_.resize(augmented_diagonal_indices.size(), - handle_ptr->get_stream()); - raft::copy(d_augmented_diagonal_indices_.data(), - augmented_diagonal_indices.data(), - augmented_diagonal_indices.size(), - handle_ptr->get_stream()); - if (has_soc) { - d_cone_csr_indices_.resize(total_block_nnz, handle_ptr->get_stream()); - raft::copy(d_cone_csr_indices_.data(), - cone_csr_indices_host.data(), - total_block_nnz, - handle_ptr->get_stream()); - d_cone_Q_values_.resize(total_block_nnz, handle_ptr->get_stream()); - raft::copy(d_cone_Q_values_.data(), - cone_Q_values_host.data(), - total_block_nnz, + { + raft::common::nvtx::range scope("Barrier: augmented: device upload"); + device_augmented.copy(augmented_CSR, handle_ptr->get_stream()); + d_augmented_diagonal_indices_.resize(augmented_diagonal_indices.size(), + handle_ptr->get_stream()); + raft::copy(d_augmented_diagonal_indices_.data(), + augmented_diagonal_indices.data(), + augmented_diagonal_indices.size(), handle_ptr->get_stream()); - } - handle_ptr->sync_stream(); + if (has_soc) { + d_cone_csr_indices_.resize(total_block_nnz, handle_ptr->get_stream()); + raft::copy(d_cone_csr_indices_.data(), + cone_csr_indices_host.data(), + total_block_nnz, + handle_ptr->get_stream()); + d_cone_Q_values_.resize(total_block_nnz, handle_ptr->get_stream()); + raft::copy(d_cone_Q_values_.data(), + cone_Q_values_host.data(), + total_block_nnz, + handle_ptr->get_stream()); + } + + handle_ptr->sync_stream(); + } #ifdef CHECK_SYMMETRY csc_matrix_t augmented_transpose(1, 1, 1); augmented.transpose(augmented_transpose); @@ -781,43 +827,53 @@ class iteration_data_t { float64_t start_form_adat = tic(); const i_t m = AD.m; - raft::copy(device_AD.x.data(), - d_original_A_values.data(), - d_original_A_values.size(), - handle_ptr->get_stream()); - if (n_dense_columns > 0) { - // Adjust inv_diag - d_inv_diag_prime.resize(AD.n, stream_view_); - // Copy If - cub::DeviceSelect::Flagged( - d_flag_buffer.data(), - flag_buffer_size, - d_inv_diag.data(), - thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), - d_inv_diag_prime.data(), - d_num_flag.data(), - d_inv_diag.size(), - stream_view_); - RAFT_CHECK_CUDA(stream_view_); - } else { - d_inv_diag_prime.resize(inv_diag.size(), stream_view_); - raft::copy(d_inv_diag_prime.data(), d_inv_diag.data(), inv_diag.size(), stream_view_); + { + raft::common::nvtx::range scope("Barrier: Form ADAT: restore A"); + raft::copy(device_AD.x.data(), + d_original_A_values.data(), + d_original_A_values.size(), + handle_ptr->get_stream()); + } + { + raft::common::nvtx::range scope("Barrier: Form ADAT: inv_diag prime"); + if (n_dense_columns > 0) { + // Adjust inv_diag + d_inv_diag_prime.resize(AD.n, stream_view_); + // Copy If + cub::DeviceSelect::Flagged( + d_flag_buffer.data(), + flag_buffer_size, + d_inv_diag.data(), + thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), + d_inv_diag_prime.data(), + d_num_flag.data(), + d_inv_diag.size(), + stream_view_); + RAFT_CHECK_CUDA(stream_view_); + } else { + d_inv_diag_prime.resize(inv_diag.size(), stream_view_); + raft::copy(d_inv_diag_prime.data(), d_inv_diag.data(), inv_diag.size(), stream_view_); + } } cuopt_assert(static_cast(d_inv_diag_prime.size()) == AD.n, "inv_diag_prime.size() != AD.n"); - thrust::for_each_n(rmm::exec_policy(stream_view_), - thrust::make_counting_iterator(0), - i_t(device_AD.x.size()), - [span_x = cuopt::make_span(device_AD.x), - span_scale = cuopt::make_span(d_inv_diag_prime), - span_col_ind = cuopt::make_span(device_AD.col_index)] __device__(i_t i) { - span_x[i] *= span_scale[span_col_ind[i]]; - }); - RAFT_CHECK_CUDA(stream_view_); + { + raft::common::nvtx::range scope("Barrier: Form ADAT: scale AD"); + thrust::for_each_n(rmm::exec_policy(stream_view_), + thrust::make_counting_iterator(0), + i_t(device_AD.x.size()), + [span_x = cuopt::make_span(device_AD.x), + span_scale = cuopt::make_span(d_inv_diag_prime), + span_col_ind = cuopt::make_span(device_AD.col_index)] __device__(i_t i) { + span_x[i] *= span_scale[span_col_ind[i]]; + }); + RAFT_CHECK_CUDA(stream_view_); + } if (settings_.concurrent_halt != nullptr && *settings_.concurrent_halt == 1) { return; } if (first_call) { + raft::common::nvtx::range scope("Barrier: Form ADAT: cusparse init"); try { initialize_cusparse_data( handle_ptr, device_A, device_AD, device_ADAT, cusparse_info); @@ -828,8 +884,11 @@ class iteration_data_t { } if (settings_.concurrent_halt != nullptr && *settings_.concurrent_halt == 1) { return; } - multiply_kernels(handle_ptr, device_A, device_AD, device_ADAT, cusparse_info); - handle_ptr->sync_stream(); + { + raft::common::nvtx::range scope("Barrier: Form ADAT: ADAT multiply"); + multiply_kernels(handle_ptr, device_A, device_AD, device_ADAT, cusparse_info); + handle_ptr->sync_stream(); + } auto adat_nnz = device_ADAT.row_start.element(device_ADAT.m, handle_ptr->get_stream()); float64_t adat_time = toc(start_form_adat); diff --git a/solve_augmented_ldl.py b/solve_augmented_ldl.py new file mode 100644 index 0000000000..03055d5198 --- /dev/null +++ b/solve_augmented_ldl.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Solve dumped augmented KKT system with symmetric LDL factorization (NumPy only).""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import numpy as np + + +def read_matrix_market(path: Path) -> np.ndarray: + with path.open(encoding="utf-8") as f: + line = f.readline() + while line.startswith("%"): + line = f.readline() + m, n, nz = map(int, line.split()) + if m != n: + raise ValueError(f"Expected square matrix, got {m}x{n}") + A = np.zeros((m, n), dtype=np.float64) + for _ in range(nz): + line = f.readline() + if not line: + break + i, j, v = line.split() + A[int(i) - 1, int(j) - 1] = float(v) + return A + + +def load_rhs(path: Path) -> np.ndarray: + data = np.loadtxt(path, dtype=np.float64) + if data.ndim == 0: + return np.array([data], dtype=np.float64) + n = int(data[0]) + b = data[1:] + if b.size != n: + raise ValueError(f"{path}: length {b.size} != header n={n}") + return b + + +def load_vector_file(path: Path) -> np.ndarray: + return load_rhs(path) + + +def ldl_factor(A: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Symmetric LDL: A = L D L^T, L unit lower, D diagonal.""" + n = A.shape[0] + L = np.eye(n, dtype=np.float64) + D = np.zeros(n, dtype=np.float64) + for j in range(n): + if j > 0: + D[j] = A[j, j] - np.dot(L[j, :j] * D[:j], L[j, :j]) + else: + D[j] = A[j, j] + if abs(D[j]) < 1e-30: + raise np.linalg.LinAlgError(f"zero pivot at j={j}, D[j]={D[j]}") + for i in range(j + 1, n): + if j > 0: + L[i, j] = (A[i, j] - np.dot(L[i, :j] * D[:j], L[j, :j])) / D[j] + else: + L[i, j] = A[i, j] / D[j] + return L, D + + +def ldl_solve(L: np.ndarray, D: np.ndarray, b: np.ndarray) -> np.ndarray: + n = b.size + y = np.empty(n, dtype=np.float64) + for i in range(n): + y[i] = b[i] - L[i, :i] @ y[:i] + z = y / D + x = np.empty(n, dtype=np.float64) + for i in range(n - 1, -1, -1): + x[i] = z[i] - L[i + 1 :, i] @ x[i + 1 :] + return x + + +def solve_ldl(A: np.ndarray, b: np.ndarray) -> tuple[np.ndarray, dict]: + asym = float(np.max(np.abs(A - A.T))) + A_sym = 0.5 * (A + A.T) + L, D = ldl_factor(A_sym) + x = ldl_solve(L, D, b) + return x, {"n": A.shape[0], "max_asymmetry": asym} + + +def write_solution(path: Path, x: np.ndarray) -> None: + with path.open("w", encoding="utf-8") as f: + f.write(f"{x.size}\n") + for val in x: + f.write(f"{val:.16e}\n") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--matrix", + type=Path, + default=Path("cuopt_barrier_aug_initial_matrix.mtx"), + help="Matrix Market file (e.g. cuopt_barrier_aug_initial_matrix.mtx)", + ) + parser.add_argument( + "--rhs", + type=Path, + default=Path("cuopt_barrier_aug_initial_rhs.txt"), + help="Dumped RHS (first line is n, then n values)", + ) + parser.add_argument( + "--out", + type=Path, + default=Path("cuopt_barrier_aug_initial_ldl_x.txt"), + help="Where to write the Python LDL solution", + ) + parser.add_argument( + "--reference", + type=Path, + default=Path("cuopt_barrier_aug_initial_x.txt"), + help="cuOpt LDL solution before GMRES IR (from CUOPT_BARRIER_AUG_DUMP)", + ) + args = parser.parse_args() + + A = read_matrix_market(args.matrix) + b = load_rhs(args.rhs) + if b.size != A.shape[0]: + print(f"Dimension mismatch: A is {A.shape[0]}, b is {b.size}", file=sys.stderr) + return 1 + + x, info = solve_ldl(A, b) + write_solution(args.out, x) + + r = b - A @ x + print(f"Matrix: {args.matrix}") + print(f"RHS: {args.rhs}") + print(f"n={info['n']}, max|A-A^T|={info['max_asymmetry']:.3e}") + print(f"LDL solution written to {args.out}") + print(f"||Ax - b||_inf = {np.max(np.abs(r)):.6e}") + print(f"||Ax - b||_2 = {np.linalg.norm(r):.6e}") + + if args.reference.is_file(): + x_ref = load_vector_file(args.reference) + diff = x - x_ref + print(f"Reference: {args.reference}") + print(f"||x_ldl - x_chol||_inf = {np.max(np.abs(diff)):.6e}") + print(f"||x_ldl - x_chol||_2 = {np.linalg.norm(diff):.6e}") + r_ref = np.max(np.abs(b - A @ x_ref)) + print(f"||A x_chol - b||_inf (reference residual) = {r_ref:.6e}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 9655f05a30c07d66011f988379de3dfcaa6e53ae Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 21 May 2026 07:57:55 -0700 Subject: [PATCH 64/68] --amend --- solve_augmented_ldl.py | 151 ----------------------------------------- 1 file changed, 151 deletions(-) delete mode 100644 solve_augmented_ldl.py diff --git a/solve_augmented_ldl.py b/solve_augmented_ldl.py deleted file mode 100644 index 03055d5198..0000000000 --- a/solve_augmented_ldl.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -"""Solve dumped augmented KKT system with symmetric LDL factorization (NumPy only).""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path - -import numpy as np - - -def read_matrix_market(path: Path) -> np.ndarray: - with path.open(encoding="utf-8") as f: - line = f.readline() - while line.startswith("%"): - line = f.readline() - m, n, nz = map(int, line.split()) - if m != n: - raise ValueError(f"Expected square matrix, got {m}x{n}") - A = np.zeros((m, n), dtype=np.float64) - for _ in range(nz): - line = f.readline() - if not line: - break - i, j, v = line.split() - A[int(i) - 1, int(j) - 1] = float(v) - return A - - -def load_rhs(path: Path) -> np.ndarray: - data = np.loadtxt(path, dtype=np.float64) - if data.ndim == 0: - return np.array([data], dtype=np.float64) - n = int(data[0]) - b = data[1:] - if b.size != n: - raise ValueError(f"{path}: length {b.size} != header n={n}") - return b - - -def load_vector_file(path: Path) -> np.ndarray: - return load_rhs(path) - - -def ldl_factor(A: np.ndarray) -> tuple[np.ndarray, np.ndarray]: - """Symmetric LDL: A = L D L^T, L unit lower, D diagonal.""" - n = A.shape[0] - L = np.eye(n, dtype=np.float64) - D = np.zeros(n, dtype=np.float64) - for j in range(n): - if j > 0: - D[j] = A[j, j] - np.dot(L[j, :j] * D[:j], L[j, :j]) - else: - D[j] = A[j, j] - if abs(D[j]) < 1e-30: - raise np.linalg.LinAlgError(f"zero pivot at j={j}, D[j]={D[j]}") - for i in range(j + 1, n): - if j > 0: - L[i, j] = (A[i, j] - np.dot(L[i, :j] * D[:j], L[j, :j])) / D[j] - else: - L[i, j] = A[i, j] / D[j] - return L, D - - -def ldl_solve(L: np.ndarray, D: np.ndarray, b: np.ndarray) -> np.ndarray: - n = b.size - y = np.empty(n, dtype=np.float64) - for i in range(n): - y[i] = b[i] - L[i, :i] @ y[:i] - z = y / D - x = np.empty(n, dtype=np.float64) - for i in range(n - 1, -1, -1): - x[i] = z[i] - L[i + 1 :, i] @ x[i + 1 :] - return x - - -def solve_ldl(A: np.ndarray, b: np.ndarray) -> tuple[np.ndarray, dict]: - asym = float(np.max(np.abs(A - A.T))) - A_sym = 0.5 * (A + A.T) - L, D = ldl_factor(A_sym) - x = ldl_solve(L, D, b) - return x, {"n": A.shape[0], "max_asymmetry": asym} - - -def write_solution(path: Path, x: np.ndarray) -> None: - with path.open("w", encoding="utf-8") as f: - f.write(f"{x.size}\n") - for val in x: - f.write(f"{val:.16e}\n") - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--matrix", - type=Path, - default=Path("cuopt_barrier_aug_initial_matrix.mtx"), - help="Matrix Market file (e.g. cuopt_barrier_aug_initial_matrix.mtx)", - ) - parser.add_argument( - "--rhs", - type=Path, - default=Path("cuopt_barrier_aug_initial_rhs.txt"), - help="Dumped RHS (first line is n, then n values)", - ) - parser.add_argument( - "--out", - type=Path, - default=Path("cuopt_barrier_aug_initial_ldl_x.txt"), - help="Where to write the Python LDL solution", - ) - parser.add_argument( - "--reference", - type=Path, - default=Path("cuopt_barrier_aug_initial_x.txt"), - help="cuOpt LDL solution before GMRES IR (from CUOPT_BARRIER_AUG_DUMP)", - ) - args = parser.parse_args() - - A = read_matrix_market(args.matrix) - b = load_rhs(args.rhs) - if b.size != A.shape[0]: - print(f"Dimension mismatch: A is {A.shape[0]}, b is {b.size}", file=sys.stderr) - return 1 - - x, info = solve_ldl(A, b) - write_solution(args.out, x) - - r = b - A @ x - print(f"Matrix: {args.matrix}") - print(f"RHS: {args.rhs}") - print(f"n={info['n']}, max|A-A^T|={info['max_asymmetry']:.3e}") - print(f"LDL solution written to {args.out}") - print(f"||Ax - b||_inf = {np.max(np.abs(r)):.6e}") - print(f"||Ax - b||_2 = {np.linalg.norm(r):.6e}") - - if args.reference.is_file(): - x_ref = load_vector_file(args.reference) - diff = x - x_ref - print(f"Reference: {args.reference}") - print(f"||x_ldl - x_chol||_inf = {np.max(np.abs(diff)):.6e}") - print(f"||x_ldl - x_chol||_2 = {np.linalg.norm(diff):.6e}") - r_ref = np.max(np.abs(b - A @ x_ref)) - print(f"||A x_chol - b||_inf (reference residual) = {r_ref:.6e}") - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From 9ea0205ae47441061aec64f77d686ef6dd764bf5 Mon Sep 17 00:00:00 2001 From: Ramakrishnap <42624703+rgsl888prabhu@users.noreply.github.com> Date: Thu, 21 May 2026 23:30:53 +0530 Subject: [PATCH 65/68] Fix version failure in check style for skills (#1271) Fix version failure in check style Authors: - Ramakrishnap (https://github.com/rgsl888prabhu) - https://github.com/svc-nvskills-signing Approvers: - Ishika Roy (https://github.com/Iroy30) URL: https://github.com/NVIDIA/cuopt/pull/1271 --- skills/skill-evolution/SKILL.md | 2 +- skills/skill-evolution/skill-card.md | 6 +++--- skills/skill-evolution/skill.oms.sig | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skills/skill-evolution/SKILL.md b/skills/skill-evolution/SKILL.md index 8fb6b7bba0..9c2dbc5b9e 100644 --- a/skills/skill-evolution/SKILL.md +++ b/skills/skill-evolution/SKILL.md @@ -137,7 +137,7 @@ When skill evolution creates an entirely new skill directory, add `origin: skill ```yaml --- name: new-skill-name -version: "26.06.00" +version: "26.08.00" description: ... origin: skill-evolution --- diff --git a/skills/skill-evolution/skill-card.md b/skills/skill-evolution/skill-card.md index 9a4bccd28e..6978f664ff 100644 --- a/skills/skill-evolution/skill-card.md +++ b/skills/skill-evolution/skill-card.md @@ -8,7 +8,7 @@ This skill is ready for commercial/non-commercial use.
### License/Terms of Use:
Apache 2.0
## Use Case:
-Developers and engineers use this skill to capture generalizable learnings from agent interactions and propose updates to existing skills, ensuring continuous improvement of agent guidance without blocking the user's primary task.
+Developers and engineers use this skill to continuously improve agent skill files by capturing generalizable learnings from problem-solving interactions and proposing structured updates.
### Deployment Geography for Use:
Global
@@ -23,12 +23,12 @@ Mitigation: Review and scan skill before deployment.
## Skill Output:
**Output Type(s):** [Analysis, Code]
-**Output Format:** [Markdown with inline code diffs]
+**Output Format:** [Markdown with inline code blocks]
**Output Parameters:** [1D]
**Other Properties Related to Output:** [None]
## Skill Version(s):
-26.06.00 (source: frontmatter)
+26.08.00 (source: frontmatter)
## Ethical Considerations:
NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal team to ensure this skill meets requirements for the relevant industry and use case and addresses unforeseen product misuse.
diff --git a/skills/skill-evolution/skill.oms.sig b/skills/skill-evolution/skill.oms.sig index a814104939..18f66aa4ea 100644 --- a/skills/skill-evolution/skill.oms.sig +++ b/skills/skill-evolution/skill.oms.sig @@ -1 +1 @@ -{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIICgzCCAgmgAwIBAgIUKIyS7SxNteQIiWzK1dWj85E6520wCgYIKoZIzj0EAwMwVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwHhcNMjYwNDAxMDAwMDAwWhcNMjgwNDIyMTUzMzA5WjBUMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTlZJRElBIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9OVklESUEgQWdlbnQgU2tpbGxzIFNpZ25pbmcgMDAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEYoRM9bQl/dGlwSRNi6bTpIJUXH8Nv9GciP6LSflJYYMLCc296kpyuTSsk5ddbAWiDcFX3C/ydX3jwc+qCLYP6uHy9XphyLjOQ27Yb2J6rBLVtRBS1mgGco/Gr7fL6ODco4GaMIGXMB0GA1UdDgQWBBRQ/5ZW3nJ6lmo9SVk7I15o7UGmpTAfBgNVHSMEGDAWgBRPGpILxMBBleJSsBGjrMKsby1CgjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vY3NwLm5kaXMubnZpZGlhLmNvbTAKBggqhkjOPQQDAwNoADBlAjAUygu/GiOCIXrgGr4SmLgeEVDcEitfFUv7ALbvLVGVyMysB3mxmO/uInZfXzWcJZsCMQDxuoxj4ZmO30jhkPIcCxGFCOvnUsnfU3TfGcouYm4M6iRpbKvtVnHPiy4bi6pcKf0="},{"rawBytes":"MIICiDCCAg6gAwIBAgIUZsIuSv9NkpJCNqtYEfCouVv5BzowCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASI72cR3ctKGg4VWnB3bNja6g1Z2PnOmFEopkPof+QeIcPk9rT+g9MjJnq51EQXL93a7C2GJ9J985G4o2V85VD7wJ1RaXhluHW2rf3y8bQGeAYaKMr5s/hUgn+M3/9WlWejgaAwgZ0wHQYDVR0OBBYEFE8akgvEwEGV4lKwEaOswqxvLUKCMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3AubmRpcy5udmlkaWEuY29tMAoGCCqGSM49BAMDA2gAMGUCMQCeIMMfAbyzPDacw2MxG+Yt1cikrJX/DVxiGfXuHmkkXn6VgSzE79+lkqDErpVO2gYCMCNEColOyvUvkzZGUEI1hQ3PfMgi3FIo9tHoBKMw4/wGBLFpu/0ubtmbBXM6/UMOEw=="},{"rawBytes":"MIICRTCCAcygAwIBAgIUeJdY3rV86EdvFmG7L8LJBsyQFYkwCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAYpiXCDjJ9NT2eSDhyHJVSw1Tbze18cGG2F/578oWvHxg23eQAhNRYdq88i1iOshZSO6C29doKui5Xpmo/7Ctw9Sx4PP2RzOmIuOLCuTdNtKcTRwi4GEsd5BAFvWj42M6NjMGEwHQYDVR0OBBYEFItnoAjjfuCEUvzyvWyI2vOGvwPjMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMCwtAjWLaNwgGWNCgdyNoTyvNhqWRECRJV2r3+7w8g0PL6NHLOsbkgE09BH95h8XlgIwTaQmbbUh2ChAJ5TA1wRiVDnCcvbzHlZl2jM2FcwQQZlk19LOAbyGMRixbu2Ww/rj"}]},"tlogEntries":[]},"dsseEnvelope":{"payload":"ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YxIiwKICAic3ViamVjdCI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAic2tpbGwtZXZvbHV0aW9uIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhMjU2IjogIjhmZGViMGU0ZmE4YmIzY2FjNDVmMTBhZTZkYzZmYTNiNDEzNWIwMmFhOWQyYzVkMjY4NDA0NTMxNzBjNDZlZDMiCiAgICAgIH0KICAgIH0KICBdLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vbW9kZWxfc2lnbmluZy9zaWduYXR1cmUvdjEuMCIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJzZXJpYWxpemF0aW9uIjogewogICAgICAiYWxsb3dfc3ltbGlua3MiOiBmYWxzZSwKICAgICAgImhhc2hfdHlwZSI6ICJzaGEyNTYiLAogICAgICAiaWdub3JlX3BhdGhzIjogWwogICAgICAgICIuZ2l0aHViIiwKICAgICAgICAiLmdpdGlnbm9yZSIsCiAgICAgICAgIi5naXRhdHRyaWJ1dGVzIiwKICAgICAgICAiLmdpdCIKICAgICAgXSwKICAgICAgIm1ldGhvZCI6ICJmaWxlcyIKICAgIH0sCiAgICAicmVzb3VyY2VzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiU0tJTEwubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjNjZjM5NDA4NmQ4MmY4MDFmNDk2NzEwYmRjZTJjOTZlZmFhMjEzM2MxZDAwYzAxYzA5N2Q3N2I3MmU3MGI4M2IiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJza2lsbC1jYXJkLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI2MDlhNzhmYzY3ZTU2NWY0MTc0N2MwYWIwMzgxMGQwZTJjYzk3ODY0NjUwOTAyZDcwNjg5MWVhZmY3OGU5MGEwIgogICAgICB9CiAgICBdCiAgfQp9","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MGUCMEEOy8isxKJRKBjKoyIlCeOC458Vg3EuaXCLD8E/4fmYxxnt2zZifq5NWf9vLfhheAIxALtU3qZWNacF74dg3lrp/BHuGnbg9SvuMQKZRJ9Il8KxNMT/Ud5dfHW+1F3+EpE+BQ==","keyid":""}]}} \ No newline at end of file +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIICgzCCAgmgAwIBAgIUKIyS7SxNteQIiWzK1dWj85E6520wCgYIKoZIzj0EAwMwVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwHhcNMjYwNDAxMDAwMDAwWhcNMjgwNDIyMTUzMzA5WjBUMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTlZJRElBIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9OVklESUEgQWdlbnQgU2tpbGxzIFNpZ25pbmcgMDAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEYoRM9bQl/dGlwSRNi6bTpIJUXH8Nv9GciP6LSflJYYMLCc296kpyuTSsk5ddbAWiDcFX3C/ydX3jwc+qCLYP6uHy9XphyLjOQ27Yb2J6rBLVtRBS1mgGco/Gr7fL6ODco4GaMIGXMB0GA1UdDgQWBBRQ/5ZW3nJ6lmo9SVk7I15o7UGmpTAfBgNVHSMEGDAWgBRPGpILxMBBleJSsBGjrMKsby1CgjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vY3NwLm5kaXMubnZpZGlhLmNvbTAKBggqhkjOPQQDAwNoADBlAjAUygu/GiOCIXrgGr4SmLgeEVDcEitfFUv7ALbvLVGVyMysB3mxmO/uInZfXzWcJZsCMQDxuoxj4ZmO30jhkPIcCxGFCOvnUsnfU3TfGcouYm4M6iRpbKvtVnHPiy4bi6pcKf0="},{"rawBytes":"MIICiDCCAg6gAwIBAgIUZsIuSv9NkpJCNqtYEfCouVv5BzowCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASI72cR3ctKGg4VWnB3bNja6g1Z2PnOmFEopkPof+QeIcPk9rT+g9MjJnq51EQXL93a7C2GJ9J985G4o2V85VD7wJ1RaXhluHW2rf3y8bQGeAYaKMr5s/hUgn+M3/9WlWejgaAwgZ0wHQYDVR0OBBYEFE8akgvEwEGV4lKwEaOswqxvLUKCMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3AubmRpcy5udmlkaWEuY29tMAoGCCqGSM49BAMDA2gAMGUCMQCeIMMfAbyzPDacw2MxG+Yt1cikrJX/DVxiGfXuHmkkXn6VgSzE79+lkqDErpVO2gYCMCNEColOyvUvkzZGUEI1hQ3PfMgi3FIo9tHoBKMw4/wGBLFpu/0ubtmbBXM6/UMOEw=="},{"rawBytes":"MIICRTCCAcygAwIBAgIUeJdY3rV86EdvFmG7L8LJBsyQFYkwCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAYpiXCDjJ9NT2eSDhyHJVSw1Tbze18cGG2F/578oWvHxg23eQAhNRYdq88i1iOshZSO6C29doKui5Xpmo/7Ctw9Sx4PP2RzOmIuOLCuTdNtKcTRwi4GEsd5BAFvWj42M6NjMGEwHQYDVR0OBBYEFItnoAjjfuCEUvzyvWyI2vOGvwPjMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMCwtAjWLaNwgGWNCgdyNoTyvNhqWRECRJV2r3+7w8g0PL6NHLOsbkgE09BH95h8XlgIwTaQmbbUh2ChAJ5TA1wRiVDnCcvbzHlZl2jM2FcwQQZlk19LOAbyGMRixbu2Ww/rj"}]},"tlogEntries":[]},"dsseEnvelope":{"payload":"ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YxIiwKICAic3ViamVjdCI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAic2tpbGwtZXZvbHV0aW9uIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhMjU2IjogIjIyYjcxMGEyOWFhYzU0NzJjZGQ0OWFlZDg4MTFhZGMyMjYwZjVjMGExMzk2YWEyNzA4MGM0ODI5ODk4YzhjZGQiCiAgICAgIH0KICAgIH0KICBdLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vbW9kZWxfc2lnbmluZy9zaWduYXR1cmUvdjEuMCIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJyZXNvdXJjZXMiOiBbCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI5Y2RjZmJmMTAxN2I4ODVkMTZmNGQ1YzFiZDI5NWQwMjA2MWFhNjExMDA3M2MxMWEyNzljNjA1YTBhMzlmNjc1IiwKICAgICAgICAibmFtZSI6ICJTS0lMTC5tZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImUwNTM3MDViYjY2ZDZiZGIzZDY0NWIxYzMxMzVlMjQyMTExOGM3MDI3MDMwOGFiNWJmZjQ5YmJmNDZmZGZhMjYiLAogICAgICAgICJuYW1lIjogInNraWxsLWNhcmQubWQiCiAgICAgIH0KICAgIF0sCiAgICAic2VyaWFsaXphdGlvbiI6IHsKICAgICAgIm1ldGhvZCI6ICJmaWxlcyIsCiAgICAgICJhbGxvd19zeW1saW5rcyI6IGZhbHNlLAogICAgICAiaWdub3JlX3BhdGhzIjogWwogICAgICAgICIuZ2l0IiwKICAgICAgICAiLmdpdGh1YiIsCiAgICAgICAgIi5naXRpZ25vcmUiLAogICAgICAgICIuZ2l0YXR0cmlidXRlcyIKICAgICAgXSwKICAgICAgImhhc2hfdHlwZSI6ICJzaGEyNTYiCiAgICB9CiAgfQp9","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MGUCMArpr3Lhi56I/2OHSrdWFzKUx2D7fNnkdsetykK0tFvW30cp0wgE4PbH7pXYmbsaAgIxAO/E9DXRr5XeRMfU4bRW6hePBjQdt2s4XlRfY6QV3/4n6OuFQsjNPpiReg/Nm/06Cw==","keyid":""}]}} \ No newline at end of file From 1ca1501c92249704a3e22ea227abce11f4ab39d7 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Thu, 21 May 2026 11:39:34 -0700 Subject: [PATCH 66/68] Add barrier_presolve back for socps --- .../cuopt/linear_programming/constants.h | 2 - .../pdlp/solver_settings.hpp | 10 --- cpp/src/dual_simplex/presolve.cpp | 60 +++++++++++----- cpp/src/dual_simplex/presolve.hpp | 3 + .../dual_simplex/simplex_solver_settings.hpp | 6 +- cpp/src/dual_simplex/solve.cpp | 24 ++----- cpp/src/math_optimization/solver_settings.cu | 2 - cpp/src/pdlp/solve.cu | 6 +- .../dual_simplex/unit_tests/solve_barrier.cu | 69 ------------------- 9 files changed, 57 insertions(+), 125 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index dc60cff036..503ae686b6 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -55,8 +55,6 @@ #define CUOPT_ELIMINATE_DENSE_COLUMNS "eliminate_dense_columns" #define CUOPT_CUDSS_DETERMINISTIC "cudss_deterministic" #define CUOPT_PRESOLVE "presolve" -#define CUOPT_BARRIER_PRESOLVE "barrier_presolve" -#define CUOPT_BARRIER_NATIVE_FREE_LINEAR "barrier_native_free_linear" #define CUOPT_DUAL_POSTSOLVE "dual_postsolve" #define CUOPT_MIP_DETERMINISM_MODE "mip_determinism_mode" #define CUOPT_MIP_ABSOLUTE_TOLERANCE "mip_absolute_tolerance" diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index fcd39f54ea..3597b85016 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -286,16 +286,6 @@ class pdlp_solver_settings_t { bool eliminate_dense_columns{true}; pdlp_precision_t pdlp_precision{pdlp_precision_t::DefaultPrecision}; bool barrier_iterative_refinement{true}; - /** - * Internal dual_simplex presolve (bound shifts, free-var bounding/split, folding) before barrier. - * Default true for LP/QP barrier. Ignored for SOCP (second-order cones): barrier presolve is off. - */ - bool barrier_presolve{true}; - /** - * Tag linear FR variables as native free for augmented SOCP/QP barrier (no x*z complementarity). - * Independent of @ref barrier_presolve. Always enabled for SOCP barrier solves. - */ - bool barrier_native_free_linear{true}; f_t barrier_step_scale{0.9}; bool save_best_primal_so_far{false}; /** diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 0950fc8c94..7cd4a62250 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -923,9 +923,7 @@ void convert_user_problem(const user_problem_t& user_problem, problem.Q.x = user_problem.Q_values; } - // Add artificial variables for LP equality rows when not using barrier presolve. - // SOCP problems must keep [linear | cone] column order; slacks for inequalities are - // inserted before the cone block in convert_less_than_to_equal — do not append here. + // Artificial vars break [linear | cone]; only for barrier_presolve-off LP-style layouts. if (!settings.barrier_presolve && problem.second_order_cone_dims.empty()) { add_artifical_variables(problem, user_problem.range_rows, equality_rows, new_slacks); } @@ -939,13 +937,16 @@ i_t presolve(const lp_problem_t& original, { problem = original; const i_t linear_cols = linear_var_count(problem); + const bool has_cones = !problem.second_order_cone_dims.empty(); std::vector row_sense(problem.num_rows, '='); + // Check for free variables (linear block only; cone columns are handled by the barrier SOC layout) i_t free_variables = 0; for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } } + // Barrier presolve phase 1: bound free linear variables from equality rows. if (settings.barrier_presolve && free_variables > 0) { // Try to remove free variables std::vector constraints_to_check; @@ -970,10 +971,30 @@ i_t presolve(const lp_problem_t& original, i_t removed_free_variables = 0; if (!constraints_to_check.empty()) { - // Check if the constraints are feasible csr_matrix_t Arow(0, 0, 0); problem.A.to_compressed_row(Arow); + // Keep only rows safe for phase-1 bound inference: no cone columns, exactly one free linear. + { + std::vector safe_constraints; + safe_constraints.reserve(constraints_to_check.size()); + for (i_t i : constraints_to_check) { + bool touches_cone = false; + i_t free_linear_in_row = 0; + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + const i_t j = Arow.j[p]; + if (j >= linear_cols) { + touches_cone = true; + continue; + } + if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_linear_in_row++; } + } + if (touches_cone || free_linear_in_row != 1) { continue; } + safe_constraints.push_back(i); + } + constraints_to_check.swap(safe_constraints); + } + // The constraints are in the form: // sum_j a_j x_j = beta for (i_t i : constraints_to_check) { @@ -1041,6 +1062,7 @@ i_t presolve(const lp_problem_t& original, // x_j <= 1/a_ij * (rhs - upper_activity_i) const i_t j = last_free_i; const f_t a_ij = last_free_coeff_i; + if (a_ij == 0) { continue; } const f_t max_bound = 1e10; bool bounded = false; if (a_ij > 0) { @@ -1090,6 +1112,9 @@ i_t presolve(const lp_problem_t& original, problem.lower[j] = -inf; } } + if (!(problem.lower[j] == -inf && problem.upper[j] == inf)) { + presolve_info.phase1_bounded_linear_indices.push_back(j); + } } if (removed_free_variables != 0) { @@ -1097,6 +1122,8 @@ i_t presolve(const lp_problem_t& original, static_cast(removed_free_variables)); } } + + // Barrier presolve phase 2: negate one-sided bounds (-inf < x <= u -> -u <= x < inf). // The original problem may have a variable without a lower bound // but a finite upper bound // -inf < x_j <= u_j (linear variables only) @@ -1147,8 +1174,8 @@ i_t presolve(const lp_problem_t& original, } } - // The original problem may have nonzero lower bounds - // 0 != l_j <= x_j <= u_j (linear variables only) + // Barrier presolve phase 3: shift nonzero lower bounds to zero (cone/stack columns not shifted). + // 0 != l_j <= x_j <= u_j (linear variables only; cone/stack columns are not shifted) i_t nonzero_lower_bounds = 0; for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] != 0.0 && problem.lower[j] > -inf) { nonzero_lower_bounds++; } @@ -1223,7 +1250,7 @@ i_t presolve(const lp_problem_t& original, } } - // Check for empty rows + // Barrier presolve phase 4: remove empty rows and empty linear columns. i_t num_empty_rows = 0; { csr_matrix_t Arow(0, 0, 0); @@ -1257,14 +1284,9 @@ i_t presolve(const lp_problem_t& original, } problem.Q.check_matrix("Before free variable expansion"); - // For QPs and SOCPs using the augmented system, keep free variables as-is rather than - // splitting x = v - w. The barrier solver handles them natively with a - // static regularizer on the diagonal instead of z/x complementarity terms. - const bool keep_native_free_variables = - problem.Q.n > 0 || !problem.second_order_cone_dims.empty(); - const bool register_native_free_linear = - (settings.barrier_native_free_linear || settings.barrier_presolve) && free_variables > 0 && - keep_native_free_variables; + // Barrier presolve phase 5: free linear variables — 5a native (QP/SOCP) or 5b v-w split (LP). + // QP/SOCP augmented system: keep free linears as-is (no x = v - w); barrier uses D = 0 on them. + const bool register_native_free_linear = free_variables > 0 && (problem.Q.n > 0 || has_cones); if (register_native_free_linear) { presolve_info.free_variable_pairs.clear(); presolve_info.native_free_linear_indices.clear(); @@ -1280,7 +1302,8 @@ i_t presolve(const lp_problem_t& original, settings.log.printf( "Keeping %d native free linear variables for augmented-system barrier (QP/SOCP)\n", native_free_count); - } else if (settings.barrier_presolve && free_variables > 0) { + } else if (settings.barrier_presolve && !has_cones && free_variables > 0) { + // Phase 5b: x_j = v - w with v, w >= 0 (LP without cones; SOCP/QP use phase 5a). // We have a variable x_j: with -inf < x_j < inf // we create new variables v and w with 0 <= v, w and x_j = v - w // Constraints @@ -1920,6 +1943,11 @@ void uncrush_solution(const presolve_info_t& presolve_info, } } + if (!presolve_info.phase1_bounded_linear_indices.empty()) { + settings.log.printf("Post-solve: %d linear column(s) had phase-1 bound tightening (x unchanged)\n", + static_cast(presolve_info.phase1_bounded_linear_indices.size())); + } + assert(uncrushed_x.size() == input_x.size()); assert(uncrushed_y.size() == input_y.size()); assert(uncrushed_z.size() == input_z.size()); diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index a8299cd228..b200d257b1 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -158,6 +158,9 @@ struct presolve_info_t { // Free variable indices for QP/SOCP augmented barrier (not split, handled natively) std::vector native_free_linear_indices; + + // Linear columns where phase 1 tightened a fully free var to a one-sided bound (same x index) + std::vector phase1_bounded_linear_indices; }; template diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index ec5134358f..b8a9f50c68 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -79,8 +79,7 @@ struct simplex_solver_settings_t { use_left_looking_lu(false), eliminate_singletons(true), print_presolve_stats(true), - barrier_presolve(false), - barrier_native_free_linear(true), + barrier_presolve(true), cudss_deterministic(false), deterministic(false), barrier(false), @@ -163,8 +162,7 @@ struct simplex_solver_settings_t { use_left_looking_lu; // true to use left looking LU factorization, false to use right looking bool eliminate_singletons; // true to eliminate singletons from the basis bool print_presolve_stats; // true to print presolve stats - bool barrier_presolve; // bound shifts, free-var bounding/split, folding, etc. - bool barrier_native_free_linear; // register native free linear cols for augmented SOCP/QP barrier + bool barrier_presolve; // bound shifts, free-var bounding; LP v-w split / folding when no cones bool cudss_deterministic; // true to use cuDSS deterministic mode, false for non-deterministic bool barrier; // true to use barrier method, false to use dual simplex method bool deterministic; // true to use B&B deterministic mode, false to use non-deterministic mode diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 9808816e31..f230b0fc2f 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -382,16 +382,6 @@ lp_status_t solve_linear_program_with_advanced_basis( return lp_status; } -/** SOCP barrier: skip bound-shift presolve; keep native free linear variables for augmented KKT. */ -template -void apply_barrier_socp_presolve_defaults(const user_problem_t& user_problem, - simplex_solver_settings_t& settings) -{ - if (user_problem.second_order_cone_dims.empty()) { return; } - settings.barrier_presolve = false; - settings.barrier_native_free_linear = true; -} - template lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, @@ -403,10 +393,8 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us // Convert the user problem to a linear program with only equality constraints std::vector new_slacks; - simplex_solver_settings_t barrier_settings = settings; - apply_barrier_socp_presolve_defaults(user_problem, barrier_settings); dualize_info_t dualize_info; - convert_user_problem(user_problem, barrier_settings, original_lp, new_slacks, dualize_info); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); if (!validate_barrier_cone_layout(original_lp, settings)) { return lp_status_t::NUMERICAL_ISSUES; } @@ -416,7 +404,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us // Presolve the linear program presolve_info_t presolve_info; lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); - const i_t ok = presolve(original_lp, barrier_settings, presolved_lp, presolve_info); + const i_t ok = presolve(original_lp, settings, presolved_lp, presolve_info); if (ok == CONCURRENT_HALT_RETURN) { return lp_status_t::CONCURRENT_LIMIT; } if (ok == TIME_LIMIT_RETURN) { return lp_status_t::TIME_LIMIT; } if (ok == -1) { return lp_status_t::INFEASIBLE; } @@ -428,12 +416,12 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; std::vector row_scales; - column_scaling(presolved_lp, barrier_settings, barrier_lp, column_scales, row_scales); + column_scaling(presolved_lp, settings, barrier_lp, column_scales, row_scales); // Solve using barrier lp_solution_t barrier_solution(barrier_lp.num_rows, barrier_lp.num_cols); - barrier_solver_t barrier_solver(barrier_lp, presolve_info, barrier_settings); + barrier_solver_t barrier_solver(barrier_lp, presolve_info, settings); lp_status_t barrier_status = barrier_solver.solve(start_time, barrier_solution); if (barrier_status == lp_status_t::OPTIMAL) { #ifdef COMPUTE_SCALED_RESIDUALS @@ -486,7 +474,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us // Undo presolve uncrush_solution(presolve_info, - barrier_settings, + settings, unscaled_x, unscaled_y, unscaled_z, @@ -707,7 +695,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us lp_solution_t crossover_solution(original_lp.num_rows, original_lp.num_cols); std::vector vstatus(original_lp.num_cols); crossover_status_t crossover_status = crossover( - original_lp, barrier_settings, lp_solution, start_time, crossover_solution, vstatus); + original_lp, settings, lp_solution, start_time, crossover_solution, vstatus); settings.log.printf("Crossover status: %d\n", crossover_status); if (crossover_status == crossover_status_t::OPTIMAL) { barrier_status = lp_status_t::OPTIMAL; } } diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index ecf8a97ec7..f57de9b8f1 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -177,8 +177,6 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_CUDSS_DETERMINISTIC, &pdlp_settings.cudss_deterministic, false}, {CUOPT_DUAL_POSTSOLVE, &pdlp_settings.dual_postsolve, true}, {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, - {CUOPT_BARRIER_PRESOLVE, &pdlp_settings.barrier_presolve, true}, - {CUOPT_BARRIER_NATIVE_FREE_LINEAR, &pdlp_settings.barrier_native_free_linear, true}, }; // String parameters string_parameters = { diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 621fe35dce..c77a87993c 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -507,10 +507,8 @@ run_barrier(dual_simplex::user_problem_t& user_problem, barrier_settings.barrier = true; barrier_settings.crossover = settings.crossover; barrier_settings.eliminate_dense_columns = settings.eliminate_dense_columns; - barrier_settings.barrier_iterative_refinement = settings.barrier_iterative_refinement; - barrier_settings.barrier_presolve = settings.barrier_presolve; - barrier_settings.barrier_native_free_linear = settings.barrier_native_free_linear; - barrier_settings.barrier_step_scale = settings.barrier_step_scale; + barrier_settings.barrier_iterative_refinement = settings.barrier_iterative_refinement; + barrier_settings.barrier_step_scale = settings.barrier_step_scale; barrier_settings.cudss_deterministic = settings.cudss_deterministic; barrier_settings.barrier_relative_feasibility_tol = settings.barrier_relative_feasibility_tolerance; diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index a064b2b756..90dec9a46e 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -207,7 +207,6 @@ TEST(barrier, cone_metadata_reindexed_when_slack_is_inserted_before_cones) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = true; settings.dualize = 0; settings.scale_columns = false; @@ -273,7 +272,6 @@ TEST(barrier, presolve_reindexes_cone_start_after_empty_column_removal) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = true; settings.dualize = 0; settings.scale_columns = false; @@ -340,7 +338,6 @@ TEST(barrier, presolve_keeps_native_free_variables_before_cones) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = true; settings.dualize = 0; settings.scale_columns = false; @@ -366,65 +363,6 @@ TEST(barrier, presolve_keeps_native_free_variables_before_cones) EXPECT_EQ(presolved_lp.upper[1], inf); } -TEST(barrier, native_free_linear_without_barrier_presolve) -{ - // barrier_presolve off skips bound shifts; barrier_native_free_linear still tags FR cols. - raft::handle_t handle{}; - init_handler(&handle); - - using namespace cuopt::linear_programming::dual_simplex; - user_problem_t user_problem(&handle); - - constexpr int m = 1; - constexpr int n = 5; - constexpr int nz = 5; - - user_problem.num_rows = m; - user_problem.num_cols = n; - user_problem.objective = {0.0, 0.0, 0.0, 0.0, 0.0}; - - user_problem.A.m = m; - user_problem.A.n = n; - user_problem.A.nz_max = nz; - user_problem.A.reallocate(nz); - user_problem.A.col_start = {0, 1, 2, 3, 4, 5}; - for (int j = 0; j < n; ++j) { - user_problem.A.i[j] = 0; - user_problem.A.x[j] = 1.0; - } - - user_problem.rhs = {1.0}; - user_problem.row_sense = {'E'}; - user_problem.lower = {-inf, -inf, 0.0, 0.0, 0.0}; - user_problem.upper.assign(n, inf); - user_problem.num_range_rows = 0; - user_problem.cone_var_start = 2; - user_problem.second_order_cone_dims = {3}; - user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); - - simplex_solver_settings_t settings; - settings.barrier = true; - settings.barrier_presolve = false; - settings.barrier_native_free_linear = true; - settings.dualize = 0; - settings.scale_columns = false; - - std::vector new_slacks; - dualize_info_t dualize_info; - lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); - convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); - - presolve_info_t presolve_info; - lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); - ASSERT_EQ(presolve(original_lp, settings, presolved_lp, presolve_info), 0); - - EXPECT_TRUE(presolve_info.removed_lower_bounds.empty()); - EXPECT_TRUE(presolve_info.negated_variables.empty()); - ASSERT_EQ(presolve_info.native_free_linear_indices.size(), 2); - EXPECT_EQ(presolve_info.native_free_linear_indices[0], 0); - EXPECT_EQ(presolve_info.native_free_linear_indices[1], 1); -} - TEST(barrier, uncrush_solution_removes_non_tail_free_variable_partner) { using namespace cuopt::linear_programming::dual_simplex; @@ -492,7 +430,6 @@ TEST(barrier, rejects_middle_cone_input_before_barrier) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = true; settings.dualize = 0; lp_solution_t solution(m, n); @@ -547,7 +484,6 @@ TEST(barrier, socp_min_x0_subject_to_norm_constraint) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = false; settings.dualize = 0; lp_solution_t solution(m, n); @@ -611,7 +547,6 @@ TEST(barrier, mixed_linear_and_soc_block) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = false; settings.dualize = 0; lp_solution_t solution(m, n); @@ -677,7 +612,6 @@ TEST(barrier, mixed_linear_and_soc_tail_coupling) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = false; settings.dualize = 0; settings.scale_columns = true; @@ -744,7 +678,6 @@ TEST(barrier, mixed_linear_and_soc_tail_coupling_with_inequality) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = false; settings.dualize = 0; settings.scale_columns = true; @@ -822,7 +755,6 @@ TEST(barrier, mixed_linear_and_two_soc_blocks) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = false; settings.dualize = 0; lp_solution_t solution(m, n); @@ -903,7 +835,6 @@ TEST(barrier, mixed_linear_and_two_soc_blocks_with_inequality) simplex_solver_settings_t settings; settings.barrier = true; - settings.barrier_presolve = false; settings.dualize = 0; settings.scale_columns = true; From 4654c934f8437e7d92146555c706e6a10249b3d0 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 22 May 2026 05:30:39 -0700 Subject: [PATCH 67/68] Improve post-solve process && Fix broken python API for socp --- cpp/src/dual_simplex/presolve.cpp | 14 +++++--------- .../cuopt/linear_programming/io/parser_wrapper.pyx | 4 ++-- .../test_socp_parser.py | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 73dbb9dcd1..721465d2a1 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -2004,6 +2004,8 @@ void uncrush_solution(const presolve_info_t& presolve_info, settings.log.printf("Post-solve: Correcting duals for %d bounded free variables\n", static_cast(presolve_info.bounded_free_variables.size())); const csc_matrix_t& A = original_problem.A; + csr_matrix_t A_row(A.m, A.n, A.nnz()); + A.to_compressed_row(A_row); // Traverse in reverse order, to ensure that all z_j = 0 after the correction for (auto it = presolve_info.bounded_free_variables.rbegin(); it != presolve_info.bounded_free_variables.rend(); @@ -2013,15 +2015,9 @@ void uncrush_solution(const presolve_info_t& presolve_info, if (w_j == 0.0) { continue; } const f_t du = w_j / bfv.coefficient; input_y[bfv.constraint] += du; - for (i_t j = 0; j < A.n; j++) { - const i_t col_start = A.col_start[j]; - const i_t col_end = A.col_start[j + 1]; - for (i_t p = col_start; p < col_end; p++) { - if (A.i[p] == bfv.constraint) { - input_z[j] -= A.x[p] * du; - break; - } - } + const auto [row_start, row_end] = A_row.get_constraint_range(bfv.constraint); + for (i_t p = row_start; p < row_end; ++p) { + input_z[A_row.j[p]] -= A_row.x[p] * du; } } } diff --git a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx index d33dbc2e98..3cc0e1a110 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx @@ -136,7 +136,7 @@ cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): data_model.set_problem_name(dm.problem_name_.decode()) cdef size_t qi - cdef size_t n_qc = dm_ret.get_quadratic_constraints().size() + cdef size_t n_qc = dm.get_quadratic_constraints().size() cdef mps_data_model_t[int, double].quadratic_constraint_t qc cdef size_t linear_nnz, quadratic_nnz cdef double[:] linear_values_view @@ -146,7 +146,7 @@ cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): cdef int[:] quadratic_col_indices_view for qi in range(n_qc): - qc = dm_ret.get_quadratic_constraints()[qi] + qc = dm.get_quadratic_constraints()[qi] linear_nnz = qc.linear_values.size() if linear_nnz > 0: linear_values_view = qc.linear_values.data() diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py index 4acb964c01..549ef99b7d 100644 --- a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py @@ -10,7 +10,7 @@ import numpy as np import pytest -from cuopt.linear_programming import data_model, mps_parser, solver, solver_settings +from cuopt.linear_programming import data_model, io as mps_parser, solver, solver_settings from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_METHOD, CUOPT_PRESOLVE, From 126c880bbcb1d54727860f68e13a9adb1e03a283 Mon Sep 17 00:00:00 2001 From: yuwenchen95 Date: Fri, 22 May 2026 05:57:01 -0700 Subject: [PATCH 68/68] Remove the unnecessary COO-> CSR -> COO conversion of quadratic constraints --- cpp/src/io/lp_parser.cpp | 47 +++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index bc5aec6743..81ec63f323 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -118,41 +118,42 @@ bool is_free_keyword(std::string_view lower) { return lower == "free"; } bool is_infinity_text(std::string_view lower) { return lower == "inf" || lower == "infinity"; } -// Builds the symmetric Q in CSR from LP-format raw upper-triangular triples. +// Builds the symmetric Q in COO from LP-format raw upper-triangular triples. // Each input triple (i, j, c) with i <= j represents `c * x_i * x_j` in the // LP source. The output Q satisfies x^T Q x = sum of those terms. // Diagonal (i == j): Q[i,i] = c (one entry). // Off-diagonal (i != j): Q[i,j] = Q[j,i] = c/2 (two entries; symmetric split). template -void build_symmetric_q_csr(const std::vector>& raw_triples, +void build_symmetric_q_coo(const std::vector>& raw_triples, i_t n_vars, - std::vector& out_values, - std::vector& out_indices, - std::vector& out_offsets) + std::vector& out_row_indices, + std::vector& out_col_indices, + std::vector& out_values) { - std::vector>> row_data(n_vars); + std::vector>> row_data(static_cast(n_vars)); for (const auto& [i, j, c] : raw_triples) { if (i == j) { - row_data[i].emplace_back(i, c); + row_data[static_cast(i)].emplace_back(i, c); } else { - row_data[i].emplace_back(j, c / f_t(2)); - row_data[j].emplace_back(i, c / f_t(2)); + row_data[static_cast(i)].emplace_back(j, c / f_t(2)); + row_data[static_cast(j)].emplace_back(i, c / f_t(2)); } } for (auto& row : row_data) { std::sort(row.begin(), row.end()); } - out_offsets.clear(); - out_indices.clear(); + out_row_indices.clear(); + out_col_indices.clear(); out_values.clear(); - out_offsets.reserve(static_cast(n_vars) + 1); - out_offsets.push_back(0); + out_row_indices.reserve(raw_triples.size() * 2); + out_col_indices.reserve(raw_triples.size() * 2); + out_values.reserve(raw_triples.size() * 2); for (i_t r = 0; r < n_vars; ++r) { - for (const auto& [col, val] : row_data[r]) { + for (const auto& [col, val] : row_data[static_cast(r)]) { + out_row_indices.push_back(r); + out_col_indices.push_back(col); out_values.push_back(val); - out_indices.push_back(col); } - out_offsets.push_back(static_cast(out_values.size())); } } @@ -1507,18 +1508,10 @@ void flush_quadratic_constraints(mps_data_model_t& problem, const i_t linear_row_count = static_cast(parser.row_names.size()); i_t k = 0; for (const auto& block : parser.quadratic_constraint_blocks) { - std::vector q_values; - std::vector q_col_indices; - std::vector q_offsets; - build_symmetric_q_csr( - block.quad_triples, n_vars, q_values, q_col_indices, q_offsets); std::vector q_row_indices; - q_row_indices.reserve(q_values.size()); - for (i_t r = 0; r < n_vars; ++r) { - for (i_t p = q_offsets[r]; p < q_offsets[r + 1]; ++p) { - q_row_indices.push_back(r); - } - } + std::vector q_col_indices; + std::vector q_values; + build_symmetric_q_coo(block.quad_triples, n_vars, q_row_indices, q_col_indices, q_values); problem.append_quadratic_constraint(linear_row_count + k, block.row_name, static_cast(block.row_type),