From 1e0bd53da23fd9e4c093603d41c3fa6a06e899e4 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 7 May 2026 15:07:26 +0200 Subject: [PATCH 001/258] first commit !! added multi_gpu_partition file to solver settings --- cpp/include/cuopt/linear_programming/constants.h | 1 + .../cuopt/linear_programming/pdlp/solver_settings.hpp | 1 + cpp/src/math_optimization/solver_settings.cu | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index b251b3eaba..7e2682b997 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -77,6 +77,7 @@ #define CUOPT_SOLUTION_FILE "solution_file" #define CUOPT_NUM_CPU_THREADS "num_cpu_threads" #define CUOPT_NUM_GPUS "num_gpus" +#define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" #define CUOPT_PRESOLVE_FILE "presolve_file" #define CUOPT_RANDOM_SEED "random_seed" diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index bcf5a736f0..4585b9d1cf 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -286,6 +286,7 @@ class pdlp_solver_settings_t { presolver_t presolver{presolver_t::Default}; bool dual_postsolve{true}; int num_gpus{1}; + std::string multi_gpu_partition_file{""}; method_t method{method_t::Concurrent}; bool inside_mip{false}; // For concurrent termination diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index b968ad18ea..42ea533152 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -136,7 +136,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_IMPLIED_BOUND_CUTS, &mip_settings.implied_bound_cuts, -1, 1, -1}, {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, - {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, 1, 2, 1}, + {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, 1, 576, 1}, {CUOPT_NUM_GPUS, &mip_settings.num_gpus, 1, 2, 1}, {CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING, &mip_settings.mip_batch_pdlp_strong_branching, 0, 2, 0}, {CUOPT_MIP_BATCH_PDLP_RELIABILITY_BRANCHING, &mip_settings.mip_batch_pdlp_reliability_branching, 0, 2, 0}, @@ -182,7 +182,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_USER_PROBLEM_FILE, &mip_settings.user_problem_file, ""}, {CUOPT_USER_PROBLEM_FILE, &pdlp_settings.user_problem_file, ""}, {CUOPT_PRESOLVE_FILE, &mip_settings.presolve_file, ""}, - {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""} + {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""}, + {CUOPT_MULTI_GPU_PARTITION_FILE, &pdlp_settings.multi_gpu_partition_file, ""}, }; // clang-format on } From 978d17bc5e81f10bb0f4305e5886b777251b4ad4 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 7 May 2026 17:51:27 +0200 Subject: [PATCH 002/258] slowly skeletonning --- .../pdlp/distributed_pdlp/communicator.cuh | 0 cpp/src/pdlp/distributed_pdlp/partition.cuh | 33 +++++++++++++++ cpp/src/pdlp/distributed_pdlp/shard.cu | 41 +++++++++++++++++++ cpp/src/pdlp/distributed_pdlp/shard.cuh | 24 +++++++++++ 4 files changed, 98 insertions(+) create mode 100644 cpp/src/pdlp/distributed_pdlp/communicator.cuh create mode 100644 cpp/src/pdlp/distributed_pdlp/partition.cuh create mode 100644 cpp/src/pdlp/distributed_pdlp/shard.cu create mode 100644 cpp/src/pdlp/distributed_pdlp/shard.cuh diff --git a/cpp/src/pdlp/distributed_pdlp/communicator.cuh b/cpp/src/pdlp/distributed_pdlp/communicator.cuh new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cpp/src/pdlp/distributed_pdlp/partition.cuh b/cpp/src/pdlp/distributed_pdlp/partition.cuh new file mode 100644 index 0000000000..38457029be --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/partition.cuh @@ -0,0 +1,33 @@ + + + +namespace cuopt::linear_programming::detail { + + +template +class partition_t { + public: + partition_t(const std::string& partition_file); + partition_t(const problem_t& op_problem); + + + size_t nb_parts; + + std::vector raw_parts; + std::vector cstr_parts; + std::vector var_parts; + std::vector> owned_cstr_per_part; + std::vector> owned_var_per_part; + std::vector> needed_cstr_per_part; + std::vector> needed_var_per_part; + std::vector>> sent_cstr_per_part; + std::vector>> sent_var_per_part; + std::vector>> received_cstr_per_part; + std::vector>> received_var_per_part; + + private: + void fill_data(); + void validate() const; + +}; +} // namespace cuopt::linear_programming::detail \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu new file mode 100644 index 0000000000..43a4526c29 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -0,0 +1,41 @@ + + + +void pre_SpMV_communication(bool is_A_x){ + // Prepare the send_buffers + for (auto& shard: shards){ + comm_planner_t& plan = is_A_x ? shard.x_plan : shard.y_plan; + raft::device_setter guard(shard.device_id); + for (size_t peer = 0; peer < partition.nb_parts; peer++){ + if (peer == shard.rank) continue; + thrust::gather( + shard.handle.get_thrust_policy(), // TODO what exactly do we put here + plan.send_indices_per_peer[peer].begin(), + plan.send_indices_per_peer[peer].end(), + plan.full_local.begin(), + plan.send_buf_per_peer[peer].begin()); + } + } + // Will merge them if it works + ncclgroupstart() + // Send all the data current shard has to send + for (auto& shard: shards){ + comm_planner_t& plan = is_A_x ? shard.x_plan : shard.y_plan; + raft::device_setter guard(shard.device_id); + for (size_t peer = 0; peer < partition.nb_parts; peer++){ + if (peer == shard.rank) continue; + ncclSend(plan.send_buf_per_peer[peer].data(), plan.nb_elt_send_per_peer[peer], peer) + } + } + // Receive all the data current shard has to receive + for (auto& shard: shards){ + comm_planner_t& plan = is_A_x ? shard.x_plan : shard.y_plan; + raft::device_setter guard(shard.device_id); + for (size_t peer = 0; peer < partition.nb_parts; peer++){ + if (peer == shard.rank) continue; + f_t* recv_buff = &plan.full_local[offset_per_peer[peer]]; + ncclRecv(recv_buff, plan.nb_elt_recv_per_peer[peer], peer); + } + } + ncclgroupend() +} \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cuh b/cpp/src/pdlp/distributed_pdlp/shard.cuh new file mode 100644 index 0000000000..30449e04a0 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/shard.cuh @@ -0,0 +1,24 @@ + + +template +struct pdlp_shard_t { + size_t rank; + comm_planner_t x_plan; + comm_planner_t y_plan; +}; + + +template +struct comm_planner_t { + + // The indices of the data we have to send to the others + // Maybe could merge evrything if it gives a speedup but a bit harder to read + std::vector> send_indices_per_peer; + std::vector nb_elt_send_per_peer; + std::vector> send_buf_per_peer; + + // Where to start writing in full_local for each peer + std::vector offset_per_peer; + std::vector nb_elt_recv_per_peer; + rmm::device_uvector full_local; // The full var/cstr vector containing all local data then all remote data +}; \ No newline at end of file From dd0c0eff2de119511065cb1e40a726c6443fb102 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 7 May 2026 18:02:02 +0200 Subject: [PATCH 003/258] better shard.cuh --- cpp/src/pdlp/distributed_pdlp/shard.cuh | 30 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cuh b/cpp/src/pdlp/distributed_pdlp/shard.cuh index 30449e04a0..6e4f7eabae 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cuh +++ b/cpp/src/pdlp/distributed_pdlp/shard.cuh @@ -1,19 +1,12 @@ -template -struct pdlp_shard_t { - size_t rank; - comm_planner_t x_plan; - comm_planner_t y_plan; -}; - -template +template struct comm_planner_t { // The indices of the data we have to send to the others // Maybe could merge evrything if it gives a speedup but a bit harder to read - std::vector> send_indices_per_peer; + std::vector> send_indices_per_peer; std::vector nb_elt_send_per_peer; std::vector> send_buf_per_peer; @@ -21,4 +14,21 @@ struct comm_planner_t { std::vector offset_per_peer; std::vector nb_elt_recv_per_peer; rmm::device_uvector full_local; // The full var/cstr vector containing all local data then all remote data -}; \ No newline at end of file +}; + +template +struct pdlp_shard_t { + + // Local per-rank PDLP data + raft::handle_t handle; // owned: the actual handle for this shard's device/stream + problem_t local_problem; // owned: holds handle_ptr = &handle (back-ref) + saddle_point_state_t saddle_point; // owned: per-iter state, sized to local + cusparse_view_t cusparse_view; // owned: descriptors bound to local_problem + saddle_point + + // Specific multi-GPU data + int device_id; + ncclComm_t comm; + comm_planner_t x_plan, y_plan; +}; + + From 2037eca41a05ac925d36bd1482c3a1e29b525b49 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 10 May 2026 18:39:30 +0200 Subject: [PATCH 004/258] wip --- cpp/src/pdlp/distributed_pdlp/partition.cu | 24 ++++++++++++++++ cpp/src/pdlp/distributed_pdlp/partition.cuh | 32 ++++++++++++++------- cpp/src/pdlp/distributed_pdlp/shard.cu | 2 +- cpp/src/pdlp/distributed_pdlp/shard.cuh | 32 +++++++++++++++++---- 4 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 cpp/src/pdlp/distributed_pdlp/partition.cu diff --git a/cpp/src/pdlp/distributed_pdlp/partition.cu b/cpp/src/pdlp/distributed_pdlp/partition.cu new file mode 100644 index 0000000000..3410b74fd1 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/partition.cu @@ -0,0 +1,24 @@ + +namespace cuopt::linear_programming::detail { + +template +partition_t::partition_t(const std::string& partition_file){ + +} + +template +partition_t::partition_t(const problem_t& op_problem) +{ + std::cout << "NOT IMPLEMENTED" << std::endl; + return; // TODO: Implement +} + +template +void export_to_file(const std::string& partition_file) const{ + std::cout << "NOT IMPLEMENTED" << std::endl; + return; // TODO: Implement +} + + + +} \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/partition.cuh b/cpp/src/pdlp/distributed_pdlp/partition.cuh index 38457029be..a5b5175105 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition.cuh +++ b/cpp/src/pdlp/distributed_pdlp/partition.cuh @@ -4,26 +4,36 @@ namespace cuopt::linear_programming::detail { +template +struct rank_data_t { + // === Ownership === + std::vector owned_var_indices; // global indices of variables in S_r + std::vector owned_constr_indices; // global indices of constraints in T_r + // === Send plan: per peer, LOCAL positions to gather + send === + std::vector> y_send_per_peer; // [peer] -> local positions in T_r to send + std::vector> x_send_per_peer; // [peer] -> local positions in S_r to send + // === Recv plan: per peer, contiguous slot in halo region === + std::vector y_recv_counts; // [peer] -> count + std::vector y_recv_offsets; // [peer] -> offset in dual halo region + std::vector x_recv_counts; + std::vector x_recv_offsets; +}; + + template class partition_t { - public: - partition_t(const std::string& partition_file); + public: + // not sure, good luck hihi + partition_t(std::vector parts, std::vector A_row_offsets, std::vector A_indices, std::vector A_t_row_offsets, std::vector A_t_indices, ); partition_t(const problem_t& op_problem); - + void export_to_file(const std::string& partition_file) const; size_t nb_parts; std::vector raw_parts; std::vector cstr_parts; std::vector var_parts; - std::vector> owned_cstr_per_part; - std::vector> owned_var_per_part; - std::vector> needed_cstr_per_part; - std::vector> needed_var_per_part; - std::vector>> sent_cstr_per_part; - std::vector>> sent_var_per_part; - std::vector>> received_cstr_per_part; - std::vector>> received_var_per_part; + std::vector> rank_data; // [rank] -> partition data for this rank private: void fill_data(); diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 43a4526c29..6de93ad3b8 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -17,7 +17,7 @@ void pre_SpMV_communication(bool is_A_x){ } } // Will merge them if it works - ncclgroupstart() + ncclgroupstart(); // Send all the data current shard has to send for (auto& shard: shards){ comm_planner_t& plan = is_A_x ? shard.x_plan : shard.y_plan; diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cuh b/cpp/src/pdlp/distributed_pdlp/shard.cuh index 6e4f7eabae..127cc496f1 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cuh +++ b/cpp/src/pdlp/distributed_pdlp/shard.cuh @@ -3,7 +3,6 @@ template struct comm_planner_t { - // The indices of the data we have to send to the others // Maybe could merge evrything if it gives a speedup but a bit harder to read std::vector> send_indices_per_peer; @@ -19,16 +18,39 @@ struct comm_planner_t { template struct pdlp_shard_t { + // Specific multi-GPU data + int device_id; + ncclComm_t comm; + comm_planner_t x_plan, y_plan; + // Local per-rank PDLP data raft::handle_t handle; // owned: the actual handle for this shard's device/stream problem_t local_problem; // owned: holds handle_ptr = &handle (back-ref) saddle_point_state_t saddle_point; // owned: per-iter state, sized to local cusparse_view_t cusparse_view; // owned: descriptors bound to local_problem + saddle_point - // Specific multi-GPU data - int device_id; - ncclComm_t comm; - comm_planner_t x_plan, y_plan; + rmm::device_uvector tmp_primal; + rmm::device_uvector tmp_dual; + rmm::device_uvector potential_next_primal; + rmm::device_uvector potential_next_dual; + rmm::device_uvector dual_slack; + rmm::device_uvector reflected_primal; // x, so it has primal_size + halo + rmm::device_uvector reflected_dual; // y, so it has dual_size + halo + + rmm::device_scalar reusable_one; // = 1.0 + rmm::device_scalar reusable_zero; // = 0.0 + rmm::device_scalar reusable_neg_one; // = -1.0 + + // ===== Missing for cuPDLP+ Halpern update ===== + rmm::device_uvector initial_primal; // snapshot at start of restart epoch + rmm::device_uvector initial_dual; + + i_t primal_size_h; + i_t dual_size_h; + i_t primal_halo_size; + i_t dual_halo_size; + i_t full_primal_size_h;// = primal_size_h + primal_halo_size + i_t full_dual_size_h; // = dual_size_h + dual_halo_size }; From 0f62eff269ce7ab5c7f5b2141c5178abeb61ec2c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 18 May 2026 18:06:27 +0200 Subject: [PATCH 005/258] added a bit of skeleton. Forward declared pdlp_solver in shard.hpp, the cycle seems to be fixed, cuopt compiles --- .../pdlp/distributed_pdlp/communicator.cuh | 0 .../distributed_pdlp/multi_gpu_engine.hpp | 14 +++++ cpp/src/pdlp/distributed_pdlp/partition.cu | 24 -------- cpp/src/pdlp/distributed_pdlp/partition.cuh | 43 -------------- .../distributed_pdlp/partition_loader.hpp | 2 + cpp/src/pdlp/distributed_pdlp/rank_data.hpp | 52 +++++++++++++++++ cpp/src/pdlp/distributed_pdlp/shard.cu | 54 ++++++------------ cpp/src/pdlp/distributed_pdlp/shard.cuh | 56 ------------------- cpp/src/pdlp/distributed_pdlp/shard.hpp | 31 ++++++++++ cpp/src/pdlp/pdlp.cuh | 4 ++ 10 files changed, 119 insertions(+), 161 deletions(-) delete mode 100644 cpp/src/pdlp/distributed_pdlp/communicator.cuh create mode 100644 cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp delete mode 100644 cpp/src/pdlp/distributed_pdlp/partition.cu delete mode 100644 cpp/src/pdlp/distributed_pdlp/partition.cuh create mode 100644 cpp/src/pdlp/distributed_pdlp/partition_loader.hpp create mode 100644 cpp/src/pdlp/distributed_pdlp/rank_data.hpp delete mode 100644 cpp/src/pdlp/distributed_pdlp/shard.cuh create mode 100644 cpp/src/pdlp/distributed_pdlp/shard.hpp diff --git a/cpp/src/pdlp/distributed_pdlp/communicator.cuh b/cpp/src/pdlp/distributed_pdlp/communicator.cuh deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp new file mode 100644 index 0000000000..13ded70009 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +namespace cuopt::linear_programming::detail { + +template +struct multi_gpu_engine_t { + std::vector> shards; +}; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partition.cu b/cpp/src/pdlp/distributed_pdlp/partition.cu deleted file mode 100644 index 3410b74fd1..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/partition.cu +++ /dev/null @@ -1,24 +0,0 @@ - -namespace cuopt::linear_programming::detail { - -template -partition_t::partition_t(const std::string& partition_file){ - -} - -template -partition_t::partition_t(const problem_t& op_problem) -{ - std::cout << "NOT IMPLEMENTED" << std::endl; - return; // TODO: Implement -} - -template -void export_to_file(const std::string& partition_file) const{ - std::cout << "NOT IMPLEMENTED" << std::endl; - return; // TODO: Implement -} - - - -} \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/partition.cuh b/cpp/src/pdlp/distributed_pdlp/partition.cuh deleted file mode 100644 index a5b5175105..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/partition.cuh +++ /dev/null @@ -1,43 +0,0 @@ - - - -namespace cuopt::linear_programming::detail { - - -template -struct rank_data_t { - // === Ownership === - std::vector owned_var_indices; // global indices of variables in S_r - std::vector owned_constr_indices; // global indices of constraints in T_r - // === Send plan: per peer, LOCAL positions to gather + send === - std::vector> y_send_per_peer; // [peer] -> local positions in T_r to send - std::vector> x_send_per_peer; // [peer] -> local positions in S_r to send - // === Recv plan: per peer, contiguous slot in halo region === - std::vector y_recv_counts; // [peer] -> count - std::vector y_recv_offsets; // [peer] -> offset in dual halo region - std::vector x_recv_counts; - std::vector x_recv_offsets; -}; - - -template -class partition_t { - public: - // not sure, good luck hihi - partition_t(std::vector parts, std::vector A_row_offsets, std::vector A_indices, std::vector A_t_row_offsets, std::vector A_t_indices, ); - partition_t(const problem_t& op_problem); - void export_to_file(const std::string& partition_file) const; - - size_t nb_parts; - - std::vector raw_parts; - std::vector cstr_parts; - std::vector var_parts; - std::vector> rank_data; // [rank] -> partition data for this rank - - private: - void fill_data(); - void validate() const; - -}; -} // namespace cuopt::linear_programming::detail \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -0,0 +1,2 @@ + + diff --git a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp new file mode 100644 index 0000000000..ee107f5cf1 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +namespace cuopt::linear_programming::detail { +template +struct rank_data_t { + rank_data_t(std::size_t nb_parts) + : var_send_per_peer(nb_parts), + cstr_send_per_peer(nb_parts), + var_recv_counts(nb_parts, 0), + var_recv_offsets(nb_parts, 0), + cstr_recv_counts(nb_parts, 0), + cstr_recv_offsets(nb_parts, 0) {} + + i_t owned_var_size{0}; + i_t total_var_size{0}; + i_t owned_cstr_size{0}; + i_t total_cstr_size{0}; + + // === Ownership === + std::vector owned_var_indices; + std::vector owned_cstr_indices; + + // === Send plan: per peer, indices to gather + send === + std::vector> var_send_per_peer; + std::vector> cstr_send_per_peer; + + // === Recv plan: per peer, contiguous slot in halo region === + std::vector var_recv_counts; + std::vector var_recv_offsets; + std::vector cstr_recv_counts; + std::vector cstr_recv_offsets; + + // === Mappings === + std::unordered_map global_to_local_var; + std::unordered_map global_to_local_cstr; + std::vector local_to_global_var; + std::vector local_to_global_cstr; + + // === Local host CSR matrices === + // A + std::vector h_A_row_offsets; + std::vector h_A_col_indices; + std::vector h_A_values; + // A_t + std::vector h_A_t_row_offsets; + std::vector h_A_t_col_indices; + std::vector h_A_t_values; + }; +} // namespace cuopt::linear_programming::detail \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 6de93ad3b8..b7e176c3ee 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -1,41 +1,19 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +namespace cuopt::linear_programming::detail { +// This must be done in .cu file because the pdlp_solver_t is not already complete in the hpp file +template +pdlp_shard_t::~pdlp_shard_t() = default; -void pre_SpMV_communication(bool is_A_x){ - // Prepare the send_buffers - for (auto& shard: shards){ - comm_planner_t& plan = is_A_x ? shard.x_plan : shard.y_plan; - raft::device_setter guard(shard.device_id); - for (size_t peer = 0; peer < partition.nb_parts; peer++){ - if (peer == shard.rank) continue; - thrust::gather( - shard.handle.get_thrust_policy(), // TODO what exactly do we put here - plan.send_indices_per_peer[peer].begin(), - plan.send_indices_per_peer[peer].end(), - plan.full_local.begin(), - plan.send_buf_per_peer[peer].begin()); - } - } - // Will merge them if it works - ncclgroupstart(); - // Send all the data current shard has to send - for (auto& shard: shards){ - comm_planner_t& plan = is_A_x ? shard.x_plan : shard.y_plan; - raft::device_setter guard(shard.device_id); - for (size_t peer = 0; peer < partition.nb_parts; peer++){ - if (peer == shard.rank) continue; - ncclSend(plan.send_buf_per_peer[peer].data(), plan.nb_elt_send_per_peer[peer], peer) - } - } - // Receive all the data current shard has to receive - for (auto& shard: shards){ - comm_planner_t& plan = is_A_x ? shard.x_plan : shard.y_plan; - raft::device_setter guard(shard.device_id); - for (size_t peer = 0; peer < partition.nb_parts; peer++){ - if (peer == shard.rank) continue; - f_t* recv_buff = &plan.full_local[offset_per_peer[peer]]; - ncclRecv(recv_buff, plan.nb_elt_recv_per_peer[peer], peer); - } - } - ncclgroupend() -} \ No newline at end of file + + + +template struct pdlp_shard_t; +//template struct pdlp_shard_t; +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cuh b/cpp/src/pdlp/distributed_pdlp/shard.cuh deleted file mode 100644 index 127cc496f1..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/shard.cuh +++ /dev/null @@ -1,56 +0,0 @@ - - - -template -struct comm_planner_t { - // The indices of the data we have to send to the others - // Maybe could merge evrything if it gives a speedup but a bit harder to read - std::vector> send_indices_per_peer; - std::vector nb_elt_send_per_peer; - std::vector> send_buf_per_peer; - - // Where to start writing in full_local for each peer - std::vector offset_per_peer; - std::vector nb_elt_recv_per_peer; - rmm::device_uvector full_local; // The full var/cstr vector containing all local data then all remote data -}; - -template -struct pdlp_shard_t { - - // Specific multi-GPU data - int device_id; - ncclComm_t comm; - comm_planner_t x_plan, y_plan; - - // Local per-rank PDLP data - raft::handle_t handle; // owned: the actual handle for this shard's device/stream - problem_t local_problem; // owned: holds handle_ptr = &handle (back-ref) - saddle_point_state_t saddle_point; // owned: per-iter state, sized to local - cusparse_view_t cusparse_view; // owned: descriptors bound to local_problem + saddle_point - - rmm::device_uvector tmp_primal; - rmm::device_uvector tmp_dual; - rmm::device_uvector potential_next_primal; - rmm::device_uvector potential_next_dual; - rmm::device_uvector dual_slack; - rmm::device_uvector reflected_primal; // x, so it has primal_size + halo - rmm::device_uvector reflected_dual; // y, so it has dual_size + halo - - rmm::device_scalar reusable_one; // = 1.0 - rmm::device_scalar reusable_zero; // = 0.0 - rmm::device_scalar reusable_neg_one; // = -1.0 - - // ===== Missing for cuPDLP+ Halpern update ===== - rmm::device_uvector initial_primal; // snapshot at start of restart epoch - rmm::device_uvector initial_dual; - - i_t primal_size_h; - i_t dual_size_h; - i_t primal_halo_size; - i_t dual_halo_size; - i_t full_primal_size_h;// = primal_size_h + primal_halo_size - i_t full_dual_size_h; // = dual_size_h + dual_halo_size -}; - - diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp new file mode 100644 index 0000000000..0fe57be974 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include +namespace cuopt::linear_programming::detail { + +template +class pdlp_solver_t; + +template +class pdlp_shard_t { + // Declaration only, will be set as default in shard.cu . Needed to manage cyclic include of pdlp_solver_t. + public: + ~pdlp_shard_t(); + pdlp_shard_t(int device_id, + rank_data_t&& rd, + ncclComm_t comm + /* ???????? */); + + pdlp_shard_t(const pdlp_shard_t&) = delete; + pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; // Specific multi-GPU data + int device_id; + raft::handle_t handle; + ncclComm_t comm; + rank_data_t rank_data; + + std::unique_ptr> sub_pdlp; +}; + +} diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index d03430f150..5cb267730f 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,7 @@ #include #include +#include "distributed_pdlp/multi_gpu_engine.hpp" namespace cuopt::linear_programming::detail { /** @@ -237,6 +239,8 @@ class pdlp_solver_t { primal_quality_adapter_t best_primal_quality_so_far_; // Flag to indicate if solver is being called from MIP. No logging is done in this case. bool inside_mip_{false}; + + multi_gpu_engine_t multi_gpu_engine; }; } // namespace cuopt::linear_programming::detail From d89c85a9af1303ae12641a868a9cb83d64c32aee Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 19 May 2026 13:49:37 +0200 Subject: [PATCH 006/258] still wip but going well --- .../pdlp/pdlp_hyper_params.cuh | 1 + cpp/src/pdlp/CMakeLists.txt | 3 + .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 73 +++++++ .../distributed_pdlp/multi_gpu_engine.hpp | 61 ++++-- .../pdlp/distributed_pdlp/partition_loader.cu | 178 ++++++++++++++++++ .../distributed_pdlp/partition_loader.hpp | 14 ++ cpp/src/pdlp/distributed_pdlp/shard.cu | 101 +++++++++- cpp/src/pdlp/distributed_pdlp/shard.hpp | 21 ++- cpp/src/pdlp/pdlp.cu | 97 +++++++++- cpp/src/pdlp/pdlp.cuh | 10 +- cpp/src/pdlp/solve.cu | 11 ++ 11 files changed, 551 insertions(+), 19 deletions(-) create mode 100644 cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu create mode 100644 cpp/src/pdlp/distributed_pdlp/partition_loader.cu diff --git a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh index 282e91d7ef..962f06ee4a 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh +++ b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh @@ -47,6 +47,7 @@ struct pdlp_hyper_params_t { bool bound_objective_rescaling = true; bool use_reflected_primal_dual = true; bool use_fixed_point_error = true; + bool use_distributed_pdlp = false; double reflection_coefficient = 1.0; double restart_k_p = 0.99; double restart_k_i = 0.01; diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index f5f26837b6..2bc2771c91 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -29,6 +29,9 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/termination_strategy/convergence_information.cu ${CMAKE_CURRENT_SOURCE_DIR}/optimal_batch_size_handler/optimal_batch_size_handler.cu ${CMAKE_CURRENT_SOURCE_DIR}/utilities/ping_pong_graph.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/shard.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/multi_gpu_engine.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu ) # C and Python adapter files diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu new file mode 100644 index 0000000000..c7307c46ee --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + + #include + + #include + + #include + + #include + + #include + + namespace cuopt::linear_programming::detail { + + template + multi_gpu_engine_t::multi_gpu_engine_t( + std::vector>&& rank_data, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& sub_solver_settings) + : stream() + { + const int nb_parts = static_cast(rank_data.size()); + cuopt_expects(nb_parts > 0, + error_type_t::ValidationError, + "multi_gpu_engine_t: rank_data must be non-empty"); + + shards.reserve(nb_parts); + + // 1:1 rank -> device mapping. (Matches metis_tests; refine later if needed.) + std::vector devices(nb_parts); + std::iota(devices.begin(), devices.end(), 0); + + // 2. Collectively bootstrap NCCL communicators across all devices. + // Must be done together; each comm is then handed to one shard, + // which wraps it in a unique_ptr with the device-aware deleter. + std::vector raw_comms(nb_parts); + cuopt_expects(ncclCommInitAll(raw_comms.data(), nb_parts, devices.data()) == ncclSuccess, + error_type_t::RuntimeError, + "ncclCommInitAll failed"); + + // 3. Construct one shard per rank, pinned to its device. + for (int r = 0; r < nb_parts; ++r) { + raft::device_setter guard(devices[r]); // shard ctor asserts current device + shards.emplace_back(std::make_unique>( + devices[r], + std::move(rank_data[r]), + raw_comms[r], + h_global_obj, + h_global_var_lower, + h_global_var_upper, + h_global_cstr_lower, + h_global_cstr_upper, + maximize, + objective_offset, + objective_scaling_factor, + sub_solver_settings)); + } + } + + template struct multi_gpu_engine_t; + // template struct multi_gpu_engine_t; + + } // namespace cuopt::linear_programming::detail \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 13ded70009..6142c938e3 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -1,14 +1,49 @@ -#pragma once +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + #pragma once -#include - -#include - -namespace cuopt::linear_programming::detail { - -template -struct multi_gpu_engine_t { - std::vector> shards; -}; - -} // namespace cuopt::linear_programming::detail + #include + #include + + #include + + #include + + #include + #include + + namespace cuopt::linear_programming::detail { + + template + struct multi_gpu_engine_t { + // Constructs one shard per partition. Caller is responsible for: + // - rank_data[i] being correctly populated for rank i + // - the host vectors holding the (already scaled) global problem data + // - sub_solver_settings being the per-shard PDLP config (num_gpus=1, + // multi_gpu_partition_file="", scaling disabled). + multi_gpu_engine_t( + std::vector>&& rank_data, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& sub_solver_settings); + + multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; + multi_gpu_engine_t& operator=(const multi_gpu_engine_t&) = delete; + + // Engine-level stream for fork/join orchestration (master side). + rmm::cuda_stream stream; + + // Shards stored by unique_ptr because pdlp_shard_t is immovable + // (owns device-affine resources: handle, NCCL comm, RMM buffers). + std::vector>> shards; + }; + + } // namespace cuopt::linear_programming::detail \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu new file mode 100644 index 0000000000..449e8640ab --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -0,0 +1,178 @@ +static std::vector parse_distributed_pdlp_partition_file(std::string file){ + //returns a vector with all the values separated by a \n +} + +std::vector create_rank_data_from_parts(const std::vector& parts, + const std::vector& A_row_offsets, + const std::vector& A_col_indices, + const std::vector& A_values, + const std::vector& A_t_row_offsets, + const std::vector& A_t_col_indices, + const std::vector& A_t_values, + i_t nb_parts, + i_t nb_cstr, + i_t nb_vars, + i_t nnz) +{ +std::vector rank_data(nb_parts, rank_data_t(nb_parts)); +std::vector cstr_parts(parts.begin(), parts.begin() + nb_cstr); +std::vector var_parts(parts.begin() + nb_cstr, parts.begin() + nb_cstr + nb_vars); + +// 1. Compute ownership +for (i_t i = 0; i < nb_cstr; i++) { +rank_data[cstr_parts[i]].owned_cstr_indices.push_back(i); +} +for (i_t i = 0; i < nb_vars; i++) { +rank_data[var_parts[i]].owned_var_indices.push_back(i); +} + +// 2. Compute local matrices and rank_data +for (i_t rank = 0; rank < nb_parts; rank++) { +auto& rd = rank_data[rank]; +rd.owned_var_size = rd.owned_var_indices.size(); +rd.owned_cstr_size = rd.owned_cstr_indices.size(); +// ---- A side ---- +std::vector local_A_row_offsets; +std::vector local_A_col_indices; +std::vector local_A_values; + +i_t local_A_nnz = 0; +local_A_row_offsets.push_back(local_A_nnz); + +// For each owned constraint, build local matrix A +for (auto owned_cstr : rd.owned_cstr_indices) { +i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; +i_t row_start = A_row_offsets[owned_cstr]; +for (i_t v = 0; v < cstr_len; v++) { +local_A_col_indices.push_back(A_col_indices[row_start + v]); +local_A_values.push_back(A_values[row_start + v]); +} +local_A_nnz += cstr_len; +local_A_row_offsets.push_back(local_A_nnz); +} + +std::set needed_vars; +for (auto indice : local_A_col_indices) { +if (var_parts[indice] != rank) +needed_vars.insert(indice); +} + +for (i_t peer = 0; peer < nb_parts; peer++) { +std::vector needed_var_from_peer; +for (auto needed_var : needed_vars) { +if (var_parts[needed_var] == peer) +needed_var_from_peer.push_back(needed_var); +} +i_t nb_recv_from_peer = needed_var_from_peer.size(); +rd.var_recv_counts[peer] = nb_recv_from_peer; +rd.var_recv_offsets[peer] = +peer == 0 +? 0 +: rd.var_recv_offsets[peer - 1] + rd.var_recv_counts[peer - 1]; +rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer); +} + +rd.h_A_row_offsets = std::move(local_A_row_offsets); +rd.h_A_col_indices = std::move(local_A_col_indices); +rd.h_A_values = std::move(local_A_values); + +// ---- A_t side ---- +std::vector local_A_t_row_offsets; +std::vector local_A_t_col_indices; +std::vector local_A_t_values; +i_t local_A_t_nnz = 0; +local_A_t_row_offsets.push_back(local_A_t_nnz); + +for (auto owned_var : rd.owned_var_indices) { +i_t var_len = A_t_row_offsets[owned_var + 1] - A_t_row_offsets[owned_var]; +i_t row_start = A_t_row_offsets[owned_var]; +for (i_t v = 0; v < var_len; v++) { +local_A_t_col_indices.push_back(A_t_col_indices[row_start + v]); +local_A_t_values.push_back(A_t_values[row_start + v]); +} +local_A_t_nnz += var_len; +local_A_t_row_offsets.push_back(local_A_t_nnz); +} + +std::set needed_cstrs; +for (auto indice : local_A_t_col_indices) { +if (cstr_parts[indice] != rank) +needed_cstrs.insert(indice); +} + +for (i_t peer = 0; peer < nb_parts; peer++) { +std::vector needed_cstr_from_peer; +for (auto needed_cstr : needed_cstrs) { +if (cstr_parts[needed_cstr] == peer) +needed_cstr_from_peer.push_back(needed_cstr); +} +i_t nb_recv_from_peer = needed_cstr_from_peer.size(); +rd.cstr_recv_counts[peer] = nb_recv_from_peer; +rd.cstr_recv_offsets[peer] = +peer == 0 +? 0 +: rd.cstr_recv_offsets[peer - 1] + rd.cstr_recv_counts[peer - 1]; +rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer); +} + +rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); +rd.h_A_t_col_indices = std::move(local_A_t_col_indices); +rd.h_A_t_values = std::move(local_A_t_values); + +rd.total_var_size = rd.owned_var_size + needed_vars.size(); +rd.total_cstr_size = rd.owned_cstr_size + needed_cstrs.size(); +} + +// 3. Generate local indices for contiguous [[self], [peer1], ..., [peer_k]] +// Build scatter_gather_maps +for (i_t rank = 0; rank < nb_parts; rank++) { +auto& rd = rank_data[rank]; + +i_t curr_id = 0; +for (auto owned_cstr : rd.owned_cstr_indices) { +rd.global_to_local_cstr[owned_cstr] = curr_id; +rd.local_to_global_cstr.push_back(owned_cstr); +curr_id++; +} +for (i_t peer = 0; peer < nb_parts; peer++) { +if (peer == rank) continue; +for (auto recv_cstr : rank_data[peer].cstr_send_per_peer[rank]) { +rd.global_to_local_cstr[recv_cstr] = curr_id; +// rd.local_to_global_cstr.push_back(recv_cstr); // Not needed, we only do local_to_global on owned side +curr_id++; +} +} + +curr_id = 0; +for (auto owned_var : rd.owned_var_indices) { +rd.global_to_local_var[owned_var] = curr_id; +rd.local_to_global_var.push_back(owned_var); +curr_id++; +} +for (i_t peer = 0; peer < nb_parts; peer++) { +if (peer == rank) continue; +for (auto recv_var : rank_data[peer].var_send_per_peer[rank]) { +rd.global_to_local_var[recv_var] = curr_id; +// rd.local_to_global_var.push_back(recv_var); // same as over +curr_id++; +} +} +} + +// 4. Remap global -> local everywhere +for (i_t rank = 0; rank < nb_parts; rank++) { +auto& rd = rank_data[rank]; + +for (auto& send_vec : rd.var_send_per_peer) { +for (auto& v : send_vec) v = rd.global_to_local_var.at(v); +} +for (auto& send_vec : rd.cstr_send_per_peer) { +for (auto& v : send_vec) v = rd.global_to_local_cstr.at(v); +} + +for (auto& v : rd.h_A_col_indices) v = rd.global_to_local_var.at(v); +for (auto& v : rd.h_A_t_col_indices) v = rd.global_to_local_cstr.at(v); +} + +return rank_data; +} diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index 139597f9cb..4d66d4445c 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -1,2 +1,16 @@ +partition_loader_t { + static std::vector parse_distributed_pdlp_partition_file(std::string file); + std::vector create_rank_data_from_parts(const std::vector& parts, + const std::vector& A_row_offsets, + const std::vector& A_col_indices, + const std::vector& A_values, + const std::vector& A_t_row_offsets, + const std::vector& A_t_col_indices, + const std::vector& A_t_values, + i_t nb_parts, + i_t nb_cstr, + i_t nb_vars, + i_t nnz); +} \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index b7e176c3ee..d5e795bb61 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -5,15 +5,114 @@ #include #include + +#include +#include + +#include +#include + namespace cuopt::linear_programming::detail { // This must be done in .cu file because the pdlp_solver_t is not already complete in the hpp file template pdlp_shard_t::~pdlp_shard_t() = default; +template +pdlp_shard_t::pdlp_shard_t( + int device_id, + rank_data_t&& rd, + ncclComm_t raw_comm, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& settings) + : device_id(device_id), + stream(), + handle(stream.view()), + comm(raw_comm, nccl_comm_deleter_t{device_id}), + rank_data(std::move(rd)), + opt_problem(std::nullopt), + sub_problem(std::nullopt), + sub_pdlp(nullptr) +{ + assert(raft::device_setter::get_current_device() == device_id && "Right device must be set before building the shard"); + + // ---- 1. Gather per-shard host slices using rank_data's index maps. ---- + // All vectors are sized to TOTAL (owned + halo). Owned slots get real + // values; halo slots keep neutral defaults so they are no-ops even if + // accidentally touched before `owned_*_size_` plumbing is in place. + std::vector h_obj (rank_data.total_var_size, f_t{0}); + std::vector h_var_lower (rank_data.total_var_size, -std::numeric_limits::infinity()); + std::vector h_var_upper (rank_data.total_var_size, std::numeric_limits::infinity()); + std::vector h_cstr_lower(rank_data.total_cstr_size, -std::numeric_limits::infinity()); + std::vector h_cstr_upper(rank_data.total_cstr_size, std::numeric_limits::infinity()); + + for (i_t i = 0; i < rank_data.owned_var_size; ++i) { + const auto g = rank_data.local_to_global_var[i]; + h_obj[i] = h_global_obj[g]; + h_var_lower[i] = h_global_var_lower[g]; + h_var_upper[i] = h_global_var_upper[g]; + } + for (i_t i = 0; i < rank_data.owned_cstr_size; ++i) { + const auto g = rank_data.local_to_global_cstr[i]; + h_cstr_lower[i] = h_global_cstr_lower[g]; + h_cstr_upper[i] = h_global_cstr_upper[g]; + } + // ---- 2. Build optimization_problem_t on this shard's device. ---- + opt_problem.emplace(&handle); + opt_problem->set_csr_constraint_matrix( + rank_data.h_A_values .data(), static_cast(rank_data.h_A_values .size()), + rank_data.h_A_col_indices.data(), static_cast(rank_data.h_A_col_indices.size()), + rank_data.h_A_row_offsets.data(), static_cast(rank_data.h_A_row_offsets.size())); + // Primal axis: TOTAL (owned + halo). Halo slots have neutral defaults. + opt_problem->set_objective_coefficients(h_obj .data(), rank_data.total_var_size); + opt_problem->set_variable_lower_bounds (h_var_lower.data(), rank_data.total_var_size); + opt_problem->set_variable_upper_bounds (h_var_upper.data(), rank_data.total_var_size); + + // Dual axis: TOTAL (owned + halo). Halo slots have ±inf so trivially satisfied. + opt_problem->set_constraint_lower_bounds(h_cstr_lower.data(), rank_data.total_cstr_size); + opt_problem->set_constraint_upper_bounds(h_cstr_upper.data(), rank_data.total_cstr_size); + + opt_problem->set_maximize(maximize); + opt_problem->set_objective_offset(objective_offset); + opt_problem->set_objective_scaling_factor(objective_scaling_factor); + opt_problem->set_problem_category(problem_category_t::LP); + + // ---- 3. Build problem_t from opt_problem. ---- + sub_problem.emplace(*opt_problem); + + // ---- 4. Override reverse_* with the real local A_T from rank_data. ---- + // problem_t's ctor computes the transpose of the LOCAL A, which is wrong + // in multi-GPU: A_local is owned_cstr x total_var, and A_t_local is the + // pre-sliced owned_var x total_cstr matrix we built during partitioning. + auto stream_view = handle.get_stream(); + sub_problem->reverse_offsets .resize(rank_data.h_A_t_row_offsets.size(), stream_view); + sub_problem->reverse_constraints .resize(rank_data.h_A_t_col_indices.size(), stream_view); + sub_problem->reverse_coefficients.resize(rank_data.h_A_t_values .size(), stream_view); + raft::copy(sub_problem->reverse_offsets.data(), + rank_data.h_A_t_row_offsets.data(), + rank_data.h_A_t_row_offsets.size(), stream_view); + raft::copy(sub_problem->reverse_constraints.data(), + rank_data.h_A_t_col_indices.data(), + rank_data.h_A_t_col_indices.size(), stream_view); + raft::copy(sub_problem->reverse_coefficients.data(), + rank_data.h_A_t_values.data(), + rank_data.h_A_t_values.size(), stream_view); + handle.sync_stream(stream_view); + + // ---- 5. Build sub_pdlp (single-GPU mode; multi_gpu flags cleared by caller). ---- + sub_pdlp = std::make_unique>(*sub_problem, settings, /*batch=*/false); +} template struct pdlp_shard_t; -//template struct pdlp_shard_t; +// template struct pdlp_shard_t; + } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 0fe57be974..7528c35dec 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -8,6 +8,18 @@ namespace cuopt::linear_programming::detail { template class pdlp_solver_t; +struct nccl_comm_deleter_t { + int device_id{-1}; + void operator()(ncclComm* comm) const noexcept + { + raft::device_setter guard(device_id); + if (comm != nullptr) { + ncclCommDestroy(comm); + } + } +}; +using nccl_comm_unique_ptr_t = std::unique_ptr; + template class pdlp_shard_t { // Declaration only, will be set as default in shard.cu . Needed to manage cyclic include of pdlp_solver_t. @@ -19,12 +31,15 @@ class pdlp_shard_t { /* ???????? */); pdlp_shard_t(const pdlp_shard_t&) = delete; - pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; // Specific multi-GPU data + pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; + // Specific multi-GPU data int device_id; + rmm::cuda_stream stream; raft::handle_t handle; - ncclComm_t comm; + nccl_comm_unique_ptr_t comm; rank_data_t rank_data; - + optimization_problem_t opt_problem; + problem_t sub_problem; std::unique_ptr> sub_pdlp; }; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index a759887fc5..a58ae4f210 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -11,12 +11,14 @@ #include #include +#include #include #include #include #include #include "cuopt/linear_programming/pdlp/solver_solution.hpp" +#include "distributed_pdlp/multi_gpu_engine.hpp" #include #include @@ -314,6 +316,95 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, } } +template +pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, + pdlp_solver_settings_t const& settings, + int num_gpus) + // 1. Delegate to single-GPU ctor to bring up all the per-master state + // (problem_ptr, op_problem_scaled_, pdhg_solver_, strategies, etc.). + : pdlp_solver_t(op_problem, settings, false) +{ + cuopt_expects(num_gpus == settings.num_gpus && settings.num_gpus > 1, + error_type_t::ValidationError, + "This constructor should only be used for distributed PDLP (num_gpus > 1)"); + // 2. Load partition + std::vector parts; + if (!settings.multi_gpu_partition_file.empty()) { + parts = partition_loader_t::parse_distributed_pdlp_partition_file( + settings.multi_gpu_partition_file); + } else { + cuopt_expects(false, error_type_t::NotImplemented, + "Metis partitioning inside cuopt not implemented yet; " + "provide a --parts file via settings.multi_gpu_partition_file"); + } + // 3. Scale now before copying to children + initial_scaling_strategy_.scale_problem(); + + // 4. Copy the scaled global problem from device -> host. + auto const stream = op_problem_scaled_.handle_ptr->get_stream(); + i_t const n_cstr = op_problem_scaled_.n_constraints; + i_t const n_vars = op_problem_scaled_.n_variables; + i_t const nnz = op_problem_scaled_.nnz; + // CSRs (A and A_t). + std::vector h_A_row_offsets (n_cstr + 1); + std::vector h_A_col_indices (nnz); + std::vector h_A_values (nnz); + std::vector h_A_t_row_offsets(n_vars + 1); + std::vector h_A_t_col_indices(nnz); + std::vector h_A_t_values (nnz); + raft::copy(h_A_row_offsets .data(), op_problem_scaled_.offsets .data(), n_cstr + 1, stream); + raft::copy(h_A_col_indices .data(), op_problem_scaled_.variables .data(), nnz, stream); + raft::copy(h_A_values .data(), op_problem_scaled_.coefficients .data(), nnz, stream); + raft::copy(h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets .data(), n_vars + 1, stream); + raft::copy(h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints .data(), nnz, stream); + raft::copy(h_A_t_values .data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); + // Objective coefficients. + std::vector h_obj(n_vars); + raft::copy(h_obj.data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); + // Variable bounds: stored interleaved as f_t2 {lower, upper}. Unpack into two host vectors. + using f_t2 = typename type_2::type; + std::vector h_var_bounds_packed(n_vars); + raft::copy(h_var_bounds_packed.data(), + op_problem_scaled_.variable_bounds.data(), n_vars, stream); + // Constraint bounds. + std::vector h_cstr_lower(n_cstr); + std::vector h_cstr_upper(n_cstr); + raft::copy(h_cstr_lower.data(), op_problem_scaled_.constraint_lower_bounds.data(), n_cstr, stream); + raft::copy(h_cstr_upper.data(), op_problem_scaled_.constraint_upper_bounds.data(), n_cstr, stream); + op_problem_scaled_.handle_ptr->sync_stream(stream); + + std::vector h_var_lower(n_vars), h_var_upper(n_vars); + for (i_t i = 0; i < n_vars; ++i) { + h_var_lower[i] = h_var_bounds_packed[i].x; + h_var_upper[i] = h_var_bounds_packed[i].y; + } + // 5. Build per-rank data and meta-data + std::vector> sub_pdlp_rank_data = + partition_loader_t::create_rank_data_from_parts( + parts, + h_A_row_offsets, h_A_col_indices, h_A_values, + h_A_t_row_offsets, h_A_t_col_indices, h_A_t_values, + settings.num_gpus, n_cstr, n_vars, nnz); + // 6. Build the per-shard PDLP settings: + // - single-GPU mode (num_gpus=1, no partition file) so sub-solvers don't recurse; + // - disable scaling (master already scaled the data we're handing out). + pdlp_solver_settings_t sub_pdlp_settings = settings; + sub_pdlp_settings.num_gpus = 1; + sub_pdlp_settings.multi_gpu_partition_file = ""; + sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; + sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; + + // 7. Construct the engine — this collectively bootstraps NCCL across all GPUs + // and constructs one shard per partition with the right slice of host data. + multi_gpu_engine.emplace( + std::move(sub_pdlp_rank_data), + h_obj, h_var_lower, h_var_upper, h_cstr_lower, h_cstr_upper, + op_problem_scaled_.maximize, + op_problem_scaled_.objective_offset, + op_problem_scaled_.presolve_data.objective_scaling_factor, + sub_pdlp_settings); +} + template void pdlp_solver_t::set_initial_primal_weight(f_t initial_primal_weight) { @@ -2258,7 +2349,11 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co !settings_.get_initial_primal_weight().has_value()) compute_initial_primal_weight(); - initial_scaling_strategy_.scale_problem(); + // In multi-GPU mode the master scaled op_problem_scaled_ in its ctor before + // distributing data to the shards, so skip the second scaling pass here. + if (!multi_gpu_engine.has_value()) { + initial_scaling_strategy_.scale_problem(); + } // Update FP32 matrix copies for mixed precision SpMV after scaling pdhg_solver_.get_cusparse_view().update_mixed_precision_matrices(); diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 5cb267730f..ef992d2a9e 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -63,6 +63,11 @@ class pdlp_solver_t { pdlp_solver_t(problem_t& op_problem, pdlp_solver_settings_t const& settings, bool is_batch_mode = false); + + // Distributed Solver Constructor + pdlp_solver_t(problem_t& op_problem, + pdlp_solver_settings_t const& settings, + int num_gpus); optimization_problem_solution_t run_solver(const timer_t& timer); @@ -240,7 +245,10 @@ class pdlp_solver_t { // Flag to indicate if solver is being called from MIP. No logging is done in this case. bool inside_mip_{false}; - multi_gpu_engine_t multi_gpu_engine; + // std::optional because multi_gpu_engine_t is non-default-constructible + // (collectively bootstraps NCCL, owns RMM resources). Stays nullopt in + // single-GPU mode; emplaced by the multi-GPU ctor. + std::optional> multi_gpu_engine; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 59f1a4517f..6057f1cb83 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -709,6 +709,17 @@ static optimization_problem_solution_t run_pdlp_solver( } } #endif + if (settings.hyper_params.use_distributed_pdlp) { + cuopt_expects(settings.num_gpus > 1, + error_type_t::ValidationError, + "use_distributed_pdlp requires settings.num_gpus > 1"); + cuopt_expects(!is_batch_mode, + error_type_t::ValidationError, + "Distributed PDLP does not support batch mode"); + // Multi-GPU ctor; dispatched by 3rd-arg TYPE (int num_gpus, not bool batch). + detail::pdlp_solver_t solver(problem, settings, settings.num_gpus); + return solver.run_solver(timer); + } detail::pdlp_solver_t solver(problem, settings, is_batch_mode); if (settings.inside_mip) { solver.set_inside_mip(true); } return solver.run_solver(timer); From 5534ff049bca7c32da24fd0dc755f5c17c5a0611 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 19 May 2026 13:54:58 +0200 Subject: [PATCH 007/258] cursor broke everything grrr --- .../pdlp/distributed_pdlp/partition_loader.cu | 371 ++++++++++-------- .../distributed_pdlp/partition_loader.hpp | 45 ++- cpp/src/pdlp/distributed_pdlp/shard.hpp | 122 +++--- 3 files changed, 305 insertions(+), 233 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 449e8640ab..a9df158601 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -1,178 +1,201 @@ -static std::vector parse_distributed_pdlp_partition_file(std::string file){ - //returns a vector with all the values separated by a \n -} +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ -std::vector create_rank_data_from_parts(const std::vector& parts, - const std::vector& A_row_offsets, - const std::vector& A_col_indices, - const std::vector& A_values, - const std::vector& A_t_row_offsets, - const std::vector& A_t_col_indices, - const std::vector& A_t_values, - i_t nb_parts, - i_t nb_cstr, - i_t nb_vars, - i_t nnz) -{ -std::vector rank_data(nb_parts, rank_data_t(nb_parts)); -std::vector cstr_parts(parts.begin(), parts.begin() + nb_cstr); -std::vector var_parts(parts.begin() + nb_cstr, parts.begin() + nb_cstr + nb_vars); +#include -// 1. Compute ownership -for (i_t i = 0; i < nb_cstr; i++) { -rank_data[cstr_parts[i]].owned_cstr_indices.push_back(i); -} -for (i_t i = 0; i < nb_vars; i++) { -rank_data[var_parts[i]].owned_var_indices.push_back(i); -} +#include +#include -// 2. Compute local matrices and rank_data -for (i_t rank = 0; rank < nb_parts; rank++) { -auto& rd = rank_data[rank]; -rd.owned_var_size = rd.owned_var_indices.size(); -rd.owned_cstr_size = rd.owned_cstr_indices.size(); -// ---- A side ---- -std::vector local_A_row_offsets; -std::vector local_A_col_indices; -std::vector local_A_values; - -i_t local_A_nnz = 0; -local_A_row_offsets.push_back(local_A_nnz); - -// For each owned constraint, build local matrix A -for (auto owned_cstr : rd.owned_cstr_indices) { -i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; -i_t row_start = A_row_offsets[owned_cstr]; -for (i_t v = 0; v < cstr_len; v++) { -local_A_col_indices.push_back(A_col_indices[row_start + v]); -local_A_values.push_back(A_values[row_start + v]); -} -local_A_nnz += cstr_len; -local_A_row_offsets.push_back(local_A_nnz); -} - -std::set needed_vars; -for (auto indice : local_A_col_indices) { -if (var_parts[indice] != rank) -needed_vars.insert(indice); -} - -for (i_t peer = 0; peer < nb_parts; peer++) { -std::vector needed_var_from_peer; -for (auto needed_var : needed_vars) { -if (var_parts[needed_var] == peer) -needed_var_from_peer.push_back(needed_var); -} -i_t nb_recv_from_peer = needed_var_from_peer.size(); -rd.var_recv_counts[peer] = nb_recv_from_peer; -rd.var_recv_offsets[peer] = -peer == 0 -? 0 -: rd.var_recv_offsets[peer - 1] + rd.var_recv_counts[peer - 1]; -rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer); -} +namespace cuopt::linear_programming::detail { -rd.h_A_row_offsets = std::move(local_A_row_offsets); -rd.h_A_col_indices = std::move(local_A_col_indices); -rd.h_A_values = std::move(local_A_values); - -// ---- A_t side ---- -std::vector local_A_t_row_offsets; -std::vector local_A_t_col_indices; -std::vector local_A_t_values; -i_t local_A_t_nnz = 0; -local_A_t_row_offsets.push_back(local_A_t_nnz); - -for (auto owned_var : rd.owned_var_indices) { -i_t var_len = A_t_row_offsets[owned_var + 1] - A_t_row_offsets[owned_var]; -i_t row_start = A_t_row_offsets[owned_var]; -for (i_t v = 0; v < var_len; v++) { -local_A_t_col_indices.push_back(A_t_col_indices[row_start + v]); -local_A_t_values.push_back(A_t_values[row_start + v]); -} -local_A_t_nnz += var_len; -local_A_t_row_offsets.push_back(local_A_t_nnz); -} - -std::set needed_cstrs; -for (auto indice : local_A_t_col_indices) { -if (cstr_parts[indice] != rank) -needed_cstrs.insert(indice); -} - -for (i_t peer = 0; peer < nb_parts; peer++) { -std::vector needed_cstr_from_peer; -for (auto needed_cstr : needed_cstrs) { -if (cstr_parts[needed_cstr] == peer) -needed_cstr_from_peer.push_back(needed_cstr); -} -i_t nb_recv_from_peer = needed_cstr_from_peer.size(); -rd.cstr_recv_counts[peer] = nb_recv_from_peer; -rd.cstr_recv_offsets[peer] = -peer == 0 -? 0 -: rd.cstr_recv_offsets[peer - 1] + rd.cstr_recv_counts[peer - 1]; -rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer); -} - -rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); -rd.h_A_t_col_indices = std::move(local_A_t_col_indices); -rd.h_A_t_values = std::move(local_A_t_values); - -rd.total_var_size = rd.owned_var_size + needed_vars.size(); -rd.total_cstr_size = rd.owned_cstr_size + needed_cstrs.size(); -} - -// 3. Generate local indices for contiguous [[self], [peer1], ..., [peer_k]] -// Build scatter_gather_maps -for (i_t rank = 0; rank < nb_parts; rank++) { -auto& rd = rank_data[rank]; - -i_t curr_id = 0; -for (auto owned_cstr : rd.owned_cstr_indices) { -rd.global_to_local_cstr[owned_cstr] = curr_id; -rd.local_to_global_cstr.push_back(owned_cstr); -curr_id++; -} -for (i_t peer = 0; peer < nb_parts; peer++) { -if (peer == rank) continue; -for (auto recv_cstr : rank_data[peer].cstr_send_per_peer[rank]) { -rd.global_to_local_cstr[recv_cstr] = curr_id; -// rd.local_to_global_cstr.push_back(recv_cstr); // Not needed, we only do local_to_global on owned side -curr_id++; -} -} - -curr_id = 0; -for (auto owned_var : rd.owned_var_indices) { -rd.global_to_local_var[owned_var] = curr_id; -rd.local_to_global_var.push_back(owned_var); -curr_id++; -} -for (i_t peer = 0; peer < nb_parts; peer++) { -if (peer == rank) continue; -for (auto recv_var : rank_data[peer].var_send_per_peer[rank]) { -rd.global_to_local_var[recv_var] = curr_id; -// rd.local_to_global_var.push_back(recv_var); // same as over -curr_id++; -} -} -} - -// 4. Remap global -> local everywhere -for (i_t rank = 0; rank < nb_parts; rank++) { -auto& rd = rank_data[rank]; - -for (auto& send_vec : rd.var_send_per_peer) { -for (auto& v : send_vec) v = rd.global_to_local_var.at(v); -} -for (auto& send_vec : rd.cstr_send_per_peer) { -for (auto& v : send_vec) v = rd.global_to_local_cstr.at(v); -} - -for (auto& v : rd.h_A_col_indices) v = rd.global_to_local_var.at(v); -for (auto& v : rd.h_A_t_col_indices) v = rd.global_to_local_cstr.at(v); -} - -return rank_data; -} +template +std::vector partition_loader_t::parse_distributed_pdlp_partition_file( + std::string file) +{ + // returns a vector with all the values separated by a \n + return {}; // TODO: implement +} + +template +std::vector> +partition_loader_t::create_rank_data_from_parts( + const std::vector& parts, + const std::vector& A_row_offsets, + const std::vector& A_col_indices, + const std::vector& A_values, + const std::vector& A_t_row_offsets, + const std::vector& A_t_col_indices, + const std::vector& A_t_values, + i_t nb_parts, + i_t nb_cstr, + i_t nb_vars, + i_t nnz) +{ + std::vector> rank_data(nb_parts, rank_data_t(nb_parts)); + std::vector cstr_parts(parts.begin(), parts.begin() + nb_cstr); + std::vector var_parts(parts.begin() + nb_cstr, parts.begin() + nb_cstr + nb_vars); + + // 1. Compute ownership + for (i_t i = 0; i < nb_cstr; i++) { + rank_data[cstr_parts[i]].owned_cstr_indices.push_back(i); + } + for (i_t i = 0; i < nb_vars; i++) { + rank_data[var_parts[i]].owned_var_indices.push_back(i); + } + + // 2. Compute local matrices and rank_data + for (i_t rank = 0; rank < nb_parts; rank++) { + auto& rd = rank_data[rank]; + rd.owned_var_size = rd.owned_var_indices.size(); + rd.owned_cstr_size = rd.owned_cstr_indices.size(); + // ---- A side ---- + std::vector local_A_row_offsets; + std::vector local_A_col_indices; + std::vector local_A_values; + + i_t local_A_nnz = 0; + local_A_row_offsets.push_back(local_A_nnz); + + // For each owned constraint, build local matrix A + for (auto owned_cstr : rd.owned_cstr_indices) { + i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; + i_t row_start = A_row_offsets[owned_cstr]; + for (i_t v = 0; v < cstr_len; v++) { + local_A_col_indices.push_back(A_col_indices[row_start + v]); + local_A_values.push_back(A_values[row_start + v]); + } + local_A_nnz += cstr_len; + local_A_row_offsets.push_back(local_A_nnz); + } + + std::set needed_vars; + for (auto indice : local_A_col_indices) { + if (var_parts[indice] != rank) + needed_vars.insert(indice); + } + + for (i_t peer = 0; peer < nb_parts; peer++) { + std::vector needed_var_from_peer; + for (auto needed_var : needed_vars) { + if (var_parts[needed_var] == peer) + needed_var_from_peer.push_back(needed_var); + } + i_t nb_recv_from_peer = needed_var_from_peer.size(); + rd.var_recv_counts[peer] = nb_recv_from_peer; + rd.var_recv_offsets[peer] = + peer == 0 + ? 0 + : rd.var_recv_offsets[peer - 1] + rd.var_recv_counts[peer - 1]; + rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer); + } + + rd.h_A_row_offsets = std::move(local_A_row_offsets); + rd.h_A_col_indices = std::move(local_A_col_indices); + rd.h_A_values = std::move(local_A_values); + + // ---- A_t side ---- + std::vector local_A_t_row_offsets; + std::vector local_A_t_col_indices; + std::vector local_A_t_values; + i_t local_A_t_nnz = 0; + local_A_t_row_offsets.push_back(local_A_t_nnz); + + for (auto owned_var : rd.owned_var_indices) { + i_t var_len = A_t_row_offsets[owned_var + 1] - A_t_row_offsets[owned_var]; + i_t row_start = A_t_row_offsets[owned_var]; + for (i_t v = 0; v < var_len; v++) { + local_A_t_col_indices.push_back(A_t_col_indices[row_start + v]); + local_A_t_values.push_back(A_t_values[row_start + v]); + } + local_A_t_nnz += var_len; + local_A_t_row_offsets.push_back(local_A_t_nnz); + } + + std::set needed_cstrs; + for (auto indice : local_A_t_col_indices) { + if (cstr_parts[indice] != rank) + needed_cstrs.insert(indice); + } + + for (i_t peer = 0; peer < nb_parts; peer++) { + std::vector needed_cstr_from_peer; + for (auto needed_cstr : needed_cstrs) { + if (cstr_parts[needed_cstr] == peer) + needed_cstr_from_peer.push_back(needed_cstr); + } + i_t nb_recv_from_peer = needed_cstr_from_peer.size(); + rd.cstr_recv_counts[peer] = nb_recv_from_peer; + rd.cstr_recv_offsets[peer] = + peer == 0 + ? 0 + : rd.cstr_recv_offsets[peer - 1] + rd.cstr_recv_counts[peer - 1]; + rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer); + } + + rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); + rd.h_A_t_col_indices = std::move(local_A_t_col_indices); + rd.h_A_t_values = std::move(local_A_t_values); + + rd.total_var_size = rd.owned_var_size + needed_vars.size(); + rd.total_cstr_size = rd.owned_cstr_size + needed_cstrs.size(); + } + + // 3. Generate local indices for contiguous [[self], [peer1], ..., [peer_k]] + // Build scatter_gather_maps + for (i_t rank = 0; rank < nb_parts; rank++) { + auto& rd = rank_data[rank]; + + i_t curr_id = 0; + for (auto owned_cstr : rd.owned_cstr_indices) { + rd.global_to_local_cstr[owned_cstr] = curr_id; + rd.local_to_global_cstr.push_back(owned_cstr); + curr_id++; + } + for (i_t peer = 0; peer < nb_parts; peer++) { + if (peer == rank) continue; + for (auto recv_cstr : rank_data[peer].cstr_send_per_peer[rank]) { + rd.global_to_local_cstr[recv_cstr] = curr_id; + // rd.local_to_global_cstr.push_back(recv_cstr); // Not needed, we only do local_to_global on owned side + curr_id++; + } + } + + curr_id = 0; + for (auto owned_var : rd.owned_var_indices) { + rd.global_to_local_var[owned_var] = curr_id; + rd.local_to_global_var.push_back(owned_var); + curr_id++; + } + for (i_t peer = 0; peer < nb_parts; peer++) { + if (peer == rank) continue; + for (auto recv_var : rank_data[peer].var_send_per_peer[rank]) { + rd.global_to_local_var[recv_var] = curr_id; + // rd.local_to_global_var.push_back(recv_var); // same as over + curr_id++; + } + } + } + + // 4. Remap global -> local everywhere + for (i_t rank = 0; rank < nb_parts; rank++) { + auto& rd = rank_data[rank]; + + for (auto& send_vec : rd.var_send_per_peer) { + for (auto& v : send_vec) v = rd.global_to_local_var.at(v); + } + for (auto& send_vec : rd.cstr_send_per_peer) { + for (auto& v : send_vec) v = rd.global_to_local_cstr.at(v); + } + + for (auto& v : rd.h_A_col_indices) v = rd.global_to_local_var.at(v); + for (auto& v : rd.h_A_t_col_indices) v = rd.global_to_local_cstr.at(v); + } + + return rank_data; +} + +template struct partition_loader_t; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index 4d66d4445c..efdfd0ba0e 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -1,16 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once -partition_loader_t { - static std::vector parse_distributed_pdlp_partition_file(std::string file); - std::vector create_rank_data_from_parts(const std::vector& parts, - const std::vector& A_row_offsets, - const std::vector& A_col_indices, - const std::vector& A_values, - const std::vector& A_t_row_offsets, - const std::vector& A_t_col_indices, - const std::vector& A_t_values, - i_t nb_parts, - i_t nb_cstr, - i_t nb_vars, - i_t nnz); -} \ No newline at end of file +#include + +#include +#include + +namespace cuopt::linear_programming::detail { + +template +struct partition_loader_t { + static std::vector parse_distributed_pdlp_partition_file(std::string file); + + static std::vector> create_rank_data_from_parts( + const std::vector& parts, + const std::vector& A_row_offsets, + const std::vector& A_col_indices, + const std::vector& A_values, + const std::vector& A_t_row_offsets, + const std::vector& A_t_col_indices, + const std::vector& A_t_values, + i_t nb_parts, + i_t nb_cstr, + i_t nb_vars, + i_t nnz); +}; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 7528c35dec..a33477edf1 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -1,46 +1,78 @@ -#pragma once -#include -#include -#include -#include -namespace cuopt::linear_programming::detail { +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + #pragma once -template -class pdlp_solver_t; - -struct nccl_comm_deleter_t { - int device_id{-1}; - void operator()(ncclComm* comm) const noexcept - { - raft::device_setter guard(device_id); - if (comm != nullptr) { - ncclCommDestroy(comm); - } - } -}; -using nccl_comm_unique_ptr_t = std::unique_ptr; - -template -class pdlp_shard_t { - // Declaration only, will be set as default in shard.cu . Needed to manage cyclic include of pdlp_solver_t. - public: - ~pdlp_shard_t(); - pdlp_shard_t(int device_id, - rank_data_t&& rd, - ncclComm_t comm - /* ???????? */); - - pdlp_shard_t(const pdlp_shard_t&) = delete; - pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; - // Specific multi-GPU data - int device_id; - rmm::cuda_stream stream; - raft::handle_t handle; - nccl_comm_unique_ptr_t comm; - rank_data_t rank_data; - optimization_problem_t opt_problem; - problem_t sub_problem; - std::unique_ptr> sub_pdlp; -}; - -} + #include + + #include + #include + #include + + #include + #include + #include + + #include + + #include + #include + #include + + namespace cuopt::linear_programming::detail { + + // Forward-declare to break the cyclic include with pdlp.cuh + // (pdlp.cuh -> multi_gpu_engine.hpp -> shard.hpp -> pdlp.cuh). + // Definitions of out-of-line members live in shard.cu, which includes pdlp.cuh. + template + class pdlp_solver_t; + + // RAII deleter for ncclComm_t; sets the right device before destroy. + struct nccl_comm_deleter_t { + int device_id{-1}; + void operator()(ncclComm* comm) const noexcept + { + if (comm == nullptr) return; + raft::device_setter guard(device_id); + ncclCommDestroy(comm); + } + }; + using nccl_comm_unique_ptr_t = std::unique_ptr; + + template + struct pdlp_shard_t { + // Out-of-line (in shard.cu) because pdlp_solver_t is incomplete here. + ~pdlp_shard_t(); + + pdlp_shard_t(int device_id, + rank_data_t&& rd, + ncclComm_t raw_comm, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& settings); + + pdlp_shard_t(const pdlp_shard_t&) = delete; + pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; + // Move ops are implicitly deleted (user-declared dtor + deleted copy). + // Intentional: shard owns device-affine resources and must never move. + // Store as std::unique_ptr in any container. + + int device_id; + rmm::cuda_stream stream; + raft::handle_t handle; + nccl_comm_unique_ptr_t comm; + rank_data_t rank_data; + std::optional> opt_problem; + std::optional> sub_problem; + std::unique_ptr> sub_pdlp; + }; + + } // namespace cuopt::linear_programming::detail + \ No newline at end of file From dd935c5307a312918121b53a27674bb4656fd291 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 19 May 2026 14:26:31 +0200 Subject: [PATCH 008/258] partition loader now partition loads --- .../pdlp/distributed_pdlp/partition_loader.cu | 28 ++++++++++++++++--- .../distributed_pdlp/partition_loader.hpp | 5 +++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index a9df158601..0e122cefc0 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -5,17 +5,37 @@ #include +#include + +#include #include #include namespace cuopt::linear_programming::detail { template -std::vector partition_loader_t::parse_distributed_pdlp_partition_file( - std::string file) +std::vector partition_loader_t::parse_distributed_pdlp_partition_file( + std::string const& file) { - // returns a vector with all the values separated by a \n - return {}; // TODO: implement + std::ifstream part_file(file); + cuopt_expects(part_file.is_open(), + error_type_t::ValidationError, + "Failed to open partition file: " + file); + + // One integer per line; operator>> skips whitespace so blank lines and + // trailing newlines are tolerated. + std::vector parts; + i_t part = 0; + while (part_file >> part) { + parts.push_back(part); + } + + // We must have hit EOF cleanly; any other state means a malformed token. + cuopt_expects(part_file.eof(), + error_type_t::ValidationError, + "Malformed partition file (expected one integer per line): " + file); + + return parts; } template diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index efdfd0ba0e..25560cdbfd 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -14,7 +14,10 @@ namespace cuopt::linear_programming::detail { template struct partition_loader_t { - static std::vector parse_distributed_pdlp_partition_file(std::string file); + // Read a Metis-style partition file: one part-id per line (whitespace-tolerant), + // ASCII integers in [0, nb_parts). Returns a flat vector of length + // nb_cstr + nb_vars, indexed as in create_rank_data_from_parts (cstrs first, then vars). + static std::vector parse_distributed_pdlp_partition_file(std::string const& file); static std::vector> create_rank_data_from_parts( const std::vector& parts, From 09eb20b7701df0079309ab6932a5a03a9fd6595e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 19 May 2026 19:44:15 +0200 Subject: [PATCH 009/258] big advancements ayo ! We can soon start working on imlementing the solver !!! --- .../pdlp/solver_settings.hpp | 2 + .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 116 ++++++++------ .../distributed_pdlp/multi_gpu_engine.hpp | 41 ++--- .../pdlp/distributed_pdlp/partition_loader.cu | 38 +++-- .../distributed_pdlp/partition_loader.hpp | 3 + cpp/src/pdlp/distributed_pdlp/rank_data.hpp | 2 + cpp/src/pdlp/distributed_pdlp/shard.cu | 102 ++++++++++-- cpp/src/pdlp/distributed_pdlp/shard.hpp | 35 +++-- .../initial_scaling.cu | 36 +++++ .../initial_scaling.cuh | 7 + cpp/src/pdlp/pdlp.cu | 145 ++++++++++++------ cpp/src/pdlp/pdlp.cuh | 8 +- 12 files changed, 382 insertions(+), 153 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index 4585b9d1cf..2a18b8060f 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -287,6 +287,8 @@ class pdlp_solver_settings_t { bool dual_postsolve{true}; int num_gpus{1}; std::string multi_gpu_partition_file{""}; + // Set to true inside the shards + bool is_distributed_sub_pdlp{false}; method_t method{method_t::Concurrent}; bool inside_mip{false}; // For concurrent termination diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index c7307c46ee..9b404bbd53 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -15,57 +15,71 @@ namespace cuopt::linear_programming::detail { - template - multi_gpu_engine_t::multi_gpu_engine_t( - std::vector>&& rank_data, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, - pdlp_solver_settings_t const& sub_solver_settings) - : stream() - { - const int nb_parts = static_cast(rank_data.size()); - cuopt_expects(nb_parts > 0, - error_type_t::ValidationError, - "multi_gpu_engine_t: rank_data must be non-empty"); - - shards.reserve(nb_parts); - - // 1:1 rank -> device mapping. (Matches metis_tests; refine later if needed.) - std::vector devices(nb_parts); - std::iota(devices.begin(), devices.end(), 0); - - // 2. Collectively bootstrap NCCL communicators across all devices. - // Must be done together; each comm is then handed to one shard, - // which wraps it in a unique_ptr with the device-aware deleter. - std::vector raw_comms(nb_parts); - cuopt_expects(ncclCommInitAll(raw_comms.data(), nb_parts, devices.data()) == ncclSuccess, - error_type_t::RuntimeError, - "ncclCommInitAll failed"); - - // 3. Construct one shard per rank, pinned to its device. - for (int r = 0; r < nb_parts; ++r) { - raft::device_setter guard(devices[r]); // shard ctor asserts current device - shards.emplace_back(std::make_unique>( - devices[r], - std::move(rank_data[r]), - raw_comms[r], - h_global_obj, - h_global_var_lower, - h_global_var_upper, - h_global_cstr_lower, - h_global_cstr_upper, - maximize, - objective_offset, - objective_scaling_factor, - sub_solver_settings)); - } - } +template +multi_gpu_engine_t::multi_gpu_engine_t( + std::vector>&& rank_data, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + std::vector const& h_global_obj_scaled, + std::vector const& h_global_var_lower_scaled, + std::vector const& h_global_var_upper_scaled, + std::vector const& h_global_cstr_lower_scaled, + std::vector const& h_global_cstr_upper_scaled, + std::vector const& h_global_cummulative_cstr_scaling, + std::vector const& h_global_cummulative_var_scaling, + f_t h_bound_rescaling, + f_t h_objective_rescaling, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& sub_solver_settings) + : stream() +{ + const int nb_parts = static_cast(rank_data.size()); + cuopt_expects(nb_parts > 0, + error_type_t::ValidationError, + "multi_gpu_engine_t: rank_data must be non-empty"); + + shards.reserve(nb_parts); + std::vector devices(nb_parts); + std::iota(devices.begin(), devices.end(), 0); + + // Create NCCL Comms then let shards own them + std::vector raw_comms(nb_parts); + cuopt_expects(ncclCommInitAll(raw_comms.data(), nb_parts, devices.data()) == ncclSuccess, + error_type_t::RuntimeError, + "ncclCommInitAll failed"); + + // 3. Construct one shard per rank, pinned to its device. + for (int r = 0; r < nb_parts; ++r) { + raft::device_setter guard(devices[r]); // shard ctor needs device set + shards.emplace_back(std::make_unique>( + devices[r], + std::move(rank_data[r]), + raw_comms[r], + h_global_obj, + h_global_var_lower, + h_global_var_upper, + h_global_cstr_lower, + h_global_cstr_upper, + h_global_obj_scaled, + h_global_var_lower_scaled, + h_global_var_upper_scaled, + h_global_cstr_lower_scaled, + h_global_cstr_upper_scaled, + h_global_cummulative_cstr_scaling, + h_global_cummulative_var_scaling, + h_bound_rescaling, + h_objective_rescaling, + maximize, + objective_offset, + objective_scaling_factor, + sub_solver_settings)); + } +} template struct multi_gpu_engine_t; // template struct multi_gpu_engine_t; diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 6142c938e3..d672e18197 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -16,24 +16,29 @@ namespace cuopt::linear_programming::detail { - template - struct multi_gpu_engine_t { - // Constructs one shard per partition. Caller is responsible for: - // - rank_data[i] being correctly populated for rank i - // - the host vectors holding the (already scaled) global problem data - // - sub_solver_settings being the per-shard PDLP config (num_gpus=1, - // multi_gpu_partition_file="", scaling disabled). - multi_gpu_engine_t( - std::vector>&& rank_data, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, - pdlp_solver_settings_t const& sub_solver_settings); +template +struct multi_gpu_engine_t { + // Constructs shards from rank_data + multi_gpu_engine_t( + std::vector>&& rank_data, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + std::vector const& h_global_obj_scaled, + std::vector const& h_global_var_lower_scaled, + std::vector const& h_global_var_upper_scaled, + std::vector const& h_global_cstr_lower_scaled, + std::vector const& h_global_cstr_upper_scaled, + std::vector const& h_global_cummulative_cstr_scaling, + std::vector const& h_global_cummulative_var_scaling, + f_t h_bound_rescaling, + f_t h_objective_rescaling, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& sub_solver_settings); multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; multi_gpu_engine_t& operator=(const multi_gpu_engine_t&) = delete; diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 0e122cefc0..047fb536d5 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -45,14 +45,23 @@ partition_loader_t::create_rank_data_from_parts( const std::vector& A_row_offsets, const std::vector& A_col_indices, const std::vector& A_values, + const std::vector& A_values_scaled, const std::vector& A_t_row_offsets, const std::vector& A_t_col_indices, const std::vector& A_t_values, + const std::vector& A_t_values_scaled, i_t nb_parts, i_t nb_cstr, i_t nb_vars, i_t nnz) { + cuopt_expects(A_values.size() == A_values_scaled.size(), + error_type_t::ValidationError, + "A_values and A_values_scaled must have the same length"); + cuopt_expects(A_t_values.size() == A_t_values_scaled.size(), + error_type_t::ValidationError, + "A_t_values and A_t_values_scaled must have the same length"); + std::vector> rank_data(nb_parts, rank_data_t(nb_parts)); std::vector cstr_parts(parts.begin(), parts.begin() + nb_cstr); std::vector var_parts(parts.begin() + nb_cstr, parts.begin() + nb_cstr + nb_vars); @@ -74,17 +83,22 @@ partition_loader_t::create_rank_data_from_parts( std::vector local_A_row_offsets; std::vector local_A_col_indices; std::vector local_A_values; + std::vector local_A_values_scaled; i_t local_A_nnz = 0; local_A_row_offsets.push_back(local_A_nnz); - // For each owned constraint, build local matrix A + // For each owned constraint, build local matrix A. We walk both the + // unscaled and scaled global value arrays in lockstep so the produced + // local arrays share identical (offsets, col_indices) and differ only + // in values. for (auto owned_cstr : rd.owned_cstr_indices) { i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; i_t row_start = A_row_offsets[owned_cstr]; for (i_t v = 0; v < cstr_len; v++) { local_A_col_indices.push_back(A_col_indices[row_start + v]); - local_A_values.push_back(A_values[row_start + v]); + local_A_values .push_back(A_values [row_start + v]); + local_A_values_scaled.push_back(A_values_scaled[row_start + v]); } local_A_nnz += cstr_len; local_A_row_offsets.push_back(local_A_nnz); @@ -111,14 +125,16 @@ partition_loader_t::create_rank_data_from_parts( rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer); } - rd.h_A_row_offsets = std::move(local_A_row_offsets); - rd.h_A_col_indices = std::move(local_A_col_indices); - rd.h_A_values = std::move(local_A_values); + rd.h_A_row_offsets = std::move(local_A_row_offsets); + rd.h_A_col_indices = std::move(local_A_col_indices); + rd.h_A_values = std::move(local_A_values); + rd.h_A_values_scaled = std::move(local_A_values_scaled); // ---- A_t side ---- std::vector local_A_t_row_offsets; std::vector local_A_t_col_indices; std::vector local_A_t_values; + std::vector local_A_t_values_scaled; i_t local_A_t_nnz = 0; local_A_t_row_offsets.push_back(local_A_t_nnz); @@ -126,8 +142,9 @@ partition_loader_t::create_rank_data_from_parts( i_t var_len = A_t_row_offsets[owned_var + 1] - A_t_row_offsets[owned_var]; i_t row_start = A_t_row_offsets[owned_var]; for (i_t v = 0; v < var_len; v++) { - local_A_t_col_indices.push_back(A_t_col_indices[row_start + v]); - local_A_t_values.push_back(A_t_values[row_start + v]); + local_A_t_col_indices .push_back(A_t_col_indices [row_start + v]); + local_A_t_values .push_back(A_t_values [row_start + v]); + local_A_t_values_scaled.push_back(A_t_values_scaled[row_start + v]); } local_A_t_nnz += var_len; local_A_t_row_offsets.push_back(local_A_t_nnz); @@ -154,9 +171,10 @@ partition_loader_t::create_rank_data_from_parts( rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer); } - rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); - rd.h_A_t_col_indices = std::move(local_A_t_col_indices); - rd.h_A_t_values = std::move(local_A_t_values); + rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); + rd.h_A_t_col_indices = std::move(local_A_t_col_indices); + rd.h_A_t_values = std::move(local_A_t_values); + rd.h_A_t_values_scaled = std::move(local_A_t_values_scaled); rd.total_var_size = rd.owned_var_size + needed_vars.size(); rd.total_cstr_size = rd.owned_cstr_size + needed_cstrs.size(); diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index 25560cdbfd..915c24a828 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -19,14 +19,17 @@ struct partition_loader_t { // nb_cstr + nb_vars, indexed as in create_rank_data_from_parts (cstrs first, then vars). static std::vector parse_distributed_pdlp_partition_file(std::string const& file); + // Slices the data to prepare a split from metis partitionning with halo communication static std::vector> create_rank_data_from_parts( const std::vector& parts, const std::vector& A_row_offsets, const std::vector& A_col_indices, const std::vector& A_values, + const std::vector& A_values_scaled, const std::vector& A_t_row_offsets, const std::vector& A_t_col_indices, const std::vector& A_t_values, + const std::vector& A_t_values_scaled, i_t nb_parts, i_t nb_cstr, i_t nb_vars, diff --git a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp index ee107f5cf1..29d76ae110 100644 --- a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp +++ b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp @@ -44,9 +44,11 @@ struct rank_data_t { std::vector h_A_row_offsets; std::vector h_A_col_indices; std::vector h_A_values; + std::vector h_A_values_scaled; // A_t std::vector h_A_t_row_offsets; std::vector h_A_t_col_indices; std::vector h_A_t_values; + std::vector h_A_t_values_scaled; }; } // namespace cuopt::linear_programming::detail \ No newline at end of file diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index d5e795bb61..41f74086ab 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -5,6 +5,9 @@ #include #include +#include + +#include #include #include @@ -28,6 +31,15 @@ pdlp_shard_t::pdlp_shard_t( std::vector const& h_global_var_upper, std::vector const& h_global_cstr_lower, std::vector const& h_global_cstr_upper, + std::vector const& h_global_obj_scaled, + std::vector const& h_global_var_lower_scaled, + std::vector const& h_global_var_upper_scaled, + std::vector const& h_global_cstr_lower_scaled, + std::vector const& h_global_cstr_upper_scaled, + std::vector const& h_global_cummulative_cstr_scaling, + std::vector const& h_global_cummulative_var_scaling, + f_t h_bound_rescaling, + f_t h_objective_rescaling, bool maximize, f_t objective_offset, f_t objective_scaling_factor, @@ -45,27 +57,47 @@ pdlp_shard_t::pdlp_shard_t( // ---- 1. Gather per-shard host slices using rank_data's index maps. ---- // All vectors are sized to TOTAL (owned + halo). Owned slots get real - // values; halo slots keep neutral defaults so they are no-ops even if - // accidentally touched before `owned_*_size_` plumbing is in place. - std::vector h_obj (rank_data.total_var_size, f_t{0}); - std::vector h_var_lower (rank_data.total_var_size, -std::numeric_limits::infinity()); - std::vector h_var_upper (rank_data.total_var_size, std::numeric_limits::infinity()); - std::vector h_cstr_lower(rank_data.total_cstr_size, -std::numeric_limits::infinity()); - std::vector h_cstr_upper(rank_data.total_cstr_size, std::numeric_limits::infinity()); + // values; halo slots keep defaults because they should not be accessed + std::vector h_obj (rank_data.total_var_size, f_t{0}); + std::vector h_var_lower (rank_data.total_var_size, -std::numeric_limits::infinity()); + std::vector h_var_upper (rank_data.total_var_size, std::numeric_limits::infinity()); + std::vector h_cstr_lower (rank_data.total_cstr_size, -std::numeric_limits::infinity()); + std::vector h_cstr_upper (rank_data.total_cstr_size, std::numeric_limits::infinity()); + + std::vector h_obj_scaled (rank_data.total_var_size, f_t{0}); + std::vector h_var_lower_scaled (rank_data.total_var_size, -std::numeric_limits::infinity()); + std::vector h_var_upper_scaled (rank_data.total_var_size, std::numeric_limits::infinity()); + std::vector h_cstr_lower_scaled(rank_data.total_cstr_size, -std::numeric_limits::infinity()); + std::vector h_cstr_upper_scaled(rank_data.total_cstr_size, std::numeric_limits::infinity()); for (i_t i = 0; i < rank_data.owned_var_size; ++i) { - const auto g = rank_data.local_to_global_var[i]; - h_obj[i] = h_global_obj[g]; - h_var_lower[i] = h_global_var_lower[g]; - h_var_upper[i] = h_global_var_upper[g]; + const auto g = rank_data.local_to_global_var[i]; + h_obj[i] = h_global_obj[g]; + h_var_lower[i] = h_global_var_lower[g]; + h_var_upper[i] = h_global_var_upper[g]; + h_obj_scaled[i] = h_global_obj_scaled[g]; + h_var_lower_scaled[i] = h_global_var_lower_scaled[g]; + h_var_upper_scaled[i] = h_global_var_upper_scaled[g]; + } + for (i_t i = 0; i < rank_data.owned_cstr_size; ++i) { + const auto g = rank_data.local_to_global_cstr[i]; + h_cstr_lower[i] = h_global_cstr_lower[g]; + h_cstr_upper[i] = h_global_cstr_upper[g]; + h_cstr_lower_scaled[i] = h_global_cstr_lower_scaled[g]; + h_cstr_upper_scaled[i] = h_global_cstr_upper_scaled[g]; } + + // Get local scaling factors + std::vector h_cstr_scaling_local(rank_data.total_cstr_size, f_t{1}); + std::vector h_var_scaling_local (rank_data.total_var_size, f_t{1}); for (i_t i = 0; i < rank_data.owned_cstr_size; ++i) { - const auto g = rank_data.local_to_global_cstr[i]; - h_cstr_lower[i] = h_global_cstr_lower[g]; - h_cstr_upper[i] = h_global_cstr_upper[g]; + h_cstr_scaling_local[i] = h_global_cummulative_cstr_scaling[rank_data.local_to_global_cstr[i]]; + } + for (i_t i = 0; i < rank_data.owned_var_size; ++i) { + h_var_scaling_local[i] = h_global_cummulative_var_scaling[rank_data.local_to_global_var[i]]; } - // ---- 2. Build optimization_problem_t on this shard's device. ---- + // ---- 2. Build optimization_problem_t on this shard's device (UNSCALED). ---- opt_problem.emplace(&handle); opt_problem->set_csr_constraint_matrix( rank_data.h_A_values .data(), static_cast(rank_data.h_A_values .size()), @@ -86,7 +118,7 @@ pdlp_shard_t::pdlp_shard_t( opt_problem->set_objective_scaling_factor(objective_scaling_factor); opt_problem->set_problem_category(problem_category_t::LP); - // ---- 3. Build problem_t from opt_problem. ---- + // ---- 3. Build problem_t from opt_problem (still UNSCALED). ---- sub_problem.emplace(*opt_problem); // ---- 4. Override reverse_* with the real local A_T from rank_data. ---- @@ -109,7 +141,45 @@ pdlp_shard_t::pdlp_shard_t( handle.sync_stream(stream_view); // ---- 5. Build sub_pdlp (single-GPU mode; multi_gpu flags cleared by caller). ---- + // At this point sub_pdlp.op_problem_scaled_ is an unscaled copy + // of sub_problem and sub_pdlp.initial_scaling_strategy_ has + // unit cumulative factors (sub-settings disable Ruiz / PC iters). sub_pdlp = std::make_unique>(*sub_problem, settings, /*batch=*/false); + + // Inject master-scaled buffers inside sub_pdlp + auto& scaled = sub_pdlp->get_op_problem_scaled(); + raft::copy(scaled.coefficients.data(), + rank_data.h_A_values_scaled.data(), + rank_data.h_A_values_scaled.size(), stream_view); + raft::copy(scaled.reverse_coefficients.data(), + rank_data.h_A_t_values_scaled.data(), + rank_data.h_A_t_values_scaled.size(), stream_view); + raft::copy(scaled.objective_coefficients.data(), + h_obj_scaled.data(), h_obj_scaled.size(), stream_view); + raft::copy(scaled.constraint_lower_bounds.data(), + h_cstr_lower_scaled.data(), h_cstr_lower_scaled.size(), stream_view); + raft::copy(scaled.constraint_upper_bounds.data(), + h_cstr_upper_scaled.data(), h_cstr_upper_scaled.size(), stream_view); + + using f_t2 = typename type_2::type; + std::vector h_var_bounds_scaled_packed(rank_data.total_var_size); + for (i_t i = 0; i < rank_data.total_var_size; ++i) { + h_var_bounds_scaled_packed[i].x = h_var_lower_scaled[i]; + h_var_bounds_scaled_packed[i].y = h_var_upper_scaled[i]; + } + raft::copy(scaled.variable_bounds.data(), + h_var_bounds_scaled_packed.data(), + h_var_bounds_scaled_packed.size(), stream_view); + + combine_constraint_bounds(scaled, scaled.combined_bounds); + + // Inject master-scaled buffers inside sub_pdlp.initil_strategy + auto& scaling = sub_pdlp->get_initial_scaling_strategy(); + scaling.set_cummulative_scaling(h_cstr_scaling_local, h_var_scaling_local); + scaling.set_h_bound_rescaling (h_bound_rescaling); + scaling.set_h_objective_rescaling(h_objective_rescaling); + + handle.sync_stream(stream_view); } template struct pdlp_shard_t; diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index a33477edf1..3c10a90f90 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -45,18 +45,29 @@ // Out-of-line (in shard.cu) because pdlp_solver_t is incomplete here. ~pdlp_shard_t(); - pdlp_shard_t(int device_id, - rank_data_t&& rd, - ncclComm_t raw_comm, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, - pdlp_solver_settings_t const& settings); + // sub worker for distributed pdlp. Owns its own view on scaled problem and unscaled problem + // Owns necessary multi-gpu data (rank_data, device_id, nccl_comm) + pdlp_shard_t(int device_id, + rank_data_t&& rd, + ncclComm_t raw_comm, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + std::vector const& h_global_obj_scaled, + std::vector const& h_global_var_lower_scaled, + std::vector const& h_global_var_upper_scaled, + std::vector const& h_global_cstr_lower_scaled, + std::vector const& h_global_cstr_upper_scaled, + std::vector const& h_global_cummulative_cstr_scaling, + std::vector const& h_global_cummulative_var_scaling, + f_t h_bound_rescaling, + f_t h_objective_rescaling, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& settings); pdlp_shard_t(const pdlp_shard_t&) = delete; pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index a76b1773f9..a94064d0af 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -809,6 +809,42 @@ pdlp_initial_scaling_strategy_t::get_variable_scaling_vector() const return cummulative_variable_scaling_; } +template +void pdlp_initial_scaling_strategy_t::set_cummulative_scaling( + const std::vector& h_cummulative_constraint_matrix_scaling, + const std::vector& h_cummulative_variable_scaling) +{ + cuopt_expects(static_cast(h_cummulative_constraint_matrix_scaling.size()) == dual_size_h_, + error_type_t::ValidationError, + "set_cummulative_scaling: host constraint scaling vector size mismatch"); + cuopt_expects(static_cast(h_cummulative_variable_scaling.size()) == primal_size_h_, + error_type_t::ValidationError, + "set_cummulative_scaling: host variable scaling vector size mismatch"); + + raft::copy(cummulative_constraint_matrix_scaling_.data(), + h_cummulative_constraint_matrix_scaling.data(), + h_cummulative_constraint_matrix_scaling.size(), + stream_view_); + raft::copy(cummulative_variable_scaling_.data(), + h_cummulative_variable_scaling.data(), + h_cummulative_variable_scaling.size(), + stream_view_); +} + +template +void pdlp_initial_scaling_strategy_t::set_h_bound_rescaling(f_t value) +{ + h_bound_rescaling = value; + bound_rescaling_.set_value_async(value, stream_view_); +} + +template +void pdlp_initial_scaling_strategy_t::set_h_objective_rescaling(f_t value) +{ + h_objective_rescaling = value; + objective_rescaling_.set_value_async(value, stream_view_); +} + template typename pdlp_initial_scaling_strategy_t::view_t pdlp_initial_scaling_strategy_t::view() diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 5a3dcfaca2..ed5f8b1851 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -76,6 +76,13 @@ class pdlp_initial_scaling_strategy_t { f_t get_h_bound_rescaling() const; f_t get_h_objective_rescaling() const; + // Inject scaling state computed by another pdlp_initial_scaling_strategy_t + // Needed by distributed PDLP + void set_cummulative_scaling(const std::vector& h_cummulative_constraint_matrix_scaling, + const std::vector& h_cummulative_variable_scaling); + void set_h_bound_rescaling(f_t value); + void set_h_objective_rescaling(f_t value); + void bound_objective_rescaling(); /** diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index a58ae4f210..612eb676ec 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -337,68 +337,119 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, "Metis partitioning inside cuopt not implemented yet; " "provide a --parts file via settings.multi_gpu_partition_file"); } - // 3. Scale now before copying to children + + // always compute initial step size before scaling and primal_weight after scaling to do like cuPDLPx + assert(settings_.hyper_params.compute_initial_primal_weight_before_scaling && "compute_initial_primal_weight_before_scaling must be true in distributed mode"); + assert(!settings_.hyper_params.compute_initial_step_size_before_scaling && "compute_initial_step_size_before_scaling must be false in distributed mode"); + + compute_initial_primal_weight(); + + // scale globally before dispatching to shards initial_scaling_strategy_.scale_problem(); + + compute_initial_step_size(); - // 4. Copy the scaled global problem from device -> host. + const f_t initial_step_size_global = get_step_size_h(0); + const f_t initial_primal_weight_global = get_primal_weight_h(0); + + // 4. Copy both scaled and unscaled pb auto const stream = op_problem_scaled_.handle_ptr->get_stream(); i_t const n_cstr = op_problem_scaled_.n_constraints; i_t const n_vars = op_problem_scaled_.n_variables; i_t const nnz = op_problem_scaled_.nnz; - // CSRs (A and A_t). + + // Shared topology (taken from the scaled problem, but identical on both). std::vector h_A_row_offsets (n_cstr + 1); std::vector h_A_col_indices (nnz); - std::vector h_A_values (nnz); std::vector h_A_t_row_offsets(n_vars + 1); std::vector h_A_t_col_indices(nnz); - std::vector h_A_t_values (nnz); - raft::copy(h_A_row_offsets .data(), op_problem_scaled_.offsets .data(), n_cstr + 1, stream); - raft::copy(h_A_col_indices .data(), op_problem_scaled_.variables .data(), nnz, stream); - raft::copy(h_A_values .data(), op_problem_scaled_.coefficients .data(), nnz, stream); - raft::copy(h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets .data(), n_vars + 1, stream); - raft::copy(h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints .data(), nnz, stream); - raft::copy(h_A_t_values .data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); - // Objective coefficients. - std::vector h_obj(n_vars); - raft::copy(h_obj.data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); - // Variable bounds: stored interleaved as f_t2 {lower, upper}. Unpack into two host vectors. + raft::copy(h_A_row_offsets .data(), op_problem_scaled_.offsets .data(), n_cstr + 1, stream); + raft::copy(h_A_col_indices .data(), op_problem_scaled_.variables .data(), nnz, stream); + raft::copy(h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets .data(), n_vars + 1, stream); + raft::copy(h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints.data(), nnz, stream); + + // Paired value arrays for A and A_T. + std::vector h_A_values (nnz); + std::vector h_A_values_scaled (nnz); + std::vector h_A_t_values (nnz); + std::vector h_A_t_values_scaled(nnz); + raft::copy(h_A_values .data(), problem_ptr->coefficients .data(), nnz, stream); + raft::copy(h_A_t_values .data(), problem_ptr->reverse_coefficients .data(), nnz, stream); + raft::copy(h_A_values_scaled .data(), op_problem_scaled_.coefficients .data(), nnz, stream); + raft::copy(h_A_t_values_scaled.data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); + using f_t2 = typename type_2::type; - std::vector h_var_bounds_packed(n_vars); - raft::copy(h_var_bounds_packed.data(), - op_problem_scaled_.variable_bounds.data(), n_vars, stream); - // Constraint bounds. - std::vector h_cstr_lower(n_cstr); - std::vector h_cstr_upper(n_cstr); - raft::copy(h_cstr_lower.data(), op_problem_scaled_.constraint_lower_bounds.data(), n_cstr, stream); - raft::copy(h_cstr_upper.data(), op_problem_scaled_.constraint_upper_bounds.data(), n_cstr, stream); + + std::vector h_obj (n_vars); + std::vector h_obj_scaled (n_vars); + std::vector h_var_bounds_packed (n_vars); + std::vector h_var_bounds_scaled_packed(n_vars); + std::vector h_cstr_lower (n_cstr); + std::vector h_cstr_upper (n_cstr); + std::vector h_cstr_lower_scaled(n_cstr); + std::vector h_cstr_upper_scaled(n_cstr); + + raft::copy(h_obj .data(), problem_ptr->objective_coefficients.data(), n_vars, stream); + raft::copy(h_obj_scaled .data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); + raft::copy(h_var_bounds_packed .data(), problem_ptr->variable_bounds.data(), n_vars, stream); + raft::copy(h_var_bounds_scaled_packed.data(), op_problem_scaled_.variable_bounds.data(), n_vars, stream); + raft::copy(h_cstr_lower .data(), problem_ptr->constraint_lower_bounds.data(), n_cstr, stream); + raft::copy(h_cstr_upper .data(), problem_ptr->constraint_upper_bounds.data(), n_cstr, stream); + raft::copy(h_cstr_lower_scaled .data(), op_problem_scaled_.constraint_lower_bounds.data(), n_cstr, stream); + raft::copy(h_cstr_upper_scaled .data(), op_problem_scaled_.constraint_upper_bounds.data(), n_cstr, stream); + + // 5. Get full scaling factors on host + std::vector h_cummulative_cstr_scaling(n_cstr); + std::vector h_cummulative_var_scaling (n_vars); + raft::copy(h_cummulative_cstr_scaling.data(), + initial_scaling_strategy_.get_constraint_matrix_scaling_vector().data(), + n_cstr, stream); + raft::copy(h_cummulative_var_scaling.data(), + initial_scaling_strategy_.get_variable_scaling_vector().data(), + n_vars, stream); + const f_t h_bound_rescaling = initial_scaling_strategy_.get_h_bound_rescaling(); + const f_t h_objective_rescaling = initial_scaling_strategy_.get_h_objective_rescaling(); + op_problem_scaled_.handle_ptr->sync_stream(stream); - - std::vector h_var_lower(n_vars), h_var_upper(n_vars); + + // Unpack interleaved {lower, upper} into separate vectors for both + // versions, so the shard ctor's slicing loop is uniform. + std::vector h_var_lower (n_vars), h_var_upper (n_vars); + std::vector h_var_lower_scaled(n_vars), h_var_upper_scaled(n_vars); for (i_t i = 0; i < n_vars; ++i) { - h_var_lower[i] = h_var_bounds_packed[i].x; - h_var_upper[i] = h_var_bounds_packed[i].y; + h_var_lower[i] = h_var_bounds_packed[i].x; + h_var_upper[i] = h_var_bounds_packed[i].y; + h_var_lower_scaled[i] = h_var_bounds_scaled_packed[i].x; + h_var_upper_scaled[i] = h_var_bounds_scaled_packed[i].y; } - // 5. Build per-rank data and meta-data + + // 6. Build per-rank data and meta-data. std::vector> sub_pdlp_rank_data = partition_loader_t::create_rank_data_from_parts( parts, - h_A_row_offsets, h_A_col_indices, h_A_values, - h_A_t_row_offsets, h_A_t_col_indices, h_A_t_values, + h_A_row_offsets, h_A_col_indices, + h_A_values, h_A_values_scaled, + h_A_t_row_offsets, h_A_t_col_indices, + h_A_t_values, h_A_t_values_scaled, settings.num_gpus, n_cstr, n_vars, nnz); - // 6. Build the per-shard PDLP settings: - // - single-GPU mode (num_gpus=1, no partition file) so sub-solvers don't recurse; - // - disable scaling (master already scaled the data we're handing out). - pdlp_solver_settings_t sub_pdlp_settings = settings; - sub_pdlp_settings.num_gpus = 1; - sub_pdlp_settings.multi_gpu_partition_file = ""; - sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; - sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; - - // 7. Construct the engine — this collectively bootstraps NCCL across all GPUs - // and constructs one shard per partition with the right slice of host data. + + // 7. Build the per-shard PDLP settings: + pdlp_solver_settings_t sub_pdlp_settings = settings; + sub_pdlp_settings.num_gpus = 1; + sub_pdlp_settings.multi_gpu_partition_file = ""; + sub_pdlp_settings.is_distributed_sub_pdlp = true; + sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; + sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; + sub_pdlp_settings.set_initial_step_size (initial_step_size_global); + sub_pdlp_settings.set_initial_primal_weight(initial_primal_weight_global); + + // 8. Construct the engine, creates NCCL comms and shards multi_gpu_engine.emplace( std::move(sub_pdlp_rank_data), - h_obj, h_var_lower, h_var_upper, h_cstr_lower, h_cstr_upper, + h_obj, h_var_lower, h_var_upper, h_cstr_lower, h_cstr_upper, + h_obj_scaled, h_var_lower_scaled, h_var_upper_scaled, h_cstr_lower_scaled, h_cstr_upper_scaled, + h_cummulative_cstr_scaling, h_cummulative_var_scaling, + h_bound_rescaling, h_objective_rescaling, op_problem_scaled_.maximize, op_problem_scaled_.objective_offset, op_problem_scaled_.presolve_data.objective_scaling_factor, @@ -2349,9 +2400,13 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co !settings_.get_initial_primal_weight().has_value()) compute_initial_primal_weight(); - // In multi-GPU mode the master scaled op_problem_scaled_ in its ctor before - // distributing data to the shards, so skip the second scaling pass here. - if (!multi_gpu_engine.has_value()) { + // Skip the in-loop scaling pass in both distributed roles: + // - The master pdlp_solver_t scaled op_problem_scaled_ in its multi-GPU + // ctor before shipping data to the shards (multi_gpu_engine present). + // - Each per-shard pdlp_solver_t received already-scaled + // op_problem_scaled_ + injected scaling state from the master, so it + // must not re-apply scale_problem() (is_distributed_sub_pdlp set). + if (!multi_gpu_engine.has_value() && !settings_.is_distributed_sub_pdlp) { initial_scaling_strategy_.scale_problem(); } diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index ef992d2a9e..532f038fbf 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -33,7 +33,6 @@ #include #include -#include "distributed_pdlp/multi_gpu_engine.hpp" namespace cuopt::linear_programming::detail { /** @@ -108,6 +107,13 @@ class pdlp_solver_t { void compute_initial_step_size(); void compute_initial_primal_weight(); + // Needed by multi-GPU to mutate them + problem_t& get_op_problem_scaled() { return op_problem_scaled_; } + detail::pdlp_initial_scaling_strategy_t& get_initial_scaling_strategy() + { + return initial_scaling_strategy_; + } + private: void print_termination_criteria(const timer_t& timer, bool is_average = false); void print_final_termination_criteria( From b5ebfd2a757e1f35bcb70af97559b1d2082c3451 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 20 May 2026 15:41:59 +0200 Subject: [PATCH 010/258] added pre loop setup need to manage boxing + style too --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 118 ++-- .../distributed_pdlp/multi_gpu_engine.hpp | 93 ++-- .../pdlp/distributed_pdlp/partition_loader.cu | 77 ++- cpp/src/pdlp/distributed_pdlp/rank_data.hpp | 101 ++-- cpp/src/pdlp/distributed_pdlp/shard.cu | 128 +++-- cpp/src/pdlp/distributed_pdlp/shard.hpp | 125 +++-- cpp/src/pdlp/pdlp.cu | 521 ++++++++++-------- cpp/src/pdlp/pdlp.cuh | 6 +- 8 files changed, 607 insertions(+), 562 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 9b404bbd53..fe95b1e5ff 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -3,45 +3,44 @@ * SPDX-License-Identifier: Apache-2.0 */ - #include +#include + +#include + +#include + +#include + +#include + +namespace cuopt::linear_programming::detail { - #include - - #include - - #include - - #include - - namespace cuopt::linear_programming::detail { - template multi_gpu_engine_t::multi_gpu_engine_t( - std::vector>&& rank_data, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - std::vector const& h_global_obj_scaled, - std::vector const& h_global_var_lower_scaled, - std::vector const& h_global_var_upper_scaled, - std::vector const& h_global_cstr_lower_scaled, - std::vector const& h_global_cstr_upper_scaled, - std::vector const& h_global_cummulative_cstr_scaling, - std::vector const& h_global_cummulative_var_scaling, - f_t h_bound_rescaling, - f_t h_objective_rescaling, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, - pdlp_solver_settings_t const& sub_solver_settings) + std::vector>&& rank_data, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + std::vector const& h_global_obj_scaled, + std::vector const& h_global_var_lower_scaled, + std::vector const& h_global_var_upper_scaled, + std::vector const& h_global_cstr_lower_scaled, + std::vector const& h_global_cstr_upper_scaled, + std::vector const& h_global_cummulative_cstr_scaling, + std::vector const& h_global_cummulative_var_scaling, + f_t h_bound_rescaling, + f_t h_objective_rescaling, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& sub_solver_settings) : stream() { const int nb_parts = static_cast(rank_data.size()); - cuopt_expects(nb_parts > 0, - error_type_t::ValidationError, - "multi_gpu_engine_t: rank_data must be non-empty"); + cuopt_expects( + nb_parts > 0, error_type_t::ValidationError, "multi_gpu_engine_t: rank_data must be non-empty"); shards.reserve(nb_parts); std::vector devices(nb_parts); @@ -56,32 +55,31 @@ multi_gpu_engine_t::multi_gpu_engine_t( // 3. Construct one shard per rank, pinned to its device. for (int r = 0; r < nb_parts; ++r) { raft::device_setter guard(devices[r]); // shard ctor needs device set - shards.emplace_back(std::make_unique>( - devices[r], - std::move(rank_data[r]), - raw_comms[r], - h_global_obj, - h_global_var_lower, - h_global_var_upper, - h_global_cstr_lower, - h_global_cstr_upper, - h_global_obj_scaled, - h_global_var_lower_scaled, - h_global_var_upper_scaled, - h_global_cstr_lower_scaled, - h_global_cstr_upper_scaled, - h_global_cummulative_cstr_scaling, - h_global_cummulative_var_scaling, - h_bound_rescaling, - h_objective_rescaling, - maximize, - objective_offset, - objective_scaling_factor, - sub_solver_settings)); + shards.emplace_back(std::make_unique>(devices[r], + std::move(rank_data[r]), + raw_comms[r], + h_global_obj, + h_global_var_lower, + h_global_var_upper, + h_global_cstr_lower, + h_global_cstr_upper, + h_global_obj_scaled, + h_global_var_lower_scaled, + h_global_var_upper_scaled, + h_global_cstr_lower_scaled, + h_global_cstr_upper_scaled, + h_global_cummulative_cstr_scaling, + h_global_cummulative_var_scaling, + h_bound_rescaling, + h_objective_rescaling, + maximize, + objective_offset, + objective_scaling_factor, + sub_solver_settings)); } } - - template struct multi_gpu_engine_t; - // template struct multi_gpu_engine_t; - - } // namespace cuopt::linear_programming::detail \ No newline at end of file + +template struct multi_gpu_engine_t; +// template struct multi_gpu_engine_t; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index d672e18197..e191a89d60 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -2,53 +2,52 @@ * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ - #pragma once - - #include - #include - - #include - - #include - - #include - #include - - namespace cuopt::linear_programming::detail { - +#pragma once + +#include +#include + +#include + +#include + +#include +#include + +namespace cuopt::linear_programming::detail { + template struct multi_gpu_engine_t { // Constructs shards from rank_data - multi_gpu_engine_t( - std::vector>&& rank_data, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - std::vector const& h_global_obj_scaled, - std::vector const& h_global_var_lower_scaled, - std::vector const& h_global_var_upper_scaled, - std::vector const& h_global_cstr_lower_scaled, - std::vector const& h_global_cstr_upper_scaled, - std::vector const& h_global_cummulative_cstr_scaling, - std::vector const& h_global_cummulative_var_scaling, - f_t h_bound_rescaling, - f_t h_objective_rescaling, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, - pdlp_solver_settings_t const& sub_solver_settings); - - multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; - multi_gpu_engine_t& operator=(const multi_gpu_engine_t&) = delete; - - // Engine-level stream for fork/join orchestration (master side). - rmm::cuda_stream stream; - - // Shards stored by unique_ptr because pdlp_shard_t is immovable - // (owns device-affine resources: handle, NCCL comm, RMM buffers). - std::vector>> shards; - }; - - } // namespace cuopt::linear_programming::detail \ No newline at end of file + multi_gpu_engine_t(std::vector>&& rank_data, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + std::vector const& h_global_obj_scaled, + std::vector const& h_global_var_lower_scaled, + std::vector const& h_global_var_upper_scaled, + std::vector const& h_global_cstr_lower_scaled, + std::vector const& h_global_cstr_upper_scaled, + std::vector const& h_global_cummulative_cstr_scaling, + std::vector const& h_global_cummulative_var_scaling, + f_t h_bound_rescaling, + f_t h_objective_rescaling, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& sub_solver_settings); + + multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; + multi_gpu_engine_t& operator=(const multi_gpu_engine_t&) = delete; + + // Engine-level stream for fork/join orchestration (master side). + rmm::cuda_stream stream; + + // Shards stored by unique_ptr because pdlp_shard_t is immovable + // (owns device-affine resources: handle, NCCL comm, RMM buffers). + std::vector>> shards; +}; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 047fb536d5..6c96e0b63d 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -18,9 +18,8 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ std::string const& file) { std::ifstream part_file(file); - cuopt_expects(part_file.is_open(), - error_type_t::ValidationError, - "Failed to open partition file: " + file); + cuopt_expects( + part_file.is_open(), error_type_t::ValidationError, "Failed to open partition file: " + file); // One integer per line; operator>> skips whitespace so blank lines and // trailing newlines are tolerated. @@ -39,8 +38,7 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ } template -std::vector> -partition_loader_t::create_rank_data_from_parts( +std::vector> partition_loader_t::create_rank_data_from_parts( const std::vector& parts, const std::vector& A_row_offsets, const std::vector& A_col_indices, @@ -76,7 +74,7 @@ partition_loader_t::create_rank_data_from_parts( // 2. Compute local matrices and rank_data for (i_t rank = 0; rank < nb_parts; rank++) { - auto& rd = rank_data[rank]; + auto& rd = rank_data[rank]; rd.owned_var_size = rd.owned_var_indices.size(); rd.owned_cstr_size = rd.owned_cstr_indices.size(); // ---- A side ---- @@ -93,11 +91,11 @@ partition_loader_t::create_rank_data_from_parts( // local arrays share identical (offsets, col_indices) and differ only // in values. for (auto owned_cstr : rd.owned_cstr_indices) { - i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; + i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; i_t row_start = A_row_offsets[owned_cstr]; for (i_t v = 0; v < cstr_len; v++) { local_A_col_indices.push_back(A_col_indices[row_start + v]); - local_A_values .push_back(A_values [row_start + v]); + local_A_values.push_back(A_values[row_start + v]); local_A_values_scaled.push_back(A_values_scaled[row_start + v]); } local_A_nnz += cstr_len; @@ -106,29 +104,25 @@ partition_loader_t::create_rank_data_from_parts( std::set needed_vars; for (auto indice : local_A_col_indices) { - if (var_parts[indice] != rank) - needed_vars.insert(indice); + if (var_parts[indice] != rank) needed_vars.insert(indice); } for (i_t peer = 0; peer < nb_parts; peer++) { std::vector needed_var_from_peer; for (auto needed_var : needed_vars) { - if (var_parts[needed_var] == peer) - needed_var_from_peer.push_back(needed_var); + if (var_parts[needed_var] == peer) needed_var_from_peer.push_back(needed_var); } - i_t nb_recv_from_peer = needed_var_from_peer.size(); + i_t nb_recv_from_peer = needed_var_from_peer.size(); rd.var_recv_counts[peer] = nb_recv_from_peer; rd.var_recv_offsets[peer] = - peer == 0 - ? 0 - : rd.var_recv_offsets[peer - 1] + rd.var_recv_counts[peer - 1]; + peer == 0 ? 0 : rd.var_recv_offsets[peer - 1] + rd.var_recv_counts[peer - 1]; rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer); } - rd.h_A_row_offsets = std::move(local_A_row_offsets); - rd.h_A_col_indices = std::move(local_A_col_indices); - rd.h_A_values = std::move(local_A_values); - rd.h_A_values_scaled = std::move(local_A_values_scaled); + rd.h_A_row_offsets = std::move(local_A_row_offsets); + rd.h_A_col_indices = std::move(local_A_col_indices); + rd.h_A_values = std::move(local_A_values); + rd.h_A_values_scaled = std::move(local_A_values_scaled); // ---- A_t side ---- std::vector local_A_t_row_offsets; @@ -139,11 +133,11 @@ partition_loader_t::create_rank_data_from_parts( local_A_t_row_offsets.push_back(local_A_t_nnz); for (auto owned_var : rd.owned_var_indices) { - i_t var_len = A_t_row_offsets[owned_var + 1] - A_t_row_offsets[owned_var]; + i_t var_len = A_t_row_offsets[owned_var + 1] - A_t_row_offsets[owned_var]; i_t row_start = A_t_row_offsets[owned_var]; for (i_t v = 0; v < var_len; v++) { - local_A_t_col_indices .push_back(A_t_col_indices [row_start + v]); - local_A_t_values .push_back(A_t_values [row_start + v]); + local_A_t_col_indices.push_back(A_t_col_indices[row_start + v]); + local_A_t_values.push_back(A_t_values[row_start + v]); local_A_t_values_scaled.push_back(A_t_values_scaled[row_start + v]); } local_A_t_nnz += var_len; @@ -152,31 +146,27 @@ partition_loader_t::create_rank_data_from_parts( std::set needed_cstrs; for (auto indice : local_A_t_col_indices) { - if (cstr_parts[indice] != rank) - needed_cstrs.insert(indice); + if (cstr_parts[indice] != rank) needed_cstrs.insert(indice); } for (i_t peer = 0; peer < nb_parts; peer++) { std::vector needed_cstr_from_peer; for (auto needed_cstr : needed_cstrs) { - if (cstr_parts[needed_cstr] == peer) - needed_cstr_from_peer.push_back(needed_cstr); + if (cstr_parts[needed_cstr] == peer) needed_cstr_from_peer.push_back(needed_cstr); } - i_t nb_recv_from_peer = needed_cstr_from_peer.size(); + i_t nb_recv_from_peer = needed_cstr_from_peer.size(); rd.cstr_recv_counts[peer] = nb_recv_from_peer; rd.cstr_recv_offsets[peer] = - peer == 0 - ? 0 - : rd.cstr_recv_offsets[peer - 1] + rd.cstr_recv_counts[peer - 1]; + peer == 0 ? 0 : rd.cstr_recv_offsets[peer - 1] + rd.cstr_recv_counts[peer - 1]; rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer); } - rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); - rd.h_A_t_col_indices = std::move(local_A_t_col_indices); - rd.h_A_t_values = std::move(local_A_t_values); - rd.h_A_t_values_scaled = std::move(local_A_t_values_scaled); + rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); + rd.h_A_t_col_indices = std::move(local_A_t_col_indices); + rd.h_A_t_values = std::move(local_A_t_values); + rd.h_A_t_values_scaled = std::move(local_A_t_values_scaled); - rd.total_var_size = rd.owned_var_size + needed_vars.size(); + rd.total_var_size = rd.owned_var_size + needed_vars.size(); rd.total_cstr_size = rd.owned_cstr_size + needed_cstrs.size(); } @@ -195,7 +185,8 @@ partition_loader_t::create_rank_data_from_parts( if (peer == rank) continue; for (auto recv_cstr : rank_data[peer].cstr_send_per_peer[rank]) { rd.global_to_local_cstr[recv_cstr] = curr_id; - // rd.local_to_global_cstr.push_back(recv_cstr); // Not needed, we only do local_to_global on owned side + // rd.local_to_global_cstr.push_back(recv_cstr); // Not needed, we only do local_to_global + // on owned side curr_id++; } } @@ -221,14 +212,18 @@ partition_loader_t::create_rank_data_from_parts( auto& rd = rank_data[rank]; for (auto& send_vec : rd.var_send_per_peer) { - for (auto& v : send_vec) v = rd.global_to_local_var.at(v); + for (auto& v : send_vec) + v = rd.global_to_local_var.at(v); } for (auto& send_vec : rd.cstr_send_per_peer) { - for (auto& v : send_vec) v = rd.global_to_local_cstr.at(v); + for (auto& v : send_vec) + v = rd.global_to_local_cstr.at(v); } - for (auto& v : rd.h_A_col_indices) v = rd.global_to_local_var.at(v); - for (auto& v : rd.h_A_t_col_indices) v = rd.global_to_local_cstr.at(v); + for (auto& v : rd.h_A_col_indices) + v = rd.global_to_local_var.at(v); + for (auto& v : rd.h_A_t_col_indices) + v = rd.global_to_local_cstr.at(v); } return rank_data; diff --git a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp index 29d76ae110..d52d277116 100644 --- a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp +++ b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp @@ -1,54 +1,61 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + #pragma once -#include #include +#include namespace cuopt::linear_programming::detail { template struct rank_data_t { - rank_data_t(std::size_t nb_parts) - : var_send_per_peer(nb_parts), - cstr_send_per_peer(nb_parts), - var_recv_counts(nb_parts, 0), - var_recv_offsets(nb_parts, 0), - cstr_recv_counts(nb_parts, 0), - cstr_recv_offsets(nb_parts, 0) {} - - i_t owned_var_size{0}; - i_t total_var_size{0}; - i_t owned_cstr_size{0}; - i_t total_cstr_size{0}; - - // === Ownership === - std::vector owned_var_indices; - std::vector owned_cstr_indices; - - // === Send plan: per peer, indices to gather + send === - std::vector> var_send_per_peer; - std::vector> cstr_send_per_peer; - - // === Recv plan: per peer, contiguous slot in halo region === - std::vector var_recv_counts; - std::vector var_recv_offsets; - std::vector cstr_recv_counts; - std::vector cstr_recv_offsets; - - // === Mappings === - std::unordered_map global_to_local_var; - std::unordered_map global_to_local_cstr; - std::vector local_to_global_var; - std::vector local_to_global_cstr; - - // === Local host CSR matrices === - // A - std::vector h_A_row_offsets; - std::vector h_A_col_indices; - std::vector h_A_values; - std::vector h_A_values_scaled; - // A_t - std::vector h_A_t_row_offsets; - std::vector h_A_t_col_indices; - std::vector h_A_t_values; - std::vector h_A_t_values_scaled; - }; -} // namespace cuopt::linear_programming::detail \ No newline at end of file + rank_data_t(std::size_t nb_parts) + : var_send_per_peer(nb_parts), + cstr_send_per_peer(nb_parts), + var_recv_counts(nb_parts, 0), + var_recv_offsets(nb_parts, 0), + cstr_recv_counts(nb_parts, 0), + cstr_recv_offsets(nb_parts, 0) + { + } + + i_t owned_var_size{0}; + i_t total_var_size{0}; + i_t owned_cstr_size{0}; + i_t total_cstr_size{0}; + + // === Ownership === + std::vector owned_var_indices; + std::vector owned_cstr_indices; + + // === Send plan: per peer, indices to gather + send === + std::vector> var_send_per_peer; + std::vector> cstr_send_per_peer; + + // === Recv plan: per peer, contiguous slot in halo region === + std::vector var_recv_counts; + std::vector var_recv_offsets; + std::vector cstr_recv_counts; + std::vector cstr_recv_offsets; + + // === Mappings === + std::unordered_map global_to_local_var; + std::unordered_map global_to_local_cstr; + std::vector local_to_global_var; + std::vector local_to_global_cstr; + + // === Local host CSR matrices === + // A + std::vector h_A_row_offsets; + std::vector h_A_col_indices; + std::vector h_A_values; + std::vector h_A_values_scaled; + // A_t + std::vector h_A_t_row_offsets; + std::vector h_A_t_col_indices; + std::vector h_A_t_values; + std::vector h_A_t_values_scaled; +}; +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 41f74086ab..596a08a3dc 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -22,28 +22,27 @@ template pdlp_shard_t::~pdlp_shard_t() = default; template -pdlp_shard_t::pdlp_shard_t( - int device_id, - rank_data_t&& rd, - ncclComm_t raw_comm, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - std::vector const& h_global_obj_scaled, - std::vector const& h_global_var_lower_scaled, - std::vector const& h_global_var_upper_scaled, - std::vector const& h_global_cstr_lower_scaled, - std::vector const& h_global_cstr_upper_scaled, - std::vector const& h_global_cummulative_cstr_scaling, - std::vector const& h_global_cummulative_var_scaling, - f_t h_bound_rescaling, - f_t h_objective_rescaling, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, - pdlp_solver_settings_t const& settings) +pdlp_shard_t::pdlp_shard_t(int device_id, + rank_data_t&& rd, + ncclComm_t raw_comm, + std::vector const& h_global_obj, + std::vector const& h_global_var_lower, + std::vector const& h_global_var_upper, + std::vector const& h_global_cstr_lower, + std::vector const& h_global_cstr_upper, + std::vector const& h_global_obj_scaled, + std::vector const& h_global_var_lower_scaled, + std::vector const& h_global_var_upper_scaled, + std::vector const& h_global_cstr_lower_scaled, + std::vector const& h_global_cstr_upper_scaled, + std::vector const& h_global_cummulative_cstr_scaling, + std::vector const& h_global_cummulative_var_scaling, + f_t h_bound_rescaling, + f_t h_objective_rescaling, + bool maximize, + f_t objective_offset, + f_t objective_scaling_factor, + pdlp_solver_settings_t const& settings) : device_id(device_id), stream(), handle(stream.view()), @@ -53,22 +52,27 @@ pdlp_shard_t::pdlp_shard_t( sub_problem(std::nullopt), sub_pdlp(nullptr) { - assert(raft::device_setter::get_current_device() == device_id && "Right device must be set before building the shard"); + assert(raft::device_setter::get_current_device() == device_id && + "Right device must be set before building the shard"); // ---- 1. Gather per-shard host slices using rank_data's index maps. ---- // All vectors are sized to TOTAL (owned + halo). Owned slots get real // values; halo slots keep defaults because they should not be accessed - std::vector h_obj (rank_data.total_var_size, f_t{0}); - std::vector h_var_lower (rank_data.total_var_size, -std::numeric_limits::infinity()); - std::vector h_var_upper (rank_data.total_var_size, std::numeric_limits::infinity()); - std::vector h_cstr_lower (rank_data.total_cstr_size, -std::numeric_limits::infinity()); - std::vector h_cstr_upper (rank_data.total_cstr_size, std::numeric_limits::infinity()); - - std::vector h_obj_scaled (rank_data.total_var_size, f_t{0}); - std::vector h_var_lower_scaled (rank_data.total_var_size, -std::numeric_limits::infinity()); - std::vector h_var_upper_scaled (rank_data.total_var_size, std::numeric_limits::infinity()); - std::vector h_cstr_lower_scaled(rank_data.total_cstr_size, -std::numeric_limits::infinity()); - std::vector h_cstr_upper_scaled(rank_data.total_cstr_size, std::numeric_limits::infinity()); + std::vector h_obj(rank_data.total_var_size, f_t{0}); + std::vector h_var_lower(rank_data.total_var_size, -std::numeric_limits::infinity()); + std::vector h_var_upper(rank_data.total_var_size, std::numeric_limits::infinity()); + std::vector h_cstr_lower(rank_data.total_cstr_size, -std::numeric_limits::infinity()); + std::vector h_cstr_upper(rank_data.total_cstr_size, std::numeric_limits::infinity()); + + std::vector h_obj_scaled(rank_data.total_var_size, f_t{0}); + std::vector h_var_lower_scaled(rank_data.total_var_size, + -std::numeric_limits::infinity()); + std::vector h_var_upper_scaled(rank_data.total_var_size, + std::numeric_limits::infinity()); + std::vector h_cstr_lower_scaled(rank_data.total_cstr_size, + -std::numeric_limits::infinity()); + std::vector h_cstr_upper_scaled(rank_data.total_cstr_size, + std::numeric_limits::infinity()); for (i_t i = 0; i < rank_data.owned_var_size; ++i) { const auto g = rank_data.local_to_global_var[i]; @@ -89,7 +93,7 @@ pdlp_shard_t::pdlp_shard_t( // Get local scaling factors std::vector h_cstr_scaling_local(rank_data.total_cstr_size, f_t{1}); - std::vector h_var_scaling_local (rank_data.total_var_size, f_t{1}); + std::vector h_var_scaling_local(rank_data.total_var_size, f_t{1}); for (i_t i = 0; i < rank_data.owned_cstr_size; ++i) { h_cstr_scaling_local[i] = h_global_cummulative_cstr_scaling[rank_data.local_to_global_cstr[i]]; } @@ -99,15 +103,17 @@ pdlp_shard_t::pdlp_shard_t( // ---- 2. Build optimization_problem_t on this shard's device (UNSCALED). ---- opt_problem.emplace(&handle); - opt_problem->set_csr_constraint_matrix( - rank_data.h_A_values .data(), static_cast(rank_data.h_A_values .size()), - rank_data.h_A_col_indices.data(), static_cast(rank_data.h_A_col_indices.size()), - rank_data.h_A_row_offsets.data(), static_cast(rank_data.h_A_row_offsets.size())); + opt_problem->set_csr_constraint_matrix(rank_data.h_A_values.data(), + static_cast(rank_data.h_A_values.size()), + rank_data.h_A_col_indices.data(), + static_cast(rank_data.h_A_col_indices.size()), + rank_data.h_A_row_offsets.data(), + static_cast(rank_data.h_A_row_offsets.size())); // Primal axis: TOTAL (owned + halo). Halo slots have neutral defaults. - opt_problem->set_objective_coefficients(h_obj .data(), rank_data.total_var_size); - opt_problem->set_variable_lower_bounds (h_var_lower.data(), rank_data.total_var_size); - opt_problem->set_variable_upper_bounds (h_var_upper.data(), rank_data.total_var_size); + opt_problem->set_objective_coefficients(h_obj.data(), rank_data.total_var_size); + opt_problem->set_variable_lower_bounds(h_var_lower.data(), rank_data.total_var_size); + opt_problem->set_variable_upper_bounds(h_var_upper.data(), rank_data.total_var_size); // Dual axis: TOTAL (owned + halo). Halo slots have ±inf so trivially satisfied. opt_problem->set_constraint_lower_bounds(h_cstr_lower.data(), rank_data.total_cstr_size); @@ -126,18 +132,21 @@ pdlp_shard_t::pdlp_shard_t( // in multi-GPU: A_local is owned_cstr x total_var, and A_t_local is the // pre-sliced owned_var x total_cstr matrix we built during partitioning. auto stream_view = handle.get_stream(); - sub_problem->reverse_offsets .resize(rank_data.h_A_t_row_offsets.size(), stream_view); - sub_problem->reverse_constraints .resize(rank_data.h_A_t_col_indices.size(), stream_view); - sub_problem->reverse_coefficients.resize(rank_data.h_A_t_values .size(), stream_view); + sub_problem->reverse_offsets.resize(rank_data.h_A_t_row_offsets.size(), stream_view); + sub_problem->reverse_constraints.resize(rank_data.h_A_t_col_indices.size(), stream_view); + sub_problem->reverse_coefficients.resize(rank_data.h_A_t_values.size(), stream_view); raft::copy(sub_problem->reverse_offsets.data(), rank_data.h_A_t_row_offsets.data(), - rank_data.h_A_t_row_offsets.size(), stream_view); + rank_data.h_A_t_row_offsets.size(), + stream_view); raft::copy(sub_problem->reverse_constraints.data(), rank_data.h_A_t_col_indices.data(), - rank_data.h_A_t_col_indices.size(), stream_view); + rank_data.h_A_t_col_indices.size(), + stream_view); raft::copy(sub_problem->reverse_coefficients.data(), rank_data.h_A_t_values.data(), - rank_data.h_A_t_values.size(), stream_view); + rank_data.h_A_t_values.size(), + stream_view); handle.sync_stream(stream_view); // ---- 5. Build sub_pdlp (single-GPU mode; multi_gpu flags cleared by caller). ---- @@ -150,16 +159,22 @@ pdlp_shard_t::pdlp_shard_t( auto& scaled = sub_pdlp->get_op_problem_scaled(); raft::copy(scaled.coefficients.data(), rank_data.h_A_values_scaled.data(), - rank_data.h_A_values_scaled.size(), stream_view); + rank_data.h_A_values_scaled.size(), + stream_view); raft::copy(scaled.reverse_coefficients.data(), rank_data.h_A_t_values_scaled.data(), - rank_data.h_A_t_values_scaled.size(), stream_view); - raft::copy(scaled.objective_coefficients.data(), - h_obj_scaled.data(), h_obj_scaled.size(), stream_view); + rank_data.h_A_t_values_scaled.size(), + stream_view); + raft::copy( + scaled.objective_coefficients.data(), h_obj_scaled.data(), h_obj_scaled.size(), stream_view); raft::copy(scaled.constraint_lower_bounds.data(), - h_cstr_lower_scaled.data(), h_cstr_lower_scaled.size(), stream_view); + h_cstr_lower_scaled.data(), + h_cstr_lower_scaled.size(), + stream_view); raft::copy(scaled.constraint_upper_bounds.data(), - h_cstr_upper_scaled.data(), h_cstr_upper_scaled.size(), stream_view); + h_cstr_upper_scaled.data(), + h_cstr_upper_scaled.size(), + stream_view); using f_t2 = typename type_2::type; std::vector h_var_bounds_scaled_packed(rank_data.total_var_size); @@ -169,14 +184,15 @@ pdlp_shard_t::pdlp_shard_t( } raft::copy(scaled.variable_bounds.data(), h_var_bounds_scaled_packed.data(), - h_var_bounds_scaled_packed.size(), stream_view); + h_var_bounds_scaled_packed.size(), + stream_view); combine_constraint_bounds(scaled, scaled.combined_bounds); // Inject master-scaled buffers inside sub_pdlp.initil_strategy auto& scaling = sub_pdlp->get_initial_scaling_strategy(); scaling.set_cummulative_scaling(h_cstr_scaling_local, h_var_scaling_local); - scaling.set_h_bound_rescaling (h_bound_rescaling); + scaling.set_h_bound_rescaling(h_bound_rescaling); scaling.set_h_objective_rescaling(h_objective_rescaling); handle.sync_stream(stream_view); diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 3c10a90f90..a5ff89c5c4 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -2,49 +2,49 @@ * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ - #pragma once +#pragma once + +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +// Forward-declare to break the cyclic include with pdlp.cuh +// (pdlp.cuh -> multi_gpu_engine.hpp -> shard.hpp -> pdlp.cuh). +// Definitions of out-of-line members live in shard.cu, which includes pdlp.cuh. +template +class pdlp_solver_t; + +// RAII deleter for ncclComm_t; sets the right device before destroy. +struct nccl_comm_deleter_t { + int device_id{-1}; + void operator()(ncclComm* comm) const noexcept + { + if (comm == nullptr) return; + raft::device_setter guard(device_id); + ncclCommDestroy(comm); + } +}; +using nccl_comm_unique_ptr_t = std::unique_ptr; + +template +struct pdlp_shard_t { + // Out-of-line (in shard.cu) because pdlp_solver_t is incomplete here. + ~pdlp_shard_t(); - #include - - #include - #include - #include - - #include - #include - #include - - #include - - #include - #include - #include - - namespace cuopt::linear_programming::detail { - - // Forward-declare to break the cyclic include with pdlp.cuh - // (pdlp.cuh -> multi_gpu_engine.hpp -> shard.hpp -> pdlp.cuh). - // Definitions of out-of-line members live in shard.cu, which includes pdlp.cuh. - template - class pdlp_solver_t; - - // RAII deleter for ncclComm_t; sets the right device before destroy. - struct nccl_comm_deleter_t { - int device_id{-1}; - void operator()(ncclComm* comm) const noexcept - { - if (comm == nullptr) return; - raft::device_setter guard(device_id); - ncclCommDestroy(comm); - } - }; - using nccl_comm_unique_ptr_t = std::unique_ptr; - - template - struct pdlp_shard_t { - // Out-of-line (in shard.cu) because pdlp_solver_t is incomplete here. - ~pdlp_shard_t(); - // sub worker for distributed pdlp. Owns its own view on scaled problem and unscaled problem // Owns necessary multi-gpu data (rank_data, device_id, nccl_comm) pdlp_shard_t(int device_id, @@ -65,25 +65,24 @@ f_t h_bound_rescaling, f_t h_objective_rescaling, bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, + f_t objective_offset, + f_t objective_scaling_factor, pdlp_solver_settings_t const& settings); - - pdlp_shard_t(const pdlp_shard_t&) = delete; - pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; - // Move ops are implicitly deleted (user-declared dtor + deleted copy). - // Intentional: shard owns device-affine resources and must never move. - // Store as std::unique_ptr in any container. - - int device_id; - rmm::cuda_stream stream; - raft::handle_t handle; - nccl_comm_unique_ptr_t comm; - rank_data_t rank_data; - std::optional> opt_problem; - std::optional> sub_problem; - std::unique_ptr> sub_pdlp; - }; - - } // namespace cuopt::linear_programming::detail - \ No newline at end of file + + pdlp_shard_t(const pdlp_shard_t&) = delete; + pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; + // Move ops are implicitly deleted (user-declared dtor + deleted copy). + // Intentional: shard owns device-affine resources and must never move. + // Store as std::unique_ptr in any container. + + int device_id; + rmm::cuda_stream stream; + raft::handle_t handle; + nccl_comm_unique_ptr_t comm; + rank_data_t rank_data; + std::optional> opt_problem; + std::optional> sub_problem; + std::unique_ptr> sub_pdlp; +}; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 612eb676ec..2a36c160fd 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -333,23 +333,28 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, parts = partition_loader_t::parse_distributed_pdlp_partition_file( settings.multi_gpu_partition_file); } else { - cuopt_expects(false, error_type_t::NotImplemented, + cuopt_expects(false, + error_type_t::NotImplemented, "Metis partitioning inside cuopt not implemented yet; " "provide a --parts file via settings.multi_gpu_partition_file"); } - // always compute initial step size before scaling and primal_weight after scaling to do like cuPDLPx - assert(settings_.hyper_params.compute_initial_primal_weight_before_scaling && "compute_initial_primal_weight_before_scaling must be true in distributed mode"); - assert(!settings_.hyper_params.compute_initial_step_size_before_scaling && "compute_initial_step_size_before_scaling must be false in distributed mode"); - + // always compute initial step size before scaling and primal_weight after scaling to do like + // cuPDLPx + assert(settings_.hyper_params.compute_initial_primal_weight_before_scaling && + "compute_initial_primal_weight_before_scaling must be true in distributed mode"); + assert(!settings_.hyper_params.compute_initial_step_size_before_scaling && + "compute_initial_step_size_before_scaling must be false in distributed mode"); + compute_initial_primal_weight(); - + // scale globally before dispatching to shards initial_scaling_strategy_.scale_problem(); - + compute_initial_step_size(); + step_size_strategy_.get_primal_and_dual_stepsizes(primal_step_size_, dual_step_size_); - const f_t initial_step_size_global = get_step_size_h(0); + const f_t initial_step_size_global = get_step_size_h(0); const f_t initial_primal_weight_global = get_primal_weight_h(0); // 4. Copy both scaled and unscaled pb @@ -359,54 +364,61 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, i_t const nnz = op_problem_scaled_.nnz; // Shared topology (taken from the scaled problem, but identical on both). - std::vector h_A_row_offsets (n_cstr + 1); - std::vector h_A_col_indices (nnz); + std::vector h_A_row_offsets(n_cstr + 1); + std::vector h_A_col_indices(nnz); std::vector h_A_t_row_offsets(n_vars + 1); std::vector h_A_t_col_indices(nnz); - raft::copy(h_A_row_offsets .data(), op_problem_scaled_.offsets .data(), n_cstr + 1, stream); - raft::copy(h_A_col_indices .data(), op_problem_scaled_.variables .data(), nnz, stream); - raft::copy(h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets .data(), n_vars + 1, stream); - raft::copy(h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints.data(), nnz, stream); + raft::copy(h_A_row_offsets.data(), op_problem_scaled_.offsets.data(), n_cstr + 1, stream); + raft::copy(h_A_col_indices.data(), op_problem_scaled_.variables.data(), nnz, stream); + raft::copy( + h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets.data(), n_vars + 1, stream); + raft::copy(h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints.data(), nnz, stream); // Paired value arrays for A and A_T. - std::vector h_A_values (nnz); - std::vector h_A_values_scaled (nnz); - std::vector h_A_t_values (nnz); + std::vector h_A_values(nnz); + std::vector h_A_values_scaled(nnz); + std::vector h_A_t_values(nnz); std::vector h_A_t_values_scaled(nnz); - raft::copy(h_A_values .data(), problem_ptr->coefficients .data(), nnz, stream); - raft::copy(h_A_t_values .data(), problem_ptr->reverse_coefficients .data(), nnz, stream); - raft::copy(h_A_values_scaled .data(), op_problem_scaled_.coefficients .data(), nnz, stream); - raft::copy(h_A_t_values_scaled.data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); + raft::copy(h_A_values.data(), problem_ptr->coefficients.data(), nnz, stream); + raft::copy(h_A_t_values.data(), problem_ptr->reverse_coefficients.data(), nnz, stream); + raft::copy(h_A_values_scaled.data(), op_problem_scaled_.coefficients.data(), nnz, stream); + raft::copy( + h_A_t_values_scaled.data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); using f_t2 = typename type_2::type; - std::vector h_obj (n_vars); - std::vector h_obj_scaled (n_vars); - std::vector h_var_bounds_packed (n_vars); + std::vector h_obj(n_vars); + std::vector h_obj_scaled(n_vars); + std::vector h_var_bounds_packed(n_vars); std::vector h_var_bounds_scaled_packed(n_vars); - std::vector h_cstr_lower (n_cstr); - std::vector h_cstr_upper (n_cstr); - std::vector h_cstr_lower_scaled(n_cstr); - std::vector h_cstr_upper_scaled(n_cstr); - - raft::copy(h_obj .data(), problem_ptr->objective_coefficients.data(), n_vars, stream); - raft::copy(h_obj_scaled .data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); - raft::copy(h_var_bounds_packed .data(), problem_ptr->variable_bounds.data(), n_vars, stream); - raft::copy(h_var_bounds_scaled_packed.data(), op_problem_scaled_.variable_bounds.data(), n_vars, stream); - raft::copy(h_cstr_lower .data(), problem_ptr->constraint_lower_bounds.data(), n_cstr, stream); - raft::copy(h_cstr_upper .data(), problem_ptr->constraint_upper_bounds.data(), n_cstr, stream); - raft::copy(h_cstr_lower_scaled .data(), op_problem_scaled_.constraint_lower_bounds.data(), n_cstr, stream); - raft::copy(h_cstr_upper_scaled .data(), op_problem_scaled_.constraint_upper_bounds.data(), n_cstr, stream); + std::vector h_cstr_lower(n_cstr); + std::vector h_cstr_upper(n_cstr); + std::vector h_cstr_lower_scaled(n_cstr); + std::vector h_cstr_upper_scaled(n_cstr); + + raft::copy(h_obj.data(), problem_ptr->objective_coefficients.data(), n_vars, stream); + raft::copy(h_obj_scaled.data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); + raft::copy(h_var_bounds_packed.data(), problem_ptr->variable_bounds.data(), n_vars, stream); + raft::copy( + h_var_bounds_scaled_packed.data(), op_problem_scaled_.variable_bounds.data(), n_vars, stream); + raft::copy(h_cstr_lower.data(), problem_ptr->constraint_lower_bounds.data(), n_cstr, stream); + raft::copy(h_cstr_upper.data(), problem_ptr->constraint_upper_bounds.data(), n_cstr, stream); + raft::copy( + h_cstr_lower_scaled.data(), op_problem_scaled_.constraint_lower_bounds.data(), n_cstr, stream); + raft::copy( + h_cstr_upper_scaled.data(), op_problem_scaled_.constraint_upper_bounds.data(), n_cstr, stream); // 5. Get full scaling factors on host std::vector h_cummulative_cstr_scaling(n_cstr); - std::vector h_cummulative_var_scaling (n_vars); + std::vector h_cummulative_var_scaling(n_vars); raft::copy(h_cummulative_cstr_scaling.data(), initial_scaling_strategy_.get_constraint_matrix_scaling_vector().data(), - n_cstr, stream); + n_cstr, + stream); raft::copy(h_cummulative_var_scaling.data(), initial_scaling_strategy_.get_variable_scaling_vector().data(), - n_vars, stream); + n_vars, + stream); const f_t h_bound_rescaling = initial_scaling_strategy_.get_h_bound_rescaling(); const f_t h_objective_rescaling = initial_scaling_strategy_.get_h_objective_rescaling(); @@ -414,7 +426,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, // Unpack interleaved {lower, upper} into separate vectors for both // versions, so the shard ctor's slicing loop is uniform. - std::vector h_var_lower (n_vars), h_var_upper (n_vars); + std::vector h_var_lower(n_vars), h_var_upper(n_vars); std::vector h_var_lower_scaled(n_vars), h_var_upper_scaled(n_vars); for (i_t i = 0; i < n_vars; ++i) { h_var_lower[i] = h_var_bounds_packed[i].x; @@ -425,35 +437,58 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, // 6. Build per-rank data and meta-data. std::vector> sub_pdlp_rank_data = - partition_loader_t::create_rank_data_from_parts( - parts, - h_A_row_offsets, h_A_col_indices, - h_A_values, h_A_values_scaled, - h_A_t_row_offsets, h_A_t_col_indices, - h_A_t_values, h_A_t_values_scaled, - settings.num_gpus, n_cstr, n_vars, nnz); + partition_loader_t::create_rank_data_from_parts(parts, + h_A_row_offsets, + h_A_col_indices, + h_A_values, + h_A_values_scaled, + h_A_t_row_offsets, + h_A_t_col_indices, + h_A_t_values, + h_A_t_values_scaled, + settings.num_gpus, + n_cstr, + n_vars, + nnz); // 7. Build the per-shard PDLP settings: - pdlp_solver_settings_t sub_pdlp_settings = settings; - sub_pdlp_settings.num_gpus = 1; - sub_pdlp_settings.multi_gpu_partition_file = ""; - sub_pdlp_settings.is_distributed_sub_pdlp = true; - sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; - sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; - sub_pdlp_settings.set_initial_step_size (initial_step_size_global); - sub_pdlp_settings.set_initial_primal_weight(initial_primal_weight_global); + pdlp_solver_settings_t sub_pdlp_settings = settings; + sub_pdlp_settings.num_gpus = 1; + sub_pdlp_settings.multi_gpu_partition_file = ""; + sub_pdlp_settings.is_distributed_sub_pdlp = true; + sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; + sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; // 8. Construct the engine, creates NCCL comms and shards - multi_gpu_engine.emplace( - std::move(sub_pdlp_rank_data), - h_obj, h_var_lower, h_var_upper, h_cstr_lower, h_cstr_upper, - h_obj_scaled, h_var_lower_scaled, h_var_upper_scaled, h_cstr_lower_scaled, h_cstr_upper_scaled, - h_cummulative_cstr_scaling, h_cummulative_var_scaling, - h_bound_rescaling, h_objective_rescaling, - op_problem_scaled_.maximize, - op_problem_scaled_.objective_offset, - op_problem_scaled_.presolve_data.objective_scaling_factor, - sub_pdlp_settings); + multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), + h_obj, + h_var_lower, + h_var_upper, + h_cstr_lower, + h_cstr_upper, + h_obj_scaled, + h_var_lower_scaled, + h_var_upper_scaled, + h_cstr_lower_scaled, + h_cstr_upper_scaled, + h_cummulative_cstr_scaling, + h_cummulative_var_scaling, + h_bound_rescaling, + h_objective_rescaling, + op_problem_scaled_.maximize, + op_problem_scaled_.objective_offset, + op_problem_scaled_.presolve_data.objective_scaling_factor, + sub_pdlp_settings); + + for (auto& shard : multi_gpu_engine.shards) { + raft::device_setter guard(shard->device_id); + auto& sub = *shard->sub_pdlp; + raft::copy(sub.step_size_.data(), step_size_.data(), 1, shard->stream); + raft::copy(sub.primal_weight_.data(), primal_weight_.data(), 1, shard->stream); + raft::copy(sub.best_primal_weight_.data(), best_primal_weight_.data(), 1, shard->stream); + raft::copy(sub.primal_step_size_.data(), primal_step_size_.data(), 1, shard->stream); + raft::copy(sub.dual_step_size_.data(), dual_step_size_.data(), 1, shard->stream); + } } template @@ -2392,219 +2427,215 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co std::cout << "Starting PDLP loop:" << std::endl; #endif - // TODO handle that properly - if (settings_.hyper_params.compute_initial_step_size_before_scaling && - !settings_.get_initial_step_size().has_value()) - compute_initial_step_size(); - if (settings_.hyper_params.compute_initial_primal_weight_before_scaling && - !settings_.get_initial_primal_weight().has_value()) - compute_initial_primal_weight(); - - // Skip the in-loop scaling pass in both distributed roles: - // - The master pdlp_solver_t scaled op_problem_scaled_ in its multi-GPU - // ctor before shipping data to the shards (multi_gpu_engine present). - // - Each per-shard pdlp_solver_t received already-scaled - // op_problem_scaled_ + injected scaling state from the master, so it - // must not re-apply scale_problem() (is_distributed_sub_pdlp set). - if (!multi_gpu_engine.has_value() && !settings_.is_distributed_sub_pdlp) { + // In distributed mode, skip all setup, it is done before + if (!settings_.hyper_params.use_distributed_pdlp) { + // TODO handle that properly + if (settings_.hyper_params.compute_initial_step_size_before_scaling && + !settings_.get_initial_step_size().has_value()) + compute_initial_step_size(); + if (settings_.hyper_params.compute_initial_primal_weight_before_scaling && + !settings_.get_initial_primal_weight().has_value()) + compute_initial_primal_weight(); + initial_scaling_strategy_.scale_problem(); - } - // Update FP32 matrix copies for mixed precision SpMV after scaling - pdhg_solver_.get_cusparse_view().update_mixed_precision_matrices(); + // Update FP32 matrix copies for mixed precision SpMV after scaling + pdhg_solver_.get_cusparse_view().update_mixed_precision_matrices(); - if (!settings_.hyper_params.compute_initial_step_size_before_scaling && - !settings_.get_initial_step_size().has_value()) - compute_initial_step_size(); - if (!settings_.hyper_params.compute_initial_primal_weight_before_scaling && - !settings_.get_initial_primal_weight().has_value()) - compute_initial_primal_weight(); + if (!settings_.hyper_params.compute_initial_step_size_before_scaling && + !settings_.get_initial_step_size().has_value()) + compute_initial_step_size(); + if (!settings_.hyper_params.compute_initial_primal_weight_before_scaling && + !settings_.get_initial_primal_weight().has_value()) + compute_initial_primal_weight(); #ifdef PDLP_DEBUG_MODE - std::cout << "Initial Scaling done" << std::endl; + std::cout << "Initial Scaling done" << std::endl; #endif - - // Needs to be performed here before the below line to make sure the initial primal_weight / step - // size are used as previous point when potentially updating them in this next call - if (settings_.get_initial_step_size().has_value() || initial_step_size_.has_value()) { - if (initial_step_size_.has_value()) - thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), - step_size_.begin(), - step_size_.end(), - initial_step_size_.value()); - else - thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), - step_size_.begin(), - step_size_.end(), - settings_.get_initial_step_size().value()); - } - if (settings_.get_initial_primal_weight().has_value() || initial_primal_weight_.has_value()) { - if (initial_primal_weight_.has_value()) { - thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), - primal_weight_.begin(), - primal_weight_.end(), - initial_primal_weight_.value()); - if (is_cupdlpx_restart(settings_.hyper_params)) + // Needs to be performed here before the below line to make sure the initial primal_weight / + // step size are used as previous point when potentially updating them in this next call + if (settings_.get_initial_step_size().has_value() || initial_step_size_.has_value()) { + if (initial_step_size_.has_value()) + thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), + step_size_.begin(), + step_size_.end(), + initial_step_size_.value()); + else + thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), + step_size_.begin(), + step_size_.end(), + settings_.get_initial_step_size().value()); + } + if (settings_.get_initial_primal_weight().has_value() || initial_primal_weight_.has_value()) { + if (initial_primal_weight_.has_value()) { thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), - best_primal_weight_.begin(), - best_primal_weight_.end(), + primal_weight_.begin(), + primal_weight_.end(), initial_primal_weight_.value()); - } else { - thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), - primal_weight_.begin(), - primal_weight_.end(), - settings_.get_initial_primal_weight().value()); - if (is_cupdlpx_restart(settings_.hyper_params)) + if (is_cupdlpx_restart(settings_.hyper_params)) + thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), + best_primal_weight_.begin(), + best_primal_weight_.end(), + initial_primal_weight_.value()); + } else { thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), - best_primal_weight_.begin(), - best_primal_weight_.end(), + primal_weight_.begin(), + primal_weight_.end(), settings_.get_initial_primal_weight().value()); + if (is_cupdlpx_restart(settings_.hyper_params)) + thrust::uninitialized_fill(handle_ptr_->get_thrust_policy(), + best_primal_weight_.begin(), + best_primal_weight_.end(), + settings_.get_initial_primal_weight().value()); + } + } + if (initial_k_.has_value()) { + pdhg_solver_.total_pdhg_iterations_ = initial_k_.value(); + pdhg_solver_.get_d_total_pdhg_iterations().set_value_async(initial_k_.value(), stream_view_); + } + if (settings_.get_initial_pdlp_iteration().has_value()) { + total_pdlp_iterations_ = settings_.get_initial_pdlp_iteration().value(); + // This is meaningless in batch mode since pdhg step is never used, set it just to avoid + // assertions + pdhg_solver_.get_d_total_pdhg_iterations().set_value_async(total_pdlp_iterations_, + stream_view_); + pdhg_solver_.total_pdhg_iterations_ = total_pdlp_iterations_; + // Reset the fixed point error since at this pdlp iteration it is expected to already be + // initialized to some value + std::fill(restart_strategy_.initial_fixed_point_error_.begin(), + restart_strategy_.initial_fixed_point_error_.end(), + f_t(0.0)); + std::fill(restart_strategy_.fixed_point_error_.begin(), + restart_strategy_.fixed_point_error_.end(), + f_t(0.0)); } - } - if (initial_k_.has_value()) { - pdhg_solver_.total_pdhg_iterations_ = initial_k_.value(); - pdhg_solver_.get_d_total_pdhg_iterations().set_value_async(initial_k_.value(), stream_view_); - } - if (settings_.get_initial_pdlp_iteration().has_value()) { - total_pdlp_iterations_ = settings_.get_initial_pdlp_iteration().value(); - // This is meaningless in batch mode since pdhg step is never used, set it just to avoid - // assertions - pdhg_solver_.get_d_total_pdhg_iterations().set_value_async(total_pdlp_iterations_, - stream_view_); - pdhg_solver_.total_pdhg_iterations_ = total_pdlp_iterations_; - // Reset the fixed point error since at this pdlp iteration it is expected to already be - // initialized to some value - std::fill(restart_strategy_.initial_fixed_point_error_.begin(), - restart_strategy_.initial_fixed_point_error_.end(), - f_t(0.0)); - std::fill(restart_strategy_.fixed_point_error_.begin(), - restart_strategy_.fixed_point_error_.end(), - f_t(0.0)); - } - // Only the primal_weight_ and step_size_ variables are initialized during the initial phase - // The associated primal/dual step_size (computed using the two firstly mentionned) are not - // initialized. This calls ensures the latter - // In the event of a given primal and dual solutions and if the option is toggled, calling the - // update primal_weight and step_size will also update the associated primal_step_size_, - // dual_step_size_. - // In summary: the below call is only mandatory at the beginning when - // computing/setting the initial primal weight and step size and if they are not recomputed later. - step_size_strategy_.get_primal_and_dual_stepsizes(primal_step_size_, dual_step_size_); + // Only the primal_weight_ and step_size_ variables are initialized during the initial phase + // The associated primal/dual step_size (computed using the two firstly mentionned) are not + // initialized. This calls ensures the latter + // In the event of a given primal and dual solutions and if the option is toggled, calling the + // update primal_weight and step_size will also update the associated primal_step_size_, + // dual_step_size_. + // In summary: the below call is only mandatory at the beginning when + // computing/setting the initial primal weight and step size and if they are not recomputed + // later. + step_size_strategy_.get_primal_and_dual_stepsizes(primal_step_size_, dual_step_size_); #ifdef CUPDLP_DEBUG_MODE - if (initial_primal_.size() != 0 || initial_dual_.size() != 0) { - std::cout << "Initial primal and dual solution before scaling" << std::endl; - if (initial_primal_.size() != 0) { print("initial_primal_", initial_primal_); } - if (initial_dual_.size() != 0) { print("initial_dual_", initial_dual_); } - } + if (initial_primal_.size() != 0 || initial_dual_.size() != 0) { + std::cout << "Initial primal and dual solution before scaling" << std::endl; + if (initial_primal_.size() != 0) { print("initial_primal_", initial_primal_); } + if (initial_dual_.size() != 0) { print("initial_dual_", initial_dual_); } + } #endif - // If there is an initial primal or dual we should update the restart info as if there was a step - // that has happend - if (initial_primal_.size() != 0 || initial_dual_.size() != 0) { - update_primal_dual_solutions( - (initial_primal_.size() != 0) ? std::make_optional(&initial_primal_) : std::nullopt, - (initial_dual_.size() != 0) ? std::make_optional(&initial_dual_) : std::nullopt); - } + // If there is an initial primal or dual we should update the restart info as if there was a + // step that has happend + if (initial_primal_.size() != 0 || initial_dual_.size() != 0) { + update_primal_dual_solutions( + (initial_primal_.size() != 0) ? std::make_optional(&initial_primal_) : std::nullopt, + (initial_dual_.size() != 0) ? std::make_optional(&initial_dual_) : std::nullopt); + } #ifdef CUPDLP_DEBUG_MODE - std::cout << "Solution before projection" << std::endl; - print("pdhg_solver_.get_primal_solution()", pdhg_solver_.get_primal_solution()); - print("pdhg_solver_.get_dual_solution()", pdhg_solver_.get_dual_solution()); - print("pdhg_solver_.get_potential_next_primal_solution()", - pdhg_solver_.get_potential_next_primal_solution()); - print("pdhg_solver_.get_potential_next_dual_solution()", - pdhg_solver_.get_potential_next_dual_solution()); - print("restart_strategy_.last_restart_duality_gap_.primal_solution_", - restart_strategy_.last_restart_duality_gap_.primal_solution_); - print("restart_strategy_.last_restart_duality_gap_.dual_solution_", - restart_strategy_.last_restart_duality_gap_.dual_solution_); + std::cout << "Solution before projection" << std::endl; + print("pdhg_solver_.get_primal_solution()", pdhg_solver_.get_primal_solution()); + print("pdhg_solver_.get_dual_solution()", pdhg_solver_.get_dual_solution()); + print("pdhg_solver_.get_potential_next_primal_solution()", + pdhg_solver_.get_potential_next_primal_solution()); + print("pdhg_solver_.get_potential_next_dual_solution()", + pdhg_solver_.get_potential_next_dual_solution()); + print("restart_strategy_.last_restart_duality_gap_.primal_solution_", + restart_strategy_.last_restart_duality_gap_.primal_solution_); + print("restart_strategy_.last_restart_duality_gap_.dual_solution_", + restart_strategy_.last_restart_duality_gap_.dual_solution_); #endif - // Project initial primal solution - if (settings_.hyper_params.project_initial_primal) { - using f_t2 = typename type_2::type; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(pdhg_solver_.get_primal_solution().data(), - problem_wrap_container(op_problem_scaled_.variable_bounds)), - pdhg_solver_.get_primal_solution().data(), - pdhg_solver_.get_primal_solution().size(), - clamp(), - stream_view_.value()); - - pdhg_solver_.refine_initial_primal_projection(); - - if (!settings_.hyper_params.never_restart_to_average) { - cuopt_expects(!batch_mode_, - cuopt::error_type_t::ValidationError, - "Restart to average not supported in batch mode"); + // Project initial primal solution + if (settings_.hyper_params.project_initial_primal) { + using f_t2 = typename type_2::type; cub::DeviceTransform::Transform( - cuda::std::make_tuple(unscaled_primal_avg_solution_.data(), - op_problem_scaled_.variable_bounds.data()), - unscaled_primal_avg_solution_.data(), - primal_size_h_, + cuda::std::make_tuple(pdhg_solver_.get_primal_solution().data(), + problem_wrap_container(op_problem_scaled_.variable_bounds)), + pdhg_solver_.get_primal_solution().data(), + pdhg_solver_.get_primal_solution().size(), clamp(), stream_view_.value()); + + pdhg_solver_.refine_initial_primal_projection(); + + if (!settings_.hyper_params.never_restart_to_average) { + cuopt_expects(!batch_mode_, + cuopt::error_type_t::ValidationError, + "Restart to average not supported in batch mode"); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(unscaled_primal_avg_solution_.data(), + op_problem_scaled_.variable_bounds.data()), + unscaled_primal_avg_solution_.data(), + primal_size_h_, + clamp(), + stream_view_.value()); + } } - } #ifdef CUPDLP_DEBUG_MODE - std::cout << "Solution after projection" << std::endl; - print("pdhg_solver_.get_primal_solution()", pdhg_solver_.get_primal_solution()); - print("pdhg_solver_.get_dual_solution()", pdhg_solver_.get_dual_solution()); - print("pdhg_solver_.get_potential_next_primal_solution()", - pdhg_solver_.get_potential_next_primal_solution()); - print("pdhg_solver_.get_potential_next_dual_solution()", - pdhg_solver_.get_potential_next_dual_solution()); - print("restart_strategy_.last_restart_duality_gap_.primal_solution_", - restart_strategy_.last_restart_duality_gap_.primal_solution_); - print("restart_strategy_.last_restart_duality_gap_.dual_solution_", - restart_strategy_.last_restart_duality_gap_.dual_solution_); + std::cout << "Solution after projection" << std::endl; + print("pdhg_solver_.get_primal_solution()", pdhg_solver_.get_primal_solution()); + print("pdhg_solver_.get_dual_solution()", pdhg_solver_.get_dual_solution()); + print("pdhg_solver_.get_potential_next_primal_solution()", + pdhg_solver_.get_potential_next_primal_solution()); + print("pdhg_solver_.get_potential_next_dual_solution()", + pdhg_solver_.get_potential_next_dual_solution()); + print("restart_strategy_.last_restart_duality_gap_.primal_solution_", + restart_strategy_.last_restart_duality_gap_.primal_solution_); + print("restart_strategy_.last_restart_duality_gap_.dual_solution_", + restart_strategy_.last_restart_duality_gap_.dual_solution_); #endif - // Need to to tranpose primal solution to row format as there might be initial values or clamping - // Value may not be all 0 - if (batch_mode_) { - rmm::device_uvector dummy(0, stream_view_); - transpose_primal_dual_to_row( - pdhg_solver_.get_primal_solution(), pdhg_solver_.get_dual_solution(), dummy); - if (settings_.hyper_params.use_reflected_primal_dual) { - transpose_primal_dual_to_row(pdhg_solver_.get_potential_next_primal_solution(), - pdhg_solver_.get_potential_next_dual_solution(), - dummy); - transpose_primal_dual_to_row(restart_strategy_.last_restart_duality_gap_.primal_solution_, - restart_strategy_.last_restart_duality_gap_.dual_solution_, - dummy); + // Need to to tranpose primal solution to row format as there might be initial values or + // clamping Value may not be all 0 + if (batch_mode_) { + rmm::device_uvector dummy(0, stream_view_); + transpose_primal_dual_to_row( + pdhg_solver_.get_primal_solution(), pdhg_solver_.get_dual_solution(), dummy); + if (settings_.hyper_params.use_reflected_primal_dual) { + transpose_primal_dual_to_row(pdhg_solver_.get_potential_next_primal_solution(), + pdhg_solver_.get_potential_next_dual_solution(), + dummy); + transpose_primal_dual_to_row(restart_strategy_.last_restart_duality_gap_.primal_solution_, + restart_strategy_.last_restart_duality_gap_.dual_solution_, + dummy); + } } - } - if (verbose) { - std::cout << "primal_size_h_ " << primal_size_h_ << " dual_size_h_ " << dual_size_h_ << " nnz " - << problem_ptr->nnz << std::endl; - std::cout << "Problem before scaling" << std::endl; - print_problem_info( - problem_ptr->coefficients, problem_ptr->objective_coefficients, problem_ptr->combined_bounds); - std::cout << "Problem after scaling" << std::endl; - print_problem_info(op_problem_scaled_.coefficients, - op_problem_scaled_.objective_coefficients, - op_problem_scaled_.combined_bounds); - raft::print_device_vector("Initial step_size", step_size_.data(), 1, std::cout); - raft::print_device_vector("Initial primal_weight", primal_weight_.data(), 1, std::cout); - raft::print_device_vector("Initial primal_step_size", primal_step_size_.data(), 1, std::cout); - raft::print_device_vector("Initial dual_step_size", dual_step_size_.data(), 1, std::cout); - } + if (verbose) { + std::cout << "primal_size_h_ " << primal_size_h_ << " dual_size_h_ " << dual_size_h_ + << " nnz " << problem_ptr->nnz << std::endl; + std::cout << "Problem before scaling" << std::endl; + print_problem_info(problem_ptr->coefficients, + problem_ptr->objective_coefficients, + problem_ptr->combined_bounds); + std::cout << "Problem after scaling" << std::endl; + print_problem_info(op_problem_scaled_.coefficients, + op_problem_scaled_.objective_coefficients, + op_problem_scaled_.combined_bounds); + raft::print_device_vector("Initial step_size", step_size_.data(), 1, std::cout); + raft::print_device_vector("Initial primal_weight", primal_weight_.data(), 1, std::cout); + raft::print_device_vector("Initial primal_step_size", primal_step_size_.data(), 1, std::cout); + raft::print_device_vector("Initial dual_step_size", dual_step_size_.data(), 1, std::cout); + } #ifdef CUPDLP_DEBUG_MODE - raft::print_device_vector("Initial step_size", step_size_.data(), step_size_.size(), std::cout); - raft::print_device_vector( - "Initial primal_weight", primal_weight_.data(), primal_weight_.size(), std::cout); + raft::print_device_vector("Initial step_size", step_size_.data(), step_size_.size(), std::cout); + raft::print_device_vector( + "Initial primal_weight", primal_weight_.data(), primal_weight_.size(), std::cout); #endif - bool warm_start_was_given = settings_.get_pdlp_warm_start_data().is_populated(); + bool warm_start_was_given = settings_.get_pdlp_warm_start_data().is_populated(); - if (!inside_mip_) { - CUOPT_LOG_INFO( - " Iter Primal Obj. Dual Obj. Gap Primal Res. Dual Res. Time"); + if (!inside_mip_) { + CUOPT_LOG_INFO( + " Iter Primal Obj. Dual Obj. Gap Primal Res. Dual Res. Time"); + } } while (true) { #ifdef CUPDLP_DEBUG_MODE diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 532f038fbf..598d93ec33 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -62,11 +62,11 @@ class pdlp_solver_t { pdlp_solver_t(problem_t& op_problem, pdlp_solver_settings_t const& settings, bool is_batch_mode = false); - + // Distributed Solver Constructor pdlp_solver_t(problem_t& op_problem, - pdlp_solver_settings_t const& settings, - int num_gpus); + pdlp_solver_settings_t const& settings, + int num_gpus); optimization_problem_solution_t run_solver(const timer_t& timer); From 0965a60dd6300638174761e686936303aef97030 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 20 May 2026 16:41:07 +0200 Subject: [PATCH 011/258] added distributed transform --- .../distributed_pdlp/multi_gpu_engine.hpp | 47 +++++++++++++++++++ cpp/src/pdlp/pdlp.cu | 13 +++++ 2 files changed, 60 insertions(+) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index e191a89d60..94f6b8584a 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -42,6 +42,53 @@ struct multi_gpu_engine_t { multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; multi_gpu_engine_t& operator=(const multi_gpu_engine_t&) = delete; + + + template + void for_each_shard(Fn&& fn) + { + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + fn(*s); + } + } + + template + void distributed_transform(std::tuple in_accessors, + OutAccess out, + SizeAccess sz, + Op op) + { + for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + // turns the Tuple of lambdas into a tuple of rmm::device_uvector + auto cub_inputs = std::apply( + [&sub](auto&... acc) { return cuda::std::make_tuple(acc(sub)...); }, + in_accessors); + + cub::DeviceTransform::Transform(cub_inputs, + out(sub), + sz(sub), + op, + shard.stream.view()); + }); + } + // --- 2) convenience: single input accessor (delegates) --- + template + void distributed_transform(InAccess in, + OutAccess out, + SizeAccess sz, + Op op) + { + distributed_transform(std::make_tuple(in), out, sz, op); + } + // Engine-level stream for fork/join orchestration (master side). rmm::cuda_stream stream; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 2a36c160fd..12717ce45b 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -489,6 +489,19 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, raft::copy(sub.primal_step_size_.data(), primal_step_size_.data(), 1, shard->stream); raft::copy(sub.dual_step_size_.data(), dual_step_size_.data(), 1, shard->stream); } + + // Project initial primal solution + if (settings_.hyper_params.project_initial_primal) { + using f_t2 = typename type_2::type; + + multi_gpu_engine->distributed_transform( + std::make_tuple( + [](auto& s) -> auto& { return s.pdhg_solver_.get_primal_solution().data();}, + [](auto& s) -> auto& { return s.get_op_problem_scaled().variable_bounds.data();}), + [](auto& s) -> auto& { return s.pdhg_solver_.get_primal_solution().data(); }, + [](auto& s) -> auto { return s.pdhg_solver_.get_primal_solution().size(); }, + clamp() + ) } template From d4d1cab460a8b06163a749d7496099c185baa2de Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 20 May 2026 16:45:59 +0200 Subject: [PATCH 012/258] added semicolon and existing runtime error enum --- cpp/src/pdlp/pdlp.cu | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 12717ce45b..6ef586a8b8 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -334,9 +334,9 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, settings.multi_gpu_partition_file); } else { cuopt_expects(false, - error_type_t::NotImplemented, - "Metis partitioning inside cuopt not implemented yet; " - "provide a --parts file via settings.multi_gpu_partition_file"); + error_type_t::RuntimeError, + "Metis partitioning inside cuopt not implemented yet; " + "provide a --parts file via settings.multi_gpu_partition_file"); } // always compute initial step size before scaling and primal_weight after scaling to do like @@ -501,7 +501,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, [](auto& s) -> auto& { return s.pdhg_solver_.get_primal_solution().data(); }, [](auto& s) -> auto { return s.pdhg_solver_.get_primal_solution().size(); }, clamp() - ) + ); } template From 6659dd9768a9db5504c3ef480bacf148f66f8f33 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 20 May 2026 16:49:05 +0200 Subject: [PATCH 013/258] added } and fixed cuot_expects in partition loader --- cpp/src/pdlp/distributed_pdlp/partition_loader.cu | 11 +++++++---- cpp/src/pdlp/pdlp.cu | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 6c96e0b63d..007df4ce1c 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -18,8 +18,10 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ std::string const& file) { std::ifstream part_file(file); - cuopt_expects( - part_file.is_open(), error_type_t::ValidationError, "Failed to open partition file: " + file); + cuopt_expects(part_file.is_open(), + error_type_t::ValidationError, + "Failed to open partition file: %s", + file.c_str()); // One integer per line; operator>> skips whitespace so blank lines and // trailing newlines are tolerated. @@ -31,8 +33,9 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ // We must have hit EOF cleanly; any other state means a malformed token. cuopt_expects(part_file.eof(), - error_type_t::ValidationError, - "Malformed partition file (expected one integer per line): " + file); + error_type_t::ValidationError, + "Malformed partition file (expected one integer per line): %s", + file.c_str()); return parts; } diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 6ef586a8b8..ee91c874a4 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -502,6 +502,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, [](auto& s) -> auto { return s.pdhg_solver_.get_primal_solution().size(); }, clamp() ); + } } template From b2ed271234f8ad3ec9483aff4fdb9f3aa5d6b21f Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 20 May 2026 16:58:37 +0200 Subject: [PATCH 014/258] small bug fixes --- cpp/src/pdlp/pdlp.cu | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index ee91c874a4..a2aa14a78a 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -480,7 +480,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, op_problem_scaled_.presolve_data.objective_scaling_factor, sub_pdlp_settings); - for (auto& shard : multi_gpu_engine.shards) { + for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); auto& sub = *shard->sub_pdlp; raft::copy(sub.step_size_.data(), step_size_.data(), 1, shard->stream); @@ -2441,6 +2441,8 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co std::cout << "Starting PDLP loop:" << std::endl; #endif + bool warm_start_was_given = settings_.get_pdlp_warm_start_data().is_populated(); + // In distributed mode, skip all setup, it is done before if (!settings_.hyper_params.use_distributed_pdlp) { // TODO handle that properly @@ -2644,7 +2646,6 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co "Initial primal_weight", primal_weight_.data(), primal_weight_.size(), std::cout); #endif - bool warm_start_was_given = settings_.get_pdlp_warm_start_data().is_populated(); if (!inside_mip_) { CUOPT_LOG_INFO( From 50d16ce7d5abb8a06bf69f4be3e96b4bb19c3f0d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 20 May 2026 17:04:18 +0200 Subject: [PATCH 015/258] =?UTF-8?q?a=20version=20that=20compiles=20#heheha?= =?UTF-8?q?=20=F0=9F=98=8E=F0=9F=98=8E=F0=9F=98=8E=F0=9F=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cpp/src/pdlp/pdlp.cu | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index a2aa14a78a..1cc987291f 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -496,10 +496,10 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, multi_gpu_engine->distributed_transform( std::make_tuple( - [](auto& s) -> auto& { return s.pdhg_solver_.get_primal_solution().data();}, - [](auto& s) -> auto& { return s.get_op_problem_scaled().variable_bounds.data();}), - [](auto& s) -> auto& { return s.pdhg_solver_.get_primal_solution().data(); }, - [](auto& s) -> auto { return s.pdhg_solver_.get_primal_solution().size(); }, + [](auto& s) { return s.pdhg_solver_.get_primal_solution().data();}, + [](auto& s) { return s.get_op_problem_scaled().variable_bounds.data();}), + [](auto& s) { return s.pdhg_solver_.get_primal_solution().data(); }, + [](auto& s) { return s.pdhg_solver_.get_primal_solution().size(); }, clamp() ); } From 359d9f49693afb5a22ea767114dd8e3b20414c9a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 21 May 2026 10:51:26 +0200 Subject: [PATCH 016/258] removed use of engine:transaform --- cpp/src/pdlp/pdlp.cu | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 1cc987291f..d5422c9d5f 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -493,15 +493,16 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, // Project initial primal solution if (settings_.hyper_params.project_initial_primal) { using f_t2 = typename type_2::type; - - multi_gpu_engine->distributed_transform( - std::make_tuple( - [](auto& s) { return s.pdhg_solver_.get_primal_solution().data();}, - [](auto& s) { return s.get_op_problem_scaled().variable_bounds.data();}), - [](auto& s) { return s.pdhg_solver_.get_primal_solution().data(); }, - [](auto& s) { return s.pdhg_solver_.get_primal_solution().size(); }, - clamp() - ); + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + auto& sub = *shard->sub_pdlp; + cub::DeviceTransform::Transform( + std::make_tuple(sub.pdhg_solver_.get_primal_solution().data(), + sub.get_op_problem_scaled().variable_bounds.data()), + sub.pdhg_solver_.get_primal_solution().data(), + sub.pdhg_solver_.get_primal_solution().size(), + clamp(), shard->stream); + } } } From 910a49ab4346d08f8c70f4fa3ee9523becf3d9a5 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 11:08:52 +0200 Subject: [PATCH 017/258] added multi-gpu SpMV #heheha --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 4 + .../distributed_pdlp/multi_gpu_engine.hpp | 147 ++++++++++++++++++ cpp/src/pdlp/distributed_pdlp/shard.cu | 24 +++ cpp/src/pdlp/distributed_pdlp/shard.hpp | 11 ++ cpp/src/pdlp/pdhg.cu | 23 +++ cpp/src/pdlp/pdhg.hpp | 27 +++- cpp/src/pdlp/pdlp.cu | 4 + 7 files changed, 238 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index fe95b1e5ff..a0b3f5dcc3 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -4,6 +4,10 @@ */ #include +// compute_A_x() / compute_At_y() (defined inline in the engine header) call +// shard.sub_pdlp->pdhg_solver_.compute_* — pdlp_solver_t must be complete at +// the explicit instantiation point below. +#include #include diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 94f6b8584a..9ea007947e 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -6,12 +6,23 @@ #include #include +#include #include +#include + #include +#include + +#include +#include +#include + +#include #include +#include #include namespace cuopt::linear_programming::detail { @@ -89,6 +100,142 @@ struct multi_gpu_engine_t { distributed_transform(std::make_tuple(in), out, sz, op); } + // -------- Halo exchange (variables / x) --------------------------------- + // Fills the halo slice [owned_var_size, total_var_size) of the per-shard + // reflected_primal vector (the buffer A @ x reads). Step 1: thrust::gather + // per-peer outgoing values into staging buffers. Step 2: a single NCCL + // group with matched ncclSend / ncclRecv across all (rank, peer) pairs. + void halo_exchange_var() + { + const int nb = static_cast(shards.size()); + + // Step 1: gather owned values that each peer needs into per-peer staging. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto& x = s.sub_pdlp->pdhg_solver_.get_reflected_primal(); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (s.var_send_indices_d[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + s.var_send_indices_d[peer].begin(), + s.var_send_indices_d[peer].end(), + x.begin(), + s.var_send_buf_d[peer].begin()); + } + } + + // Step 2: matched send / recv across the whole topology in one NCCL group. + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + ncclSend(s.var_send_buf_d[peer].data(), + s.var_send_buf_d[peer].size(), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + auto& rd = s.rank_data; + raft::device_setter guard(s.device_id); + auto& x = s.sub_pdlp->pdhg_solver_.get_reflected_primal(); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; + ncclRecv(recv_ptr, + static_cast(rd.var_recv_counts[peer]), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + ncclGroupEnd(); + } + + // -------- Halo exchange (constraints / y) ------------------------------- + // Same as halo_exchange_var but for the per-shard dual solution (the buffer + // A_T @ y reads) and constraint halos. + void halo_exchange_cstr() + { + const int nb = static_cast(shards.size()); + + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto& y = s.sub_pdlp->pdhg_solver_.get_dual_solution(); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (s.cstr_send_indices_d[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + s.cstr_send_indices_d[peer].begin(), + s.cstr_send_indices_d[peer].end(), + y.begin(), + s.cstr_send_buf_d[peer].begin()); + } + } + + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + ncclSend(s.cstr_send_buf_d[peer].data(), + s.cstr_send_buf_d[peer].size(), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + auto& rd = s.rank_data; + raft::device_setter guard(s.device_id); + auto& y = s.sub_pdlp->pdhg_solver_.get_dual_solution(); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; + ncclRecv(recv_ptr, + static_cast(rd.cstr_recv_counts[peer]), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + ncclGroupEnd(); + } + + // -------- High-level: A @ x and A_T @ y --------------------------------- + // A @ x: halo-update the reflected_primal vector, then per-shard SpMV. + // Named distributed_* (rather than compute_*) to make call sites in pdhg.cu + // self-documenting and to avoid name collision with pdhg_solver_t's own + // compute_A_x / compute_At_y, which the engine dispatches into per shard. + void distributed_compute_A_x() + { + halo_exchange_var(); + for_each_shard([&](auto& shard) { + shard.sub_pdlp->pdhg_solver_.compute_A_x(); + }); + } + + // A_T @ y: halo-update the dual solution vector, then per-shard SpMV. + void distributed_compute_At_y() + { + halo_exchange_cstr(); + for_each_shard([&](auto& shard) { + shard.sub_pdlp->pdhg_solver_.compute_At_y(); + }); + } + // Engine-level stream for fork/join orchestration (master side). rmm::cuda_stream stream; diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 596a08a3dc..bbc02559cf 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -195,6 +195,30 @@ pdlp_shard_t::pdlp_shard_t(int device_id, scaling.set_h_bound_rescaling(h_bound_rescaling); scaling.set_h_objective_rescaling(h_objective_rescaling); + // ---- 6. Build per-peer halo-exchange plans (ported from metis_tests). ---- + // For each peer p, we precompute: + // send_indices_d[p] : local indices to gather (uploaded from host send plan) + // send_buf_d[p] : f_t staging buffer sized to match + // Self-peer slot is present but empty (size 0). Used in engine halo exchange. + auto build_send_plan = [&](auto const& send_per_peer, + auto& indices_d, + auto& buf_d) { + const std::size_t n_peers = send_per_peer.size(); + indices_d.reserve(n_peers); + buf_d.reserve(n_peers); + for (auto const& send_to_peer : send_per_peer) { + rmm::device_uvector idx(send_to_peer.size(), stream_view); + rmm::device_uvector buf(send_to_peer.size(), stream_view); + if (!send_to_peer.empty()) { + raft::copy(idx.data(), send_to_peer.data(), send_to_peer.size(), stream_view); + } + indices_d.emplace_back(std::move(idx)); + buf_d.emplace_back(std::move(buf)); + } + }; + build_send_plan(rank_data.var_send_per_peer, var_send_indices_d, var_send_buf_d); + build_send_plan(rank_data.cstr_send_per_peer, cstr_send_indices_d, cstr_send_buf_d); + handle.sync_stream(stream_view); } diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index a5ff89c5c4..35babc12db 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -83,6 +84,16 @@ struct pdlp_shard_t { std::optional> opt_problem; std::optional> sub_problem; std::unique_ptr> sub_pdlp; + + // Per-peer halo-exchange state. Inner index = peer rank. + // Slot for self (peer == this rank) is present but unused (size 0). + // var_send_indices_d[peer] : local indices into primal vector to gather and ncclSend + // var_send_buf_d [peer] : staging buffer for outgoing variable values + // cstr_send_indices_d/cstr_send_buf_d : same, for dual vector + std::vector> var_send_indices_d; + std::vector> var_send_buf_d; + std::vector> cstr_send_indices_d; + std::vector> cstr_send_buf_d; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index cb16c9d662..9cf2087c8b 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -5,6 +5,11 @@ */ /* clang-format on */ #include +// pdlp.cuh defines pdlp_solver_t which the engine's compute_A_x/compute_At_y +// template bodies dereference via shard.sub_pdlp->pdhg_solver_. Must be a +// complete type at the point of template instantiation below. +#include +#include #include #include #include @@ -306,6 +311,15 @@ void pdhg_solver_t::compute_At_y() { // A_t @ y + // Multi-GPU dispatch: when the master pdhg has an engine, drive halo + // exchange + per-shard SpMV via the engine. Shards' pdhg_solver_ have no + // engine pointer set, so their compute_At_y falls through to the cusparse + // path below on each shard's local A_t. + if (mgpu_engine_ != nullptr) { + mgpu_engine_->distributed_compute_At_y(); + return; + } + if (!batch_mode_) { if constexpr (std::is_same_v) { if (cusparse_view_.mixed_precision_enabled_) { @@ -354,6 +368,15 @@ template void pdhg_solver_t::compute_A_x() { // A @ x + + // Multi-GPU dispatch: see compute_At_y. The engine halo-updates the + // reflected_primal vector (the buffer this SpMV reads) and then drives + // per-shard local cusparse SpMV. + if (mgpu_engine_ != nullptr) { + mgpu_engine_->distributed_compute_A_x(); + return; + } + if (!batch_mode_) { if constexpr (std::is_same_v) { if (cusparse_view_.mixed_precision_enabled_) { diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index 0a64e49efb..d258afb091 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -21,6 +21,12 @@ #include namespace cuopt::linear_programming::detail { + +// Forward-declared to avoid include cycle: multi_gpu_engine.hpp itself includes pdhg.hpp +// (engine calls per-shard pdhg compute_*). pdhg.cu does the full include. +template +struct multi_gpu_engine_t; + template class pdhg_solver_t { public: @@ -69,6 +75,21 @@ class pdhg_solver_t { void update_solution(cusparse_view_t& current_op_problem_evaluation_cusparse_view_); void refine_initial_primal_projection(); + // SpMV primitives. Public so the multi-GPU engine can drive them per-shard + // after halo-exchanging the relevant vector. Single-GPU PDLP still calls + // them internally via take_step / compute_next_*. + // + // If set_multi_gpu_engine() has been called, these dispatch to the engine + // (halo exchange + per-shard SpMV). Otherwise they run the single-GPU + // cusparse path on the local matrix. + void compute_At_y(); + void compute_A_x(); + + // Master PDLP wires up the engine pointer here after the engine is built. + // Shards' pdhg_solver_ leaves this null so each shard runs single-GPU SpMV + // on its local matrix. + void set_multi_gpu_engine(multi_gpu_engine_t* engine) { mgpu_engine_ = engine; } + i_t total_pdhg_iterations_; private: @@ -84,8 +105,6 @@ class pdhg_solver_t { void compute_primal_projection_with_gradient(rmm::device_uvector& primal_step_size); void compute_primal_projection(rmm::device_uvector& primal_step_size); - void compute_At_y(); - void compute_A_x(); bool batch_mode_{false}; raft::handle_t const* handle_ptr_{nullptr}; @@ -132,6 +151,10 @@ class pdhg_solver_t { rmm::device_uvector new_bounds_lower_; rmm::device_uvector new_bounds_upper_; cuda::fast_mod_div batch_size_divisor_; + + // Non-owning. Set on the master pdhg_solver_ in distributed mode; null + // (default) means single-GPU path. See compute_At_y / compute_A_x. + multi_gpu_engine_t* mgpu_engine_{nullptr}; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index d5422c9d5f..348d41a512 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -490,6 +490,10 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, raft::copy(sub.dual_step_size_.data(), dual_step_size_.data(), 1, shard->stream); } + // Wire the engine into the master pdhg_solver_. Shards' pdhg_solver_ keep + // mgpu_engine_ == nullptr so they run plain single-GPU SpMV on local A. + pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); + // Project initial primal solution if (settings_.hyper_params.project_initial_primal) { using f_t2 = typename type_2::type; From 76c0b3f50b96647d23534729a68c6b2f5702848d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 11:48:51 +0200 Subject: [PATCH 018/258] transformed a transform. it compiles hehe --- cpp/src/pdlp/pdhg.cu | 40 +++++++++++++++++++++++++++++----------- cpp/src/pdlp/pdhg.hpp | 7 +++++++ cpp/src/pdlp/pdlp.cu | 7 ++++--- cpp/src/pdlp/pdlp.cuh | 6 ++++++ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index 9cf2087c8b..09d439cc0e 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -521,6 +521,26 @@ struct primal_reflected_major_projection { const f_t* scalar_; }; +// Pure cub-transform extract — body byte-identical to the non-batch inline +// path in compute_next_primal_dual_solution_reflected. The platform dispatch +// (single-GPU vs per-shard fan-out) lives at the call site, not here. +// Placed after primal_reflected_major_projection so the functor is visible. +template +void pdhg_solver_t::primal_reflected_major_projection_transform( + rmm::device_uvector& primal_step_size) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(current_saddle_point_state_.get_primal_solution().data(), + problem_ptr->objective_coefficients.data(), + current_saddle_point_state_.get_current_AtY().data(), + problem_ptr->variable_bounds.data()), + thrust::make_zip_iterator( + potential_next_primal_solution_.data(), dual_slack_.data(), reflected_primal_.data()), + primal_size_h_, + primal_reflected_major_projection(primal_step_size.data()), + stream_view_.value()); +} + template struct primal_reflected_major_projection_batch { using f_t2 = typename type_2::type; @@ -910,17 +930,15 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( graph_all.start_capture(should_major); compute_At_y(); - if (!batch_mode_) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(current_saddle_point_state_.get_primal_solution().data(), - problem_ptr->objective_coefficients.data(), - current_saddle_point_state_.get_current_AtY().data(), - problem_ptr->variable_bounds.data()), - thrust::make_zip_iterator( - potential_next_primal_solution_.data(), dual_slack_.data(), reflected_primal_.data()), - primal_size_h_, - primal_reflected_major_projection(primal_step_size.data()), - stream_view_.value()); + if (mgpu_engine_ != nullptr) { + for (auto& shard : mgpu_engine_->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdlp = *shard->sub_pdlp; + sub_pdlp.pdhg_solver_.primal_reflected_major_projection_transform( + sub_pdlp.get_primal_step_size()); + } + } else if (!batch_mode_) { + primal_reflected_major_projection_transform(primal_step_size); } else { cub::DeviceFor::Bulk(potential_next_primal_solution_.size(), primal_reflected_major_projection_bulk_op{ diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index d258afb091..3a1795ce6f 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -85,6 +85,13 @@ class pdhg_solver_t { void compute_At_y(); void compute_A_x(); + // Pure cub-transform extractions. Each one is byte-identical to the inline + // cub call it replaces — no platform dispatch inside. Callers handle the + // single-GPU vs per-shard branching at the call site (see the + // "if (mgpu_engine_) for shard..." blocks in compute_next_*). + void primal_reflected_major_projection_transform( + rmm::device_uvector& primal_step_size); + // Master PDLP wires up the engine pointer here after the engine is built. // Shards' pdhg_solver_ leaves this null so each shard runs single-GPU SpMV // on its local matrix. diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 348d41a512..168f997724 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -501,11 +501,12 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, raft::device_setter guard(shard->device_id); auto& sub = *shard->sub_pdlp; cub::DeviceTransform::Transform( - std::make_tuple(sub.pdhg_solver_.get_primal_solution().data(), - sub.get_op_problem_scaled().variable_bounds.data()), + cuda::std::make_tuple(sub.pdhg_solver_.get_primal_solution().data(), + sub.get_op_problem_scaled().variable_bounds.data()), sub.pdhg_solver_.get_primal_solution().data(), sub.pdhg_solver_.get_primal_solution().size(), - clamp(), shard->stream); + clamp(), + shard->stream.view()); } } } diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 598d93ec33..6b2bc35a24 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -114,6 +114,12 @@ class pdlp_solver_t { return initial_scaling_strategy_; } + // Per-shard primal/dual step sizes are private state on pdlp_solver_t but + // are needed inside the multi-GPU dispatch paths that fan out a master cub + // call across all shards' pdhg_solver_t::*_transform methods. + rmm::device_uvector& get_primal_step_size() { return primal_step_size_; } + rmm::device_uvector& get_dual_step_size() { return dual_step_size_; } + private: void print_termination_criteria(const timer_t& timer, bool is_average = false); void print_final_termination_criteria( From 5ec713842159df18170a2c6798f8c92344c789e6 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 12:55:29 +0200 Subject: [PATCH 019/258] updated take step for distributed. compiles but doesnt run. will check on main --- cpp/CMakeLists.txt | 29 ++++++++++++ cpp/src/pdlp/pdhg.cu | 102 +++++++++++++++++++++++++++++------------- cpp/src/pdlp/pdhg.hpp | 3 ++ cpp/src/pdlp/pdlp.cu | 15 +++++++ 4 files changed, 119 insertions(+), 30 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index e7b4693547..627e086343 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -288,6 +288,34 @@ create_logger_macros(CUOPT "cuopt::default_logger()" include/cuopt) find_package(CUDSS REQUIRED) +# ################################################################################################## +# - NCCL (multi-GPU distributed PDLP) ------------------------------------------------------------- +# NCCL is shipped via the conda env; no canonical CMake config target, so look it +# up by name in the standard lib paths (plus CONDA_PREFIX as a hint). +set(NCCL_HINT_PREFIXES "") +if (DEFINED ENV{CONDA_PREFIX} AND NOT "$ENV{CONDA_PREFIX}" STREQUAL "") + list(APPEND NCCL_HINT_PREFIXES "$ENV{CONDA_PREFIX}") +endif () +find_path(NCCL_INCLUDE_DIR + NAMES nccl.h + HINTS ${NCCL_HINT_PREFIXES} + PATH_SUFFIXES include +) +find_library(NCCL_LIBRARY + NAMES nccl + HINTS ${NCCL_HINT_PREFIXES} + PATH_SUFFIXES lib lib64 +) +if (NOT NCCL_INCLUDE_DIR OR NOT NCCL_LIBRARY) + message(FATAL_ERROR "NCCL not found. Looked in ${NCCL_HINT_PREFIXES}. Install nccl-dev / libnccl-dev in the active env.") +endif () +add_library(nccl_external UNKNOWN IMPORTED GLOBAL) +set_target_properties(nccl_external PROPERTIES + IMPORTED_LOCATION "${NCCL_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${NCCL_INCLUDE_DIR}" +) +message(STATUS "Using NCCL: ${NCCL_LIBRARY}") + # ################################################################################################## # - gRPC and Protobuf setup ----------------------------------------------------------------------- @@ -549,6 +577,7 @@ target_link_libraries(cuopt ${CUDSS_LIB_FILE} PRIVATE ${CUOPT_PRIVATE_CUDA_LIBS} + nccl_external $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> ) diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index 09d439cc0e..eb60a43603 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -569,6 +569,21 @@ struct primal_reflected_projection { const f_t* scalar_; }; +template +void pdhg_solver_t::primal_reflected_projection_transform( + rmm::device_uvector& primal_step_size) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(current_saddle_point_state_.get_primal_solution().data(), + problem_ptr->objective_coefficients.data(), + current_saddle_point_state_.get_current_AtY().data(), + problem_ptr->variable_bounds.data()), + reflected_primal_.data(), + primal_size_h_, + primal_reflected_projection(primal_step_size.data()), + stream_view_.value()); +} + template struct primal_reflected_projection_batch { using f_t2 = typename type_2::type; @@ -598,6 +613,21 @@ struct dual_reflected_major_projection { const f_t* scalar_; }; +template +void pdhg_solver_t::dual_reflected_major_projection_transform( + rmm::device_uvector& dual_step_size) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(current_saddle_point_state_.get_dual_solution().data(), + current_saddle_point_state_.get_dual_gradient().data(), + problem_ptr->constraint_lower_bounds.data(), + problem_ptr->constraint_upper_bounds.data()), + thrust::make_zip_iterator(potential_next_dual_solution_.data(), reflected_dual_.data()), + dual_size_h_, + dual_reflected_major_projection(dual_step_size.data()), + stream_view_.value()); +} + template struct dual_reflected_major_projection_batch { HDI thrust::tuple operator()( @@ -626,6 +656,21 @@ struct dual_reflected_projection { const f_t* scalar_; }; +template +void pdhg_solver_t::dual_reflected_projection_transform( + rmm::device_uvector& dual_step_size) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(current_saddle_point_state_.get_dual_solution().data(), + current_saddle_point_state_.get_dual_gradient().data(), + problem_ptr->constraint_lower_bounds.data(), + problem_ptr->constraint_upper_bounds.data()), + reflected_dual_.data(), + dual_size_h_, + dual_reflected_projection(dual_step_size.data()), + stream_view_.value()); +} + template struct dual_reflected_projection_batch { HDI f_t @@ -989,16 +1034,15 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // Compute next dual compute_A_x(); - if (!batch_mode_) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(current_saddle_point_state_.get_dual_solution().data(), - current_saddle_point_state_.get_dual_gradient().data(), - problem_ptr->constraint_lower_bounds.data(), - problem_ptr->constraint_upper_bounds.data()), - thrust::make_zip_iterator(potential_next_dual_solution_.data(), reflected_dual_.data()), - dual_size_h_, - dual_reflected_major_projection(dual_step_size.data()), - stream_view_.value()); + if (mgpu_engine_ != nullptr) { + for (auto& shard : mgpu_engine_->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdlp = *shard->sub_pdlp; + sub_pdlp.pdhg_solver_.dual_reflected_major_projection_transform( + sub_pdlp.get_dual_step_size()); + } + } else if (!batch_mode_) { + dual_reflected_major_projection_transform(dual_step_size); } else { cub::DeviceFor::Bulk(potential_next_dual_solution_.size(), dual_reflected_major_projection_bulk_op{ @@ -1036,16 +1080,15 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( current_saddle_point_state_.get_current_AtY()); #endif - if (!batch_mode_) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(current_saddle_point_state_.get_primal_solution().data(), - problem_ptr->objective_coefficients.data(), - current_saddle_point_state_.get_current_AtY().data(), - problem_ptr->variable_bounds.data()), - reflected_primal_.data(), - primal_size_h_, - primal_reflected_projection(primal_step_size.data()), - stream_view_.value()); + if (mgpu_engine_ != nullptr) { + for (auto& shard : mgpu_engine_->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdlp = *shard->sub_pdlp; + sub_pdlp.pdhg_solver_.primal_reflected_projection_transform( + sub_pdlp.get_primal_step_size()); + } + } else if (!batch_mode_) { + primal_reflected_projection_transform(primal_step_size); } else { cub::DeviceFor::Bulk(reflected_primal_.size(), primal_reflected_projection_bulk_op{ @@ -1097,16 +1140,15 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // Compute next dual compute_A_x(); - if (!batch_mode_) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(current_saddle_point_state_.get_dual_solution().data(), - current_saddle_point_state_.get_dual_gradient().data(), - problem_ptr->constraint_lower_bounds.data(), - problem_ptr->constraint_upper_bounds.data()), - reflected_dual_.data(), - dual_size_h_, - dual_reflected_projection(dual_step_size.data()), - stream_view_.value()); + if (mgpu_engine_ != nullptr) { + for (auto& shard : mgpu_engine_->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdlp = *shard->sub_pdlp; + sub_pdlp.pdhg_solver_.dual_reflected_projection_transform( + sub_pdlp.get_dual_step_size()); + } + } else if (!batch_mode_) { + dual_reflected_projection_transform(dual_step_size); } else { cub::DeviceFor::Bulk(reflected_dual_.size(), dual_reflected_projection_bulk_op{ diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index 3a1795ce6f..628c3897e2 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -91,6 +91,9 @@ class pdhg_solver_t { // "if (mgpu_engine_) for shard..." blocks in compute_next_*). void primal_reflected_major_projection_transform( rmm::device_uvector& primal_step_size); + void dual_reflected_major_projection_transform(rmm::device_uvector& dual_step_size); + void primal_reflected_projection_transform(rmm::device_uvector& primal_step_size); + void dual_reflected_projection_transform(rmm::device_uvector& dual_step_size); // Master PDLP wires up the engine pointer here after the engine is built. // Shards' pdhg_solver_ leaves this null so each shard runs single-GPU SpMV diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 168f997724..37de2d8537 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -42,6 +42,7 @@ #include #include +#include #include namespace cuopt::linear_programming::detail { @@ -327,6 +328,19 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, cuopt_expects(num_gpus == settings.num_gpus && settings.num_gpus > 1, error_type_t::ValidationError, "This constructor should only be used for distributed PDLP (num_gpus > 1)"); + + // Distributed PDLP is currently double-only. The body is guarded with + // `if constexpr` so the float instantiation never references the + // multi_gpu_engine_t / partition_loader_t symbols + // (those are intentionally not instantiated in their .cu files), keeping + // the link clean. Trying to use distributed PDLP with f_t = float will + // throw at runtime instead. + if constexpr (!std::is_same_v) { + cuopt_expects(false, + error_type_t::ValidationError, + "Distributed PDLP (num_gpus > 1) currently requires double precision"); + return; + } else { // 2. Load partition std::vector parts; if (!settings.multi_gpu_partition_file.empty()) { @@ -509,6 +523,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, shard->stream.view()); } } + } // end if constexpr (std::is_same_v) } template From de19f38f5e771e8470d0ae8e711676fb47912c4f Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 13:40:31 +0200 Subject: [PATCH 020/258] support spmvop on multi-gpu --- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp | 4 ++-- cpp/src/pdlp/distributed_pdlp/shard.cu | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 9ea007947e..e9f48b9666 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -223,7 +223,7 @@ struct multi_gpu_engine_t { { halo_exchange_var(); for_each_shard([&](auto& shard) { - shard.sub_pdlp->pdhg_solver_.compute_A_x(); + shard.sub_pdlp->pdhg_solver_.spmvop_A_x(); }); } @@ -232,7 +232,7 @@ struct multi_gpu_engine_t { { halo_exchange_cstr(); for_each_shard([&](auto& shard) { - shard.sub_pdlp->pdhg_solver_.compute_At_y(); + shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); }); } diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index bbc02559cf..06c6f8c8de 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -195,6 +195,8 @@ pdlp_shard_t::pdlp_shard_t(int device_id, scaling.set_h_bound_rescaling(h_bound_rescaling); scaling.set_h_objective_rescaling(h_objective_rescaling); + sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( + /* is_reflected */ true); // ---- 6. Build per-peer halo-exchange plans (ported from metis_tests). ---- // For each peer p, we precompute: // send_indices_d[p] : local indices to gather (uploaded from host send plan) From 0030a6c5d7b3f9e22c3da791c25a09869679f3e0 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 14:06:47 +0200 Subject: [PATCH 021/258] compile ready --- .../initial_scaling_strategy/initial_scaling.cu | 14 ++++++++++---- cpp/src/pdlp/pdhg.hpp | 6 +++--- cpp/src/pdlp/pdlp.cu | 4 +++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index eb1bae2e95..fd6e02079e 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -938,15 +938,21 @@ void pdlp_initial_scaling_strategy_t::set_cummulative_scaling( template void pdlp_initial_scaling_strategy_t::set_h_bound_rescaling(f_t value) { - h_bound_rescaling = value; - bound_rescaling_.set_value_async(value, stream_view_); + std::fill(h_bound_rescaling_.begin(), h_bound_rescaling_.end(), value); + thrust::fill(handle_ptr_->get_thrust_policy(), + bound_rescaling_.begin(), + bound_rescaling_.end(), + value); } template void pdlp_initial_scaling_strategy_t::set_h_objective_rescaling(f_t value) { - h_objective_rescaling = value; - objective_rescaling_.set_value_async(value, stream_view_); + std::fill(h_objective_rescaling_.begin(), h_objective_rescaling_.end(), value); + thrust::fill(handle_ptr_->get_thrust_policy(), + objective_rescaling_.begin(), + objective_rescaling_.end(), + value); } template diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index 7b2e606864..8226d2cecc 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -91,7 +91,9 @@ class pdhg_solver_t { // cusparse path on the local matrix. void compute_At_y(); void compute_A_x(); - + void spmvop_At_y(); + void spmvop_A_x(); + // Pure cub-transform extractions. Each one is byte-identical to the inline // cub call it replaces — no platform dispatch inside. Callers handle the // single-GPU vs per-shard branching at the call site (see the @@ -124,8 +126,6 @@ class pdhg_solver_t { void compute_primal_projection_with_gradient(rmm::device_uvector& primal_step_size); void compute_primal_projection(rmm::device_uvector& primal_step_size); - void spmvop_At_y(); - void spmvop_A_x(); bool batch_mode_{false}; raft::handle_t const* handle_ptr_{nullptr}; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 8ceb712aff..ec7ed16c30 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -567,6 +567,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, // Project initial primal solution if (settings_.hyper_params.project_initial_primal) { + // Use refine_initial_primal_projection ??? using f_t2 = typename type_2::type; for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); @@ -2672,7 +2673,8 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co clamp(), stream_view_.value()); - pdhg_solver_.refine_initial_primal_projection(); + pdhg_solver_.refine_initial_primal_projection( + initial_scaling_strategy_.get_bound_rescaling_vector()); if (!settings_.hyper_params.never_restart_to_average) { cuopt_expects(!batch_mode_, From 172ebc29da1eb892da5a2fe22d2df1f57d93f773 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 14:14:10 +0200 Subject: [PATCH 022/258] can run now --- cpp/src/pdlp/distributed_pdlp/shard.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 06c6f8c8de..c66b03755e 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -225,6 +225,6 @@ pdlp_shard_t::pdlp_shard_t(int device_id, } template struct pdlp_shard_t; -// template struct pdlp_shard_t; +template struct pdlp_shard_t; } // namespace cuopt::linear_programming::detail From 23d07981d0dd7b84d953437aafd921292f4db4d8 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 14:57:51 +0200 Subject: [PATCH 023/258] passing all tests, good merge --- cpp/src/pdlp/pdlp.cu | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index ec7ed16c30..b5fe5ad6ca 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2665,13 +2665,31 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co // Project initial primal solution if (settings_.hyper_params.project_initial_primal) { using f_t2 = typename type_2::type; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(pdhg_solver_.get_primal_solution().data(), - problem_wrap_container(op_problem_scaled_.variable_bounds)), - pdhg_solver_.get_primal_solution().data(), - pdhg_solver_.get_primal_solution().size(), - clamp(), - stream_view_.value()); + if (batch_mode_) { + // In batch mode variable_bounds are shared and only the bound rescaling is per climber. + // Apply it here too so the initial point is projected into the correct scaled space. + cub::DeviceTransform::Transform( + cuda::std::make_tuple( + pdhg_solver_.get_primal_solution().data(), + thrust::make_transform_iterator( + thrust::make_zip_iterator( + problem_wrap_container(op_problem_scaled_.variable_bounds), + batch_wrapped_container(initial_scaling_strategy_.get_bound_rescaling_vector(), + primal_size_h_)), + scale_bounds_by_scalar_op{})), + pdhg_solver_.get_primal_solution().data(), + pdhg_solver_.get_primal_solution().size(), + clamp(), + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(pdhg_solver_.get_primal_solution().data(), + problem_wrap_container(op_problem_scaled_.variable_bounds)), + pdhg_solver_.get_primal_solution().data(), + pdhg_solver_.get_primal_solution().size(), + clamp(), + stream_view_.value()); + } pdhg_solver_.refine_initial_primal_projection( initial_scaling_strategy_.get_bound_rescaling_vector()); @@ -2718,6 +2736,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co restart_strategy_.last_restart_duality_gap_.dual_solution_, dummy); } + transpose_problem_fields(/*to_row=*/true); } if (verbose) { From 30881ce2393292d2d4b7422f682857df074798c7 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 16:24:24 +0200 Subject: [PATCH 024/258] fixed the errors hihi, finished distributed part for compte_fixed_error --- .../distributed_pdlp/multi_gpu_engine.hpp | 99 ++++++++++--- cpp/src/pdlp/pdhg.cu | 45 ++++++ cpp/src/pdlp/pdhg.hpp | 9 ++ cpp/src/pdlp/pdlp.cu | 133 ++++++++++++++---- .../adaptive_step_size_strategy.cu | 20 +++ .../adaptive_step_size_strategy.hpp | 7 + 6 files changed, 266 insertions(+), 47 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index e9f48b9666..6d9cf9d3a3 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -102,10 +102,12 @@ struct multi_gpu_engine_t { // -------- Halo exchange (variables / x) --------------------------------- // Fills the halo slice [owned_var_size, total_var_size) of the per-shard - // reflected_primal vector (the buffer A @ x reads). Step 1: thrust::gather - // per-peer outgoing values into staging buffers. Step 2: a single NCCL - // group with matched ncclSend / ncclRecv across all (rank, peer) pairs. - void halo_exchange_var() + // input buffer returned by `buf_access(pdhg)` (the buffer A @ x will read). + // Step 1: thrust::gather per-peer outgoing values into staging buffers. + // Step 2: a single NCCL group with matched ncclSend / ncclRecv across all + // (rank, peer) pairs. + template + void halo_exchange_var(BufAccess&& buf_access) { const int nb = static_cast(shards.size()); @@ -113,7 +115,7 @@ struct multi_gpu_engine_t { for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto& x = s.sub_pdlp->pdhg_solver_.get_reflected_primal(); + auto& x = buf_access(s.sub_pdlp->pdhg_solver_); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; if (s.var_send_indices_d[peer].size() == 0) continue; @@ -144,7 +146,7 @@ struct multi_gpu_engine_t { auto& s = *shards[r]; auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& x = s.sub_pdlp->pdhg_solver_.get_reflected_primal(); + auto& x = buf_access(s.sub_pdlp->pdhg_solver_); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; @@ -160,16 +162,17 @@ struct multi_gpu_engine_t { } // -------- Halo exchange (constraints / y) ------------------------------- - // Same as halo_exchange_var but for the per-shard dual solution (the buffer - // A_T @ y reads) and constraint halos. - void halo_exchange_cstr() + // Same as halo_exchange_var but for a constraint-shaped buffer (the input + // A_T @ y will read) and constraint halos. + template + void halo_exchange_cstr(BufAccess&& buf_access) { const int nb = static_cast(shards.size()); for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto& y = s.sub_pdlp->pdhg_solver_.get_dual_solution(); + auto& y = buf_access(s.sub_pdlp->pdhg_solver_); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; if (s.cstr_send_indices_d[peer].size() == 0) continue; @@ -199,7 +202,7 @@ struct multi_gpu_engine_t { auto& s = *shards[r]; auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& y = s.sub_pdlp->pdhg_solver_.get_dual_solution(); + auto& y = buf_access(s.sub_pdlp->pdhg_solver_); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; @@ -214,28 +217,78 @@ struct multi_gpu_engine_t { ncclGroupEnd(); } - // -------- High-level: A @ x and A_T @ y --------------------------------- - // A @ x: halo-update the reflected_primal vector, then per-shard SpMV. - // Named distributed_* (rather than compute_*) to make call sites in pdhg.cu - // self-documenting and to avoid name collision with pdhg_solver_t's own - // compute_A_x / compute_At_y, which the engine dispatches into per shard. - void distributed_compute_A_x() + // -------- NCCL allreduce (sum, in place) -------------------------------- + // Per-shard in-place sum-allreduce. Each shard's stream issues an + // ncclAllReduce(buf, buf, count, ncclFloat64, ncclSum, ...) inside a single + // group. After this returns, every shard's buffer holds the global sum. + // + // PtrAccess: pdlp_solver_t& -> f_t* (e.g. into step_size_strategy_). + template + void allreduce_sum_inplace(PtrAccess&& ptr_access, size_t count = 1) + { + ncclGroupStart(); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + f_t* buf = ptr_access(*s->sub_pdlp); + ncclAllReduce(buf, + buf, + count, + ncclFloat64, + ncclSum, + s->comm.get(), + s->stream.view().value()); + } + ncclGroupEnd(); + } + + // -------- Generic distributed SpMVs ------------------------------------- + // distributed_spmv_A : halo-update the var-shaped input buffer returned by + // `in_buf(pdhg)`, then per-shard A @ in_buf -> out_desc. + // distributed_spmv_At: halo-update the cstr-shaped input buffer returned by + // `in_buf(pdhg)`, then per-shard A_T @ in_buf -> out_desc. + // + // Accessor signatures: + // in_buf (pdhg_solver_t&) -> rmm::device_uvector& + // out_desc(pdhg_solver_t&) -> cusparseDnVecDescr_t + template + void distributed_spmv_A(InBufAccess&& in_buf, OutDescAccess&& out_desc) { - halo_exchange_var(); + halo_exchange_var(in_buf); for_each_shard([&](auto& shard) { - shard.sub_pdlp->pdhg_solver_.spmvop_A_x(); + auto& sub_pdhg = shard.sub_pdlp->pdhg_solver_; + sub_pdhg.spmv_A_into(in_buf(sub_pdhg), out_desc(sub_pdhg)); }); } - // A_T @ y: halo-update the dual solution vector, then per-shard SpMV. - void distributed_compute_At_y() + template + void distributed_spmv_At(InBufAccess&& in_buf, OutDescAccess&& out_desc) { - halo_exchange_cstr(); + halo_exchange_cstr(in_buf); for_each_shard([&](auto& shard) { - shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); + auto& sub_pdhg = shard.sub_pdlp->pdhg_solver_; + sub_pdhg.spmv_At_into(in_buf(sub_pdhg), out_desc(sub_pdhg)); }); } + // -------- High-level: A @ x and A_T @ y --------------------------------- + // Thin wrappers used from pdhg_solver_t::compute_A_x / compute_At_y when an + // engine is wired in. They use the canonical PDHG buffers/descriptors so the + // result lands where single-GPU PDHG would have put it (dual_gradient for A, + // current_AtY for A_T). + void distributed_compute_A_x() + { + distributed_spmv_A( + [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_primal(); }, + [](auto& pdhg) -> cusparseDnVecDescr_t { return pdhg.get_cusparse_view().dual_gradient; }); + } + + void distributed_compute_At_y() + { + distributed_spmv_At( + [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_dual_solution(); }, + [](auto& pdhg) -> cusparseDnVecDescr_t { return pdhg.get_cusparse_view().current_AtY; }); + } + // Engine-level stream for fork/join orchestration (master side). rmm::cuda_stream stream; diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index fb0fc9b611..56c61aedda 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -623,6 +623,51 @@ void pdhg_solver_t::compute_A_x() } } +template +void pdhg_solver_t::spmv_At_into(rmm::device_uvector& in_buf, + cusparseDnVecDescr_t out_desc) +{ + RAFT_CUSPARSE_TRY(cusparseDnVecSetValues(cusparse_view_.dual_solution, in_buf.data())); + RAFT_CUSPARSE_TRY( + raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), + CUSPARSE_OPERATION_NON_TRANSPOSE, + reusable_device_scalar_value_1_.data(), + cusparse_view_.A_T, + cusparse_view_.dual_solution, + reusable_device_scalar_value_0_.data(), + out_desc, + CUSPARSE_SPMV_CSR_ALG2, + (f_t*)cusparse_view_.buffer_transpose.data(), + stream_view_)); + // Restore the canonical binding so subsequent code on this shard that reads + // cv.dual_solution sees the dual_solution_ buffer it was constructed with. + RAFT_CUSPARSE_TRY(cusparseDnVecSetValues( + cusparse_view_.dual_solution, current_saddle_point_state_.get_dual_solution().data())); +} + +template +void pdhg_solver_t::spmv_A_into(rmm::device_uvector& in_buf, + cusparseDnVecDescr_t out_desc) +{ + RAFT_CUSPARSE_TRY( + cusparseDnVecSetValues(cusparse_view_.reflected_primal_solution, in_buf.data())); + RAFT_CUSPARSE_TRY( + raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), + CUSPARSE_OPERATION_NON_TRANSPOSE, + reusable_device_scalar_value_1_.data(), + cusparse_view_.A, + cusparse_view_.reflected_primal_solution, + reusable_device_scalar_value_0_.data(), + out_desc, + CUSPARSE_SPMV_CSR_ALG2, + (f_t*)cusparse_view_.buffer_non_transpose.data(), + stream_view_)); + // Restore the canonical binding so subsequent code on this shard that reads + // cv.reflected_primal_solution sees the reflected_primal_ buffer. + RAFT_CUSPARSE_TRY( + cusparseDnVecSetValues(cusparse_view_.reflected_primal_solution, reflected_primal_.data())); +} + template void pdhg_solver_t::compute_primal_projection_with_gradient( rmm::device_uvector& primal_step_size) diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index 8226d2cecc..8fbee24e71 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -93,6 +93,15 @@ class pdhg_solver_t { void compute_A_x(); void spmvop_At_y(); void spmvop_A_x(); + + // Parameterized SpMVs used by the multi-GPU engine. + // Both temporarily hijack a canonical input descriptor in cusparse_view_ + // (cv.dual_solution for At, cv.reflected_primal_solution for A) to point at + // `in_buf.data()`, run the local SpMV into `out_desc`, then restore the + // descriptor to its original buffer so other code on this shard is unaffected. + // No multi-GPU dispatch inside — the engine is the orchestrator. + void spmv_At_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); + void spmv_A_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); // Pure cub-transform extractions. Each one is byte-identical to the inline // cub call it replaces — no platform dispatch inside. Callers handle the diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index b5fe5ad6ca..7203c11a42 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2221,34 +2221,118 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte // Computing the deltas // TODO batch mdoe: this only works if everyone restarts - cub::DeviceTransform::Transform(cuda::std::make_tuple(pdhg_solver_.get_reflected_primal().data(), - pdhg_solver_.get_primal_solution().data()), - pdhg_solver_.get_saddle_point_state().get_delta_primal().data(), - pdhg_solver_.get_primal_solution().size(), - cuda::std::minus{}, - stream_view_.value()); - cub::DeviceTransform::Transform(cuda::std::make_tuple(pdhg_solver_.get_reflected_dual().data(), - pdhg_solver_.get_dual_solution().data()), - pdhg_solver_.get_saddle_point_state().get_delta_dual().data(), - pdhg_solver_.get_dual_solution().size(), - cuda::std::minus{}, - stream_view_.value()); + if (multi_gpu_engine) { + // Go faire une fonction compute_delta_primal, compute_delta primal ? + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdhg = shard->sub_pdlp->pdhg_solver_; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(sub_pdhg.get_reflected_primal().data(), + sub_pdhg.get_primal_solution().data()), + sub_pdhg.get_saddle_point_state().get_delta_primal().data(), + sub_pdhg.get_primal_solution().size(), + cuda::std::minus{}, + shard->stream.view()); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(sub_pdhg.get_reflected_dual().data(), + sub_pdhg.get_dual_solution().data()), + sub_pdhg.get_saddle_point_state().get_delta_dual().data(), + sub_pdhg.get_dual_solution().size(), + cuda::std::minus{}, + shard->stream.view()); + } + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(pdhg_solver_.get_reflected_primal().data(), + pdhg_solver_.get_primal_solution().data()), + pdhg_solver_.get_saddle_point_state().get_delta_primal().data(), + pdhg_solver_.get_primal_solution().size(), + cuda::std::minus{}, + stream_view_.value()); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(pdhg_solver_.get_reflected_dual().data(), + pdhg_solver_.get_dual_solution().data()), + pdhg_solver_.get_saddle_point_state().get_delta_dual().data(), + pdhg_solver_.get_dual_solution().size(), + cuda::std::minus{}, + stream_view_.value()); + } auto& cusparse_view = pdhg_solver_.get_cusparse_view(); - // Sync to make sure all previous cuSparse operations are finished before setting the - // potential_next_dual_solution - RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); - // Make potential_next_dual_solution point towards reflected dual solution to reuse the code - RAFT_CUSPARSE_TRY(cusparseDnVecSetValues(cusparse_view.potential_next_dual_solution, - (void*)pdhg_solver_.get_reflected_dual().data())); + if (multi_gpu_engine) { - if (batch_mode_) - RAFT_CUSPARSE_TRY(cusparseDnMatSetValues(cusparse_view.batch_potential_next_dual_solution, + // SpMV is the first operation in compute_interaction_and_movement so we can do halo before and call it naturally + // we then reduce the local dot products + multi_gpu_engine->halo_exchange_cstr( + [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_dual(); }); + + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdlp = *shard->sub_pdlp; + auto& sub_cv = sub_pdlp.pdhg_solver_.get_cusparse_view(); + + RAFT_CUSPARSE_TRY( + cusparseDnVecSetValues(sub_cv.potential_next_dual_solution, + (void*)sub_pdlp.pdhg_solver_.get_reflected_dual().data())); + + sub_pdlp.step_size_strategy_.compute_interaction_and_movement( + sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), + sub_cv, + sub_pdlp.pdhg_solver_.get_saddle_point_state()); + + RAFT_CUSPARSE_TRY(cusparseDnVecSetValues( + sub_cv.potential_next_dual_solution, + (void*)sub_pdlp.pdhg_solver_.get_potential_next_dual_solution().data())); + } + + multi_gpu_engine->allreduce_sum_inplace( + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }, 1); + multi_gpu_engine->allreduce_sum_inplace( + [](auto& sp) -> f_t* { + return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); + }, + 1); + multi_gpu_engine->allreduce_sum_inplace( + [](auto& sp) -> f_t* { + return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); + }, + 1); + + auto& s0 = *multi_gpu_engine->shards[0]; + { + raft::device_setter guard(s0.device_id); + RAFT_CUDA_TRY(cudaStreamSynchronize(s0.stream.view().value())); + } + auto& src_sp = s0.sub_pdlp->step_size_strategy_; + raft::copy(step_size_strategy_.get_interaction().data(), + src_sp.get_interaction().data(), + 1, + stream_view_); + raft::copy(step_size_strategy_.get_norm_squared_delta_primal().data(), + src_sp.get_norm_squared_delta_primal().data(), + 1, + stream_view_); + raft::copy(step_size_strategy_.get_norm_squared_delta_dual().data(), + src_sp.get_norm_squared_delta_dual().data(), + 1, + stream_view_); + } else { + // Sync to make sure all previous cuSparse operations are finished before setting the + // potential_next_dual_solution + RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); + + // Make potential_next_dual_solution point towards reflected dual solution to reuse the code + RAFT_CUSPARSE_TRY(cusparseDnVecSetValues(cusparse_view.potential_next_dual_solution, (void*)pdhg_solver_.get_reflected_dual().data())); - step_size_strategy_.compute_interaction_and_movement( - pdhg_solver_.get_primal_tmp_resource(), cusparse_view, pdhg_solver_.get_saddle_point_state()); + if (batch_mode_) + RAFT_CUSPARSE_TRY(cusparseDnMatSetValues(cusparse_view.batch_potential_next_dual_solution, + (void*)pdhg_solver_.get_reflected_dual().data())); + + step_size_strategy_.compute_interaction_and_movement( + pdhg_solver_.get_primal_tmp_resource(), cusparse_view, pdhg_solver_.get_saddle_point_state()); + } if (batch_mode_) { const auto [grid_size, block_size] = kernel_config_from_batch_size(climber_strategies_.size()); @@ -2279,11 +2363,12 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte // potential_next_dual_solution RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); - // Put back + // Put back, already done in multi-gpu side + if (!multi_gpu_engine) { RAFT_CUSPARSE_TRY( cusparseDnVecSetValues(cusparse_view.potential_next_dual_solution, (void*)pdhg_solver_.get_potential_next_dual_solution().data())); - + } if (batch_mode_) { RAFT_CUSPARSE_TRY( cusparseDnMatSetValues(cusparse_view.batch_potential_next_dual_solution, diff --git a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu index 1f137dc9ea..fb85be4280 100644 --- a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu +++ b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu @@ -309,6 +309,26 @@ adaptive_step_size_strategy_t::get_norm_squared_delta_dual() const return norm_squared_delta_dual_; } +template +rmm::device_uvector& adaptive_step_size_strategy_t::get_interaction() +{ + return interaction_; +} + +template +rmm::device_uvector& +adaptive_step_size_strategy_t::get_norm_squared_delta_primal() +{ + return norm_squared_delta_primal_; +} + +template +rmm::device_uvector& +adaptive_step_size_strategy_t::get_norm_squared_delta_dual() +{ + return norm_squared_delta_dual_; +} + template void adaptive_step_size_strategy_t::set_valid_step_size(i_t valid) { diff --git a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp index 1e969150e7..896c6fa24e 100644 --- a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp +++ b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp @@ -81,6 +81,13 @@ class adaptive_step_size_strategy_t { const rmm::device_uvector& get_norm_squared_delta_primal() const; const rmm::device_uvector& get_norm_squared_delta_dual() const; + // Mutable overloads — used by the multi-GPU path to NCCL-allreduce the + // per-shard scalar contributions in place and to mirror them back to the + // master step_size_strategy_. + rmm::device_uvector& get_interaction(); + rmm::device_uvector& get_norm_squared_delta_primal(); + rmm::device_uvector& get_norm_squared_delta_dual(); + void compute_interaction_and_movement(rmm::device_uvector& tmp_primal, cusparse_view_t& cusparse_view, saddle_point_state_t& current_saddle_point_state); From c33faf2d4d0ce0b00390553bae0c9c6e70b0c03d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 22 May 2026 16:27:00 +0200 Subject: [PATCH 025/258] style --- .../distributed_pdlp/multi_gpu_engine.hpp | 60 +-- .../pdlp/distributed_pdlp/partition_loader.cu | 12 +- cpp/src/pdlp/distributed_pdlp/shard.cu | 6 +- .../initial_scaling.cu | 6 +- cpp/src/pdlp/pdhg.cu | 30 +- cpp/src/pdlp/pdhg.hpp | 5 +- cpp/src/pdlp/pdlp.cu | 424 +++++++++--------- .../adaptive_step_size_strategy.cu | 6 +- 8 files changed, 259 insertions(+), 290 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 6d9cf9d3a3..001f9b760e 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -15,9 +15,9 @@ #include #include +#include #include #include -#include #include @@ -53,51 +53,35 @@ struct multi_gpu_engine_t { multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; multi_gpu_engine_t& operator=(const multi_gpu_engine_t&) = delete; - - template void for_each_shard(Fn&& fn) { for (auto& s : shards) { - raft::device_setter guard(s->device_id); - fn(*s); + raft::device_setter guard(s->device_id); + fn(*s); } } - template + template void distributed_transform(std::tuple in_accessors, - OutAccess out, - SizeAccess sz, - Op op) + OutAccess out, + SizeAccess sz, + Op op) { for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; // turns the Tuple of lambdas into a tuple of rmm::device_uvector auto cub_inputs = std::apply( - [&sub](auto&... acc) { return cuda::std::make_tuple(acc(sub)...); }, - in_accessors); + [&sub](auto&... acc) { return cuda::std::make_tuple(acc(sub)...); }, in_accessors); - cub::DeviceTransform::Transform(cub_inputs, - out(sub), - sz(sub), - op, - shard.stream.view()); + cub::DeviceTransform::Transform(cub_inputs, out(sub), sz(sub), op, shard.stream.view()); }); } // --- 2) convenience: single input accessor (delegates) --- - template - void distributed_transform(InAccess in, - OutAccess out, - SizeAccess sz, - Op op) + template + void distributed_transform(InAccess in, OutAccess out, SizeAccess sz, Op op) { - distributed_transform(std::make_tuple(in), out, sz, op); + distributed_transform(std::make_tuple(in), out, sz, op); } // -------- Halo exchange (variables / x) --------------------------------- @@ -143,10 +127,10 @@ struct multi_gpu_engine_t { } } for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - auto& rd = s.rank_data; + auto& s = *shards[r]; + auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& x = buf_access(s.sub_pdlp->pdhg_solver_); + auto& x = buf_access(s.sub_pdlp->pdhg_solver_); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; @@ -199,10 +183,10 @@ struct multi_gpu_engine_t { } } for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - auto& rd = s.rank_data; + auto& s = *shards[r]; + auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& y = buf_access(s.sub_pdlp->pdhg_solver_); + auto& y = buf_access(s.sub_pdlp->pdhg_solver_); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; @@ -230,13 +214,7 @@ struct multi_gpu_engine_t { for (auto& s : shards) { raft::device_setter guard(s->device_id); f_t* buf = ptr_access(*s->sub_pdlp); - ncclAllReduce(buf, - buf, - count, - ncclFloat64, - ncclSum, - s->comm.get(), - s->stream.view().value()); + ncclAllReduce(buf, buf, count, ncclFloat64, ncclSum, s->comm.get(), s->stream.view().value()); } ncclGroupEnd(); } diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 007df4ce1c..b9bc71ae9e 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -19,9 +19,9 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ { std::ifstream part_file(file); cuopt_expects(part_file.is_open(), - error_type_t::ValidationError, - "Failed to open partition file: %s", - file.c_str()); + error_type_t::ValidationError, + "Failed to open partition file: %s", + file.c_str()); // One integer per line; operator>> skips whitespace so blank lines and // trailing newlines are tolerated. @@ -33,9 +33,9 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ // We must have hit EOF cleanly; any other state means a malformed token. cuopt_expects(part_file.eof(), - error_type_t::ValidationError, - "Malformed partition file (expected one integer per line): %s", - file.c_str()); + error_type_t::ValidationError, + "Malformed partition file (expected one integer per line): %s", + file.c_str()); return parts; } diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index c66b03755e..33aac38103 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -202,9 +202,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, // send_indices_d[p] : local indices to gather (uploaded from host send plan) // send_buf_d[p] : f_t staging buffer sized to match // Self-peer slot is present but empty (size 0). Used in engine halo exchange. - auto build_send_plan = [&](auto const& send_per_peer, - auto& indices_d, - auto& buf_d) { + auto build_send_plan = [&](auto const& send_per_peer, auto& indices_d, auto& buf_d) { const std::size_t n_peers = send_per_peer.size(); indices_d.reserve(n_peers); buf_d.reserve(n_peers); @@ -218,7 +216,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, buf_d.emplace_back(std::move(buf)); } }; - build_send_plan(rank_data.var_send_per_peer, var_send_indices_d, var_send_buf_d); + build_send_plan(rank_data.var_send_per_peer, var_send_indices_d, var_send_buf_d); build_send_plan(rank_data.cstr_send_per_peer, cstr_send_indices_d, cstr_send_buf_d); handle.sync_stream(stream_view); diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index fd6e02079e..478753e9d9 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -939,10 +939,8 @@ template void pdlp_initial_scaling_strategy_t::set_h_bound_rescaling(f_t value) { std::fill(h_bound_rescaling_.begin(), h_bound_rescaling_.end(), value); - thrust::fill(handle_ptr_->get_thrust_policy(), - bound_rescaling_.begin(), - bound_rescaling_.end(), - value); + thrust::fill( + handle_ptr_->get_thrust_policy(), bound_rescaling_.begin(), bound_rescaling_.end(), value); } template diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index 56c61aedda..969f5d0d30 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -8,8 +8,8 @@ // pdlp.cuh defines pdlp_solver_t which the engine's compute_A_x/compute_At_y // template bodies dereference via shard.sub_pdlp->pdhg_solver_. Must be a // complete type at the point of template instantiation below. -#include #include +#include #include #include #include @@ -628,21 +628,20 @@ void pdhg_solver_t::spmv_At_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc) { RAFT_CUSPARSE_TRY(cusparseDnVecSetValues(cusparse_view_.dual_solution, in_buf.data())); - RAFT_CUSPARSE_TRY( - raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), - CUSPARSE_OPERATION_NON_TRANSPOSE, - reusable_device_scalar_value_1_.data(), - cusparse_view_.A_T, - cusparse_view_.dual_solution, - reusable_device_scalar_value_0_.data(), - out_desc, - CUSPARSE_SPMV_CSR_ALG2, - (f_t*)cusparse_view_.buffer_transpose.data(), - stream_view_)); + RAFT_CUSPARSE_TRY(raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), + CUSPARSE_OPERATION_NON_TRANSPOSE, + reusable_device_scalar_value_1_.data(), + cusparse_view_.A_T, + cusparse_view_.dual_solution, + reusable_device_scalar_value_0_.data(), + out_desc, + CUSPARSE_SPMV_CSR_ALG2, + (f_t*)cusparse_view_.buffer_transpose.data(), + stream_view_)); // Restore the canonical binding so subsequent code on this shard that reads // cv.dual_solution sees the dual_solution_ buffer it was constructed with. - RAFT_CUSPARSE_TRY(cusparseDnVecSetValues( - cusparse_view_.dual_solution, current_saddle_point_state_.get_dual_solution().data())); + RAFT_CUSPARSE_TRY(cusparseDnVecSetValues(cusparse_view_.dual_solution, + current_saddle_point_state_.get_dual_solution().data())); } template @@ -1434,8 +1433,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( for (auto& shard : mgpu_engine_->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; - sub_pdlp.pdhg_solver_.dual_reflected_projection_transform( - sub_pdlp.get_dual_step_size()); + sub_pdlp.pdhg_solver_.dual_reflected_projection_transform(sub_pdlp.get_dual_step_size()); } } else if (!batch_mode_) { dual_reflected_projection_transform(dual_step_size); diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index 8fbee24e71..e38ea9389c 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -102,13 +102,12 @@ class pdhg_solver_t { // No multi-GPU dispatch inside — the engine is the orchestrator. void spmv_At_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); void spmv_A_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); - + // Pure cub-transform extractions. Each one is byte-identical to the inline // cub call it replaces — no platform dispatch inside. Callers handle the // single-GPU vs per-shard branching at the call site (see the // "if (mgpu_engine_) for shard..." blocks in compute_next_*). - void primal_reflected_major_projection_transform( - rmm::device_uvector& primal_step_size); + void primal_reflected_major_projection_transform(rmm::device_uvector& primal_step_size); void dual_reflected_major_projection_transform(rmm::device_uvector& dual_step_size); void primal_reflected_projection_transform(rmm::device_uvector& primal_step_size); void dual_reflected_projection_transform(rmm::device_uvector& dual_step_size); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 7203c11a42..302f62e56a 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -44,8 +44,8 @@ #include #include #include -#include #include +#include #include namespace cuopt::linear_programming::detail { @@ -398,189 +398,195 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, "Distributed PDLP (num_gpus > 1) currently requires double precision"); return; } else { - // 2. Load partition - std::vector parts; - if (!settings.multi_gpu_partition_file.empty()) { - parts = partition_loader_t::parse_distributed_pdlp_partition_file( - settings.multi_gpu_partition_file); - } else { - cuopt_expects(false, - error_type_t::RuntimeError, - "Metis partitioning inside cuopt not implemented yet; " - "provide a --parts file via settings.multi_gpu_partition_file"); - } - - // always compute initial step size before scaling and primal_weight after scaling to do like - // cuPDLPx - assert(settings_.hyper_params.compute_initial_primal_weight_before_scaling && - "compute_initial_primal_weight_before_scaling must be true in distributed mode"); - assert(!settings_.hyper_params.compute_initial_step_size_before_scaling && - "compute_initial_step_size_before_scaling must be false in distributed mode"); - - compute_initial_primal_weight(); - - // scale globally before dispatching to shards - initial_scaling_strategy_.scale_problem(); - - compute_initial_step_size(); - step_size_strategy_.get_primal_and_dual_stepsizes(primal_step_size_, dual_step_size_); - - const f_t initial_step_size_global = get_step_size_h(0); - const f_t initial_primal_weight_global = get_primal_weight_h(0); - - // 4. Copy both scaled and unscaled pb - auto const stream = op_problem_scaled_.handle_ptr->get_stream(); - i_t const n_cstr = op_problem_scaled_.n_constraints; - i_t const n_vars = op_problem_scaled_.n_variables; - i_t const nnz = op_problem_scaled_.nnz; - - // Shared topology (taken from the scaled problem, but identical on both). - std::vector h_A_row_offsets(n_cstr + 1); - std::vector h_A_col_indices(nnz); - std::vector h_A_t_row_offsets(n_vars + 1); - std::vector h_A_t_col_indices(nnz); - raft::copy(h_A_row_offsets.data(), op_problem_scaled_.offsets.data(), n_cstr + 1, stream); - raft::copy(h_A_col_indices.data(), op_problem_scaled_.variables.data(), nnz, stream); - raft::copy( - h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets.data(), n_vars + 1, stream); - raft::copy(h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints.data(), nnz, stream); - - // Paired value arrays for A and A_T. - std::vector h_A_values(nnz); - std::vector h_A_values_scaled(nnz); - std::vector h_A_t_values(nnz); - std::vector h_A_t_values_scaled(nnz); - raft::copy(h_A_values.data(), problem_ptr->coefficients.data(), nnz, stream); - raft::copy(h_A_t_values.data(), problem_ptr->reverse_coefficients.data(), nnz, stream); - raft::copy(h_A_values_scaled.data(), op_problem_scaled_.coefficients.data(), nnz, stream); - raft::copy( - h_A_t_values_scaled.data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); + // 2. Load partition + std::vector parts; + if (!settings.multi_gpu_partition_file.empty()) { + parts = partition_loader_t::parse_distributed_pdlp_partition_file( + settings.multi_gpu_partition_file); + } else { + cuopt_expects(false, + error_type_t::RuntimeError, + "Metis partitioning inside cuopt not implemented yet; " + "provide a --parts file via settings.multi_gpu_partition_file"); + } - using f_t2 = typename type_2::type; + // always compute initial step size before scaling and primal_weight after scaling to do like + // cuPDLPx + assert(settings_.hyper_params.compute_initial_primal_weight_before_scaling && + "compute_initial_primal_weight_before_scaling must be true in distributed mode"); + assert(!settings_.hyper_params.compute_initial_step_size_before_scaling && + "compute_initial_step_size_before_scaling must be false in distributed mode"); + + compute_initial_primal_weight(); + + // scale globally before dispatching to shards + initial_scaling_strategy_.scale_problem(); + + compute_initial_step_size(); + step_size_strategy_.get_primal_and_dual_stepsizes(primal_step_size_, dual_step_size_); + + const f_t initial_step_size_global = get_step_size_h(0); + const f_t initial_primal_weight_global = get_primal_weight_h(0); + + // 4. Copy both scaled and unscaled pb + auto const stream = op_problem_scaled_.handle_ptr->get_stream(); + i_t const n_cstr = op_problem_scaled_.n_constraints; + i_t const n_vars = op_problem_scaled_.n_variables; + i_t const nnz = op_problem_scaled_.nnz; + + // Shared topology (taken from the scaled problem, but identical on both). + std::vector h_A_row_offsets(n_cstr + 1); + std::vector h_A_col_indices(nnz); + std::vector h_A_t_row_offsets(n_vars + 1); + std::vector h_A_t_col_indices(nnz); + raft::copy(h_A_row_offsets.data(), op_problem_scaled_.offsets.data(), n_cstr + 1, stream); + raft::copy(h_A_col_indices.data(), op_problem_scaled_.variables.data(), nnz, stream); + raft::copy( + h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets.data(), n_vars + 1, stream); + raft::copy( + h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints.data(), nnz, stream); + + // Paired value arrays for A and A_T. + std::vector h_A_values(nnz); + std::vector h_A_values_scaled(nnz); + std::vector h_A_t_values(nnz); + std::vector h_A_t_values_scaled(nnz); + raft::copy(h_A_values.data(), problem_ptr->coefficients.data(), nnz, stream); + raft::copy(h_A_t_values.data(), problem_ptr->reverse_coefficients.data(), nnz, stream); + raft::copy(h_A_values_scaled.data(), op_problem_scaled_.coefficients.data(), nnz, stream); + raft::copy( + h_A_t_values_scaled.data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); - std::vector h_obj(n_vars); - std::vector h_obj_scaled(n_vars); - std::vector h_var_bounds_packed(n_vars); - std::vector h_var_bounds_scaled_packed(n_vars); - std::vector h_cstr_lower(n_cstr); - std::vector h_cstr_upper(n_cstr); - std::vector h_cstr_lower_scaled(n_cstr); - std::vector h_cstr_upper_scaled(n_cstr); - - raft::copy(h_obj.data(), problem_ptr->objective_coefficients.data(), n_vars, stream); - raft::copy(h_obj_scaled.data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); - raft::copy(h_var_bounds_packed.data(), problem_ptr->variable_bounds.data(), n_vars, stream); - raft::copy( - h_var_bounds_scaled_packed.data(), op_problem_scaled_.variable_bounds.data(), n_vars, stream); - raft::copy(h_cstr_lower.data(), problem_ptr->constraint_lower_bounds.data(), n_cstr, stream); - raft::copy(h_cstr_upper.data(), problem_ptr->constraint_upper_bounds.data(), n_cstr, stream); - raft::copy( - h_cstr_lower_scaled.data(), op_problem_scaled_.constraint_lower_bounds.data(), n_cstr, stream); - raft::copy( - h_cstr_upper_scaled.data(), op_problem_scaled_.constraint_upper_bounds.data(), n_cstr, stream); - - // 5. Get full scaling factors on host - std::vector h_cummulative_cstr_scaling(n_cstr); - std::vector h_cummulative_var_scaling(n_vars); - raft::copy(h_cummulative_cstr_scaling.data(), - initial_scaling_strategy_.get_constraint_matrix_scaling_vector().data(), - n_cstr, - stream); - raft::copy(h_cummulative_var_scaling.data(), - initial_scaling_strategy_.get_variable_scaling_vector().data(), - n_vars, - stream); - const f_t h_bound_rescaling = initial_scaling_strategy_.get_h_bound_rescaling(); - const f_t h_objective_rescaling = initial_scaling_strategy_.get_h_objective_rescaling(); - - op_problem_scaled_.handle_ptr->sync_stream(stream); - - // Unpack interleaved {lower, upper} into separate vectors for both - // versions, so the shard ctor's slicing loop is uniform. - std::vector h_var_lower(n_vars), h_var_upper(n_vars); - std::vector h_var_lower_scaled(n_vars), h_var_upper_scaled(n_vars); - for (i_t i = 0; i < n_vars; ++i) { - h_var_lower[i] = h_var_bounds_packed[i].x; - h_var_upper[i] = h_var_bounds_packed[i].y; - h_var_lower_scaled[i] = h_var_bounds_scaled_packed[i].x; - h_var_upper_scaled[i] = h_var_bounds_scaled_packed[i].y; - } - - // 6. Build per-rank data and meta-data. - std::vector> sub_pdlp_rank_data = - partition_loader_t::create_rank_data_from_parts(parts, - h_A_row_offsets, - h_A_col_indices, - h_A_values, - h_A_values_scaled, - h_A_t_row_offsets, - h_A_t_col_indices, - h_A_t_values, - h_A_t_values_scaled, - settings.num_gpus, - n_cstr, - n_vars, - nnz); - - // 7. Build the per-shard PDLP settings: - pdlp_solver_settings_t sub_pdlp_settings = settings; - sub_pdlp_settings.num_gpus = 1; - sub_pdlp_settings.multi_gpu_partition_file = ""; - sub_pdlp_settings.is_distributed_sub_pdlp = true; - sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; - sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; - - // 8. Construct the engine, creates NCCL comms and shards - multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), - h_obj, - h_var_lower, - h_var_upper, - h_cstr_lower, - h_cstr_upper, - h_obj_scaled, - h_var_lower_scaled, - h_var_upper_scaled, - h_cstr_lower_scaled, - h_cstr_upper_scaled, - h_cummulative_cstr_scaling, - h_cummulative_var_scaling, - h_bound_rescaling, - h_objective_rescaling, - op_problem_scaled_.maximize, - op_problem_scaled_.objective_offset, - op_problem_scaled_.presolve_data.objective_scaling_factor, - sub_pdlp_settings); - - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub = *shard->sub_pdlp; - raft::copy(sub.step_size_.data(), step_size_.data(), 1, shard->stream); - raft::copy(sub.primal_weight_.data(), primal_weight_.data(), 1, shard->stream); - raft::copy(sub.best_primal_weight_.data(), best_primal_weight_.data(), 1, shard->stream); - raft::copy(sub.primal_step_size_.data(), primal_step_size_.data(), 1, shard->stream); - raft::copy(sub.dual_step_size_.data(), dual_step_size_.data(), 1, shard->stream); - } - - // Wire the engine into the master pdhg_solver_. Shards' pdhg_solver_ keep - // mgpu_engine_ == nullptr so they run plain single-GPU SpMV on local A. - pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); - - // Project initial primal solution - if (settings_.hyper_params.project_initial_primal) { - // Use refine_initial_primal_projection ??? using f_t2 = typename type_2::type; + + std::vector h_obj(n_vars); + std::vector h_obj_scaled(n_vars); + std::vector h_var_bounds_packed(n_vars); + std::vector h_var_bounds_scaled_packed(n_vars); + std::vector h_cstr_lower(n_cstr); + std::vector h_cstr_upper(n_cstr); + std::vector h_cstr_lower_scaled(n_cstr); + std::vector h_cstr_upper_scaled(n_cstr); + + raft::copy(h_obj.data(), problem_ptr->objective_coefficients.data(), n_vars, stream); + raft::copy( + h_obj_scaled.data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); + raft::copy(h_var_bounds_packed.data(), problem_ptr->variable_bounds.data(), n_vars, stream); + raft::copy( + h_var_bounds_scaled_packed.data(), op_problem_scaled_.variable_bounds.data(), n_vars, stream); + raft::copy(h_cstr_lower.data(), problem_ptr->constraint_lower_bounds.data(), n_cstr, stream); + raft::copy(h_cstr_upper.data(), problem_ptr->constraint_upper_bounds.data(), n_cstr, stream); + raft::copy(h_cstr_lower_scaled.data(), + op_problem_scaled_.constraint_lower_bounds.data(), + n_cstr, + stream); + raft::copy(h_cstr_upper_scaled.data(), + op_problem_scaled_.constraint_upper_bounds.data(), + n_cstr, + stream); + + // 5. Get full scaling factors on host + std::vector h_cummulative_cstr_scaling(n_cstr); + std::vector h_cummulative_var_scaling(n_vars); + raft::copy(h_cummulative_cstr_scaling.data(), + initial_scaling_strategy_.get_constraint_matrix_scaling_vector().data(), + n_cstr, + stream); + raft::copy(h_cummulative_var_scaling.data(), + initial_scaling_strategy_.get_variable_scaling_vector().data(), + n_vars, + stream); + const f_t h_bound_rescaling = initial_scaling_strategy_.get_h_bound_rescaling(); + const f_t h_objective_rescaling = initial_scaling_strategy_.get_h_objective_rescaling(); + + op_problem_scaled_.handle_ptr->sync_stream(stream); + + // Unpack interleaved {lower, upper} into separate vectors for both + // versions, so the shard ctor's slicing loop is uniform. + std::vector h_var_lower(n_vars), h_var_upper(n_vars); + std::vector h_var_lower_scaled(n_vars), h_var_upper_scaled(n_vars); + for (i_t i = 0; i < n_vars; ++i) { + h_var_lower[i] = h_var_bounds_packed[i].x; + h_var_upper[i] = h_var_bounds_packed[i].y; + h_var_lower_scaled[i] = h_var_bounds_scaled_packed[i].x; + h_var_upper_scaled[i] = h_var_bounds_scaled_packed[i].y; + } + + // 6. Build per-rank data and meta-data. + std::vector> sub_pdlp_rank_data = + partition_loader_t::create_rank_data_from_parts(parts, + h_A_row_offsets, + h_A_col_indices, + h_A_values, + h_A_values_scaled, + h_A_t_row_offsets, + h_A_t_col_indices, + h_A_t_values, + h_A_t_values_scaled, + settings.num_gpus, + n_cstr, + n_vars, + nnz); + + // 7. Build the per-shard PDLP settings: + pdlp_solver_settings_t sub_pdlp_settings = settings; + sub_pdlp_settings.num_gpus = 1; + sub_pdlp_settings.multi_gpu_partition_file = ""; + sub_pdlp_settings.is_distributed_sub_pdlp = true; + sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; + sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; + + // 8. Construct the engine, creates NCCL comms and shards + multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), + h_obj, + h_var_lower, + h_var_upper, + h_cstr_lower, + h_cstr_upper, + h_obj_scaled, + h_var_lower_scaled, + h_var_upper_scaled, + h_cstr_lower_scaled, + h_cstr_upper_scaled, + h_cummulative_cstr_scaling, + h_cummulative_var_scaling, + h_bound_rescaling, + h_objective_rescaling, + op_problem_scaled_.maximize, + op_problem_scaled_.objective_offset, + op_problem_scaled_.presolve_data.objective_scaling_factor, + sub_pdlp_settings); + for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); auto& sub = *shard->sub_pdlp; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(sub.pdhg_solver_.get_primal_solution().data(), - sub.get_op_problem_scaled().variable_bounds.data()), - sub.pdhg_solver_.get_primal_solution().data(), - sub.pdhg_solver_.get_primal_solution().size(), - clamp(), - shard->stream.view()); + raft::copy(sub.step_size_.data(), step_size_.data(), 1, shard->stream); + raft::copy(sub.primal_weight_.data(), primal_weight_.data(), 1, shard->stream); + raft::copy(sub.best_primal_weight_.data(), best_primal_weight_.data(), 1, shard->stream); + raft::copy(sub.primal_step_size_.data(), primal_step_size_.data(), 1, shard->stream); + raft::copy(sub.dual_step_size_.data(), dual_step_size_.data(), 1, shard->stream); + } + + // Wire the engine into the master pdhg_solver_. Shards' pdhg_solver_ keep + // mgpu_engine_ == nullptr so they run plain single-GPU SpMV on local A. + pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); + + // Project initial primal solution + if (settings_.hyper_params.project_initial_primal) { + // Use refine_initial_primal_projection ??? + using f_t2 = typename type_2::type; + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + auto& sub = *shard->sub_pdlp; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(sub.pdhg_solver_.get_primal_solution().data(), + sub.get_op_problem_scaled().variable_bounds.data()), + sub.pdhg_solver_.get_primal_solution().data(), + sub.pdhg_solver_.get_primal_solution().size(), + clamp(), + shard->stream.view()); + } } - } } // end if constexpr (std::is_same_v) } @@ -2222,24 +2228,22 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte // Computing the deltas // TODO batch mdoe: this only works if everyone restarts if (multi_gpu_engine) { - // Go faire une fonction compute_delta_primal, compute_delta primal ? + // Go faire une fonction compute_delta_primal, compute_delta primal ? for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdhg = shard->sub_pdlp->pdhg_solver_; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(sub_pdhg.get_reflected_primal().data(), - sub_pdhg.get_primal_solution().data()), - sub_pdhg.get_saddle_point_state().get_delta_primal().data(), - sub_pdhg.get_primal_solution().size(), - cuda::std::minus{}, - shard->stream.view()); - cub::DeviceTransform::Transform( - cuda::std::make_tuple(sub_pdhg.get_reflected_dual().data(), - sub_pdhg.get_dual_solution().data()), - sub_pdhg.get_saddle_point_state().get_delta_dual().data(), - sub_pdhg.get_dual_solution().size(), - cuda::std::minus{}, - shard->stream.view()); + cub::DeviceTransform::Transform(cuda::std::make_tuple(sub_pdhg.get_reflected_primal().data(), + sub_pdhg.get_primal_solution().data()), + sub_pdhg.get_saddle_point_state().get_delta_primal().data(), + sub_pdhg.get_primal_solution().size(), + cuda::std::minus{}, + shard->stream.view()); + cub::DeviceTransform::Transform(cuda::std::make_tuple(sub_pdhg.get_reflected_dual().data(), + sub_pdhg.get_dual_solution().data()), + sub_pdhg.get_saddle_point_state().get_delta_dual().data(), + sub_pdhg.get_dual_solution().size(), + cuda::std::minus{}, + shard->stream.view()); } } else { cub::DeviceTransform::Transform( @@ -2249,21 +2253,19 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte pdhg_solver_.get_primal_solution().size(), cuda::std::minus{}, stream_view_.value()); - cub::DeviceTransform::Transform( - cuda::std::make_tuple(pdhg_solver_.get_reflected_dual().data(), - pdhg_solver_.get_dual_solution().data()), - pdhg_solver_.get_saddle_point_state().get_delta_dual().data(), - pdhg_solver_.get_dual_solution().size(), - cuda::std::minus{}, - stream_view_.value()); + cub::DeviceTransform::Transform(cuda::std::make_tuple(pdhg_solver_.get_reflected_dual().data(), + pdhg_solver_.get_dual_solution().data()), + pdhg_solver_.get_saddle_point_state().get_delta_dual().data(), + pdhg_solver_.get_dual_solution().size(), + cuda::std::minus{}, + stream_view_.value()); } auto& cusparse_view = pdhg_solver_.get_cusparse_view(); if (multi_gpu_engine) { - - // SpMV is the first operation in compute_interaction_and_movement so we can do halo before and call it naturally - // we then reduce the local dot products + // SpMV is the first operation in compute_interaction_and_movement so we can do halo before and + // call it naturally we then reduce the local dot products multi_gpu_engine->halo_exchange_cstr( [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_dual(); }); @@ -2294,9 +2296,7 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte }, 1); multi_gpu_engine->allreduce_sum_inplace( - [](auto& sp) -> f_t* { - return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); - }, + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }, 1); auto& s0 = *multi_gpu_engine->shards[0]; @@ -2365,10 +2365,10 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte // Put back, already done in multi-gpu side if (!multi_gpu_engine) { - RAFT_CUSPARSE_TRY( - cusparseDnVecSetValues(cusparse_view.potential_next_dual_solution, - (void*)pdhg_solver_.get_potential_next_dual_solution().data())); - } + RAFT_CUSPARSE_TRY( + cusparseDnVecSetValues(cusparse_view.potential_next_dual_solution, + (void*)pdhg_solver_.get_potential_next_dual_solution().data())); + } if (batch_mode_) { RAFT_CUSPARSE_TRY( cusparseDnMatSetValues(cusparse_view.batch_potential_next_dual_solution, @@ -2630,8 +2630,9 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co // Update FP32 matrix copies for mixed precision SpMV after scaling pdhg_solver_.get_cusparse_view().update_mixed_precision_matrices(); - // Redirect cuSPARSE descriptors to use the original problem's structural data (offsets, indices), - // then free the duplicated structural vectors from the scaled copy to save device memory. + // Redirect cuSPARSE descriptors to use the original problem's structural data (offsets, + // indices), then free the duplicated structural vectors from the scaled copy to save device + // memory. pdhg_solver_.get_cusparse_view().redirect_cusparse_csr_structure_pointers(*problem_ptr); op_problem_scaled_.variables.resize(0, stream_view_); op_problem_scaled_.offsets.resize(0, stream_view_); @@ -2846,7 +2847,6 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co "Initial primal_weight", primal_weight_.data(), primal_weight_.size(), std::cout); #endif - if (!inside_mip_) { CUOPT_LOG_INFO( " Iter Primal Obj. Dual Obj. Gap Primal Res. Dual Res. Time"); diff --git a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu index fb85be4280..2cb843ae86 100644 --- a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu +++ b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu @@ -316,15 +316,13 @@ rmm::device_uvector& adaptive_step_size_strategy_t::get_interacti } template -rmm::device_uvector& -adaptive_step_size_strategy_t::get_norm_squared_delta_primal() +rmm::device_uvector& adaptive_step_size_strategy_t::get_norm_squared_delta_primal() { return norm_squared_delta_primal_; } template -rmm::device_uvector& -adaptive_step_size_strategy_t::get_norm_squared_delta_dual() +rmm::device_uvector& adaptive_step_size_strategy_t::get_norm_squared_delta_dual() { return norm_squared_delta_dual_; } From 98e0ce68d67f3b9701c7b196d490754401c18a31 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 11:06:24 +0200 Subject: [PATCH 026/258] now manage halpern update in multi-gpu pdlp --- cpp/src/pdlp/pdlp.cu | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 302f62e56a..b69ceccae5 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3085,13 +3085,22 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co transpose_problem_fields(/*to_row=*/true); } } - halpern_update(); + if (multi_gpu_engine_) { + multi_gpu_engine_->for_each_shard([&](auto& shard) { shard.sub_pdlp->halpern_update(); }); + } else { + halpern_update(); + } } ++total_pdlp_iterations_; ++internal_solver_iterations_; - if (settings_.hyper_params.never_restart_to_average) - restart_strategy_.increment_iteration_since_last_restart(); + if (settings_.hyper_params.never_restart_to_average) { + if (multi_gpu_engine_) { + multi_gpu_engine_->for_each_shard([&](auto& shard) { shard.sub_pdlp->restart_strategy_.increment_iteration_since_last_restart(); }); + } else { + restart_strategy_.increment_iteration_since_last_restart(); + } + } } return optimization_problem_solution_t{pdlp_termination_status_t::NumericalError, stream_view_}; From 84128bf809348932fb6b540ae93d893feb7c4756 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 11:46:46 +0200 Subject: [PATCH 027/258] small fix to calls of multi_gpu_engine_ and scale/unscale solutions. compiles and runs --- cpp/src/pdlp/pdlp.cu | 47 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index b69ceccae5..36ba854439 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2896,6 +2896,9 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co // 1. At the very beginning of the solver, when no steps have been taken yet // 2. After a single step, since average of one step is the same step if (internal_solver_iterations_ <= 1) { + if (multi_gpu_engine) { + assert(false && "Not implemented"); + } raft::copy(unscaled_primal_avg_solution_.data(), pdhg_solver_.get_primal_solution().data(), primal_size_h_, @@ -2946,8 +2949,22 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co unscaled_dual_avg_solution_); } if (settings_.hyper_params.use_adaptive_step_size_strategy) { - initial_scaling_strategy_.unscale_solutions(pdhg_solver_.get_primal_solution(), - pdhg_solver_.get_dual_solution()); + if (multi_gpu_engine) { + // Master's pdhg_solver_.{primal,dual}_solution_ is stale in mGPU mode + // (live state lives on shards). Unscale in place on each shard with + // the shard's own initial_scaling_strategy_, which already holds the + // global cumulative scaling factors for its owned slice (set up in + // shard.cu via set_cummulative_scaling). Halo slots have unit scaling + // so unscaling is a no-op there (their values are junk anyway). + multi_gpu_engine->for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + sub.get_initial_scaling_strategy().unscale_solutions( + sub.pdhg_solver_.get_primal_solution(), sub.pdhg_solver_.get_dual_solution()); + }); + } else { + initial_scaling_strategy_.unscale_solutions(pdhg_solver_.get_primal_solution(), + pdhg_solver_.get_dual_solution()); + } } else { initial_scaling_strategy_.unscale_solutions( pdhg_solver_.get_potential_next_primal_solution(), @@ -2981,8 +2998,20 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co unscaled_dual_avg_solution_); } if (settings_.hyper_params.use_adaptive_step_size_strategy) { - initial_scaling_strategy_.scale_solutions(pdhg_solver_.get_primal_solution(), - pdhg_solver_.get_dual_solution()); + if (multi_gpu_engine) { + // Symmetric to the unscale dispatch above. Live state lives on + // shards; each shard's initial_scaling_strategy_ holds the global + // cumulative scaling factors for its owned slice (halo slots have + // unit scaling, so they're no-ops). Scale in place per shard. + multi_gpu_engine->for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + sub.get_initial_scaling_strategy().scale_solutions( + sub.pdhg_solver_.get_primal_solution(), sub.pdhg_solver_.get_dual_solution()); + }); + } else { + initial_scaling_strategy_.scale_solutions(pdhg_solver_.get_primal_solution(), + pdhg_solver_.get_dual_solution()); + } } else { initial_scaling_strategy_.scale_solutions( pdhg_solver_.get_potential_next_primal_solution(), @@ -3085,8 +3114,8 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co transpose_problem_fields(/*to_row=*/true); } } - if (multi_gpu_engine_) { - multi_gpu_engine_->for_each_shard([&](auto& shard) { shard.sub_pdlp->halpern_update(); }); + if (multi_gpu_engine) { + multi_gpu_engine->for_each_shard([&](auto& shard) { shard.sub_pdlp->halpern_update(); }); } else { halpern_update(); } @@ -3095,8 +3124,10 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co ++total_pdlp_iterations_; ++internal_solver_iterations_; if (settings_.hyper_params.never_restart_to_average) { - if (multi_gpu_engine_) { - multi_gpu_engine_->for_each_shard([&](auto& shard) { shard.sub_pdlp->restart_strategy_.increment_iteration_since_last_restart(); }); + if (multi_gpu_engine) { + multi_gpu_engine->for_each_shard([&](auto& shard) { + shard.sub_pdlp->restart_strategy_.increment_iteration_since_last_restart(); + }); } else { restart_strategy_.increment_iteration_since_last_restart(); } From abe4dd23e41ee7cb7cdba4ca3ca7979874b39856 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 11:54:45 +0200 Subject: [PATCH 028/258] comments --- cpp/src/pdlp/pdlp.cu | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 36ba854439..e2aeb3f08c 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2950,12 +2950,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co } if (settings_.hyper_params.use_adaptive_step_size_strategy) { if (multi_gpu_engine) { - // Master's pdhg_solver_.{primal,dual}_solution_ is stale in mGPU mode - // (live state lives on shards). Unscale in place on each shard with - // the shard's own initial_scaling_strategy_, which already holds the - // global cumulative scaling factors for its owned slice (set up in - // shard.cu via set_cummulative_scaling). Halo slots have unit scaling - // so unscaling is a no-op there (their values are junk anyway). + // The only branch in cuPDLPx multi_gpu_engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; sub.get_initial_scaling_strategy().unscale_solutions( @@ -2999,10 +2994,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co } if (settings_.hyper_params.use_adaptive_step_size_strategy) { if (multi_gpu_engine) { - // Symmetric to the unscale dispatch above. Live state lives on - // shards; each shard's initial_scaling_strategy_ holds the global - // cumulative scaling factors for its owned slice (halo slots have - // unit scaling, so they're no-ops). Scale in place per shard. + // The only branch in cuPDLPx multi_gpu_engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; sub.get_initial_scaling_strategy().scale_solutions( From 5c41497080dd3950c378d485a5ada75c1658f31f Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 12:06:51 +0200 Subject: [PATCH 029/258] added is multi gpu to pdhg --- cpp/src/pdlp/distributed_pdlp/shard.cu | 2 ++ cpp/src/pdlp/pdhg.hpp | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 33aac38103..405e6fa05c 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -155,6 +155,8 @@ pdlp_shard_t::pdlp_shard_t(int device_id, // unit cumulative factors (sub-settings disable Ruiz / PC iters). sub_pdlp = std::make_unique>(*sub_problem, settings, /*batch=*/false); + sub_pdlp->pdhg_solver_.set_is_multi_gpu(true); + // Inject master-scaled buffers inside sub_pdlp auto& scaled = sub_pdlp->get_op_problem_scaled(); raft::copy(scaled.coefficients.data(), diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index e38ea9389c..2e230eaf86 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -114,8 +114,19 @@ class pdhg_solver_t { // Master PDLP wires up the engine pointer here after the engine is built. // Shards' pdhg_solver_ leaves this null so each shard runs single-GPU SpMV - // on its local matrix. - void set_multi_gpu_engine(multi_gpu_engine_t* engine) { mgpu_engine_ = engine; } + // on its local matrix. Also flips is_multi_gpu_ — convenience flag that any + // pdhg participating in a distributed run (master OR shard) carries true. + void set_multi_gpu_engine(multi_gpu_engine_t* engine) + { + mgpu_engine_ = engine; + is_multi_gpu_ = (engine != nullptr); + } + + // Mark a shard's pdhg_solver_ as part of a distributed run without giving it + // an engine (shards don't orchestrate; they only run local SpMV on owned + // rows). Called from shard.cu right after sub_pdlp is constructed. + void set_is_multi_gpu(bool v) { is_multi_gpu_ = v; } + bool is_multi_gpu() const { return is_multi_gpu_; } i_t total_pdhg_iterations_; @@ -136,6 +147,7 @@ class pdhg_solver_t { void compute_primal_projection(rmm::device_uvector& primal_step_size); bool batch_mode_{false}; + bool is_multi_gpu_{false}; raft::handle_t const* handle_ptr_{nullptr}; rmm::cuda_stream_view stream_view_; From 37b1fdafab439c4b0be39b7d467b31d0f23110b5 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 12:24:45 +0200 Subject: [PATCH 030/258] added pdhg get mgpu engine --- cpp/src/pdlp/pdhg.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index 2e230eaf86..e4d16360a7 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -127,6 +127,7 @@ class pdhg_solver_t { // rows). Called from shard.cu right after sub_pdlp is constructed. void set_is_multi_gpu(bool v) { is_multi_gpu_ = v; } bool is_multi_gpu() const { return is_multi_gpu_; } + multi_gpu_engine_t* get_mgpu_engine() const { return mgpu_engine_; } i_t total_pdhg_iterations_; From 57c70615337bd12fe803938d5f1bc44c4d9fa7f1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 12:25:25 +0200 Subject: [PATCH 031/258] added non const convergence information getter --- cpp/src/pdlp/termination_strategy/termination_strategy.cu | 7 +++++++ cpp/src/pdlp/termination_strategy/termination_strategy.hpp | 1 + 2 files changed, 8 insertions(+) diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index d1a88799d6..0320b420a8 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -195,6 +195,13 @@ pdlp_termination_strategy_t::get_convergence_information() const return convergence_information_; } +template +convergence_information_t& +pdlp_termination_strategy_t::get_convergence_information() +{ + return convergence_information_; +} + template const infeasibility_information_t& pdlp_termination_strategy_t::get_infeasibility_information() const diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.hpp b/cpp/src/pdlp/termination_strategy/termination_strategy.hpp index 5cd43d7be7..63b2e81ff4 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.hpp +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.hpp @@ -187,6 +187,7 @@ class pdlp_termination_strategy_t { i_t get_optimal_solution_id() const; const convergence_information_t& get_convergence_information() const; + convergence_information_t& get_convergence_information(); const infeasibility_information_t& get_infeasibility_information() const; // Deep copy is used when save best primal so far is toggled From 9f78d0534c232055b7da4e425379e9f86a436e08 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 14:29:36 +0200 Subject: [PATCH 032/258] compute_convergence_information is now on multi-gpu --- .../distributed_pdlp/multi_gpu_engine.hpp | 58 ++++ cpp/src/pdlp/pdlp.cu | 8 +- .../convergence_information.cu | 296 ++++++++++++++++-- .../convergence_information.hpp | 31 ++ 4 files changed, 360 insertions(+), 33 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 001f9b760e..438a878834 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -7,10 +7,12 @@ #include #include #include +#include #include #include +#include #include #include @@ -27,6 +29,16 @@ namespace cuopt::linear_programming::detail { +// Element-wise sqrt functor. Defined at namespace scope (not as a local +// extended HD lambda) because nvcc disallows extended __host__ __device__ +// lambdas appearing inside templates whose template arguments are +// themselves local lambda types (which happens when distributed_l2_norm is +// invoked with closure accessors). +template +struct sqrt_inplace_op_t { + __host__ __device__ f_t operator()(f_t x) const { return raft::sqrt(x); } +}; + template struct multi_gpu_engine_t { // Constructs shards from rank_data @@ -219,6 +231,52 @@ struct multi_gpu_engine_t { ncclGroupEnd(); } + // -------- Distributed L2 norm ------------------------------------------ + // Computes sqrt(Σ_k Σ_{i ∈ owned_k} buf_k[i]²) and writes the scalar into + // the buffer returned by `out_access` on EVERY shard. + // + // Algorithm: + // 1) per shard: out = cublasdot(buf[0:n_owned], buf[0:n_owned]) (partial Σ²) + // 2) NCCL allreduce SUM on out (count = 1) (global Σ²) + // 3) per shard: out = sqrt(out) + // + // The caller is responsible for clipping correctness via `size_access` + // (which picks `rank_data.owned_var_size` or `rank_data.owned_cstr_size` + // depending on the shape of the input buffer), and for mirroring the + // result back to master if downstream code needs it there. + // + // BufAccess : pdlp_solver_t& -> rmm::device_uvector& + // OutAccess : pdlp_solver_t& -> f_t* (single scalar in shard memory) + // SizeAccess : pdlp_shard_t& -> i_t (owned slice length) + template + void distributed_l2_norm(BufAccess&& buf_access, + OutAccess&& out_access, + SizeAccess&& size_access) + { + for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + auto& buf = buf_access(sub); + const i_t n = size_access(shard); + f_t* out = out_access(sub); + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(shard.handle.get_cublas_handle(), + static_cast(n), + buf.data(), + 1, + buf.data(), + 1, + out, + shard.stream.view().value())); + }); + + allreduce_sum_inplace(out_access, /*count=*/1); + + for_each_shard([&](auto& shard) { + f_t* out = out_access(*shard.sub_pdlp); + cub::DeviceTransform::Transform( + out, out, 1, sqrt_inplace_op_t{}, shard.stream.view().value()); + }); + } + // -------- Generic distributed SpMVs ------------------------------------- // distributed_spmv_A : halo-update the var-shaped input buffer returned by // `in_buf(pdhg)`, then per-shard A @ in_buf -> out_desc. diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index e2aeb3f08c..9522ae4065 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2289,15 +2289,13 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte } multi_gpu_engine->allreduce_sum_inplace( - [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }, 1); + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }); multi_gpu_engine->allreduce_sum_inplace( [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); - }, - 1); + }); multi_gpu_engine->allreduce_sum_inplace( - [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }, - 1); + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }); auto& s0 = *multi_gpu_engine->shards[0]; { diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index a6d6d14d96..28b33582ab 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -5,12 +5,16 @@ */ /* clang-format on */ +#include +#include #include #include #include #include #include +#include + #include #include @@ -416,17 +420,89 @@ void convergence_information_t::compute_convergence_information( print("dual_slack", dual_slack); #endif + if (current_pdhg_solver.is_multi_gpu()) + { + auto* engine = current_pdhg_solver.get_mgpu_engine(); + cuopt_assert(engine != nullptr, + "mGPU branch reached but current_pdhg_solver has no engine (shard pdhg?)"); + cuopt_expects(!settings.per_constraint_residual, + error_type_t::ValidationError, + "per_constraint_residual is not yet supported in multi-GPU mode"); + + // Prepares halo values in primal_solution + engine->halo_exchange_var( + [](pdhg_solver_t& pdhg) -> rmm::device_uvector& { + return pdhg.get_primal_solution(); + }); + + // Compute the primal residual and objective on each shard + for (auto& shard : engine->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdlp = *shard->sub_pdlp; + auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); + sub_conv.compute_primal_residual(sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_dual_tmp_resource(), + sub_pdlp.pdhg_solver_.get_dual_solution()); + sub_conv.compute_primal_objective_owned_partial(sub_pdlp.pdhg_solver_.get_primal_solution(), + shard->rank_data.owned_var_size); + } + + // Reduce all primal objectives across shards + cuopt_assert(!batch_mode_, "multi-GPU PDLP is not supported in batch mode"); + engine->allreduce_sum_inplace( + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .get_primal_objective() + .data(); + }); + + // Get the reduced primal objective from the shard[0] (arbitrary) + { + auto& s0 = *engine->shards[0]; + raft::device_setter guard(s0.device_id); + auto& s0_conv = + s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); + raft::copy(primal_objective_.data(), s0_conv.get_primal_objective().data(), 1, stream_view_); + } + apply_primal_objective_scaling_and_offset(); + } + else { compute_primal_residual( op_problem_cusparse_view_, current_pdhg_solver.get_dual_tmp_resource(), dual_iterate); - compute_primal_objective(primal_iterate); + compute_primal_objective(primal_iterate);} #ifdef CUPDLP_DEBUG_MODE print("Primal Residual", primal_residual_); #endif - if (!batch_mode_) + // L2 Norm + if (current_pdhg_solver.is_multi_gpu()) { + auto* engine = current_pdhg_solver.get_mgpu_engine(); + engine->distributed_l2_norm( + [](pdlp_solver_t& sp) -> rmm::device_uvector& { + return sp.get_current_termination_strategy() + .get_convergence_information() + .primal_residual_; + }, + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_primal_residual_.data(); + }, + [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_cstr_size; }); + + auto& s0 = *engine->shards[0]; + raft::device_setter guard(s0.device_id); + raft::copy(l2_primal_residual_.data(), + s0.sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .l2_primal_residual_.data(), + 1, + stream_view_); + } else if (!batch_mode_) { my_l2_norm(primal_residual_, l2_primal_residual_, handle_ptr_); - else { + } else { segmented_sum_handler_.segmented_sum_helper( thrust::make_transform_iterator(primal_residual_.data(), power_two_func_t{}), l2_primal_residual_.data(), @@ -444,6 +520,7 @@ void convergence_information_t::compute_convergence_information( print("Absolute Primal Residual", l2_primal_residual_); #endif // If per_constraint_residual is false we still need to perform the l2 since it's used in kkt + // Not suported in mGPU if (settings.per_constraint_residual) { // Compute the linf of (residual_i - rel * b_i) if (settings.save_best_primal_so_far) { @@ -466,19 +543,98 @@ void convergence_information_t::compute_convergence_information( std::numeric_limits::lowest()); } - compute_dual_residual(op_problem_cusparse_view_, - current_pdhg_solver.get_primal_tmp_resource(), - primal_iterate, - dual_slack); - compute_dual_objective(dual_iterate, primal_iterate, dual_slack); + if (current_pdhg_solver.is_multi_gpu()) { + auto* engine = current_pdhg_solver.get_mgpu_engine(); + + // 1) Halo-exchange the dual solution on every shard so the upcoming + // A_T_shard @ dual SpMV inside compute_dual_residual reads correct + // values in the cstr halo region. + engine->halo_exchange_cstr( + [](pdhg_solver_t& pdhg) -> rmm::device_uvector& { + return pdhg.get_dual_solution(); + }); + + // 2-3) Per-shard: + // - compute_dual_residual: shard.dual_residual_ has owned-var entries + // correct, halo var entries garbage (their A_T row isn't on this + // shard). + // - compute_dual_objective_owned_partial: writes a *partial* + // dot(slack[0:nv], x[0:nv]) + Σ primal_slack[0:nc] into + // shard.dual_objective_, with NO scaling/offset. Relies on + // primal_slack_ already populated by the per-shard + // compute_primal_residual above. + for (auto& shard : engine->shards) { + raft::device_setter guard(shard->device_id); + auto& sub_pdlp = *shard->sub_pdlp; + auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); + sub_conv.compute_dual_residual(sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), + sub_pdlp.pdhg_solver_.get_primal_solution(), + sub_pdlp.pdhg_solver_.get_dual_slack()); + sub_conv.compute_dual_objective_owned_partial(sub_pdlp.pdhg_solver_.get_primal_solution(), + sub_pdlp.pdhg_solver_.get_dual_slack(), + shard->rank_data.owned_var_size, + shard->rank_data.owned_cstr_size); + } + + // 4) Allreduce dual_objective_ across shards (sum, in place). Same + // offset/scaling-after-allreduce reasoning as primal: applying offset + // per-shard would over-count it Nshards times. + engine->allreduce_sum_inplace( + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .get_dual_objective() + .data(); + }); + + { + auto& s0 = *engine->shards[0]; + raft::device_setter guard(s0.device_id); + auto& s0_conv = + s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); + raft::copy(dual_objective_.data(), s0_conv.get_dual_objective().data(), 1, stream_view_); + } + apply_dual_objective_scaling_and_offset(); + } else { + compute_dual_residual(op_problem_cusparse_view_, + current_pdhg_solver.get_primal_tmp_resource(), + primal_iterate, + dual_slack); + compute_dual_objective(dual_iterate, primal_iterate, dual_slack); + } #ifdef CUPDLP_DEBUG_MODE print("Dual Residual", dual_residual_); #endif - if (!batch_mode_) + if (current_pdhg_solver.is_multi_gpu()) { + // Multi-GPU dual residual L2 norm: same pattern as the primal L2 above, + // but the dual residual is var-shaped so we clip to owned_var_size. + auto* engine = current_pdhg_solver.get_mgpu_engine(); + engine->distributed_l2_norm( + [](pdlp_solver_t& sp) -> rmm::device_uvector& { + return sp.get_current_termination_strategy() + .get_convergence_information() + .dual_residual_; + }, + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_dual_residual_.data(); + }, + [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_var_size; }); + auto& s0 = *engine->shards[0]; + raft::device_setter guard(s0.device_id); + raft::copy(l2_dual_residual_.data(), + s0.sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .l2_dual_residual_.data(), + 1, + stream_view_); + } else if (!batch_mode_) { my_l2_norm(dual_residual_, l2_dual_residual_, handle_ptr_); - else { + } else { segmented_sum_handler_.segmented_sum_helper( thrust::make_transform_iterator(dual_residual_.data(), power_two_func_t{}), l2_dual_residual_.data(), @@ -509,6 +665,7 @@ void convergence_information_t::compute_convergence_information( std::numeric_limits::lowest()); } + // In mGPU, full primal_objective and dual_objective already mirrored to master so no special behaviour const auto [grid_size, block_size] = kernel_config_from_batch_size(climber_strategies_.size()); compute_remaining_stats_kernel <<>>(this->view(), climber_strategies_.size()); @@ -615,6 +772,24 @@ __global__ void apply_objective_scaling_and_offset(raft::device_span object objective[idx] = objective_scaling_factor * (objective[idx] + objective_offsets[idx]); } +template +void convergence_information_t::compute_primal_objective_owned_partial( + rmm::device_uvector& primal_solution, i_t n_owned) +{ + raft::common::nvtx::range fun_scope("compute_primal_objective_owned_partial"); + cuopt_assert(!batch_mode_, "owned-partial primal objective is only used in non-batch mGPU mode"); + cuopt_assert(n_owned <= primal_size_h_, + "n_owned must be <= primal_size_h_ (owned slice is a prefix)"); + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(handle_ptr_->get_cublas_handle(), + static_cast(n_owned), + primal_solution.data(), + primal_stride, + problem_ptr->objective_coefficients.data(), + primal_stride, + primal_objective_.data(), + stream_view_)); +} + template void convergence_information_t::compute_primal_objective( rmm::device_uvector& primal_solution) @@ -643,21 +818,25 @@ void convergence_information_t::compute_primal_objective( // Apply per-climber objective scaling and offset. objective_offsets_ is always populated // (defaults to the scalar problem offset replicated, or user-specified per-climber offsets). - { - const auto [grid_size, block_size] = kernel_config_from_batch_size(climber_strategies_.size()); - apply_objective_scaling_and_offset<<>>( - make_span(primal_objective_), - problem_ptr->presolve_data.objective_scaling_factor, - make_span(objective_offsets_), - climber_strategies_.size()); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - } + apply_primal_objective_scaling_and_offset(); #ifdef CUPDLP_DEBUG_MODE print("Primal objective", primal_objective_); #endif } +template +void convergence_information_t::apply_primal_objective_scaling_and_offset() +{ + const auto [grid_size, block_size] = kernel_config_from_batch_size(climber_strategies_.size()); + apply_objective_scaling_and_offset<<>>( + make_span(primal_objective_), + problem_ptr->presolve_data.objective_scaling_factor, + make_span(objective_offsets_), + climber_strategies_.size()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + template void convergence_information_t::compute_dual_residual( cusparse_view_t& cusparse_view, @@ -740,6 +919,51 @@ void convergence_information_t::compute_dual_residual( } } +template +void convergence_information_t::compute_dual_objective_owned_partial( + rmm::device_uvector& primal_solution, + rmm::device_uvector& dual_slack, + i_t n_owned_var, + i_t n_owned_cstr) +{ + raft::common::nvtx::range fun_scope("compute_dual_objective_owned_partial"); + cuopt_assert(!batch_mode_, "owned-partial dual objective is only used in non-batch mGPU mode"); + cuopt_assert(hyper_params_.use_reflected_primal_dual, + "owned-partial dual objective requires use_reflected_primal_dual"); + cuopt_assert(n_owned_var <= primal_size_h_, + "n_owned_var must be <= primal_size_h_ (owned slice is a prefix)"); + cuopt_assert(n_owned_cstr <= dual_size_h_, + "n_owned_cstr must be <= dual_size_h_ (owned slice is a prefix)"); + + // dual_dot_ = dot(dual_slack[0:n_owned_var], primal_solution[0:n_owned_var]) + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(handle_ptr_->get_cublas_handle(), + static_cast(n_owned_var), + dual_slack.data(), + primal_stride, + primal_solution.data(), + primal_stride, + dual_dot_.data(), + stream_view_)); + + // sum_primal_slack_ = Σ primal_slack_[0:n_owned_cstr] + // primal_slack_ is assumed populated for owned cstrs by a prior + // compute_primal_residual call on this same shard. + cub::DeviceReduce::Sum(rmm_tmp_buffer_.data(), + size_of_buffer_, + primal_slack_.data(), + sum_primal_slack_.data(), + static_cast(n_owned_cstr), + stream_view_); + + // dual_objective_ = dual_dot_ + sum_primal_slack_ (still a partial sum). + cub::DeviceTransform::Transform( + cuda::std::make_tuple(dual_dot_.data(), sum_primal_slack_.data()), + dual_objective_.data(), + 1, + cuda::std::plus<>{}, + stream_view_); +} + template void convergence_information_t::compute_dual_objective( rmm::device_uvector& dual_solution, @@ -821,21 +1045,25 @@ void convergence_information_t::compute_dual_objective( } // Apply per-climber objective scaling and offset. - { - const auto [grid_size, block_size] = kernel_config_from_batch_size(climber_strategies_.size()); - apply_objective_scaling_and_offset<<>>( - make_span(dual_objective_), - problem_ptr->presolve_data.objective_scaling_factor, - make_span(objective_offsets_), - climber_strategies_.size()); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - } + apply_dual_objective_scaling_and_offset(); #ifdef CUPDLP_DEBUG_MODE print("Dual objective", dual_objective_); #endif } +template +void convergence_information_t::apply_dual_objective_scaling_and_offset() +{ + const auto [grid_size, block_size] = kernel_config_from_batch_size(climber_strategies_.size()); + apply_objective_scaling_and_offset<<>>( + make_span(dual_objective_), + problem_ptr->presolve_data.objective_scaling_factor, + make_span(objective_offsets_), + climber_strategies_.size()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + template void convergence_information_t::compute_reduced_cost_from_primal_gradient( const rmm::device_uvector& primal_gradient, const rmm::device_uvector& primal_solution) @@ -916,12 +1144,24 @@ const rmm::device_uvector& convergence_information_t::get_primal_ return primal_objective_; } +template +rmm::device_uvector& convergence_information_t::get_primal_objective() +{ + return primal_objective_; +} + template const rmm::device_uvector& convergence_information_t::get_dual_objective() const { return dual_objective_; } +template +rmm::device_uvector& convergence_information_t::get_dual_objective() +{ + return dual_objective_; +} + template const rmm::device_uvector& convergence_information_t::get_l2_dual_residual() const { diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.hpp b/cpp/src/pdlp/termination_strategy/convergence_information.hpp index 2389a60fae..6325622a2b 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.hpp +++ b/cpp/src/pdlp/termination_strategy/convergence_information.hpp @@ -52,7 +52,10 @@ class convergence_information_t { // Needed for kkt restart & debug prints const rmm::device_uvector& get_primal_objective() const; + // Non-const overload used by the multi-GPU branch to mirror / allreduce. + rmm::device_uvector& get_primal_objective(); const rmm::device_uvector& get_dual_objective() const; + rmm::device_uvector& get_dual_objective(); const rmm::device_uvector& get_l2_primal_residual() const; const rmm::device_uvector& get_l2_dual_residual() const; const rmm::device_uvector& get_relative_linf_primal_residual() const; @@ -123,12 +126,40 @@ class convergence_information_t { rmm::device_uvector& tmp_dual, [[maybe_unused]] const rmm::device_uvector& dual_iterate); + // Multi-GPU shard helper: writes a partial dot(c[0:n_owned], x[0:n_owned]) + // into primal_objective_ (no scaling, no offset). Master is responsible for + // allreduce SUM across shards and then applying scaling + offset once on the + // reduced value. n_owned must be <= primal_size_h_; pass owned_var_size on + // each shard. + void compute_primal_objective_owned_partial(rmm::device_uvector& primal_solution, + i_t n_owned); + + // Multi-GPU shard helper: writes a partial dual objective into + // dual_objective_ (no scaling, no offset). Computes + // dual_dot_ = dot(dual_slack[0:n_owned_var], primal_solution[0:n_owned_var]) + // sum_primal_slack_ = Σ primal_slack_[0:n_owned_cstr] + // dual_objective_ = dual_dot_ + sum_primal_slack_ + // primal_slack_ is assumed already populated by a prior per-shard + // compute_primal_residual call. Use only in the use_reflected_primal_dual + // path (the multi-GPU mode). + void compute_dual_objective_owned_partial(rmm::device_uvector& primal_solution, + rmm::device_uvector& dual_slack, + i_t n_owned_var, + i_t n_owned_cstr); + void swap_context(const thrust::universal_host_pinned_vector>& swap_pairs); void resize_context(i_t new_size); private: void compute_primal_objective(rmm::device_uvector& primal_solution); + // Applies per-climber objective scaling + offset to primal_objective_. + // Single-GPU path: called from compute_primal_objective right after the dot. + // Multi-GPU path: called on master once after allreduce of partial sums. + void apply_primal_objective_scaling_and_offset(); + // Same as above but for dual_objective_. + void apply_dual_objective_scaling_and_offset(); + void compute_dual_residual(cusparse_view_t& cusparse_view, rmm::device_uvector& tmp_primal, rmm::device_uvector& primal_solution, From c484485d9debf9d5a1d7246dcf34f7f919d5f344 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 16:14:44 +0200 Subject: [PATCH 033/258] fill_return_problem_solutionis now ready !! --- .../distributed_pdlp/multi_gpu_engine.hpp | 86 +++++++++++++++++++ cpp/src/pdlp/pdlp.cu | 9 ++ 2 files changed, 95 insertions(+) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 438a878834..e04f2e26eb 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -17,7 +17,9 @@ #include #include +#include #include +#include #include #include @@ -325,6 +327,90 @@ struct multi_gpu_engine_t { [](auto& pdhg) -> cusparseDnVecDescr_t { return pdhg.get_cusparse_view().current_AtY; }); } + // -------- Solution gather (shards -> master) ---------------------------- + // Assembles the global potential_next primal/dual solutions on the master + // pdhg_solver_ from the owned slices distributed across shards. Each shard's + // first owned_var_size (resp. owned_cstr_size) entries of its + // potential_next_primal_solution_ (resp. _dual_) are the live, up-to-date + // owned values; the master pdhg_solver_'s buffers are not updated during + // iterations and would otherwise return stale data. + // + // Used right before fill_return_problem_solution() at the return sites in + // pdlp_solver_t::check_termination() and pdlp_solver_t::check_limits(): the + // user-visible solution must contain gathered global values. + // + // Mirrors the metis_tests engine::get_x_output / get_y_output pattern: + // per shard: alloc small host tmp, copy owned slice device->host, sync, + // host-scatter via rank_data.local_to_global_{var,cstr} into a contiguous + // host buffer. Then one host->device copy into the master pdhg buffer. + void gather_potential_next_solutions_to_master(pdhg_solver_t& master_pdhg) + { + const std::size_t total_vars = + master_pdhg.get_potential_next_primal_solution().size(); + const std::size_t total_cstrs = + master_pdhg.get_potential_next_dual_solution().size(); + + std::vector h_primal(total_vars); + std::vector h_dual(total_cstrs); + + for (auto& s_uptr : shards) { + auto& s = *s_uptr; + raft::device_setter guard(s.device_id); + const i_t nv = s.rank_data.owned_var_size; + const i_t nc = s.rank_data.owned_cstr_size; + + std::vector tmp_primal(nv); + std::vector tmp_dual(nc); + + if (nv > 0) { + RAFT_CUDA_TRY( + cudaMemcpyAsync(tmp_primal.data(), + s.sub_pdlp->pdhg_solver_.get_potential_next_primal_solution().data(), + static_cast(nv) * sizeof(f_t), + cudaMemcpyDeviceToHost, + s.stream.view().value())); + } + if (nc > 0) { + RAFT_CUDA_TRY( + cudaMemcpyAsync(tmp_dual.data(), + s.sub_pdlp->pdhg_solver_.get_potential_next_dual_solution().data(), + static_cast(nc) * sizeof(f_t), + cudaMemcpyDeviceToHost, + s.stream.view().value())); + } + RAFT_CUDA_TRY(cudaStreamSynchronize(s.stream.view().value())); + + if (nv > 0) { + thrust::scatter(thrust::host, + tmp_primal.begin(), + tmp_primal.end(), + s.rank_data.local_to_global_var.begin(), + h_primal.begin()); + } + if (nc > 0) { + thrust::scatter(thrust::host, + tmp_dual.begin(), + tmp_dual.end(), + s.rank_data.local_to_global_cstr.begin(), + h_dual.begin()); + } + } + + // Host -> master device. engine.stream lives on the master device + // (created at engine construction when master device was current). + RAFT_CUDA_TRY(cudaMemcpyAsync(master_pdhg.get_potential_next_primal_solution().data(), + h_primal.data(), + total_vars * sizeof(f_t), + cudaMemcpyHostToDevice, + stream.view().value())); + RAFT_CUDA_TRY(cudaMemcpyAsync(master_pdhg.get_potential_next_dual_solution().data(), + h_dual.data(), + total_cstrs * sizeof(f_t), + cudaMemcpyHostToDevice, + stream.view().value())); + RAFT_CUDA_TRY(cudaStreamSynchronize(stream.view().value())); + } + // Engine-level stream for fork/join orchestration (master side). rmm::cuda_stream stream; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 9522ae4065..e9cf194d98 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -661,6 +661,9 @@ std::optional> pdlp_solver_t RAFT_CUDA_TRY(cudaDeviceSynchronize()); std::cout << "Time Limit reached, returning current solution" << std::endl; #endif + if (auto* engine = pdhg_solver_.get_mgpu_engine()) { + engine->gather_potential_next_solutions_to_master(pdhg_solver_); + } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, pdhg_solver_, @@ -694,6 +697,9 @@ std::optional> pdlp_solver_t return finalize_batch_return_with_limit_reached(pdlp_termination_status_t::IterationLimit); } + if (auto* engine = pdhg_solver_.get_mgpu_engine()) { + engine->gather_potential_next_solutions_to_master(pdhg_solver_); + } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, pdhg_solver_, @@ -1371,6 +1377,9 @@ std::optional> pdlp_solver_t #endif print_final_termination_criteria( timer, current_termination_strategy_.get_convergence_information(), termination_current); + if (auto* engine = pdhg_solver_.get_mgpu_engine()) { + engine->gather_potential_next_solutions_to_master(pdhg_solver_); + } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, pdhg_solver_, From fc46080d24d7566729a1a505837cfc41e023997d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 26 May 2026 16:39:26 +0200 Subject: [PATCH 034/258] added reduced cost in gathering of solution, builds and runs --- .../distributed_pdlp/multi_gpu_engine.hpp | 47 +++++++++++++++---- cpp/src/pdlp/pdlp.cu | 12 +++-- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index e04f2e26eb..d156e889af 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -328,22 +328,30 @@ struct multi_gpu_engine_t { } // -------- Solution gather (shards -> master) ---------------------------- - // Assembles the global potential_next primal/dual solutions on the master - // pdhg_solver_ from the owned slices distributed across shards. Each shard's - // first owned_var_size (resp. owned_cstr_size) entries of its - // potential_next_primal_solution_ (resp. _dual_) are the live, up-to-date - // owned values; the master pdhg_solver_'s buffers are not updated during - // iterations and would otherwise return stale data. + // Assembles the global potential_next primal/dual solutions and the + // reduced_cost on the master from the owned slices distributed across + // shards. Each shard's first owned_var_size (resp. owned_cstr_size) entries + // of its potential_next_primal_solution_ / reduced_cost_ (resp. + // potential_next_dual_solution_) are the live, up-to-date owned values; the + // master buffers are not updated during iterations and would otherwise + // return stale data. // // Used right before fill_return_problem_solution() at the return sites in // pdlp_solver_t::check_termination() and pdlp_solver_t::check_limits(): the - // user-visible solution must contain gathered global values. + // user-visible solution must contain gathered global values for primal, + // dual, and reduced_cost. // // Mirrors the metis_tests engine::get_x_output / get_y_output pattern: // per shard: alloc small host tmp, copy owned slice device->host, sync, // host-scatter via rank_data.local_to_global_{var,cstr} into a contiguous - // host buffer. Then one host->device copy into the master pdhg buffer. - void gather_potential_next_solutions_to_master(pdhg_solver_t& master_pdhg) + // host buffer. Then one host->device copy into the master buffer per field. + // + // master_pdhg : provides destinations for primal / dual. + // master_reduced_cost : destination for the reduced_cost (var-shaped, lives + // in the master pdlp_solver_t's termination strategy + // convergence_information_). + void gather_potential_next_solutions_to_master( + pdhg_solver_t& master_pdhg, rmm::device_uvector& master_reduced_cost) { const std::size_t total_vars = master_pdhg.get_potential_next_primal_solution().size(); @@ -352,6 +360,7 @@ struct multi_gpu_engine_t { std::vector h_primal(total_vars); std::vector h_dual(total_cstrs); + std::vector h_reduced_cost(total_vars); for (auto& s_uptr : shards) { auto& s = *s_uptr; @@ -361,6 +370,11 @@ struct multi_gpu_engine_t { std::vector tmp_primal(nv); std::vector tmp_dual(nc); + std::vector tmp_reduced_cost(nv); + + auto& sub_reduced_cost = s.sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .get_reduced_cost(); if (nv > 0) { RAFT_CUDA_TRY( @@ -369,6 +383,11 @@ struct multi_gpu_engine_t { static_cast(nv) * sizeof(f_t), cudaMemcpyDeviceToHost, s.stream.view().value())); + RAFT_CUDA_TRY(cudaMemcpyAsync(tmp_reduced_cost.data(), + sub_reduced_cost.data(), + static_cast(nv) * sizeof(f_t), + cudaMemcpyDeviceToHost, + s.stream.view().value())); } if (nc > 0) { RAFT_CUDA_TRY( @@ -386,6 +405,11 @@ struct multi_gpu_engine_t { tmp_primal.end(), s.rank_data.local_to_global_var.begin(), h_primal.begin()); + thrust::scatter(thrust::host, + tmp_reduced_cost.begin(), + tmp_reduced_cost.end(), + s.rank_data.local_to_global_var.begin(), + h_reduced_cost.begin()); } if (nc > 0) { thrust::scatter(thrust::host, @@ -408,6 +432,11 @@ struct multi_gpu_engine_t { total_cstrs * sizeof(f_t), cudaMemcpyHostToDevice, stream.view().value())); + RAFT_CUDA_TRY(cudaMemcpyAsync(master_reduced_cost.data(), + h_reduced_cost.data(), + total_vars * sizeof(f_t), + cudaMemcpyHostToDevice, + stream.view().value())); RAFT_CUDA_TRY(cudaStreamSynchronize(stream.view().value())); } diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index e9cf194d98..7bd6d34473 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -662,7 +662,9 @@ std::optional> pdlp_solver_t std::cout << "Time Limit reached, returning current solution" << std::endl; #endif if (auto* engine = pdhg_solver_.get_mgpu_engine()) { - engine->gather_potential_next_solutions_to_master(pdhg_solver_); + engine->gather_potential_next_solutions_to_master( + pdhg_solver_, + current_termination_strategy_.get_convergence_information().get_reduced_cost()); } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, @@ -698,7 +700,9 @@ std::optional> pdlp_solver_t } if (auto* engine = pdhg_solver_.get_mgpu_engine()) { - engine->gather_potential_next_solutions_to_master(pdhg_solver_); + engine->gather_potential_next_solutions_to_master( + pdhg_solver_, + current_termination_strategy_.get_convergence_information().get_reduced_cost()); } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, @@ -1378,7 +1382,9 @@ std::optional> pdlp_solver_t print_final_termination_criteria( timer, current_termination_strategy_.get_convergence_information(), termination_current); if (auto* engine = pdhg_solver_.get_mgpu_engine()) { - engine->gather_potential_next_solutions_to_master(pdhg_solver_); + engine->gather_potential_next_solutions_to_master( + pdhg_solver_, + current_termination_strategy_.get_convergence_information().get_reduced_cost()); } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, From 6538382755ed20571b649e0366afbebd95493053 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 27 May 2026 13:41:27 +0200 Subject: [PATCH 035/258] updated mgpu scale/unscale logic --- cpp/src/pdlp/pdlp.cu | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 7bd6d34473..c31c528d8d 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2962,22 +2962,24 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co unscaled_dual_avg_solution_); } if (settings_.hyper_params.use_adaptive_step_size_strategy) { + initial_scaling_strategy_.unscale_solutions(pdhg_solver_.get_primal_solution(), + pdhg_solver_.get_dual_solution()); + } else { if (multi_gpu_engine) { - // The only branch in cuPDLPx + // The only branch in cuPDLPx (Stable3) multi_gpu_engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; sub.get_initial_scaling_strategy().unscale_solutions( - sub.pdhg_solver_.get_primal_solution(), sub.pdhg_solver_.get_dual_solution()); + sub.pdhg_solver_.get_potential_next_primal_solution(), + sub.pdhg_solver_.get_potential_next_dual_solution(), + sub.pdhg_solver_.get_dual_slack()); }); } else { - initial_scaling_strategy_.unscale_solutions(pdhg_solver_.get_primal_solution(), - pdhg_solver_.get_dual_solution()); + initial_scaling_strategy_.unscale_solutions( + pdhg_solver_.get_potential_next_primal_solution(), + pdhg_solver_.get_potential_next_dual_solution(), + pdhg_solver_.get_dual_slack()); } - } else { - initial_scaling_strategy_.unscale_solutions( - pdhg_solver_.get_potential_next_primal_solution(), - pdhg_solver_.get_potential_next_dual_solution(), - pdhg_solver_.get_dual_slack()); } #ifdef CUPDLP_DEBUG_MODE @@ -3006,22 +3008,24 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co unscaled_dual_avg_solution_); } if (settings_.hyper_params.use_adaptive_step_size_strategy) { + initial_scaling_strategy_.scale_solutions(pdhg_solver_.get_primal_solution(), + pdhg_solver_.get_dual_solution()); + } else { if (multi_gpu_engine) { - // The only branch in cuPDLPx + // The only branch in cuPDLPx (Stable3) multi_gpu_engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; sub.get_initial_scaling_strategy().scale_solutions( - sub.pdhg_solver_.get_primal_solution(), sub.pdhg_solver_.get_dual_solution()); + sub.pdhg_solver_.get_potential_next_primal_solution(), + sub.pdhg_solver_.get_potential_next_dual_solution(), + sub.pdhg_solver_.get_dual_slack()); }); } else { - initial_scaling_strategy_.scale_solutions(pdhg_solver_.get_primal_solution(), - pdhg_solver_.get_dual_solution()); + initial_scaling_strategy_.scale_solutions( + pdhg_solver_.get_potential_next_primal_solution(), + pdhg_solver_.get_potential_next_dual_solution(), + pdhg_solver_.get_dual_slack()); } - } else { - initial_scaling_strategy_.scale_solutions( - pdhg_solver_.get_potential_next_primal_solution(), - pdhg_solver_.get_potential_next_dual_solution(), - pdhg_solver_.get_dual_slack()); } } From a88285a9f8d4ed8ec568307d0fac1866ed404831 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 27 May 2026 14:09:56 +0200 Subject: [PATCH 036/258] wired mgpu restart --- cpp/src/pdlp/pdlp.cuh | 1 + .../restart_strategy/pdlp_restart_strategy.cu | 147 ++++++++++++++---- 2 files changed, 118 insertions(+), 30 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 63aef7b43a..17fb05080f 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -110,6 +110,7 @@ class pdlp_solver_t { { return initial_scaling_strategy_; } + detail::pdlp_restart_strategy_t& get_restart_strategy() { return restart_strategy_; } // Per-shard primal/dual step sizes are private state on pdlp_solver_t but // are needed inside the multi-GPU dispatch paths that fan out a master cub diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index 17c7abcac5..00c5b16c8b 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include #include @@ -892,20 +894,64 @@ void pdlp_restart_strategy_t::cupdlpx_restart( "If any, all should be true"); // Computing the deltas - distance_squared_moved_from_last_restart_period( - pdhg_solver.get_potential_next_primal_solution(), - last_restart_duality_gap_.primal_solution_, - pdhg_solver.get_primal_tmp_resource(), - primal_size_h_, - 1, - last_restart_duality_gap_.primal_distance_traveled_); - distance_squared_moved_from_last_restart_period( - pdhg_solver.get_potential_next_dual_solution(), - last_restart_duality_gap_.dual_solution_, - pdhg_solver.get_dual_tmp_resource(), - dual_size_h_, - 1, - last_restart_duality_gap_.dual_distance_traveled_); + if (auto* engine = pdhg_solver.get_mgpu_engine()) { + engine->for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + auto& sub_rest = sub.get_restart_strategy(); + sub_rest.distance_squared_moved_from_last_restart_period( + sub.pdhg_solver_.get_potential_next_primal_solution(), + sub_rest.last_restart_duality_gap_.primal_solution_, + sub.pdhg_solver_.get_primal_tmp_resource(), + shard.rank_data.owned_var_size, + 1, + sub_rest.last_restart_duality_gap_.primal_distance_traveled_); + sub_rest.distance_squared_moved_from_last_restart_period( + sub.pdhg_solver_.get_potential_next_dual_solution(), + sub_rest.last_restart_duality_gap_.dual_solution_, + sub.pdhg_solver_.get_dual_tmp_resource(), + shard.rank_data.owned_cstr_size, + 1, + sub_rest.last_restart_duality_gap_.dual_distance_traveled_); + }); + + engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_restart_strategy().last_restart_duality_gap_.primal_distance_traveled_.data(); + }); + engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(); + }); + + auto& s0 = *engine->shards[0]; + { + raft::device_setter guard(s0.device_id); + RAFT_CUDA_TRY(cudaStreamSynchronize(s0.stream.view().value())); + } + raft::copy(last_restart_duality_gap_.primal_distance_traveled_.data(), + s0.sub_pdlp->get_restart_strategy() + .last_restart_duality_gap_.primal_distance_traveled_.data(), + 1, + stream_view_); + raft::copy(last_restart_duality_gap_.dual_distance_traveled_.data(), + s0.sub_pdlp->get_restart_strategy() + .last_restart_duality_gap_.dual_distance_traveled_.data(), + 1, + stream_view_); + } else { + distance_squared_moved_from_last_restart_period( + pdhg_solver.get_potential_next_primal_solution(), + last_restart_duality_gap_.primal_solution_, + pdhg_solver.get_primal_tmp_resource(), + primal_size_h_, + 1, + last_restart_duality_gap_.primal_distance_traveled_); + distance_squared_moved_from_last_restart_period( + pdhg_solver.get_potential_next_dual_solution(), + last_restart_duality_gap_.dual_solution_, + pdhg_solver.get_dual_tmp_resource(), + dual_size_h_, + 1, + last_restart_duality_gap_.dual_distance_traveled_); + } auto view = make_cupdlpx_restart_view(last_restart_duality_gap_.primal_distance_traveled_, last_restart_duality_gap_.dual_distance_traveled_, @@ -958,24 +1004,58 @@ void pdlp_restart_strategy_t::cupdlpx_restart( best_primal_weight.set_element_async(0, best_primal_weight_value, stream_view_); } + // Broadcast the primal and dual step sizes to all shards + if (auto* engine = pdhg_solver.get_mgpu_engine()) { + RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); + engine->for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + raft::copy(sub.get_primal_step_size().data(), + primal_step_size.data(), 1, shard.stream.view()); + raft::copy(sub.get_dual_step_size().data(), + dual_step_size.data(), 1, shard.stream.view()); + }); + } // TODO later batch mode: remove if you have per climber restart - raft::copy(last_restart_duality_gap_.primal_solution_.data(), - pdhg_solver.get_potential_next_primal_solution().data(), - last_restart_duality_gap_.primal_solution_.size(), - stream_view_); - raft::copy(pdhg_solver.get_primal_solution().data(), - pdhg_solver.get_potential_next_primal_solution().data(), - last_restart_duality_gap_.primal_solution_.size(), - stream_view_); - raft::copy(last_restart_duality_gap_.dual_solution_.data(), - pdhg_solver.get_potential_next_dual_solution().data(), - last_restart_duality_gap_.dual_solution_.size(), - stream_view_); - raft::copy(pdhg_solver.get_dual_solution().data(), - pdhg_solver.get_potential_next_dual_solution().data(), - last_restart_duality_gap_.dual_solution_.size(), - stream_view_); + if (auto* engine = pdhg_solver.get_mgpu_engine()) { + engine->for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + auto& sub_rest = sub.get_restart_strategy(); + raft::copy(sub_rest.last_restart_duality_gap_.primal_solution_.data(), + sub.pdhg_solver_.get_potential_next_primal_solution().data(), + sub_rest.last_restart_duality_gap_.primal_solution_.size(), + shard.stream.view()); + raft::copy(sub.pdhg_solver_.get_primal_solution().data(), + sub.pdhg_solver_.get_potential_next_primal_solution().data(), + sub.pdhg_solver_.get_primal_solution().size(), + shard.stream.view()); + raft::copy(sub_rest.last_restart_duality_gap_.dual_solution_.data(), + sub.pdhg_solver_.get_potential_next_dual_solution().data(), + sub_rest.last_restart_duality_gap_.dual_solution_.size(), + shard.stream.view()); + raft::copy(sub.pdhg_solver_.get_dual_solution().data(), + sub.pdhg_solver_.get_potential_next_dual_solution().data(), + sub.pdhg_solver_.get_dual_solution().size(), + shard.stream.view()); + }); + } else { + raft::copy(last_restart_duality_gap_.primal_solution_.data(), + pdhg_solver.get_potential_next_primal_solution().data(), + last_restart_duality_gap_.primal_solution_.size(), + stream_view_); + raft::copy(pdhg_solver.get_primal_solution().data(), + pdhg_solver.get_potential_next_primal_solution().data(), + last_restart_duality_gap_.primal_solution_.size(), + stream_view_); + raft::copy(last_restart_duality_gap_.dual_solution_.data(), + pdhg_solver.get_potential_next_dual_solution().data(), + last_restart_duality_gap_.dual_solution_.size(), + stream_view_); + raft::copy(pdhg_solver.get_dual_solution().data(), + pdhg_solver.get_potential_next_dual_solution().data(), + last_restart_duality_gap_.dual_solution_.size(), + stream_view_); + } #ifdef CUPDLP_DEBUG_MODE print("New last_restart_duality_gap_.primal_solution_", @@ -990,6 +1070,13 @@ void pdlp_restart_strategy_t::cupdlpx_restart( weighted_average_solution_.iterations_since_last_restart_ = 0; last_trial_fixed_point_error_[i] = std::numeric_limits::infinity(); } + + if (auto* engine = pdhg_solver.get_mgpu_engine()) { + engine->for_each_shard([&](auto& shard) { + shard.sub_pdlp->get_restart_strategy().weighted_average_solution_.iterations_since_last_restart_ = + 0; + }); + } } template From b34c5f6b286add3911c85fe8f747f2b8f6ccc9c2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 27 May 2026 14:38:56 +0200 Subject: [PATCH 037/258] dummy version locally seems to work ????? --- .../cuopt/linear_programming/constants.h | 1 + cpp/src/math_optimization/solver_settings.cu | 1 + cpp/src/pdlp/pdlp.cu | 24 +++++++++++++------ cpp/src/pdlp/solve.cu | 4 +++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 39685251b6..26ef3653e0 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -84,6 +84,7 @@ #define CUOPT_NUM_CPU_THREADS "num_cpu_threads" #define CUOPT_NUM_GPUS "num_gpus" #define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" +#define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" #define CUOPT_PRESOLVE_FILE "presolve_file" #define CUOPT_RANDOM_SEED "random_seed" diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 6d7e7504e4..991b0d62c1 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -177,6 +177,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_DUAL_POSTSOLVE, &pdlp_settings.dual_postsolve, true}, {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, {CUOPT_MIP_PROBING, &mip_settings.probing, true}, + {CUOPT_USE_DISTRIBUTED_PDLP, &pdlp_settings.hyper_params.use_distributed_pdlp, false}, }; // String parameters string_parameters = { diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index c31c528d8d..1e76fa4251 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -382,16 +382,14 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, // (problem_ptr, op_problem_scaled_, pdhg_solver_, strategies, etc.). : pdlp_solver_t(op_problem, settings, false) { - cuopt_expects(num_gpus == settings.num_gpus && settings.num_gpus > 1, + if (num_gpus == 1) { + std::cout << "CAREFUL: num_gpus == 1, running dummy version" << std::endl; + } + cuopt_expects(num_gpus == settings.num_gpus /*&& settings.num_gpus > 1*/, error_type_t::ValidationError, "This constructor should only be used for distributed PDLP (num_gpus > 1)"); - // Distributed PDLP is currently double-only. The body is guarded with - // `if constexpr` so the float instantiation never references the - // multi_gpu_engine_t / partition_loader_t symbols - // (those are intentionally not instantiated in their .cu files), keeping - // the link clean. Trying to use distributed PDLP with f_t = float will - // throw at runtime instead. + // Distributed PDLP is currently double-only if constexpr (!std::is_same_v) { cuopt_expects(false, error_type_t::ValidationError, @@ -403,6 +401,18 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, if (!settings.multi_gpu_partition_file.empty()) { parts = partition_loader_t::parse_distributed_pdlp_partition_file( settings.multi_gpu_partition_file); + } else if (num_gpus == 1) { + // Single-part dummy run: useful for exercising the mGPU code paths on a + // single physical GPU without a real Metis partition file. The downstream + // create_rank_data_from_parts expects a flat vector of length + // (n_constraints + n_variables) where each entry is the owning part-id + // (cstrs first, then vars). With nb_parts == 1, every entry is 0. + std::cout << "CAREFUL: num_gpus == 1, running dummy version (single part covering " + << op_problem_scaled_.n_constraints << " cstrs + " + << op_problem_scaled_.n_variables << " vars)" << std::endl; + parts = std::vector( + static_cast(op_problem_scaled_.n_constraints + op_problem_scaled_.n_variables), + 0); } else { cuopt_expects(false, error_type_t::RuntimeError, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 479340810c..e401ab35b6 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -771,9 +771,11 @@ static optimization_problem_solution_t run_pdlp_solver( } #endif if (settings.hyper_params.use_distributed_pdlp) { + /* cuopt_expects(settings.num_gpus > 1, error_type_t::ValidationError, - "use_distributed_pdlp requires settings.num_gpus > 1"); + "use_distributed_pdlp requires settings.num_gpus > 1"); */ + if (settings.num_gpus == 1) {std::cout << "CAREFUL: use_distributed_pdlp requires settings.num_gpus > 1" << std::endl;} cuopt_expects(!is_batch_mode, error_type_t::ValidationError, "Distributed PDLP does not support batch mode"); From b784a441395c092f13a94b276b8caad03a7cac7e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 27 May 2026 06:12:59 -0700 Subject: [PATCH 038/258] added dummy partitionner --- cpp/src/pdlp/CMakeLists.txt | 1 + .../distributed_pdlp/metis_partitioner.hpp | 24 +++++ cpp/src/pdlp/distributed_pdlp/partitioner.cu | 87 +++++++++++++++++++ cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 63 ++++++++++++++ cpp/src/pdlp/pdlp.cu | 39 +++++---- 5 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp create mode 100644 cpp/src/pdlp/distributed_pdlp/partitioner.cu create mode 100644 cpp/src/pdlp/distributed_pdlp/partitioner.hpp diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 2bc2771c91..a6ef14e3ff 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -32,6 +32,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/shard.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/multi_gpu_engine.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cu ) # C and Python adapter files diff --git a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp new file mode 100644 index 0000000000..c4e37f57a9 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace cuopt::linear_programming::detail { + +// METIS k-way partitioner on the constraint/variable bipartite graph induced by A. +// Requires partitioner_input_t::A and A_t (or A row_offsets/col_indices only — the +// implementation builds the bipartite adjacency the same way as metis_tests: +// cstr nodes [0, nb_cstr), var nodes [nb_cstr, nb_cstr+nb_vars), edges from A and A_t). +// +// Wire into make_partitioner() once METIS is an optional cuOpt dependency. +template +class metis_partitioner_t : public partitioner_i { + public: + std::vector partition(partitioner_input_t const& input) const override; +}; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cu b/cpp/src/pdlp/distributed_pdlp/partitioner.cu new file mode 100644 index 0000000000..bdbfcacf06 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cu @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include +#include + +namespace cuopt::linear_programming::detail { + +template +std::vector dummy_partitioner_t::partition( + partitioner_input_t const& input) const +{ + cuopt_expects(input.nb_parts > 0, + error_type_t::ValidationError, + "dummy_partitioner: nb_parts must be positive"); + cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, + error_type_t::ValidationError, + "dummy_partitioner: invalid problem dimensions"); + + const std::size_t nvtx = + static_cast(input.nb_cstr) + static_cast(input.nb_vars); + std::vector parts(nvtx); + for (std::size_t i = 0; i < nvtx; ++i) { + parts[i] = static_cast(i % static_cast(input.nb_parts)); + } + validate_partition(parts, + static_cast(input.nb_cstr), + static_cast(input.nb_vars), + static_cast(input.nb_parts), + "dummy_partitioner"); + return parts; +} + +void validate_partition(std::vector const& parts, + int nb_cstr, + int nb_vars, + int nb_parts, + char const* context) +{ + const std::size_t expected = + static_cast(nb_cstr) + static_cast(nb_vars); + cuopt_expects(parts.size() == expected, + error_type_t::ValidationError, + "%s: expected %zu part entries (cstrs + vars), got %zu", + context, + expected, + parts.size()); + cuopt_expects(nb_parts > 0, + error_type_t::ValidationError, + "%s: nb_parts must be positive", + context); + if (parts.empty()) { return; } + const auto [min_it, max_it] = std::minmax_element(parts.begin(), parts.end()); + cuopt_expects(*min_it >= 0, + error_type_t::ValidationError, + "%s: partition ids must be non-negative (min=%d)", + context, + static_cast(*min_it)); + cuopt_expects(*max_it < nb_parts, + error_type_t::ValidationError, + "%s: partition ids must be in [0, %d) (max=%d)", + context, + static_cast(nb_parts), + static_cast(*max_it)); +} + +template +std::unique_ptr> make_partitioner(partitioner_kind_t kind) +{ + switch (kind) { + case partitioner_kind_t::Dummy: + return std::make_unique>(); + } + cuopt_expects(false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); + return nullptr; +} + +template class dummy_partitioner_t; +template std::unique_ptr> make_partitioner(partitioner_kind_t); + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp new file mode 100644 index 0000000000..ee5798fd0b --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +// Non-owning view of a host CSR matrix (A or A_t). +template +struct csr_host_view_t { + std::vector const* row_offsets{nullptr}; + std::vector const* col_indices{nullptr}; + std::vector const* values{nullptr}; // optional; unused by topology-only partitioners + i_t num_rows{0}; + i_t num_cols{0}; +}; + +// Inputs shared by all distributed-PDLP partitioners. +// Returns a flat vector of length (nb_cstr + nb_vars): constraint part-ids first, +// then variable part-ids, each in [0, nb_parts). +template +struct partitioner_input_t { + i_t nb_cstr{0}; + i_t nb_vars{0}; + i_t nb_parts{0}; + // Constraint matrix A (rows = constraints, cols = variables). + csr_host_view_t A{}; + // Transpose A_t (rows = variables, cols = constraints). Optional for partitioners + // that build a bipartite graph (e.g. METIS); dummy partitioner ignores both matrices. + csr_host_view_t A_t{}; +}; + +enum class partitioner_kind_t { Dummy /*, Metis */ }; + +template +class partitioner_i { + public: + virtual ~partitioner_i() = default; + virtual std::vector partition(partitioner_input_t const& input) const = 0; +}; + +template +class dummy_partitioner_t : public partitioner_i { + public: + std::vector partition(partitioner_input_t const& input) const override; +}; + +void validate_partition(std::vector const& parts, + int nb_cstr, + int nb_vars, + int nb_parts, + char const* context = "partition"); + +template +std::unique_ptr> make_partitioner(partitioner_kind_t kind); + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 1e76fa4251..203547367b 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -396,28 +397,32 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, "Distributed PDLP (num_gpus > 1) currently requires double precision"); return; } else { - // 2. Load partition + // 2. Load or compute partition std::vector parts; if (!settings.multi_gpu_partition_file.empty()) { parts = partition_loader_t::parse_distributed_pdlp_partition_file( settings.multi_gpu_partition_file); - } else if (num_gpus == 1) { - // Single-part dummy run: useful for exercising the mGPU code paths on a - // single physical GPU without a real Metis partition file. The downstream - // create_rank_data_from_parts expects a flat vector of length - // (n_constraints + n_variables) where each entry is the owning part-id - // (cstrs first, then vars). With nb_parts == 1, every entry is 0. - std::cout << "CAREFUL: num_gpus == 1, running dummy version (single part covering " - << op_problem_scaled_.n_constraints << " cstrs + " - << op_problem_scaled_.n_variables << " vars)" << std::endl; - parts = std::vector( - static_cast(op_problem_scaled_.n_constraints + op_problem_scaled_.n_variables), - 0); + validate_partition(parts, + op_problem_scaled_.n_constraints, + op_problem_scaled_.n_variables, + num_gpus, + "partition file"); } else { - cuopt_expects(false, - error_type_t::RuntimeError, - "Metis partitioning inside cuopt not implemented yet; " - "provide a --parts file via settings.multi_gpu_partition_file"); + if (num_gpus == 1) { + // Single-part dummy run: useful for exercising the mGPU code paths on a + // single physical GPU without a real partition file. + std::cout << "CAREFUL: num_gpus == 1, running dummy version (single part covering " + << op_problem_scaled_.n_constraints << " cstrs + " + << op_problem_scaled_.n_variables << " vars)" << std::endl; + } + partitioner_input_t partition_input; + partition_input.nb_cstr = op_problem_scaled_.n_constraints; + partition_input.nb_vars = op_problem_scaled_.n_variables; + partition_input.nb_parts = num_gpus; + // Dummy partitioner ignores A / A_t for now; future METIS partitioners will + // fill these CSR views before calling partition(). + auto partitioner = make_partitioner(partitioner_kind_t::Dummy); + parts = partitioner->partition(partition_input); } // always compute initial step size before scaling and primal_weight after scaling to do like From ca7d7a91b33b72c60c885a7314619097d82e19ad Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 27 May 2026 15:57:37 +0200 Subject: [PATCH 039/258] added stream forking for cuda graph --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 13 +++++ .../distributed_pdlp/multi_gpu_engine.hpp | 55 +++++++++++++++++++ cpp/src/pdlp/pdhg.cu | 17 ++++++ 3 files changed, 85 insertions(+) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index a0b3f5dcc3..796153fd79 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -81,6 +81,19 @@ multi_gpu_engine_t::multi_gpu_engine_t( objective_scaling_factor, sub_solver_settings)); } + + // 4. Allocate fork/join events for cross-stream graph capture splicing. + // fork_event_ on the master device (whatever device is current when the + // engine is constructed -- pdlp_solver_t's mGPU ctor runs on master). + // join_events_[r] on shard r's device. event_handler_t uses the default + // cudaEventCreate (no flags), matching the rest of the codebase. + // Cleanup is automatic via event_handler_t's RAII destructor. + fork_event_ = std::make_unique(); + join_events_.reserve(nb_parts); + for (int r = 0; r < nb_parts; ++r) { + raft::device_setter guard(devices[r]); + join_events_.emplace_back(std::make_unique()); + } } template struct multi_gpu_engine_t; diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index d156e889af..ade0da1c66 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -446,6 +447,60 @@ struct multi_gpu_engine_t { // Shards stored by unique_ptr because pdlp_shard_t is immovable // (owns device-affine resources: handle, NCCL comm, RMM buffers). std::vector>> shards; + + // ===== Fork/join events for CUDA graph capture spanning shard streams ===== + // + // CUDA graph capture starts on the master pdhg stream (in pdhg_solver_t). + // The per-iteration work then dispatches kernels and NCCL collectives onto + // each shard's own stream. For these cross-stream operations to be + // recorded into the same captured graph (instead of escaping the capture + // and either invalidating it or being silently dropped), every shard + // stream must be "spliced" into the active capture via fork/join events. + // + // master_stream ──record(fork_event_)──┐ + // ├─> shard_0.stream (waits) ──┐ + // ├─> shard_1.stream (waits) ──┤ + // └─> shard_{n-1}.stream ──┘ + // (record join_events_[r]) + // master waits on each + // + // Pattern mirrors metis_tests/src/bench.cu. Events are reused across + // iterations (created once at engine construction) and cleaned up + // automatically by event_handler_t's RAII destructor. + // + // unique_ptr because event_handler_t is non-copyable and we need + // per-device construction (each join event must be created with its + // shard's device current). + std::unique_ptr fork_event_; + std::vector> join_events_; + + // fork_to_shards: record fork_event_ on `master_stream`, then make every + // shard stream wait on it. Inside a graph capture, this splices every + // shard stream into the same captured graph. + void fork_to_shards(rmm::cuda_stream_view master_stream) + { + fork_event_->record(master_stream); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + fork_event_->stream_wait(s->stream.view()); + } + } + + // join_from_shards: each shard records its join event on its own stream, + // then `master_stream` waits on every join event. Closes the captured + // sub-graph back into the master stream so cudaStreamEndCapture can + // produce a single graph spanning all streams. + void join_from_shards(rmm::cuda_stream_view master_stream) + { + const int nb = static_cast(shards.size()); + for (int r = 0; r < nb; ++r) { + raft::device_setter guard(shards[r]->device_id); + join_events_[r]->record(shards[r]->stream.view()); + } + for (auto& e : join_events_) { + e->stream_wait(master_stream); + } + } }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index 969f5d0d30..df183dc7e6 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -1249,6 +1249,14 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( if (should_major) { graph_all.run(should_major, [&]() { + // Multi-GPU: splice shard streams into the capture so their kernels and + // NCCL collectives are recorded into the same graph. Without this, work + // issued on shard.stream from inside this lambda would either invalidate + // the capture or run outside the graph, leaving the captured graph + // empty (or broken) -- which produces the cycling/stall behavior we + // observed on larger problems. Mirrors metis_tests bench.cu fork/join. + if (mgpu_engine_ != nullptr) { mgpu_engine_->fork_to_shards(stream_view_); } + compute_At_y(); if (mgpu_engine_ != nullptr) { for (auto& shard : mgpu_engine_->shards) { @@ -1346,10 +1354,17 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( print("potential_next_dual_solution_", potential_next_dual_solution_); print("reflected_dual_", reflected_dual_); #endif + + // Multi-GPU: close the fork by joining every shard stream back into + // the master stream so cudaStreamEndCapture sees a single graph + // spanning all streams. + if (mgpu_engine_ != nullptr) { mgpu_engine_->join_from_shards(stream_view_); } }); } else { graph_all.run(should_major, [&]() { + if (mgpu_engine_ != nullptr) { mgpu_engine_->fork_to_shards(stream_view_); } + // Compute next primal compute_At_y(); @@ -1454,6 +1469,8 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( #ifdef CUPDLP_DEBUG_MODE print("reflected_dual_", reflected_dual_); #endif + + if (mgpu_engine_ != nullptr) { mgpu_engine_->join_from_shards(stream_view_); } }); } } From 0310d50a57dbb6b7f5752a8630f19cf663658795 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 27 May 2026 18:49:12 +0200 Subject: [PATCH 040/258] updated convergence information to use potential_next rather than current in compute_primal/dual_residual, as the dual_iterate parameter --- .../convergence_information.cu | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 28b33582ab..608590ffa0 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -429,22 +429,24 @@ void convergence_information_t::compute_convergence_information( error_type_t::ValidationError, "per_constraint_residual is not yet supported in multi-GPU mode"); - // Prepares halo values in primal_solution + // Prepares halo values in potential_next_primal_solution + engine->halo_exchange_var( [](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_primal_solution(); + return pdhg.get_potential_next_primal_solution(); }); - // Compute the primal residual and objective on each shard for (auto& shard : engine->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); - sub_conv.compute_primal_residual(sub_conv.op_problem_cusparse_view_, - sub_pdlp.pdhg_solver_.get_dual_tmp_resource(), - sub_pdlp.pdhg_solver_.get_dual_solution()); - sub_conv.compute_primal_objective_owned_partial(sub_pdlp.pdhg_solver_.get_primal_solution(), - shard->rank_data.owned_var_size); + sub_conv.compute_primal_residual( + sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_dual_tmp_resource(), + sub_pdlp.pdhg_solver_.get_potential_next_dual_solution()); + sub_conv.compute_primal_objective_owned_partial( + sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), + shard->rank_data.owned_var_size); } // Reduce all primal objectives across shards @@ -546,12 +548,15 @@ void convergence_information_t::compute_convergence_information( if (current_pdhg_solver.is_multi_gpu()) { auto* engine = current_pdhg_solver.get_mgpu_engine(); - // 1) Halo-exchange the dual solution on every shard so the upcoming - // A_T_shard @ dual SpMV inside compute_dual_residual reads correct - // values in the cstr halo region. + // 1) Halo-exchange potential_next_dual_solution on every shard so the + // A_T_shard @ y SpMV inside compute_dual_residual reads correct values + // in the cstr halo region. The SpMV is driven through the eval view's + // cv.dual_solution descriptor, which (cuPDLPx, see + // cusparse_view.cu:931-937) is bound to _potential_next_dual -- not to + // current.dual_solution. So we must halo-exchange the same buffer. engine->halo_exchange_cstr( [](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_dual_solution(); + return pdhg.get_potential_next_dual_solution(); }); // 2-3) Per-shard: @@ -563,18 +568,26 @@ void convergence_information_t::compute_convergence_information( // shard.dual_objective_, with NO scaling/offset. Relies on // primal_slack_ already populated by the per-shard // compute_primal_residual above. + // + // Same primal_iterate fix as the primal block above: use the shard's + // (fresh, unscaled) potential_next_primal_solution, matching single-GPU + // cuPDLPx (pdlp.cu:1190-1203). The previous code's get_primal_solution() + // would mix scaled x with unscaled dual_slack in the dual_objective + // cublasdot. for (auto& shard : engine->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); - sub_conv.compute_dual_residual(sub_conv.op_problem_cusparse_view_, - sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), - sub_pdlp.pdhg_solver_.get_primal_solution(), - sub_pdlp.pdhg_solver_.get_dual_slack()); - sub_conv.compute_dual_objective_owned_partial(sub_pdlp.pdhg_solver_.get_primal_solution(), - sub_pdlp.pdhg_solver_.get_dual_slack(), - shard->rank_data.owned_var_size, - shard->rank_data.owned_cstr_size); + sub_conv.compute_dual_residual( + sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), + sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), + sub_pdlp.pdhg_solver_.get_dual_slack()); + sub_conv.compute_dual_objective_owned_partial( + sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), + sub_pdlp.pdhg_solver_.get_dual_slack(), + shard->rank_data.owned_var_size, + shard->rank_data.owned_cstr_size); } // 4) Allreduce dual_objective_ across shards (sum, in place). Same From f811bc8459f71f6690efce337daacf6db4892141 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 27 May 2026 19:49:54 +0200 Subject: [PATCH 041/258] disabled graph, can sole afiro hehe --- cpp/src/pdlp/distributed_pdlp/shard.cu | 8 +++++++- cpp/src/pdlp/pdlp.cu | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 405e6fa05c..45f9f7a880 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -153,7 +153,13 @@ pdlp_shard_t::pdlp_shard_t(int device_id, // At this point sub_pdlp.op_problem_scaled_ is an unscaled copy // of sub_problem and sub_pdlp.initial_scaling_strategy_ has // unit cumulative factors (sub-settings disable Ruiz / PC iters). - sub_pdlp = std::make_unique>(*sub_problem, settings, /*batch=*/false); + // NOTE: pass is_legacy_batch_mode=true to disable CUDA-graph capture inside + // sub_pdlp while debugging fake-mGPU divergence. The flag is a pure + // graph-capture toggle (ping_pong_graph_t / manual_cuda_graph_t) and does + // not change any algorithm semantics. Restore to false once the path is + // confirmed correct. + sub_pdlp = std::make_unique>( + *sub_problem, settings, /*is_legacy_batch_mode=*/true); sub_pdlp->pdhg_solver_.set_is_multi_gpu(true); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 203547367b..ecc2e35c20 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -381,7 +381,13 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, int num_gpus) // 1. Delegate to single-GPU ctor to bring up all the per-master state // (problem_ptr, op_problem_scaled_, pdhg_solver_, strategies, etc.). - : pdlp_solver_t(op_problem, settings, false) + // + // NOTE: pass is_legacy_batch_mode=true to disable CUDA-graph capture on the + // master while we are debugging fake-mGPU divergence. The flag is a pure + // graph-capture toggle (see ping_pong_graph_t / manual_cuda_graph_t); it does + // not change any algorithm semantics. Restore to false once the path is + // confirmed correct. + : pdlp_solver_t(op_problem, settings, /*is_legacy_batch_mode=*/true) { if (num_gpus == 1) { std::cout << "CAREFUL: num_gpus == 1, running dummy version" << std::endl; From 4d7e2fced7f3600ca45e6a171972483af876576a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 28 May 2026 12:49:58 +0200 Subject: [PATCH 042/258] added join_from_shards in convergence_info, now afiro is erfect 510 but a28 is 2100 vs 1500 hmmmm --- .../convergence_information.cu | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 608590ffa0..7877a64c88 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -459,7 +459,10 @@ void convergence_information_t::compute_convergence_information( .data(); }); - // Get the reduced primal objective from the shard[0] (arbitrary) + // Get the reduced primal objective from the shard[0] (arbitrary) + // Race fix: master stream must wait for shard streams to finish the + // allreduce above before copying scalar data out of shard 0's buffer. + engine->join_from_shards(stream_view_); { auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); @@ -494,6 +497,9 @@ void convergence_information_t::compute_convergence_information( }, [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_cstr_size; }); + // Race fix: master stream must wait for shard streams to finish the + // distributed L2 norm before copying scalar data out of shard 0. + engine->join_from_shards(stream_view_); auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); raft::copy(l2_primal_residual_.data(), @@ -601,6 +607,9 @@ void convergence_information_t::compute_convergence_information( .data(); }); + // Race fix: master stream must wait for shard streams to finish the + // allreduce above before copying scalar data out of shard 0's buffer. + engine->join_from_shards(stream_view_); { auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); @@ -637,6 +646,9 @@ void convergence_information_t::compute_convergence_information( .l2_dual_residual_.data(); }, [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_var_size; }); + // Race fix: master stream must wait for shard streams to finish the + // distributed L2 norm before copying scalar data out of shard 0. + engine->join_from_shards(stream_view_); auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); raft::copy(l2_dual_residual_.data(), From 7ad460664e5c09328fa3ef3c32dcc2b7598cbc77 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 28 May 2026 13:52:22 +0200 Subject: [PATCH 043/258] use spmvop in mgpu and fixed small bug of increment_iteration_since_last_restart. now we have exact same iter for A28 --- .../distributed_pdlp/multi_gpu_engine.hpp | 26 ++++++++++++------- cpp/src/pdlp/pdlp.cu | 3 +-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index ade0da1c66..637c342975 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -311,21 +311,29 @@ struct multi_gpu_engine_t { // -------- High-level: A @ x and A_T @ y --------------------------------- // Thin wrappers used from pdhg_solver_t::compute_A_x / compute_At_y when an - // engine is wired in. They use the canonical PDHG buffers/descriptors so the - // result lands where single-GPU PDHG would have put it (dual_gradient for A, - // current_AtY for A_T). + // engine is wired in. They drive the per-shard plan-based SpMV via the + // canonical cusparse_view bindings (no rebinding) so the descriptor binding + // is never disturbed by mGPU machinery. + // + // The halo-exchange MUST target the exact buffer the canonical descriptor + // is bound to in the PDHG cusparse_view (see cusparse_view.cu lines 516-519 + // and 595-599): + // - cv.reflected_primal_solution -> reflected_primal_ (var-shaped) + // - cv.dual_solution -> current.dual_solution_ (cstr-shaped) + // For 1 shard the halo-exchange is a no-op, but the buffer choice is what + // makes multi-shard correctness work, so we keep it accurate either way. void distributed_compute_A_x() { - distributed_spmv_A( - [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_primal(); }, - [](auto& pdhg) -> cusparseDnVecDescr_t { return pdhg.get_cusparse_view().dual_gradient; }); + halo_exchange_var( + [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_primal(); }); + for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_A_x(); }); } void distributed_compute_At_y() { - distributed_spmv_At( - [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_dual_solution(); }, - [](auto& pdhg) -> cusparseDnVecDescr_t { return pdhg.get_cusparse_view().current_AtY; }); + halo_exchange_cstr( + [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_dual_solution(); }); + for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); }); } // -------- Solution gather (shards -> master) ---------------------------- diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index ecc2e35c20..91263828c1 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3154,12 +3154,11 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co ++total_pdlp_iterations_; ++internal_solver_iterations_; if (settings_.hyper_params.never_restart_to_average) { + restart_strategy_.increment_iteration_since_last_restart(); if (multi_gpu_engine) { multi_gpu_engine->for_each_shard([&](auto& shard) { shard.sub_pdlp->restart_strategy_.increment_iteration_since_last_restart(); }); - } else { - restart_strategy_.increment_iteration_since_last_restart(); } } } From 03d1259e668b2103c0547e7b3a0826eb9c18f311 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 28 May 2026 13:57:10 +0200 Subject: [PATCH 044/258] re-enabled graph. not working --- cpp/src/pdlp/distributed_pdlp/shard.cu | 15 +++++++++------ cpp/src/pdlp/pdlp.cu | 12 ++++++------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 45f9f7a880..93dc1403fc 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -153,13 +153,16 @@ pdlp_shard_t::pdlp_shard_t(int device_id, // At this point sub_pdlp.op_problem_scaled_ is an unscaled copy // of sub_problem and sub_pdlp.initial_scaling_strategy_ has // unit cumulative factors (sub-settings disable Ruiz / PC iters). - // NOTE: pass is_legacy_batch_mode=true to disable CUDA-graph capture inside - // sub_pdlp while debugging fake-mGPU divergence. The flag is a pure - // graph-capture toggle (ping_pong_graph_t / manual_cuda_graph_t) and does - // not change any algorithm semantics. Restore to false once the path is - // confirmed correct. + // Graph capture is enabled. The per-shard kernels invoked by the master's + // captured graph (compute_next_primal_dual_solution_reflected → for_each_shard + // → primal/dual_reflected_*_projection_transform on sub_pdlp's pdhg) are + // recorded into the same graph via the fork_to_shards / join_from_shards + // splicing on the master stream. Shards never own their own graph; their + // pdhg ping_pong_graph_t is only constructed because pdlp_solver_t requires + // it, but no graph.run() on a shard's pdhg is ever invoked from the mGPU + // path (compute_next_primal_dual_solution_reflected runs on master). sub_pdlp = std::make_unique>( - *sub_problem, settings, /*is_legacy_batch_mode=*/true); + *sub_problem, settings, /*is_legacy_batch_mode=*/false); sub_pdlp->pdhg_solver_.set_is_multi_gpu(true); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 91263828c1..fd78a0ac9d 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -382,12 +382,12 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, // 1. Delegate to single-GPU ctor to bring up all the per-master state // (problem_ptr, op_problem_scaled_, pdhg_solver_, strategies, etc.). // - // NOTE: pass is_legacy_batch_mode=true to disable CUDA-graph capture on the - // master while we are debugging fake-mGPU divergence. The flag is a pure - // graph-capture toggle (see ping_pong_graph_t / manual_cuda_graph_t); it does - // not change any algorithm semantics. Restore to false once the path is - // confirmed correct. - : pdlp_solver_t(op_problem, settings, /*is_legacy_batch_mode=*/true) + // Graph capture is enabled here. The master's captured graph splices the + // shard streams via fork_to_shards/join_from_shards inside + // compute_next_primal_dual_solution_reflected (see pdhg.cu) so every + // per-shard kernel and NCCL collective is recorded into the same parent + // graph. + : pdlp_solver_t(op_problem, settings, /*is_legacy_batch_mode=*/false) { if (num_gpus == 1) { std::cout << "CAREFUL: num_gpus == 1, running dummy version" << std::endl; From cdc912b50d0d2549c6f9a43576efcc7b4a2edb3c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 28 May 2026 14:40:47 +0200 Subject: [PATCH 045/258] Cleaner sync semantics, ez ez ez, single mGPU gives exact same results as base PDLP on afiro and a28, with graphs !!!! EZ --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 18 ++--- .../distributed_pdlp/multi_gpu_engine.hpp | 81 ++++++++++--------- cpp/src/pdlp/distributed_pdlp/shard.cu | 11 --- cpp/src/pdlp/pdhg.cu | 21 ++++- cpp/src/pdlp/pdlp.cu | 6 -- .../convergence_information.cu | 17 ++-- 6 files changed, 75 insertions(+), 79 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 796153fd79..98f33b6c88 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -82,17 +82,17 @@ multi_gpu_engine_t::multi_gpu_engine_t( sub_solver_settings)); } - // 4. Allocate fork/join events for cross-stream graph capture splicing. - // fork_event_ on the master device (whatever device is current when the - // engine is constructed -- pdlp_solver_t's mGPU ctor runs on master). - // join_events_[r] on shard r's device. event_handler_t uses the default - // cudaEventCreate (no flags), matching the rest of the codebase. - // Cleanup is automatic via event_handler_t's RAII destructor. - fork_event_ = std::make_unique(); - join_events_.reserve(nb_parts); + // Two different events + // capture_*_event_ are used inside graph capture + // ext_*_event_ are used when sync is needed outside of graph + graph_master_ready_event_ = std::make_unique(); + sync_master_ready_event_ = std::make_unique(); + graph_shard_ready_events_.reserve(nb_parts); + sync_shard_ready_events_.reserve(nb_parts); for (int r = 0; r < nb_parts; ++r) { raft::device_setter guard(devices[r]); - join_events_.emplace_back(std::make_unique()); + graph_shard_ready_events_.emplace_back(std::make_unique()); + sync_shard_ready_events_.emplace_back(std::make_unique()); } } diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 637c342975..674c4c0ef2 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -456,56 +456,59 @@ struct multi_gpu_engine_t { // (owns device-affine resources: handle, NCCL comm, RMM buffers). std::vector>> shards; - // ===== Fork/join events for CUDA graph capture spanning shard streams ===== - // - // CUDA graph capture starts on the master pdhg stream (in pdhg_solver_t). - // The per-iteration work then dispatches kernels and NCCL collectives onto - // each shard's own stream. For these cross-stream operations to be - // recorded into the same captured graph (instead of escaping the capture - // and either invalidating it or being silently dropped), every shard - // stream must be "spliced" into the active capture via fork/join events. - // - // master_stream ──record(fork_event_)──┐ - // ├─> shard_0.stream (waits) ──┐ - // ├─> shard_1.stream (waits) ──┤ - // └─> shard_{n-1}.stream ──┘ - // (record join_events_[r]) - // master waits on each - // - // Pattern mirrors metis_tests/src/bench.cu. Events are reused across - // iterations (created once at engine construction) and cleaned up - // automatically by event_handler_t's RAII destructor. - // - // unique_ptr because event_handler_t is non-copyable and we need - // per-device construction (each join event must be created with its - // shard's device current). - std::unique_ptr fork_event_; - std::vector> join_events_; - - // fork_to_shards: record fork_event_ on `master_stream`, then make every - // shard stream wait on it. Inside a graph capture, this splices every - // shard stream into the same captured graph. - void fork_to_shards(rmm::cuda_stream_view master_stream) + // ===== Cross-stream synchronization events ===== + // two different events + // capture_*_event_ are used inside graph capture + // ext_*_event_ are used when sync is needed outside of graph + std::unique_ptr graph_master_ready_event_; + std::vector> graph_shard_ready_events_; + std::unique_ptr sync_master_ready_event_; + std::vector> sync_shard_ready_events_; + + // Forks master stream to shards, so that the captured graph can see the work on the shards + void graph_capture_fork_to_shards(rmm::cuda_stream_view master_stream) + { + graph_master_ready_event_->record(master_stream); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + graph_master_ready_event_->stream_wait(s->stream.view()); + } + } + + // Joins shards back to master stream for correct graph capture + void graph_capture_join_from_shards(rmm::cuda_stream_view master_stream) + { + const int nb = static_cast(shards.size()); + for (int r = 0; r < nb; ++r) { + raft::device_setter guard(shards[r]->device_id); + graph_shard_ready_events_[r]->record(shards[r]->stream.view()); + } + for (auto& e : graph_shard_ready_events_) { + e->stream_wait(master_stream); + } + } + + // Functionnaly same as graph_capture_fork_to_shards but on a different event to avoid race conditions + // Can be used as a way to sync shards with master stream + void sync_await_master(rmm::cuda_stream_view master_stream) { - fork_event_->record(master_stream); + sync_master_ready_event_->record(master_stream); for (auto& s : shards) { raft::device_setter guard(s->device_id); - fork_event_->stream_wait(s->stream.view()); + sync_master_ready_event_->stream_wait(s->stream.view()); } } - // join_from_shards: each shard records its join event on its own stream, - // then `master_stream` waits on every join event. Closes the captured - // sub-graph back into the master stream so cudaStreamEndCapture can - // produce a single graph spanning all streams. - void join_from_shards(rmm::cuda_stream_view master_stream) + // Same as sync_await_master + // Can be used as a way to sync master stream with shards + void sync_await_shards(rmm::cuda_stream_view master_stream) { const int nb = static_cast(shards.size()); for (int r = 0; r < nb; ++r) { raft::device_setter guard(shards[r]->device_id); - join_events_[r]->record(shards[r]->stream.view()); + sync_shard_ready_events_[r]->record(shards[r]->stream.view()); } - for (auto& e : join_events_) { + for (auto& e : sync_shard_ready_events_) { e->stream_wait(master_stream); } } diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 93dc1403fc..3a49287362 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -150,17 +150,6 @@ pdlp_shard_t::pdlp_shard_t(int device_id, handle.sync_stream(stream_view); // ---- 5. Build sub_pdlp (single-GPU mode; multi_gpu flags cleared by caller). ---- - // At this point sub_pdlp.op_problem_scaled_ is an unscaled copy - // of sub_problem and sub_pdlp.initial_scaling_strategy_ has - // unit cumulative factors (sub-settings disable Ruiz / PC iters). - // Graph capture is enabled. The per-shard kernels invoked by the master's - // captured graph (compute_next_primal_dual_solution_reflected → for_each_shard - // → primal/dual_reflected_*_projection_transform on sub_pdlp's pdhg) are - // recorded into the same graph via the fork_to_shards / join_from_shards - // splicing on the master stream. Shards never own their own graph; their - // pdhg ping_pong_graph_t is only constructed because pdlp_solver_t requires - // it, but no graph.run() on a shard's pdhg is ever invoked from the mGPU - // path (compute_next_primal_dual_solution_reflected runs on master). sub_pdlp = std::make_unique>( *sub_problem, settings, /*is_legacy_batch_mode=*/false); diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index df183dc7e6..ec983fd01b 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -1245,6 +1245,8 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( using f_t2 = typename type_2::type; + if (mgpu_engine_ != nullptr) { mgpu_engine_->sync_await_shards(stream_view_); } + // Compute next primal solution reflected. if (should_major) { @@ -1255,7 +1257,9 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // the capture or run outside the graph, leaving the captured graph // empty (or broken) -- which produces the cycling/stall behavior we // observed on larger problems. Mirrors metis_tests bench.cu fork/join. - if (mgpu_engine_ != nullptr) { mgpu_engine_->fork_to_shards(stream_view_); } + if (mgpu_engine_ != nullptr) { + mgpu_engine_->graph_capture_fork_to_shards(stream_view_); + } compute_At_y(); if (mgpu_engine_ != nullptr) { @@ -1358,12 +1362,16 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // Multi-GPU: close the fork by joining every shard stream back into // the master stream so cudaStreamEndCapture sees a single graph // spanning all streams. - if (mgpu_engine_ != nullptr) { mgpu_engine_->join_from_shards(stream_view_); } + if (mgpu_engine_ != nullptr) { + mgpu_engine_->graph_capture_join_from_shards(stream_view_); + } }); } else { graph_all.run(should_major, [&]() { - if (mgpu_engine_ != nullptr) { mgpu_engine_->fork_to_shards(stream_view_); } + if (mgpu_engine_ != nullptr) { + mgpu_engine_->graph_capture_fork_to_shards(stream_view_); + } // Compute next primal compute_At_y(); @@ -1470,9 +1478,14 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( print("reflected_dual_", reflected_dual_); #endif - if (mgpu_engine_ != nullptr) { mgpu_engine_->join_from_shards(stream_view_); } + if (mgpu_engine_ != nullptr) { + mgpu_engine_->graph_capture_join_from_shards(stream_view_); + } }); } + + // sync to master stream after the graph is captured + if (mgpu_engine_ != nullptr) { mgpu_engine_->sync_await_master(stream_view_); } } template diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index fd78a0ac9d..8cf37cd8a1 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -381,12 +381,6 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, int num_gpus) // 1. Delegate to single-GPU ctor to bring up all the per-master state // (problem_ptr, op_problem_scaled_, pdhg_solver_, strategies, etc.). - // - // Graph capture is enabled here. The master's captured graph splices the - // shard streams via fork_to_shards/join_from_shards inside - // compute_next_primal_dual_solution_reflected (see pdhg.cu) so every - // per-shard kernel and NCCL collective is recorded into the same parent - // graph. : pdlp_solver_t(op_problem, settings, /*is_legacy_batch_mode=*/false) { if (num_gpus == 1) { diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 7877a64c88..da2340146a 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -460,9 +460,8 @@ void convergence_information_t::compute_convergence_information( }); // Get the reduced primal objective from the shard[0] (arbitrary) - // Race fix: master stream must wait for shard streams to finish the - // allreduce above before copying scalar data out of shard 0's buffer. - engine->join_from_shards(stream_view_); + // Sync shards with master stream to avoid race conditions + engine->sync_await_shards(stream_view_); { auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); @@ -497,9 +496,8 @@ void convergence_information_t::compute_convergence_information( }, [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_cstr_size; }); - // Race fix: master stream must wait for shard streams to finish the // distributed L2 norm before copying scalar data out of shard 0. - engine->join_from_shards(stream_view_); + engine->sync_await_shards(stream_view_); auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); raft::copy(l2_primal_residual_.data(), @@ -607,9 +605,8 @@ void convergence_information_t::compute_convergence_information( .data(); }); - // Race fix: master stream must wait for shard streams to finish the - // allreduce above before copying scalar data out of shard 0's buffer. - engine->join_from_shards(stream_view_); + // Sync shards with master stream to avoid race conditions + engine->sync_await_shards(stream_view_); { auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); @@ -646,9 +643,9 @@ void convergence_information_t::compute_convergence_information( .l2_dual_residual_.data(); }, [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_var_size; }); - // Race fix: master stream must wait for shard streams to finish the + // distributed L2 norm before copying scalar data out of shard 0. - engine->join_from_shards(stream_view_); + engine->sync_await_shards(stream_view_); auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); raft::copy(l2_dual_residual_.data(), From 04d22cf161e55aef6f37add6ff3d988dcd2c34de Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 29 May 2026 05:48:01 -0700 Subject: [PATCH 046/258] pad local matrices for easier integration and allow mismatch of nnz between A and A_t for shards --- cpp/src/pdlp/cusparse_view.cu | 17 +++++++++++------ .../pdlp/distributed_pdlp/partition_loader.cu | 10 ++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/cpp/src/pdlp/cusparse_view.cu b/cpp/src/pdlp/cusparse_view.cu index 396fd27499..1e3638cdbd 100644 --- a/cpp/src/pdlp/cusparse_view.cu +++ b/cpp/src/pdlp/cusparse_view.cu @@ -498,14 +498,17 @@ cusparse_view_t::cusparse_view_t( // setup cusparse view A.create(op_problem_scaled.n_constraints, op_problem_scaled.n_variables, - op_problem_scaled.nnz, + static_cast(A_.size()), const_cast(op_problem_scaled.offsets.data()), const_cast(op_problem_scaled.variables.data()), const_cast(op_problem_scaled.coefficients.data())); + // A_T can have a different nnz than A in multi-GPU shards + // A is just what is needed to compute A_x for owned constraints + // A_T is just what is needed to compute A_T_y for owned variables A_T.create(op_problem_scaled.n_variables, op_problem_scaled.n_constraints, - op_problem_scaled.nnz, + static_cast(A_T_.size()), const_cast(A_T_offsets_.data()), const_cast(A_T_indices_.data()), const_cast(A_T_.data())); @@ -914,14 +917,14 @@ cusparse_view_t::cusparse_view_t( // setup cusparse view A.create(op_problem.n_constraints, op_problem.n_variables, - op_problem.nnz, + static_cast(A_.size()), const_cast(op_problem.offsets.data()), const_cast(op_problem.variables.data()), const_cast(op_problem.coefficients.data())); A_T.create(op_problem.n_variables, op_problem.n_constraints, - op_problem.nnz, + static_cast(A_T_.size()), const_cast(A_T_offsets_.data()), const_cast(A_T_indices_.data()), const_cast(A_T_.data())); @@ -1129,16 +1132,18 @@ cusparse_view_t::cusparse_view_t( // Copying them from the existing cuSparse view is a bad practice and creates segfault post // CUDA 12.4 Using the saved pointer of the existing cusparse view to make sure we capture the // correct pointer + // See comment in the PDHG cusparse_view_t ctor: bind the descriptor nnz to + // the actual value-buffer length so A and A_T stay symmetric and shard-safe. A.create(op_problem.n_constraints, op_problem.n_variables, - op_problem.nnz, + static_cast(A_.size()), const_cast(A_offsets_.data()), const_cast(A_indices_.data()), const_cast(A_.data())); A_T.create(op_problem.n_variables, op_problem.n_constraints, - op_problem.nnz, + static_cast(existing_cusparse_view.A_T_.size()), const_cast(existing_cusparse_view.A_T_offsets_.data()), const_cast(existing_cusparse_view.A_T_indices_.data()), const_cast(existing_cusparse_view.A_T_.data())); diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index b9bc71ae9e..5014607736 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -171,6 +171,16 @@ std::vector> partition_loader_t::create_rank_dat rd.total_var_size = rd.owned_var_size + needed_vars.size(); rd.total_cstr_size = rd.owned_cstr_size + needed_cstrs.size(); + + // Pad row-offset arrays so cuSPARSE sees the local matrices as + // (total_cstr x total_var) for A and (total_var x total_cstr) for A_T + const i_t a_last_nnz = + rd.h_A_row_offsets.empty() ? i_t{0} : rd.h_A_row_offsets.back(); + rd.h_A_row_offsets.resize(rd.total_cstr_size + 1, a_last_nnz); + + const i_t at_last_nnz = + rd.h_A_t_row_offsets.empty() ? i_t{0} : rd.h_A_t_row_offsets.back(); + rd.h_A_t_row_offsets.resize(rd.total_var_size + 1, at_last_nnz); } // 3. Generate local indices for contiguous [[self], [peer1], ..., [peer_k]] From b41df4583df78ec095efc351df8c10159d78ccdb Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 29 May 2026 06:24:42 -0700 Subject: [PATCH 047/258] copy scalars to host rather than direct d2d. better --- cpp/src/pdlp/pdlp.cu | 21 ++++++++++++---- cpp/src/pdlp/pdlp.cuh | 5 ++++ .../restart_strategy/pdlp_restart_strategy.cu | 24 +++++++++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 8cf37cd8a1..241b9a5aeb 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -572,14 +572,25 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, op_problem_scaled_.presolve_data.objective_scaling_factor, sub_pdlp_settings); + // Copy to host and then to shards. + // More robust than cudaDeviceEnablePeerAccess and cost-free-ish. + f_t h_step_size{}, h_primal_weight{}, h_best_primal_weight{}; + f_t h_primal_step_size{}, h_dual_step_size{}; + raft::copy(&h_step_size, step_size_.data(), 1, stream_view_); + raft::copy(&h_primal_weight, primal_weight_.data(), 1, stream_view_); + raft::copy(&h_best_primal_weight, best_primal_weight_.data(), 1, stream_view_); + raft::copy(&h_primal_step_size, primal_step_size_.data(), 1, stream_view_); + raft::copy(&h_dual_step_size, dual_step_size_.data(), 1, stream_view_); + handle_ptr_->sync_stream(stream_view_); + for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); auto& sub = *shard->sub_pdlp; - raft::copy(sub.step_size_.data(), step_size_.data(), 1, shard->stream); - raft::copy(sub.primal_weight_.data(), primal_weight_.data(), 1, shard->stream); - raft::copy(sub.best_primal_weight_.data(), best_primal_weight_.data(), 1, shard->stream); - raft::copy(sub.primal_step_size_.data(), primal_step_size_.data(), 1, shard->stream); - raft::copy(sub.dual_step_size_.data(), dual_step_size_.data(), 1, shard->stream); + raft::copy(sub.step_size_.data(), &h_step_size, 1, shard->stream); + raft::copy(sub.primal_weight_.data(), &h_primal_weight, 1, shard->stream); + raft::copy(sub.best_primal_weight_.data(), &h_best_primal_weight, 1, shard->stream); + raft::copy(sub.primal_step_size_.data(), &h_primal_step_size, 1, shard->stream); + raft::copy(sub.dual_step_size_.data(), &h_dual_step_size, 1, shard->stream); } // Wire the engine into the master pdhg_solver_. Shards' pdhg_solver_ keep diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 17fb05080f..15ddfdaad3 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -117,6 +117,11 @@ class pdlp_solver_t { // call across all shards' pdhg_solver_t::*_transform methods. rmm::device_uvector& get_primal_step_size() { return primal_step_size_; } rmm::device_uvector& get_dual_step_size() { return dual_step_size_; } + // Multi-GPU restart broadcast needs to mirror master's primal_weight / + // best_primal_weight onto every shard after each cuPDLPx restart so that + // downstream shard-side restart machinery stays in sync with master. + rmm::device_uvector& get_primal_weight() { return primal_weight_; } + rmm::device_uvector& get_best_primal_weight() { return best_primal_weight_; } private: void print_termination_criteria(const timer_t& timer, bool is_average = false); diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index 00c5b16c8b..b7d49fc32f 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -1004,15 +1004,29 @@ void pdlp_restart_strategy_t::cupdlpx_restart( best_primal_weight.set_element_async(0, best_primal_weight_value, stream_view_); } - // Broadcast the primal and dual step sizes to all shards + // mGPU: Broadcast all primal-weight / step-size scalars updated by the cuPDLPx + // restart on the master to every shard so the restart-state on + // each shard stays in sync with master. if (auto* engine = pdhg_solver.get_mgpu_engine()) { RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); + + f_t h_primal_step_size{}, h_dual_step_size{}; + f_t h_primal_weight{}, h_best_primal_weight{}; + + raft::copy(&h_primal_step_size, primal_step_size.data(), 1, stream_view_); + raft::copy(&h_dual_step_size, dual_step_size.data(), 1, stream_view_); + raft::copy(&h_primal_weight, primal_weight.data(), 1, stream_view_); + raft::copy(&h_best_primal_weight, best_primal_weight.data(), 1, stream_view_); + RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); + engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; - raft::copy(sub.get_primal_step_size().data(), - primal_step_size.data(), 1, shard.stream.view()); - raft::copy(sub.get_dual_step_size().data(), - dual_step_size.data(), 1, shard.stream.view()); + raft::copy( + sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard.stream.view()); + raft::copy(sub.get_dual_step_size().data(), &h_dual_step_size, 1, shard.stream.view()); + raft::copy(sub.get_primal_weight().data(), &h_primal_weight, 1, shard.stream.view()); + raft::copy( + sub.get_best_primal_weight().data(), &h_best_primal_weight, 1, shard.stream.view()); }); } // TODO later batch mode: remove if you have per climber restart From a1ffe1d791d203a903956769e89cde5452309d91 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 29 May 2026 06:29:39 -0700 Subject: [PATCH 048/258] force re-inject offset and variables to undo the sort, cheap and ugly but works --- cpp/src/pdlp/distributed_pdlp/shard.cu | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 3a49287362..356e10a03c 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -155,12 +155,26 @@ pdlp_shard_t::pdlp_shard_t(int device_id, sub_pdlp->pdhg_solver_.set_is_multi_gpu(true); - // Inject master-scaled buffers inside sub_pdlp + // Re-inject master-scaled buffers inside sub_pdlp. + // Need to also re-inject the offsets and variables arrays to revert + // the csrsort done by problem_t's constructor. auto& scaled = sub_pdlp->get_op_problem_scaled(); + raft::copy(scaled.offsets.data(), + rank_data.h_A_row_offsets.data(), + rank_data.h_A_row_offsets.size(), + stream_view); + raft::copy(scaled.variables.data(), + rank_data.h_A_col_indices.data(), + rank_data.h_A_col_indices.size(), + stream_view); raft::copy(scaled.coefficients.data(), rank_data.h_A_values_scaled.data(), rank_data.h_A_values_scaled.size(), stream_view); + // A_T side: all three arrays were already overridden together from + // rank_data on sub_problem (see step 4 above) and deep-copied into the + // scaled problem, so reverse_offsets / reverse_constraints already match + // h_A_t_values_scaled's order. Only the values need a SCALED swap-in. raft::copy(scaled.reverse_coefficients.data(), rank_data.h_A_t_values_scaled.data(), rank_data.h_A_t_values_scaled.size(), From c9394d9d00147ab740765ef02ba1ec6a77de2514 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 29 May 2026 06:48:38 -0700 Subject: [PATCH 049/258] few style changes, better args and prints --- cpp/cuopt_cli.cpp | 18 +++++++++-- .../cuopt/linear_programming/constants.h | 1 + .../pdlp/solver_settings.hpp | 3 ++ cpp/src/math_optimization/solver_settings.cu | 3 +- cpp/src/pdlp/pdlp.cu | 30 ++++++++++++------- cpp/src/pdlp/pdlp.cuh | 2 +- cpp/src/pdlp/solve.cu | 25 ++++++++++++---- 7 files changed, 60 insertions(+), 22 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 39aab47170..7c0a9111d9 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -426,10 +426,22 @@ int main(int argc, char* argv[]) std::vector memory_resources; if (memory_backend == cuopt::linear_programming::memory_backend_t::GPU) { - const int num_gpus = settings.get_parameter(CUOPT_NUM_GPUS); + // Distributed PDLP scales one shard per GPU and uses its own knob; everything else + // (concurrent, batch, MIP) uses num_gpus which is capped at 2. + // For distributed PDLP, -1 means "auto-detect": resolve to the visible device + // count so the RMM memory pools match what solve.cu will eventually dispatch. + const bool use_distributed_pdlp = settings.get_parameter(CUOPT_USE_DISTRIBUTED_PDLP); + int requested_gpus = + use_distributed_pdlp ? settings.get_parameter(CUOPT_DISTRIBUTED_PDLP_NUM_GPUS) + : settings.get_parameter(CUOPT_NUM_GPUS); + if (use_distributed_pdlp && requested_gpus == -1) { + requested_gpus = raft::device_setter::get_device_count(); + } + const int provisioned_gpus = + std::min(raft::device_setter::get_device_count(), requested_gpus); - memory_resources.reserve(std::min(raft::device_setter::get_device_count(), num_gpus)); - for (int i = 0; i < std::min(raft::device_setter::get_device_count(), num_gpus); ++i) { + memory_resources.reserve(provisioned_gpus); + for (int i = 0; i < provisioned_gpus; ++i) { RAFT_CUDA_TRY(cudaSetDevice(i)); memory_resources.emplace_back(); rmm::mr::set_per_device_resource(rmm::cuda_device_id{i}, memory_resources.back()); diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 26ef3653e0..3346ab3565 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -83,6 +83,7 @@ #define CUOPT_SOLUTION_FILE "solution_file" #define CUOPT_NUM_CPU_THREADS "num_cpu_threads" #define CUOPT_NUM_GPUS "num_gpus" +#define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" #define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" #define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index e8beef007d..efdbd5733c 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -307,6 +307,9 @@ class pdlp_solver_settings_t { presolver_t presolver{presolver_t::Default}; bool dual_postsolve{true}; int num_gpus{1}; + // Number of GPUs to use specifically for distributed PDLP (use_distributed_pdlp=true). + // -1 means auto-detect + int distributed_pdlp_num_gpus{-1}; std::string multi_gpu_partition_file{""}; // Set to true inside the shards bool is_distributed_sub_pdlp{false}; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 991b0d62c1..207e53f20d 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -138,8 +138,9 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_IMPLIED_BOUND_CUTS, &mip_settings.implied_bound_cuts, -1, 1, -1}, {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, - {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, 1, 576, 1}, + {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, 1, 2, 1}, {CUOPT_NUM_GPUS, &mip_settings.num_gpus, 1, 2, 1}, + {CUOPT_DISTRIBUTED_PDLP_NUM_GPUS, &pdlp_settings.distributed_pdlp_num_gpus, -1, 576, -1}, {CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING, &mip_settings.mip_batch_pdlp_strong_branching, 0, 2, 0}, {CUOPT_MIP_BATCH_PDLP_RELIABILITY_BRANCHING, &mip_settings.mip_batch_pdlp_reliability_branching, 0, 2, 0}, {CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT, &mip_settings.strong_branching_simplex_iteration_limit, -1,std::numeric_limits::max(), -1}, diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 241b9a5aeb..a061a2d468 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -378,23 +378,29 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, template pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, pdlp_solver_settings_t const& settings, - int num_gpus) + int distributed_pdlp_num_gpus) // 1. Delegate to single-GPU ctor to bring up all the per-master state // (problem_ptr, op_problem_scaled_, pdhg_solver_, strategies, etc.). : pdlp_solver_t(op_problem, settings, /*is_legacy_batch_mode=*/false) { - if (num_gpus == 1) { - std::cout << "CAREFUL: num_gpus == 1, running dummy version" << std::endl; + CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU", + distributed_pdlp_num_gpus); + if (distributed_pdlp_num_gpus == 1) { + std::cout << "CAREFUL !!: distributed_pdlp_num_gpus == 1, running single-shard dummy path, " + "if you want to set the number of GPUs to use for distributed PDLP, set the " + "parameter --distributed-pdlp-num-gpus" + << std::endl; } - cuopt_expects(num_gpus == settings.num_gpus /*&& settings.num_gpus > 1*/, + cuopt_expects(distributed_pdlp_num_gpus == settings.distributed_pdlp_num_gpus, error_type_t::ValidationError, - "This constructor should only be used for distributed PDLP (num_gpus > 1)"); + "This constructor's distributed_pdlp_num_gpus argument must match " + "settings.distributed_pdlp_num_gpus"); // Distributed PDLP is currently double-only if constexpr (!std::is_same_v) { cuopt_expects(false, error_type_t::ValidationError, - "Distributed PDLP (num_gpus > 1) currently requires double precision"); + "Distributed PDLP currently requires double precision"); return; } else { // 2. Load or compute partition @@ -405,20 +411,21 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, validate_partition(parts, op_problem_scaled_.n_constraints, op_problem_scaled_.n_variables, - num_gpus, + distributed_pdlp_num_gpus, "partition file"); } else { - if (num_gpus == 1) { + if (distributed_pdlp_num_gpus == 1) { // Single-part dummy run: useful for exercising the mGPU code paths on a // single physical GPU without a real partition file. - std::cout << "CAREFUL: num_gpus == 1, running dummy version (single part covering " + std::cout << "CAREFUL: distributed_pdlp_num_gpus == 1, running dummy version (single " + "part covering " << op_problem_scaled_.n_constraints << " cstrs + " << op_problem_scaled_.n_variables << " vars)" << std::endl; } partitioner_input_t partition_input; partition_input.nb_cstr = op_problem_scaled_.n_constraints; partition_input.nb_vars = op_problem_scaled_.n_variables; - partition_input.nb_parts = num_gpus; + partition_input.nb_parts = distributed_pdlp_num_gpus; // Dummy partitioner ignores A / A_t for now; future METIS partitioners will // fill these CSR views before calling partition(). auto partitioner = make_partitioner(partitioner_kind_t::Dummy); @@ -538,7 +545,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, h_A_t_col_indices, h_A_t_values, h_A_t_values_scaled, - settings.num_gpus, + settings.distributed_pdlp_num_gpus, n_cstr, n_vars, nnz); @@ -546,6 +553,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, // 7. Build the per-shard PDLP settings: pdlp_solver_settings_t sub_pdlp_settings = settings; sub_pdlp_settings.num_gpus = 1; + sub_pdlp_settings.distributed_pdlp_num_gpus = 1; sub_pdlp_settings.multi_gpu_partition_file = ""; sub_pdlp_settings.is_distributed_sub_pdlp = true; sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 15ddfdaad3..14651eab3f 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -66,7 +66,7 @@ class pdlp_solver_t { // Distributed Solver Constructor pdlp_solver_t(problem_t& op_problem, pdlp_solver_settings_t const& settings, - int num_gpus); + int distributed_pdlp_num_gpus); optimization_problem_solution_t run_solver(const timer_t& timer); diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index e401ab35b6..338083f03a 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -771,16 +771,29 @@ static optimization_problem_solution_t run_pdlp_solver( } #endif if (settings.hyper_params.use_distributed_pdlp) { - /* - cuopt_expects(settings.num_gpus > 1, + // Resolve the -1 "auto-detect" sentinel to the actual visible-device count on + // the master process + pdlp_solver_settings_t settings_resolved = settings; + if (settings_resolved.distributed_pdlp_num_gpus == -1) { + settings_resolved.distributed_pdlp_num_gpus = raft::device_setter::get_device_count(); + CUOPT_LOG_INFO("distributed_pdlp_num_gpus == -1: auto-detected %d visible CUDA device", + settings_resolved.distributed_pdlp_num_gpus); + } + cuopt_expects(settings_resolved.distributed_pdlp_num_gpus >= 1, error_type_t::ValidationError, - "use_distributed_pdlp requires settings.num_gpus > 1"); */ - if (settings.num_gpus == 1) {std::cout << "CAREFUL: use_distributed_pdlp requires settings.num_gpus > 1" << std::endl;} + "distributed_pdlp_num_gpus must be >= 1 or -1 (auto-detect)"); + if (settings_resolved.distributed_pdlp_num_gpus == 1) { + std::cout + << "CAREFUL: use_distributed_pdlp with distributed_pdlp_num_gpus == 1 runs the " + "single-shard dummy path" + << std::endl; + } cuopt_expects(!is_batch_mode, error_type_t::ValidationError, "Distributed PDLP does not support batch mode"); - // Multi-GPU ctor; dispatched by 3rd-arg TYPE (int num_gpus, not bool batch). - detail::pdlp_solver_t solver(problem, settings, settings.num_gpus); + // Multi-GPU ctor; dispatched by 3rd-arg TYPE (int, not bool batch). + detail::pdlp_solver_t solver( + problem, settings_resolved, settings_resolved.distributed_pdlp_num_gpus); return solver.run_solver(timer); } detail::pdlp_solver_t solver(problem, settings, is_batch_mode); From 4faa7df79320fc5588796e6828642bce523ea726 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 29 May 2026 07:20:12 -0700 Subject: [PATCH 050/258] added disable_graph flag, afiro gets solved on non-graph just as if it was single --- .../cuopt/linear_programming/constants.h | 1 + .../pdlp/pdlp_hyper_params.cuh | 3 +++ cpp/src/math_optimization/solver_settings.cu | 1 + cpp/src/pdlp/solve.cu | 3 +++ cpp/src/pdlp/utilities/ping_pong_graph.cuh | 17 ++++++++++++++++- 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 3346ab3565..e695bb21d3 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -86,6 +86,7 @@ #define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" #define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" #define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" +#define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" #define CUOPT_PRESOLVE_FILE "presolve_file" #define CUOPT_RANDOM_SEED "random_seed" diff --git a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh index 962f06ee4a..c68dc86d6a 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh +++ b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh @@ -48,6 +48,9 @@ struct pdlp_hyper_params_t { bool use_reflected_primal_dual = true; bool use_fixed_point_error = true; bool use_distributed_pdlp = false; + // Debug/diagnostic knob: when true, PDLP bypasses CUDA-graph capture in + // ping_pong_graph_t and executes each iteration eagerly + bool pdlp_disable_graph = false; double reflection_coefficient = 1.0; double restart_k_p = 0.99; double restart_k_i = 0.01; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 207e53f20d..629c8a8428 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -179,6 +179,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, {CUOPT_MIP_PROBING, &mip_settings.probing, true}, {CUOPT_USE_DISTRIBUTED_PDLP, &pdlp_settings.hyper_params.use_distributed_pdlp, false}, + {CUOPT_PDLP_DISABLE_GRAPH, &pdlp_settings.hyper_params.pdlp_disable_graph, false}, }; // String parameters string_parameters = { diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 338083f03a..70c488e3f3 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -756,6 +756,9 @@ static optimization_problem_solution_t run_pdlp_solver( const timer_t& timer, bool is_batch_mode) { + detail::pdlp_graph_disabled_flag().store(settings.hyper_params.pdlp_disable_graph, + std::memory_order_relaxed); + if (problem.n_constraints == 0) { CUOPT_LOG_CONDITIONAL_INFO( !settings.inside_mip, diff --git a/cpp/src/pdlp/utilities/ping_pong_graph.cuh b/cpp/src/pdlp/utilities/ping_pong_graph.cuh index dbc8fe5828..6b527f81b2 100644 --- a/cpp/src/pdlp/utilities/ping_pong_graph.cuh +++ b/cpp/src/pdlp/utilities/ping_pong_graph.cuh @@ -12,10 +12,25 @@ #include +#include #include namespace cuopt::linear_programming::detail { +// Debug/diagnostic toggle: when set, ping_pong_graph_t::run() bypasses CUDA +// graph capture and executes its work eagerly on every iteration. Useful for +// for debugging +inline std::atomic& pdlp_graph_disabled_flag() +{ + static std::atomic s_flag{false}; + return s_flag; +} + +inline bool pdlp_graph_disabled() +{ + return pdlp_graph_disabled_flag().load(std::memory_order_relaxed); +} + // Two-slot CUDA-graph cache for PDLP. PDLP swaps pointers (rather than // copying vectors) at the end of adaptive pdhg step, so the captured graph // topology alternates between two layouts depending on iteration parity. @@ -49,7 +64,7 @@ class ping_pong_graph_t { #ifdef CUPDLP_DEBUG_MODE work(); #else - if (is_legacy_batch_mode_) { + if (is_legacy_batch_mode_ || pdlp_graph_disabled()) { work(); return; } From 61acddb5cd0c0df8f09086d87264759e66ac94dd Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 31 May 2026 10:02:48 -0700 Subject: [PATCH 051/258] makes reductions in compute interraction adn movement use owned_size rather than total size hehehehe --- cpp/src/pdlp/pdlp.cu | 5 ++++- .../adaptive_step_size_strategy.cu | 18 ++++++++++++++---- .../adaptive_step_size_strategy.hpp | 6 +++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index a061a2d468..3b77a1cf47 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2327,10 +2327,13 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte cusparseDnVecSetValues(sub_cv.potential_next_dual_solution, (void*)sub_pdlp.pdhg_solver_.get_reflected_dual().data())); + // Ensure norm is on owned size sub_pdlp.step_size_strategy_.compute_interaction_and_movement( sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), sub_cv, - sub_pdlp.pdhg_solver_.get_saddle_point_state()); + sub_pdlp.pdhg_solver_.get_saddle_point_state(), + shard->rank_data.owned_var_size, + shard->rank_data.owned_cstr_size); RAFT_CUSPARSE_TRY(cusparseDnVecSetValues( sub_cv.potential_next_dual_solution, diff --git a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu index 2cb843ae86..530a426117 100644 --- a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu +++ b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu @@ -364,8 +364,18 @@ template void adaptive_step_size_strategy_t::compute_interaction_and_movement( rmm::device_uvector& tmp_primal, cusparse_view_t& cusparse_view, - saddle_point_state_t& current_saddle_point_state) + saddle_point_state_t& current_saddle_point_state, + i_t owned_primal_size, + i_t owned_cstr_size) { + // mGPU needs to know owned size to restrict the reductions to the owned prefix + const i_t reduce_primal_size = (owned_primal_size >= 0) + ? owned_primal_size + : current_saddle_point_state.get_primal_size(); + const i_t reduce_dual_size = (owned_cstr_size >= 0) + ? owned_cstr_size + : current_saddle_point_state.get_dual_size(); + // QP would need this: // if iszero(problem.objective_matrix) // primal_objective_interaction = 0.0 @@ -444,7 +454,7 @@ void adaptive_step_size_strategy_t::compute_interaction_and_movement( // compute interaction (x'-x) . (A(y'-y)) RAFT_CUBLAS_TRY( raft::linalg::detail::cublasdot(handle_ptr_->get_cublas_handle(), - current_saddle_point_state.get_primal_size(), + reduce_primal_size, tmp_primal.data(), primal_stride, current_saddle_point_state.get_delta_primal().data(), @@ -462,7 +472,7 @@ void adaptive_step_size_strategy_t::compute_interaction_and_movement( // norm(delta_dual) ^ 2; RAFT_CUBLAS_TRY( raft::linalg::detail::cublasdot(handle_ptr_->get_cublas_handle(), - current_saddle_point_state.get_primal_size(), + reduce_primal_size, current_saddle_point_state.get_delta_primal().data(), primal_stride, current_saddle_point_state.get_delta_primal().data(), @@ -472,7 +482,7 @@ void adaptive_step_size_strategy_t::compute_interaction_and_movement( RAFT_CUBLAS_TRY( raft::linalg::detail::cublasdot(handle_ptr_->get_cublas_handle(), - current_saddle_point_state.get_dual_size(), + reduce_dual_size, current_saddle_point_state.get_delta_dual().data(), dual_stride, current_saddle_point_state.get_delta_dual().data(), diff --git a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp index 896c6fa24e..238735e8ff 100644 --- a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp +++ b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.hpp @@ -88,9 +88,13 @@ class adaptive_step_size_strategy_t { rmm::device_uvector& get_norm_squared_delta_primal(); rmm::device_uvector& get_norm_squared_delta_dual(); + // owned_primal_size / owned_cstr_size are mGPU overrides. + // mGPU needs to know owned size to restrict the reductions to the owned prefix void compute_interaction_and_movement(rmm::device_uvector& tmp_primal, cusparse_view_t& cusparse_view, - saddle_point_state_t& current_saddle_point_state); + saddle_point_state_t& current_saddle_point_state, + i_t owned_primal_size = -1, + i_t owned_cstr_size = -1); void swap_context(const thrust::universal_host_pinned_vector>& swap_pairs); void resize_context(i_t new_size); From b8b59bfce89a26652d809dc2b9966d20febc28ef Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 31 May 2026 11:49:27 -0700 Subject: [PATCH 052/258] added emtis partitionner, still need it in the env. it is FAST. but we lose a lot of time on actal partitionning and data movements. Everything seems to be working --- cpp/CMakeLists.txt | 37 +++++ cpp/src/pdlp/CMakeLists.txt | 1 + .../distributed_pdlp/metis_partitioner.cu | 142 ++++++++++++++++++ cpp/src/pdlp/distributed_pdlp/partitioner.cu | 3 + cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 2 +- cpp/src/pdlp/utilities/mgpu_trace.cuh | 52 +++++++ 6 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu create mode 100644 cpp/src/pdlp/utilities/mgpu_trace.cuh diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index da7d4a4d35..d27072bcf9 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -342,6 +342,42 @@ set_target_properties(nccl_external PROPERTIES ) message(STATUS "Using NCCL: ${NCCL_LIBRARY}") +# ################################################################################################## +# - METIS (graph partitioning for distributed PDLP) ----------------------------------------------- +# Found by searching CONDA_PREFIX first, then CUOPT_METIS_ROOT (cmake var or env) +# if the user wants to pull METIS from a different conda env / system path. +set(METIS_HINT_PREFIXES "") +if (DEFINED ENV{CONDA_PREFIX} AND NOT "$ENV{CONDA_PREFIX}" STREQUAL "") + list(APPEND METIS_HINT_PREFIXES "$ENV{CONDA_PREFIX}") +endif () +if (DEFINED CUOPT_METIS_ROOT AND NOT "${CUOPT_METIS_ROOT}" STREQUAL "") + list(APPEND METIS_HINT_PREFIXES "${CUOPT_METIS_ROOT}") +endif () +if (DEFINED ENV{CUOPT_METIS_ROOT} AND NOT "$ENV{CUOPT_METIS_ROOT}" STREQUAL "") + list(APPEND METIS_HINT_PREFIXES "$ENV{CUOPT_METIS_ROOT}") +endif () +find_path(METIS_INCLUDE_DIR + NAMES metis.h + HINTS ${METIS_HINT_PREFIXES} + PATH_SUFFIXES include +) +find_library(METIS_LIBRARY + NAMES metis libmetis + HINTS ${METIS_HINT_PREFIXES} + PATH_SUFFIXES lib lib64 +) +if (NOT METIS_INCLUDE_DIR OR NOT METIS_LIBRARY) + message(FATAL_ERROR "METIS not found. Looked in: ${METIS_HINT_PREFIXES}. " + "Install it via 'conda install -c conda-forge metis' in the active env, " + "or set CUOPT_METIS_ROOT to a prefix containing include/metis.h and lib/libmetis.{so,a}.") +endif () +add_library(metis_external UNKNOWN IMPORTED GLOBAL) +set_target_properties(metis_external PROPERTIES + IMPORTED_LOCATION "${METIS_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${METIS_INCLUDE_DIR}" +) +message(STATUS "Using METIS: ${METIS_LIBRARY}") + # ################################################################################################## # - gRPC and Protobuf setup ----------------------------------------------------------------------- @@ -605,6 +641,7 @@ target_link_libraries(cuopt PRIVATE ${CUOPT_PRIVATE_CUDA_LIBS} nccl_external + metis_external $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> ) diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index a6ef14e3ff..863cf20962 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -33,6 +33,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/multi_gpu_engine.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/metis_partitioner.cu ) # C and Python adapter files diff --git a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu new file mode 100644 index 0000000000..6ed80b0047 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include + +#include + +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +// Builds the bipartite constraint/variable graph induced by A and runs +// METIS_PartGraphKway to assign each of the (nb_cstr + nb_vars) nodes to a +// part in [0, nb_parts). Layout matches metis_tests: +// * nodes [0, nb_cstr) : constraint nodes +// * nodes [nb_cstr, nb_cstr+nb_vars): variable nodes +// * undirected edges from each A nonzero (one half via A, one via A_t) +// The output is consumed by partition_loader_t::create_rank_data_from_parts. +template +std::vector metis_partitioner_t::partition( + partitioner_input_t const& input) const +{ + cuopt_expects(input.nb_parts > 0, + error_type_t::ValidationError, + "metis_partitioner: nb_parts must be positive"); + cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, + error_type_t::ValidationError, + "metis_partitioner: invalid problem dimensions"); + + cuopt_expects(input.A.row_offsets != nullptr && input.A.col_indices != nullptr, + error_type_t::ValidationError, + "metis_partitioner: A.row_offsets and A.col_indices are required"); + cuopt_expects(input.A_t.row_offsets != nullptr && input.A_t.col_indices != nullptr, + error_type_t::ValidationError, + "metis_partitioner: A_t.row_offsets and A_t.col_indices are required"); + + auto const& A_offsets = *input.A.row_offsets; + auto const& A_cols = *input.A.col_indices; + auto const& A_t_offsets = *input.A_t.row_offsets; + auto const& A_t_cols = *input.A_t.col_indices; + + cuopt_expects(static_cast(A_offsets.size()) == input.nb_cstr + 1, + error_type_t::ValidationError, + "metis_partitioner: A.row_offsets size mismatch (expected nb_cstr+1)"); + cuopt_expects(static_cast(A_t_offsets.size()) == input.nb_vars + 1, + error_type_t::ValidationError, + "metis_partitioner: A_t.row_offsets size mismatch (expected nb_vars+1)"); + cuopt_expects(A_cols.size() == A_t_cols.size(), + error_type_t::ValidationError, + "metis_partitioner: A and A_t nnz mismatch"); + + const i_t nb_cstr = input.nb_cstr; + const i_t nb_vars = input.nb_vars; + const i_t nnz = static_cast(A_cols.size()); + const i_t nvtx = nb_cstr + nb_vars; + + // Bipartite CSR. Same construction as metis_tests/src/main.cpp: + // xadj has length nvtx + 1 + // adjncy has length 2 * nnz (each A nonzero contributes one half-edge + // from cstr side via A and one half-edge from var side via A_t) + std::vector xadj(nvtx + 1); + std::vector adjncy(2 * static_cast(nnz)); + + // cstr-side row offsets: A_offsets[0..nb_cstr] (no shift). + for (i_t i = 0; i <= nb_cstr; ++i) { xadj[i] = static_cast(A_offsets[i]); } + // var-side row offsets: A_t_offsets[0..nb_vars], shifted by +nnz so that + // they index into the second half of adjncy. + for (i_t i = 0; i <= nb_vars; ++i) { + xadj[nb_cstr + i] = static_cast(A_t_offsets[i]) + static_cast(nnz); + } + + // cstr-side neighbours: A_cols[i] shifted by +nb_cstr to index into the + // variable node block. + for (i_t k = 0; k < nnz; ++k) { + adjncy[k] = static_cast(A_cols[k]) + static_cast(nb_cstr); + } + // var-side neighbours: A_t_cols[i] already in [0, nb_cstr). + for (i_t k = 0; k < nnz; ++k) { + adjncy[nnz + k] = static_cast(A_t_cols[k]); + } + + idx_t metis_options[METIS_NOPTIONS]; + METIS_SetDefaultOptions(metis_options); + metis_options[METIS_OPTION_OBJTYPE] = METIS_OBJTYPE_CUT; + + idx_t metis_nvtx = static_cast(nvtx); + idx_t ncon = 1; + idx_t nparts = static_cast(input.nb_parts); + idx_t objval = 0; + std::vector metis_parts(nvtx); + + auto t0 = std::chrono::high_resolution_clock::now(); + const int status = METIS_PartGraphKway(&metis_nvtx, + &ncon, + xadj.data(), + adjncy.data(), + /*vwgt=*/nullptr, + /*vsize=*/nullptr, + /*adjwgt=*/nullptr, + &nparts, + /*tpwgts=*/nullptr, + /*ubvec=*/nullptr, + metis_options, + &objval, + metis_parts.data()); + auto t1 = std::chrono::high_resolution_clock::now(); + const double dt = std::chrono::duration(t1 - t0).count(); + cuopt_expects(status == METIS_OK, + error_type_t::RuntimeError, + "METIS_PartGraphKway failed (status=%d)", + status); + CUOPT_LOG_INFO( + "METIS partitioned bipartite graph: nvtx=%d nnz=%d nb_parts=%d edge_cut=%lld in %.3fs", + static_cast(nvtx), + static_cast(nnz), + static_cast(input.nb_parts), + static_cast(objval), + dt); + + std::vector parts(static_cast(nvtx)); + for (i_t i = 0; i < nvtx; ++i) { parts[i] = static_cast(metis_parts[i]); } + + validate_partition(parts, + static_cast(nb_cstr), + static_cast(nb_vars), + static_cast(input.nb_parts), + "metis_partitioner"); + return parts; +} + +template class metis_partitioner_t; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cu b/cpp/src/pdlp/distributed_pdlp/partitioner.cu index bdbfcacf06..4b809986ce 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cu @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include @@ -76,6 +77,8 @@ std::unique_ptr> make_partitioner(partitioner_kind_t kin switch (kind) { case partitioner_kind_t::Dummy: return std::make_unique>(); + case partitioner_kind_t::Metis: + return std::make_unique>(); } cuopt_expects(false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); return nullptr; diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index ee5798fd0b..82650ad805 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -36,7 +36,7 @@ struct partitioner_input_t { csr_host_view_t A_t{}; }; -enum class partitioner_kind_t { Dummy /*, Metis */ }; +enum class partitioner_kind_t { Dummy, Metis }; template class partitioner_i { diff --git a/cpp/src/pdlp/utilities/mgpu_trace.cuh b/cpp/src/pdlp/utilities/mgpu_trace.cuh new file mode 100644 index 0000000000..06a848b18e --- /dev/null +++ b/cpp/src/pdlp/utilities/mgpu_trace.cuh @@ -0,0 +1,52 @@ +/* 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 + +// Lightweight env-gated tracing for multi-GPU PDLP diagnosis. +// +// Enable by setting CUOPT_MGPU_TRACE=1 in the environment. +// All prints go to stderr (line-buffered + explicit flush) so they survive +// a CUDA hang and interleave with cuOpt's normal output. +// +// Usage: +// MGPU_TRACE("entering compute_At_y"); +// MGPU_TRACE_FMT("shard %d nnz=%lld", r, (long long)nnz); +// +// The guard reads the env var once on first use (thread-safe via static +// initialization) and the cost when disabled is a single load + branch. + +#include +#include + +namespace cuopt::linear_programming::detail { + +inline bool mgpu_trace_enabled() +{ + static const bool enabled = []() { + const char* v = std::getenv("CUOPT_MGPU_TRACE"); + return v != nullptr && v[0] != '\0' && v[0] != '0'; + }(); + return enabled; +} + +} // namespace cuopt::linear_programming::detail + +#define MGPU_TRACE(msg) \ + do { \ + if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ + std::fprintf(stderr, "[mgpu %s:%d] %s\n", __func__, __LINE__, (msg)); \ + std::fflush(stderr); \ + } \ + } while (0) + +#define MGPU_TRACE_FMT(fmt, ...) \ + do { \ + if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ + std::fprintf(stderr, "[mgpu %s:%d] " fmt "\n", __func__, __LINE__, __VA_ARGS__); \ + std::fflush(stderr); \ + } \ + } while (0) From 7d74e740ca3369ff10e9402573c1bed73dcae13a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 31 May 2026 11:53:44 -0700 Subject: [PATCH 053/258] forgot to push a file, maybe doesnt compile lol --- cpp/src/pdlp/pdlp.cu | 47 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 3b77a1cf47..d80adf248d 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -426,9 +426,50 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, partition_input.nb_cstr = op_problem_scaled_.n_constraints; partition_input.nb_vars = op_problem_scaled_.n_variables; partition_input.nb_parts = distributed_pdlp_num_gpus; - // Dummy partitioner ignores A / A_t for now; future METIS partitioners will - // fill these CSR views before calling partition(). - auto partitioner = make_partitioner(partitioner_kind_t::Dummy); + + // Topology buffers: only needed for METIS (Dummy ignores them). + // Read CSR offsets and col indices from the (unscaled) problem; the + // partitioner only needs topology, not values, and scaled/unscaled share + // the same nonzero pattern. + std::vector h_part_A_row_offsets; + std::vector h_part_A_col_indices; + std::vector h_part_A_t_row_offsets; + std::vector h_part_A_t_col_indices; + + const partitioner_kind_t kind = partitioner_kind_t::Metis; + if (kind == partitioner_kind_t::Metis) { + const auto stream = op_problem_scaled_.handle_ptr->get_stream(); + const i_t n_cstr = op_problem_scaled_.n_constraints; + const i_t n_vars = op_problem_scaled_.n_variables; + const i_t nnz = op_problem_scaled_.nnz; + h_part_A_row_offsets.resize(n_cstr + 1); + h_part_A_col_indices.resize(nnz); + h_part_A_t_row_offsets.resize(n_vars + 1); + h_part_A_t_col_indices.resize(nnz); + raft::copy( + h_part_A_row_offsets.data(), op_problem_scaled_.offsets.data(), n_cstr + 1, stream); + raft::copy( + h_part_A_col_indices.data(), op_problem_scaled_.variables.data(), nnz, stream); + raft::copy(h_part_A_t_row_offsets.data(), + op_problem_scaled_.reverse_offsets.data(), + n_vars + 1, + stream); + raft::copy(h_part_A_t_col_indices.data(), + op_problem_scaled_.reverse_constraints.data(), + nnz, + stream); + op_problem_scaled_.handle_ptr->sync_stream(stream); + + partition_input.A.row_offsets = &h_part_A_row_offsets; + partition_input.A.col_indices = &h_part_A_col_indices; + partition_input.A.num_rows = n_cstr; + partition_input.A.num_cols = n_vars; + partition_input.A_t.row_offsets = &h_part_A_t_row_offsets; + partition_input.A_t.col_indices = &h_part_A_t_col_indices; + partition_input.A_t.num_rows = n_vars; + partition_input.A_t.num_cols = n_cstr; + } + auto partitioner = make_partitioner(kind); parts = partitioner->partition(partition_input); } From 859a299b0e3b296957a6de64418b2807796b87db Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 1 Jun 2026 10:43:00 +0200 Subject: [PATCH 054/258] fixed dummy partitionner on single gpu --- cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu | 8 ++++++++ cpp/src/pdlp/pdlp.cu | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu index 6ed80b0047..73e2736251 100644 --- a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu @@ -32,6 +32,14 @@ std::vector metis_partitioner_t::partition( cuopt_expects(input.nb_parts > 0, error_type_t::ValidationError, "metis_partitioner: nb_parts must be positive"); + // METIS_PartGraphKway internally does integer arithmetic of the form + // `nedges / nparts` and traps with SIGFPE when nparts == 1. The single-part + // case is also trivial (everything in part 0) so callers should route it to + // the Dummy partitioner instead (see pdlp_solver_t mGPU ctor). + cuopt_expects(input.nb_parts >= 2, + error_type_t::ValidationError, + "metis_partitioner: nb_parts must be >= 2 (METIS_PartGraphKway requirement); " + "use the Dummy partitioner for the single-shard case"); cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, error_type_t::ValidationError, "metis_partitioner: invalid problem dimensions"); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index d80adf248d..a747706639 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -436,7 +436,13 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, std::vector h_part_A_t_row_offsets; std::vector h_part_A_t_col_indices; - const partitioner_kind_t kind = partitioner_kind_t::Metis; + // METIS_PartGraphKway requires nparts >= 2; calling it with nparts == 1 + // traps inside METIS (SIGFPE on integer division by zero). The + // num_gpus == 1 path is the single-shard dummy run anyway -- there's + // nothing for METIS to do, so route directly to Dummy which just places + // every vertex into part 0. + const partitioner_kind_t kind = + (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::Metis; if (kind == partitioner_kind_t::Metis) { const auto stream = op_problem_scaled_.handle_ptr->get_stream(); const i_t n_cstr = op_problem_scaled_.n_constraints; From 7daa7400e1f1b7a421a8ac9f9fbbba3d42489c16 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 1 Jun 2026 11:34:16 +0200 Subject: [PATCH 055/258] added some plumbing, will not load full problem on gpu --- cpp/src/pdlp/solve.cu | 47 ++++++++++++++++++++++++++++++++++++++++++ cpp/src/pdlp/solve.cuh | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 70c488e3f3..8081c42ffb 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2143,10 +2143,49 @@ optimization_problem_solution_t solve_lp( bool problem_checking, bool use_pdlp_solver_mode) { + // In distributed PDLP we can't allocate the full problem on the master device + if (settings.hyper_params.use_distributed_pdlp) { + return solve_lp_distributed_from_mps( + handle_ptr, mps_data_model, settings, problem_checking, use_pdlp_solver_mode); + } auto op_problem = mps_data_model_to_optimization_problem(handle_ptr, mps_data_model); return solve_lp(op_problem, settings, problem_checking, use_pdlp_solver_mode); } +template +optimization_problem_solution_t solve_lp_distributed_from_mps( + raft::handle_t const* handle_ptr, + const cuopt::linear_programming::io::mps_data_model_t& mps_data_model, + pdlp_solver_settings_t const& settings, + bool problem_checking, + bool use_pdlp_solver_mode) +{ + cuopt_expects(handle_ptr != nullptr, + error_type_t::ValidationError, + "solve_lp_distributed_from_mps: handle_ptr must not be null"); + cuopt_expects(settings.hyper_params.use_distributed_pdlp, + error_type_t::ValidationError, + "solve_lp_distributed_from_mps: settings.hyper_params.use_distributed_pdlp " + "must be true"); + + pdlp_solver_settings_t settings_resolved = settings; + if (settings_resolved.distributed_pdlp_num_gpus == -1) { + settings_resolved.distributed_pdlp_num_gpus = raft::device_setter::get_device_count(); + CUOPT_LOG_INFO( + "solve_lp_distributed_from_mps: distributed_pdlp_num_gpus == -1, auto-detected " + "%d visible CUDA device(s)", + settings_resolved.distributed_pdlp_num_gpus); + } + if (settings_resolved.distributed_pdlp_num_gpus <= 1) + { + std::cout << "CAREFUL: use_distributed_pdlp with distributed_pdlp_num_gpus == 1 runs the " + "single-shard dummy path" + << std::endl; + } + auto op_problem = mps_data_model_to_optimization_problem(handle_ptr, mps_data_model); + return solve_lp(op_problem, settings_resolved, problem_checking, use_pdlp_solver_mode); +} + // ============================================================================ // CPU problem overloads (convert to GPU, solve, convert solution back) // ============================================================================ @@ -2287,6 +2326,14 @@ std::unique_ptr> solve_lp( template optimization_problem_t mps_data_model_to_optimization_problem( \ raft::handle_t const* handle_ptr, \ const cuopt::linear_programming::io::mps_data_model_t& data_model); \ + \ + template optimization_problem_solution_t solve_lp_distributed_from_mps( \ + raft::handle_t const* handle_ptr, \ + const cuopt::linear_programming::io::mps_data_model_t& mps_data_model, \ + pdlp_solver_settings_t const& settings, \ + bool problem_checking, \ + bool use_pdlp_solver_mode); \ + \ template void set_pdlp_solver_mode(pdlp_solver_settings_t& settings); #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/pdlp/solve.cuh b/cpp/src/pdlp/solve.cuh index 90e5e4fe95..abb657943f 100644 --- a/cpp/src/pdlp/solve.cuh +++ b/cpp/src/pdlp/solve.cuh @@ -32,6 +32,46 @@ cuopt::linear_programming::optimization_problem_solution_t solve_lp_wi const timer_t& timer, bool is_batch_mode = false); +/** + * @brief Distributed-PDLP entry point that consumes the host MPS data model + * directly, without ever materializing the full problem on a single + * (master) GPU. + * + * This is the entry point intended for problems whose `nnz` exceeds the memory + * of a single device. Today (Step 1 of the mGPU memory refactor) it is a thin + * routing shim: it resolves `distributed_pdlp_num_gpus == -1` against the + * visible-device count and delegates to the legacy + * `mps_data_model_to_optimization_problem(...)` + device-side `solve_lp(...)` + * pipeline, which still allocates the full problem on master. The shim exists + * so the public-facing call site is already in place; subsequent commits will + * replace the body with: + * 1. host-side METIS partitioning straight off the MPS CSR + * 2. per-shard host CSR slicing + * 3. construction of an mGPU-native pdlp_solver_t whose master only holds + * scalar metadata + gather buffers (no full A / A^T / scaled copies). + * + * Until then, behaviour and memory footprint are identical to the legacy path. + * + * @param handle_ptr Master raft handle (its stream owns the gather buffers + * and any master-side aggregator allocations). + * @param mps_data_model Host-resident MPS data (CPU vectors only). + * @param settings User-supplied PDLP solver settings; the + * `distributed_pdlp_num_gpus == -1` sentinel is resolved + * here against the visible-device count. + * @param problem_checking Forwarded to the eventual solver. + * @param use_pdlp_solver_mode Forwarded to the eventual solver. + * + * @pre `settings.hyper_params.use_distributed_pdlp == true`. + */ +template +cuopt::linear_programming::optimization_problem_solution_t +solve_lp_distributed_from_mps( + raft::handle_t const* handle_ptr, + const cuopt::linear_programming::io::mps_data_model_t& mps_data_model, + pdlp_solver_settings_t const& settings, + bool problem_checking, + bool use_pdlp_solver_mode); + /** * @brief Entry point for batch PDLP. Solves multiple LPs sharing the same constraint * matrix structure in a single batched GPU run. From 8a39e8c9e1b62cff57b09e80c013ae6ee53e30d4 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 1 Jun 2026 13:53:49 +0200 Subject: [PATCH 056/258] added guard to ensure presolver is not supported in mGPU --- cpp/src/pdlp/solve.cu | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 8081c42ffb..b32bad87f8 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2167,6 +2167,10 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( error_type_t::ValidationError, "solve_lp_distributed_from_mps: settings.hyper_params.use_distributed_pdlp " "must be true"); + cuopt_expects(settings.presolver == cuopt::linear_programming::presolver_t::None, + error_type_t::ValidationError, + "solve_lp_distributed_from_mps: presolve is not yet supported with " + "use_distributed_pdlp; please set settings.presolver = presolver_t::None"); pdlp_solver_settings_t settings_resolved = settings; if (settings_resolved.distributed_pdlp_num_gpus == -1) { From 5a3b9ce521ac23d10a2356bcb2bb5413c66e98e0 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 2 Jun 2026 10:41:06 +0200 Subject: [PATCH 057/258] plumbed pdlp_distributed_solver with mps_data_model and now data doesnt transit on master device ! --- cpp/src/pdlp/pdlp.cu | 327 +++++++++++++++++------------------------- cpp/src/pdlp/pdlp.cuh | 7 +- cpp/src/pdlp/solve.cu | 104 ++++++++++---- 3 files changed, 211 insertions(+), 227 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index a747706639..21291b853d 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -17,6 +17,7 @@ #include #include +#include #include #include "cuopt/linear_programming/pdlp/solver_solution.hpp" #include "distributed_pdlp/multi_gpu_engine.hpp" @@ -375,15 +376,28 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, } } +// ============================================================================ +// Distributed multi-GPU ctor. +// needs placeholder_problem to be a shape-0 problem +// reads the problem from mps_data_model directly +// builds internal attributes from the placeholder_problem +// builds the engine from the mps_data_model template -pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, - pdlp_solver_settings_t const& settings, - int distributed_pdlp_num_gpus) - // 1. Delegate to single-GPU ctor to bring up all the per-master state - // (problem_ptr, op_problem_scaled_, pdhg_solver_, strategies, etc.). - : pdlp_solver_t(op_problem, settings, /*is_legacy_batch_mode=*/false) +pdlp_solver_t::pdlp_solver_t( + problem_t& placeholder_problem, + cuopt::linear_programming::io::mps_data_model_t const& mps, + pdlp_solver_settings_t const& settings) + // Makes all inner feilds of master 0 size + : pdlp_solver_t(placeholder_problem, settings, /*is_legacy_batch_mode=*/false) { - CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU", + cuopt_expects(placeholder_problem.n_variables == 0 && + placeholder_problem.n_constraints == 0 && + placeholder_problem.nnz == 0, + error_type_t::ValidationError, + "Distributed mGPU pdlp_solver_t ctor requires a shape-0 " + "placeholder problem (n_variables == n_constraints == nnz == 0)"); + const int distributed_pdlp_num_gpus = settings.distributed_pdlp_num_gpus; + CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU (mps direct path)", distributed_pdlp_num_gpus); if (distributed_pdlp_num_gpus == 1) { std::cout << "CAREFUL !!: distributed_pdlp_num_gpus == 1, running single-shard dummy path, " @@ -391,87 +405,125 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, "parameter --distributed-pdlp-num-gpus" << std::endl; } - cuopt_expects(distributed_pdlp_num_gpus == settings.distributed_pdlp_num_gpus, - error_type_t::ValidationError, - "This constructor's distributed_pdlp_num_gpus argument must match " - "settings.distributed_pdlp_num_gpus"); - // Distributed PDLP is currently double-only if constexpr (!std::is_same_v) { cuopt_expects(false, error_type_t::ValidationError, "Distributed PDLP currently requires double precision"); return; - } else { - // 2. Load or compute partition + } + // ----- 1. Read problem shape and bulk data directly from mps (host) ----- + const i_t n_vars = static_cast(mps.get_objective_coefficients().size()); + const i_t n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); + const i_t nnz = static_cast(mps.get_constraint_matrix_values().size()); + cuopt_expects(n_vars > 0, + error_type_t::ValidationError, + "Distributed PDLP from mps requires a non-empty objective"); + cuopt_expects(n_cstr > 0, + error_type_t::ValidationError, + "Distributed PDLP from mps requires at least one constraint"); + cuopt_expects(static_cast(mps.get_constraint_matrix_offsets().size()) == n_cstr + 1, + error_type_t::ValidationError, + "mps constraint_matrix_offsets size must equal n_constraints + 1"); + cuopt_expects( + static_cast(mps.get_constraint_matrix_indices().size()) == nnz, + error_type_t::ValidationError, + "mps constraint_matrix_indices size must equal nnz (constraint_matrix_values size)"); + cuopt_expects(static_cast(mps.get_constraint_upper_bounds().size()) == n_cstr, + error_type_t::ValidationError, + "mps constraint_upper_bounds size must equal n_constraints"); + cuopt_expects(static_cast(mps.get_variable_lower_bounds().size()) == n_vars, + error_type_t::ValidationError, + "mps variable_lower_bounds size must equal n_variables"); + cuopt_expects(static_cast(mps.get_variable_upper_bounds().size()) == n_vars, + error_type_t::ValidationError, + "mps variable_upper_bounds size must equal n_variables"); + + const bool maximize = mps.get_sense(); + f_t objective_offset = mps.get_objective_offset(); + f_t objective_scaling_factor = mps.get_objective_scaling_factor(); + + // Objective: copy (mutable so we can negate for maximize, matching + // problem_helpers.cuh::convert_to_maximization_problem). + std::vector h_obj = mps.get_objective_coefficients(); + if (maximize) { + for (auto& v : h_obj) v = -v; + objective_offset = -objective_offset; + objective_scaling_factor = -objective_scaling_factor; + } + + // Bounds (copy from mps; engine ctor takes by const ref to std::vector). + std::vector h_var_lower = mps.get_variable_lower_bounds(); + std::vector h_var_upper = mps.get_variable_upper_bounds(); + std::vector h_cstr_lower = mps.get_constraint_lower_bounds(); + std::vector h_cstr_upper = mps.get_constraint_upper_bounds(); + + // A (CSR) — mutable copies for the engine + partitioner consumers below. + std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); + std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); + std::vector h_A_values = mps.get_constraint_matrix_values(); + + // ----- 2. Transpose A -> A^T on the host (one-shot CSR transpose) ----- + // CSC(A) and CSR(A^T) share the same memory layout, so the CSC produced + // by dual_simplex::csr_matrix_t::to_compressed_col IS the CSR of A^T. + // O(nnz + n_vars) counting sort, same as problem_t::compute_transpose. + namespace ds = cuopt::linear_programming::dual_simplex; + ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); + A_csr.row_start = h_A_row_offsets; + A_csr.j = h_A_col_indices; + A_csr.x = h_A_values; + ds::csc_matrix_t AT_as_csc(n_vars, n_cstr, nnz); + A_csr.to_compressed_col(AT_as_csc); + std::vector h_A_t_row_offsets = std::move(AT_as_csc.col_start); + std::vector h_A_t_col_indices = std::move(AT_as_csc.i); + std::vector h_A_t_values = std::move(AT_as_csc.x); + + // ----- 3. Identity scaling for V1 ----- + // Real multi-GPU scaling is a TODO; ship the unscaled problem to shards as + // both "unscaled" and "scaled" so the engine and per-shard pdlp_solver_t + // can run end-to-end. Scaling factor vectors are 1.0 everywhere so the + // shard-side unscale at the end is a no-op. + std::vector h_A_values_scaled = h_A_values; + std::vector h_A_t_values_scaled = h_A_t_values; + std::vector h_obj_scaled = h_obj; + std::vector h_var_lower_scaled = h_var_lower; + std::vector h_var_upper_scaled = h_var_upper; + std::vector h_cstr_lower_scaled = h_cstr_lower; + std::vector h_cstr_upper_scaled = h_cstr_upper; + std::vector h_cummulative_cstr_scaling(n_cstr, f_t(1.0)); + std::vector h_cummulative_var_scaling(n_vars, f_t(1.0)); + const f_t h_bound_rescaling = f_t(1.0); + const f_t h_objective_rescaling = f_t(1.0); + + // ----- 4. Partition ----- std::vector parts; if (!settings.multi_gpu_partition_file.empty()) { parts = partition_loader_t::parse_distributed_pdlp_partition_file( settings.multi_gpu_partition_file); - validate_partition(parts, - op_problem_scaled_.n_constraints, - op_problem_scaled_.n_variables, - distributed_pdlp_num_gpus, - "partition file"); + validate_partition(parts, n_cstr, n_vars, distributed_pdlp_num_gpus, "partition file"); } else { if (distributed_pdlp_num_gpus == 1) { - // Single-part dummy run: useful for exercising the mGPU code paths on a - // single physical GPU without a real partition file. std::cout << "CAREFUL: distributed_pdlp_num_gpus == 1, running dummy version (single " "part covering " - << op_problem_scaled_.n_constraints << " cstrs + " - << op_problem_scaled_.n_variables << " vars)" << std::endl; + << n_cstr << " cstrs + " << n_vars << " vars)" << std::endl; } partitioner_input_t partition_input; - partition_input.nb_cstr = op_problem_scaled_.n_constraints; - partition_input.nb_vars = op_problem_scaled_.n_variables; + partition_input.nb_cstr = n_cstr; + partition_input.nb_vars = n_vars; partition_input.nb_parts = distributed_pdlp_num_gpus; - // Topology buffers: only needed for METIS (Dummy ignores them). - // Read CSR offsets and col indices from the (unscaled) problem; the - // partitioner only needs topology, not values, and scaled/unscaled share - // the same nonzero pattern. - std::vector h_part_A_row_offsets; - std::vector h_part_A_col_indices; - std::vector h_part_A_t_row_offsets; - std::vector h_part_A_t_col_indices; - - // METIS_PartGraphKway requires nparts >= 2; calling it with nparts == 1 - // traps inside METIS (SIGFPE on integer division by zero). The - // num_gpus == 1 path is the single-shard dummy run anyway -- there's - // nothing for METIS to do, so route directly to Dummy which just places - // every vertex into part 0. + // METIS_PartGraphKway requires nparts >= 2; route num_gpus == 1 to Dummy. const partitioner_kind_t kind = (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::Metis; if (kind == partitioner_kind_t::Metis) { - const auto stream = op_problem_scaled_.handle_ptr->get_stream(); - const i_t n_cstr = op_problem_scaled_.n_constraints; - const i_t n_vars = op_problem_scaled_.n_variables; - const i_t nnz = op_problem_scaled_.nnz; - h_part_A_row_offsets.resize(n_cstr + 1); - h_part_A_col_indices.resize(nnz); - h_part_A_t_row_offsets.resize(n_vars + 1); - h_part_A_t_col_indices.resize(nnz); - raft::copy( - h_part_A_row_offsets.data(), op_problem_scaled_.offsets.data(), n_cstr + 1, stream); - raft::copy( - h_part_A_col_indices.data(), op_problem_scaled_.variables.data(), nnz, stream); - raft::copy(h_part_A_t_row_offsets.data(), - op_problem_scaled_.reverse_offsets.data(), - n_vars + 1, - stream); - raft::copy(h_part_A_t_col_indices.data(), - op_problem_scaled_.reverse_constraints.data(), - nnz, - stream); - op_problem_scaled_.handle_ptr->sync_stream(stream); - - partition_input.A.row_offsets = &h_part_A_row_offsets; - partition_input.A.col_indices = &h_part_A_col_indices; + // partitioner_input_t holds non-const std::vector* pointers; we + // already have the data in our local mutable buffers above. + partition_input.A.row_offsets = &h_A_row_offsets; + partition_input.A.col_indices = &h_A_col_indices; partition_input.A.num_rows = n_cstr; partition_input.A.num_cols = n_vars; - partition_input.A_t.row_offsets = &h_part_A_t_row_offsets; - partition_input.A_t.col_indices = &h_part_A_t_col_indices; + partition_input.A_t.row_offsets = &h_A_t_row_offsets; + partition_input.A_t.col_indices = &h_A_t_col_indices; partition_input.A_t.num_rows = n_vars; partition_input.A_t.num_cols = n_cstr; } @@ -479,109 +531,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, parts = partitioner->partition(partition_input); } - // always compute initial step size before scaling and primal_weight after scaling to do like - // cuPDLPx - assert(settings_.hyper_params.compute_initial_primal_weight_before_scaling && - "compute_initial_primal_weight_before_scaling must be true in distributed mode"); - assert(!settings_.hyper_params.compute_initial_step_size_before_scaling && - "compute_initial_step_size_before_scaling must be false in distributed mode"); - - compute_initial_primal_weight(); - - // scale globally before dispatching to shards - initial_scaling_strategy_.scale_problem(); - - compute_initial_step_size(); - step_size_strategy_.get_primal_and_dual_stepsizes(primal_step_size_, dual_step_size_); - - const f_t initial_step_size_global = get_step_size_h(0); - const f_t initial_primal_weight_global = get_primal_weight_h(0); - - // 4. Copy both scaled and unscaled pb - auto const stream = op_problem_scaled_.handle_ptr->get_stream(); - i_t const n_cstr = op_problem_scaled_.n_constraints; - i_t const n_vars = op_problem_scaled_.n_variables; - i_t const nnz = op_problem_scaled_.nnz; - - // Shared topology (taken from the scaled problem, but identical on both). - std::vector h_A_row_offsets(n_cstr + 1); - std::vector h_A_col_indices(nnz); - std::vector h_A_t_row_offsets(n_vars + 1); - std::vector h_A_t_col_indices(nnz); - raft::copy(h_A_row_offsets.data(), op_problem_scaled_.offsets.data(), n_cstr + 1, stream); - raft::copy(h_A_col_indices.data(), op_problem_scaled_.variables.data(), nnz, stream); - raft::copy( - h_A_t_row_offsets.data(), op_problem_scaled_.reverse_offsets.data(), n_vars + 1, stream); - raft::copy( - h_A_t_col_indices.data(), op_problem_scaled_.reverse_constraints.data(), nnz, stream); - - // Paired value arrays for A and A_T. - std::vector h_A_values(nnz); - std::vector h_A_values_scaled(nnz); - std::vector h_A_t_values(nnz); - std::vector h_A_t_values_scaled(nnz); - raft::copy(h_A_values.data(), problem_ptr->coefficients.data(), nnz, stream); - raft::copy(h_A_t_values.data(), problem_ptr->reverse_coefficients.data(), nnz, stream); - raft::copy(h_A_values_scaled.data(), op_problem_scaled_.coefficients.data(), nnz, stream); - raft::copy( - h_A_t_values_scaled.data(), op_problem_scaled_.reverse_coefficients.data(), nnz, stream); - - using f_t2 = typename type_2::type; - - std::vector h_obj(n_vars); - std::vector h_obj_scaled(n_vars); - std::vector h_var_bounds_packed(n_vars); - std::vector h_var_bounds_scaled_packed(n_vars); - std::vector h_cstr_lower(n_cstr); - std::vector h_cstr_upper(n_cstr); - std::vector h_cstr_lower_scaled(n_cstr); - std::vector h_cstr_upper_scaled(n_cstr); - - raft::copy(h_obj.data(), problem_ptr->objective_coefficients.data(), n_vars, stream); - raft::copy( - h_obj_scaled.data(), op_problem_scaled_.objective_coefficients.data(), n_vars, stream); - raft::copy(h_var_bounds_packed.data(), problem_ptr->variable_bounds.data(), n_vars, stream); - raft::copy( - h_var_bounds_scaled_packed.data(), op_problem_scaled_.variable_bounds.data(), n_vars, stream); - raft::copy(h_cstr_lower.data(), problem_ptr->constraint_lower_bounds.data(), n_cstr, stream); - raft::copy(h_cstr_upper.data(), problem_ptr->constraint_upper_bounds.data(), n_cstr, stream); - raft::copy(h_cstr_lower_scaled.data(), - op_problem_scaled_.constraint_lower_bounds.data(), - n_cstr, - stream); - raft::copy(h_cstr_upper_scaled.data(), - op_problem_scaled_.constraint_upper_bounds.data(), - n_cstr, - stream); - - // 5. Get full scaling factors on host - std::vector h_cummulative_cstr_scaling(n_cstr); - std::vector h_cummulative_var_scaling(n_vars); - raft::copy(h_cummulative_cstr_scaling.data(), - initial_scaling_strategy_.get_constraint_matrix_scaling_vector().data(), - n_cstr, - stream); - raft::copy(h_cummulative_var_scaling.data(), - initial_scaling_strategy_.get_variable_scaling_vector().data(), - n_vars, - stream); - const f_t h_bound_rescaling = initial_scaling_strategy_.get_h_bound_rescaling(); - const f_t h_objective_rescaling = initial_scaling_strategy_.get_h_objective_rescaling(); - - op_problem_scaled_.handle_ptr->sync_stream(stream); - - // Unpack interleaved {lower, upper} into separate vectors for both - // versions, so the shard ctor's slicing loop is uniform. - std::vector h_var_lower(n_vars), h_var_upper(n_vars); - std::vector h_var_lower_scaled(n_vars), h_var_upper_scaled(n_vars); - for (i_t i = 0; i < n_vars; ++i) { - h_var_lower[i] = h_var_bounds_packed[i].x; - h_var_upper[i] = h_var_bounds_packed[i].y; - h_var_lower_scaled[i] = h_var_bounds_scaled_packed[i].x; - h_var_upper_scaled[i] = h_var_bounds_scaled_packed[i].y; - } - - // 6. Build per-rank data and meta-data. + // ----- 5. Build per-rank data ----- std::vector> sub_pdlp_rank_data = partition_loader_t::create_rank_data_from_parts(parts, h_A_row_offsets, @@ -597,7 +547,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, n_vars, nnz); - // 7. Build the per-shard PDLP settings: + // ----- 6. Per-shard settings ----- pdlp_solver_settings_t sub_pdlp_settings = settings; sub_pdlp_settings.num_gpus = 1; sub_pdlp_settings.distributed_pdlp_num_gpus = 1; @@ -606,7 +556,7 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; - // 8. Construct the engine, creates NCCL comms and shards + // ----- 7. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), h_obj, h_var_lower, @@ -622,13 +572,12 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, h_cummulative_var_scaling, h_bound_rescaling, h_objective_rescaling, - op_problem_scaled_.maximize, - op_problem_scaled_.objective_offset, - op_problem_scaled_.presolve_data.objective_scaling_factor, + maximize, + objective_offset, + objective_scaling_factor, sub_pdlp_settings); - // Copy to host and then to shards. - // More robust than cudaDeviceEnablePeerAccess and cost-free-ish. + // ----- 8. Seed shard step-size / primal-weight scalars from the master ----- f_t h_step_size{}, h_primal_weight{}, h_best_primal_weight{}; f_t h_primal_step_size{}, h_dual_step_size{}; raft::copy(&h_step_size, step_size_.data(), 1, stream_view_); @@ -648,27 +597,17 @@ pdlp_solver_t::pdlp_solver_t(problem_t& op_problem, raft::copy(sub.dual_step_size_.data(), &h_dual_step_size, 1, shard->stream); } - // Wire the engine into the master pdhg_solver_. Shards' pdhg_solver_ keep - // mgpu_engine_ == nullptr so they run plain single-GPU SpMV on local A. + // Wire the engine into master's pdhg_solver_; shards keep mgpu_engine_ == nullptr. pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); - // Project initial primal solution - if (settings_.hyper_params.project_initial_primal) { - // Use refine_initial_primal_projection ??? - using f_t2 = typename type_2::type; - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub = *shard->sub_pdlp; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(sub.pdhg_solver_.get_primal_solution().data(), - sub.get_op_problem_scaled().variable_bounds.data()), - sub.pdhg_solver_.get_primal_solution().data(), - sub.pdhg_solver_.get_primal_solution().size(), - clamp(), - shard->stream.view()); - } - } - } // end if constexpr (std::is_same_v) + // ----- 9. Resize master gather destinations to the full problem size ----- + pdhg_solver_.get_potential_next_primal_solution().resize(n_vars, stream_view_); + pdhg_solver_.get_potential_next_dual_solution().resize(n_cstr, stream_view_); + current_termination_strategy_.get_convergence_information().get_reduced_cost().resize( + n_vars, stream_view_); + primal_size_h_ = n_vars; + dual_size_h_ = n_cstr; + handle_ptr_->sync_stream(stream_view_); } template diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 14651eab3f..3544de89fa 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include @@ -64,9 +65,9 @@ class pdlp_solver_t { bool is_batch_mode = false); // Distributed Solver Constructor - pdlp_solver_t(problem_t& op_problem, - pdlp_solver_settings_t const& settings, - int distributed_pdlp_num_gpus); + pdlp_solver_t(problem_t& placeholder_problem, + cuopt::linear_programming::io::mps_data_model_t const& mps, + pdlp_solver_settings_t const& settings); optimization_problem_solution_t run_solver(const timer_t& timer); diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index b32bad87f8..ef273faf13 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -773,32 +773,15 @@ static optimization_problem_solution_t run_pdlp_solver( } } #endif - if (settings.hyper_params.use_distributed_pdlp) { - // Resolve the -1 "auto-detect" sentinel to the actual visible-device count on - // the master process - pdlp_solver_settings_t settings_resolved = settings; - if (settings_resolved.distributed_pdlp_num_gpus == -1) { - settings_resolved.distributed_pdlp_num_gpus = raft::device_setter::get_device_count(); - CUOPT_LOG_INFO("distributed_pdlp_num_gpus == -1: auto-detected %d visible CUDA device", - settings_resolved.distributed_pdlp_num_gpus); - } - cuopt_expects(settings_resolved.distributed_pdlp_num_gpus >= 1, - error_type_t::ValidationError, - "distributed_pdlp_num_gpus must be >= 1 or -1 (auto-detect)"); - if (settings_resolved.distributed_pdlp_num_gpus == 1) { - std::cout - << "CAREFUL: use_distributed_pdlp with distributed_pdlp_num_gpus == 1 runs the " - "single-shard dummy path" - << std::endl; - } - cuopt_expects(!is_batch_mode, - error_type_t::ValidationError, - "Distributed PDLP does not support batch mode"); - // Multi-GPU ctor; dispatched by 3rd-arg TYPE (int, not bool batch). - detail::pdlp_solver_t solver( - problem, settings_resolved, settings_resolved.distributed_pdlp_num_gpus); - return solver.run_solver(timer); - } + // Distributed PDLP cannot enter through this path: by the time we have a + // problem_t, the full problem already lives on the master GPU, which defeats + // the purpose of distributed mode. Callers must route to + // solve_lp_distributed_from_mps via solve_lp(mps_data_model, ...). + cuopt_expects(!settings.hyper_params.use_distributed_pdlp, + error_type_t::ValidationError, + "Distributed PDLP must be entered via solve_lp(mps_data_model, ...) " + "so the master GPU never materializes the full problem. Call sites " + "with a problem_t cannot dispatch to distributed mode."); detail::pdlp_solver_t solver(problem, settings, is_batch_mode); if (settings.inside_mip) { solver.set_inside_mip(true); } return solver.run_solver(timer); @@ -2180,14 +2163,75 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( "%d visible CUDA device(s)", settings_resolved.distributed_pdlp_num_gpus); } - if (settings_resolved.distributed_pdlp_num_gpus <= 1) - { + if (settings_resolved.distributed_pdlp_num_gpus <= 1) { std::cout << "CAREFUL: use_distributed_pdlp with distributed_pdlp_num_gpus == 1 runs the " "single-shard dummy path" << std::endl; } - auto op_problem = mps_data_model_to_optimization_problem(handle_ptr, mps_data_model); - return solve_lp(op_problem, settings_resolved, problem_checking, use_pdlp_solver_mode); + // PDLP precision validations (mirror the checks in run_pdlp; distributed + // path only supports the default-precision, non-batch double config). + cuopt_expects(settings_resolved.pdlp_precision == pdlp_precision_t::DefaultPrecision, + error_type_t::ValidationError, + "Distributed PDLP only supports DefaultPrecision (double)."); + cuopt_expects(!settings_resolved.inside_mip, + error_type_t::ValidationError, + "Distributed PDLP is not yet supported from inside MIP."); + + init_logger_t log(settings_resolved.log_file, settings_resolved.log_to_console); + print_version_info(); + init_handler(handle_ptr); + + const i_t n_vars = static_cast(mps_data_model.get_objective_coefficients().size()); + const i_t n_cstr = static_cast(mps_data_model.get_constraint_lower_bounds().size()); + const i_t nnz = static_cast(mps_data_model.get_constraint_matrix_values().size()); + CUOPT_LOG_INFO("Solving a problem with %d constraints, %d variables (%d integers), and %d " + "nonzeros (distributed mps-direct path)", + n_cstr, + n_vars, + 0, + nnz); + + auto lp_timer = cuopt::timer_t(settings_resolved.time_limit); + + // Shape-0 placeholder: needed to build an empty pdlp_solver + cuopt::linear_programming::optimization_problem_t placeholder_op(handle_ptr); + { + std::vector empty_offsets = {0}; + placeholder_op.set_csr_constraint_matrix( + nullptr, 0, nullptr, 0, empty_offsets.data(), static_cast(empty_offsets.size())); + } + detail::problem_t placeholder_problem(placeholder_op); + + detail::pdlp_solver_t solver( + placeholder_problem, mps_data_model, settings_resolved); + + auto sol = solver.run_solver(lp_timer); + + // Maximization post-processing (matches run_pdlp at solve.cu:835-839): + // PDLP internally solves the negated objective, so flip dual / reduced + // cost signs on the gathered solution before returning. + if (mps_data_model.get_sense()) { + adjust_dual_solution_and_reduced_cost( + sol.get_dual_solution(), sol.get_reduced_cost(), handle_ptr->get_stream()); + handle_ptr->sync_stream(); + } + + sol.set_solve_time(lp_timer.elapsed_time()); + CUOPT_LOG_INFO("PDLP finished"); + if (sol.get_termination_status() != pdlp_termination_status_t::ConcurrentLimit) { + CUOPT_LOG_INFO("Status: %s Objective: %.8e Iterations: %d Time: %.3fs", + sol.get_termination_status_string().c_str(), + sol.get_objective_value(), + sol.get_additional_termination_information().number_of_steps_taken, + sol.get_solve_time()); + } + + if (settings_resolved.sol_file != "") { + CUOPT_LOG_INFO("Writing solution to file %s", settings_resolved.sol_file.c_str()); + sol.write_to_sol_file(settings_resolved.sol_file, handle_ptr->get_stream()); + } + + return sol; } // ============================================================================ From e4739b5a16c94d719187e28cd4ea3e32740c8f0b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 2 Jun 2026 15:21:20 +0200 Subject: [PATCH 058/258] removed usage of problem_t for distributed PDLP --- cpp/cuopt_cli.cpp | 7 +- .../distributed_pdlp/multi_gpu_engine.hpp | 499 ++++++++++++++++++ .../initial_scaling.cu | 120 +++-- .../initial_scaling.cuh | 15 +- cpp/src/pdlp/pdlp.cu | 112 +++- cpp/src/pdlp/saddle_point.cu | 7 +- .../convergence_information.cu | 71 +++ .../convergence_information.hpp | 5 + 8 files changed, 790 insertions(+), 46 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 7c0a9111d9..0ea79bd4ec 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -176,7 +176,12 @@ int run_single_file(const std::string& file_path, auto solution = cuopt::linear_programming::solve_mip(problem_interface.get(), mip_settings); } else { auto& lp_settings = settings.get_pdlp_settings(); - auto solution = cuopt::linear_programming::solve_lp(problem_interface.get(), lp_settings); + + if (lp_settings.hyper_params.use_distributed_pdlp) { + cuopt::linear_programming::solve_lp(handle_ptr.get(), mps_data_model, lp_settings); + } else { + cuopt::linear_programming::solve_lp(problem_interface.get(), lp_settings); + } } } catch (const std::exception& e) { fprintf(stderr, "cuopt_cli error: %s\n", e.what()); diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 674c4c0ef2..6ab4e35b71 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -12,6 +12,8 @@ #include +#include +#include #include #include @@ -27,6 +29,7 @@ #include #include +#include #include #include @@ -336,6 +339,502 @@ struct multi_gpu_engine_t { for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); }); } + // -------- Distributed Ruiz inf-scaling ----------------------------------- + void alloc_global_var_scratch(i_t n_global_vars, + std::vector>& global_var_buf, + std::vector>& local_to_global_var_d) + { + const int nb = static_cast(shards.size()); + global_var_buf.reserve(nb); + local_to_global_var_d.reserve(nb); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + global_var_buf.emplace_back(static_cast(n_global_vars), s.stream.view()); + local_to_global_var_d.emplace_back(static_cast(s.rank_data.total_var_size), + s.stream.view()); + if (s.rank_data.total_var_size > 0) { + RAFT_CUDA_TRY(cudaMemcpyAsync(local_to_global_var_d.back().data(), + s.rank_data.local_to_global_var.data(), + sizeof(i_t) * s.rank_data.local_to_global_var.size(), + cudaMemcpyHostToDevice, + s.stream.view().value())); + } + } + } + + void reduce_iteration_variable_scaling_across_shards( + ncclRedOp_t op, + i_t n_global_vars, + std::vector>& global_var_buf, + std::vector>& local_to_global_var_d) + { + const int nb = static_cast(shards.size()); + + // Zero global buffers, then scatter each shard's local values into their + // global column indices. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + RAFT_CUDA_TRY(cudaMemsetAsync(global_var_buf[r].data(), + 0, + sizeof(f_t) * static_cast(n_global_vars), + s.stream.view().value())); + auto& iter_var_scaling = + s.sub_pdlp->get_initial_scaling_strategy().get_iteration_variable_scaling(); + if (s.rank_data.total_var_size > 0) { + thrust::scatter(rmm::exec_policy_nosync(s.stream.view()), + iter_var_scaling.begin(), + iter_var_scaling.begin() + s.rank_data.total_var_size, + local_to_global_var_d[r].begin(), + global_var_buf[r].begin()); + } + } + + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + ncclAllReduce(global_var_buf[r].data(), + global_var_buf[r].data(), + static_cast(n_global_vars), + ncclFloat64, + op, + s.comm.get(), + s.stream.view().value()); + } + ncclGroupEnd(); + + // Gather the global per-column value back into each shard's local iter vector. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto& iter_var_scaling = + s.sub_pdlp->get_initial_scaling_strategy().get_iteration_variable_scaling(); + if (s.rank_data.total_var_size > 0) { + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + local_to_global_var_d[r].begin(), + local_to_global_var_d[r].begin() + s.rank_data.total_var_size, + global_var_buf[r].begin(), + iter_var_scaling.begin()); + } + } + } + + void distributed_ruiz_inf_scaling(int num_iter, i_t n_global_vars) + { + if (num_iter <= 0 || n_global_vars <= 0) return; + raft::common::nvtx::range scope("distributed_ruiz_inf_scaling"); + + std::vector> global_var_buf; + std::vector> local_to_global_var_d; + alloc_global_var_scratch(n_global_vars, global_var_buf, local_to_global_var_d); + + for (int it = 0; it < num_iter; ++it) { + // 1) per-shard local kernel: writes iteration_variable_scaling (per-column + // inf-norm partial) and iteration_constraint_matrix_scaling (row, complete). + for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_compute_local_iteration_vectors(); + }); + + // 2) cross-shard column inf-norm reduction (MAX). + reduce_iteration_variable_scaling_across_shards( + ncclMax, n_global_vars, global_var_buf, local_to_global_var_d); + + // 3) per-shard fold into cumulative + reset iter vectors. + for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_apply_cumulative_update(); + }); + } + + // Make sure per-shard cumulative writes are observable on subsequent + // calls (e.g., the next distributed_max_singular_value). + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + } + + // Distributed Pock-Chambolle: one pass, mirroring single-GPU + // pock_chambolle_scaling but with the per-column sum-of-powers reduced across + // shards (SUM) between the local kernels and the cumulative fold. Rows are + // owned exclusively, so the row half stays local. Runs after the distributed + // Ruiz pass, matching the single-GPU order (Ruiz then Pock-Chambolle). + void distributed_pock_chambolle_scaling(f_t alpha, i_t n_global_vars) + { + if (n_global_vars <= 0) return; + raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); + + std::vector> global_var_buf; + std::vector> local_to_global_var_d; + alloc_global_var_scratch(n_global_vars, global_var_buf, local_to_global_var_d); + + // 1) per-shard local kernels: row sum (complete) + column sum (partial). + for_each_shard([alpha](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_compute_local_iteration_vectors( + alpha); + }); + + // 2) cross-shard column sum-of-powers reduction (SUM). + reduce_iteration_variable_scaling_across_shards( + ncclSum, n_global_vars, global_var_buf, local_to_global_var_d); + + // 3) per-shard fold into cumulative (cumulative /= sqrt(iteration)). + for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_apply_cumulative_update(); + }); + + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + } + + // -------- Distributed σ_max(A) via power iteration ---------------------- + f_t distributed_max_singular_value(i_t n_global_cstrs, + int max_iterations = 5000, + f_t tolerance = 1e-4) + { + raft::common::nvtx::range scope("distributed_max_singular_value"); + + const int nb = static_cast(shards.size()); + + // Generate the GLOBAL z[] sequence in cstr-index order from a fresh + // mt19937(1), once per call. It's m doubles regardless of N (cheap). + // Each shard then keeps only z[global_idx_for_owned_local_i]. + std::vector h_global_z(static_cast(n_global_cstrs)); + { + std::mt19937 gen(1); + std::normal_distribution dist(f_t(0.0), f_t(1.0)); + for (i_t i = 0; i < n_global_cstrs; ++i) { + h_global_z[i] = dist(gen); + } + } + + // Per-shard scratch lives on each shard's device. We use total (owned + + // halo) sizes for q/z/atq because they're SpMV inputs that need halo + // space. Norms / dot are scalars. + // We use size-1 rmm::device_uvector instead of rmm::device_scalar for the + // per-shard scratch scalars: nvcc + libcudacxx fail the + // copy_constructible concept check when device_scalar appears in a + // std::vector (the check transitively touches rmm::cuda_stream, which is + // non-copyable). device_uvector avoids that path. + std::vector> q; + std::vector> z; + std::vector> atq; + std::vector> sigma_sq; + std::vector> norm_q; + std::vector> residual_norm; + std::vector z_dn(nb, nullptr); + std::vector atq_dn(nb, nullptr); + q.reserve(nb); + z.reserve(nb); + atq.reserve(nb); + sigma_sq.reserve(nb); + norm_q.reserve(nb); + residual_norm.reserve(nb); + + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const i_t cstr_total = s.rank_data.total_cstr_size; + const i_t var_total = s.rank_data.total_var_size; + q.emplace_back(static_cast(cstr_total), s.stream.view()); + z.emplace_back(static_cast(cstr_total), s.stream.view()); + atq.emplace_back(static_cast(var_total), s.stream.view()); + sigma_sq.emplace_back(std::size_t{1}, s.stream.view()); + norm_q.emplace_back(std::size_t{1}, s.stream.view()); + residual_norm.emplace_back(std::size_t{1}, s.stream.view()); + RAFT_CUSPARSE_TRY(raft::sparse::detail::cusparsecreatednvec( + &z_dn[r], static_cast(cstr_total), z.back().data())); + RAFT_CUSPARSE_TRY(raft::sparse::detail::cusparsecreatednvec( + &atq_dn[r], static_cast(var_total), atq.back().data())); + + std::vector h_owned_z(static_cast(s.rank_data.owned_cstr_size)); + for (i_t i = 0; i < s.rank_data.owned_cstr_size; ++i) { + const i_t g = s.rank_data.local_to_global_cstr[i]; + h_owned_z[i] = h_global_z[g]; + } + if (s.rank_data.owned_cstr_size > 0) { + RAFT_CUDA_TRY( + cudaMemcpyAsync(z.back().data(), + h_owned_z.data(), + sizeof(f_t) * static_cast(s.rank_data.owned_cstr_size), + cudaMemcpyHostToDevice, + s.stream.view().value())); + } + if (cstr_total > s.rank_data.owned_cstr_size) { + RAFT_CUDA_TRY(cudaMemsetAsync( + z.back().data() + s.rank_data.owned_cstr_size, + 0, + sizeof(f_t) * static_cast(cstr_total - s.rank_data.owned_cstr_size), + s.stream.view().value())); + } + // Sync to ensure h_owned_z stays valid through the H2D copy (it goes + // out of scope at end of this iteration of the per-shard loop). + s.stream.synchronize(); + } + + // Local halo-exchange helpers that work directly on per-shard external + // buffers (the engine's halo_exchange_var/cstr expect accessors that + // resolve through pdhg_solver_t, which doesn't see our scratch). + auto halo_exchange_cstr_bufs = [&](std::vector>& bufs) { + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto& y = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (s.cstr_send_indices_d[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + s.cstr_send_indices_d[peer].begin(), + s.cstr_send_indices_d[peer].end(), + y.begin(), + s.cstr_send_buf_d[peer].begin()); + } + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + ncclSend(s.cstr_send_buf_d[peer].data(), + s.cstr_send_buf_d[peer].size(), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + auto& rd = s.rank_data; + raft::device_setter guard(s.device_id); + auto& y = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; + ncclRecv(recv_ptr, + static_cast(rd.cstr_recv_counts[peer]), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + ncclGroupEnd(); + }; + auto halo_exchange_var_bufs = [&](std::vector>& bufs) { + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto& x = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (s.var_send_indices_d[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + s.var_send_indices_d[peer].begin(), + s.var_send_indices_d[peer].end(), + x.begin(), + s.var_send_buf_d[peer].begin()); + } + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + ncclSend(s.var_send_buf_d[peer].data(), + s.var_send_buf_d[peer].size(), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + auto& rd = s.rank_data; + raft::device_setter guard(s.device_id); + auto& x = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; + ncclRecv(recv_ptr, + static_cast(rd.var_recv_counts[peer]), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + ncclGroupEnd(); + }; + + // Per-shard partial reductions over the OWNED cstr slice + NCCL allreduce. + // For norm: out := sqrt(Σ_r ||bufs[r][0:owned_cstr]||²). + // For dot : out := Σ_r . + auto distributed_norm_owned_cstr = [&](std::vector>& bufs, + std::vector>& out) { + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), + static_cast(n_owned), + bufs[r].data(), + 1, + bufs[r].data(), + 1, + out[r].data(), + s.stream.view().value())); + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + ncclAllReduce(out[r].data(), + out[r].data(), + 1, + ncclFloat64, + ncclSum, + s.comm.get(), + s.stream.view().value()); + } + ncclGroupEnd(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + cub::DeviceTransform::Transform( + out[r].data(), out[r].data(), 1, sqrt_inplace_op_t{}, s.stream.view().value()); + } + }; + auto distributed_dot_owned_cstr = [&](std::vector>& a, + std::vector>& b, + std::vector>& out) { + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), + static_cast(n_owned), + a[r].data(), + 1, + b[r].data(), + 1, + out[r].data(), + s.stream.view().value())); + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + ncclAllReduce(out[r].data(), + out[r].data(), + 1, + ncclFloat64, + ncclSum, + s.comm.get(), + s.stream.view().value()); + } + ncclGroupEnd(); + }; + + // ===== Power iteration ===== + // Mirrors single-GPU compute_initial_step_size: z is the carried iterate + // (A Aᵀ q each step); at the top of each iteration q := z then q is + // normalized; the residual z − σ²q is written back into q only to drive + // the convergence check (next iteration's q := z discards it). + for (int it = 0; it < max_iterations; ++it) { + // q := z on the owned slice (the carried iterate), then normalize. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + raft::copy(q[r].data(), z[r].data(), n_owned, s.stream.view()); + } + + // ||q||₂ over the global OWNED cstr slice (one allreduce-sum + sqrt). + distributed_norm_owned_cstr(q, norm_q); + + // q /= ||q||₂ on owned slice (halo gets refreshed by next exchange). + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + cub::DeviceTransform::Transform( + q[r].data(), + q[r].data(), + n_owned, + [n = norm_q[r].data()] __device__(f_t v) { return v / *n; }, + s.stream.view().value()); + } + + // atq = A^T q : halo-exchange q, then per-shard SpMV. spmv_At_into + // rebinds the dual_solution dnvec to q[r].data() and restores the + // canonical binding after the call (see pdhg.cu:643-644). + halo_exchange_cstr_bufs(q); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + s.sub_pdlp->pdhg_solver_.spmv_At_into(q[r], atq_dn[r]); + } + + // z = A atq : halo-exchange atq, then per-shard SpMV. + halo_exchange_var_bufs(atq); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + s.sub_pdlp->pdhg_solver_.spmv_A_into(atq[r], z_dn[r]); + } + + // σ² = q · z over the global OWNED cstr slice (= q^T A A^T q = σ_max² + // when q is the dominant left-singular vector). + distributed_dot_owned_cstr(q, z, sigma_sq); + + // q := -σ² q + z (owned slice) — residual of the eigen-equation. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(q[r].data(), z[r].data()), + q[r].data(), + n_owned, + [s2 = sigma_sq[r].data()] __device__(f_t qv, f_t zv) { return -(*s2) * qv + zv; }, + s.stream.view().value()); + } + + // Convergence check via global residual norm. + distributed_norm_owned_cstr(q, residual_norm); + auto& s0 = *shards[0]; + raft::device_setter guard0(s0.device_id); + f_t h_res{}; + RAFT_CUDA_TRY(cudaMemcpyAsync(&h_res, + residual_norm[0].data(), + sizeof(f_t), + cudaMemcpyDeviceToHost, + s0.stream.view().value())); + s0.stream.synchronize(); + if (h_res < tolerance) break; + } + + // σ_max² is the same on every shard after the last allreduce. + auto& s0 = *shards[0]; + raft::device_setter guard0(s0.device_id); + f_t sigma_sq_h{}; + RAFT_CUDA_TRY(cudaMemcpyAsync(&sigma_sq_h, + sigma_sq[0].data(), + sizeof(f_t), + cudaMemcpyDeviceToHost, + s0.stream.view().value())); + s0.stream.synchronize(); + + for (int r = 0; r < nb; ++r) { + raft::device_setter guard(shards[r]->device_id); + RAFT_CUSPARSE_TRY(cusparseDestroyDnVec(z_dn[r])); + RAFT_CUSPARSE_TRY(cusparseDestroyDnVec(atq_dn[r])); + } + + return std::sqrt(std::max(sigma_sq_h, f_t(0))); + } + // -------- Solution gather (shards -> master) ---------------------------- // Assembles the global potential_next primal/dual solutions and the // reduced_cost on the master from the owned slices distributed across diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 478753e9d9..dcc3e662b0 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -142,6 +142,10 @@ void pdlp_initial_scaling_strategy_t::compute_scaling_vectors( { raft::common::nvtx::range fun_scope("compute_scaling_vectors"); + // Skip scaling entirely for a shape-0 problem (distributed PDLP builds the + // master pdlp_solver_t from a shape-0 placeholder) + if (primal_size_h_ == 0 || dual_size_h_ == 0) return; + if (hyper_params_.do_ruiz_scaling) { ruiz_inf_scaling(number_of_ruiz_iterations); } if (hyper_params_.do_pock_chambolle_scaling) { pock_chambolle_scaling(alpha); } } @@ -213,6 +217,72 @@ __global__ void inf_norm_row_and_col_kernel( } } +template +void pdlp_initial_scaling_strategy_t::ruiz_iter_compute_local_iteration_vectors() +{ + // find inf norm over rows and columns of the scaled matrix in given iteration + i_t number_of_blocks = op_problem_scaled_.n_constraints / block_size; + if (op_problem_scaled_.n_constraints % block_size) number_of_blocks++; + i_t number_of_threads = std::min(op_problem_scaled_.n_variables, (i_t)block_size); + inf_norm_row_and_col_kernel<<>>( + op_problem_scaled_.view(), this->view()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + if (running_mip_) { reset_integer_variables(); } +} + +template +void pdlp_initial_scaling_strategy_t::ruiz_iter_apply_cumulative_update() +{ + raft::linalg::binaryOp(cummulative_constraint_matrix_scaling_.data(), + cummulative_constraint_matrix_scaling_.data(), + iteration_constraint_matrix_scaling_.data(), + dual_size_h_, + a_divides_sqrt_b_bounded(), + stream_view_); + + raft::linalg::binaryOp(cummulative_variable_scaling_.data(), + cummulative_variable_scaling_.data(), + iteration_variable_scaling_.data(), + primal_size_h_, + a_divides_sqrt_b_bounded(), + stream_view_); + + // Reset the iteration_scaling vectors to all 0 + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_constraint_matrix_scaling_.data(), 0.0, sizeof(f_t) * dual_size_h_, stream_view_)); + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_variable_scaling_.data(), 0.0, sizeof(f_t) * primal_size_h_, stream_view_)); +} + +template +void pdlp_initial_scaling_strategy_t::reset_scaling_state_for_distributed() +{ + if (primal_size_h_ == 0 || dual_size_h_ == 0) return; + + // Re-allocate the iteration vectors the ctor shrank to 0 and zero them. + iteration_constraint_matrix_scaling_.resize(static_cast(dual_size_h_), stream_view_); + iteration_variable_scaling_.resize(static_cast(primal_size_h_), stream_view_); + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); + + // Reset cumulative scaling + rescaling to identity (the ctor's stray + // Pock-Chambolle pass and shard.cu's set_cummulative_scaling left these in + // an arbitrary state; distributed scaling recomputes from a clean slate). + thrust::fill(handle_ptr_->get_thrust_policy(), + cummulative_constraint_matrix_scaling_.begin(), + cummulative_constraint_matrix_scaling_.end(), + f_t(1)); + thrust::fill(handle_ptr_->get_thrust_policy(), + cummulative_variable_scaling_.begin(), + cummulative_variable_scaling_.end(), + f_t(1)); + set_h_bound_rescaling(f_t(1)); + set_h_objective_rescaling(f_t(1)); +} + template void pdlp_initial_scaling_strategy_t::ruiz_inf_scaling(i_t number_of_ruiz_iterations) { @@ -221,36 +291,8 @@ void pdlp_initial_scaling_strategy_t::ruiz_inf_scaling(i_t number_of_r std::cout << "Doing ruiz_inf_scaling" << std::endl; #endif for (int i = 0; i < number_of_ruiz_iterations; i++) { - // find inf norm over rows and columns of the scaled matrix in given iteration (matrix is not - // actually updated, but the scaled value is computed and evaluated) - i_t number_of_blocks = op_problem_scaled_.n_constraints / block_size; - if (op_problem_scaled_.n_constraints % block_size) number_of_blocks++; - i_t number_of_threads = std::min(op_problem_scaled_.n_variables, (i_t)block_size); - inf_norm_row_and_col_kernel<<>>( - op_problem_scaled_.view(), this->view()); - RAFT_CUDA_TRY(cudaPeekAtLastError()); - - if (running_mip_) { reset_integer_variables(); } - - raft::linalg::binaryOp(cummulative_constraint_matrix_scaling_.data(), - cummulative_constraint_matrix_scaling_.data(), - iteration_constraint_matrix_scaling_.data(), - dual_size_h_, - a_divides_sqrt_b_bounded(), - stream_view_); - - raft::linalg::binaryOp(cummulative_variable_scaling_.data(), - cummulative_variable_scaling_.data(), - iteration_variable_scaling_.data(), - primal_size_h_, - a_divides_sqrt_b_bounded(), - stream_view_); - - // Reset the iteration_scaling vectors to all 0 - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_constraint_matrix_scaling_.data(), 0.0, sizeof(f_t) * dual_size_h_, stream_view_)); - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_variable_scaling_.data(), 0.0, sizeof(f_t) * primal_size_h_, stream_view_)); + ruiz_iter_compute_local_iteration_vectors(); + ruiz_iter_apply_cumulative_update(); } } @@ -343,8 +385,12 @@ __global__ void pock_chambolle_scaling_kernel_col( if (threadIdx.x == 0) initial_scaling_view.iteration_variable_scaling[col] = accumulated_value; } +// Local half of one Pock-Chambolle pass: writes the per-row and per-column +// sums-of-powers into iteration_constraint_matrix_scaling_ / +// iteration_variable_scaling_ template -void pdlp_initial_scaling_strategy_t::pock_chambolle_scaling(f_t alpha) +void pdlp_initial_scaling_strategy_t::pock_chambolle_compute_local_iteration_vectors( + f_t alpha) { // Reset the iteration_scaling vectors to all 0 RAFT_CUDA_TRY(cudaMemsetAsync( @@ -379,7 +425,12 @@ void pdlp_initial_scaling_strategy_t::pock_chambolle_scaling(f_t alpha A_T_offsets_.data(), A_T_indices_.data()); RAFT_CUDA_TRY(cudaPeekAtLastError()); +} +// Fold half of one Pock-Chambolle pass: cumulative /= sqrt(iteration). +template +void pdlp_initial_scaling_strategy_t::pock_chambolle_apply_cumulative_update() +{ if (running_mip_) { reset_integer_variables(); } // divide the sqrt of the vectors of the sums from above to the respective scaling vectors @@ -398,6 +449,13 @@ void pdlp_initial_scaling_strategy_t::pock_chambolle_scaling(f_t alpha stream_view_); } +template +void pdlp_initial_scaling_strategy_t::pock_chambolle_scaling(f_t alpha) +{ + pock_chambolle_compute_local_iteration_vectors(alpha); + pock_chambolle_apply_cumulative_update(); +} + template __global__ void scale_problem_kernel( const typename pdlp_initial_scaling_strategy_t::view_t initial_scaling_view, diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index dbdb604082..148ccce238 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -94,6 +94,20 @@ class pdlp_initial_scaling_strategy_t { void bound_objective_rescaling(); + // Public for distributed PDLP + void compute_scaling_vectors(i_t number_of_ruiz_iterations, f_t alpha); + + // ----- Distributed-PDLP hooks ----- + + void ruiz_iter_compute_local_iteration_vectors(); + void ruiz_iter_apply_cumulative_update(); + void pock_chambolle_compute_local_iteration_vectors(f_t alpha); + void pock_chambolle_apply_cumulative_update(); + rmm::device_uvector& get_iteration_variable_scaling() { return iteration_variable_scaling_; } + + // Restore the clean pre-scaling state for the distributed path. + void reset_scaling_state_for_distributed(); + /** * @brief Gets the device-side view (with raw pointers), for ease of access * inside cuda kernels @@ -101,7 +115,6 @@ class pdlp_initial_scaling_strategy_t { view_t view(); private: - void compute_scaling_vectors(i_t number_of_ruiz_iterations, f_t alpha); void ruiz_inf_scaling(i_t number_of_ruiz_iterations); void pock_chambolle_scaling(f_t alpha); void reset_integer_variables(); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 21291b853d..013905b4fb 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -577,24 +577,71 @@ pdlp_solver_t::pdlp_solver_t( objective_scaling_factor, sub_pdlp_settings); - // ----- 8. Seed shard step-size / primal-weight scalars from the master ----- - f_t h_step_size{}, h_primal_weight{}, h_best_primal_weight{}; - f_t h_primal_step_size{}, h_dual_step_size{}; - raft::copy(&h_step_size, step_size_.data(), 1, stream_view_); - raft::copy(&h_primal_weight, primal_weight_.data(), 1, stream_view_); - raft::copy(&h_best_primal_weight, best_primal_weight_.data(), 1, stream_view_); - raft::copy(&h_primal_step_size, primal_step_size_.data(), 1, stream_view_); - raft::copy(&h_dual_step_size, dual_step_size_.data(), 1, stream_view_); + // ----- 8 Distributed Scaling ----- + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->sub_pdlp->get_initial_scaling_strategy().reset_scaling_state_for_distributed(); + } + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->stream.synchronize(); + } + + // Distributed scaling + if (settings_.hyper_params.do_ruiz_scaling) { + multi_gpu_engine->distributed_ruiz_inf_scaling( + settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); + } + if (settings_.hyper_params.do_pock_chambolle_scaling) { + multi_gpu_engine->distributed_pock_chambolle_scaling( + static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), n_vars); + } + + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + auto& scaling = shard->sub_pdlp->get_initial_scaling_strategy(); + scaling.scale_problem(); + + shard->sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( + /*is_reflected=*/settings_.hyper_params.use_reflected_primal_dual); + } + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->stream.synchronize(); + } + + // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- + constexpr f_t kStepSizeScale = f_t{0.998}; + const f_t sigma_max = multi_gpu_engine->distributed_max_singular_value(n_cstr); + const f_t h_primal_weight = f_t{1}; + const f_t h_step_size = (sigma_max > f_t{0}) ? kStepSizeScale / sigma_max : f_t{1}; + // With primal_weight = 1 the adaptive step-size strategy collapses to + // primal_step_size = step_size / primal_weight = step_size + // dual_step_size = step_size * primal_weight = step_size. + const f_t h_primal_step_size = h_step_size; + const f_t h_dual_step_size = h_step_size; + + // Put the values on master + raft::copy(step_size_.data(), &h_step_size, 1, stream_view_); + raft::copy(primal_weight_.data(), &h_primal_weight, 1, stream_view_); + raft::copy(best_primal_weight_.data(), &h_primal_weight, 1, stream_view_); + raft::copy(primal_step_size_.data(), &h_primal_step_size, 1, stream_view_); + raft::copy(dual_step_size_.data(), &h_dual_step_size, 1, stream_view_); handle_ptr_->sync_stream(stream_view_); + // put the values on each shard for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); auto& sub = *shard->sub_pdlp; raft::copy(sub.step_size_.data(), &h_step_size, 1, shard->stream); raft::copy(sub.primal_weight_.data(), &h_primal_weight, 1, shard->stream); - raft::copy(sub.best_primal_weight_.data(), &h_best_primal_weight, 1, shard->stream); - raft::copy(sub.primal_step_size_.data(), &h_primal_step_size, 1, shard->stream); - raft::copy(sub.dual_step_size_.data(), &h_dual_step_size, 1, shard->stream); + raft::copy(sub.best_primal_weight_.data(), &h_primal_weight, 1, shard->stream); + raft::copy(sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard->stream); + raft::copy(sub.get_dual_step_size().data(), &h_dual_step_size, 1, shard->stream); + } + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->stream.synchronize(); } // Wire the engine into master's pdhg_solver_; shards keep mgpu_engine_ == nullptr. @@ -607,6 +654,49 @@ pdlp_solver_t::pdlp_solver_t( n_vars, stream_view_); primal_size_h_ = n_vars; dual_size_h_ = n_cstr; + + // Distributed conergence_information::init_l2_norms + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .compute_owned_reference_norm_partials(shard->rank_data.owned_var_size, + shard->rank_data.owned_cstr_size); + } + multi_gpu_engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_norm_primal_right_hand_side_data(); + }); + multi_gpu_engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_norm_primal_linear_objective_data(); + }); + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .sqrt_reference_norms_inplace(); + shard->stream.synchronize(); + } + // Broadcast the values to the master + { + auto& s0 = *multi_gpu_engine->shards[0]; + auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); + raft::device_setter guard(s0.device_id); + for (auto* ts : {¤t_termination_strategy_, &average_termination_strategy_}) { + auto& ci = ts->get_convergence_information(); + raft::copy(ci.l2_norm_primal_right_hand_side_data(), + s0_conv.l2_norm_primal_right_hand_side_data(), + 1, + stream_view_); + raft::copy(ci.l2_norm_primal_linear_objective_data(), + s0_conv.l2_norm_primal_linear_objective_data(), + 1, + stream_view_); + } + } handle_ptr_->sync_stream(stream_view_); } diff --git a/cpp/src/pdlp/saddle_point.cu b/cpp/src/pdlp/saddle_point.cu index f740176a3c..07a5d0146e 100644 --- a/cpp/src/pdlp/saddle_point.cu +++ b/cpp/src/pdlp/saddle_point.cu @@ -38,8 +38,11 @@ saddle_point_state_t::saddle_point_state_t( current_AtY_{batch_size * primal_size, handle_ptr->get_stream()}, next_AtY_{batch_size * primal_size, handle_ptr->get_stream()} { - EXE_CUOPT_EXPECTS(primal_size > 0, "Size of the primal problem must be larger than 0"); - EXE_CUOPT_EXPECTS(dual_size > 0, "Size of the dual problem must be larger than 0"); + // >= 0 (not > 0): distributed PDLP builds the master pdlp_solver_t from a + // shape-0 placeholder problem so the master never materializes per-variable + // / per-constraint vectors; size-0 device_uvectors are valid throughout. + EXE_CUOPT_EXPECTS(primal_size >= 0, "Size of the primal problem must be non-negative"); + EXE_CUOPT_EXPECTS(dual_size >= 0, "Size of the dual problem must be non-negative"); // Starting from all 0 thrust::fill( diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index da2340146a..44ddd5b2a1 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -212,6 +212,77 @@ void convergence_information_t::init_l2_norms() } } +template +void convergence_information_t::compute_owned_reference_norm_partials( + i_t owned_var_size, i_t owned_cstr_size) +{ + cuopt_assert(!batch_mode_, "owned reference-norm partials only used in non-batch mGPU mode"); + cuopt_assert(owned_var_size <= primal_size_h_, "owned_var_size must be <= primal_size_h_"); + cuopt_assert(owned_cstr_size <= dual_size_h_, "owned_cstr_size must be <= dual_size_h_"); + + // Σ objective[0:owned_var]² + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(handle_ptr_->get_cublas_handle(), + static_cast(owned_var_size), + problem_ptr->objective_coefficients.data(), + 1, + problem_ptr->objective_coefficients.data(), + 1, + l2_norm_primal_linear_objective_.data(), + stream_view_)); + + // rhs_sum_of_squares(lower[0:owned_cstr], upper[0:owned_cstr]) (no sqrt) + { + rmm::device_buffer d_temp_storage; + size_t bytes = 0; + auto zip_begin = thrust::make_zip_iterator(problem_ptr->constraint_lower_bounds.data(), + problem_ptr->constraint_upper_bounds.data()); + cub::DeviceReduce::TransformReduce(nullptr, + bytes, + zip_begin, + l2_norm_primal_right_hand_side_.data(), + static_cast(owned_cstr_size), + cuda::std::plus<>{}, + rhs_sum_of_squares_t{}, + f_t(0), + stream_view_); + d_temp_storage.resize(bytes, stream_view_); + cub::DeviceReduce::TransformReduce(d_temp_storage.data(), + bytes, + zip_begin, + l2_norm_primal_right_hand_side_.data(), + static_cast(owned_cstr_size), + cuda::std::plus<>{}, + rhs_sum_of_squares_t{}, + f_t(0), + stream_view_); + } + RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); +} + +template +void convergence_information_t::sqrt_reference_norms_inplace() +{ + cub::DeviceTransform::Transform(l2_norm_primal_linear_objective_.data(), + l2_norm_primal_linear_objective_.data(), + 1, + sqrt_func_t{}, + stream_view_); + cub::DeviceTransform::Transform(l2_norm_primal_right_hand_side_.data(), + l2_norm_primal_right_hand_side_.data(), + 1, + sqrt_func_t{}, + stream_view_); + // Broadcast slot [0] to all climbers (no-op outside batch mode). + thrust::fill(handle_ptr_->get_thrust_policy(), + l2_norm_primal_linear_objective_.begin(), + l2_norm_primal_linear_objective_.end(), + l2_norm_primal_linear_objective_.element(0, stream_view_)); + thrust::fill(handle_ptr_->get_thrust_policy(), + l2_norm_primal_right_hand_side_.begin(), + l2_norm_primal_right_hand_side_.end(), + l2_norm_primal_right_hand_side_.element(0, stream_view_)); +} + // --------------------------------------------------------------------------- // init_reduction_storage: allocate and size the temporary buffers used by // cub::DeviceReduce and cub::DeviceSegmentedReduce throughout solving. diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.hpp b/cpp/src/pdlp/termination_strategy/convergence_information.hpp index 6325622a2b..7ff45e46f0 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.hpp +++ b/cpp/src/pdlp/termination_strategy/convergence_information.hpp @@ -69,6 +69,11 @@ class convergence_information_t { const rmm::device_uvector& get_l2_norm_primal_linear_objective() const; const rmm::device_uvector& get_l2_norm_primal_right_hand_side() const; + void compute_owned_reference_norm_partials(i_t owned_var_size, i_t owned_cstr_size); + void sqrt_reference_norms_inplace(); + f_t* l2_norm_primal_right_hand_side_data() { return l2_norm_primal_right_hand_side_.data(); } + f_t* l2_norm_primal_linear_objective_data() { return l2_norm_primal_linear_objective_.data(); } + struct view_t { i_t primal_size; i_t dual_size; From 1903f4bfea48d25ba4042bb7d2a02e0a41267718 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 2 Jun 2026 15:39:56 +0200 Subject: [PATCH 059/258] added a cuopt assert for solve_lp in mgpu mode --- cpp/src/pdlp/solve.cu | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index ef273faf13..feaeb7bd57 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2126,13 +2126,11 @@ optimization_problem_solution_t solve_lp( bool problem_checking, bool use_pdlp_solver_mode) { - // In distributed PDLP we can't allocate the full problem on the master device - if (settings.hyper_params.use_distributed_pdlp) { + cuopt_expects(settings.hyper_params.use_distributed_pdlp, + error_type_t::ValidationError, + "solve_lp from mps_data_model: settings.hyper_params.use_distributed_pdlp must be true"); return solve_lp_distributed_from_mps( handle_ptr, mps_data_model, settings, problem_checking, use_pdlp_solver_mode); - } - auto op_problem = mps_data_model_to_optimization_problem(handle_ptr, mps_data_model); - return solve_lp(op_problem, settings, problem_checking, use_pdlp_solver_mode); } template From 0aacb4f702fe0a413623f07522dac6745f484692 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 2 Jun 2026 15:42:45 +0200 Subject: [PATCH 060/258] style --- cpp/cuopt_cli.cpp | 9 +- .../cuopt/linear_programming/constants.h | 22 +- .../pdlp/pdlp_hyper_params.cuh | 14 +- .../distributed_pdlp/metis_partitioner.cu | 14 +- .../distributed_pdlp/multi_gpu_engine.hpp | 18 +- .../pdlp/distributed_pdlp/partition_loader.cu | 6 +- cpp/src/pdlp/distributed_pdlp/partitioner.cu | 25 +- cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 2 +- cpp/src/pdlp/pdhg.cu | 16 +- cpp/src/pdlp/pdlp.cu | 586 +++++++++--------- .../restart_strategy/pdlp_restart_strategy.cu | 17 +- cpp/src/pdlp/solve.cu | 27 +- cpp/src/pdlp/solve.cuh | 3 +- .../adaptive_step_size_strategy.cu | 10 +- .../convergence_information.cu | 101 ++- cpp/src/pdlp/utilities/mgpu_trace.cuh | 24 +- 16 files changed, 429 insertions(+), 465 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 0ea79bd4ec..b730067a28 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -436,14 +436,13 @@ int main(int argc, char* argv[]) // For distributed PDLP, -1 means "auto-detect": resolve to the visible device // count so the RMM memory pools match what solve.cu will eventually dispatch. const bool use_distributed_pdlp = settings.get_parameter(CUOPT_USE_DISTRIBUTED_PDLP); - int requested_gpus = - use_distributed_pdlp ? settings.get_parameter(CUOPT_DISTRIBUTED_PDLP_NUM_GPUS) - : settings.get_parameter(CUOPT_NUM_GPUS); + int requested_gpus = use_distributed_pdlp + ? settings.get_parameter(CUOPT_DISTRIBUTED_PDLP_NUM_GPUS) + : settings.get_parameter(CUOPT_NUM_GPUS); if (use_distributed_pdlp && requested_gpus == -1) { requested_gpus = raft::device_setter::get_device_count(); } - const int provisioned_gpus = - std::min(raft::device_setter::get_device_count(), requested_gpus); + const int provisioned_gpus = std::min(raft::device_setter::get_device_count(), requested_gpus); memory_resources.reserve(provisioned_gpus); for (int i = 0; i < provisioned_gpus; ++i) { diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index e695bb21d3..e2cc264cdc 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -80,18 +80,18 @@ #define CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT \ "mip_strong_branching_simplex_iteration_limit" -#define CUOPT_SOLUTION_FILE "solution_file" -#define CUOPT_NUM_CPU_THREADS "num_cpu_threads" -#define CUOPT_NUM_GPUS "num_gpus" +#define CUOPT_SOLUTION_FILE "solution_file" +#define CUOPT_NUM_CPU_THREADS "num_cpu_threads" +#define CUOPT_NUM_GPUS "num_gpus" #define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" -#define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" -#define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" -#define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" -#define CUOPT_USER_PROBLEM_FILE "user_problem_file" -#define CUOPT_PRESOLVE_FILE "presolve_file" -#define CUOPT_RANDOM_SEED "random_seed" -#define CUOPT_PDLP_PRECISION "pdlp_precision" -#define CUOPT_MIP_SEMICONTINUOUS_BIG_M "mip_semi_continuous_big_m" +#define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" +#define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" +#define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" +#define CUOPT_USER_PROBLEM_FILE "user_problem_file" +#define CUOPT_PRESOLVE_FILE "presolve_file" +#define CUOPT_RANDOM_SEED "random_seed" +#define CUOPT_PDLP_PRECISION "pdlp_precision" +#define CUOPT_MIP_SEMICONTINUOUS_BIG_M "mip_semi_continuous_big_m" #define CUOPT_MIP_HYPER_HEURISTIC_POPULATION_SIZE "mip_hyper_heuristic_population_size" #define CUOPT_MIP_HYPER_HEURISTIC_NUM_CPUFJ_THREADS "mip_hyper_heuristic_num_cpufj_threads" diff --git a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh index c68dc86d6a..0ce90e7228 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh +++ b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh @@ -50,13 +50,13 @@ struct pdlp_hyper_params_t { bool use_distributed_pdlp = false; // Debug/diagnostic knob: when true, PDLP bypasses CUDA-graph capture in // ping_pong_graph_t and executes each iteration eagerly - bool pdlp_disable_graph = false; - double reflection_coefficient = 1.0; - double restart_k_p = 0.99; - double restart_k_i = 0.01; - double restart_k_d = 0.0; - double restart_i_smooth = 0.3; - bool use_conditional_major = true; + bool pdlp_disable_graph = false; + double reflection_coefficient = 1.0; + double restart_k_p = 0.99; + double restart_k_i = 0.01; + double restart_k_d = 0.0; + double restart_i_smooth = 0.3; + bool use_conditional_major = true; }; // TODO most likely we want to get rid of pdlp_solver_mode and just have prebuilt diff --git a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu index 73e2736251..ecc60adda0 100644 --- a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu @@ -79,7 +79,9 @@ std::vector metis_partitioner_t::partition( std::vector adjncy(2 * static_cast(nnz)); // cstr-side row offsets: A_offsets[0..nb_cstr] (no shift). - for (i_t i = 0; i <= nb_cstr; ++i) { xadj[i] = static_cast(A_offsets[i]); } + for (i_t i = 0; i <= nb_cstr; ++i) { + xadj[i] = static_cast(A_offsets[i]); + } // var-side row offsets: A_t_offsets[0..nb_vars], shifted by +nnz so that // they index into the second half of adjncy. for (i_t i = 0; i <= nb_vars; ++i) { @@ -106,7 +108,7 @@ std::vector metis_partitioner_t::partition( idx_t objval = 0; std::vector metis_parts(nvtx); - auto t0 = std::chrono::high_resolution_clock::now(); + auto t0 = std::chrono::high_resolution_clock::now(); const int status = METIS_PartGraphKway(&metis_nvtx, &ncon, xadj.data(), @@ -120,8 +122,8 @@ std::vector metis_partitioner_t::partition( metis_options, &objval, metis_parts.data()); - auto t1 = std::chrono::high_resolution_clock::now(); - const double dt = std::chrono::duration(t1 - t0).count(); + auto t1 = std::chrono::high_resolution_clock::now(); + const double dt = std::chrono::duration(t1 - t0).count(); cuopt_expects(status == METIS_OK, error_type_t::RuntimeError, "METIS_PartGraphKway failed (status=%d)", @@ -135,7 +137,9 @@ std::vector metis_partitioner_t::partition( dt); std::vector parts(static_cast(nvtx)); - for (i_t i = 0; i < nvtx; ++i) { parts[i] = static_cast(metis_parts[i]); } + for (i_t i = 0; i < nvtx; ++i) { + parts[i] = static_cast(metis_parts[i]); + } validate_partition(parts, static_cast(nb_cstr), diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 6ab4e35b71..0297ecc0a6 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -255,9 +255,7 @@ struct multi_gpu_engine_t { // OutAccess : pdlp_solver_t& -> f_t* (single scalar in shard memory) // SizeAccess : pdlp_shard_t& -> i_t (owned slice length) template - void distributed_l2_norm(BufAccess&& buf_access, - OutAccess&& out_access, - SizeAccess&& size_access) + void distributed_l2_norm(BufAccess&& buf_access, OutAccess&& out_access, SizeAccess&& size_access) { for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; @@ -858,13 +856,11 @@ struct multi_gpu_engine_t { // master_reduced_cost : destination for the reduced_cost (var-shaped, lives // in the master pdlp_solver_t's termination strategy // convergence_information_). - void gather_potential_next_solutions_to_master( - pdhg_solver_t& master_pdhg, rmm::device_uvector& master_reduced_cost) + void gather_potential_next_solutions_to_master(pdhg_solver_t& master_pdhg, + rmm::device_uvector& master_reduced_cost) { - const std::size_t total_vars = - master_pdhg.get_potential_next_primal_solution().size(); - const std::size_t total_cstrs = - master_pdhg.get_potential_next_dual_solution().size(); + const std::size_t total_vars = master_pdhg.get_potential_next_primal_solution().size(); + const std::size_t total_cstrs = master_pdhg.get_potential_next_dual_solution().size(); std::vector h_primal(total_vars); std::vector h_dual(total_cstrs); @@ -987,8 +983,8 @@ struct multi_gpu_engine_t { } } - // Functionnaly same as graph_capture_fork_to_shards but on a different event to avoid race conditions - // Can be used as a way to sync shards with master stream + // Functionnaly same as graph_capture_fork_to_shards but on a different event to avoid race + // conditions Can be used as a way to sync shards with master stream void sync_await_master(rmm::cuda_stream_view master_stream) { sync_master_ready_event_->record(master_stream); diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 5014607736..5c317f664e 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -174,12 +174,10 @@ std::vector> partition_loader_t::create_rank_dat // Pad row-offset arrays so cuSPARSE sees the local matrices as // (total_cstr x total_var) for A and (total_var x total_cstr) for A_T - const i_t a_last_nnz = - rd.h_A_row_offsets.empty() ? i_t{0} : rd.h_A_row_offsets.back(); + const i_t a_last_nnz = rd.h_A_row_offsets.empty() ? i_t{0} : rd.h_A_row_offsets.back(); rd.h_A_row_offsets.resize(rd.total_cstr_size + 1, a_last_nnz); - const i_t at_last_nnz = - rd.h_A_t_row_offsets.empty() ? i_t{0} : rd.h_A_t_row_offsets.back(); + const i_t at_last_nnz = rd.h_A_t_row_offsets.empty() ? i_t{0} : rd.h_A_t_row_offsets.back(); rd.h_A_t_row_offsets.resize(rd.total_var_size + 1, at_last_nnz); } diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cu b/cpp/src/pdlp/distributed_pdlp/partitioner.cu index 4b809986ce..bc84e521e2 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cu @@ -38,11 +38,8 @@ std::vector dummy_partitioner_t::partition( return parts; } -void validate_partition(std::vector const& parts, - int nb_cstr, - int nb_vars, - int nb_parts, - char const* context) +void validate_partition( + std::vector const& parts, int nb_cstr, int nb_vars, int nb_parts, char const* context) { const std::size_t expected = static_cast(nb_cstr) + static_cast(nb_vars); @@ -52,10 +49,8 @@ void validate_partition(std::vector const& parts, context, expected, parts.size()); - cuopt_expects(nb_parts > 0, - error_type_t::ValidationError, - "%s: nb_parts must be positive", - context); + cuopt_expects( + nb_parts > 0, error_type_t::ValidationError, "%s: nb_parts must be positive", context); if (parts.empty()) { return; } const auto [min_it, max_it] = std::minmax_element(parts.begin(), parts.end()); cuopt_expects(*min_it >= 0, @@ -75,16 +70,16 @@ template std::unique_ptr> make_partitioner(partitioner_kind_t kind) { switch (kind) { - case partitioner_kind_t::Dummy: - return std::make_unique>(); - case partitioner_kind_t::Metis: - return std::make_unique>(); + case partitioner_kind_t::Dummy: return std::make_unique>(); + case partitioner_kind_t::Metis: return std::make_unique>(); } - cuopt_expects(false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); + cuopt_expects( + false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); return nullptr; } template class dummy_partitioner_t; -template std::unique_ptr> make_partitioner(partitioner_kind_t); +template std::unique_ptr> make_partitioner( + partitioner_kind_t); } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index 82650ad805..2a2149db63 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -41,7 +41,7 @@ enum class partitioner_kind_t { Dummy, Metis }; template class partitioner_i { public: - virtual ~partitioner_i() = default; + virtual ~partitioner_i() = default; virtual std::vector partition(partitioner_input_t const& input) const = 0; }; diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index ec983fd01b..b1f1a59ada 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -1257,9 +1257,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // the capture or run outside the graph, leaving the captured graph // empty (or broken) -- which produces the cycling/stall behavior we // observed on larger problems. Mirrors metis_tests bench.cu fork/join. - if (mgpu_engine_ != nullptr) { - mgpu_engine_->graph_capture_fork_to_shards(stream_view_); - } + if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } compute_At_y(); if (mgpu_engine_ != nullptr) { @@ -1362,16 +1360,12 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // Multi-GPU: close the fork by joining every shard stream back into // the master stream so cudaStreamEndCapture sees a single graph // spanning all streams. - if (mgpu_engine_ != nullptr) { - mgpu_engine_->graph_capture_join_from_shards(stream_view_); - } + if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_join_from_shards(stream_view_); } }); } else { graph_all.run(should_major, [&]() { - if (mgpu_engine_ != nullptr) { - mgpu_engine_->graph_capture_fork_to_shards(stream_view_); - } + if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } // Compute next primal compute_At_y(); @@ -1478,9 +1472,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( print("reflected_dual_", reflected_dual_); #endif - if (mgpu_engine_ != nullptr) { - mgpu_engine_->graph_capture_join_from_shards(stream_view_); - } + if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_join_from_shards(stream_view_); } }); } diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 013905b4fb..576ab417f1 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -390,8 +390,7 @@ pdlp_solver_t::pdlp_solver_t( // Makes all inner feilds of master 0 size : pdlp_solver_t(placeholder_problem, settings, /*is_legacy_batch_mode=*/false) { - cuopt_expects(placeholder_problem.n_variables == 0 && - placeholder_problem.n_constraints == 0 && + cuopt_expects(placeholder_problem.n_variables == 0 && placeholder_problem.n_constraints == 0 && placeholder_problem.nnz == 0, error_type_t::ValidationError, "Distributed mGPU pdlp_solver_t ctor requires a shape-0 " @@ -407,297 +406,297 @@ pdlp_solver_t::pdlp_solver_t( } if constexpr (!std::is_same_v) { - cuopt_expects(false, - error_type_t::ValidationError, - "Distributed PDLP currently requires double precision"); + cuopt_expects( + false, error_type_t::ValidationError, "Distributed PDLP currently requires double precision"); return; } - // ----- 1. Read problem shape and bulk data directly from mps (host) ----- - const i_t n_vars = static_cast(mps.get_objective_coefficients().size()); - const i_t n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); - const i_t nnz = static_cast(mps.get_constraint_matrix_values().size()); - cuopt_expects(n_vars > 0, - error_type_t::ValidationError, - "Distributed PDLP from mps requires a non-empty objective"); - cuopt_expects(n_cstr > 0, - error_type_t::ValidationError, - "Distributed PDLP from mps requires at least one constraint"); - cuopt_expects(static_cast(mps.get_constraint_matrix_offsets().size()) == n_cstr + 1, - error_type_t::ValidationError, - "mps constraint_matrix_offsets size must equal n_constraints + 1"); - cuopt_expects( - static_cast(mps.get_constraint_matrix_indices().size()) == nnz, - error_type_t::ValidationError, - "mps constraint_matrix_indices size must equal nnz (constraint_matrix_values size)"); - cuopt_expects(static_cast(mps.get_constraint_upper_bounds().size()) == n_cstr, - error_type_t::ValidationError, - "mps constraint_upper_bounds size must equal n_constraints"); - cuopt_expects(static_cast(mps.get_variable_lower_bounds().size()) == n_vars, - error_type_t::ValidationError, - "mps variable_lower_bounds size must equal n_variables"); - cuopt_expects(static_cast(mps.get_variable_upper_bounds().size()) == n_vars, - error_type_t::ValidationError, - "mps variable_upper_bounds size must equal n_variables"); - - const bool maximize = mps.get_sense(); - f_t objective_offset = mps.get_objective_offset(); - f_t objective_scaling_factor = mps.get_objective_scaling_factor(); - - // Objective: copy (mutable so we can negate for maximize, matching - // problem_helpers.cuh::convert_to_maximization_problem). - std::vector h_obj = mps.get_objective_coefficients(); - if (maximize) { - for (auto& v : h_obj) v = -v; - objective_offset = -objective_offset; - objective_scaling_factor = -objective_scaling_factor; - } - - // Bounds (copy from mps; engine ctor takes by const ref to std::vector). - std::vector h_var_lower = mps.get_variable_lower_bounds(); - std::vector h_var_upper = mps.get_variable_upper_bounds(); - std::vector h_cstr_lower = mps.get_constraint_lower_bounds(); - std::vector h_cstr_upper = mps.get_constraint_upper_bounds(); - - // A (CSR) — mutable copies for the engine + partitioner consumers below. - std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); - std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); - std::vector h_A_values = mps.get_constraint_matrix_values(); - - // ----- 2. Transpose A -> A^T on the host (one-shot CSR transpose) ----- - // CSC(A) and CSR(A^T) share the same memory layout, so the CSC produced - // by dual_simplex::csr_matrix_t::to_compressed_col IS the CSR of A^T. - // O(nnz + n_vars) counting sort, same as problem_t::compute_transpose. - namespace ds = cuopt::linear_programming::dual_simplex; - ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); - A_csr.row_start = h_A_row_offsets; - A_csr.j = h_A_col_indices; - A_csr.x = h_A_values; - ds::csc_matrix_t AT_as_csc(n_vars, n_cstr, nnz); - A_csr.to_compressed_col(AT_as_csc); - std::vector h_A_t_row_offsets = std::move(AT_as_csc.col_start); - std::vector h_A_t_col_indices = std::move(AT_as_csc.i); - std::vector h_A_t_values = std::move(AT_as_csc.x); - - // ----- 3. Identity scaling for V1 ----- - // Real multi-GPU scaling is a TODO; ship the unscaled problem to shards as - // both "unscaled" and "scaled" so the engine and per-shard pdlp_solver_t - // can run end-to-end. Scaling factor vectors are 1.0 everywhere so the - // shard-side unscale at the end is a no-op. - std::vector h_A_values_scaled = h_A_values; - std::vector h_A_t_values_scaled = h_A_t_values; - std::vector h_obj_scaled = h_obj; - std::vector h_var_lower_scaled = h_var_lower; - std::vector h_var_upper_scaled = h_var_upper; - std::vector h_cstr_lower_scaled = h_cstr_lower; - std::vector h_cstr_upper_scaled = h_cstr_upper; - std::vector h_cummulative_cstr_scaling(n_cstr, f_t(1.0)); - std::vector h_cummulative_var_scaling(n_vars, f_t(1.0)); - const f_t h_bound_rescaling = f_t(1.0); - const f_t h_objective_rescaling = f_t(1.0); - - // ----- 4. Partition ----- - std::vector parts; - if (!settings.multi_gpu_partition_file.empty()) { - parts = partition_loader_t::parse_distributed_pdlp_partition_file( - settings.multi_gpu_partition_file); - validate_partition(parts, n_cstr, n_vars, distributed_pdlp_num_gpus, "partition file"); - } else { - if (distributed_pdlp_num_gpus == 1) { - std::cout << "CAREFUL: distributed_pdlp_num_gpus == 1, running dummy version (single " - "part covering " - << n_cstr << " cstrs + " << n_vars << " vars)" << std::endl; - } - partitioner_input_t partition_input; - partition_input.nb_cstr = n_cstr; - partition_input.nb_vars = n_vars; - partition_input.nb_parts = distributed_pdlp_num_gpus; - - // METIS_PartGraphKway requires nparts >= 2; route num_gpus == 1 to Dummy. - const partitioner_kind_t kind = - (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::Metis; - if (kind == partitioner_kind_t::Metis) { - // partitioner_input_t holds non-const std::vector* pointers; we - // already have the data in our local mutable buffers above. - partition_input.A.row_offsets = &h_A_row_offsets; - partition_input.A.col_indices = &h_A_col_indices; - partition_input.A.num_rows = n_cstr; - partition_input.A.num_cols = n_vars; - partition_input.A_t.row_offsets = &h_A_t_row_offsets; - partition_input.A_t.col_indices = &h_A_t_col_indices; - partition_input.A_t.num_rows = n_vars; - partition_input.A_t.num_cols = n_cstr; - } - auto partitioner = make_partitioner(kind); - parts = partitioner->partition(partition_input); - } - - // ----- 5. Build per-rank data ----- - std::vector> sub_pdlp_rank_data = - partition_loader_t::create_rank_data_from_parts(parts, - h_A_row_offsets, - h_A_col_indices, - h_A_values, - h_A_values_scaled, - h_A_t_row_offsets, - h_A_t_col_indices, - h_A_t_values, - h_A_t_values_scaled, - settings.distributed_pdlp_num_gpus, - n_cstr, - n_vars, - nnz); - - // ----- 6. Per-shard settings ----- - pdlp_solver_settings_t sub_pdlp_settings = settings; - sub_pdlp_settings.num_gpus = 1; - sub_pdlp_settings.distributed_pdlp_num_gpus = 1; - sub_pdlp_settings.multi_gpu_partition_file = ""; - sub_pdlp_settings.is_distributed_sub_pdlp = true; - sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; - sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; - - // ----- 7. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- - multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), - h_obj, - h_var_lower, - h_var_upper, - h_cstr_lower, - h_cstr_upper, - h_obj_scaled, - h_var_lower_scaled, - h_var_upper_scaled, - h_cstr_lower_scaled, - h_cstr_upper_scaled, - h_cummulative_cstr_scaling, - h_cummulative_var_scaling, - h_bound_rescaling, - h_objective_rescaling, - maximize, - objective_offset, - objective_scaling_factor, - sub_pdlp_settings); - - // ----- 8 Distributed Scaling ----- - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->sub_pdlp->get_initial_scaling_strategy().reset_scaling_state_for_distributed(); - } - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->stream.synchronize(); - } - - // Distributed scaling - if (settings_.hyper_params.do_ruiz_scaling) { - multi_gpu_engine->distributed_ruiz_inf_scaling( - settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); - } - if (settings_.hyper_params.do_pock_chambolle_scaling) { - multi_gpu_engine->distributed_pock_chambolle_scaling( - static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), n_vars); - } - - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& scaling = shard->sub_pdlp->get_initial_scaling_strategy(); - scaling.scale_problem(); - - shard->sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( - /*is_reflected=*/settings_.hyper_params.use_reflected_primal_dual); - } - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->stream.synchronize(); - } - - // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- - constexpr f_t kStepSizeScale = f_t{0.998}; - const f_t sigma_max = multi_gpu_engine->distributed_max_singular_value(n_cstr); - const f_t h_primal_weight = f_t{1}; - const f_t h_step_size = (sigma_max > f_t{0}) ? kStepSizeScale / sigma_max : f_t{1}; - // With primal_weight = 1 the adaptive step-size strategy collapses to - // primal_step_size = step_size / primal_weight = step_size - // dual_step_size = step_size * primal_weight = step_size. - const f_t h_primal_step_size = h_step_size; - const f_t h_dual_step_size = h_step_size; - - // Put the values on master - raft::copy(step_size_.data(), &h_step_size, 1, stream_view_); - raft::copy(primal_weight_.data(), &h_primal_weight, 1, stream_view_); - raft::copy(best_primal_weight_.data(), &h_primal_weight, 1, stream_view_); - raft::copy(primal_step_size_.data(), &h_primal_step_size, 1, stream_view_); - raft::copy(dual_step_size_.data(), &h_dual_step_size, 1, stream_view_); - handle_ptr_->sync_stream(stream_view_); - - // put the values on each shard - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub = *shard->sub_pdlp; - raft::copy(sub.step_size_.data(), &h_step_size, 1, shard->stream); - raft::copy(sub.primal_weight_.data(), &h_primal_weight, 1, shard->stream); - raft::copy(sub.best_primal_weight_.data(), &h_primal_weight, 1, shard->stream); - raft::copy(sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard->stream); - raft::copy(sub.get_dual_step_size().data(), &h_dual_step_size, 1, shard->stream); - } - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->stream.synchronize(); - } - - // Wire the engine into master's pdhg_solver_; shards keep mgpu_engine_ == nullptr. - pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); - - // ----- 9. Resize master gather destinations to the full problem size ----- - pdhg_solver_.get_potential_next_primal_solution().resize(n_vars, stream_view_); - pdhg_solver_.get_potential_next_dual_solution().resize(n_cstr, stream_view_); - current_termination_strategy_.get_convergence_information().get_reduced_cost().resize( - n_vars, stream_view_); - primal_size_h_ = n_vars; - dual_size_h_ = n_cstr; - - // Distributed conergence_information::init_l2_norms - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->sub_pdlp->get_current_termination_strategy() - .get_convergence_information() - .compute_owned_reference_norm_partials(shard->rank_data.owned_var_size, - shard->rank_data.owned_cstr_size); + // ----- 1. Read problem shape and bulk data directly from mps (host) ----- + const i_t n_vars = static_cast(mps.get_objective_coefficients().size()); + const i_t n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); + const i_t nnz = static_cast(mps.get_constraint_matrix_values().size()); + cuopt_expects(n_vars > 0, + error_type_t::ValidationError, + "Distributed PDLP from mps requires a non-empty objective"); + cuopt_expects(n_cstr > 0, + error_type_t::ValidationError, + "Distributed PDLP from mps requires at least one constraint"); + cuopt_expects(static_cast(mps.get_constraint_matrix_offsets().size()) == n_cstr + 1, + error_type_t::ValidationError, + "mps constraint_matrix_offsets size must equal n_constraints + 1"); + cuopt_expects( + static_cast(mps.get_constraint_matrix_indices().size()) == nnz, + error_type_t::ValidationError, + "mps constraint_matrix_indices size must equal nnz (constraint_matrix_values size)"); + cuopt_expects(static_cast(mps.get_constraint_upper_bounds().size()) == n_cstr, + error_type_t::ValidationError, + "mps constraint_upper_bounds size must equal n_constraints"); + cuopt_expects(static_cast(mps.get_variable_lower_bounds().size()) == n_vars, + error_type_t::ValidationError, + "mps variable_lower_bounds size must equal n_variables"); + cuopt_expects(static_cast(mps.get_variable_upper_bounds().size()) == n_vars, + error_type_t::ValidationError, + "mps variable_upper_bounds size must equal n_variables"); + + const bool maximize = mps.get_sense(); + f_t objective_offset = mps.get_objective_offset(); + f_t objective_scaling_factor = mps.get_objective_scaling_factor(); + + // Objective: copy (mutable so we can negate for maximize, matching + // problem_helpers.cuh::convert_to_maximization_problem). + std::vector h_obj = mps.get_objective_coefficients(); + if (maximize) { + for (auto& v : h_obj) + v = -v; + objective_offset = -objective_offset; + objective_scaling_factor = -objective_scaling_factor; + } + + // Bounds (copy from mps; engine ctor takes by const ref to std::vector). + std::vector h_var_lower = mps.get_variable_lower_bounds(); + std::vector h_var_upper = mps.get_variable_upper_bounds(); + std::vector h_cstr_lower = mps.get_constraint_lower_bounds(); + std::vector h_cstr_upper = mps.get_constraint_upper_bounds(); + + // A (CSR) — mutable copies for the engine + partitioner consumers below. + std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); + std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); + std::vector h_A_values = mps.get_constraint_matrix_values(); + + // ----- 2. Transpose A -> A^T on the host (one-shot CSR transpose) ----- + // CSC(A) and CSR(A^T) share the same memory layout, so the CSC produced + // by dual_simplex::csr_matrix_t::to_compressed_col IS the CSR of A^T. + // O(nnz + n_vars) counting sort, same as problem_t::compute_transpose. + namespace ds = cuopt::linear_programming::dual_simplex; + ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); + A_csr.row_start = h_A_row_offsets; + A_csr.j = h_A_col_indices; + A_csr.x = h_A_values; + ds::csc_matrix_t AT_as_csc(n_vars, n_cstr, nnz); + A_csr.to_compressed_col(AT_as_csc); + std::vector h_A_t_row_offsets = std::move(AT_as_csc.col_start); + std::vector h_A_t_col_indices = std::move(AT_as_csc.i); + std::vector h_A_t_values = std::move(AT_as_csc.x); + + // ----- 3. Identity scaling for V1 ----- + // Real multi-GPU scaling is a TODO; ship the unscaled problem to shards as + // both "unscaled" and "scaled" so the engine and per-shard pdlp_solver_t + // can run end-to-end. Scaling factor vectors are 1.0 everywhere so the + // shard-side unscale at the end is a no-op. + std::vector h_A_values_scaled = h_A_values; + std::vector h_A_t_values_scaled = h_A_t_values; + std::vector h_obj_scaled = h_obj; + std::vector h_var_lower_scaled = h_var_lower; + std::vector h_var_upper_scaled = h_var_upper; + std::vector h_cstr_lower_scaled = h_cstr_lower; + std::vector h_cstr_upper_scaled = h_cstr_upper; + std::vector h_cummulative_cstr_scaling(n_cstr, f_t(1.0)); + std::vector h_cummulative_var_scaling(n_vars, f_t(1.0)); + const f_t h_bound_rescaling = f_t(1.0); + const f_t h_objective_rescaling = f_t(1.0); + + // ----- 4. Partition ----- + std::vector parts; + if (!settings.multi_gpu_partition_file.empty()) { + parts = partition_loader_t::parse_distributed_pdlp_partition_file( + settings.multi_gpu_partition_file); + validate_partition(parts, n_cstr, n_vars, distributed_pdlp_num_gpus, "partition file"); + } else { + if (distributed_pdlp_num_gpus == 1) { + std::cout << "CAREFUL: distributed_pdlp_num_gpus == 1, running dummy version (single " + "part covering " + << n_cstr << " cstrs + " << n_vars << " vars)" << std::endl; } - multi_gpu_engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .l2_norm_primal_right_hand_side_data(); - }); - multi_gpu_engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .l2_norm_primal_linear_objective_data(); - }); - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->sub_pdlp->get_current_termination_strategy() - .get_convergence_information() - .sqrt_reference_norms_inplace(); - shard->stream.synchronize(); + partitioner_input_t partition_input; + partition_input.nb_cstr = n_cstr; + partition_input.nb_vars = n_vars; + partition_input.nb_parts = distributed_pdlp_num_gpus; + + // METIS_PartGraphKway requires nparts >= 2; route num_gpus == 1 to Dummy. + const partitioner_kind_t kind = + (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::Metis; + if (kind == partitioner_kind_t::Metis) { + // partitioner_input_t holds non-const std::vector* pointers; we + // already have the data in our local mutable buffers above. + partition_input.A.row_offsets = &h_A_row_offsets; + partition_input.A.col_indices = &h_A_col_indices; + partition_input.A.num_rows = n_cstr; + partition_input.A.num_cols = n_vars; + partition_input.A_t.row_offsets = &h_A_t_row_offsets; + partition_input.A_t.col_indices = &h_A_t_col_indices; + partition_input.A_t.num_rows = n_vars; + partition_input.A_t.num_cols = n_cstr; } - // Broadcast the values to the master - { - auto& s0 = *multi_gpu_engine->shards[0]; - auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); - raft::device_setter guard(s0.device_id); - for (auto* ts : {¤t_termination_strategy_, &average_termination_strategy_}) { - auto& ci = ts->get_convergence_information(); - raft::copy(ci.l2_norm_primal_right_hand_side_data(), - s0_conv.l2_norm_primal_right_hand_side_data(), - 1, - stream_view_); - raft::copy(ci.l2_norm_primal_linear_objective_data(), - s0_conv.l2_norm_primal_linear_objective_data(), - 1, - stream_view_); - } + auto partitioner = make_partitioner(kind); + parts = partitioner->partition(partition_input); + } + + // ----- 5. Build per-rank data ----- + std::vector> sub_pdlp_rank_data = + partition_loader_t::create_rank_data_from_parts(parts, + h_A_row_offsets, + h_A_col_indices, + h_A_values, + h_A_values_scaled, + h_A_t_row_offsets, + h_A_t_col_indices, + h_A_t_values, + h_A_t_values_scaled, + settings.distributed_pdlp_num_gpus, + n_cstr, + n_vars, + nnz); + + // ----- 6. Per-shard settings ----- + pdlp_solver_settings_t sub_pdlp_settings = settings; + sub_pdlp_settings.num_gpus = 1; + sub_pdlp_settings.distributed_pdlp_num_gpus = 1; + sub_pdlp_settings.multi_gpu_partition_file = ""; + sub_pdlp_settings.is_distributed_sub_pdlp = true; + sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; + sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; + + // ----- 7. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- + multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), + h_obj, + h_var_lower, + h_var_upper, + h_cstr_lower, + h_cstr_upper, + h_obj_scaled, + h_var_lower_scaled, + h_var_upper_scaled, + h_cstr_lower_scaled, + h_cstr_upper_scaled, + h_cummulative_cstr_scaling, + h_cummulative_var_scaling, + h_bound_rescaling, + h_objective_rescaling, + maximize, + objective_offset, + objective_scaling_factor, + sub_pdlp_settings); + + // ----- 8 Distributed Scaling ----- + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->sub_pdlp->get_initial_scaling_strategy().reset_scaling_state_for_distributed(); + } + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->stream.synchronize(); + } + + // Distributed scaling + if (settings_.hyper_params.do_ruiz_scaling) { + multi_gpu_engine->distributed_ruiz_inf_scaling( + settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); + } + if (settings_.hyper_params.do_pock_chambolle_scaling) { + multi_gpu_engine->distributed_pock_chambolle_scaling( + static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), n_vars); + } + + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + auto& scaling = shard->sub_pdlp->get_initial_scaling_strategy(); + scaling.scale_problem(); + + shard->sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( + /*is_reflected=*/settings_.hyper_params.use_reflected_primal_dual); + } + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->stream.synchronize(); + } + + // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- + constexpr f_t kStepSizeScale = f_t{0.998}; + const f_t sigma_max = multi_gpu_engine->distributed_max_singular_value(n_cstr); + const f_t h_primal_weight = f_t{1}; + const f_t h_step_size = (sigma_max > f_t{0}) ? kStepSizeScale / sigma_max : f_t{1}; + // With primal_weight = 1 the adaptive step-size strategy collapses to + // primal_step_size = step_size / primal_weight = step_size + // dual_step_size = step_size * primal_weight = step_size. + const f_t h_primal_step_size = h_step_size; + const f_t h_dual_step_size = h_step_size; + + // Put the values on master + raft::copy(step_size_.data(), &h_step_size, 1, stream_view_); + raft::copy(primal_weight_.data(), &h_primal_weight, 1, stream_view_); + raft::copy(best_primal_weight_.data(), &h_primal_weight, 1, stream_view_); + raft::copy(primal_step_size_.data(), &h_primal_step_size, 1, stream_view_); + raft::copy(dual_step_size_.data(), &h_dual_step_size, 1, stream_view_); + handle_ptr_->sync_stream(stream_view_); + + // put the values on each shard + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + auto& sub = *shard->sub_pdlp; + raft::copy(sub.step_size_.data(), &h_step_size, 1, shard->stream); + raft::copy(sub.primal_weight_.data(), &h_primal_weight, 1, shard->stream); + raft::copy(sub.best_primal_weight_.data(), &h_primal_weight, 1, shard->stream); + raft::copy(sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard->stream); + raft::copy(sub.get_dual_step_size().data(), &h_dual_step_size, 1, shard->stream); + } + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->stream.synchronize(); + } + + // Wire the engine into master's pdhg_solver_; shards keep mgpu_engine_ == nullptr. + pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); + + // ----- 9. Resize master gather destinations to the full problem size ----- + pdhg_solver_.get_potential_next_primal_solution().resize(n_vars, stream_view_); + pdhg_solver_.get_potential_next_dual_solution().resize(n_cstr, stream_view_); + current_termination_strategy_.get_convergence_information().get_reduced_cost().resize( + n_vars, stream_view_); + primal_size_h_ = n_vars; + dual_size_h_ = n_cstr; + + // Distributed conergence_information::init_l2_norms + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .compute_owned_reference_norm_partials(shard->rank_data.owned_var_size, + shard->rank_data.owned_cstr_size); + } + multi_gpu_engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_norm_primal_right_hand_side_data(); + }); + multi_gpu_engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_norm_primal_linear_objective_data(); + }); + for (auto& shard : multi_gpu_engine->shards) { + raft::device_setter guard(shard->device_id); + shard->sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .sqrt_reference_norms_inplace(); + shard->stream.synchronize(); + } + // Broadcast the values to the master + { + auto& s0 = *multi_gpu_engine->shards[0]; + auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); + raft::device_setter guard(s0.device_id); + for (auto* ts : {¤t_termination_strategy_, &average_termination_strategy_}) { + auto& ci = ts->get_convergence_information(); + raft::copy(ci.l2_norm_primal_right_hand_side_data(), + s0_conv.l2_norm_primal_right_hand_side_data(), + 1, + stream_view_); + raft::copy(ci.l2_norm_primal_linear_objective_data(), + s0_conv.l2_norm_primal_linear_objective_data(), + 1, + stream_view_); } - handle_ptr_->sync_stream(stream_view_); + } + handle_ptr_->sync_stream(stream_view_); } template @@ -2418,10 +2417,9 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte multi_gpu_engine->allreduce_sum_inplace( [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }); - multi_gpu_engine->allreduce_sum_inplace( - [](auto& sp) -> f_t* { - return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); - }); + multi_gpu_engine->allreduce_sum_inplace([](auto& sp) -> f_t* { + return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); + }); multi_gpu_engine->allreduce_sum_inplace( [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }); @@ -3022,9 +3020,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co // 1. At the very beginning of the solver, when no steps have been taken yet // 2. After a single step, since average of one step is the same step if (internal_solver_iterations_ <= 1) { - if (multi_gpu_engine) { - assert(false && "Not implemented"); - } + if (multi_gpu_engine) { assert(false && "Not implemented"); } raft::copy(unscaled_primal_avg_solution_.data(), pdhg_solver_.get_primal_solution().data(), primal_size_h_, diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index b7d49fc32f..ee1d19b96b 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -931,11 +931,11 @@ void pdlp_restart_strategy_t::cupdlpx_restart( .last_restart_duality_gap_.primal_distance_traveled_.data(), 1, stream_view_); - raft::copy(last_restart_duality_gap_.dual_distance_traveled_.data(), - s0.sub_pdlp->get_restart_strategy() - .last_restart_duality_gap_.dual_distance_traveled_.data(), - 1, - stream_view_); + raft::copy( + last_restart_duality_gap_.dual_distance_traveled_.data(), + s0.sub_pdlp->get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(), + 1, + stream_view_); } else { distance_squared_moved_from_last_restart_period( pdhg_solver.get_potential_next_primal_solution(), @@ -1021,8 +1021,7 @@ void pdlp_restart_strategy_t::cupdlpx_restart( engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; - raft::copy( - sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard.stream.view()); + raft::copy(sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard.stream.view()); raft::copy(sub.get_dual_step_size().data(), &h_dual_step_size, 1, shard.stream.view()); raft::copy(sub.get_primal_weight().data(), &h_primal_weight, 1, shard.stream.view()); raft::copy( @@ -1087,8 +1086,8 @@ void pdlp_restart_strategy_t::cupdlpx_restart( if (auto* engine = pdhg_solver.get_mgpu_engine()) { engine->for_each_shard([&](auto& shard) { - shard.sub_pdlp->get_restart_strategy().weighted_average_solution_.iterations_since_last_restart_ = - 0; + shard.sub_pdlp->get_restart_strategy() + .weighted_average_solution_.iterations_since_last_restart_ = 0; }); } } diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index feaeb7bd57..156a601b29 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2126,11 +2126,12 @@ optimization_problem_solution_t solve_lp( bool problem_checking, bool use_pdlp_solver_mode) { - cuopt_expects(settings.hyper_params.use_distributed_pdlp, - error_type_t::ValidationError, - "solve_lp from mps_data_model: settings.hyper_params.use_distributed_pdlp must be true"); - return solve_lp_distributed_from_mps( - handle_ptr, mps_data_model, settings, problem_checking, use_pdlp_solver_mode); + cuopt_expects( + settings.hyper_params.use_distributed_pdlp, + error_type_t::ValidationError, + "solve_lp from mps_data_model: settings.hyper_params.use_distributed_pdlp must be true"); + return solve_lp_distributed_from_mps( + handle_ptr, mps_data_model, settings, problem_checking, use_pdlp_solver_mode); } template @@ -2182,12 +2183,13 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( const i_t n_vars = static_cast(mps_data_model.get_objective_coefficients().size()); const i_t n_cstr = static_cast(mps_data_model.get_constraint_lower_bounds().size()); const i_t nnz = static_cast(mps_data_model.get_constraint_matrix_values().size()); - CUOPT_LOG_INFO("Solving a problem with %d constraints, %d variables (%d integers), and %d " - "nonzeros (distributed mps-direct path)", - n_cstr, - n_vars, - 0, - nnz); + CUOPT_LOG_INFO( + "Solving a problem with %d constraints, %d variables (%d integers), and %d " + "nonzeros (distributed mps-direct path)", + n_cstr, + n_vars, + 0, + nnz); auto lp_timer = cuopt::timer_t(settings_resolved.time_limit); @@ -2200,8 +2202,7 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( } detail::problem_t placeholder_problem(placeholder_op); - detail::pdlp_solver_t solver( - placeholder_problem, mps_data_model, settings_resolved); + detail::pdlp_solver_t solver(placeholder_problem, mps_data_model, settings_resolved); auto sol = solver.run_solver(lp_timer); diff --git a/cpp/src/pdlp/solve.cuh b/cpp/src/pdlp/solve.cuh index abb657943f..160f4602ba 100644 --- a/cpp/src/pdlp/solve.cuh +++ b/cpp/src/pdlp/solve.cuh @@ -64,8 +64,7 @@ cuopt::linear_programming::optimization_problem_solution_t solve_lp_wi * @pre `settings.hyper_params.use_distributed_pdlp == true`. */ template -cuopt::linear_programming::optimization_problem_solution_t -solve_lp_distributed_from_mps( +cuopt::linear_programming::optimization_problem_solution_t solve_lp_distributed_from_mps( raft::handle_t const* handle_ptr, const cuopt::linear_programming::io::mps_data_model_t& mps_data_model, pdlp_solver_settings_t const& settings, diff --git a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu index 530a426117..aac777a44e 100644 --- a/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu +++ b/cpp/src/pdlp/step_size_strategy/adaptive_step_size_strategy.cu @@ -369,12 +369,10 @@ void adaptive_step_size_strategy_t::compute_interaction_and_movement( i_t owned_cstr_size) { // mGPU needs to know owned size to restrict the reductions to the owned prefix - const i_t reduce_primal_size = (owned_primal_size >= 0) - ? owned_primal_size - : current_saddle_point_state.get_primal_size(); - const i_t reduce_dual_size = (owned_cstr_size >= 0) - ? owned_cstr_size - : current_saddle_point_state.get_dual_size(); + const i_t reduce_primal_size = + (owned_primal_size >= 0) ? owned_primal_size : current_saddle_point_state.get_primal_size(); + const i_t reduce_dual_size = + (owned_cstr_size >= 0) ? owned_cstr_size : current_saddle_point_state.get_dual_size(); // QP would need this: // if iszero(problem.objective_matrix) diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 44ddd5b2a1..1dfc8229da 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -213,8 +213,8 @@ void convergence_information_t::init_l2_norms() } template -void convergence_information_t::compute_owned_reference_norm_partials( - i_t owned_var_size, i_t owned_cstr_size) +void convergence_information_t::compute_owned_reference_norm_partials(i_t owned_var_size, + i_t owned_cstr_size) { cuopt_assert(!batch_mode_, "owned reference-norm partials only used in non-batch mGPU mode"); cuopt_assert(owned_var_size <= primal_size_h_, "owned_var_size must be <= primal_size_h_"); @@ -233,7 +233,7 @@ void convergence_information_t::compute_owned_reference_norm_partials( // rhs_sum_of_squares(lower[0:owned_cstr], upper[0:owned_cstr]) (no sqrt) { rmm::device_buffer d_temp_storage; - size_t bytes = 0; + size_t bytes = 0; auto zip_begin = thrust::make_zip_iterator(problem_ptr->constraint_lower_bounds.data(), problem_ptr->constraint_upper_bounds.data()); cub::DeviceReduce::TransformReduce(nullptr, @@ -491,8 +491,7 @@ void convergence_information_t::compute_convergence_information( print("dual_slack", dual_slack); #endif - if (current_pdhg_solver.is_multi_gpu()) - { + if (current_pdhg_solver.is_multi_gpu()) { auto* engine = current_pdhg_solver.get_mgpu_engine(); cuopt_assert(engine != nullptr, "mGPU branch reached but current_pdhg_solver has no engine (shard pdhg?)"); @@ -502,19 +501,17 @@ void convergence_information_t::compute_convergence_information( // Prepares halo values in potential_next_primal_solution - engine->halo_exchange_var( - [](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_potential_next_primal_solution(); - }); + engine->halo_exchange_var([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { + return pdhg.get_potential_next_primal_solution(); + }); for (auto& shard : engine->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); - sub_conv.compute_primal_residual( - sub_conv.op_problem_cusparse_view_, - sub_pdlp.pdhg_solver_.get_dual_tmp_resource(), - sub_pdlp.pdhg_solver_.get_potential_next_dual_solution()); + sub_conv.compute_primal_residual(sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_dual_tmp_resource(), + sub_pdlp.pdhg_solver_.get_potential_next_dual_solution()); sub_conv.compute_primal_objective_owned_partial( sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), shard->rank_data.owned_var_size); @@ -522,13 +519,12 @@ void convergence_information_t::compute_convergence_information( // Reduce all primal objectives across shards cuopt_assert(!batch_mode_, "multi-GPU PDLP is not supported in batch mode"); - engine->allreduce_sum_inplace( - [](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .get_primal_objective() - .data(); - }); + engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .get_primal_objective() + .data(); + }); // Get the reduced primal objective from the shard[0] (arbitrary) // Sync shards with master stream to avoid race conditions @@ -536,16 +532,15 @@ void convergence_information_t::compute_convergence_information( { auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); - auto& s0_conv = - s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); + auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); raft::copy(primal_objective_.data(), s0_conv.get_primal_objective().data(), 1, stream_view_); } apply_primal_objective_scaling_and_offset(); + } else { + compute_primal_residual( + op_problem_cusparse_view_, current_pdhg_solver.get_dual_tmp_resource(), dual_iterate); + compute_primal_objective(primal_iterate); } - else { - compute_primal_residual( - op_problem_cusparse_view_, current_pdhg_solver.get_dual_tmp_resource(), dual_iterate); - compute_primal_objective(primal_iterate);} #ifdef CUPDLP_DEBUG_MODE print("Primal Residual", primal_residual_); @@ -556,9 +551,7 @@ void convergence_information_t::compute_convergence_information( auto* engine = current_pdhg_solver.get_mgpu_engine(); engine->distributed_l2_norm( [](pdlp_solver_t& sp) -> rmm::device_uvector& { - return sp.get_current_termination_strategy() - .get_convergence_information() - .primal_residual_; + return sp.get_current_termination_strategy().get_convergence_information().primal_residual_; }, [](pdlp_solver_t& sp) -> f_t* { return sp.get_current_termination_strategy() @@ -629,10 +622,9 @@ void convergence_information_t::compute_convergence_information( // cv.dual_solution descriptor, which (cuPDLPx, see // cusparse_view.cu:931-937) is bound to _potential_next_dual -- not to // current.dual_solution. So we must halo-exchange the same buffer. - engine->halo_exchange_cstr( - [](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_potential_next_dual_solution(); - }); + engine->halo_exchange_cstr([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { + return pdhg.get_potential_next_dual_solution(); + }); // 2-3) Per-shard: // - compute_dual_residual: shard.dual_residual_ has owned-var entries @@ -653,11 +645,10 @@ void convergence_information_t::compute_convergence_information( raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); - sub_conv.compute_dual_residual( - sub_conv.op_problem_cusparse_view_, - sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), - sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), - sub_pdlp.pdhg_solver_.get_dual_slack()); + sub_conv.compute_dual_residual(sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), + sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), + sub_pdlp.pdhg_solver_.get_dual_slack()); sub_conv.compute_dual_objective_owned_partial( sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), sub_pdlp.pdhg_solver_.get_dual_slack(), @@ -668,21 +659,19 @@ void convergence_information_t::compute_convergence_information( // 4) Allreduce dual_objective_ across shards (sum, in place). Same // offset/scaling-after-allreduce reasoning as primal: applying offset // per-shard would over-count it Nshards times. - engine->allreduce_sum_inplace( - [](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .get_dual_objective() - .data(); - }); + engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .get_dual_objective() + .data(); + }); // Sync shards with master stream to avoid race conditions engine->sync_await_shards(stream_view_); { auto& s0 = *engine->shards[0]; raft::device_setter guard(s0.device_id); - auto& s0_conv = - s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); + auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); raft::copy(dual_objective_.data(), s0_conv.get_dual_objective().data(), 1, stream_view_); } apply_dual_objective_scaling_and_offset(); @@ -704,9 +693,7 @@ void convergence_information_t::compute_convergence_information( auto* engine = current_pdhg_solver.get_mgpu_engine(); engine->distributed_l2_norm( [](pdlp_solver_t& sp) -> rmm::device_uvector& { - return sp.get_current_termination_strategy() - .get_convergence_information() - .dual_residual_; + return sp.get_current_termination_strategy().get_convergence_information().dual_residual_; }, [](pdlp_solver_t& sp) -> f_t* { return sp.get_current_termination_strategy() @@ -758,7 +745,8 @@ void convergence_information_t::compute_convergence_information( std::numeric_limits::lowest()); } - // In mGPU, full primal_objective and dual_objective already mirrored to master so no special behaviour + // In mGPU, full primal_objective and dual_objective already mirrored to master so no special + // behaviour const auto [grid_size, block_size] = kernel_config_from_batch_size(climber_strategies_.size()); compute_remaining_stats_kernel <<>>(this->view(), climber_strategies_.size()); @@ -1049,12 +1037,11 @@ void convergence_information_t::compute_dual_objective_owned_partial( stream_view_); // dual_objective_ = dual_dot_ + sum_primal_slack_ (still a partial sum). - cub::DeviceTransform::Transform( - cuda::std::make_tuple(dual_dot_.data(), sum_primal_slack_.data()), - dual_objective_.data(), - 1, - cuda::std::plus<>{}, - stream_view_); + cub::DeviceTransform::Transform(cuda::std::make_tuple(dual_dot_.data(), sum_primal_slack_.data()), + dual_objective_.data(), + 1, + cuda::std::plus<>{}, + stream_view_); } template diff --git a/cpp/src/pdlp/utilities/mgpu_trace.cuh b/cpp/src/pdlp/utilities/mgpu_trace.cuh index 06a848b18e..d9975d3202 100644 --- a/cpp/src/pdlp/utilities/mgpu_trace.cuh +++ b/cpp/src/pdlp/utilities/mgpu_trace.cuh @@ -35,18 +35,18 @@ inline bool mgpu_trace_enabled() } // namespace cuopt::linear_programming::detail -#define MGPU_TRACE(msg) \ - do { \ - if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ - std::fprintf(stderr, "[mgpu %s:%d] %s\n", __func__, __LINE__, (msg)); \ - std::fflush(stderr); \ - } \ +#define MGPU_TRACE(msg) \ + do { \ + if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ + std::fprintf(stderr, "[mgpu %s:%d] %s\n", __func__, __LINE__, (msg)); \ + std::fflush(stderr); \ + } \ } while (0) -#define MGPU_TRACE_FMT(fmt, ...) \ - do { \ - if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ - std::fprintf(stderr, "[mgpu %s:%d] " fmt "\n", __func__, __LINE__, __VA_ARGS__); \ - std::fflush(stderr); \ - } \ +#define MGPU_TRACE_FMT(fmt, ...) \ + do { \ + if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ + std::fprintf(stderr, "[mgpu %s:%d] " fmt "\n", __func__, __LINE__, __VA_ARGS__); \ + std::fflush(stderr); \ + } \ } while (0) From 6df81454a13b14e51bf504615996f066e72172ec Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 2 Jun 2026 09:16:25 -0700 Subject: [PATCH 061/258] fixed bound/objective rescaling, now afiro on 8 shards work but hangs in the end --- .../distributed_pdlp/multi_gpu_engine.hpp | 179 ++++++++++++++++++ .../pdlp/distributed_pdlp/partition_loader.cu | 5 +- .../initial_scaling.cu | 47 ++++- .../initial_scaling.cuh | 16 ++ cpp/src/pdlp/pdlp.cu | 13 ++ 5 files changed, 256 insertions(+), 4 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 0297ecc0a6..3a0fcb755d 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -18,16 +18,21 @@ #include #include +#include #include #include #include +#include +#include #include +#include #include #include #include +#include #include #include #include @@ -45,6 +50,29 @@ struct sqrt_inplace_op_t { __host__ __device__ f_t operator()(f_t x) const { return raft::sqrt(x); } }; +// Squared-norm contribution of a constraint's [lower, upper] bound pair, used to +// build the distributed bound rescaling (mirrors rhs_sum_of_squares_t). Defined +// at namespace scope to avoid extended-lambda-in-template restrictions. +template +struct mgpu_rhs_sq_op_t { + __host__ __device__ f_t operator()(const thrust::tuple& t) const + { + const f_t lower = thrust::get<0>(t); + const f_t upper = thrust::get<1>(t); + f_t sum = f_t(0); + if (isfinite(lower) && (lower != upper)) sum += lower * lower; + if (isfinite(upper)) sum += upper * upper; + return sum; + } +}; + +// Weighted square of an objective coefficient (mirrors weighted_square_op). +template +struct mgpu_weighted_sq_op_t { + f_t weight; + __host__ __device__ f_t operator()(f_t v) const { return v * v * weight; } +}; + template struct multi_gpu_engine_t { // Constructs shards from rank_data @@ -219,6 +247,63 @@ struct multi_gpu_engine_t { ncclGroupEnd(); } + // -------- Broadcast owned constraint (row) scaling into halo ------------ + void broadcast_constraint_scaling_to_halo() + { + const int nb = static_cast(shards.size()); + auto buf_access = [](pdlp_shard_t& s) -> rmm::device_uvector& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + }; + + // Gather each owner's owned scaling values that peers need. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto& y = buf_access(s); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (s.cstr_send_indices_d[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + s.cstr_send_indices_d[peer].begin(), + s.cstr_send_indices_d[peer].end(), + y.begin(), + s.cstr_send_buf_d[peer].begin()); + } + } + + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + ncclSend(s.cstr_send_buf_d[peer].data(), + s.cstr_send_buf_d[peer].size(), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + auto& rd = s.rank_data; + raft::device_setter guard(s.device_id); + auto& y = buf_access(s); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; + ncclRecv(recv_ptr, + static_cast(rd.cstr_recv_counts[peer]), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + ncclGroupEnd(); + } + // -------- NCCL allreduce (sum, in place) -------------------------------- // Per-shard in-place sum-allreduce. Each shard's stream issues an // ncclAllReduce(buf, buf, count, ncclFloat64, ncclSum, ...) inside a single @@ -281,6 +366,100 @@ struct multi_gpu_engine_t { }); } + // -------- Distributed bound / objective rescaling ----------------------- + void distributed_bound_objective_rescaling(f_t c_scaling_weight) + { + const int nb = static_cast(shards.size()); + + std::vector> bound_sq; + std::vector> obj_sq; + bound_sq.reserve(nb); + obj_sq.reserve(nb); + + // 1) per-shard partial squared norms over OWNED entries only (halo rhs is + // +/-inf and would otherwise double-count owned entries shared as halo). + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + bound_sq.emplace_back(1, s.stream.view()); + obj_sq.emplace_back(1, s.stream.view()); + + const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); + const int n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); + const int n_owned_var = static_cast(s.rank_data.owned_var_size); + + auto bound_in = thrust::make_transform_iterator( + thrust::make_zip_iterator(scaled.constraint_lower_bounds.data(), + scaled.constraint_upper_bounds.data()), + mgpu_rhs_sq_op_t{}); + size_t tmp_bytes_b = 0; + cub::DeviceReduce::Sum( + nullptr, tmp_bytes_b, bound_in, bound_sq[r].data(), n_owned_cstr, s.stream.view().value()); + rmm::device_buffer scratch_b(tmp_bytes_b, s.stream.view()); + cub::DeviceReduce::Sum(scratch_b.data(), + tmp_bytes_b, + bound_in, + bound_sq[r].data(), + n_owned_cstr, + s.stream.view().value()); + + auto obj_in = thrust::make_transform_iterator(scaled.objective_coefficients.data(), + mgpu_weighted_sq_op_t{c_scaling_weight}); + size_t tmp_bytes_o = 0; + cub::DeviceReduce::Sum( + nullptr, tmp_bytes_o, obj_in, obj_sq[r].data(), n_owned_var, s.stream.view().value()); + rmm::device_buffer scratch_o(tmp_bytes_o, s.stream.view()); + cub::DeviceReduce::Sum(scratch_o.data(), + tmp_bytes_o, + obj_in, + obj_sq[r].data(), + n_owned_var, + s.stream.view().value()); + } + + // 2) NCCL allreduce SUM -> every shard holds the global squared norms. + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + ncclAllReduce(bound_sq[r].data(), + bound_sq[r].data(), + 1, + ncclFloat64, + ncclSum, + s.comm.get(), + s.stream.view().value()); + ncclAllReduce(obj_sq[r].data(), + obj_sq[r].data(), + 1, + ncclFloat64, + ncclSum, + s.comm.get(), + s.stream.view().value()); + } + ncclGroupEnd(); + + // 3) derive the identical scalars and apply on every shard. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + f_t h_bound_sq = f_t(0); + f_t h_obj_sq = f_t(0); + raft::copy(&h_bound_sq, bound_sq[r].data(), 1, s.stream.view()); + raft::copy(&h_obj_sq, obj_sq[r].data(), 1, s.stream.view()); + s.stream.synchronize(); + const f_t bound_rescaling = f_t(1) / (std::sqrt(h_bound_sq) + f_t(1)); + const f_t objective_rescaling = f_t(1) / (std::sqrt(h_obj_sq) + f_t(1)); + s.sub_pdlp->get_initial_scaling_strategy().apply_distributed_bound_objective_rescaling( + bound_rescaling, objective_rescaling); + } + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + s.stream.synchronize(); + } + } + // -------- Generic distributed SpMVs ------------------------------------- // distributed_spmv_A : halo-update the var-shaped input buffer returned by // `in_buf(pdhg)`, then per-shard A @ in_buf -> out_desc. diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 5c317f664e..0ef1eaf4da 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -196,8 +196,7 @@ std::vector> partition_loader_t::create_rank_dat if (peer == rank) continue; for (auto recv_cstr : rank_data[peer].cstr_send_per_peer[rank]) { rd.global_to_local_cstr[recv_cstr] = curr_id; - // rd.local_to_global_cstr.push_back(recv_cstr); // Not needed, we only do local_to_global - // on owned side + rd.local_to_global_cstr.push_back(recv_cstr); curr_id++; } } @@ -212,7 +211,7 @@ std::vector> partition_loader_t::create_rank_dat if (peer == rank) continue; for (auto recv_var : rank_data[peer].var_send_per_peer[rank]) { rd.global_to_local_var[recv_var] = curr_id; - // rd.local_to_global_var.push_back(recv_var); // same as over + rd.local_to_global_var.push_back(recv_var); curr_id++; } } diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index dcc3e662b0..cb498b3756 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -644,7 +644,8 @@ void pdlp_initial_scaling_strategy_t::scale_problem() cuda::std::multiplies{}, stream_view_); - if (hyper_params_.bound_objective_rescaling && !running_mip_) { + if (hyper_params_.bound_objective_rescaling && !running_mip_ && + !skip_distributed_local_rescaling_) { // Coefficients are computed on the already scaled values bound_objective_rescaling(); @@ -957,6 +958,50 @@ const problem_t& pdlp_initial_scaling_strategy_t::get_scaled return op_problem_scaled_; } +template +void pdlp_initial_scaling_strategy_t::apply_distributed_bound_objective_rescaling( + f_t bound_rescaling, f_t objective_rescaling) +{ + using f_t2 = typename type_2::type; + + // constraint bounds *= bound_rescaling (matches scale_problem() bound block) + cub::DeviceTransform::Transform( + cuda::std::make_tuple(op_problem_scaled_.constraint_lower_bounds.data(), + op_problem_scaled_.constraint_upper_bounds.data()), + thrust::make_zip_iterator(op_problem_scaled_.constraint_lower_bounds.data(), + op_problem_scaled_.constraint_upper_bounds.data()), + op_problem_scaled_.constraint_upper_bounds.size(), + [bound_rescaling] __device__(f_t lower, f_t upper) -> thrust::tuple { + return {lower * bound_rescaling, upper * bound_rescaling}; + }, + stream_view_.value()); + + // variable bounds *= bound_rescaling (batch-1 path only; distributed is batch 1) + cub::DeviceTransform::Transform( + op_problem_scaled_.variable_bounds.data(), + op_problem_scaled_.variable_bounds.data(), + op_problem_scaled_.variable_bounds.size(), + [bound_rescaling] __device__(f_t2 variable_bounds) -> f_t2 { + return {variable_bounds.x * bound_rescaling, variable_bounds.y * bound_rescaling}; + }, + stream_view_); + + // objective *= objective_rescaling + cub::DeviceTransform::Transform( + op_problem_scaled_.objective_coefficients.data(), + op_problem_scaled_.objective_coefficients.data(), + op_problem_scaled_.objective_coefficients.size(), + [objective_rescaling] __device__(f_t c) -> f_t { return c * objective_rescaling; }, + stream_view_); + + // Store the factors (sets both host copies and the device rescaling vectors) + // so unscale_solutions() / scale_solutions() apply them consistently. The flag + // hyper_params_.bound_objective_rescaling stays true on shards so those paths + // are active; only scale_problem()'s local recompute is skipped. + set_h_bound_rescaling(bound_rescaling); + set_h_objective_rescaling(objective_rescaling); +} + template const rmm::device_uvector& pdlp_initial_scaling_strategy_t::get_constraint_matrix_scaling_vector() const diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 148ccce238..409df5340a 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -75,6 +75,11 @@ class pdlp_initial_scaling_strategy_t { rmm::device_uvector& dual_slack) const; void unscale_solutions(solution_t& solution) const; const rmm::device_uvector& get_constraint_matrix_scaling_vector() const; + // Mutable access needed by distributed PDLP to broadcast owned constraint + rmm::device_uvector& get_cummulative_constraint_matrix_scaling() + { + return cummulative_constraint_matrix_scaling_; + } const rmm::device_uvector& get_variable_scaling_vector() const; const problem_t& get_scaled_op_problem(); @@ -94,6 +99,14 @@ class pdlp_initial_scaling_strategy_t { void bound_objective_rescaling(); + // Distributed PDLP: apply an externally-computed GLOBAL bound / objective + // rescaling to the already-scaled problem. + void apply_distributed_bound_objective_rescaling(f_t bound_rescaling, f_t objective_rescaling); + + // Distributed PDLP: skip the LOCAL bound/objective rescaling inside + // scale_problem() + void set_skip_distributed_local_rescaling(bool value) { skip_distributed_local_rescaling_ = value; } + // Public for distributed PDLP void compute_scaling_vectors(i_t number_of_ruiz_iterations, f_t alpha); @@ -144,5 +157,8 @@ class pdlp_initial_scaling_strategy_t { rmm::device_uvector& A_T_indices_; const pdlp_hyper_params::pdlp_hyper_params_t& hyper_params_; bool running_mip_; + // Distributed PDLP: when true, scale_problem() skips its local + // bound/objective rescaling (the global factor is applied separately). + bool skip_distributed_local_rescaling_{false}; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 576ab417f1..4200b487c8 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -591,14 +591,21 @@ pdlp_solver_t::pdlp_solver_t( multi_gpu_engine->distributed_ruiz_inf_scaling( settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); } + // push local scaling to halo + multi_gpu_engine->broadcast_constraint_scaling_to_halo(); if (settings_.hyper_params.do_pock_chambolle_scaling) { multi_gpu_engine->distributed_pock_chambolle_scaling( static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), n_vars); } + // Refresh the halo constraint scaling after Pock-Chambolle + multi_gpu_engine->broadcast_constraint_scaling_to_halo(); for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); auto& scaling = shard->sub_pdlp->get_initial_scaling_strategy(); + // Skip the per-shard local bound/objective rescaling; the global factor is + // applied below. Keeps the unscale path active (flag stays true). + scaling.set_skip_distributed_local_rescaling(true); scaling.scale_problem(); shard->sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( @@ -609,6 +616,12 @@ pdlp_solver_t::pdlp_solver_t( shard->stream.synchronize(); } + // Global bound/objective rescaling: allreduce the owned partial squared-norms + if (settings_.hyper_params.bound_objective_rescaling && !inside_mip_) { + multi_gpu_engine->distributed_bound_objective_rescaling( + static_cast(settings_.hyper_params.initial_primal_weight_c_scaling)); + } + // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- constexpr f_t kStepSizeScale = f_t{0.998}; const f_t sigma_max = multi_gpu_engine->distributed_max_singular_value(n_cstr); From df9f79366cfc9a997bd59480bae9ae623edafcc6 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 2 Jun 2026 12:29:09 -0700 Subject: [PATCH 062/258] actually disable the graph ^^ (kms) --- cpp/src/pdlp/solve.cu | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 156a601b29..228bacfd21 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2155,6 +2155,10 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( "use_distributed_pdlp; please set settings.presolver = presolver_t::None"); pdlp_solver_settings_t settings_resolved = settings; + + detail::pdlp_graph_disabled_flag().store(settings_resolved.hyper_params.pdlp_disable_graph, + std::memory_order_relaxed); + if (settings_resolved.distributed_pdlp_num_gpus == -1) { settings_resolved.distributed_pdlp_num_gpus = raft::device_setter::get_device_count(); CUOPT_LOG_INFO( From 4c8bcd1a6710fc4b56b74d1d99ed21f39221b0c1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 4 Jun 2026 12:21:34 +0200 Subject: [PATCH 063/258] added option to export parts file --- .../cuopt/linear_programming/constants.h | 1 + .../pdlp/solver_settings.hpp | 5 +++++ cpp/src/math_optimization/solver_settings.cu | 1 + .../pdlp/distributed_pdlp/partition_loader.cu | 18 ++++++++++++++++++ .../pdlp/distributed_pdlp/partition_loader.hpp | 6 ++++++ cpp/src/pdlp/pdlp.cu | 9 +++++++++ 6 files changed, 40 insertions(+) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index e2cc264cdc..e24ca5c346 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -85,6 +85,7 @@ #define CUOPT_NUM_GPUS "num_gpus" #define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" #define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" +#define CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE "multi_gpu_export_partition_file" #define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" #define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index efdbd5733c..1443333df4 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -311,6 +311,11 @@ class pdlp_solver_settings_t { // -1 means auto-detect int distributed_pdlp_num_gpus{-1}; std::string multi_gpu_partition_file{""}; + // If non-empty, the partition computed for distributed PDLP is written to this + // path (one part-id per line) right after partitioning. The file can be fed + // back via multi_gpu_partition_file. Exposed as the multi_gpu_export_partition_file + // parameter (CLI: --multi-gpu-export-partition-file ). + std::string multi_gpu_export_partition_file{""}; // Set to true inside the shards bool is_distributed_sub_pdlp{false}; method_t method{method_t::Concurrent}; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 629c8a8428..87324524f1 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -192,6 +192,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_PRESOLVE_FILE, &mip_settings.presolve_file, ""}, {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""}, {CUOPT_MULTI_GPU_PARTITION_FILE, &pdlp_settings.multi_gpu_partition_file, ""}, + {CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE, &pdlp_settings.multi_gpu_export_partition_file, ""}, }; // clang-format on } diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 0ef1eaf4da..a6db3a9fe8 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -40,6 +40,24 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ return parts; } +template +void partition_loader_t::export_distributed_pdlp_partition_file( + std::string const& file, std::vector const& parts) +{ + std::ofstream part_file(file); + cuopt_expects(part_file.is_open(), + error_type_t::ValidationError, + "Failed to open partition file for export: %s", + file.c_str()); + for (auto const& part : parts) { + part_file << part << "\n"; + } + cuopt_expects(part_file.good(), + error_type_t::RuntimeError, + "Failed while writing partition file: %s", + file.c_str()); +} + template std::vector> partition_loader_t::create_rank_data_from_parts( const std::vector& parts, diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index 915c24a828..ce12d241f9 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -19,6 +19,12 @@ struct partition_loader_t { // nb_cstr + nb_vars, indexed as in create_rank_data_from_parts (cstrs first, then vars). static std::vector parse_distributed_pdlp_partition_file(std::string const& file); + // Write a partition vector to file in the same format parse_... reads back: + // one part-id per line. Useful for inspecting / reusing a computed partition + // (e.g. CLI --distributed-pdlp-export-parts). + static void export_distributed_pdlp_partition_file(std::string const& file, + std::vector const& parts); + // Slices the data to prepare a split from metis partitionning with halo communication static std::vector> create_rank_data_from_parts( const std::vector& parts, diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 4200b487c8..150311ae33 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -530,6 +530,14 @@ pdlp_solver_t::pdlp_solver_t( parts = partitioner->partition(partition_input); } + // Optionally dump the partition right after computing it (one part-id per line). + if (!settings.multi_gpu_export_partition_file.empty()) { + partition_loader_t::export_distributed_pdlp_partition_file( + settings.multi_gpu_export_partition_file, parts); + std::cout << "Exported " << parts.size() << " part-ids to " + << settings.multi_gpu_export_partition_file << std::endl; + } + // ----- 5. Build per-rank data ----- std::vector> sub_pdlp_rank_data = partition_loader_t::create_rank_data_from_parts(parts, @@ -551,6 +559,7 @@ pdlp_solver_t::pdlp_solver_t( sub_pdlp_settings.num_gpus = 1; sub_pdlp_settings.distributed_pdlp_num_gpus = 1; sub_pdlp_settings.multi_gpu_partition_file = ""; + sub_pdlp_settings.multi_gpu_export_partition_file = ""; sub_pdlp_settings.is_distributed_sub_pdlp = true; sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; From a8a8054b36333ffebeaba6312c2d998bfa9156ec Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 4 Jun 2026 13:29:27 +0200 Subject: [PATCH 064/258] addded test for import export parts file --- cpp/tests/linear_programming/pdlp_test.cu | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index d29995efc5..b20ce4a1c9 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -8,12 +8,16 @@ #include #include #include +#include +#include #include #include #include #include #include +#include + #include "utilities/pdlp_test_utilities.cuh" #include "../mip/mip_utils.cuh" @@ -91,6 +95,64 @@ TEST(pdlp_class, run_double) afiro_primal_objective, solution.get_additional_termination_information().primal_objective)); } +// Distributed-PDLP partition round-trip: partition the afiro constraint/variable +// bipartite graph with METIS, write it out, read it back, and confirm the parsed +// vector is identical to what the partitioner produced. +TEST(pdlp_class, distributed_partition_metis_export_import_roundtrip) +{ + using namespace cuopt::linear_programming::detail; + namespace ds = cuopt::linear_programming::dual_simplex; + + auto path = make_path_absolute("linear_programming/afiro_original.mps"); + cuopt::linear_programming::io::mps_data_model_t mps = + cuopt::linear_programming::io::parse_mps(path, true); + + const int n_vars = static_cast(mps.get_objective_coefficients().size()); + const int n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); + const int nnz = static_cast(mps.get_constraint_matrix_values().size()); + + std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); + std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); + std::vector h_A_values = mps.get_constraint_matrix_values(); + + // Transpose A -> A^T (CSR of A^T == CSC of A), mirroring solve_lp_distributed_from_mps. + ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); + A_csr.row_start = h_A_row_offsets; + A_csr.j = h_A_col_indices; + A_csr.x = h_A_values; + ds::csc_matrix_t AT_as_csc(n_vars, n_cstr, nnz); + A_csr.to_compressed_col(AT_as_csc); + std::vector h_A_t_row_offsets = AT_as_csc.col_start; + std::vector h_A_t_col_indices = AT_as_csc.i; + + partitioner_input_t input; + input.nb_cstr = n_cstr; + input.nb_vars = n_vars; + input.nb_parts = 2; + input.A.row_offsets = &h_A_row_offsets; + input.A.col_indices = &h_A_col_indices; + input.A.num_rows = n_cstr; + input.A.num_cols = n_vars; + input.A_t.row_offsets = &h_A_t_row_offsets; + input.A_t.col_indices = &h_A_t_col_indices; + input.A_t.num_rows = n_vars; + input.A_t.num_cols = n_cstr; + + auto partitioner = make_partitioner(partitioner_kind_t::Metis); + std::vector parts = partitioner->partition(input); + ASSERT_EQ(parts.size(), static_cast(n_cstr + n_vars)); + + std::string dir = ::testing::TempDir(); + if (!dir.empty() && dir.back() != '/') { dir.push_back('/'); } + const std::string out_path = dir + "afiro_metis_roundtrip.parts"; + + partition_loader_t::export_distributed_pdlp_partition_file(out_path, parts); + std::vector reloaded = + partition_loader_t::parse_distributed_pdlp_partition_file(out_path); + + EXPECT_EQ(parts, reloaded); +} + TEST(pdlp_class, precision_mixed) { using namespace cuopt::linear_programming::detail; From 5abcd2e0feaa00efa9a43daa1be94cf4cb89f034 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 4 Jun 2026 14:43:36 +0200 Subject: [PATCH 065/258] added full solve tests --- cpp/tests/linear_programming/pdlp_test.cu | 104 ++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index b20ce4a1c9..65cc2f0d9f 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -49,11 +49,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -153,6 +155,108 @@ TEST(pdlp_class, distributed_partition_metis_export_import_roundtrip) EXPECT_EQ(parts, reloaded); } +namespace { + +// Solve `mps_rel_path` with the single-GPU PDLP ("base") and with distributed PDLP +// (num_gpus = -1 => auto-detect; 1 GPU is fine), then assert the distributed run +// matches the base run on everything meaningful: termination status, step count +// (within 15%), primal/dual objective, and the full primal/dual solution vectors. +// All value comparisons use a loose relative tolerance. +void expect_distributed_matches_base(raft::handle_t const& handle, + std::string const& mps_rel_path, + bool fixed_mps_format = false) +{ + constexpr double loose_rel = 1e-3; + auto near_rel = [](double a, double b, double rel) { + return std::fabs(a - b) <= rel * (1.0 + std::fabs(a)); + }; + + auto path = make_path_absolute(mps_rel_path); + io::mps_data_model_t problem = io::parse_mps(path, fixed_mps_format); + + // Shared settings: PDLP, no presolve (distributed requires presolver == None, so the + // base run must match to keep the two problems identical). + pdlp_solver_settings_t base_settings{}; + base_settings.method = method_t::PDLP; + base_settings.presolver = presolver_t::None; + + // ----- base: single-GPU PDLP (materialize the full problem on one GPU) ----- + auto base_op = mps_data_model_to_optimization_problem(&handle, problem); + auto base = solve_lp(base_op, base_settings); + + // ----- distributed PDLP (identical settings, only the distributed flags flipped) ----- + pdlp_solver_settings_t dist_settings = base_settings; + dist_settings.hyper_params.use_distributed_pdlp = true; + dist_settings.distributed_pdlp_num_gpus = -1; + auto dist = solve_lp(&handle, problem, dist_settings); + + // ----- termination status ----- + ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) + << mps_rel_path << ": base did not reach optimal"; + EXPECT_EQ(static_cast(dist.get_termination_status()), + static_cast(base.get_termination_status())) + << mps_rel_path << ": distributed termination status differs from base"; + + const auto& base_info = base.get_additional_termination_information(); + const auto& dist_info = dist.get_additional_termination_information(); + + // ----- objectives ----- + EXPECT_TRUE(near_rel(base_info.primal_objective, dist_info.primal_objective, loose_rel)) + << mps_rel_path << ": primal objective base=" << base_info.primal_objective + << " distributed=" << dist_info.primal_objective; + EXPECT_TRUE(near_rel(base_info.dual_objective, dist_info.dual_objective, loose_rel)) + << mps_rel_path << ": dual objective base=" << base_info.dual_objective + << " distributed=" << dist_info.dual_objective; + + // ----- step count: within 15% of the larger of the two ----- + const int base_steps = base_info.number_of_steps_taken; + const int dist_steps = dist_info.number_of_steps_taken; + const int max_steps = std::max(base_steps, dist_steps); + const int step_diff = std::max(base_steps, dist_steps) - std::min(base_steps, dist_steps); + EXPECT_LE(static_cast(step_diff), 0.15 * max_steps) + << mps_rel_path << ": step counts differ by >15% (base=" << base_steps + << ", distributed=" << dist_steps << ")"; + + // ----- primal / dual solution vectors ----- + auto base_primal = cuopt::host_copy(base.get_primal_solution(), handle.get_stream()); + auto dist_primal = cuopt::host_copy(dist.get_primal_solution(), handle.get_stream()); + ASSERT_EQ(base_primal.size(), dist_primal.size()) << mps_rel_path << ": primal size mismatch"; + for (std::size_t i = 0; i < base_primal.size(); ++i) { + EXPECT_TRUE(near_rel(base_primal[i], dist_primal[i], loose_rel)) + << mps_rel_path << ": primal[" << i << "] base=" << base_primal[i] + << " distributed=" << dist_primal[i]; + } + + auto base_dual = cuopt::host_copy(base.get_dual_solution(), handle.get_stream()); + auto dist_dual = cuopt::host_copy(dist.get_dual_solution(), handle.get_stream()); + ASSERT_EQ(base_dual.size(), dist_dual.size()) << mps_rel_path << ": dual size mismatch"; + for (std::size_t i = 0; i < base_dual.size(); ++i) { + EXPECT_TRUE(near_rel(base_dual[i], dist_dual[i], loose_rel)) + << mps_rel_path << ": dual[" << i << "] base=" << base_dual[i] + << " distributed=" << dist_dual[i]; + } +} + +} // namespace + +TEST(pdlp_class, distributed_parity_afiro) +{ + const raft::handle_t handle{}; + expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); +} + +TEST(pdlp_class, distributed_parity_square41) +{ + const raft::handle_t handle{}; + expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); +} + +TEST(pdlp_class, distributed_parity_a2864) +{ + const raft::handle_t handle{}; + expect_distributed_matches_base(handle, "linear_programming/a2864/a2864.mps"); +} + TEST(pdlp_class, precision_mixed) { using namespace cuopt::linear_programming::detail; From 0b0ce2ccd9b2d4f2e1273d7c5a548f81619836ba Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 4 Jun 2026 16:15:58 +0200 Subject: [PATCH 066/258] added kaminpar partitionner and possibility to chose the partitionner --- cpp/CMakeLists.txt | 12 ++ cpp/cmake/thirdparty/get_kaminpar.cmake | 48 ++++++ .../cuopt/linear_programming/constants.h | 1 + .../pdlp/solver_settings.hpp | 8 + cpp/src/math_optimization/solver_settings.cu | 1 + cpp/src/pdlp/CMakeLists.txt | 1 + .../distributed_pdlp/kaminpar_partitioner.cpp | 142 ++++++++++++++++++ .../distributed_pdlp/kaminpar_partitioner.hpp | 23 +++ .../distributed_pdlp/metis_partitioner.cu | 13 +- cpp/src/pdlp/distributed_pdlp/partitioner.cu | 3 + cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 9 +- cpp/src/pdlp/pdlp.cu | 54 +++++-- cpp/src/pdlp/solve.cu | 5 - 13 files changed, 291 insertions(+), 29 deletions(-) create mode 100644 cpp/cmake/thirdparty/get_kaminpar.cmake create mode 100644 cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp create mode 100644 cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d27072bcf9..0bf2b0f3f7 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -378,6 +378,17 @@ set_target_properties(metis_external PROPERTIES ) message(STATUS "Using METIS: ${METIS_LIBRARY}") +# ################################################################################################## +# - KaMinPar (multi-threaded partitioning for distributed PDLP) ------------------------------------ +# Brought in the RAPIDS way (rapids_cpm_find): uses an installed KaMinPar (deb/rpm/conda, +# discovered via its CMake config) if present, otherwise builds the pinned source via CPM. +# Distributed PDLP prefers KaMinPar over METIS. +include(cmake/thirdparty/get_kaminpar.cmake) +if (NOT TARGET KaMinPar::KaMinPar) + message(FATAL_ERROR "KaMinPar::KaMinPar was not made available by get_kaminpar.cmake") +endif () +message(STATUS "Using KaMinPar (distributed PDLP prefers KaMinPar over METIS)") + # ################################################################################################## # - gRPC and Protobuf setup ----------------------------------------------------------------------- @@ -642,6 +653,7 @@ target_link_libraries(cuopt ${CUOPT_PRIVATE_CUDA_LIBS} nccl_external metis_external + KaMinPar::KaMinPar $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> ) diff --git a/cpp/cmake/thirdparty/get_kaminpar.cmake b/cpp/cmake/thirdparty/get_kaminpar.cmake new file mode 100644 index 0000000000..d548a76115 --- /dev/null +++ b/cpp/cmake/thirdparty/get_kaminpar.cmake @@ -0,0 +1,48 @@ +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on + +# Multi-threaded graph partitioner for distributed PDLP. +# Uses rapids_cpm_find so a system / conda / .deb install of KaMinPar (which ships a +# CMake config package exporting KaMinPar::KaMinPar) is used when available, and +# otherwise the pinned source is cloned and built via CPM. KaMinPar depends on TBB, +# which cuOpt already requires (see find_package(TBB) for papilo). +function(find_and_configure_kaminpar) + set(oneValueArgs VERSION PINNED_TAG) + cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN}) + + rapids_cpm_find(KaMinPar ${PKG_VERSION} + GLOBAL_TARGETS KaMinPar::KaMinPar + CPM_ARGS + GIT_REPOSITORY https://github.com/KaHIP/KaMinPar.git + GIT_TAG ${PKG_PINNED_TAG} + EXCLUDE_FROM_ALL + OPTIONS + "KAMINPAR_BUILD_APPS OFF" + "KAMINPAR_BUILD_TOOLS OFF" + "KAMINPAR_BUILD_TESTS OFF" + "KAMINPAR_BUILD_BENCHMARKS OFF" + "KAMINPAR_BUILD_EXAMPLES OFF" + "KAMINPAR_BUILD_DISTRIBUTED OFF" + # Timers use global state and force single-threaded use of the library + # interface; disable so cuOpt can call the partitioner freely. + "KAMINPAR_ENABLE_TIMERS OFF" + # Avoid an extra hard dependency on Google Sparsehash. + "KAMINPAR_BUILD_WITH_SPARSEHASH OFF" + # cuOpt's TBB is discovered via a legacy find that only exposes TBB::tbb + # (no TBB::tbbmalloc target); disable KaMinPar's optional tbbmalloc use. + "KAMINPAR_ENABLE_TBB_MALLOC OFF" + # Large LP constraint graphs can exceed 2^31 directed edges. + "KAMINPAR_64BIT_EDGE_IDS ON" + "INSTALL_KAMINPAR OFF" + ) + + if(KaMinPar_ADDED) + message(VERBOSE "CUOPT: Using KaMinPar located in ${KaMinPar_SOURCE_DIR}") + else() + message(VERBOSE "CUOPT: Using KaMinPar located in ${KaMinPar_DIR}") + endif() +endfunction() + +find_and_configure_kaminpar(VERSION 3.7.3 PINNED_TAG v3.7.3) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index e24ca5c346..420a03526b 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -86,6 +86,7 @@ #define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" #define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" #define CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE "multi_gpu_export_partition_file" +#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" #define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" #define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index 1443333df4..42ef1f592a 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -316,6 +316,14 @@ class pdlp_solver_settings_t { // back via multi_gpu_partition_file. Exposed as the multi_gpu_export_partition_file // parameter (CLI: --multi-gpu-export-partition-file ). std::string multi_gpu_export_partition_file{""}; + // Which graph partitioner distributed PDLP uses. One of: + // "auto" - 1 GPU => Dummy; otherwise KaMinPar + // "dummy" - round-robin, no graph (trivial) + // "metis" - serial METIS_PartGraphKway + // "kaminpar" - multi-threaded KaMinPar + // Exposed as the distributed_pdlp_partitioner parameter + // (CLI: --distributed-pdlp-partitioner ). + std::string distributed_pdlp_partitioner{"auto"}; // Set to true inside the shards bool is_distributed_sub_pdlp{false}; method_t method{method_t::Concurrent}; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 87324524f1..254a3afb38 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -193,6 +193,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""}, {CUOPT_MULTI_GPU_PARTITION_FILE, &pdlp_settings.multi_gpu_partition_file, ""}, {CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE, &pdlp_settings.multi_gpu_export_partition_file, ""}, + {CUOPT_DISTRIBUTED_PDLP_PARTITIONER, &pdlp_settings.distributed_pdlp_partitioner, "auto"}, }; // clang-format on } diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 863cf20962..12f2550203 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -34,6 +34,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/metis_partitioner.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/kaminpar_partitioner.cpp ) # C and Python adapter files diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp new file mode 100644 index 0000000000..e7bf943f92 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Plain C++ translation unit (not .cu): KaMinPar's public header is C++20 host code +// and pulls in TBB; keeping it out of nvcc avoids device-compiler friction. + +#include +#include + +#include + +#include + +#include + +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +// Builds the bipartite constraint/variable graph induced by A (identical layout +// to metis_partitioner_t) and runs the multi-threaded KaMinPar k-way kernel. +// * nodes [0, nb_cstr) : constraint nodes +// * nodes [nb_cstr, nb_cstr+nb_vars): variable nodes +// * undirected edges from each A nonzero (one half via A, one via A_t) +template +std::vector kaminpar_partitioner_t::partition( + partitioner_input_t const& input) const +{ + cuopt_expects(input.nb_parts >= 1, + error_type_t::ValidationError, + "kaminpar_partitioner: nb_parts must be >= 1"); + cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, + error_type_t::ValidationError, + "kaminpar_partitioner: invalid problem dimensions"); + + // The k-way kernel needs at least 2 blocks. For the single-shard case the + // partition is trivial (everything in block 0); short-circuit so KaMinPar can + // still be selected with distributed_pdlp_num_gpus == 1 without crashing. + if (input.nb_parts == 1) { + CUOPT_LOG_INFO("KaMinPar: nb_parts == 1, returning trivial single-block partition"); + return std::vector(static_cast(input.nb_cstr + input.nb_vars), i_t{0}); + } + cuopt_expects(input.A.row_offsets != nullptr && input.A.col_indices != nullptr, + error_type_t::ValidationError, + "kaminpar_partitioner: A.row_offsets and A.col_indices are required"); + cuopt_expects(input.A_t.row_offsets != nullptr && input.A_t.col_indices != nullptr, + error_type_t::ValidationError, + "kaminpar_partitioner: A_t.row_offsets and A_t.col_indices are required"); + + auto const& A_offsets = *input.A.row_offsets; + auto const& A_cols = *input.A.col_indices; + auto const& A_t_offsets = *input.A_t.row_offsets; + auto const& A_t_cols = *input.A_t.col_indices; + + cuopt_expects(static_cast(A_offsets.size()) == input.nb_cstr + 1, + error_type_t::ValidationError, + "kaminpar_partitioner: A.row_offsets size mismatch (expected nb_cstr+1)"); + cuopt_expects(static_cast(A_t_offsets.size()) == input.nb_vars + 1, + error_type_t::ValidationError, + "kaminpar_partitioner: A_t.row_offsets size mismatch (expected nb_vars+1)"); + cuopt_expects(A_cols.size() == A_t_cols.size(), + error_type_t::ValidationError, + "kaminpar_partitioner: A and A_t nnz mismatch"); + + const i_t nb_cstr = input.nb_cstr; + const i_t nb_vars = input.nb_vars; + const i_t nnz = static_cast(A_cols.size()); + const i_t nvtx = nb_cstr + nb_vars; + + // Resolve thread count: <= 0 => all hardware threads (1 as a last resort). + int nthreads = input.nb_threads > 0 ? static_cast(input.nb_threads) : 0; + if (nthreads <= 0) { + nthreads = static_cast(std::thread::hardware_concurrency()); + if (nthreads <= 0) { nthreads = 1; } + } + + // Bipartite CSR using KaMinPar index types (EdgeID for offsets, NodeID for neighbours). + std::vector xadj(static_cast(nvtx) + 1); + std::vector adjncy(2 * static_cast(nnz)); + + for (i_t i = 0; i <= nb_cstr; ++i) { + xadj[i] = static_cast(A_offsets[i]); + } + for (i_t i = 0; i <= nb_vars; ++i) { + xadj[nb_cstr + i] = + static_cast(A_t_offsets[i]) + static_cast(nnz); + } + for (i_t k = 0; k < nnz; ++k) { + adjncy[k] = + static_cast(A_cols[k]) + static_cast(nb_cstr); + } + for (i_t k = 0; k < nnz; ++k) { + adjncy[nnz + k] = static_cast(A_t_cols[k]); + } + + std::vector block_of(static_cast(nvtx)); + + kaminpar::KaMinPar engine(nthreads, kaminpar::shm::create_default_context()); + engine.copy_graph(std::span(xadj), + std::span(adjncy)); + engine.set_k(static_cast(input.nb_parts)); + // ~3% imbalance, matching METIS_PartGraphKway's default balance constraint. + engine.set_uniform_max_block_weights(0.03); + + auto t0 = std::chrono::high_resolution_clock::now(); + const kaminpar::shm::EdgeWeight edge_cut = + engine.compute_partition(std::span(block_of)); + auto t1 = std::chrono::high_resolution_clock::now(); + const double dt = std::chrono::duration(t1 - t0).count(); + + CUOPT_LOG_INFO( + "KaMinPar partitioned bipartite graph: nvtx=%d nnz=%d nb_parts=%d nthreads=%d edge_cut=%lld " + "in %.3fs", + static_cast(nvtx), + static_cast(nnz), + static_cast(input.nb_parts), + nthreads, + static_cast(edge_cut), + dt); + + std::vector parts(static_cast(nvtx)); + for (i_t i = 0; i < nvtx; ++i) { + parts[i] = static_cast(block_of[i]); + } + + validate_partition(parts, + static_cast(nb_cstr), + static_cast(nb_vars), + static_cast(input.nb_parts), + "kaminpar_partitioner"); + return parts; +} + +template class kaminpar_partitioner_t; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp new file mode 100644 index 0000000000..43fda76f9f --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace cuopt::linear_programming::detail { + +// Multi-threaded k-way partitioner backed by KaMinPar. Builds the same +// constraint/variable bipartite graph as metis_partitioner_t, but runs the +// shared-memory parallel KaMinPar kernel so partitioning scales across all CPU +// cores of a node (set via partitioner_input_t::nb_threads; <= 0 => all +// hardware threads). +template +class kaminpar_partitioner_t : public partitioner_i { + public: + std::vector partition(partitioner_input_t const& input) const override; +}; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu index ecc60adda0..9a4f0f50b1 100644 --- a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu @@ -32,18 +32,15 @@ std::vector metis_partitioner_t::partition( cuopt_expects(input.nb_parts > 0, error_type_t::ValidationError, "metis_partitioner: nb_parts must be positive"); - // METIS_PartGraphKway internally does integer arithmetic of the form - // `nedges / nparts` and traps with SIGFPE when nparts == 1. The single-part - // case is also trivial (everything in part 0) so callers should route it to - // the Dummy partitioner instead (see pdlp_solver_t mGPU ctor). - cuopt_expects(input.nb_parts >= 2, - error_type_t::ValidationError, - "metis_partitioner: nb_parts must be >= 2 (METIS_PartGraphKway requirement); " - "use the Dummy partitioner for the single-shard case"); cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, error_type_t::ValidationError, "metis_partitioner: invalid problem dimensions"); + if (input.nb_parts == 1) { + CUOPT_LOG_INFO("METIS: nb_parts == 1, returning trivial single-block partition"); + return std::vector(static_cast(input.nb_cstr + input.nb_vars), i_t{0}); + } + cuopt_expects(input.A.row_offsets != nullptr && input.A.col_indices != nullptr, error_type_t::ValidationError, "metis_partitioner: A.row_offsets and A.col_indices are required"); diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cu b/cpp/src/pdlp/distributed_pdlp/partitioner.cu index bc84e521e2..e3866c3ad1 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cu @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include @@ -72,6 +73,8 @@ std::unique_ptr> make_partitioner(partitioner_kind_t kin switch (kind) { case partitioner_kind_t::Dummy: return std::make_unique>(); case partitioner_kind_t::Metis: return std::make_unique>(); + case partitioner_kind_t::KaMinPar: + return std::make_unique>(); } cuopt_expects( false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index 2a2149db63..70b2e34c06 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -29,6 +29,10 @@ struct partitioner_input_t { i_t nb_cstr{0}; i_t nb_vars{0}; i_t nb_parts{0}; + // Number of CPU threads the partitioner may use. Only honored by the + // multi-threaded KaMinPar backend; <= 0 means "auto" (all hardware threads). + // Serial backends (METIS, Dummy) ignore it. + i_t nb_threads{0}; // Constraint matrix A (rows = constraints, cols = variables). csr_host_view_t A{}; // Transpose A_t (rows = variables, cols = constraints). Optional for partitioners @@ -36,7 +40,10 @@ struct partitioner_input_t { csr_host_view_t A_t{}; }; -enum class partitioner_kind_t { Dummy, Metis }; +// Dummy: round-robin, no graph (single-shard / debugging). +// Metis: serial METIS_PartGraphKway. +// KaMinPar: multi-threaded KaMinPar (preferred for multi-shard partitioning). +enum class partitioner_kind_t { Dummy, Metis, KaMinPar }; template class partitioner_i { diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 150311ae33..0514ae1d13 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -44,6 +44,7 @@ #include #include +#include #include #include #include @@ -398,12 +399,6 @@ pdlp_solver_t::pdlp_solver_t( const int distributed_pdlp_num_gpus = settings.distributed_pdlp_num_gpus; CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU (mps direct path)", distributed_pdlp_num_gpus); - if (distributed_pdlp_num_gpus == 1) { - std::cout << "CAREFUL !!: distributed_pdlp_num_gpus == 1, running single-shard dummy path, " - "if you want to set the number of GPUs to use for distributed PDLP, set the " - "parameter --distributed-pdlp-num-gpus" - << std::endl; - } if constexpr (!std::is_same_v) { cuopt_expects( @@ -501,20 +496,37 @@ pdlp_solver_t::pdlp_solver_t( settings.multi_gpu_partition_file); validate_partition(parts, n_cstr, n_vars, distributed_pdlp_num_gpus, "partition file"); } else { - if (distributed_pdlp_num_gpus == 1) { - std::cout << "CAREFUL: distributed_pdlp_num_gpus == 1, running dummy version (single " - "part covering " - << n_cstr << " cstrs + " << n_vars << " vars)" << std::endl; - } partitioner_input_t partition_input; partition_input.nb_cstr = n_cstr; partition_input.nb_vars = n_vars; partition_input.nb_parts = distributed_pdlp_num_gpus; - // METIS_PartGraphKway requires nparts >= 2; route num_gpus == 1 to Dummy. - const partitioner_kind_t kind = - (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::Metis; - if (kind == partitioner_kind_t::Metis) { + // Resolve which partitioner to use. + std::string partitioner_choice = settings.distributed_pdlp_partitioner; + std::transform(partitioner_choice.begin(), + partitioner_choice.end(), + partitioner_choice.begin(), + [](unsigned char c) { return std::tolower(c); }); + partitioner_kind_t kind; + if (partitioner_choice.empty() || partitioner_choice == "auto") { + kind = (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy + : partitioner_kind_t::KaMinPar; + } else if (partitioner_choice == "dummy") { + kind = partitioner_kind_t::Dummy; + } else if (partitioner_choice == "metis") { + kind = partitioner_kind_t::Metis; + } else if (partitioner_choice == "kaminpar") { + kind = partitioner_kind_t::KaMinPar; + } else { + cuopt_expects(false, + error_type_t::ValidationError, + "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|metis|kaminpar)", + settings.distributed_pdlp_partitioner.c_str()); + kind = partitioner_kind_t::Dummy; // unreachable; silences -Wmaybe-uninitialized + } + const bool needs_graph = + (kind == partitioner_kind_t::Metis || kind == partitioner_kind_t::KaMinPar); + if (needs_graph) { // partitioner_input_t holds non-const std::vector* pointers; we // already have the data in our local mutable buffers above. partition_input.A.row_offsets = &h_A_row_offsets; @@ -525,7 +537,19 @@ pdlp_solver_t::pdlp_solver_t( partition_input.A_t.col_indices = &h_A_t_col_indices; partition_input.A_t.num_rows = n_vars; partition_input.A_t.num_cols = n_cstr; + // 0 => KaMinPar auto-detects and uses all hardware threads (ignored by METIS). + partition_input.nb_threads = 0; } + const char* kind_name = (kind == partitioner_kind_t::Dummy) ? "dummy" + : (kind == partitioner_kind_t::Metis) ? "metis" + : (kind == partitioner_kind_t::KaMinPar) ? "kaminpar" + : "unknown"; + CUOPT_LOG_INFO("Partitioning %d constraints + %d variables into %d part(s) using the %s " + "partitioner", + n_cstr, + n_vars, + distributed_pdlp_num_gpus, + kind_name); auto partitioner = make_partitioner(kind); parts = partitioner->partition(partition_input); } diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 228bacfd21..595c06b20a 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2166,11 +2166,6 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( "%d visible CUDA device(s)", settings_resolved.distributed_pdlp_num_gpus); } - if (settings_resolved.distributed_pdlp_num_gpus <= 1) { - std::cout << "CAREFUL: use_distributed_pdlp with distributed_pdlp_num_gpus == 1 runs the " - "single-shard dummy path" - << std::endl; - } // PDLP precision validations (mirror the checks in run_pdlp; distributed // path only supports the default-precision, non-batch double config). cuopt_expects(settings_resolved.pdlp_precision == pdlp_precision_t::DefaultPrecision, From 91b1ae5a619bb9edec2a6775a24304a95b73fdf6 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 4 Jun 2026 16:16:19 +0200 Subject: [PATCH 067/258] style --- .../cuopt/linear_programming/constants.h | 26 +++++++++---------- .../distributed_pdlp/multi_gpu_engine.hpp | 6 ++--- cpp/src/pdlp/distributed_pdlp/partitioner.cu | 3 +-- .../initial_scaling.cuh | 5 +++- cpp/src/pdlp/pdlp.cu | 26 ++++++++++--------- cpp/tests/linear_programming/pdlp_test.cu | 14 +++++----- 6 files changed, 42 insertions(+), 38 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 420a03526b..29648d1a0f 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -80,20 +80,20 @@ #define CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT \ "mip_strong_branching_simplex_iteration_limit" -#define CUOPT_SOLUTION_FILE "solution_file" -#define CUOPT_NUM_CPU_THREADS "num_cpu_threads" -#define CUOPT_NUM_GPUS "num_gpus" -#define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" -#define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" +#define CUOPT_SOLUTION_FILE "solution_file" +#define CUOPT_NUM_CPU_THREADS "num_cpu_threads" +#define CUOPT_NUM_GPUS "num_gpus" +#define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" +#define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" #define CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE "multi_gpu_export_partition_file" -#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" -#define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" -#define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" -#define CUOPT_USER_PROBLEM_FILE "user_problem_file" -#define CUOPT_PRESOLVE_FILE "presolve_file" -#define CUOPT_RANDOM_SEED "random_seed" -#define CUOPT_PDLP_PRECISION "pdlp_precision" -#define CUOPT_MIP_SEMICONTINUOUS_BIG_M "mip_semi_continuous_big_m" +#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" +#define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" +#define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" +#define CUOPT_USER_PROBLEM_FILE "user_problem_file" +#define CUOPT_PRESOLVE_FILE "presolve_file" +#define CUOPT_RANDOM_SEED "random_seed" +#define CUOPT_PDLP_PRECISION "pdlp_precision" +#define CUOPT_MIP_SEMICONTINUOUS_BIG_M "mip_semi_continuous_big_m" #define CUOPT_MIP_HYPER_HEURISTIC_POPULATION_SIZE "mip_hyper_heuristic_population_size" #define CUOPT_MIP_HYPER_HEURISTIC_NUM_CPUFJ_THREADS "mip_hyper_heuristic_num_cpufj_threads" diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 3a0fcb755d..89153e8bd7 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -250,7 +250,7 @@ struct multi_gpu_engine_t { // -------- Broadcast owned constraint (row) scaling into halo ------------ void broadcast_constraint_scaling_to_halo() { - const int nb = static_cast(shards.size()); + const int nb = static_cast(shards.size()); auto buf_access = [](pdlp_shard_t& s) -> rmm::device_uvector& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }; @@ -384,7 +384,7 @@ struct multi_gpu_engine_t { bound_sq.emplace_back(1, s.stream.view()); obj_sq.emplace_back(1, s.stream.view()); - const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); + const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); const int n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); const int n_owned_var = static_cast(s.rank_data.owned_var_size); @@ -403,7 +403,7 @@ struct multi_gpu_engine_t { n_owned_cstr, s.stream.view().value()); - auto obj_in = thrust::make_transform_iterator(scaled.objective_coefficients.data(), + auto obj_in = thrust::make_transform_iterator(scaled.objective_coefficients.data(), mgpu_weighted_sq_op_t{c_scaling_weight}); size_t tmp_bytes_o = 0; cub::DeviceReduce::Sum( diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cu b/cpp/src/pdlp/distributed_pdlp/partitioner.cu index e3866c3ad1..727a8b56f9 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cu @@ -73,8 +73,7 @@ std::unique_ptr> make_partitioner(partitioner_kind_t kin switch (kind) { case partitioner_kind_t::Dummy: return std::make_unique>(); case partitioner_kind_t::Metis: return std::make_unique>(); - case partitioner_kind_t::KaMinPar: - return std::make_unique>(); + case partitioner_kind_t::KaMinPar: return std::make_unique>(); } cuopt_expects( false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 409df5340a..13f639079d 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -105,7 +105,10 @@ class pdlp_initial_scaling_strategy_t { // Distributed PDLP: skip the LOCAL bound/objective rescaling inside // scale_problem() - void set_skip_distributed_local_rescaling(bool value) { skip_distributed_local_rescaling_ = value; } + void set_skip_distributed_local_rescaling(bool value) + { + skip_distributed_local_rescaling_ = value; + } // Public for distributed PDLP void compute_scaling_vectors(i_t number_of_ruiz_iterations, f_t alpha); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 0514ae1d13..71c6b0a48c 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -509,8 +509,8 @@ pdlp_solver_t::pdlp_solver_t( [](unsigned char c) { return std::tolower(c); }); partitioner_kind_t kind; if (partitioner_choice.empty() || partitioner_choice == "auto") { - kind = (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy - : partitioner_kind_t::KaMinPar; + kind = + (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::KaMinPar; } else if (partitioner_choice == "dummy") { kind = partitioner_kind_t::Dummy; } else if (partitioner_choice == "metis") { @@ -518,10 +518,11 @@ pdlp_solver_t::pdlp_solver_t( } else if (partitioner_choice == "kaminpar") { kind = partitioner_kind_t::KaMinPar; } else { - cuopt_expects(false, - error_type_t::ValidationError, - "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|metis|kaminpar)", - settings.distributed_pdlp_partitioner.c_str()); + cuopt_expects( + false, + error_type_t::ValidationError, + "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|metis|kaminpar)", + settings.distributed_pdlp_partitioner.c_str()); kind = partitioner_kind_t::Dummy; // unreachable; silences -Wmaybe-uninitialized } const bool needs_graph = @@ -544,12 +545,13 @@ pdlp_solver_t::pdlp_solver_t( : (kind == partitioner_kind_t::Metis) ? "metis" : (kind == partitioner_kind_t::KaMinPar) ? "kaminpar" : "unknown"; - CUOPT_LOG_INFO("Partitioning %d constraints + %d variables into %d part(s) using the %s " - "partitioner", - n_cstr, - n_vars, - distributed_pdlp_num_gpus, - kind_name); + CUOPT_LOG_INFO( + "Partitioning %d constraints + %d variables into %d part(s) using the %s " + "partitioner", + n_cstr, + n_vars, + distributed_pdlp_num_gpus, + kind_name); auto partitioner = make_partitioner(kind); parts = partitioner->partition(partition_input); } diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 65cc2f0d9f..d17cf2af6f 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -113,9 +113,9 @@ TEST(pdlp_class, distributed_partition_metis_export_import_roundtrip) const int n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); const int nnz = static_cast(mps.get_constraint_matrix_values().size()); - std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); - std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); - std::vector h_A_values = mps.get_constraint_matrix_values(); + std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); + std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); + std::vector h_A_values = mps.get_constraint_matrix_values(); // Transpose A -> A^T (CSR of A^T == CSC of A), mirroring solve_lp_distributed_from_mps. ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); @@ -171,7 +171,7 @@ void expect_distributed_matches_base(raft::handle_t const& handle, return std::fabs(a - b) <= rel * (1.0 + std::fabs(a)); }; - auto path = make_path_absolute(mps_rel_path); + auto path = make_path_absolute(mps_rel_path); io::mps_data_model_t problem = io::parse_mps(path, fixed_mps_format); // Shared settings: PDLP, no presolve (distributed requires presolver == None, so the @@ -186,9 +186,9 @@ void expect_distributed_matches_base(raft::handle_t const& handle, // ----- distributed PDLP (identical settings, only the distributed flags flipped) ----- pdlp_solver_settings_t dist_settings = base_settings; - dist_settings.hyper_params.use_distributed_pdlp = true; - dist_settings.distributed_pdlp_num_gpus = -1; - auto dist = solve_lp(&handle, problem, dist_settings); + dist_settings.hyper_params.use_distributed_pdlp = true; + dist_settings.distributed_pdlp_num_gpus = -1; + auto dist = solve_lp(&handle, problem, dist_settings); // ----- termination status ----- ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) From c6c59404451bb6c2e5976cedc10fe7a9558b3d39 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 11 Jun 2026 12:00:39 +0200 Subject: [PATCH 068/258] moved an expect for edge case from code rabbit --- cpp/src/pdlp/solve.cu | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 73840366d5..0300a53706 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -761,6 +761,11 @@ static optimization_problem_solution_t run_pdlp_solver( const timer_t& timer, bool is_batch_mode) { + cuopt_expects(!settings.hyper_params.use_distributed_pdlp, + error_type_t::ValidationError, + "Distributed PDLP must be entered via solve_lp(mps_data_model, ...) " + "so the master GPU never materializes the full problem. Call sites " + "with a problem_t cannot dispatch to distributed mode."); detail::pdlp_graph_disabled_flag().store(settings.hyper_params.pdlp_disable_graph, std::memory_order_relaxed); @@ -778,15 +783,6 @@ static optimization_problem_solution_t run_pdlp_solver( } } #endif - // Distributed PDLP cannot enter through this path: by the time we have a - // problem_t, the full problem already lives on the master GPU, which defeats - // the purpose of distributed mode. Callers must route to - // solve_lp_distributed_from_mps via solve_lp(mps_data_model, ...). - cuopt_expects(!settings.hyper_params.use_distributed_pdlp, - error_type_t::ValidationError, - "Distributed PDLP must be entered via solve_lp(mps_data_model, ...) " - "so the master GPU never materializes the full problem. Call sites " - "with a problem_t cannot dispatch to distributed mode."); detail::pdlp_solver_t solver(problem, settings, is_batch_mode); if (settings.inside_mip) { solver.set_inside_mip(true); } return solver.run_solver(timer); From 3488874776a14729b8dd5b2efa5f91637855be2f Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 11 Jun 2026 13:34:21 +0200 Subject: [PATCH 069/258] updated test --- cpp/tests/linear_programming/pdlp_test.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 2bafc9bfb5..39ec8d53e6 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -246,7 +246,7 @@ TEST(pdlp_class, distributed_parity_afiro) expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); } -TEST(pdlp_class, distributed_parity_square41) +TEST(pdlp_class, distributed_parity_neos3) { const raft::handle_t handle{}; expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); From bc1f87ec6065787f7f6027fd6690f4b020cfb7b2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 11 Jun 2026 13:36:03 +0200 Subject: [PATCH 070/258] update comment for code rabbit --- cpp/tests/linear_programming/pdlp_test.cu | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 39ec8d53e6..fbe97faef1 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -186,6 +186,7 @@ void expect_distributed_matches_base(raft::handle_t const& handle, auto base = solve_lp(base_op, base_settings); // ----- distributed PDLP (identical settings, only the distributed flags flipped) ----- + // testing on only one gpu is allowed and is already a great test for the distributed path pdlp_solver_settings_t dist_settings = base_settings; dist_settings.hyper_params.use_distributed_pdlp = true; dist_settings.distributed_pdlp_num_gpus = -1; From b28f07d66dc704e5465b7938309618516be58401 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 11 Jun 2026 13:39:33 +0200 Subject: [PATCH 071/258] added check to ensure distributed pdlp is only activated with method_t::PDLP --- cpp/src/pdlp/solve.cu | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 0300a53706..a19b390bd5 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2250,6 +2250,10 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( "use_distributed_pdlp; please set settings.presolver = presolver_t::None"); pdlp_solver_settings_t settings_resolved = settings; + cuopt_expects(settings_resolved.method == method_t::PDLP, + error_type_t::ValidationError, + "Distributed MPS solve currently supports only method_t::PDLP"); + if (use_pdlp_solver_mode) { set_pdlp_solver_mode(settings_resolved); } detail::pdlp_graph_disabled_flag().store(settings_resolved.hyper_params.pdlp_disable_graph, std::memory_order_relaxed); From 9ae23a079cb87bafeeaa73a426e9530807afdf65 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 11 Jun 2026 14:14:49 +0200 Subject: [PATCH 072/258] reverted back solve_lp from mps for better handling --- cpp/src/pdlp/solve.cu | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index a19b390bd5..ada1f53e8e 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2221,12 +2221,12 @@ optimization_problem_solution_t solve_lp( bool problem_checking, bool use_pdlp_solver_mode) { - cuopt_expects( - settings.hyper_params.use_distributed_pdlp, - error_type_t::ValidationError, - "solve_lp from mps_data_model: settings.hyper_params.use_distributed_pdlp must be true"); - return solve_lp_distributed_from_mps( - handle_ptr, mps_data_model, settings, problem_checking, use_pdlp_solver_mode); + if (settings.hyper_params.use_distributed_pdlp) { + return solve_lp_distributed_from_mps( + handle_ptr, mps_data_model, settings, problem_checking, use_pdlp_solver_mode); + } + auto op_problem = mps_data_model_to_optimization_problem(handle_ptr, mps_data_model); + return solve_lp(op_problem, settings, problem_checking, use_pdlp_solver_mode, false); } template From 818ffcdd190a0d271e6a3707f5640a9b9c86b075 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 11 Jun 2026 14:18:25 +0200 Subject: [PATCH 073/258] small clearer comment for pdlp_disable_graph --- cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh index 0ce90e7228..fc81d05d99 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh +++ b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh @@ -50,6 +50,7 @@ struct pdlp_hyper_params_t { bool use_distributed_pdlp = false; // Debug/diagnostic knob: when true, PDLP bypasses CUDA-graph capture in // ping_pong_graph_t and executes each iteration eagerly + // Useful for cuD-PDLP in current state bool pdlp_disable_graph = false; double reflection_coefficient = 1.0; double restart_k_p = 0.99; From d3dad662c2e0084601ce76c10a2f85779fff6957 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 11 Jun 2026 14:40:49 +0200 Subject: [PATCH 074/258] updated gather of final solution positionning in the code --- cpp/src/pdlp/pdlp.cu | 15 --------------- .../termination_strategy/termination_strategy.cu | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 71c6b0a48c..53d91233aa 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -818,11 +818,6 @@ std::optional> pdlp_solver_t RAFT_CUDA_TRY(cudaDeviceSynchronize()); std::cout << "Time Limit reached, returning current solution" << std::endl; #endif - if (auto* engine = pdhg_solver_.get_mgpu_engine()) { - engine->gather_potential_next_solutions_to_master( - pdhg_solver_, - current_termination_strategy_.get_convergence_information().get_reduced_cost()); - } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, pdhg_solver_, @@ -856,11 +851,6 @@ std::optional> pdlp_solver_t return finalize_batch_return_with_limit_reached(pdlp_termination_status_t::IterationLimit); } - if (auto* engine = pdhg_solver_.get_mgpu_engine()) { - engine->gather_potential_next_solutions_to_master( - pdhg_solver_, - current_termination_strategy_.get_convergence_information().get_reduced_cost()); - } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, pdhg_solver_, @@ -1538,11 +1528,6 @@ std::optional> pdlp_solver_t #endif print_final_termination_criteria( timer, current_termination_strategy_.get_convergence_information(), termination_current); - if (auto* engine = pdhg_solver_.get_mgpu_engine()) { - engine->gather_potential_next_solutions_to_master( - pdhg_solver_, - current_termination_strategy_.get_convergence_information().get_reduced_cost()); - } return current_termination_strategy_.fill_return_problem_solution( internal_solver_iterations_, pdhg_solver_, diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index 0320b420a8..5c305dea9d 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -5,6 +5,7 @@ */ /* clang-format on */ +#include #include #include #include @@ -559,6 +560,19 @@ pdlp_termination_strategy_t::fill_return_problem_solution( dual_iterate.size() == current_pdhg_solver.get_dual_size() * termination_status.size(), "Dual iterate size mismatch"); + // In distributed PDLP, gather solutions to master here + if (!deep_copy) { + if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { + const bool is_current_live_iterate = + (&primal_iterate == ¤t_pdhg_solver.get_potential_next_primal_solution()) || + (&primal_iterate == ¤t_pdhg_solver.get_primal_solution()); + if (is_current_live_iterate) { + engine->gather_potential_next_solutions_to_master( + current_pdhg_solver, convergence_information_.get_reduced_cost()); + } + } + } + typename convergence_information_t::view_t convergence_information_view = convergence_information_.view(); typename infeasibility_information_t::view_t infeasibility_information_view = From 74c2d8f10a1efb23097f1c4a6d79ea06bf67f0b3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 12 Jun 2026 12:48:00 +0200 Subject: [PATCH 075/258] added include for compile --- cpp/src/pdlp/termination_strategy/termination_strategy.cu | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index 5c305dea9d..7137ff4822 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -6,6 +6,8 @@ /* clang-format on */ #include + +#include #include #include #include From cfdacd4a471af51834fec0b64517e0060e5b1406 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 12 Jun 2026 12:50:43 +0200 Subject: [PATCH 076/258] kaminpar compile --- cpp/cmake/thirdparty/get_kaminpar.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/cmake/thirdparty/get_kaminpar.cmake b/cpp/cmake/thirdparty/get_kaminpar.cmake index d548a76115..c662809fd2 100644 --- a/cpp/cmake/thirdparty/get_kaminpar.cmake +++ b/cpp/cmake/thirdparty/get_kaminpar.cmake @@ -14,6 +14,8 @@ function(find_and_configure_kaminpar) rapids_cpm_find(KaMinPar ${PKG_VERSION} GLOBAL_TARGETS KaMinPar::KaMinPar + BUILD_EXPORT_SET cuopt-exports + INSTALL_EXPORT_SET cuopt-exports CPM_ARGS GIT_REPOSITORY https://github.com/KaHIP/KaMinPar.git GIT_TAG ${PKG_PINNED_TAG} From 0de96097b34e2179b0d31c07e08a005ffac8e9f9 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 12 Jun 2026 13:00:39 +0200 Subject: [PATCH 077/258] expect no initial or warm start --- cpp/src/pdlp/solve.cu | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index ada1f53e8e..8c2eaae80b 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2273,6 +2273,13 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( cuopt_expects(!settings_resolved.inside_mip, error_type_t::ValidationError, "Distributed PDLP is not yet supported from inside MIP."); + // Reject initial solution and warm starts as they are not supported yes for distributed PDLP + cuopt_expects(!settings_resolved.has_initial_primal_solution() && + !settings_resolved.has_initial_dual_solution() && + !settings_resolved.get_pdlp_warm_start_data().is_populated(), + error_type_t::ValidationError, + "Distributed PDLP does not support initial primal/dual solutions or warm-start " + "data."); init_logger_t log(settings_resolved.log_file, settings_resolved.log_to_console); print_version_info(); From 1d43e8a9d6d81c8be08f83986ebb8f3ab95f8265 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 12 Jun 2026 15:36:58 +0200 Subject: [PATCH 078/258] clean error if handle is null --- cpp/cuopt_cli.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 19d1078a42..068a76dce1 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -5,6 +5,7 @@ */ /* clang-format on */ +#include #include #include #include @@ -178,6 +179,13 @@ int run_single_file(const std::string& file_path, auto& lp_settings = settings.get_pdlp_settings(); if (lp_settings.hyper_params.use_distributed_pdlp) { + // handle_ptr is only created for the GPU memory backend (see above). Distributed PDLP is + // GPU-only. + cuopt::cuopt_expects( + handle_ptr != nullptr, + cuopt::error_type_t::ValidationError, + "Distributed PDLP requires the GPU memory backend; no GPU handle is available for the " + "selected memory backend."); cuopt::linear_programming::solve_lp(handle_ptr.get(), mps_data_model, lp_settings); } else { cuopt::linear_programming::solve_lp(problem_interface.get(), lp_settings); From f3b6343731dd843ddd9c031b5774ac48c4ebb85a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 12 Jun 2026 15:42:24 +0200 Subject: [PATCH 079/258] better exept for never_restart_to_average --- cpp/src/pdlp/pdlp.cu | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 53d91233aa..461074e90b 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3053,7 +3053,10 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co // 1. At the very beginning of the solver, when no steps have been taken yet // 2. After a single step, since average of one step is the same step if (internal_solver_iterations_ <= 1) { - if (multi_gpu_engine) { assert(false && "Not implemented"); } + cuopt_expects(!multi_gpu_engine.has_value(), + error_type_t::RuntimeError, + "Distributed PDLP does not support average restart; run with " + "never_restart_to_average = true."); raft::copy(unscaled_primal_avg_solution_.data(), pdhg_solver_.get_primal_solution().data(), primal_size_h_, From c658769a9ea6e65ddee5f89bd4e040953af6ec37 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 12 Jun 2026 16:03:39 +0200 Subject: [PATCH 080/258] style --- cpp/src/pdlp/solve.cu | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 8c2eaae80b..3bfbd1f494 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -762,10 +762,10 @@ static optimization_problem_solution_t run_pdlp_solver( bool is_batch_mode) { cuopt_expects(!settings.hyper_params.use_distributed_pdlp, - error_type_t::ValidationError, - "Distributed PDLP must be entered via solve_lp(mps_data_model, ...) " - "so the master GPU never materializes the full problem. Call sites " - "with a problem_t cannot dispatch to distributed mode."); + error_type_t::ValidationError, + "Distributed PDLP must be entered via solve_lp(mps_data_model, ...) " + "so the master GPU never materializes the full problem. Call sites " + "with a problem_t cannot dispatch to distributed mode."); detail::pdlp_graph_disabled_flag().store(settings.hyper_params.pdlp_disable_graph, std::memory_order_relaxed); From 38fffaf003a806005de9a3682136f9a5c33cba95 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 5 Jun 2026 09:40:06 -0700 Subject: [PATCH 081/258] removed scaled problem before shard building, and optimized code to get a 6x on construction time !! #devtechouquoilateam --- .../pdlp/distributed_pdlp/partition_loader.cu | 121 +++++++++++------- .../distributed_pdlp/partition_loader.hpp | 2 - cpp/src/pdlp/distributed_pdlp/rank_data.hpp | 2 - cpp/src/pdlp/distributed_pdlp/shard.cu | 11 +- cpp/src/pdlp/pdlp.cu | 4 - 5 files changed, 82 insertions(+), 58 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index a6db3a9fe8..a3496bce7c 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -8,7 +8,7 @@ #include #include -#include +#include #include namespace cuopt::linear_programming::detail { @@ -64,36 +64,56 @@ std::vector> partition_loader_t::create_rank_dat const std::vector& A_row_offsets, const std::vector& A_col_indices, const std::vector& A_values, - const std::vector& A_values_scaled, const std::vector& A_t_row_offsets, const std::vector& A_t_col_indices, const std::vector& A_t_values, - const std::vector& A_t_values_scaled, i_t nb_parts, i_t nb_cstr, i_t nb_vars, i_t nnz) { - cuopt_expects(A_values.size() == A_values_scaled.size(), - error_type_t::ValidationError, - "A_values and A_values_scaled must have the same length"); - cuopt_expects(A_t_values.size() == A_t_values_scaled.size(), + std::vector> rank_data(nb_parts, rank_data_t(nb_parts)); + cuopt_expects(static_cast(parts.size()) == nb_cstr + nb_vars, error_type_t::ValidationError, - "A_t_values and A_t_values_scaled must have the same length"); + "parts size mismatch: expected nb_cstr + nb_vars"); + + // Same as two vectors but faster + auto cstr_owner = [&](i_t c) { return parts[c]; }; + auto var_owner = [&](i_t v) { return parts[nb_cstr + v]; }; + + std::vector owned_cstr_counts(nb_parts, 0); + std::vector owned_var_counts(nb_parts, 0); + std::vector owned_A_nnz(nb_parts, 0); + std::vector owned_A_t_nnz(nb_parts, 0); + + // Pre-count ownership and nnz to reserve exact capacities and avoid + // repeated growth/reallocation across huge vectors. + for (i_t i = 0; i < nb_cstr; ++i) { + const i_t owner = cstr_owner(i); + ++owned_cstr_counts[owner]; + owned_A_nnz[owner] += (A_row_offsets[i + 1] - A_row_offsets[i]); + } + for (i_t j = 0; j < nb_vars; ++j) { + const i_t owner = var_owner(j); + ++owned_var_counts[owner]; + owned_A_t_nnz[owner] += (A_t_row_offsets[j + 1] - A_t_row_offsets[j]); + } - std::vector> rank_data(nb_parts, rank_data_t(nb_parts)); - std::vector cstr_parts(parts.begin(), parts.begin() + nb_cstr); - std::vector var_parts(parts.begin() + nb_cstr, parts.begin() + nb_cstr + nb_vars); + for (i_t rank = 0; rank < nb_parts; ++rank) { + rank_data[rank].owned_cstr_indices.reserve(static_cast(owned_cstr_counts[rank])); + rank_data[rank].owned_var_indices.reserve(static_cast(owned_var_counts[rank])); + } // 1. Compute ownership for (i_t i = 0; i < nb_cstr; i++) { - rank_data[cstr_parts[i]].owned_cstr_indices.push_back(i); + rank_data[cstr_owner(i)].owned_cstr_indices.push_back(i); } for (i_t i = 0; i < nb_vars; i++) { - rank_data[var_parts[i]].owned_var_indices.push_back(i); + rank_data[var_owner(i)].owned_var_indices.push_back(i); } // 2. Compute local matrices and rank_data +#pragma omp parallel for for (i_t rank = 0; rank < nb_parts; rank++) { auto& rd = rank_data[rank]; rd.owned_var_size = rd.owned_var_indices.size(); @@ -102,7 +122,9 @@ std::vector> partition_loader_t::create_rank_dat std::vector local_A_row_offsets; std::vector local_A_col_indices; std::vector local_A_values; - std::vector local_A_values_scaled; + local_A_row_offsets.reserve(static_cast(rd.owned_cstr_size) + 1); + local_A_col_indices.reserve(static_cast(owned_A_nnz[rank])); + local_A_values.reserve(static_cast(owned_A_nnz[rank])); i_t local_A_nnz = 0; local_A_row_offsets.push_back(local_A_nnz); @@ -114,81 +136,83 @@ std::vector> partition_loader_t::create_rank_dat for (auto owned_cstr : rd.owned_cstr_indices) { i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; i_t row_start = A_row_offsets[owned_cstr]; - for (i_t v = 0; v < cstr_len; v++) { - local_A_col_indices.push_back(A_col_indices[row_start + v]); - local_A_values.push_back(A_values[row_start + v]); - local_A_values_scaled.push_back(A_values_scaled[row_start + v]); - } + local_A_col_indices.insert( + local_A_col_indices.end(), A_col_indices.begin() + row_start, A_col_indices.begin() + row_start + cstr_len); + local_A_values.insert( + local_A_values.end(), A_values.begin() + row_start, A_values.begin() + row_start + cstr_len); local_A_nnz += cstr_len; local_A_row_offsets.push_back(local_A_nnz); } - std::set needed_vars; + std::vector> needed_var_from_peer(nb_parts); + std::unordered_set seen_needed_vars; + // size / 2 + 1 is a heuristic to avoid overestimating and resizing + seen_needed_vars.reserve(local_A_col_indices.size() / 2 + 1); for (auto indice : local_A_col_indices) { - if (var_parts[indice] != rank) needed_vars.insert(indice); + const i_t owner = var_owner(indice); + if (owner != rank && seen_needed_vars.insert(indice).second) { + needed_var_from_peer[owner].push_back(indice); + } } for (i_t peer = 0; peer < nb_parts; peer++) { - std::vector needed_var_from_peer; - for (auto needed_var : needed_vars) { - if (var_parts[needed_var] == peer) needed_var_from_peer.push_back(needed_var); - } - i_t nb_recv_from_peer = needed_var_from_peer.size(); + i_t nb_recv_from_peer = needed_var_from_peer[peer].size(); rd.var_recv_counts[peer] = nb_recv_from_peer; rd.var_recv_offsets[peer] = peer == 0 ? 0 : rd.var_recv_offsets[peer - 1] + rd.var_recv_counts[peer - 1]; - rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer); + rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer[peer]); } rd.h_A_row_offsets = std::move(local_A_row_offsets); rd.h_A_col_indices = std::move(local_A_col_indices); rd.h_A_values = std::move(local_A_values); - rd.h_A_values_scaled = std::move(local_A_values_scaled); // ---- A_t side ---- std::vector local_A_t_row_offsets; std::vector local_A_t_col_indices; std::vector local_A_t_values; - std::vector local_A_t_values_scaled; + local_A_t_row_offsets.reserve(static_cast(rd.owned_var_size) + 1); + local_A_t_col_indices.reserve(static_cast(owned_A_t_nnz[rank])); + local_A_t_values.reserve(static_cast(owned_A_t_nnz[rank])); i_t local_A_t_nnz = 0; local_A_t_row_offsets.push_back(local_A_t_nnz); for (auto owned_var : rd.owned_var_indices) { i_t var_len = A_t_row_offsets[owned_var + 1] - A_t_row_offsets[owned_var]; i_t row_start = A_t_row_offsets[owned_var]; - for (i_t v = 0; v < var_len; v++) { - local_A_t_col_indices.push_back(A_t_col_indices[row_start + v]); - local_A_t_values.push_back(A_t_values[row_start + v]); - local_A_t_values_scaled.push_back(A_t_values_scaled[row_start + v]); - } + local_A_t_col_indices.insert(local_A_t_col_indices.end(), + A_t_col_indices.begin() + row_start, + A_t_col_indices.begin() + row_start + var_len); + local_A_t_values.insert( + local_A_t_values.end(), A_t_values.begin() + row_start, A_t_values.begin() + row_start + var_len); local_A_t_nnz += var_len; local_A_t_row_offsets.push_back(local_A_t_nnz); } - std::set needed_cstrs; + std::vector> needed_cstr_from_peer(nb_parts); + std::unordered_set seen_needed_cstrs; + seen_needed_cstrs.reserve(local_A_t_col_indices.size() / 2 + 1); for (auto indice : local_A_t_col_indices) { - if (cstr_parts[indice] != rank) needed_cstrs.insert(indice); + const i_t owner = cstr_owner(indice); + if (owner != rank && seen_needed_cstrs.insert(indice).second) { + needed_cstr_from_peer[owner].push_back(indice); + } } for (i_t peer = 0; peer < nb_parts; peer++) { - std::vector needed_cstr_from_peer; - for (auto needed_cstr : needed_cstrs) { - if (cstr_parts[needed_cstr] == peer) needed_cstr_from_peer.push_back(needed_cstr); - } - i_t nb_recv_from_peer = needed_cstr_from_peer.size(); + i_t nb_recv_from_peer = needed_cstr_from_peer[peer].size(); rd.cstr_recv_counts[peer] = nb_recv_from_peer; rd.cstr_recv_offsets[peer] = peer == 0 ? 0 : rd.cstr_recv_offsets[peer - 1] + rd.cstr_recv_counts[peer - 1]; - rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer); + rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer[peer]); } rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); rd.h_A_t_col_indices = std::move(local_A_t_col_indices); rd.h_A_t_values = std::move(local_A_t_values); - rd.h_A_t_values_scaled = std::move(local_A_t_values_scaled); - rd.total_var_size = rd.owned_var_size + needed_vars.size(); - rd.total_cstr_size = rd.owned_cstr_size + needed_cstrs.size(); + rd.total_var_size = rd.owned_var_size + static_cast(seen_needed_vars.size()); + rd.total_cstr_size = rd.owned_cstr_size + static_cast(seen_needed_cstrs.size()); // Pad row-offset arrays so cuSPARSE sees the local matrices as // (total_cstr x total_var) for A and (total_var x total_cstr) for A_T @@ -201,8 +225,14 @@ std::vector> partition_loader_t::create_rank_dat // 3. Generate local indices for contiguous [[self], [peer1], ..., [peer_k]] // Build scatter_gather_maps + // 3. Build local<->global maps in parallel across ranks. +#pragma omp parallel for for (i_t rank = 0; rank < nb_parts; rank++) { auto& rd = rank_data[rank]; + rd.global_to_local_cstr.reserve(static_cast(rd.total_cstr_size)); + rd.global_to_local_var.reserve(static_cast(rd.total_var_size)); + rd.local_to_global_cstr.reserve(static_cast(rd.total_cstr_size)); + rd.local_to_global_var.reserve(static_cast(rd.total_var_size)); i_t curr_id = 0; for (auto owned_cstr : rd.owned_cstr_indices) { @@ -236,6 +266,7 @@ std::vector> partition_loader_t::create_rank_dat } // 4. Remap global -> local everywhere +#pragma omp parallel for for (i_t rank = 0; rank < nb_parts; rank++) { auto& rd = rank_data[rank]; diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index ce12d241f9..ff60f11cca 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -31,11 +31,9 @@ struct partition_loader_t { const std::vector& A_row_offsets, const std::vector& A_col_indices, const std::vector& A_values, - const std::vector& A_values_scaled, const std::vector& A_t_row_offsets, const std::vector& A_t_col_indices, const std::vector& A_t_values, - const std::vector& A_t_values_scaled, i_t nb_parts, i_t nb_cstr, i_t nb_vars, diff --git a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp index d52d277116..006c52749f 100644 --- a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp +++ b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp @@ -51,11 +51,9 @@ struct rank_data_t { std::vector h_A_row_offsets; std::vector h_A_col_indices; std::vector h_A_values; - std::vector h_A_values_scaled; // A_t std::vector h_A_t_row_offsets; std::vector h_A_t_col_indices; std::vector h_A_t_values; - std::vector h_A_t_values_scaled; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 356e10a03c..b451b52c0e 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -168,16 +168,17 @@ pdlp_shard_t::pdlp_shard_t(int device_id, rank_data.h_A_col_indices.size(), stream_view); raft::copy(scaled.coefficients.data(), - rank_data.h_A_values_scaled.data(), - rank_data.h_A_values_scaled.size(), + rank_data.h_A_values.data(), + rank_data.h_A_values.size(), stream_view); // A_T side: all three arrays were already overridden together from // rank_data on sub_problem (see step 4 above) and deep-copied into the // scaled problem, so reverse_offsets / reverse_constraints already match - // h_A_t_values_scaled's order. Only the values need a SCALED swap-in. + // h_A_t_values's order. At this stage distributed initial scaling starts from + // identity, so the matrix values are injected from the unscaled host slices. raft::copy(scaled.reverse_coefficients.data(), - rank_data.h_A_t_values_scaled.data(), - rank_data.h_A_t_values_scaled.size(), + rank_data.h_A_t_values.data(), + rank_data.h_A_t_values.size(), stream_view); raft::copy( scaled.objective_coefficients.data(), h_obj_scaled.data(), h_obj_scaled.size(), stream_view); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 461074e90b..f549cc81f0 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -477,8 +477,6 @@ pdlp_solver_t::pdlp_solver_t( // both "unscaled" and "scaled" so the engine and per-shard pdlp_solver_t // can run end-to-end. Scaling factor vectors are 1.0 everywhere so the // shard-side unscale at the end is a no-op. - std::vector h_A_values_scaled = h_A_values; - std::vector h_A_t_values_scaled = h_A_t_values; std::vector h_obj_scaled = h_obj; std::vector h_var_lower_scaled = h_var_lower; std::vector h_var_upper_scaled = h_var_upper; @@ -570,11 +568,9 @@ pdlp_solver_t::pdlp_solver_t( h_A_row_offsets, h_A_col_indices, h_A_values, - h_A_values_scaled, h_A_t_row_offsets, h_A_t_col_indices, h_A_t_values, - h_A_t_values_scaled, settings.distributed_pdlp_num_gpus, n_cstr, n_vars, From eb08f11b0cf8da2b75646ccb4f451afef6e361b2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 5 Jun 2026 08:54:58 -0700 Subject: [PATCH 082/258] added prints to now rank data timings --- cpp/src/pdlp/pdlp.cu | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index f549cc81f0..af6f33956d 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -563,6 +563,9 @@ pdlp_solver_t::pdlp_solver_t( } // ----- 5. Build per-rank data ----- + CUOPT_LOG_INFO("distributed_pdlp: building rank_data for %d parts ...", + settings.distributed_pdlp_num_gpus); + auto rank_data_t0 = std::chrono::high_resolution_clock::now(); std::vector> sub_pdlp_rank_data = partition_loader_t::create_rank_data_from_parts(parts, h_A_row_offsets, @@ -575,6 +578,9 @@ pdlp_solver_t::pdlp_solver_t( n_cstr, n_vars, nnz); + auto rank_data_t1 = std::chrono::high_resolution_clock::now(); + CUOPT_LOG_INFO("distributed_pdlp: rank_data build done in %.3f s", + std::chrono::duration(rank_data_t1 - rank_data_t0).count()); // ----- 6. Per-shard settings ----- pdlp_solver_settings_t sub_pdlp_settings = settings; From 0816778a49d7bf744750259e2b784ade83f29a80 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 5 Jun 2026 08:55:23 -0700 Subject: [PATCH 083/258] also print shard time --- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 98f33b6c88..6ad7eebb22 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -15,8 +15,11 @@ #include +#include #include +#include + namespace cuopt::linear_programming::detail { template @@ -57,6 +60,8 @@ multi_gpu_engine_t::multi_gpu_engine_t( "ncclCommInitAll failed"); // 3. Construct one shard per rank, pinned to its device. + CUOPT_LOG_INFO("distributed_pdlp: building %d shard solver(s) ...", nb_parts); + auto shard_build_t0 = std::chrono::high_resolution_clock::now(); for (int r = 0; r < nb_parts; ++r) { raft::device_setter guard(devices[r]); // shard ctor needs device set shards.emplace_back(std::make_unique>(devices[r], @@ -81,6 +86,9 @@ multi_gpu_engine_t::multi_gpu_engine_t( objective_scaling_factor, sub_solver_settings)); } + auto shard_build_t1 = std::chrono::high_resolution_clock::now(); + CUOPT_LOG_INFO("distributed_pdlp: shard build done in %.3f s", + std::chrono::duration(shard_build_t1 - shard_build_t0).count()); // Two different events // capture_*_event_ are used inside graph capture From 9aca02956a6a3223ff4f23c62ab6136fe09cfb6a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 5 Jun 2026 08:11:51 -0700 Subject: [PATCH 084/258] kaminpar quiet --- cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp index e7bf943f92..355643e5c1 100644 --- a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp @@ -102,6 +102,7 @@ std::vector kaminpar_partitioner_t::partition( std::vector block_of(static_cast(nvtx)); kaminpar::KaMinPar engine(nthreads, kaminpar::shm::create_default_context()); + engine.set_output_level(kaminpar::OutputLevel::QUIET); engine.copy_graph(std::span(xadj), std::span(adjncy)); engine.set_k(static_cast(input.nb_parts)); From 873d1679a43f373b5070e67900327df388c30320 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 12 Jun 2026 16:18:28 +0200 Subject: [PATCH 085/258] read mps for the compile !! --- cpp/tests/linear_programming/pdlp_test.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index fbe97faef1..6def807e62 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -108,7 +108,7 @@ TEST(pdlp_class, distributed_partition_metis_export_import_roundtrip) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t mps = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const int n_vars = static_cast(mps.get_objective_coefficients().size()); const int n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); @@ -173,7 +173,7 @@ void expect_distributed_matches_base(raft::handle_t const& handle, }; auto path = make_path_absolute(mps_rel_path); - io::mps_data_model_t problem = io::parse_mps(path, fixed_mps_format); + io::mps_data_model_t problem = io::read_mps(path, fixed_mps_format); // Shared settings: PDLP, no presolve (distributed requires presolver == None, so the // base run must match to keep the two problems identical). From e9cad7a4e7b072e806be663c6a17c1b3e3efd511 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 15 Jun 2026 15:21:52 +0200 Subject: [PATCH 086/258] removed any reference to metis partitionner --- cpp/CMakeLists.txt | 40 +---- .../pdlp/solver_settings.hpp | 3 +- cpp/src/pdlp/CMakeLists.txt | 1 - .../distributed_pdlp/kaminpar_partitioner.cpp | 6 +- .../distributed_pdlp/kaminpar_partitioner.hpp | 4 +- .../distributed_pdlp/metis_partitioner.cu | 151 ------------------ .../distributed_pdlp/metis_partitioner.hpp | 24 --- .../distributed_pdlp/partition_loader.hpp | 4 +- cpp/src/pdlp/distributed_pdlp/partitioner.cu | 2 - cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 7 +- cpp/src/pdlp/pdlp.cu | 10 +- cpp/src/pdlp/solve.cuh | 2 +- cpp/tests/linear_programming/pdlp_test.cu | 8 +- 13 files changed, 20 insertions(+), 242 deletions(-) delete mode 100644 cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu delete mode 100644 cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 669c27c5d0..d612c1b88c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -339,52 +339,15 @@ set_target_properties(nccl_external PROPERTIES ) message(STATUS "Using NCCL: ${NCCL_LIBRARY}") -# ################################################################################################## -# - METIS (graph partitioning for distributed PDLP) ----------------------------------------------- -# Found by searching CONDA_PREFIX first, then CUOPT_METIS_ROOT (cmake var or env) -# if the user wants to pull METIS from a different conda env / system path. -set(METIS_HINT_PREFIXES "") -if (DEFINED ENV{CONDA_PREFIX} AND NOT "$ENV{CONDA_PREFIX}" STREQUAL "") - list(APPEND METIS_HINT_PREFIXES "$ENV{CONDA_PREFIX}") -endif () -if (DEFINED CUOPT_METIS_ROOT AND NOT "${CUOPT_METIS_ROOT}" STREQUAL "") - list(APPEND METIS_HINT_PREFIXES "${CUOPT_METIS_ROOT}") -endif () -if (DEFINED ENV{CUOPT_METIS_ROOT} AND NOT "$ENV{CUOPT_METIS_ROOT}" STREQUAL "") - list(APPEND METIS_HINT_PREFIXES "$ENV{CUOPT_METIS_ROOT}") -endif () -find_path(METIS_INCLUDE_DIR - NAMES metis.h - HINTS ${METIS_HINT_PREFIXES} - PATH_SUFFIXES include -) -find_library(METIS_LIBRARY - NAMES metis libmetis - HINTS ${METIS_HINT_PREFIXES} - PATH_SUFFIXES lib lib64 -) -if (NOT METIS_INCLUDE_DIR OR NOT METIS_LIBRARY) - message(FATAL_ERROR "METIS not found. Looked in: ${METIS_HINT_PREFIXES}. " - "Install it via 'conda install -c conda-forge metis' in the active env, " - "or set CUOPT_METIS_ROOT to a prefix containing include/metis.h and lib/libmetis.{so,a}.") -endif () -add_library(metis_external UNKNOWN IMPORTED GLOBAL) -set_target_properties(metis_external PROPERTIES - IMPORTED_LOCATION "${METIS_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${METIS_INCLUDE_DIR}" -) -message(STATUS "Using METIS: ${METIS_LIBRARY}") - # ################################################################################################## # - KaMinPar (multi-threaded partitioning for distributed PDLP) ------------------------------------ # Brought in the RAPIDS way (rapids_cpm_find): uses an installed KaMinPar (deb/rpm/conda, # discovered via its CMake config) if present, otherwise builds the pinned source via CPM. -# Distributed PDLP prefers KaMinPar over METIS. include(cmake/thirdparty/get_kaminpar.cmake) if (NOT TARGET KaMinPar::KaMinPar) message(FATAL_ERROR "KaMinPar::KaMinPar was not made available by get_kaminpar.cmake") endif () -message(STATUS "Using KaMinPar (distributed PDLP prefers KaMinPar over METIS)") +message(STATUS "Using KaMinPar (distributed PDLP graph partitioner)") # ################################################################################################## # - gRPC and Protobuf setup ----------------------------------------------------------------------- @@ -672,7 +635,6 @@ target_link_libraries(cuopt PRIVATE ${CUOPT_PRIVATE_CUDA_LIBS} nccl_external - metis_external KaMinPar::KaMinPar $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index 42ef1f592a..12c29f681b 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -319,10 +319,9 @@ class pdlp_solver_settings_t { // Which graph partitioner distributed PDLP uses. One of: // "auto" - 1 GPU => Dummy; otherwise KaMinPar // "dummy" - round-robin, no graph (trivial) - // "metis" - serial METIS_PartGraphKway // "kaminpar" - multi-threaded KaMinPar // Exposed as the distributed_pdlp_partitioner parameter - // (CLI: --distributed-pdlp-partitioner ). + // (CLI: --distributed-pdlp-partitioner ). std::string distributed_pdlp_partitioner{"auto"}; // Set to true inside the shards bool is_distributed_sub_pdlp{false}; diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 12f2550203..a20244bd26 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -33,7 +33,6 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/multi_gpu_engine.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cu - ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/metis_partitioner.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/kaminpar_partitioner.cpp ) diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp index 355643e5c1..487b8da9bd 100644 --- a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp @@ -23,8 +23,8 @@ namespace cuopt::linear_programming::detail { -// Builds the bipartite constraint/variable graph induced by A (identical layout -// to metis_partitioner_t) and runs the multi-threaded KaMinPar k-way kernel. +// Builds the bipartite constraint/variable graph induced by A and runs the +// multi-threaded KaMinPar k-way kernel. // * nodes [0, nb_cstr) : constraint nodes // * nodes [nb_cstr, nb_cstr+nb_vars): variable nodes // * undirected edges from each A nonzero (one half via A, one via A_t) @@ -106,7 +106,7 @@ std::vector kaminpar_partitioner_t::partition( engine.copy_graph(std::span(xadj), std::span(adjncy)); engine.set_k(static_cast(input.nb_parts)); - // ~3% imbalance, matching METIS_PartGraphKway's default balance constraint. + // ~3% imbalance balance constraint. engine.set_uniform_max_block_weights(0.03); auto t0 = std::chrono::high_resolution_clock::now(); diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp index 43fda76f9f..babbc60732 100644 --- a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp @@ -9,8 +9,8 @@ namespace cuopt::linear_programming::detail { -// Multi-threaded k-way partitioner backed by KaMinPar. Builds the same -// constraint/variable bipartite graph as metis_partitioner_t, but runs the +// Multi-threaded k-way partitioner backed by KaMinPar. Builds a +// constraint/variable bipartite graph and runs the // shared-memory parallel KaMinPar kernel so partitioning scales across all CPU // cores of a node (set via partitioner_input_t::nb_threads; <= 0 => all // hardware threads). diff --git a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu deleted file mode 100644 index 9a4f0f50b1..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.cu +++ /dev/null @@ -1,151 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include - -#include - -#include - -#include - -#include -#include -#include - -namespace cuopt::linear_programming::detail { - -// Builds the bipartite constraint/variable graph induced by A and runs -// METIS_PartGraphKway to assign each of the (nb_cstr + nb_vars) nodes to a -// part in [0, nb_parts). Layout matches metis_tests: -// * nodes [0, nb_cstr) : constraint nodes -// * nodes [nb_cstr, nb_cstr+nb_vars): variable nodes -// * undirected edges from each A nonzero (one half via A, one via A_t) -// The output is consumed by partition_loader_t::create_rank_data_from_parts. -template -std::vector metis_partitioner_t::partition( - partitioner_input_t const& input) const -{ - cuopt_expects(input.nb_parts > 0, - error_type_t::ValidationError, - "metis_partitioner: nb_parts must be positive"); - cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, - error_type_t::ValidationError, - "metis_partitioner: invalid problem dimensions"); - - if (input.nb_parts == 1) { - CUOPT_LOG_INFO("METIS: nb_parts == 1, returning trivial single-block partition"); - return std::vector(static_cast(input.nb_cstr + input.nb_vars), i_t{0}); - } - - cuopt_expects(input.A.row_offsets != nullptr && input.A.col_indices != nullptr, - error_type_t::ValidationError, - "metis_partitioner: A.row_offsets and A.col_indices are required"); - cuopt_expects(input.A_t.row_offsets != nullptr && input.A_t.col_indices != nullptr, - error_type_t::ValidationError, - "metis_partitioner: A_t.row_offsets and A_t.col_indices are required"); - - auto const& A_offsets = *input.A.row_offsets; - auto const& A_cols = *input.A.col_indices; - auto const& A_t_offsets = *input.A_t.row_offsets; - auto const& A_t_cols = *input.A_t.col_indices; - - cuopt_expects(static_cast(A_offsets.size()) == input.nb_cstr + 1, - error_type_t::ValidationError, - "metis_partitioner: A.row_offsets size mismatch (expected nb_cstr+1)"); - cuopt_expects(static_cast(A_t_offsets.size()) == input.nb_vars + 1, - error_type_t::ValidationError, - "metis_partitioner: A_t.row_offsets size mismatch (expected nb_vars+1)"); - cuopt_expects(A_cols.size() == A_t_cols.size(), - error_type_t::ValidationError, - "metis_partitioner: A and A_t nnz mismatch"); - - const i_t nb_cstr = input.nb_cstr; - const i_t nb_vars = input.nb_vars; - const i_t nnz = static_cast(A_cols.size()); - const i_t nvtx = nb_cstr + nb_vars; - - // Bipartite CSR. Same construction as metis_tests/src/main.cpp: - // xadj has length nvtx + 1 - // adjncy has length 2 * nnz (each A nonzero contributes one half-edge - // from cstr side via A and one half-edge from var side via A_t) - std::vector xadj(nvtx + 1); - std::vector adjncy(2 * static_cast(nnz)); - - // cstr-side row offsets: A_offsets[0..nb_cstr] (no shift). - for (i_t i = 0; i <= nb_cstr; ++i) { - xadj[i] = static_cast(A_offsets[i]); - } - // var-side row offsets: A_t_offsets[0..nb_vars], shifted by +nnz so that - // they index into the second half of adjncy. - for (i_t i = 0; i <= nb_vars; ++i) { - xadj[nb_cstr + i] = static_cast(A_t_offsets[i]) + static_cast(nnz); - } - - // cstr-side neighbours: A_cols[i] shifted by +nb_cstr to index into the - // variable node block. - for (i_t k = 0; k < nnz; ++k) { - adjncy[k] = static_cast(A_cols[k]) + static_cast(nb_cstr); - } - // var-side neighbours: A_t_cols[i] already in [0, nb_cstr). - for (i_t k = 0; k < nnz; ++k) { - adjncy[nnz + k] = static_cast(A_t_cols[k]); - } - - idx_t metis_options[METIS_NOPTIONS]; - METIS_SetDefaultOptions(metis_options); - metis_options[METIS_OPTION_OBJTYPE] = METIS_OBJTYPE_CUT; - - idx_t metis_nvtx = static_cast(nvtx); - idx_t ncon = 1; - idx_t nparts = static_cast(input.nb_parts); - idx_t objval = 0; - std::vector metis_parts(nvtx); - - auto t0 = std::chrono::high_resolution_clock::now(); - const int status = METIS_PartGraphKway(&metis_nvtx, - &ncon, - xadj.data(), - adjncy.data(), - /*vwgt=*/nullptr, - /*vsize=*/nullptr, - /*adjwgt=*/nullptr, - &nparts, - /*tpwgts=*/nullptr, - /*ubvec=*/nullptr, - metis_options, - &objval, - metis_parts.data()); - auto t1 = std::chrono::high_resolution_clock::now(); - const double dt = std::chrono::duration(t1 - t0).count(); - cuopt_expects(status == METIS_OK, - error_type_t::RuntimeError, - "METIS_PartGraphKway failed (status=%d)", - status); - CUOPT_LOG_INFO( - "METIS partitioned bipartite graph: nvtx=%d nnz=%d nb_parts=%d edge_cut=%lld in %.3fs", - static_cast(nvtx), - static_cast(nnz), - static_cast(input.nb_parts), - static_cast(objval), - dt); - - std::vector parts(static_cast(nvtx)); - for (i_t i = 0; i < nvtx; ++i) { - parts[i] = static_cast(metis_parts[i]); - } - - validate_partition(parts, - static_cast(nb_cstr), - static_cast(nb_vars), - static_cast(input.nb_parts), - "metis_partitioner"); - return parts; -} - -template class metis_partitioner_t; - -} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp deleted file mode 100644 index c4e37f57a9..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/metis_partitioner.hpp +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -namespace cuopt::linear_programming::detail { - -// METIS k-way partitioner on the constraint/variable bipartite graph induced by A. -// Requires partitioner_input_t::A and A_t (or A row_offsets/col_indices only — the -// implementation builds the bipartite adjacency the same way as metis_tests: -// cstr nodes [0, nb_cstr), var nodes [nb_cstr, nb_cstr+nb_vars), edges from A and A_t). -// -// Wire into make_partitioner() once METIS is an optional cuOpt dependency. -template -class metis_partitioner_t : public partitioner_i { - public: - std::vector partition(partitioner_input_t const& input) const override; -}; - -} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index ff60f11cca..d498940e09 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -14,7 +14,7 @@ namespace cuopt::linear_programming::detail { template struct partition_loader_t { - // Read a Metis-style partition file: one part-id per line (whitespace-tolerant), + // Read a partition file: one part-id per line (whitespace-tolerant), // ASCII integers in [0, nb_parts). Returns a flat vector of length // nb_cstr + nb_vars, indexed as in create_rank_data_from_parts (cstrs first, then vars). static std::vector parse_distributed_pdlp_partition_file(std::string const& file); @@ -25,7 +25,7 @@ struct partition_loader_t { static void export_distributed_pdlp_partition_file(std::string const& file, std::vector const& parts); - // Slices the data to prepare a split from metis partitionning with halo communication + // Slices the data to prepare a split from graph partitioning with halo communication static std::vector> create_rank_data_from_parts( const std::vector& parts, const std::vector& A_row_offsets, diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cu b/cpp/src/pdlp/distributed_pdlp/partitioner.cu index 727a8b56f9..093c2cb23b 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cu +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cu @@ -4,7 +4,6 @@ */ #include -#include #include #include @@ -72,7 +71,6 @@ std::unique_ptr> make_partitioner(partitioner_kind_t kin { switch (kind) { case partitioner_kind_t::Dummy: return std::make_unique>(); - case partitioner_kind_t::Metis: return std::make_unique>(); case partitioner_kind_t::KaMinPar: return std::make_unique>(); } cuopt_expects( diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index 70b2e34c06..07db06fe3b 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -31,19 +31,18 @@ struct partitioner_input_t { i_t nb_parts{0}; // Number of CPU threads the partitioner may use. Only honored by the // multi-threaded KaMinPar backend; <= 0 means "auto" (all hardware threads). - // Serial backends (METIS, Dummy) ignore it. + // Serial backend (Dummy) ignore it. i_t nb_threads{0}; // Constraint matrix A (rows = constraints, cols = variables). csr_host_view_t A{}; // Transpose A_t (rows = variables, cols = constraints). Optional for partitioners - // that build a bipartite graph (e.g. METIS); dummy partitioner ignores both matrices. + // that build a bipartite graph (e.g. KaMinPar); dummy partitioner ignores both matrices. csr_host_view_t A_t{}; }; // Dummy: round-robin, no graph (single-shard / debugging). -// Metis: serial METIS_PartGraphKway. // KaMinPar: multi-threaded KaMinPar (preferred for multi-shard partitioning). -enum class partitioner_kind_t { Dummy, Metis, KaMinPar }; +enum class partitioner_kind_t { Dummy, KaMinPar }; template class partitioner_i { diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index af6f33956d..d63ac62c35 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -511,20 +511,17 @@ pdlp_solver_t::pdlp_solver_t( (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::KaMinPar; } else if (partitioner_choice == "dummy") { kind = partitioner_kind_t::Dummy; - } else if (partitioner_choice == "metis") { - kind = partitioner_kind_t::Metis; } else if (partitioner_choice == "kaminpar") { kind = partitioner_kind_t::KaMinPar; } else { cuopt_expects( false, error_type_t::ValidationError, - "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|metis|kaminpar)", + "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|kaminpar)", settings.distributed_pdlp_partitioner.c_str()); kind = partitioner_kind_t::Dummy; // unreachable; silences -Wmaybe-uninitialized } - const bool needs_graph = - (kind == partitioner_kind_t::Metis || kind == partitioner_kind_t::KaMinPar); + const bool needs_graph = (kind == partitioner_kind_t::KaMinPar); if (needs_graph) { // partitioner_input_t holds non-const std::vector* pointers; we // already have the data in our local mutable buffers above. @@ -536,11 +533,10 @@ pdlp_solver_t::pdlp_solver_t( partition_input.A_t.col_indices = &h_A_t_col_indices; partition_input.A_t.num_rows = n_vars; partition_input.A_t.num_cols = n_cstr; - // 0 => KaMinPar auto-detects and uses all hardware threads (ignored by METIS). + // 0 => KaMinPar auto-detects and uses all hardware threads. partition_input.nb_threads = 0; } const char* kind_name = (kind == partitioner_kind_t::Dummy) ? "dummy" - : (kind == partitioner_kind_t::Metis) ? "metis" : (kind == partitioner_kind_t::KaMinPar) ? "kaminpar" : "unknown"; CUOPT_LOG_INFO( diff --git a/cpp/src/pdlp/solve.cuh b/cpp/src/pdlp/solve.cuh index 160f4602ba..265f2ed2a2 100644 --- a/cpp/src/pdlp/solve.cuh +++ b/cpp/src/pdlp/solve.cuh @@ -45,7 +45,7 @@ cuopt::linear_programming::optimization_problem_solution_t solve_lp_wi * pipeline, which still allocates the full problem on master. The shim exists * so the public-facing call site is already in place; subsequent commits will * replace the body with: - * 1. host-side METIS partitioning straight off the MPS CSR + * 1. host-side graph partitioning straight off the MPS CSR * 2. per-shard host CSR slicing * 3. construction of an mGPU-native pdlp_solver_t whose master only holds * scalar metadata + gather buffers (no full A / A^T / scaled copies). diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 6def807e62..3728aba81c 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -99,9 +99,9 @@ TEST(pdlp_class, run_double) } // Distributed-PDLP partition round-trip: partition the afiro constraint/variable -// bipartite graph with METIS, write it out, read it back, and confirm the parsed +// bipartite graph with KaMinPar, write it out, read it back, and confirm the parsed // vector is identical to what the partitioner produced. -TEST(pdlp_class, distributed_partition_metis_export_import_roundtrip) +TEST(pdlp_class, distributed_partition_kaminpar_export_import_roundtrip) { using namespace cuopt::linear_programming::detail; namespace ds = cuopt::linear_programming::dual_simplex; @@ -141,13 +141,13 @@ TEST(pdlp_class, distributed_partition_metis_export_import_roundtrip) input.A_t.num_rows = n_vars; input.A_t.num_cols = n_cstr; - auto partitioner = make_partitioner(partitioner_kind_t::Metis); + auto partitioner = make_partitioner(partitioner_kind_t::KaMinPar); std::vector parts = partitioner->partition(input); ASSERT_EQ(parts.size(), static_cast(n_cstr + n_vars)); std::string dir = ::testing::TempDir(); if (!dir.empty() && dir.back() != '/') { dir.push_back('/'); } - const std::string out_path = dir + "afiro_metis_roundtrip.parts"; + const std::string out_path = dir + "afiro_kaminpar_roundtrip.parts"; partition_loader_t::export_distributed_pdlp_partition_file(out_path, parts); std::vector reloaded = From 12edf2db4e0cf8a733ecd679178b9c3e19c27e62 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 15 Jun 2026 16:01:04 +0200 Subject: [PATCH 087/258] Change the way kaminpar is pulled --- cpp/CMakeLists.txt | 17 ++++++++++++++++- cpp/cmake/thirdparty/get_kaminpar.cmake | 8 ++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d612c1b88c..61f6cd5b86 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -584,6 +584,22 @@ target_include_directories(cuopt target_link_libraries(cuopt PRIVATE $) add_dependencies(cuopt PSLP) +# Link KaMinPar by file to avoid export dependency tracking (mirrors PSLP above). +# KaMinPar is a from-source static library fully embedded into libcuopt.so; it is never +# installed (INSTALL_KAMINPAR OFF) and consumers of cuopt::cuopt never use it, so it must +# not leak into cuopt's exported link interface (otherwise rapids_export fails with +# "target KaMinPar is not in any export set"). libKaMinPar.a is self-contained (the +# kaminpar-common OBJECT lib is archived into it); we only need its public headers +# (, which pulls in stdlib + TBB) at compile time. +target_include_directories(cuopt SYSTEM PRIVATE + $) +target_link_libraries(cuopt PRIVATE $) +# Real (non-imported) target only exists when KaMinPar is built from source via CPM; +# force it to build before cuopt since it is EXCLUDE_FROM_ALL. +if (TARGET KaMinPar) + add_dependencies(cuopt KaMinPar) +endif () + # ################################################################################################## # - link libraries -------------------------------------------------------------------------------- @@ -635,7 +651,6 @@ target_link_libraries(cuopt PRIVATE ${CUOPT_PRIVATE_CUDA_LIBS} nccl_external - KaMinPar::KaMinPar $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> ) diff --git a/cpp/cmake/thirdparty/get_kaminpar.cmake b/cpp/cmake/thirdparty/get_kaminpar.cmake index c662809fd2..c3e4856867 100644 --- a/cpp/cmake/thirdparty/get_kaminpar.cmake +++ b/cpp/cmake/thirdparty/get_kaminpar.cmake @@ -12,10 +12,14 @@ function(find_and_configure_kaminpar) set(oneValueArgs VERSION PINNED_TAG) cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN}) + # NOTE: KaMinPar is intentionally NOT added to cuopt's BUILD/INSTALL export sets. + # It is a from-source static dependency that is fully embedded into libcuopt.so and + # never installed (INSTALL_KAMINPAR OFF below). Registering it in cuopt-exports would + # both break export generation ("target KaMinPar is not in any export set") and emit a + # bogus find_dependency(KaMinPar) into the installed cuopt config. It is linked by file + # in cpp/CMakeLists.txt (mirroring PSLP) so it stays out of cuopt's export interface. rapids_cpm_find(KaMinPar ${PKG_VERSION} GLOBAL_TARGETS KaMinPar::KaMinPar - BUILD_EXPORT_SET cuopt-exports - INSTALL_EXPORT_SET cuopt-exports CPM_ARGS GIT_REPOSITORY https://github.com/KaHIP/KaMinPar.git GIT_TAG ${PKG_PINNED_TAG} From 2a357037add96ee64e3b8022eccd831fef048ace Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 15 Jun 2026 16:13:12 +0200 Subject: [PATCH 088/258] style --- .../pdlp/distributed_pdlp/partition_loader.cu | 27 ++++++++++--------- cpp/src/pdlp/pdlp.cu | 9 +++---- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index a3496bce7c..3672ce009a 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -136,10 +136,12 @@ std::vector> partition_loader_t::create_rank_dat for (auto owned_cstr : rd.owned_cstr_indices) { i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; i_t row_start = A_row_offsets[owned_cstr]; - local_A_col_indices.insert( - local_A_col_indices.end(), A_col_indices.begin() + row_start, A_col_indices.begin() + row_start + cstr_len); - local_A_values.insert( - local_A_values.end(), A_values.begin() + row_start, A_values.begin() + row_start + cstr_len); + local_A_col_indices.insert(local_A_col_indices.end(), + A_col_indices.begin() + row_start, + A_col_indices.begin() + row_start + cstr_len); + local_A_values.insert(local_A_values.end(), + A_values.begin() + row_start, + A_values.begin() + row_start + cstr_len); local_A_nnz += cstr_len; local_A_row_offsets.push_back(local_A_nnz); } @@ -163,9 +165,9 @@ std::vector> partition_loader_t::create_rank_dat rank_data[peer].var_send_per_peer[rank] = std::move(needed_var_from_peer[peer]); } - rd.h_A_row_offsets = std::move(local_A_row_offsets); - rd.h_A_col_indices = std::move(local_A_col_indices); - rd.h_A_values = std::move(local_A_values); + rd.h_A_row_offsets = std::move(local_A_row_offsets); + rd.h_A_col_indices = std::move(local_A_col_indices); + rd.h_A_values = std::move(local_A_values); // ---- A_t side ---- std::vector local_A_t_row_offsets; @@ -183,8 +185,9 @@ std::vector> partition_loader_t::create_rank_dat local_A_t_col_indices.insert(local_A_t_col_indices.end(), A_t_col_indices.begin() + row_start, A_t_col_indices.begin() + row_start + var_len); - local_A_t_values.insert( - local_A_t_values.end(), A_t_values.begin() + row_start, A_t_values.begin() + row_start + var_len); + local_A_t_values.insert(local_A_t_values.end(), + A_t_values.begin() + row_start, + A_t_values.begin() + row_start + var_len); local_A_t_nnz += var_len; local_A_t_row_offsets.push_back(local_A_t_nnz); } @@ -207,9 +210,9 @@ std::vector> partition_loader_t::create_rank_dat rank_data[peer].cstr_send_per_peer[rank] = std::move(needed_cstr_from_peer[peer]); } - rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); - rd.h_A_t_col_indices = std::move(local_A_t_col_indices); - rd.h_A_t_values = std::move(local_A_t_values); + rd.h_A_t_row_offsets = std::move(local_A_t_row_offsets); + rd.h_A_t_col_indices = std::move(local_A_t_col_indices); + rd.h_A_t_values = std::move(local_A_t_values); rd.total_var_size = rd.owned_var_size + static_cast(seen_needed_vars.size()); rd.total_cstr_size = rd.owned_cstr_size + static_cast(seen_needed_cstrs.size()); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index d63ac62c35..21c9f6530a 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -514,11 +514,10 @@ pdlp_solver_t::pdlp_solver_t( } else if (partitioner_choice == "kaminpar") { kind = partitioner_kind_t::KaMinPar; } else { - cuopt_expects( - false, - error_type_t::ValidationError, - "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|kaminpar)", - settings.distributed_pdlp_partitioner.c_str()); + cuopt_expects(false, + error_type_t::ValidationError, + "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|kaminpar)", + settings.distributed_pdlp_partitioner.c_str()); kind = partitioner_kind_t::Dummy; // unreachable; silences -Wmaybe-uninitialized } const bool needs_graph = (kind == partitioner_kind_t::KaMinPar); From f5ac616d1999e80fa969e911731fbda2cd4c9187 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 15 Jun 2026 16:27:40 +0200 Subject: [PATCH 089/258] Better KaminPAr pulling --- cpp/CMakeLists.txt | 4 ++++ cpp/cmake/thirdparty/get_kaminpar.cmake | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 61f6cd5b86..ef07a63124 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -593,6 +593,10 @@ add_dependencies(cuopt PSLP) # (, which pulls in stdlib + TBB) at compile time. target_include_directories(cuopt SYSTEM PRIVATE $) +# kaminpar_partitioner.cpp includes , which pulls in . +# On older TBB that header is gated behind TBB_PREVIEW_GLOBAL_CONTROL. Since KaMinPar is +# linked by file (no PUBLIC propagation of its compile definitions), define it on cuopt too. +target_compile_definitions(cuopt PRIVATE TBB_PREVIEW_GLOBAL_CONTROL) target_link_libraries(cuopt PRIVATE $) # Real (non-imported) target only exists when KaMinPar is built from source via CPM; # force it to build before cuopt since it is EXCLUDE_FROM_ALL. diff --git a/cpp/cmake/thirdparty/get_kaminpar.cmake b/cpp/cmake/thirdparty/get_kaminpar.cmake index c3e4856867..9aaa9b762b 100644 --- a/cpp/cmake/thirdparty/get_kaminpar.cmake +++ b/cpp/cmake/thirdparty/get_kaminpar.cmake @@ -46,6 +46,14 @@ function(find_and_configure_kaminpar) if(KaMinPar_ADDED) message(VERBOSE "CUOPT: Using KaMinPar located in ${KaMinPar_SOURCE_DIR}") + # KaMinPar's public header pulls in . On older TBB releases + # that header is gated behind TBB_PREVIEW_GLOBAL_CONTROL (KaMinPar upstream assumes a + # newer oneTBB and never defines it). Define it on KaMinParCommon PUBLIC so it + # propagates to all KaMinPar translation units (KaMinPar links KaMinParCommon PUBLIC). + # Harmless on newer oneTBB where global_control is no longer a preview feature. + if(TARGET KaMinParCommon) + target_compile_definitions(KaMinParCommon PUBLIC TBB_PREVIEW_GLOBAL_CONTROL) + endif() else() message(VERBOSE "CUOPT: Using KaMinPar located in ${KaMinPar_DIR}") endif() From 40f7cf5232c988a76dd088e022758bf9b27abe26 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 15 Jun 2026 16:58:09 +0200 Subject: [PATCH 090/258] better kaminpar pulling again --- cpp/cmake/thirdparty/get_kaminpar.cmake | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cpp/cmake/thirdparty/get_kaminpar.cmake b/cpp/cmake/thirdparty/get_kaminpar.cmake index 9aaa9b762b..8e060544a2 100644 --- a/cpp/cmake/thirdparty/get_kaminpar.cmake +++ b/cpp/cmake/thirdparty/get_kaminpar.cmake @@ -42,6 +42,14 @@ function(find_and_configure_kaminpar) # Large LP constraint graphs can exceed 2^31 directed edges. "KAMINPAR_64BIT_EDGE_IDS ON" "INSTALL_KAMINPAR OFF" + # Build KaMinPar as a STATIC library that is embedded into libcuopt.so (linked + # by file in cpp/CMakeLists.txt). The wheel build configures with + # BUILD_SHARED_LIBS=ON; without this override KaMinPar would build a separate + # libKaMinPar.so that is neither embedded nor shipped in the wheel. Forcing PIC + # is required so the static objects can be linked into the shared libcuopt.so + # (KaMinPar's KaMinParCommon OBJECT lib otherwise lacks -fPIC). + "BUILD_SHARED_LIBS OFF" + "CMAKE_POSITION_INDEPENDENT_CODE ON" ) if(KaMinPar_ADDED) @@ -51,6 +59,14 @@ function(find_and_configure_kaminpar) # newer oneTBB and never defines it). Define it on KaMinParCommon PUBLIC so it # propagates to all KaMinPar translation units (KaMinPar links KaMinParCommon PUBLIC). # Harmless on newer oneTBB where global_control is no longer a preview feature. + # Also force PIC on every KaMinPar target (the KaMinParCommon OBJECT library does not + # reliably inherit CMAKE_POSITION_INDEPENDENT_CODE) so the static archive can be + # embedded into the shared libcuopt.so. + foreach(_kaminpar_tgt KaMinParCommon KaMinPar KaMinParIO) + if(TARGET ${_kaminpar_tgt}) + set_target_properties(${_kaminpar_tgt} PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() + endforeach() if(TARGET KaMinParCommon) target_compile_definitions(KaMinParCommon PUBLIC TBB_PREVIEW_GLOBAL_CONTROL) endif() From c4565d14014843db3d45ec67c015eff3d62c00e7 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 14:07:39 +0200 Subject: [PATCH 091/258] added KAMINPAR_64BIT_EDGE_IDS --- cpp/CMakeLists.txt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index ef07a63124..77a6c0473e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -593,10 +593,16 @@ add_dependencies(cuopt PSLP) # (, which pulls in stdlib + TBB) at compile time. target_include_directories(cuopt SYSTEM PRIVATE $) -# kaminpar_partitioner.cpp includes , which pulls in . -# On older TBB that header is gated behind TBB_PREVIEW_GLOBAL_CONTROL. Since KaMinPar is -# linked by file (no PUBLIC propagation of its compile definitions), define it on cuopt too. -target_compile_definitions(cuopt PRIVATE TBB_PREVIEW_GLOBAL_CONTROL) +# kaminpar_partitioner.cpp includes . Because KaMinPar is linked by file, cuopt +# does NOT inherit KaMinPar's PUBLIC compile definitions, so we must replicate the ones that +# affect what compiles to: +# * TBB_PREVIEW_GLOBAL_CONTROL - includes , which older +# TBB gates behind this macro. +# * KAMINPAR_64BIT_EDGE_IDS - selects EdgeID = uint64 in . MUST stay in sync +# with the KAMINPAR_64BIT_* options in get_kaminpar.cmake; +# otherwise the public API types (e.g. copy_graph's span<> +# widths) mismatch the prebuilt archive => undefined refs. +target_compile_definitions(cuopt PRIVATE TBB_PREVIEW_GLOBAL_CONTROL KAMINPAR_64BIT_EDGE_IDS) target_link_libraries(cuopt PRIVATE $) # Real (non-imported) target only exists when KaMinPar is built from source via CPM; # force it to build before cuopt since it is EXCLUDE_FROM_ALL. From 3acd4d3681c887649215af86859a273e608f0637 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 14:22:31 +0200 Subject: [PATCH 092/258] review ready kaminpar partitionner --- .../distributed_pdlp/kaminpar_partitioner.cpp | 17 +++++++++++------ .../distributed_pdlp/kaminpar_partitioner.hpp | 3 +-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp index 487b8da9bd..41d0eb34b5 100644 --- a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp @@ -27,7 +27,7 @@ namespace cuopt::linear_programming::detail { // multi-threaded KaMinPar k-way kernel. // * nodes [0, nb_cstr) : constraint nodes // * nodes [nb_cstr, nb_cstr+nb_vars): variable nodes -// * undirected edges from each A nonzero (one half via A, one via A_t) +// * each edge is a nnz between a constraint and a variable template std::vector kaminpar_partitioner_t::partition( partitioner_input_t const& input) const @@ -39,9 +39,7 @@ std::vector kaminpar_partitioner_t::partition( error_type_t::ValidationError, "kaminpar_partitioner: invalid problem dimensions"); - // The k-way kernel needs at least 2 blocks. For the single-shard case the - // partition is trivial (everything in block 0); short-circuit so KaMinPar can - // still be selected with distributed_pdlp_num_gpus == 1 without crashing. + // return trivial partition if only one part if (input.nb_parts == 1) { CUOPT_LOG_INFO("KaMinPar: nb_parts == 1, returning trivial single-block partition"); return std::vector(static_cast(input.nb_cstr + input.nb_vars), i_t{0}); @@ -84,6 +82,9 @@ std::vector kaminpar_partitioner_t::partition( std::vector xadj(static_cast(nvtx) + 1); std::vector adjncy(2 * static_cast(nnz)); + // CSR already represents an adjency list of cstr -> variables. + // Adding the transpose to represent the var -> cstr edges. + // Casting the types to KaMinPar friendly types for (i_t i = 0; i <= nb_cstr; ++i) { xadj[i] = static_cast(A_offsets[i]); } @@ -106,13 +107,17 @@ std::vector kaminpar_partitioner_t::partition( engine.copy_graph(std::span(xadj), std::span(adjncy)); engine.set_k(static_cast(input.nb_parts)); - // ~3% imbalance balance constraint. + + // Allow up to 3% number of nodes imbalance. Could be knobbed for maximum performance engine.set_uniform_max_block_weights(0.03); + // The actual partition computation auto t0 = std::chrono::high_resolution_clock::now(); + const kaminpar::shm::EdgeWeight edge_cut = engine.compute_partition(std::span(block_of)); - auto t1 = std::chrono::high_resolution_clock::now(); + + auto t1 = std::chrono::high_resolution_clock::now(); const double dt = std::chrono::duration(t1 - t0).count(); CUOPT_LOG_INFO( diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp index babbc60732..02150f6e61 100644 --- a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp @@ -12,8 +12,7 @@ namespace cuopt::linear_programming::detail { // Multi-threaded k-way partitioner backed by KaMinPar. Builds a // constraint/variable bipartite graph and runs the // shared-memory parallel KaMinPar kernel so partitioning scales across all CPU -// cores of a node (set via partitioner_input_t::nb_threads; <= 0 => all -// hardware threads). +// cores of a node template class kaminpar_partitioner_t : public partitioner_i { public: From cfbb27a4c9d3c0c7f6351653c5ab08b81ac35264 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 14:38:40 +0200 Subject: [PATCH 093/258] wheel measured ~727 MiB so bump up max allowed size --- ci/validate_wheel.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/validate_wheel.sh b/ci/validate_wheel.sh index a603c69098..128b23ddca 100755 --- a/ci/validate_wheel.sh +++ b/ci/validate_wheel.sh @@ -22,11 +22,11 @@ PYDISTCHECK_ARGS=( if [[ "${package_dir}" == "python/libcuopt" ]]; then if [[ "${RAPIDS_CUDA_MAJOR}" == "12" ]]; then PYDISTCHECK_ARGS+=( - --max-allowed-size-compressed '690Mi' + --max-allowed-size-compressed '850Mi' ) else PYDISTCHECK_ARGS+=( - --max-allowed-size-compressed '550Mi' + --max-allowed-size-compressed '800Mi' ) fi elif [[ "${package_dir}" != "python/cuopt" ]] && \ From 5855aa8bd4bcb4decf455dc9ccf4e383a989dbf4 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 15:14:05 +0200 Subject: [PATCH 094/258] bump wheel size --- ci/validate_wheel.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/validate_wheel.sh b/ci/validate_wheel.sh index 128b23ddca..a5ea2a2a48 100755 --- a/ci/validate_wheel.sh +++ b/ci/validate_wheel.sh @@ -22,7 +22,7 @@ PYDISTCHECK_ARGS=( if [[ "${package_dir}" == "python/libcuopt" ]]; then if [[ "${RAPIDS_CUDA_MAJOR}" == "12" ]]; then PYDISTCHECK_ARGS+=( - --max-allowed-size-compressed '850Mi' + --max-allowed-size-compressed '960Mi' ) else PYDISTCHECK_ARGS+=( From 7917f6662e65e9818a53f4ad54f8a673af9796de Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 15:19:04 +0200 Subject: [PATCH 095/258] replaced 19 args function with a mps_data_model_t --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 41 +-------- .../distributed_pdlp/multi_gpu_engine.hpp | 22 +---- cpp/src/pdlp/distributed_pdlp/shard.cu | 88 ++++++++----------- cpp/src/pdlp/distributed_pdlp/shard.hpp | 19 +--- cpp/src/pdlp/pdlp.cu | 62 ++----------- 5 files changed, 53 insertions(+), 179 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 6ad7eebb22..dd5857b14d 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -25,23 +25,7 @@ namespace cuopt::linear_programming::detail { template multi_gpu_engine_t::multi_gpu_engine_t( std::vector>&& rank_data, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - std::vector const& h_global_obj_scaled, - std::vector const& h_global_var_lower_scaled, - std::vector const& h_global_var_upper_scaled, - std::vector const& h_global_cstr_lower_scaled, - std::vector const& h_global_cstr_upper_scaled, - std::vector const& h_global_cummulative_cstr_scaling, - std::vector const& h_global_cummulative_var_scaling, - f_t h_bound_rescaling, - f_t h_objective_rescaling, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, + io::mps_data_model_t const& mps, pdlp_solver_settings_t const& sub_solver_settings) : stream() { @@ -64,27 +48,8 @@ multi_gpu_engine_t::multi_gpu_engine_t( auto shard_build_t0 = std::chrono::high_resolution_clock::now(); for (int r = 0; r < nb_parts; ++r) { raft::device_setter guard(devices[r]); // shard ctor needs device set - shards.emplace_back(std::make_unique>(devices[r], - std::move(rank_data[r]), - raw_comms[r], - h_global_obj, - h_global_var_lower, - h_global_var_upper, - h_global_cstr_lower, - h_global_cstr_upper, - h_global_obj_scaled, - h_global_var_lower_scaled, - h_global_var_upper_scaled, - h_global_cstr_lower_scaled, - h_global_cstr_upper_scaled, - h_global_cummulative_cstr_scaling, - h_global_cummulative_var_scaling, - h_bound_rescaling, - h_objective_rescaling, - maximize, - objective_offset, - objective_scaling_factor, - sub_solver_settings)); + shards.emplace_back(std::make_unique>( + devices[r], std::move(rank_data[r]), raw_comms[r], mps, sub_solver_settings)); } auto shard_build_t1 = std::chrono::high_resolution_clock::now(); CUOPT_LOG_INFO("distributed_pdlp: shard build done in %.3f s", diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 89153e8bd7..522cf12888 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -75,25 +76,10 @@ struct mgpu_weighted_sq_op_t { template struct multi_gpu_engine_t { - // Constructs shards from rank_data + // Constructs shards from rank_data. The global (unpartitioned) problem is + // read straight from `mps`; each shard slices out the entries it owns. multi_gpu_engine_t(std::vector>&& rank_data, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - std::vector const& h_global_obj_scaled, - std::vector const& h_global_var_lower_scaled, - std::vector const& h_global_var_upper_scaled, - std::vector const& h_global_cstr_lower_scaled, - std::vector const& h_global_cstr_upper_scaled, - std::vector const& h_global_cummulative_cstr_scaling, - std::vector const& h_global_cummulative_var_scaling, - f_t h_bound_rescaling, - f_t h_objective_rescaling, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, + io::mps_data_model_t const& mps, pdlp_solver_settings_t const& sub_solver_settings); multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index b451b52c0e..44ca0c88f4 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -25,23 +25,7 @@ template pdlp_shard_t::pdlp_shard_t(int device_id, rank_data_t&& rd, ncclComm_t raw_comm, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - std::vector const& h_global_obj_scaled, - std::vector const& h_global_var_lower_scaled, - std::vector const& h_global_var_upper_scaled, - std::vector const& h_global_cstr_lower_scaled, - std::vector const& h_global_cstr_upper_scaled, - std::vector const& h_global_cummulative_cstr_scaling, - std::vector const& h_global_cummulative_var_scaling, - f_t h_bound_rescaling, - f_t h_objective_rescaling, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, + io::mps_data_model_t const& mps, pdlp_solver_settings_t const& settings) : device_id(device_id), stream(), @@ -55,51 +39,57 @@ pdlp_shard_t::pdlp_shard_t(int device_id, assert(raft::device_setter::get_current_device() == device_id && "Right device must be set before building the shard"); + // ---- 0. Problem-level scalars, taken straight from the global mps. ---- + // For a maximize problem we negate the objective (and offset / scaling + // factor), matching problem_helpers.cuh::convert_to_maximization_problem. + const bool maximize = mps.get_sense(); + f_t objective_offset = mps.get_objective_offset(); + f_t objective_scaling_factor = mps.get_objective_scaling_factor(); + if (maximize) { + objective_offset = -objective_offset; + objective_scaling_factor = -objective_scaling_factor; + } + + // Global (unpartitioned) host arrays, indexed by global var / cstr id. + const std::vector& g_obj = mps.get_objective_coefficients(); + const std::vector& g_var_lower = mps.get_variable_lower_bounds(); + const std::vector& g_var_upper = mps.get_variable_upper_bounds(); + const std::vector& g_cstr_lower = mps.get_constraint_lower_bounds(); + const std::vector& g_cstr_upper = mps.get_constraint_upper_bounds(); + // ---- 1. Gather per-shard host slices using rank_data's index maps. ---- // All vectors are sized to TOTAL (owned + halo). Owned slots get real - // values; halo slots keep defaults because they should not be accessed + // values; halo slots keep defaults because they should not be accessed. std::vector h_obj(rank_data.total_var_size, f_t{0}); std::vector h_var_lower(rank_data.total_var_size, -std::numeric_limits::infinity()); std::vector h_var_upper(rank_data.total_var_size, std::numeric_limits::infinity()); std::vector h_cstr_lower(rank_data.total_cstr_size, -std::numeric_limits::infinity()); std::vector h_cstr_upper(rank_data.total_cstr_size, std::numeric_limits::infinity()); - std::vector h_obj_scaled(rank_data.total_var_size, f_t{0}); - std::vector h_var_lower_scaled(rank_data.total_var_size, - -std::numeric_limits::infinity()); - std::vector h_var_upper_scaled(rank_data.total_var_size, - std::numeric_limits::infinity()); - std::vector h_cstr_lower_scaled(rank_data.total_cstr_size, - -std::numeric_limits::infinity()); - std::vector h_cstr_upper_scaled(rank_data.total_cstr_size, - std::numeric_limits::infinity()); - for (i_t i = 0; i < rank_data.owned_var_size; ++i) { - const auto g = rank_data.local_to_global_var[i]; - h_obj[i] = h_global_obj[g]; - h_var_lower[i] = h_global_var_lower[g]; - h_var_upper[i] = h_global_var_upper[g]; - h_obj_scaled[i] = h_global_obj_scaled[g]; - h_var_lower_scaled[i] = h_global_var_lower_scaled[g]; - h_var_upper_scaled[i] = h_global_var_upper_scaled[g]; + const auto g = rank_data.local_to_global_var[i]; + h_obj[i] = maximize ? -g_obj[g] : g_obj[g]; + h_var_lower[i] = g_var_lower[g]; + h_var_upper[i] = g_var_upper[g]; } for (i_t i = 0; i < rank_data.owned_cstr_size; ++i) { - const auto g = rank_data.local_to_global_cstr[i]; - h_cstr_lower[i] = h_global_cstr_lower[g]; - h_cstr_upper[i] = h_global_cstr_upper[g]; - h_cstr_lower_scaled[i] = h_global_cstr_lower_scaled[g]; - h_cstr_upper_scaled[i] = h_global_cstr_upper_scaled[g]; + const auto g = rank_data.local_to_global_cstr[i]; + h_cstr_lower[i] = g_cstr_lower[g]; + h_cstr_upper[i] = g_cstr_upper[g]; } - // Get local scaling factors - std::vector h_cstr_scaling_local(rank_data.total_cstr_size, f_t{1}); - std::vector h_var_scaling_local(rank_data.total_var_size, f_t{1}); - for (i_t i = 0; i < rank_data.owned_cstr_size; ++i) { - h_cstr_scaling_local[i] = h_global_cummulative_cstr_scaling[rank_data.local_to_global_cstr[i]]; - } - for (i_t i = 0; i < rank_data.owned_var_size; ++i) { - h_var_scaling_local[i] = h_global_cummulative_var_scaling[rank_data.local_to_global_var[i]]; - } + // Identity scaling: cumulative scaling factors are 1 and the bound / + // objective rescaling scalars are 1. The "scaled" arrays injected below are + // just the unscaled slices. + const std::vector& h_obj_scaled = h_obj; + const std::vector& h_var_lower_scaled = h_var_lower; + const std::vector& h_var_upper_scaled = h_var_upper; + const std::vector& h_cstr_lower_scaled = h_cstr_lower; + const std::vector& h_cstr_upper_scaled = h_cstr_upper; + const std::vector h_cstr_scaling_local(rank_data.total_cstr_size, f_t{1}); + const std::vector h_var_scaling_local(rank_data.total_var_size, f_t{1}); + const f_t h_bound_rescaling = f_t{1}; + const f_t h_objective_rescaling = f_t{1}; // ---- 2. Build optimization_problem_t on this shard's device (UNSCALED). ---- opt_problem.emplace(&handle); diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 35babc12db..f96c014f77 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -51,23 +52,7 @@ struct pdlp_shard_t { pdlp_shard_t(int device_id, rank_data_t&& rd, ncclComm_t raw_comm, - std::vector const& h_global_obj, - std::vector const& h_global_var_lower, - std::vector const& h_global_var_upper, - std::vector const& h_global_cstr_lower, - std::vector const& h_global_cstr_upper, - std::vector const& h_global_obj_scaled, - std::vector const& h_global_var_lower_scaled, - std::vector const& h_global_var_upper_scaled, - std::vector const& h_global_cstr_lower_scaled, - std::vector const& h_global_cstr_upper_scaled, - std::vector const& h_global_cummulative_cstr_scaling, - std::vector const& h_global_cummulative_var_scaling, - f_t h_bound_rescaling, - f_t h_objective_rescaling, - bool maximize, - f_t objective_offset, - f_t objective_scaling_factor, + io::mps_data_model_t const& mps, pdlp_solver_settings_t const& settings); pdlp_shard_t(const pdlp_shard_t&) = delete; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 21c9f6530a..0105087ca5 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -432,25 +432,6 @@ pdlp_solver_t::pdlp_solver_t( error_type_t::ValidationError, "mps variable_upper_bounds size must equal n_variables"); - const bool maximize = mps.get_sense(); - f_t objective_offset = mps.get_objective_offset(); - f_t objective_scaling_factor = mps.get_objective_scaling_factor(); - - // Objective: copy (mutable so we can negate for maximize, matching - // problem_helpers.cuh::convert_to_maximization_problem). - std::vector h_obj = mps.get_objective_coefficients(); - if (maximize) { - for (auto& v : h_obj) - v = -v; - objective_offset = -objective_offset; - objective_scaling_factor = -objective_scaling_factor; - } - - // Bounds (copy from mps; engine ctor takes by const ref to std::vector). - std::vector h_var_lower = mps.get_variable_lower_bounds(); - std::vector h_var_upper = mps.get_variable_upper_bounds(); - std::vector h_cstr_lower = mps.get_constraint_lower_bounds(); - std::vector h_cstr_upper = mps.get_constraint_upper_bounds(); // A (CSR) — mutable copies for the engine + partitioner consumers below. std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); @@ -472,22 +453,7 @@ pdlp_solver_t::pdlp_solver_t( std::vector h_A_t_col_indices = std::move(AT_as_csc.i); std::vector h_A_t_values = std::move(AT_as_csc.x); - // ----- 3. Identity scaling for V1 ----- - // Real multi-GPU scaling is a TODO; ship the unscaled problem to shards as - // both "unscaled" and "scaled" so the engine and per-shard pdlp_solver_t - // can run end-to-end. Scaling factor vectors are 1.0 everywhere so the - // shard-side unscale at the end is a no-op. - std::vector h_obj_scaled = h_obj; - std::vector h_var_lower_scaled = h_var_lower; - std::vector h_var_upper_scaled = h_var_upper; - std::vector h_cstr_lower_scaled = h_cstr_lower; - std::vector h_cstr_upper_scaled = h_cstr_upper; - std::vector h_cummulative_cstr_scaling(n_cstr, f_t(1.0)); - std::vector h_cummulative_var_scaling(n_vars, f_t(1.0)); - const f_t h_bound_rescaling = f_t(1.0); - const f_t h_objective_rescaling = f_t(1.0); - - // ----- 4. Partition ----- + // ----- 3. Partition ----- std::vector parts; if (!settings.multi_gpu_partition_file.empty()) { parts = partition_loader_t::parse_distributed_pdlp_partition_file( @@ -557,7 +523,7 @@ pdlp_solver_t::pdlp_solver_t( << settings.multi_gpu_export_partition_file << std::endl; } - // ----- 5. Build per-rank data ----- + // ----- 4. Build per-rank data ----- CUOPT_LOG_INFO("distributed_pdlp: building rank_data for %d parts ...", settings.distributed_pdlp_num_gpus); auto rank_data_t0 = std::chrono::high_resolution_clock::now(); @@ -577,7 +543,7 @@ pdlp_solver_t::pdlp_solver_t( CUOPT_LOG_INFO("distributed_pdlp: rank_data build done in %.3f s", std::chrono::duration(rank_data_t1 - rank_data_t0).count()); - // ----- 6. Per-shard settings ----- + // ----- 5. Per-shard settings ----- pdlp_solver_settings_t sub_pdlp_settings = settings; sub_pdlp_settings.num_gpus = 1; sub_pdlp_settings.distributed_pdlp_num_gpus = 1; @@ -587,26 +553,8 @@ pdlp_solver_t::pdlp_solver_t( sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; - // ----- 7. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- - multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), - h_obj, - h_var_lower, - h_var_upper, - h_cstr_lower, - h_cstr_upper, - h_obj_scaled, - h_var_lower_scaled, - h_var_upper_scaled, - h_cstr_lower_scaled, - h_cstr_upper_scaled, - h_cummulative_cstr_scaling, - h_cummulative_var_scaling, - h_bound_rescaling, - h_objective_rescaling, - maximize, - objective_offset, - objective_scaling_factor, - sub_pdlp_settings); + // ----- 6. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- + multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), mps, sub_pdlp_settings); // ----- 8 Distributed Scaling ----- for (auto& shard : multi_gpu_engine->shards) { From a286381621c3287d1d07fa7d10b4f9a6f4b595ce Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 17:32:42 +0200 Subject: [PATCH 096/258] cleaned multi_gpu engine. now taking mps_data_model as input. --- .../distributed_pdlp/multi_gpu_engine.hpp | 286 +++++++----------- cpp/src/pdlp/distributed_pdlp/shard.cu | 10 + cpp/src/pdlp/distributed_pdlp/shard.hpp | 6 + 3 files changed, 118 insertions(+), 184 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 522cf12888..8eabb7d7f3 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -24,10 +25,7 @@ #include #include -#include -#include #include -#include #include #include @@ -51,29 +49,6 @@ struct sqrt_inplace_op_t { __host__ __device__ f_t operator()(f_t x) const { return raft::sqrt(x); } }; -// Squared-norm contribution of a constraint's [lower, upper] bound pair, used to -// build the distributed bound rescaling (mirrors rhs_sum_of_squares_t). Defined -// at namespace scope to avoid extended-lambda-in-template restrictions. -template -struct mgpu_rhs_sq_op_t { - __host__ __device__ f_t operator()(const thrust::tuple& t) const - { - const f_t lower = thrust::get<0>(t); - const f_t upper = thrust::get<1>(t); - f_t sum = f_t(0); - if (isfinite(lower) && (lower != upper)) sum += lower * lower; - if (isfinite(upper)) sum += upper * upper; - return sum; - } -}; - -// Weighted square of an objective coefficient (mirrors weighted_square_op). -template -struct mgpu_weighted_sq_op_t { - f_t weight; - __host__ __device__ f_t operator()(f_t v) const { return v * v * weight; } -}; - template struct multi_gpu_engine_t { // Constructs shards from rank_data. The global (unpartitioned) problem is @@ -109,7 +84,9 @@ struct multi_gpu_engine_t { cub::DeviceTransform::Transform(cub_inputs, out(sub), sz(sub), op, shard.stream.view()); }); } + // --- 2) convenience: single input accessor (delegates) --- + // Allows to use distributed_transform on single input without having to do a std::make_tuple(in) template void distributed_transform(InAccess in, OutAccess out, SizeAccess sz, Op op) { @@ -119,11 +96,22 @@ struct multi_gpu_engine_t { // -------- Halo exchange (variables / x) --------------------------------- // Fills the halo slice [owned_var_size, total_var_size) of the per-shard // input buffer returned by `buf_access(pdhg)` (the buffer A @ x will read). - // Step 1: thrust::gather per-peer outgoing values into staging buffers. - // Step 2: a single NCCL group with matched ncclSend / ncclRecv across all - // (rank, peer) pairs. template void halo_exchange_var(BufAccess&& buf_access) + { + halo_exchange_var_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { + return buf_access(s.sub_pdlp->pdhg_solver_); + }); + } + + // Core variable halo exchange. ShardBufAccess maps a shard to the var-shaped + // device buffer to synchronize (owned slice [0, owned_var_size) followed by + // the per-peer halo tail). + // Step 1: thrust::gather per-peer outgoing values into staging buffers. + // Step 2: a single NCCL group with matched ncclSend / ncclRecv across all + // (rank, peer) pairs, receiving into each shard's halo region. + template + void halo_exchange_var_shard(ShardBufAccess&& buf_access) { const int nb = static_cast(shards.size()); @@ -131,7 +119,7 @@ struct multi_gpu_engine_t { for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto& x = buf_access(s.sub_pdlp->pdhg_solver_); + auto& x = buf_access(s); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; if (s.var_send_indices_d[peer].size() == 0) continue; @@ -162,7 +150,7 @@ struct multi_gpu_engine_t { auto& s = *shards[r]; auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& x = buf_access(s.sub_pdlp->pdhg_solver_); + auto& x = buf_access(s); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; @@ -179,16 +167,27 @@ struct multi_gpu_engine_t { // -------- Halo exchange (constraints / y) ------------------------------- // Same as halo_exchange_var but for a constraint-shaped buffer (the input - // A_T @ y will read) and constraint halos. + // A_T @ y will read) and constraint halos. buf_access maps a + // pdhg_solver_t to the cstr-shaped buffer to exchange. template void halo_exchange_cstr(BufAccess&& buf_access) + { + halo_exchange_cstr_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { + return buf_access(s.sub_pdlp->pdhg_solver_); + }); + } + + // Same as halo_exchange_var_shard for cstr + template + void halo_exchange_cstr_shard(ShardBufAccess&& buf_access) { const int nb = static_cast(shards.size()); + // Gather each owner's owned values that peers need. for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto& y = buf_access(s.sub_pdlp->pdhg_solver_); + auto& y = buf_access(s); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; if (s.cstr_send_indices_d[peer].size() == 0) continue; @@ -218,7 +217,7 @@ struct multi_gpu_engine_t { auto& s = *shards[r]; auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& y = buf_access(s.sub_pdlp->pdhg_solver_); + auto& y = buf_access(s); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; @@ -234,60 +233,13 @@ struct multi_gpu_engine_t { } // -------- Broadcast owned constraint (row) scaling into halo ------------ + // The cumulative constraint-matrix (row) scaling is computed only on owned + // rows; push each owner's values into the peers' halo copies. void broadcast_constraint_scaling_to_halo() { - const int nb = static_cast(shards.size()); - auto buf_access = [](pdlp_shard_t& s) -> rmm::device_uvector& { + halo_exchange_cstr_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); - }; - - // Gather each owner's owned scaling values that peers need. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto& y = buf_access(s); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - if (s.cstr_send_indices_d[peer].size() == 0) continue; - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.cstr_send_indices_d[peer].begin(), - s.cstr_send_indices_d[peer].end(), - y.begin(), - s.cstr_send_buf_d[peer].begin()); - } - } - - ncclGroupStart(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - ncclSend(s.cstr_send_buf_d[peer].data(), - s.cstr_send_buf_d[peer].size(), - ncclFloat64, - peer, - s.comm.get(), - s.stream.view().value()); - } - } - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - auto& rd = s.rank_data; - raft::device_setter guard(s.device_id); - auto& y = buf_access(s); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; - ncclRecv(recv_ptr, - static_cast(rd.cstr_recv_counts[peer]), - ncclFloat64, - peer, - s.comm.get(), - s.stream.view().value()); - } - } - ncclGroupEnd(); + }); } // -------- NCCL allreduce (sum, in place) -------------------------------- @@ -357,71 +309,62 @@ struct multi_gpu_engine_t { { const int nb = static_cast(shards.size()); - std::vector> bound_sq; - std::vector> obj_sq; - bound_sq.reserve(nb); - obj_sq.reserve(nb); + // Per-shard packed partial squared norms: [0] = bound (rhs) sq, [1] = obj sq. + std::vector> sq; + sq.reserve(nb); - // 1) per-shard partial squared norms over OWNED entries only (halo rhs is - // +/-inf and would otherwise double-count owned entries shared as halo). + // 1) per-shard partial squared norms over OWNED entries only for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - bound_sq.emplace_back(1, s.stream.view()); - obj_sq.emplace_back(1, s.stream.view()); + sq.emplace_back(2, s.stream.view()); const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); const int n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); const int n_owned_var = static_cast(s.rank_data.owned_var_size); - auto bound_in = thrust::make_transform_iterator( - thrust::make_zip_iterator(scaled.constraint_lower_bounds.data(), - scaled.constraint_upper_bounds.data()), - mgpu_rhs_sq_op_t{}); - size_t tmp_bytes_b = 0; - cub::DeviceReduce::Sum( - nullptr, tmp_bytes_b, bound_in, bound_sq[r].data(), n_owned_cstr, s.stream.view().value()); - rmm::device_buffer scratch_b(tmp_bytes_b, s.stream.view()); - cub::DeviceReduce::Sum(scratch_b.data(), - tmp_bytes_b, - bound_in, - bound_sq[r].data(), - n_owned_cstr, - s.stream.view().value()); - - auto obj_in = thrust::make_transform_iterator(scaled.objective_coefficients.data(), - mgpu_weighted_sq_op_t{c_scaling_weight}); - size_t tmp_bytes_o = 0; - cub::DeviceReduce::Sum( - nullptr, tmp_bytes_o, obj_in, obj_sq[r].data(), n_owned_var, s.stream.view().value()); - rmm::device_buffer scratch_o(tmp_bytes_o, s.stream.view()); - cub::DeviceReduce::Sum(scratch_o.data(), - tmp_bytes_o, - obj_in, - obj_sq[r].data(), - n_owned_var, - s.stream.view().value()); + // Squared-norm contribution of each constraint's [lower, upper] bound pair + // (mirrors rhs_sum_of_squares_t). The lower bound is the reduce input; the + // matching upper bound is fetched by index inside the op. + const f_t* upper = scaled.constraint_upper_bounds.data(); + auto bound_op = [upper] __device__(f_t lower, i_t i) { + const f_t u = upper[i]; + f_t sum = f_t(0); + if (isfinite(lower) && (lower != u)) sum += lower * lower; + if (isfinite(u)) sum += u * u; + return sum; + }; + raft::linalg::reduce(sq[r].data() + 0, + scaled.constraint_lower_bounds.data(), + n_owned_cstr, + 1, + f_t(0), + s.stream.view(), + false, + bound_op, + raft::Sum()); + + // Weighted sum of squares of the objective coefficients. + auto obj_op = [c_scaling_weight] __device__(f_t v, i_t) { return v * v * c_scaling_weight; }; + raft::linalg::reduce(sq[r].data() + 1, + scaled.objective_coefficients.data(), + n_owned_var, + 1, + f_t(0), + s.stream.view(), + false, + obj_op, + raft::Sum()); } - // 2) NCCL allreduce SUM -> every shard holds the global squared norms. + // 2) NCCL allreduce SUM (both scalars at once) -> every shard holds the + // global squared norms. ncclGroupStart(); for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - ncclAllReduce(bound_sq[r].data(), - bound_sq[r].data(), - 1, - ncclFloat64, - ncclSum, - s.comm.get(), - s.stream.view().value()); - ncclAllReduce(obj_sq[r].data(), - obj_sq[r].data(), - 1, - ncclFloat64, - ncclSum, - s.comm.get(), - s.stream.view().value()); + ncclAllReduce( + sq[r].data(), sq[r].data(), 2, ncclFloat64, ncclSum, s.comm.get(), s.stream.view().value()); } ncclGroupEnd(); @@ -429,13 +372,11 @@ struct multi_gpu_engine_t { for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - f_t h_bound_sq = f_t(0); - f_t h_obj_sq = f_t(0); - raft::copy(&h_bound_sq, bound_sq[r].data(), 1, s.stream.view()); - raft::copy(&h_obj_sq, obj_sq[r].data(), 1, s.stream.view()); + f_t h_sq[2] = {f_t(0), f_t(0)}; + raft::copy(h_sq, sq[r].data(), 2, s.stream.view()); s.stream.synchronize(); - const f_t bound_rescaling = f_t(1) / (std::sqrt(h_bound_sq) + f_t(1)); - const f_t objective_rescaling = f_t(1) / (std::sqrt(h_obj_sq) + f_t(1)); + const f_t bound_rescaling = f_t(1) / (std::sqrt(h_sq[0]) + f_t(1)); + const f_t objective_rescaling = f_t(1) / (std::sqrt(h_sq[1]) + f_t(1)); s.sub_pdlp->get_initial_scaling_strategy().apply_distributed_bound_objective_rescaling( bound_rescaling, objective_rescaling); } @@ -476,18 +417,8 @@ struct multi_gpu_engine_t { } // -------- High-level: A @ x and A_T @ y --------------------------------- - // Thin wrappers used from pdhg_solver_t::compute_A_x / compute_At_y when an - // engine is wired in. They drive the per-shard plan-based SpMV via the - // canonical cusparse_view bindings (no rebinding) so the descriptor binding - // is never disturbed by mGPU machinery. - // - // The halo-exchange MUST target the exact buffer the canonical descriptor - // is bound to in the PDHG cusparse_view (see cusparse_view.cu lines 516-519 - // and 595-599): - // - cv.reflected_primal_solution -> reflected_primal_ (var-shaped) - // - cv.dual_solution -> current.dual_solution_ (cstr-shaped) - // For 1 shard the halo-exchange is a no-op, but the buffer choice is what - // makes multi-shard correctness work, so we keep it accurate either way. + // Distributed counterpart to pdhg_solver_t::compute_A_x() + // We don't use distributed_spmv_A() because we are using SpMVOp rather than SpMV void distributed_compute_A_x() { halo_exchange_var( @@ -495,6 +426,8 @@ struct multi_gpu_engine_t { for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_A_x(); }); } + // Distributed counterpart to pdhg_solver_t::compute_At_y() + // We don't use distributed_spmv_At() because we are using SpMVOp rather than SpMV void distributed_compute_At_y() { halo_exchange_cstr( @@ -503,34 +436,23 @@ struct multi_gpu_engine_t { } // -------- Distributed Ruiz inf-scaling ----------------------------------- - void alloc_global_var_scratch(i_t n_global_vars, - std::vector>& global_var_buf, - std::vector>& local_to_global_var_d) + std::vector> alloc_global_var_scratch(i_t n_global_vars) { const int nb = static_cast(shards.size()); + std::vector> global_var_buf; global_var_buf.reserve(nb); - local_to_global_var_d.reserve(nb); for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); global_var_buf.emplace_back(static_cast(n_global_vars), s.stream.view()); - local_to_global_var_d.emplace_back(static_cast(s.rank_data.total_var_size), - s.stream.view()); - if (s.rank_data.total_var_size > 0) { - RAFT_CUDA_TRY(cudaMemcpyAsync(local_to_global_var_d.back().data(), - s.rank_data.local_to_global_var.data(), - sizeof(i_t) * s.rank_data.local_to_global_var.size(), - cudaMemcpyHostToDevice, - s.stream.view().value())); - } } + return global_var_buf; } void reduce_iteration_variable_scaling_across_shards( ncclRedOp_t op, i_t n_global_vars, - std::vector>& global_var_buf, - std::vector>& local_to_global_var_d) + std::vector>& global_var_buf) { const int nb = static_cast(shards.size()); @@ -549,7 +471,7 @@ struct multi_gpu_engine_t { thrust::scatter(rmm::exec_policy_nosync(s.stream.view()), iter_var_scaling.begin(), iter_var_scaling.begin() + s.rank_data.total_var_size, - local_to_global_var_d[r].begin(), + s.local_to_global_var_d.begin(), global_var_buf[r].begin()); } } @@ -576,8 +498,8 @@ struct multi_gpu_engine_t { s.sub_pdlp->get_initial_scaling_strategy().get_iteration_variable_scaling(); if (s.rank_data.total_var_size > 0) { thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - local_to_global_var_d[r].begin(), - local_to_global_var_d[r].begin() + s.rank_data.total_var_size, + s.local_to_global_var_d.begin(), + s.local_to_global_var_d.begin() + s.rank_data.total_var_size, global_var_buf[r].begin(), iter_var_scaling.begin()); } @@ -589,9 +511,7 @@ struct multi_gpu_engine_t { if (num_iter <= 0 || n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_ruiz_inf_scaling"); - std::vector> global_var_buf; - std::vector> local_to_global_var_d; - alloc_global_var_scratch(n_global_vars, global_var_buf, local_to_global_var_d); + auto global_var_buf = alloc_global_var_scratch(n_global_vars); for (int it = 0; it < num_iter; ++it) { // 1) per-shard local kernel: writes iteration_variable_scaling (per-column @@ -601,8 +521,7 @@ struct multi_gpu_engine_t { }); // 2) cross-shard column inf-norm reduction (MAX). - reduce_iteration_variable_scaling_across_shards( - ncclMax, n_global_vars, global_var_buf, local_to_global_var_d); + reduce_iteration_variable_scaling_across_shards(ncclMax, n_global_vars, global_var_buf); // 3) per-shard fold into cumulative + reset iter vectors. for_each_shard([](auto& shard) { @@ -617,17 +536,17 @@ struct multi_gpu_engine_t { // Distributed Pock-Chambolle: one pass, mirroring single-GPU // pock_chambolle_scaling but with the per-column sum-of-powers reduced across - // shards (SUM) between the local kernels and the cumulative fold. Rows are - // owned exclusively, so the row half stays local. Runs after the distributed - // Ruiz pass, matching the single-GPU order (Ruiz then Pock-Chambolle). + // shards (SUM) between the local kernels and the cumulative fold. Each shard + // stores its owned rows complete, so the row half is computed locally (then + // broadcast to halo copies); only the column half is split across shards and + // needs the reduction. Runs after the distributed Ruiz pass, matching the + // single-GPU order (Ruiz then Pock-Chambolle). void distributed_pock_chambolle_scaling(f_t alpha, i_t n_global_vars) { if (n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); - std::vector> global_var_buf; - std::vector> local_to_global_var_d; - alloc_global_var_scratch(n_global_vars, global_var_buf, local_to_global_var_d); + auto global_var_buf = alloc_global_var_scratch(n_global_vars); // 1) per-shard local kernels: row sum (complete) + column sum (partial). for_each_shard([alpha](auto& shard) { @@ -636,8 +555,7 @@ struct multi_gpu_engine_t { }); // 2) cross-shard column sum-of-powers reduction (SUM). - reduce_iteration_variable_scaling_across_shards( - ncclSum, n_global_vars, global_var_buf, local_to_global_var_d); + reduce_iteration_variable_scaling_across_shards(ncclSum, n_global_vars, global_var_buf); // 3) per-shard fold into cumulative (cumulative /= sqrt(iteration)). for_each_shard([](auto& shard) { diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 44ca0c88f4..ef05a73993 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -32,6 +32,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, handle(stream.view()), comm(raw_comm, nccl_comm_deleter_t{device_id}), rank_data(std::move(rd)), + local_to_global_var_d(static_cast(rank_data.total_var_size), stream.view()), opt_problem(std::nullopt), sub_problem(std::nullopt), sub_pdlp(nullptr) @@ -224,6 +225,15 @@ pdlp_shard_t::pdlp_shard_t(int device_id, build_send_plan(rank_data.var_send_per_peer, var_send_indices_d, var_send_buf_d); build_send_plan(rank_data.cstr_send_per_peer, cstr_send_indices_d, cstr_send_buf_d); + // ---- 7. Upload the immutable local->global variable map once. ---- + // Reused by the engine's distributed scaling scatter/gather. + if (rank_data.total_var_size > 0) { + raft::copy(local_to_global_var_d.data(), + rank_data.local_to_global_var.data(), + rank_data.local_to_global_var.size(), + stream_view); + } + handle.sync_stream(stream_view); } diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index f96c014f77..c02b84479e 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -66,6 +66,12 @@ struct pdlp_shard_t { raft::handle_t handle; nccl_comm_unique_ptr_t comm; rank_data_t rank_data; + + // Persistent device copy of rank_data.local_to_global_var (immutable after + // partitioning), sized total_var_size. Uploaded once at construction and + // reused by the engine's distributed scaling scatter/gather, so that map does + // not have to be re-uploaded on every scaling pass. + rmm::device_uvector local_to_global_var_d; std::optional> opt_problem; std::optional> sub_problem; std::unique_ptr> sub_pdlp; From fb3692df6162d15e62afd899dac9a1a232ad97fd Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 19:06:11 +0200 Subject: [PATCH 097/258] new scaling --- .../distributed_pdlp/multi_gpu_engine.hpp | 138 ++++++------------ cpp/src/pdlp/distributed_pdlp/shard.cu | 10 -- cpp/src/pdlp/distributed_pdlp/shard.hpp | 6 - .../initial_scaling.cu | 63 +++++++- .../initial_scaling.cuh | 6 + cpp/src/pdlp/pdlp.cu | 8 +- 6 files changed, 111 insertions(+), 120 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 8eabb7d7f3..0e4d78a691 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -242,6 +242,17 @@ struct multi_gpu_engine_t { }); } + // -------- Broadcast owned variable (column) scaling into halo ----------- + // The cumulative variable (column) scaling is computed only on owned columns; + // push each owner's values into the peers' halo copies so the next scaling + // iteration's row / column inf-norm kernels read correct factors on halo. + void broadcast_variable_scaling_to_halo() + { + halo_exchange_var_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + }); + } + // -------- NCCL allreduce (sum, in place) -------------------------------- // Per-shard in-place sum-allreduce. Each shard's stream issues an // ncclAllReduce(buf, buf, count, ncclFloat64, ncclSum, ...) inside a single @@ -436,132 +447,77 @@ struct multi_gpu_engine_t { } // -------- Distributed Ruiz inf-scaling ----------------------------------- - std::vector> alloc_global_var_scratch(i_t n_global_vars) - { - const int nb = static_cast(shards.size()); - std::vector> global_var_buf; - global_var_buf.reserve(nb); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - global_var_buf.emplace_back(static_cast(n_global_vars), s.stream.view()); - } - return global_var_buf; - } - - void reduce_iteration_variable_scaling_across_shards( - ncclRedOp_t op, - i_t n_global_vars, - std::vector>& global_var_buf) - { - const int nb = static_cast(shards.size()); - - // Zero global buffers, then scatter each shard's local values into their - // global column indices. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - RAFT_CUDA_TRY(cudaMemsetAsync(global_var_buf[r].data(), - 0, - sizeof(f_t) * static_cast(n_global_vars), - s.stream.view().value())); - auto& iter_var_scaling = - s.sub_pdlp->get_initial_scaling_strategy().get_iteration_variable_scaling(); - if (s.rank_data.total_var_size > 0) { - thrust::scatter(rmm::exec_policy_nosync(s.stream.view()), - iter_var_scaling.begin(), - iter_var_scaling.begin() + s.rank_data.total_var_size, - s.local_to_global_var_d.begin(), - global_var_buf[r].begin()); - } - } - - ncclGroupStart(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - ncclAllReduce(global_var_buf[r].data(), - global_var_buf[r].data(), - static_cast(n_global_vars), - ncclFloat64, - op, - s.comm.get(), - s.stream.view().value()); - } - ncclGroupEnd(); - - // Gather the global per-column value back into each shard's local iter vector. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto& iter_var_scaling = - s.sub_pdlp->get_initial_scaling_strategy().get_iteration_variable_scaling(); - if (s.rank_data.total_var_size > 0) { - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.local_to_global_var_d.begin(), - s.local_to_global_var_d.begin() + s.rank_data.total_var_size, - global_var_buf[r].begin(), - iter_var_scaling.begin()); - } - } - } - + // Each shard owns its rows AND its columns and stores both complete (h_A = + // owned rows, h_A_t = owned columns), exactly like the SpMV path. So every + // owned row's inf-norm (from the row-major matrix) and every owned column's + // inf-norm (from the transpose A_T) is computed locally and completely -- no + // cross-shard reduction. The only cross-shard step is a halo broadcast of + // both cumulative scalings before each iteration, so the row kernel sees + // correct column factors on its halo columns and the column kernel sees + // correct row factors on its halo rows (the same forward halo exchange SpMV + // uses, just carrying a scaling factor instead of x / y). void distributed_ruiz_inf_scaling(int num_iter, i_t n_global_vars) { if (num_iter <= 0 || n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_ruiz_inf_scaling"); - auto global_var_buf = alloc_global_var_scratch(n_global_vars); - for (int it = 0; it < num_iter; ++it) { - // 1) per-shard local kernel: writes iteration_variable_scaling (per-column - // inf-norm partial) and iteration_constraint_matrix_scaling (row, complete). + // Refresh halo copies of both cumulative scalings (owner -> halo) so the + // per-shard kernels read correct opposite-axis factors on their halo. + broadcast_variable_scaling_to_halo(); + broadcast_constraint_scaling_to_halo(); + + // Per-shard local kernels: row inf-norm (owned rows, complete) + column + // inf-norm from A_T (owned columns, complete; halo columns -> 0). for_each_shard([](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_compute_local_iteration_vectors(); }); - // 2) cross-shard column inf-norm reduction (MAX). - reduce_iteration_variable_scaling_across_shards(ncclMax, n_global_vars, global_var_buf); - - // 3) per-shard fold into cumulative + reset iter vectors. + // Fold into cumulative on owned entries (halo entries get refreshed by + // the next iteration's broadcast). for_each_shard([](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_apply_cumulative_update(); }); } - // Make sure per-shard cumulative writes are observable on subsequent - // calls (e.g., the next distributed_max_singular_value). + // Final refresh so downstream consumers (the scaled problem, the next + // distributed_max_singular_value, etc.) see correct halo factors. + broadcast_variable_scaling_to_halo(); + broadcast_constraint_scaling_to_halo(); + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } // Distributed Pock-Chambolle: one pass, mirroring single-GPU - // pock_chambolle_scaling but with the per-column sum-of-powers reduced across - // shards (SUM) between the local kernels and the cumulative fold. Each shard - // stores its owned rows complete, so the row half is computed locally (then - // broadcast to halo copies); only the column half is split across shards and - // needs the reduction. Runs after the distributed Ruiz pass, matching the + // pock_chambolle_scaling. Row sum-of-powers come from the row-major matrix + // (owned rows, complete) and column sum-of-powers from A_T (owned columns, + // complete) -- no cross-shard reduction, only the halo broadcast of both + // cumulative scalings. Runs after the distributed Ruiz pass, matching the // single-GPU order (Ruiz then Pock-Chambolle). void distributed_pock_chambolle_scaling(f_t alpha, i_t n_global_vars) { if (n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); - auto global_var_buf = alloc_global_var_scratch(n_global_vars); + // Refresh halo copies of both cumulative scalings so the row kernel sees + // correct column factors on halo columns and the A_T column kernel sees + // correct row factors on halo rows. + broadcast_variable_scaling_to_halo(); + broadcast_constraint_scaling_to_halo(); - // 1) per-shard local kernels: row sum (complete) + column sum (partial). for_each_shard([alpha](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_compute_local_iteration_vectors( alpha); }); - // 2) cross-shard column sum-of-powers reduction (SUM). - reduce_iteration_variable_scaling_across_shards(ncclSum, n_global_vars, global_var_buf); - - // 3) per-shard fold into cumulative (cumulative /= sqrt(iteration)). for_each_shard([](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_apply_cumulative_update(); }); + // Final refresh for downstream consumers. + broadcast_variable_scaling_to_halo(); + broadcast_constraint_scaling_to_halo(); + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index ef05a73993..44ca0c88f4 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -32,7 +32,6 @@ pdlp_shard_t::pdlp_shard_t(int device_id, handle(stream.view()), comm(raw_comm, nccl_comm_deleter_t{device_id}), rank_data(std::move(rd)), - local_to_global_var_d(static_cast(rank_data.total_var_size), stream.view()), opt_problem(std::nullopt), sub_problem(std::nullopt), sub_pdlp(nullptr) @@ -225,15 +224,6 @@ pdlp_shard_t::pdlp_shard_t(int device_id, build_send_plan(rank_data.var_send_per_peer, var_send_indices_d, var_send_buf_d); build_send_plan(rank_data.cstr_send_per_peer, cstr_send_indices_d, cstr_send_buf_d); - // ---- 7. Upload the immutable local->global variable map once. ---- - // Reused by the engine's distributed scaling scatter/gather. - if (rank_data.total_var_size > 0) { - raft::copy(local_to_global_var_d.data(), - rank_data.local_to_global_var.data(), - rank_data.local_to_global_var.size(), - stream_view); - } - handle.sync_stream(stream_view); } diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index c02b84479e..f96c014f77 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -66,12 +66,6 @@ struct pdlp_shard_t { raft::handle_t handle; nccl_comm_unique_ptr_t comm; rank_data_t rank_data; - - // Persistent device copy of rank_data.local_to_global_var (immutable after - // partitioning), sized total_var_size. Uploaded once at construction and - // reused by the engine's distributed scaling scatter/gather, so that map does - // not have to be re-uploaded on every scaling pass. - rmm::device_uvector local_to_global_var_d; std::optional> opt_problem; std::optional> sub_problem; std::unique_ptr> sub_pdlp; diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index cb498b3756..2ca7a5fc23 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -187,8 +187,10 @@ void pdlp_initial_scaling_strategy_t::bound_objective_rescaling() h_objective_rescaling_ = cuopt::host_copy(objective_rescaling_, stream_view_); } +// Row inf-norm of the scaled matrix, over the row-major matrix: each row is +// reduced from its own nonzeros. (Owns the complete row in distributed PDLP.) template -__global__ void inf_norm_row_and_col_kernel( +__global__ void inf_norm_row_kernel( const typename problem_t::view_t op_problem, typename pdlp_initial_scaling_strategy_t::view_t initial_scaling_view) { @@ -202,14 +204,37 @@ __global__ void inf_norm_row_and_col_kernel( f_t scaled_val = (op_problem.coefficients[row_offset + j] * constraint_scale_factor) * variable_scale_factor; f_t abs_val = raft::abs(scaled_val); - - // row part if (abs_val > initial_scaling_view.iteration_constraint_matrix_scaling[row]) { raft::myAtomicMax(&initial_scaling_view.iteration_constraint_matrix_scaling[row], abs_val); } + } + } +} - // col part - // Add max with abs val in objective_matrix here for QP for cols +// Column inf-norm of the scaled matrix, over the TRANSPOSE A_T: each column is +// reduced from its own nonzeros. Computing it from A_T (instead of the +// row-major matrix) makes every OWNED column complete in distributed PDLP +// without any cross-shard reduction (halo columns have no A_T rows -> 0), +// mirroring pock_chambolle_scaling_kernel_col. The scaled value uses the same +// operands and order as the row kernel, so results are identical to the fused +// single-GPU computation (max is exact in floating point). +template +__global__ void inf_norm_col_kernel( + const typename problem_t::view_t op_problem, + typename pdlp_initial_scaling_strategy_t::view_t initial_scaling_view, + const f_t* A_T, + const i_t* A_T_offsets, + const i_t* A_T_indices) +{ + for (int col = blockIdx.x; col < op_problem.n_variables; col += gridDim.x) { + i_t col_offset = A_T_offsets[col]; + i_t nnz_in_col = A_T_offsets[col + 1] - col_offset; + f_t variable_scale_factor = initial_scaling_view.cummulative_variable_scaling[col]; + for (int j = threadIdx.x; j < nnz_in_col; j += blockDim.x) { + i_t row = A_T_indices[col_offset + j]; + f_t constraint_scale_factor = initial_scaling_view.cummulative_constraint_matrix_scaling[row]; + f_t scaled_val = (A_T[col_offset + j] * constraint_scale_factor) * variable_scale_factor; + f_t abs_val = raft::abs(scaled_val); if (abs_val > initial_scaling_view.iteration_variable_scaling[col]) { raft::myAtomicMax(&initial_scaling_view.iteration_variable_scaling[col], abs_val); } @@ -220,14 +245,29 @@ __global__ void inf_norm_row_and_col_kernel( template void pdlp_initial_scaling_strategy_t::ruiz_iter_compute_local_iteration_vectors() { - // find inf norm over rows and columns of the scaled matrix in given iteration + // find inf norm over rows and columns of the scaled matrix in given iteration. + // Rows are reduced from the row-major matrix and columns from the transpose + // A_T (both kernels atomicMax into zero-initialized iteration vectors). + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); + i_t number_of_blocks = op_problem_scaled_.n_constraints / block_size; if (op_problem_scaled_.n_constraints % block_size) number_of_blocks++; i_t number_of_threads = std::min(op_problem_scaled_.n_variables, (i_t)block_size); - inf_norm_row_and_col_kernel<<>>( + inf_norm_row_kernel<<>>( op_problem_scaled_.view(), this->view()); RAFT_CUDA_TRY(cudaPeekAtLastError()); + inf_norm_col_kernel<<>>( + op_problem_scaled_.view(), + this->view(), + A_T_.data(), + A_T_offsets_.data(), + A_T_indices_.data()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + if (running_mip_) { reset_integer_variables(); } } @@ -1080,10 +1120,17 @@ pdlp_initial_scaling_strategy_t::view() #define INSTANTIATE(F_TYPE) \ template class pdlp_initial_scaling_strategy_t; \ \ - template __global__ void inf_norm_row_and_col_kernel( \ + template __global__ void inf_norm_row_kernel( \ const typename problem_t::view_t op_problem, \ typename pdlp_initial_scaling_strategy_t::view_t initial_scaling_view); \ \ + template __global__ void inf_norm_col_kernel( \ + const typename problem_t::view_t op_problem, \ + typename pdlp_initial_scaling_strategy_t::view_t initial_scaling_view, \ + const F_TYPE* A_T, \ + const int* A_T_offsets, \ + const int* A_T_indices); \ + \ template __global__ void pock_chambolle_scaling_kernel_col( \ const typename problem_t::view_t op_problem, \ F_TYPE alpha, \ diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 13f639079d..24e2ab2a49 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -80,6 +80,12 @@ class pdlp_initial_scaling_strategy_t { { return cummulative_constraint_matrix_scaling_; } + // Mutable access needed by distributed PDLP to broadcast owned variable + // (column) scaling into the halo copies between scaling iterations. + rmm::device_uvector& get_cummulative_variable_scaling() + { + return cummulative_variable_scaling_; + } const rmm::device_uvector& get_variable_scaling_vector() const; const problem_t& get_scaled_op_problem(); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 0105087ca5..287693ab12 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -566,19 +566,17 @@ pdlp_solver_t::pdlp_solver_t( shard->stream.synchronize(); } - // Distributed scaling + // Distributed scaling. Each pass keeps the halo copies of both cumulative + // scalings refreshed internally (owner -> halo broadcast), so no extra halo + // push is needed here. if (settings_.hyper_params.do_ruiz_scaling) { multi_gpu_engine->distributed_ruiz_inf_scaling( settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); } - // push local scaling to halo - multi_gpu_engine->broadcast_constraint_scaling_to_halo(); if (settings_.hyper_params.do_pock_chambolle_scaling) { multi_gpu_engine->distributed_pock_chambolle_scaling( static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), n_vars); } - // Refresh the halo constraint scaling after Pock-Chambolle - multi_gpu_engine->broadcast_constraint_scaling_to_halo(); for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); From 6ebdcb9444ae46a482899fc755a2f197e0de72c1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 19:32:09 +0200 Subject: [PATCH 098/258] disable graph in distributed tests --- cpp/tests/linear_programming/pdlp_test.cu | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 3728aba81c..f879f962c8 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -190,7 +190,10 @@ void expect_distributed_matches_base(raft::handle_t const& handle, pdlp_solver_settings_t dist_settings = base_settings; dist_settings.hyper_params.use_distributed_pdlp = true; dist_settings.distributed_pdlp_num_gpus = -1; - auto dist = solve_lp(&handle, problem, dist_settings); + // Disable CUDA-graph capture for the distributed run (execute each iteration + // eagerly); KaMinPar graph partitioning is left enabled. + dist_settings.hyper_params.pdlp_disable_graph = true; + auto dist = solve_lp(&handle, problem, dist_settings); // ----- termination status ----- ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) From d65fca5193128c931b49b6dba08c5e81bc88c1e1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 20:19:05 +0200 Subject: [PATCH 099/258] moved function into distributed_algorithms.hpp --- cpp/src/pdlp/CMakeLists.txt | 1 + .../distributed_algorithms.cu | 531 ++++++++++++++++++ .../distributed_algorithms.hpp | 44 ++ .../distributed_pdlp/multi_gpu_engine.hpp | 509 ----------------- cpp/src/pdlp/pdlp.cu | 16 +- 5 files changed, 586 insertions(+), 515 deletions(-) create mode 100644 cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu create mode 100644 cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index a20244bd26..97b5d42b1b 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -31,6 +31,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/ping_pong_graph.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/shard.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/multi_gpu_engine.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/distributed_algorithms.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/kaminpar_partitioner.cpp diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu new file mode 100644 index 0000000000..22170b6cb9 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -0,0 +1,531 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +// The bodies below call shard.sub_pdlp->get_initial_scaling_strategy() and +// shard.sub_pdlp->pdhg_solver_.spmv_* — pdlp_solver_t / pdhg_solver_t must be +// complete at the explicit instantiation point below. +#include + +#include + +#include + +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +// -------- Distributed bound / objective rescaling ------------------------- +template +void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, + f_t c_scaling_weight) +{ + const int nb = static_cast(engine.shards.size()); + + // Per-shard packed partial squared norms: [0] = bound (rhs) sq, [1] = obj sq. + std::vector> sq; + sq.reserve(nb); + + // 1) per-shard partial squared norms over owned entries only + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + sq.emplace_back(2, s.stream.view()); + + const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); + const int n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); + const int n_owned_var = static_cast(s.rank_data.owned_var_size); + + // Squared-norm contribution of each constraint's [lower, upper] bound pair + // (mirrors rhs_sum_of_squares_t). The lower bound is the reduce input; the + // matching upper bound is fetched by index inside the op. + const f_t* upper = scaled.constraint_upper_bounds.data(); + auto bound_op = [upper] __device__(f_t lower, i_t i) { + const f_t u = upper[i]; + f_t sum = f_t(0); + if (isfinite(lower) && (lower != u)) sum += lower * lower; + if (isfinite(u)) sum += u * u; + return sum; + }; + raft::linalg::reduce(sq[r].data() + 0, + scaled.constraint_lower_bounds.data(), + n_owned_cstr, + 1, + f_t(0), + s.stream.view(), + false, + bound_op, + raft::Sum()); + + // Weighted sum of squares of the objective coefficients. + auto obj_op = [c_scaling_weight] __device__(f_t v, i_t) { return v * v * c_scaling_weight; }; + raft::linalg::reduce(sq[r].data() + 1, + scaled.objective_coefficients.data(), + n_owned_var, + 1, + f_t(0), + s.stream.view(), + false, + obj_op, + raft::Sum()); + } + + // 2) NCCL allreduce SUM (both scalars at once) -> every shard holds the + // global squared norms. + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + ncclAllReduce( + sq[r].data(), sq[r].data(), 2, ncclFloat64, ncclSum, s.comm.get(), s.stream.view().value()); + } + ncclGroupEnd(); + + // 3) derive the identical scalars and apply on every shard. + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + f_t h_sq[2] = {f_t(0), f_t(0)}; + raft::copy(h_sq, sq[r].data(), 2, s.stream.view()); + s.stream.synchronize(); + const f_t bound_rescaling = f_t(1) / (std::sqrt(h_sq[0]) + f_t(1)); + const f_t objective_rescaling = f_t(1) / (std::sqrt(h_sq[1]) + f_t(1)); + s.sub_pdlp->get_initial_scaling_strategy().apply_distributed_bound_objective_rescaling( + bound_rescaling, objective_rescaling); + } + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + s.stream.synchronize(); + } +} + +// -------- Distributed Ruiz inf-scaling ------------------------------------ +// Each shard owns its rows AND its columns and stores both complete (h_A = +// owned rows, h_A_t = owned columns) +template +void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, + int num_iter, + i_t n_global_vars) +{ + if (num_iter <= 0 || n_global_vars <= 0) return; + raft::common::nvtx::range scope("distributed_ruiz_inf_scaling"); + + for (int it = 0; it < num_iter; ++it) { + // Refresh halo copies of both cumulative scalings (owner -> halo) so the + // per-shard kernels read correct opposite-axis factors on their halo. + engine.broadcast_variable_scaling_to_halo(); + engine.broadcast_constraint_scaling_to_halo(); + + // Per-shard local kernels: row inf-norm (owned rows, complete) + column + // inf-norm from A_T (owned columns, complete; halo columns -> 0). + engine.for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_compute_local_iteration_vectors(); + }); + + // Fold into cumulative on owned entries (halo entries get refreshed by + // the next iteration's broadcast). + engine.for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_apply_cumulative_update(); + }); + } + + // Final refresh so downstream consumers (the scaled problem, the next + // distributed_max_singular_value, etc.) see correct halo factors. + engine.broadcast_variable_scaling_to_halo(); + engine.broadcast_constraint_scaling_to_halo(); + + engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); +} + +// -------- Distributed Pock-Chambolle scaling ------------------------------ +// Distributed Pock-Chambolle: one pass, mirroring single-GPU +// pock_chambolle_scaling. Row sum-of-powers come from the row-major matrix +// (owned rows) and column sum-of-powers from A_T (owned columns). +template +void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, + f_t alpha, + i_t n_global_vars) +{ + if (n_global_vars <= 0) return; + raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); + + // Refresh halo copies of both cumulative scalings + engine.broadcast_variable_scaling_to_halo(); + engine.broadcast_constraint_scaling_to_halo(); + + engine.for_each_shard([alpha](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_compute_local_iteration_vectors( + alpha); + }); + + engine.for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_apply_cumulative_update(); + }); + + // Final refresh for downstream consumers. + engine.broadcast_variable_scaling_to_halo(); + engine.broadcast_constraint_scaling_to_halo(); + + engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); +} + +// -------- Distributed sigma_max(A) via power iteration -------------------- +// The function has to re-implement the multi_gpu_engine_t preimitives as the scratch buffers +// are not associated with shards. +template +f_t distributed_max_singular_value(multi_gpu_engine_t& engine, + i_t n_global_cstrs, + int max_iterations, + f_t tolerance) +{ + raft::common::nvtx::range scope("distributed_max_singular_value"); + + const int nb = static_cast(engine.shards.size()); + + // Generate the GLOBAL z[] sequence in cstr-index order from a fresh + // mt19937(1), once per call. It's m doubles regardless of N (cheap). + // Each shard then keeps only z[global_idx_for_owned_local_i]. + std::vector h_global_z(static_cast(n_global_cstrs)); + { + std::mt19937 gen(1); + std::normal_distribution dist(f_t(0.0), f_t(1.0)); + for (i_t i = 0; i < n_global_cstrs; ++i) { + h_global_z[i] = dist(gen); + } + } + + // Per-shard scratch lives on each shard's device. We use total (owned + + // halo) sizes for q/z/atq because they're SpMV inputs that need halo + // space. Norms / dot are scalars. + // We use size-1 rmm::device_uvector instead of rmm::device_scalar for the + // per-shard scratch scalars: nvcc + libcudacxx fail the + // copy_constructible concept check when device_scalar appears in a + // std::vector (the check transitively touches rmm::cuda_stream, which is + // non-copyable). device_uvector avoids that path. + std::vector> q; + std::vector> z; + std::vector> atq; + std::vector> sigma_sq; + std::vector> norm_q; + std::vector> residual_norm; + // RAII descriptors: created below per shard, destroyed automatically when + // the vectors go out of scope (no manual cusparseDestroyDnVec needed). + std::vector> z_dn(nb); + std::vector> atq_dn(nb); + q.reserve(nb); + z.reserve(nb); + atq.reserve(nb); + sigma_sq.reserve(nb); + norm_q.reserve(nb); + residual_norm.reserve(nb); + + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + const i_t cstr_total = s.rank_data.total_cstr_size; + const i_t var_total = s.rank_data.total_var_size; + q.emplace_back(static_cast(cstr_total), s.stream.view()); + z.emplace_back(static_cast(cstr_total), s.stream.view()); + atq.emplace_back(static_cast(var_total), s.stream.view()); + sigma_sq.emplace_back(std::size_t{1}, s.stream.view()); + norm_q.emplace_back(std::size_t{1}, s.stream.view()); + residual_norm.emplace_back(std::size_t{1}, s.stream.view()); + z_dn[r].create(static_cast(cstr_total), z.back().data()); + atq_dn[r].create(static_cast(var_total), atq.back().data()); + + std::vector h_owned_z(static_cast(s.rank_data.owned_cstr_size)); + for (i_t i = 0; i < s.rank_data.owned_cstr_size; ++i) { + const i_t g = s.rank_data.local_to_global_cstr[i]; + h_owned_z[i] = h_global_z[g]; + } + if (s.rank_data.owned_cstr_size > 0) { + raft::copy(z.back().data(), + h_owned_z.data(), + static_cast(s.rank_data.owned_cstr_size), + s.stream.view()); + } + if (cstr_total > s.rank_data.owned_cstr_size) { + thrust::fill(rmm::exec_policy_nosync(s.stream.view()), + z.back().data() + s.rank_data.owned_cstr_size, + z.back().data() + cstr_total, + f_t(0)); + } + // Sync to ensure h_owned_z stays valid through the H2D copy (it goes + // out of scope at end of this iteration of the per-shard loop). + s.stream.synchronize(); + } + + // Local halo-exchange helpers that work directly on per-shard external + // buffers (the engine's halo_exchange_var/cstr expect accessors that + // resolve through pdhg_solver_t, which doesn't see our scratch). + auto halo_exchange_cstr_bufs = [&](std::vector>& bufs) { + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + auto& y = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (s.cstr_send_indices_d[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + s.cstr_send_indices_d[peer].begin(), + s.cstr_send_indices_d[peer].end(), + y.begin(), + s.cstr_send_buf_d[peer].begin()); + } + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + ncclSend(s.cstr_send_buf_d[peer].data(), + s.cstr_send_buf_d[peer].size(), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + auto& rd = s.rank_data; + raft::device_setter guard(s.device_id); + auto& y = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; + ncclRecv(recv_ptr, + static_cast(rd.cstr_recv_counts[peer]), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + ncclGroupEnd(); + }; + auto halo_exchange_var_bufs = [&](std::vector>& bufs) { + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + auto& x = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (s.var_send_indices_d[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + s.var_send_indices_d[peer].begin(), + s.var_send_indices_d[peer].end(), + x.begin(), + s.var_send_buf_d[peer].begin()); + } + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + ncclSend(s.var_send_buf_d[peer].data(), + s.var_send_buf_d[peer].size(), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + auto& rd = s.rank_data; + raft::device_setter guard(s.device_id); + auto& x = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; + ncclRecv(recv_ptr, + static_cast(rd.var_recv_counts[peer]), + ncclFloat64, + peer, + s.comm.get(), + s.stream.view().value()); + } + } + ncclGroupEnd(); + }; + + // Per-shard partial reductions over the OWNED cstr slice + NCCL allreduce. + // For norm: out := sqrt(Σ_r ||bufs[r][0:owned_cstr]||²). + // For dot : out := Σ_r . + auto distributed_norm_owned_cstr = [&](std::vector>& bufs, + std::vector>& out) { + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), + static_cast(n_owned), + bufs[r].data(), + 1, + bufs[r].data(), + 1, + out[r].data(), + s.stream.view().value())); + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + ncclAllReduce(out[r].data(), + out[r].data(), + 1, + ncclFloat64, + ncclSum, + s.comm.get(), + s.stream.view().value()); + } + ncclGroupEnd(); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + cub::DeviceTransform::Transform( + out[r].data(), out[r].data(), 1, sqrt_inplace_op_t{}, s.stream.view().value()); + } + }; + auto distributed_dot_owned_cstr = [&](std::vector>& a, + std::vector>& b, + std::vector>& out) { + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), + static_cast(n_owned), + a[r].data(), + 1, + b[r].data(), + 1, + out[r].data(), + s.stream.view().value())); + } + ncclGroupStart(); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + ncclAllReduce(out[r].data(), + out[r].data(), + 1, + ncclFloat64, + ncclSum, + s.comm.get(), + s.stream.view().value()); + } + ncclGroupEnd(); + }; + + // ===== Power iteration ===== + // Mirrors single-GPU compute_initial_step_size: z is the carried iterate + // (A Aᵀ q each step); at the top of each iteration q := z then q is + // normalized; the residual z − σ²q is written back into q only to drive + // the convergence check (next iteration's q := z discards it). + for (int it = 0; it < max_iterations; ++it) { + // q := z on the owned slice (the carried iterate), then normalize. + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + raft::copy(q[r].data(), z[r].data(), n_owned, s.stream.view()); + } + + // ||q||₂ over the global OWNED cstr slice (one allreduce-sum + sqrt). + distributed_norm_owned_cstr(q, norm_q); + + // q /= ||q||₂ on owned slice (halo gets refreshed by next exchange). + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + cub::DeviceTransform::Transform( + q[r].data(), + q[r].data(), + n_owned, + [n = norm_q[r].data()] __device__(f_t v) { return v / *n; }, + s.stream.view().value()); + } + + // atq = A^T q : halo-exchange q, then per-shard SpMV. spmv_At_into + // rebinds the dual_solution dnvec to q[r].data() and restores the + // canonical binding after the call (see pdhg.cu:643-644). + halo_exchange_cstr_bufs(q); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + s.sub_pdlp->pdhg_solver_.spmv_At_into(q[r], atq_dn[r]); + } + + // z = A atq : halo-exchange atq, then per-shard SpMV. + halo_exchange_var_bufs(atq); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + s.sub_pdlp->pdhg_solver_.spmv_A_into(atq[r], z_dn[r]); + } + + // σ² = q · z over the global OWNED cstr slice (= q^T A A^T q = σ_max² + // when q is the dominant left-singular vector). + distributed_dot_owned_cstr(q, z, sigma_sq); + + // q := -σ² q + z (owned slice) — residual of the eigen-equation. + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + raft::device_setter guard(s.device_id); + const i_t n_owned = s.rank_data.owned_cstr_size; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(q[r].data(), z[r].data()), + q[r].data(), + n_owned, + [s2 = sigma_sq[r].data()] __device__(f_t qv, f_t zv) { return -(*s2) * qv + zv; }, + s.stream.view().value()); + } + + // Convergence check via global residual norm. + distributed_norm_owned_cstr(q, residual_norm); + auto& s0 = *engine.shards[0]; + raft::device_setter guard0(s0.device_id); + f_t h_res{}; + raft::copy(&h_res, residual_norm[0].data(), 1, s0.stream.view()); + s0.stream.synchronize(); + if (h_res < tolerance) break; + } + + // σ_max² is the same on every shard after the last allreduce. + auto& s0 = *engine.shards[0]; + raft::device_setter guard0(s0.device_id); + f_t sigma_sq_h{}; + raft::copy(&sigma_sq_h, sigma_sq[0].data(), 1, s0.stream.view()); + s0.stream.synchronize(); + + // z_dn / atq_dn descriptors are released by their RAII wrappers on return. + return std::sqrt(std::max(sigma_sq_h, f_t(0))); +} + +// ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- +template void distributed_bound_objective_rescaling( + multi_gpu_engine_t& engine, double c_scaling_weight); +template void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, + int num_iter, + int n_global_vars); +template void distributed_pock_chambolle_scaling( + multi_gpu_engine_t& engine, double alpha, int n_global_vars); +template double distributed_max_singular_value(multi_gpu_engine_t& engine, + int n_global_cstrs, + int max_iterations, + double tolerance); + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp new file mode 100644 index 0000000000..85948c0af0 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +// Algorithm-level distributed PDLP numerical methods. +namespace cuopt::linear_programming::detail { + +template +struct multi_gpu_engine_t; + +// Global bound/objective rescaling: allreduce the owned partial squared norms +// of the constraint bounds and (weighted) objective, then apply the identical +// scalar on every shard. +template +void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, + f_t c_scaling_weight); + +// Distributed Ruiz inf-scaling (num_iter passes). Each shard computes both its +// owned-row and owned-column inf-norms locally; a per-iteration halo broadcast +// of both cumulative scalings is the only cross-shard communication. +template +void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, + int num_iter, + i_t n_global_vars); + +// Distributed Pock-Chambolle scaling (one pass), mirroring the single-GPU +// pock_chambolle_scaling. +template +void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, + f_t alpha, + i_t n_global_vars); + +// Distributed sigma_max(A) via power iteration (used to seed the initial +// step size). Returns the largest singular value of the scaled constraint +// matrix; identical on every shard. +template +f_t distributed_max_singular_value(multi_gpu_engine_t& engine, + i_t n_global_cstrs, + int max_iterations = 5000, + f_t tolerance = 1e-4); + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 0e4d78a691..50d9291c15 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -315,89 +315,6 @@ struct multi_gpu_engine_t { }); } - // -------- Distributed bound / objective rescaling ----------------------- - void distributed_bound_objective_rescaling(f_t c_scaling_weight) - { - const int nb = static_cast(shards.size()); - - // Per-shard packed partial squared norms: [0] = bound (rhs) sq, [1] = obj sq. - std::vector> sq; - sq.reserve(nb); - - // 1) per-shard partial squared norms over OWNED entries only - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - sq.emplace_back(2, s.stream.view()); - - const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); - const int n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); - const int n_owned_var = static_cast(s.rank_data.owned_var_size); - - // Squared-norm contribution of each constraint's [lower, upper] bound pair - // (mirrors rhs_sum_of_squares_t). The lower bound is the reduce input; the - // matching upper bound is fetched by index inside the op. - const f_t* upper = scaled.constraint_upper_bounds.data(); - auto bound_op = [upper] __device__(f_t lower, i_t i) { - const f_t u = upper[i]; - f_t sum = f_t(0); - if (isfinite(lower) && (lower != u)) sum += lower * lower; - if (isfinite(u)) sum += u * u; - return sum; - }; - raft::linalg::reduce(sq[r].data() + 0, - scaled.constraint_lower_bounds.data(), - n_owned_cstr, - 1, - f_t(0), - s.stream.view(), - false, - bound_op, - raft::Sum()); - - // Weighted sum of squares of the objective coefficients. - auto obj_op = [c_scaling_weight] __device__(f_t v, i_t) { return v * v * c_scaling_weight; }; - raft::linalg::reduce(sq[r].data() + 1, - scaled.objective_coefficients.data(), - n_owned_var, - 1, - f_t(0), - s.stream.view(), - false, - obj_op, - raft::Sum()); - } - - // 2) NCCL allreduce SUM (both scalars at once) -> every shard holds the - // global squared norms. - ncclGroupStart(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - ncclAllReduce( - sq[r].data(), sq[r].data(), 2, ncclFloat64, ncclSum, s.comm.get(), s.stream.view().value()); - } - ncclGroupEnd(); - - // 3) derive the identical scalars and apply on every shard. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - f_t h_sq[2] = {f_t(0), f_t(0)}; - raft::copy(h_sq, sq[r].data(), 2, s.stream.view()); - s.stream.synchronize(); - const f_t bound_rescaling = f_t(1) / (std::sqrt(h_sq[0]) + f_t(1)); - const f_t objective_rescaling = f_t(1) / (std::sqrt(h_sq[1]) + f_t(1)); - s.sub_pdlp->get_initial_scaling_strategy().apply_distributed_bound_objective_rescaling( - bound_rescaling, objective_rescaling); - } - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - s.stream.synchronize(); - } - } - // -------- Generic distributed SpMVs ------------------------------------- // distributed_spmv_A : halo-update the var-shaped input buffer returned by // `in_buf(pdhg)`, then per-shard A @ in_buf -> out_desc. @@ -446,432 +363,6 @@ struct multi_gpu_engine_t { for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); }); } - // -------- Distributed Ruiz inf-scaling ----------------------------------- - // Each shard owns its rows AND its columns and stores both complete (h_A = - // owned rows, h_A_t = owned columns), exactly like the SpMV path. So every - // owned row's inf-norm (from the row-major matrix) and every owned column's - // inf-norm (from the transpose A_T) is computed locally and completely -- no - // cross-shard reduction. The only cross-shard step is a halo broadcast of - // both cumulative scalings before each iteration, so the row kernel sees - // correct column factors on its halo columns and the column kernel sees - // correct row factors on its halo rows (the same forward halo exchange SpMV - // uses, just carrying a scaling factor instead of x / y). - void distributed_ruiz_inf_scaling(int num_iter, i_t n_global_vars) - { - if (num_iter <= 0 || n_global_vars <= 0) return; - raft::common::nvtx::range scope("distributed_ruiz_inf_scaling"); - - for (int it = 0; it < num_iter; ++it) { - // Refresh halo copies of both cumulative scalings (owner -> halo) so the - // per-shard kernels read correct opposite-axis factors on their halo. - broadcast_variable_scaling_to_halo(); - broadcast_constraint_scaling_to_halo(); - - // Per-shard local kernels: row inf-norm (owned rows, complete) + column - // inf-norm from A_T (owned columns, complete; halo columns -> 0). - for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_compute_local_iteration_vectors(); - }); - - // Fold into cumulative on owned entries (halo entries get refreshed by - // the next iteration's broadcast). - for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_apply_cumulative_update(); - }); - } - - // Final refresh so downstream consumers (the scaled problem, the next - // distributed_max_singular_value, etc.) see correct halo factors. - broadcast_variable_scaling_to_halo(); - broadcast_constraint_scaling_to_halo(); - - for_each_shard([](auto& shard) { shard.stream.synchronize(); }); - } - - // Distributed Pock-Chambolle: one pass, mirroring single-GPU - // pock_chambolle_scaling. Row sum-of-powers come from the row-major matrix - // (owned rows, complete) and column sum-of-powers from A_T (owned columns, - // complete) -- no cross-shard reduction, only the halo broadcast of both - // cumulative scalings. Runs after the distributed Ruiz pass, matching the - // single-GPU order (Ruiz then Pock-Chambolle). - void distributed_pock_chambolle_scaling(f_t alpha, i_t n_global_vars) - { - if (n_global_vars <= 0) return; - raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); - - // Refresh halo copies of both cumulative scalings so the row kernel sees - // correct column factors on halo columns and the A_T column kernel sees - // correct row factors on halo rows. - broadcast_variable_scaling_to_halo(); - broadcast_constraint_scaling_to_halo(); - - for_each_shard([alpha](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_compute_local_iteration_vectors( - alpha); - }); - - for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_apply_cumulative_update(); - }); - - // Final refresh for downstream consumers. - broadcast_variable_scaling_to_halo(); - broadcast_constraint_scaling_to_halo(); - - for_each_shard([](auto& shard) { shard.stream.synchronize(); }); - } - - // -------- Distributed σ_max(A) via power iteration ---------------------- - f_t distributed_max_singular_value(i_t n_global_cstrs, - int max_iterations = 5000, - f_t tolerance = 1e-4) - { - raft::common::nvtx::range scope("distributed_max_singular_value"); - - const int nb = static_cast(shards.size()); - - // Generate the GLOBAL z[] sequence in cstr-index order from a fresh - // mt19937(1), once per call. It's m doubles regardless of N (cheap). - // Each shard then keeps only z[global_idx_for_owned_local_i]. - std::vector h_global_z(static_cast(n_global_cstrs)); - { - std::mt19937 gen(1); - std::normal_distribution dist(f_t(0.0), f_t(1.0)); - for (i_t i = 0; i < n_global_cstrs; ++i) { - h_global_z[i] = dist(gen); - } - } - - // Per-shard scratch lives on each shard's device. We use total (owned + - // halo) sizes for q/z/atq because they're SpMV inputs that need halo - // space. Norms / dot are scalars. - // We use size-1 rmm::device_uvector instead of rmm::device_scalar for the - // per-shard scratch scalars: nvcc + libcudacxx fail the - // copy_constructible concept check when device_scalar appears in a - // std::vector (the check transitively touches rmm::cuda_stream, which is - // non-copyable). device_uvector avoids that path. - std::vector> q; - std::vector> z; - std::vector> atq; - std::vector> sigma_sq; - std::vector> norm_q; - std::vector> residual_norm; - std::vector z_dn(nb, nullptr); - std::vector atq_dn(nb, nullptr); - q.reserve(nb); - z.reserve(nb); - atq.reserve(nb); - sigma_sq.reserve(nb); - norm_q.reserve(nb); - residual_norm.reserve(nb); - - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - const i_t cstr_total = s.rank_data.total_cstr_size; - const i_t var_total = s.rank_data.total_var_size; - q.emplace_back(static_cast(cstr_total), s.stream.view()); - z.emplace_back(static_cast(cstr_total), s.stream.view()); - atq.emplace_back(static_cast(var_total), s.stream.view()); - sigma_sq.emplace_back(std::size_t{1}, s.stream.view()); - norm_q.emplace_back(std::size_t{1}, s.stream.view()); - residual_norm.emplace_back(std::size_t{1}, s.stream.view()); - RAFT_CUSPARSE_TRY(raft::sparse::detail::cusparsecreatednvec( - &z_dn[r], static_cast(cstr_total), z.back().data())); - RAFT_CUSPARSE_TRY(raft::sparse::detail::cusparsecreatednvec( - &atq_dn[r], static_cast(var_total), atq.back().data())); - - std::vector h_owned_z(static_cast(s.rank_data.owned_cstr_size)); - for (i_t i = 0; i < s.rank_data.owned_cstr_size; ++i) { - const i_t g = s.rank_data.local_to_global_cstr[i]; - h_owned_z[i] = h_global_z[g]; - } - if (s.rank_data.owned_cstr_size > 0) { - RAFT_CUDA_TRY( - cudaMemcpyAsync(z.back().data(), - h_owned_z.data(), - sizeof(f_t) * static_cast(s.rank_data.owned_cstr_size), - cudaMemcpyHostToDevice, - s.stream.view().value())); - } - if (cstr_total > s.rank_data.owned_cstr_size) { - RAFT_CUDA_TRY(cudaMemsetAsync( - z.back().data() + s.rank_data.owned_cstr_size, - 0, - sizeof(f_t) * static_cast(cstr_total - s.rank_data.owned_cstr_size), - s.stream.view().value())); - } - // Sync to ensure h_owned_z stays valid through the H2D copy (it goes - // out of scope at end of this iteration of the per-shard loop). - s.stream.synchronize(); - } - - // Local halo-exchange helpers that work directly on per-shard external - // buffers (the engine's halo_exchange_var/cstr expect accessors that - // resolve through pdhg_solver_t, which doesn't see our scratch). - auto halo_exchange_cstr_bufs = [&](std::vector>& bufs) { - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto& y = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - if (s.cstr_send_indices_d[peer].size() == 0) continue; - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.cstr_send_indices_d[peer].begin(), - s.cstr_send_indices_d[peer].end(), - y.begin(), - s.cstr_send_buf_d[peer].begin()); - } - } - ncclGroupStart(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - ncclSend(s.cstr_send_buf_d[peer].data(), - s.cstr_send_buf_d[peer].size(), - ncclFloat64, - peer, - s.comm.get(), - s.stream.view().value()); - } - } - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - auto& rd = s.rank_data; - raft::device_setter guard(s.device_id); - auto& y = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; - ncclRecv(recv_ptr, - static_cast(rd.cstr_recv_counts[peer]), - ncclFloat64, - peer, - s.comm.get(), - s.stream.view().value()); - } - } - ncclGroupEnd(); - }; - auto halo_exchange_var_bufs = [&](std::vector>& bufs) { - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto& x = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - if (s.var_send_indices_d[peer].size() == 0) continue; - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.var_send_indices_d[peer].begin(), - s.var_send_indices_d[peer].end(), - x.begin(), - s.var_send_buf_d[peer].begin()); - } - } - ncclGroupStart(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - ncclSend(s.var_send_buf_d[peer].data(), - s.var_send_buf_d[peer].size(), - ncclFloat64, - peer, - s.comm.get(), - s.stream.view().value()); - } - } - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - auto& rd = s.rank_data; - raft::device_setter guard(s.device_id); - auto& x = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; - ncclRecv(recv_ptr, - static_cast(rd.var_recv_counts[peer]), - ncclFloat64, - peer, - s.comm.get(), - s.stream.view().value()); - } - } - ncclGroupEnd(); - }; - - // Per-shard partial reductions over the OWNED cstr slice + NCCL allreduce. - // For norm: out := sqrt(Σ_r ||bufs[r][0:owned_cstr]||²). - // For dot : out := Σ_r . - auto distributed_norm_owned_cstr = [&](std::vector>& bufs, - std::vector>& out) { - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - const i_t n_owned = s.rank_data.owned_cstr_size; - RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), - static_cast(n_owned), - bufs[r].data(), - 1, - bufs[r].data(), - 1, - out[r].data(), - s.stream.view().value())); - } - ncclGroupStart(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - ncclAllReduce(out[r].data(), - out[r].data(), - 1, - ncclFloat64, - ncclSum, - s.comm.get(), - s.stream.view().value()); - } - ncclGroupEnd(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - cub::DeviceTransform::Transform( - out[r].data(), out[r].data(), 1, sqrt_inplace_op_t{}, s.stream.view().value()); - } - }; - auto distributed_dot_owned_cstr = [&](std::vector>& a, - std::vector>& b, - std::vector>& out) { - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - const i_t n_owned = s.rank_data.owned_cstr_size; - RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), - static_cast(n_owned), - a[r].data(), - 1, - b[r].data(), - 1, - out[r].data(), - s.stream.view().value())); - } - ncclGroupStart(); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - ncclAllReduce(out[r].data(), - out[r].data(), - 1, - ncclFloat64, - ncclSum, - s.comm.get(), - s.stream.view().value()); - } - ncclGroupEnd(); - }; - - // ===== Power iteration ===== - // Mirrors single-GPU compute_initial_step_size: z is the carried iterate - // (A Aᵀ q each step); at the top of each iteration q := z then q is - // normalized; the residual z − σ²q is written back into q only to drive - // the convergence check (next iteration's q := z discards it). - for (int it = 0; it < max_iterations; ++it) { - // q := z on the owned slice (the carried iterate), then normalize. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - const i_t n_owned = s.rank_data.owned_cstr_size; - raft::copy(q[r].data(), z[r].data(), n_owned, s.stream.view()); - } - - // ||q||₂ over the global OWNED cstr slice (one allreduce-sum + sqrt). - distributed_norm_owned_cstr(q, norm_q); - - // q /= ||q||₂ on owned slice (halo gets refreshed by next exchange). - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - const i_t n_owned = s.rank_data.owned_cstr_size; - cub::DeviceTransform::Transform( - q[r].data(), - q[r].data(), - n_owned, - [n = norm_q[r].data()] __device__(f_t v) { return v / *n; }, - s.stream.view().value()); - } - - // atq = A^T q : halo-exchange q, then per-shard SpMV. spmv_At_into - // rebinds the dual_solution dnvec to q[r].data() and restores the - // canonical binding after the call (see pdhg.cu:643-644). - halo_exchange_cstr_bufs(q); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - s.sub_pdlp->pdhg_solver_.spmv_At_into(q[r], atq_dn[r]); - } - - // z = A atq : halo-exchange atq, then per-shard SpMV. - halo_exchange_var_bufs(atq); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - s.sub_pdlp->pdhg_solver_.spmv_A_into(atq[r], z_dn[r]); - } - - // σ² = q · z over the global OWNED cstr slice (= q^T A A^T q = σ_max² - // when q is the dominant left-singular vector). - distributed_dot_owned_cstr(q, z, sigma_sq); - - // q := -σ² q + z (owned slice) — residual of the eigen-equation. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - const i_t n_owned = s.rank_data.owned_cstr_size; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(q[r].data(), z[r].data()), - q[r].data(), - n_owned, - [s2 = sigma_sq[r].data()] __device__(f_t qv, f_t zv) { return -(*s2) * qv + zv; }, - s.stream.view().value()); - } - - // Convergence check via global residual norm. - distributed_norm_owned_cstr(q, residual_norm); - auto& s0 = *shards[0]; - raft::device_setter guard0(s0.device_id); - f_t h_res{}; - RAFT_CUDA_TRY(cudaMemcpyAsync(&h_res, - residual_norm[0].data(), - sizeof(f_t), - cudaMemcpyDeviceToHost, - s0.stream.view().value())); - s0.stream.synchronize(); - if (h_res < tolerance) break; - } - - // σ_max² is the same on every shard after the last allreduce. - auto& s0 = *shards[0]; - raft::device_setter guard0(s0.device_id); - f_t sigma_sq_h{}; - RAFT_CUDA_TRY(cudaMemcpyAsync(&sigma_sq_h, - sigma_sq[0].data(), - sizeof(f_t), - cudaMemcpyDeviceToHost, - s0.stream.view().value())); - s0.stream.synchronize(); - - for (int r = 0; r < nb; ++r) { - raft::device_setter guard(shards[r]->device_id); - RAFT_CUSPARSE_TRY(cusparseDestroyDnVec(z_dn[r])); - RAFT_CUSPARSE_TRY(cusparseDestroyDnVec(atq_dn[r])); - } - - return std::sqrt(std::max(sigma_sq_h, f_t(0))); - } - // -------- Solution gather (shards -> master) ---------------------------- // Assembles the global potential_next primal/dual solutions and the // reduced_cost on the master from the owned slices distributed across diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 287693ab12..29bace13aa 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -20,6 +20,7 @@ #include #include #include "cuopt/linear_programming/pdlp/solver_solution.hpp" +#include "distributed_pdlp/distributed_algorithms.hpp" #include "distributed_pdlp/multi_gpu_engine.hpp" #include @@ -570,12 +571,14 @@ pdlp_solver_t::pdlp_solver_t( // scalings refreshed internally (owner -> halo broadcast), so no extra halo // push is needed here. if (settings_.hyper_params.do_ruiz_scaling) { - multi_gpu_engine->distributed_ruiz_inf_scaling( - settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); + distributed_ruiz_inf_scaling( + *multi_gpu_engine, settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); } if (settings_.hyper_params.do_pock_chambolle_scaling) { - multi_gpu_engine->distributed_pock_chambolle_scaling( - static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), n_vars); + distributed_pock_chambolle_scaling( + *multi_gpu_engine, + static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), + n_vars); } for (auto& shard : multi_gpu_engine->shards) { @@ -596,13 +599,14 @@ pdlp_solver_t::pdlp_solver_t( // Global bound/objective rescaling: allreduce the owned partial squared-norms if (settings_.hyper_params.bound_objective_rescaling && !inside_mip_) { - multi_gpu_engine->distributed_bound_objective_rescaling( + distributed_bound_objective_rescaling( + *multi_gpu_engine, static_cast(settings_.hyper_params.initial_primal_weight_c_scaling)); } // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- constexpr f_t kStepSizeScale = f_t{0.998}; - const f_t sigma_max = multi_gpu_engine->distributed_max_singular_value(n_cstr); + const f_t sigma_max = distributed_max_singular_value(*multi_gpu_engine, n_cstr); const f_t h_primal_weight = f_t{1}; const f_t h_step_size = (sigma_max > f_t{0}) ? kStepSizeScale / sigma_max : f_t{1}; // With primal_weight = 1 the adaptive step-size strategy collapses to From 197df24ee3ae1932a77a6f498fd77645c2d9edd4 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 21:15:04 +0200 Subject: [PATCH 100/258] use RAII communicators --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 17 +++++++++++++---- cpp/src/pdlp/distributed_pdlp/shard.cu | 4 ++-- cpp/src/pdlp/distributed_pdlp/shard.hpp | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index dd5857b14d..7846814dcf 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -37,19 +37,28 @@ multi_gpu_engine_t::multi_gpu_engine_t( std::vector devices(nb_parts); std::iota(devices.begin(), devices.end(), 0); - // Create NCCL Comms then let shards own them - std::vector raw_comms(nb_parts); + // Create NCCL Comms, then immediately wrap each in a RAII owner so they are + // destroyed on any exception (e.g. a shard ctor throwing) before being + // handed off to a shard. + std::vector raw_comms(nb_parts, nullptr); cuopt_expects(ncclCommInitAll(raw_comms.data(), nb_parts, devices.data()) == ncclSuccess, error_type_t::RuntimeError, "ncclCommInitAll failed"); - // 3. Construct one shard per rank, pinned to its device. + std::vector comms; + comms.reserve(nb_parts); + for (int r = 0; r < nb_parts; ++r) { + comms.emplace_back(raw_comms[r], nccl_comm_deleter_t{devices[r]}); + } + + // 3. Construct one shard per rank, pinned to its device. Ownership of each + // communicator moves into its shard. CUOPT_LOG_INFO("distributed_pdlp: building %d shard solver(s) ...", nb_parts); auto shard_build_t0 = std::chrono::high_resolution_clock::now(); for (int r = 0; r < nb_parts; ++r) { raft::device_setter guard(devices[r]); // shard ctor needs device set shards.emplace_back(std::make_unique>( - devices[r], std::move(rank_data[r]), raw_comms[r], mps, sub_solver_settings)); + devices[r], std::move(rank_data[r]), std::move(comms[r]), mps, sub_solver_settings)); } auto shard_build_t1 = std::chrono::high_resolution_clock::now(); CUOPT_LOG_INFO("distributed_pdlp: shard build done in %.3f s", diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 44ca0c88f4..aadca28089 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -24,13 +24,13 @@ pdlp_shard_t::~pdlp_shard_t() = default; template pdlp_shard_t::pdlp_shard_t(int device_id, rank_data_t&& rd, - ncclComm_t raw_comm, + nccl_comm_unique_ptr_t&& comm, io::mps_data_model_t const& mps, pdlp_solver_settings_t const& settings) : device_id(device_id), stream(), handle(stream.view()), - comm(raw_comm, nccl_comm_deleter_t{device_id}), + comm(std::move(comm)), rank_data(std::move(rd)), opt_problem(std::nullopt), sub_problem(std::nullopt), diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index f96c014f77..e78516eb36 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -51,7 +51,7 @@ struct pdlp_shard_t { // Owns necessary multi-gpu data (rank_data, device_id, nccl_comm) pdlp_shard_t(int device_id, rank_data_t&& rd, - ncclComm_t raw_comm, + nccl_comm_unique_ptr_t&& comm, io::mps_data_model_t const& mps, pdlp_solver_settings_t const& settings); From ed4f9e16bc2cac6585d8ba5642ef8b880211582d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 21:18:53 +0200 Subject: [PATCH 101/258] cleaned distributed_algorithm.cu --- .../pdlp/distributed_pdlp/distributed_algorithms.cu | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 22170b6cb9..33d99c03ce 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -6,9 +6,6 @@ #include #include #include -// The bodies below call shard.sub_pdlp->get_initial_scaling_strategy() and -// shard.sub_pdlp->pdhg_solver_.spmv_* — pdlp_solver_t / pdhg_solver_t must be -// complete at the explicit instantiation point below. #include #include @@ -43,8 +40,7 @@ void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, const int n_owned_var = static_cast(s.rank_data.owned_var_size); // Squared-norm contribution of each constraint's [lower, upper] bound pair - // (mirrors rhs_sum_of_squares_t). The lower bound is the reduce input; the - // matching upper bound is fetched by index inside the op. + // (mirrors rhs_sum_of_squares_t). const f_t* upper = scaled.constraint_upper_bounds.data(); auto bound_op = [upper] __device__(f_t lower, i_t i) { const f_t u = upper[i]; @@ -430,10 +426,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, }; // ===== Power iteration ===== - // Mirrors single-GPU compute_initial_step_size: z is the carried iterate - // (A Aᵀ q each step); at the top of each iteration q := z then q is - // normalized; the residual z − σ²q is written back into q only to drive - // the convergence check (next iteration's q := z discards it). + // Mirrors single-GPU compute_initial_step_size for (int it = 0; it < max_iterations; ++it) { // q := z on the owned slice (the carried iterate), then normalize. for (int r = 0; r < nb; ++r) { @@ -461,7 +454,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, // atq = A^T q : halo-exchange q, then per-shard SpMV. spmv_At_into // rebinds the dual_solution dnvec to q[r].data() and restores the - // canonical binding after the call (see pdhg.cu:643-644). + // canonical binding after the call halo_exchange_cstr_bufs(q); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; From c193656af9a54198a257d569366b0384895ccd19 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 21:24:34 +0200 Subject: [PATCH 102/258] replaced raw cudamemcopy with raft::copy --- .../distributed_pdlp/multi_gpu_engine.hpp | 53 ++++++++----------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 50d9291c15..3ce45c861d 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -411,25 +411,20 @@ struct multi_gpu_engine_t { .get_reduced_cost(); if (nv > 0) { - RAFT_CUDA_TRY( - cudaMemcpyAsync(tmp_primal.data(), - s.sub_pdlp->pdhg_solver_.get_potential_next_primal_solution().data(), - static_cast(nv) * sizeof(f_t), - cudaMemcpyDeviceToHost, - s.stream.view().value())); - RAFT_CUDA_TRY(cudaMemcpyAsync(tmp_reduced_cost.data(), - sub_reduced_cost.data(), - static_cast(nv) * sizeof(f_t), - cudaMemcpyDeviceToHost, - s.stream.view().value())); + raft::copy(tmp_primal.data(), + s.sub_pdlp->pdhg_solver_.get_potential_next_primal_solution().data(), + static_cast(nv), + s.stream.view()); + raft::copy(tmp_reduced_cost.data(), + sub_reduced_cost.data(), + static_cast(nv), + s.stream.view()); } if (nc > 0) { - RAFT_CUDA_TRY( - cudaMemcpyAsync(tmp_dual.data(), - s.sub_pdlp->pdhg_solver_.get_potential_next_dual_solution().data(), - static_cast(nc) * sizeof(f_t), - cudaMemcpyDeviceToHost, - s.stream.view().value())); + raft::copy(tmp_dual.data(), + s.sub_pdlp->pdhg_solver_.get_potential_next_dual_solution().data(), + static_cast(nc), + s.stream.view()); } RAFT_CUDA_TRY(cudaStreamSynchronize(s.stream.view().value())); @@ -456,21 +451,15 @@ struct multi_gpu_engine_t { // Host -> master device. engine.stream lives on the master device // (created at engine construction when master device was current). - RAFT_CUDA_TRY(cudaMemcpyAsync(master_pdhg.get_potential_next_primal_solution().data(), - h_primal.data(), - total_vars * sizeof(f_t), - cudaMemcpyHostToDevice, - stream.view().value())); - RAFT_CUDA_TRY(cudaMemcpyAsync(master_pdhg.get_potential_next_dual_solution().data(), - h_dual.data(), - total_cstrs * sizeof(f_t), - cudaMemcpyHostToDevice, - stream.view().value())); - RAFT_CUDA_TRY(cudaMemcpyAsync(master_reduced_cost.data(), - h_reduced_cost.data(), - total_vars * sizeof(f_t), - cudaMemcpyHostToDevice, - stream.view().value())); + raft::copy(master_pdhg.get_potential_next_primal_solution().data(), + h_primal.data(), + total_vars, + stream.view()); + raft::copy(master_pdhg.get_potential_next_dual_solution().data(), + h_dual.data(), + total_cstrs, + stream.view()); + raft::copy(master_reduced_cost.data(), h_reduced_cost.data(), total_vars, stream.view()); RAFT_CUDA_TRY(cudaStreamSynchronize(stream.view().value())); } From c42f7709ed9ab69f3dacab5d1d9ddbc10ba77c44 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 16 Jun 2026 21:41:08 +0200 Subject: [PATCH 103/258] updated multi_gpu_engine for clarity --- .../distributed_algorithms.cu | 123 +++++++++++- .../distributed_algorithms.hpp | 26 ++- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 73 +++++++- .../distributed_pdlp/multi_gpu_engine.hpp | 177 +----------------- .../termination_strategy.cu | 5 +- 5 files changed, 217 insertions(+), 187 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 33d99c03ce..a1bce8aacc 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -18,6 +18,105 @@ namespace cuopt::linear_programming::detail { +// -------- Broadcast owned constraint (row) scaling into halo -------------- +template +void broadcast_constraint_scaling_to_halo(multi_gpu_engine_t& engine) +{ + engine.halo_exchange_cstr_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + }); +} + +// -------- Broadcast owned variable (column) scaling into halo ------------- +template +void broadcast_variable_scaling_to_halo(multi_gpu_engine_t& engine) +{ + engine.halo_exchange_var_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + }); +} + +// -------- Solution gather (shards -> master) ------------------------------ +template +void gather_potential_next_solutions_to_master(multi_gpu_engine_t& engine, + pdhg_solver_t& master_pdhg, + rmm::device_uvector& master_reduced_cost) +{ + const std::size_t total_vars = master_pdhg.get_potential_next_primal_solution().size(); + const std::size_t total_cstrs = master_pdhg.get_potential_next_dual_solution().size(); + + std::vector h_primal(total_vars); + std::vector h_dual(total_cstrs); + std::vector h_reduced_cost(total_vars); + + for (auto& s_uptr : engine.shards) { + auto& s = *s_uptr; + raft::device_setter guard(s.device_id); + const i_t nv = s.rank_data.owned_var_size; + const i_t nc = s.rank_data.owned_cstr_size; + + std::vector tmp_primal(nv); + std::vector tmp_dual(nc); + std::vector tmp_reduced_cost(nv); + + auto& sub_reduced_cost = s.sub_pdlp->get_current_termination_strategy() + .get_convergence_information() + .get_reduced_cost(); + + if (nv > 0) { + raft::copy(tmp_primal.data(), + s.sub_pdlp->pdhg_solver_.get_potential_next_primal_solution().data(), + static_cast(nv), + s.stream.view()); + raft::copy(tmp_reduced_cost.data(), + sub_reduced_cost.data(), + static_cast(nv), + s.stream.view()); + } + if (nc > 0) { + raft::copy(tmp_dual.data(), + s.sub_pdlp->pdhg_solver_.get_potential_next_dual_solution().data(), + static_cast(nc), + s.stream.view()); + } + RAFT_CUDA_TRY(cudaStreamSynchronize(s.stream.view().value())); + + if (nv > 0) { + thrust::scatter(thrust::host, + tmp_primal.begin(), + tmp_primal.end(), + s.rank_data.local_to_global_var.begin(), + h_primal.begin()); + thrust::scatter(thrust::host, + tmp_reduced_cost.begin(), + tmp_reduced_cost.end(), + s.rank_data.local_to_global_var.begin(), + h_reduced_cost.begin()); + } + if (nc > 0) { + thrust::scatter(thrust::host, + tmp_dual.begin(), + tmp_dual.end(), + s.rank_data.local_to_global_cstr.begin(), + h_dual.begin()); + } + } + + // Host -> master device. engine.stream lives on the master device + // (created at engine construction when master device was current). + raft::copy(master_pdhg.get_potential_next_primal_solution().data(), + h_primal.data(), + total_vars, + engine.stream.view()); + raft::copy(master_pdhg.get_potential_next_dual_solution().data(), + h_dual.data(), + total_cstrs, + engine.stream.view()); + raft::copy( + master_reduced_cost.data(), h_reduced_cost.data(), total_vars, engine.stream.view()); + RAFT_CUDA_TRY(cudaStreamSynchronize(engine.stream.view().value())); +} + // -------- Distributed bound / objective rescaling ------------------------- template void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, @@ -116,8 +215,8 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, for (int it = 0; it < num_iter; ++it) { // Refresh halo copies of both cumulative scalings (owner -> halo) so the // per-shard kernels read correct opposite-axis factors on their halo. - engine.broadcast_variable_scaling_to_halo(); - engine.broadcast_constraint_scaling_to_halo(); + broadcast_variable_scaling_to_halo(engine); + broadcast_constraint_scaling_to_halo(engine); // Per-shard local kernels: row inf-norm (owned rows, complete) + column // inf-norm from A_T (owned columns, complete; halo columns -> 0). @@ -134,8 +233,8 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, // Final refresh so downstream consumers (the scaled problem, the next // distributed_max_singular_value, etc.) see correct halo factors. - engine.broadcast_variable_scaling_to_halo(); - engine.broadcast_constraint_scaling_to_halo(); + broadcast_variable_scaling_to_halo(engine); + broadcast_constraint_scaling_to_halo(engine); engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } @@ -153,8 +252,8 @@ void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); // Refresh halo copies of both cumulative scalings - engine.broadcast_variable_scaling_to_halo(); - engine.broadcast_constraint_scaling_to_halo(); + broadcast_variable_scaling_to_halo(engine); + broadcast_constraint_scaling_to_halo(engine); engine.for_each_shard([alpha](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_compute_local_iteration_vectors( @@ -166,8 +265,8 @@ void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, }); // Final refresh for downstream consumers. - engine.broadcast_variable_scaling_to_halo(); - engine.broadcast_constraint_scaling_to_halo(); + broadcast_variable_scaling_to_halo(engine); + broadcast_constraint_scaling_to_halo(engine); engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } @@ -509,6 +608,10 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, } // ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- +template void broadcast_constraint_scaling_to_halo( + multi_gpu_engine_t& engine); +template void broadcast_variable_scaling_to_halo( + multi_gpu_engine_t& engine); template void distributed_bound_objective_rescaling( multi_gpu_engine_t& engine, double c_scaling_weight); template void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, @@ -520,5 +623,9 @@ template double distributed_max_singular_value(multi_gpu_engine_t( + multi_gpu_engine_t& engine, + pdhg_solver_t& master_pdhg, + rmm::device_uvector& master_reduced_cost); } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp index 85948c0af0..9a00f83de8 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp @@ -4,12 +4,29 @@ */ #pragma once -// Algorithm-level distributed PDLP numerical methods. +#include + +// Algorithm-level distributed PDLP namespace cuopt::linear_programming::detail { template struct multi_gpu_engine_t; +template +class pdhg_solver_t; + +// Broadcast the owned constraint (row) cumulative scaling into every peer's +// halo copy. The factor is computed only on owned rows; this pushes each +// owner's values out so halo rows carry correct factors for the next pass. +template +void broadcast_constraint_scaling_to_halo(multi_gpu_engine_t& engine); + +// Broadcast the owned variable (column) cumulative scaling into every peer's +// halo copy, so the next scaling iteration's row / column inf-norm kernels read +// correct factors on their halo columns. +template +void broadcast_variable_scaling_to_halo(multi_gpu_engine_t& engine); + // Global bound/objective rescaling: allreduce the owned partial squared norms // of the constraint bounds and (weighted) objective, then apply the identical // scalar on every shard. @@ -41,4 +58,11 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, int max_iterations = 5000, f_t tolerance = 1e-4); +// Gather the global potential_next primal/dual solutions and the reduced cost +// onto the master from the owned slices distributed across shards. +template +void gather_potential_next_solutions_to_master(multi_gpu_engine_t& engine, + pdhg_solver_t& master_pdhg, + rmm::device_uvector& master_reduced_cost); + } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 7846814dcf..36f4064a1c 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -4,9 +4,6 @@ */ #include -// compute_A_x() / compute_At_y() (defined inline in the engine header) call -// shard.sub_pdlp->pdhg_solver_.compute_* — pdlp_solver_t must be complete at -// the explicit instantiation point below. #include #include @@ -40,13 +37,13 @@ multi_gpu_engine_t::multi_gpu_engine_t( // Create NCCL Comms, then immediately wrap each in a RAII owner so they are // destroyed on any exception (e.g. a shard ctor throwing) before being // handed off to a shard. + std::vector comms; + comms.reserve(nb_parts); std::vector raw_comms(nb_parts, nullptr); cuopt_expects(ncclCommInitAll(raw_comms.data(), nb_parts, devices.data()) == ncclSuccess, error_type_t::RuntimeError, "ncclCommInitAll failed"); - std::vector comms; - comms.reserve(nb_parts); for (int r = 0; r < nb_parts; ++r) { comms.emplace_back(raw_comms[r], nccl_comm_deleter_t{devices[r]}); } @@ -78,6 +75,72 @@ multi_gpu_engine_t::multi_gpu_engine_t( } } +// -------- High-level: A @ x and A_T @ y ----------------------------------- +template +void multi_gpu_engine_t::distributed_compute_A_x() +{ + halo_exchange_var( + [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_primal(); }); + for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_A_x(); }); +} + +template +void multi_gpu_engine_t::distributed_compute_At_y() +{ + halo_exchange_cstr( + [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_dual_solution(); }); + for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); }); +} + +// -------- Cross-stream fork / join / sync --------------------------------- +template +void multi_gpu_engine_t::graph_capture_fork_to_shards( + rmm::cuda_stream_view master_stream) +{ + graph_master_ready_event_->record(master_stream); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + graph_master_ready_event_->stream_wait(s->stream.view()); + } +} + +template +void multi_gpu_engine_t::graph_capture_join_from_shards( + rmm::cuda_stream_view master_stream) +{ + const int nb = static_cast(shards.size()); + for (int r = 0; r < nb; ++r) { + raft::device_setter guard(shards[r]->device_id); + graph_shard_ready_events_[r]->record(shards[r]->stream.view()); + } + for (auto& e : graph_shard_ready_events_) { + e->stream_wait(master_stream); + } +} + +template +void multi_gpu_engine_t::sync_await_master(rmm::cuda_stream_view master_stream) +{ + sync_master_ready_event_->record(master_stream); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + sync_master_ready_event_->stream_wait(s->stream.view()); + } +} + +template +void multi_gpu_engine_t::sync_await_shards(rmm::cuda_stream_view master_stream) +{ + const int nb = static_cast(shards.size()); + for (int r = 0; r < nb; ++r) { + raft::device_setter guard(shards[r]->device_id); + sync_shard_ready_events_[r]->record(shards[r]->stream.view()); + } + for (auto& e : sync_shard_ready_events_) { + e->stream_wait(master_stream); + } +} + template struct multi_gpu_engine_t; // template struct multi_gpu_engine_t; diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 3ce45c861d..24aa5f414b 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -232,27 +232,6 @@ struct multi_gpu_engine_t { ncclGroupEnd(); } - // -------- Broadcast owned constraint (row) scaling into halo ------------ - // The cumulative constraint-matrix (row) scaling is computed only on owned - // rows; push each owner's values into the peers' halo copies. - void broadcast_constraint_scaling_to_halo() - { - halo_exchange_cstr_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); - }); - } - - // -------- Broadcast owned variable (column) scaling into halo ----------- - // The cumulative variable (column) scaling is computed only on owned columns; - // push each owner's values into the peers' halo copies so the next scaling - // iteration's row / column inf-norm kernels read correct factors on halo. - void broadcast_variable_scaling_to_halo() - { - halo_exchange_var_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); - }); - } - // -------- NCCL allreduce (sum, in place) -------------------------------- // Per-shard in-place sum-allreduce. Each shard's stream issues an // ncclAllReduce(buf, buf, count, ncclFloat64, ncclSum, ...) inside a single @@ -347,121 +326,11 @@ struct multi_gpu_engine_t { // -------- High-level: A @ x and A_T @ y --------------------------------- // Distributed counterpart to pdhg_solver_t::compute_A_x() // We don't use distributed_spmv_A() because we are using SpMVOp rather than SpMV - void distributed_compute_A_x() - { - halo_exchange_var( - [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_primal(); }); - for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_A_x(); }); - } + void distributed_compute_A_x(); // Distributed counterpart to pdhg_solver_t::compute_At_y() // We don't use distributed_spmv_At() because we are using SpMVOp rather than SpMV - void distributed_compute_At_y() - { - halo_exchange_cstr( - [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_dual_solution(); }); - for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); }); - } - - // -------- Solution gather (shards -> master) ---------------------------- - // Assembles the global potential_next primal/dual solutions and the - // reduced_cost on the master from the owned slices distributed across - // shards. Each shard's first owned_var_size (resp. owned_cstr_size) entries - // of its potential_next_primal_solution_ / reduced_cost_ (resp. - // potential_next_dual_solution_) are the live, up-to-date owned values; the - // master buffers are not updated during iterations and would otherwise - // return stale data. - // - // Used right before fill_return_problem_solution() at the return sites in - // pdlp_solver_t::check_termination() and pdlp_solver_t::check_limits(): the - // user-visible solution must contain gathered global values for primal, - // dual, and reduced_cost. - // - // Mirrors the metis_tests engine::get_x_output / get_y_output pattern: - // per shard: alloc small host tmp, copy owned slice device->host, sync, - // host-scatter via rank_data.local_to_global_{var,cstr} into a contiguous - // host buffer. Then one host->device copy into the master buffer per field. - // - // master_pdhg : provides destinations for primal / dual. - // master_reduced_cost : destination for the reduced_cost (var-shaped, lives - // in the master pdlp_solver_t's termination strategy - // convergence_information_). - void gather_potential_next_solutions_to_master(pdhg_solver_t& master_pdhg, - rmm::device_uvector& master_reduced_cost) - { - const std::size_t total_vars = master_pdhg.get_potential_next_primal_solution().size(); - const std::size_t total_cstrs = master_pdhg.get_potential_next_dual_solution().size(); - - std::vector h_primal(total_vars); - std::vector h_dual(total_cstrs); - std::vector h_reduced_cost(total_vars); - - for (auto& s_uptr : shards) { - auto& s = *s_uptr; - raft::device_setter guard(s.device_id); - const i_t nv = s.rank_data.owned_var_size; - const i_t nc = s.rank_data.owned_cstr_size; - - std::vector tmp_primal(nv); - std::vector tmp_dual(nc); - std::vector tmp_reduced_cost(nv); - - auto& sub_reduced_cost = s.sub_pdlp->get_current_termination_strategy() - .get_convergence_information() - .get_reduced_cost(); - - if (nv > 0) { - raft::copy(tmp_primal.data(), - s.sub_pdlp->pdhg_solver_.get_potential_next_primal_solution().data(), - static_cast(nv), - s.stream.view()); - raft::copy(tmp_reduced_cost.data(), - sub_reduced_cost.data(), - static_cast(nv), - s.stream.view()); - } - if (nc > 0) { - raft::copy(tmp_dual.data(), - s.sub_pdlp->pdhg_solver_.get_potential_next_dual_solution().data(), - static_cast(nc), - s.stream.view()); - } - RAFT_CUDA_TRY(cudaStreamSynchronize(s.stream.view().value())); - - if (nv > 0) { - thrust::scatter(thrust::host, - tmp_primal.begin(), - tmp_primal.end(), - s.rank_data.local_to_global_var.begin(), - h_primal.begin()); - thrust::scatter(thrust::host, - tmp_reduced_cost.begin(), - tmp_reduced_cost.end(), - s.rank_data.local_to_global_var.begin(), - h_reduced_cost.begin()); - } - if (nc > 0) { - thrust::scatter(thrust::host, - tmp_dual.begin(), - tmp_dual.end(), - s.rank_data.local_to_global_cstr.begin(), - h_dual.begin()); - } - } - - // Host -> master device. engine.stream lives on the master device - // (created at engine construction when master device was current). - raft::copy(master_pdhg.get_potential_next_primal_solution().data(), - h_primal.data(), - total_vars, - stream.view()); - raft::copy(master_pdhg.get_potential_next_dual_solution().data(), - h_dual.data(), - total_cstrs, - stream.view()); - raft::copy(master_reduced_cost.data(), h_reduced_cost.data(), total_vars, stream.view()); - RAFT_CUDA_TRY(cudaStreamSynchronize(stream.view().value())); - } + void distributed_compute_At_y(); // Engine-level stream for fork/join orchestration (master side). rmm::cuda_stream stream; @@ -480,52 +349,18 @@ struct multi_gpu_engine_t { std::vector> sync_shard_ready_events_; // Forks master stream to shards, so that the captured graph can see the work on the shards - void graph_capture_fork_to_shards(rmm::cuda_stream_view master_stream) - { - graph_master_ready_event_->record(master_stream); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - graph_master_ready_event_->stream_wait(s->stream.view()); - } - } + void graph_capture_fork_to_shards(rmm::cuda_stream_view master_stream); // Joins shards back to master stream for correct graph capture - void graph_capture_join_from_shards(rmm::cuda_stream_view master_stream) - { - const int nb = static_cast(shards.size()); - for (int r = 0; r < nb; ++r) { - raft::device_setter guard(shards[r]->device_id); - graph_shard_ready_events_[r]->record(shards[r]->stream.view()); - } - for (auto& e : graph_shard_ready_events_) { - e->stream_wait(master_stream); - } - } + void graph_capture_join_from_shards(rmm::cuda_stream_view master_stream); // Functionnaly same as graph_capture_fork_to_shards but on a different event to avoid race // conditions Can be used as a way to sync shards with master stream - void sync_await_master(rmm::cuda_stream_view master_stream) - { - sync_master_ready_event_->record(master_stream); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - sync_master_ready_event_->stream_wait(s->stream.view()); - } - } + void sync_await_master(rmm::cuda_stream_view master_stream); // Same as sync_await_master // Can be used as a way to sync master stream with shards - void sync_await_shards(rmm::cuda_stream_view master_stream) - { - const int nb = static_cast(shards.size()); - for (int r = 0; r < nb; ++r) { - raft::device_setter guard(shards[r]->device_id); - sync_shard_ready_events_[r]->record(shards[r]->stream.view()); - } - for (auto& e : sync_shard_ready_events_) { - e->stream_wait(master_stream); - } - } + void sync_await_shards(rmm::cuda_stream_view master_stream); }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index 7137ff4822..e0d68a78a2 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -5,6 +5,7 @@ */ /* clang-format on */ +#include #include #include @@ -569,8 +570,8 @@ pdlp_termination_strategy_t::fill_return_problem_solution( (&primal_iterate == ¤t_pdhg_solver.get_potential_next_primal_solution()) || (&primal_iterate == ¤t_pdhg_solver.get_primal_solution()); if (is_current_live_iterate) { - engine->gather_potential_next_solutions_to_master( - current_pdhg_solver, convergence_information_.get_reduced_cost()); + gather_potential_next_solutions_to_master( + *engine, current_pdhg_solver, convergence_information_.get_reduced_cost()); } } } From c5514157e13eb0a3aeb6a0e6b6c62e7c0c704f3f Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 11:29:38 +0200 Subject: [PATCH 104/258] instantiate as float to allow compilation --- .../distributed_algorithms.cu | 44 ++++++++++--------- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 2 +- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index a1bce8aacc..9be6b2d5a8 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -607,25 +607,29 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, return std::sqrt(std::max(sigma_sq_h, f_t(0))); } -// ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- -template void broadcast_constraint_scaling_to_halo( - multi_gpu_engine_t& engine); -template void broadcast_variable_scaling_to_halo( - multi_gpu_engine_t& engine); -template void distributed_bound_objective_rescaling( - multi_gpu_engine_t& engine, double c_scaling_weight); -template void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, - int num_iter, - int n_global_vars); -template void distributed_pock_chambolle_scaling( - multi_gpu_engine_t& engine, double alpha, int n_global_vars); -template double distributed_max_singular_value(multi_gpu_engine_t& engine, - int n_global_cstrs, - int max_iterations, - double tolerance); -template void gather_potential_next_solutions_to_master( - multi_gpu_engine_t& engine, - pdhg_solver_t& master_pdhg, - rmm::device_uvector& master_reduced_cost); +// ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- +#define INSTANTIATE(F_TYPE) \ + template void broadcast_constraint_scaling_to_halo( \ + multi_gpu_engine_t& engine); \ + template void broadcast_variable_scaling_to_halo( \ + multi_gpu_engine_t& engine); \ + template void distributed_bound_objective_rescaling( \ + multi_gpu_engine_t& engine, F_TYPE c_scaling_weight); \ + template void distributed_ruiz_inf_scaling( \ + multi_gpu_engine_t& engine, int num_iter, int n_global_vars); \ + template void distributed_pock_chambolle_scaling( \ + multi_gpu_engine_t& engine, F_TYPE alpha, int n_global_vars); \ + template F_TYPE distributed_max_singular_value( \ + multi_gpu_engine_t& engine, int n_global_cstrs, int max_iterations, \ + F_TYPE tolerance); \ + template void gather_potential_next_solutions_to_master( \ + multi_gpu_engine_t& engine, \ + pdhg_solver_t& master_pdhg, \ + rmm::device_uvector& master_reduced_cost); + +INSTANTIATE(double) +INSTANTIATE(float) + +#undef INSTANTIATE } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 36f4064a1c..b99ffbad16 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -142,6 +142,6 @@ void multi_gpu_engine_t::sync_await_shards(rmm::cuda_stream_view maste } template struct multi_gpu_engine_t; -// template struct multi_gpu_engine_t; +template struct multi_gpu_engine_t; } // namespace cuopt::linear_programming::detail From 558b90d7a4b5d8bf60f32ef35d78a2e285ae026d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 11:43:31 +0200 Subject: [PATCH 105/258] updated comments and cleaned partition_loader --- .../pdlp/distributed_pdlp/partition_loader.cu | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 3672ce009a..a6af208deb 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -23,8 +23,7 @@ std::vector partition_loader_t::parse_distributed_pdlp_partition_ "Failed to open partition file: %s", file.c_str()); - // One integer per line; operator>> skips whitespace so blank lines and - // trailing newlines are tolerated. + // One integer per line std::vector parts; i_t part = 0; while (part_file >> part) { @@ -98,7 +97,7 @@ std::vector> partition_loader_t::create_rank_dat ++owned_var_counts[owner]; owned_A_t_nnz[owner] += (A_t_row_offsets[j + 1] - A_t_row_offsets[j]); } - + // Reserve exact capacities for (i_t rank = 0; rank < nb_parts; ++rank) { rank_data[rank].owned_cstr_indices.reserve(static_cast(owned_cstr_counts[rank])); rank_data[rank].owned_var_indices.reserve(static_cast(owned_var_counts[rank])); @@ -129,10 +128,8 @@ std::vector> partition_loader_t::create_rank_dat i_t local_A_nnz = 0; local_A_row_offsets.push_back(local_A_nnz); - // For each owned constraint, build local matrix A. We walk both the - // unscaled and scaled global value arrays in lockstep so the produced - // local arrays share identical (offsets, col_indices) and differ only - // in values. + // For each owned constraint, add associated matrix row to local A + // Keep the same indices here but re-order later for (auto owned_cstr : rd.owned_cstr_indices) { i_t cstr_len = A_row_offsets[owned_cstr + 1] - A_row_offsets[owned_cstr]; i_t row_start = A_row_offsets[owned_cstr]; @@ -146,8 +143,10 @@ std::vector> partition_loader_t::create_rank_dat local_A_row_offsets.push_back(local_A_nnz); } + // Compute halo std::vector> needed_var_from_peer(nb_parts); std::unordered_set seen_needed_vars; + // size / 2 + 1 is a heuristic to avoid overestimating and resizing seen_needed_vars.reserve(local_A_col_indices.size() / 2 + 1); for (auto indice : local_A_col_indices) { @@ -157,6 +156,7 @@ std::vector> partition_loader_t::create_rank_dat } } + // Compute counts and offsets of halo data to stack them all at the end of the vector for (i_t peer = 0; peer < nb_parts; peer++) { i_t nb_recv_from_peer = needed_var_from_peer[peer].size(); rd.var_recv_counts[peer] = nb_recv_from_peer; @@ -170,6 +170,7 @@ std::vector> partition_loader_t::create_rank_dat rd.h_A_values = std::move(local_A_values); // ---- A_t side ---- + // conceptually same as A side std::vector local_A_t_row_offsets; std::vector local_A_t_col_indices; std::vector local_A_t_values; @@ -228,7 +229,6 @@ std::vector> partition_loader_t::create_rank_dat // 3. Generate local indices for contiguous [[self], [peer1], ..., [peer_k]] // Build scatter_gather_maps - // 3. Build local<->global maps in parallel across ranks. #pragma omp parallel for for (i_t rank = 0; rank < nb_parts; rank++) { auto& rd = rank_data[rank]; @@ -269,6 +269,7 @@ std::vector> partition_loader_t::create_rank_dat } // 4. Remap global -> local everywhere + // Including in A_local and At_local #pragma omp parallel for for (i_t rank = 0; rank < nb_parts; rank++) { auto& rd = rank_data[rank]; From 1223b4ab73d4f6084a337d95b9b73ac4e7278656 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 11:44:47 +0200 Subject: [PATCH 106/258] replaced hardcoded nccl types with compile time found types --- .../distributed_algorithms.cu | 14 +++++----- .../distributed_pdlp/multi_gpu_engine.hpp | 28 +++++++++++++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 9be6b2d5a8..7fe3a89126 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -178,7 +178,7 @@ void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); ncclAllReduce( - sq[r].data(), sq[r].data(), 2, ncclFloat64, ncclSum, s.comm.get(), s.stream.view().value()); + sq[r].data(), sq[r].data(), 2, nccl_data_type(), ncclSum, s.comm.get(), s.stream.view().value()); } ncclGroupEnd(); @@ -383,7 +383,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, if (peer == r) continue; ncclSend(s.cstr_send_buf_d[peer].data(), s.cstr_send_buf_d[peer].size(), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -399,7 +399,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; ncclRecv(recv_ptr, static_cast(rd.cstr_recv_counts[peer]), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -430,7 +430,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, if (peer == r) continue; ncclSend(s.var_send_buf_d[peer].data(), s.var_send_buf_d[peer].size(), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -446,7 +446,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; ncclRecv(recv_ptr, static_cast(rd.var_recv_counts[peer]), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -480,7 +480,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, ncclAllReduce(out[r].data(), out[r].data(), 1, - ncclFloat64, + nccl_data_type(), ncclSum, s.comm.get(), s.stream.view().value()); @@ -516,7 +516,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, ncclAllReduce(out[r].data(), out[r].data(), 1, - ncclFloat64, + nccl_data_type(), ncclSum, s.comm.get(), s.stream.view().value()); diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 24aa5f414b..a312cb1327 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -35,10 +35,26 @@ #include #include #include +#include #include namespace cuopt::linear_programming::detail { +// Maps the solver floating-point type to the matching NCCL datatype so that +// halo exchanges / all-reduces transfer the correct element size for both +// double and float instantiations. +template +constexpr ncclDataType_t nccl_data_type() +{ + static_assert(std::is_same_v || std::is_same_v, + "Unsupported floating-point type for NCCL transfers"); + if constexpr (std::is_same_v) { + return ncclFloat64; + } else { + return ncclFloat32; + } +} + // Element-wise sqrt functor. Defined at namespace scope (not as a local // extended HD lambda) because nvcc disallows extended __host__ __device__ // lambdas appearing inside templates whose template arguments are @@ -140,7 +156,7 @@ struct multi_gpu_engine_t { if (peer == r) continue; ncclSend(s.var_send_buf_d[peer].data(), s.var_send_buf_d[peer].size(), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -156,7 +172,7 @@ struct multi_gpu_engine_t { f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; ncclRecv(recv_ptr, static_cast(rd.var_recv_counts[peer]), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -207,7 +223,7 @@ struct multi_gpu_engine_t { if (peer == r) continue; ncclSend(s.cstr_send_buf_d[peer].data(), s.cstr_send_buf_d[peer].size(), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -223,7 +239,7 @@ struct multi_gpu_engine_t { f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; ncclRecv(recv_ptr, static_cast(rd.cstr_recv_counts[peer]), - ncclFloat64, + nccl_data_type(), peer, s.comm.get(), s.stream.view().value()); @@ -234,7 +250,7 @@ struct multi_gpu_engine_t { // -------- NCCL allreduce (sum, in place) -------------------------------- // Per-shard in-place sum-allreduce. Each shard's stream issues an - // ncclAllReduce(buf, buf, count, ncclFloat64, ncclSum, ...) inside a single + // ncclAllReduce(buf, buf, count, nccl_data_type(), ncclSum, ...) inside a single // group. After this returns, every shard's buffer holds the global sum. // // PtrAccess: pdlp_solver_t& -> f_t* (e.g. into step_size_strategy_). @@ -245,7 +261,7 @@ struct multi_gpu_engine_t { for (auto& s : shards) { raft::device_setter guard(s->device_id); f_t* buf = ptr_access(*s->sub_pdlp); - ncclAllReduce(buf, buf, count, ncclFloat64, ncclSum, s->comm.get(), s->stream.view().value()); + ncclAllReduce(buf, buf, count, nccl_data_type(), ncclSum, s->comm.get(), s->stream.view().value()); } ncclGroupEnd(); } From 57c860a16dee6433c7de4354b75bd7ce9b3385d8 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 12:05:01 +0200 Subject: [PATCH 107/258] cleaned partition.cpp/hpp and moved kaminpar partitionner into it --- cpp/CMakeLists.txt | 2 +- cpp/src/pdlp/CMakeLists.txt | 3 +- .../distributed_pdlp/kaminpar_partitioner.hpp | 22 ----- ...minpar_partitioner.cpp => partitioner.cpp} | 84 ++++++++++++++++-- cpp/src/pdlp/distributed_pdlp/partitioner.cu | 85 ------------------- cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 15 +++- 6 files changed, 90 insertions(+), 121 deletions(-) delete mode 100644 cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp rename cpp/src/pdlp/distributed_pdlp/{kaminpar_partitioner.cpp => partitioner.cpp} (64%) delete mode 100644 cpp/src/pdlp/distributed_pdlp/partitioner.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 77a6c0473e..076e23f36a 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -593,7 +593,7 @@ add_dependencies(cuopt PSLP) # (, which pulls in stdlib + TBB) at compile time. target_include_directories(cuopt SYSTEM PRIVATE $) -# kaminpar_partitioner.cpp includes . Because KaMinPar is linked by file, cuopt +# partitioner.cpp includes . Because KaMinPar is linked by file, cuopt # does NOT inherit KaMinPar's PUBLIC compile definitions, so we must replicate the ones that # affect what compiles to: # * TBB_PREVIEW_GLOBAL_CONTROL - includes , which older diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 97b5d42b1b..1da0028eea 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -33,8 +33,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/multi_gpu_engine.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/distributed_algorithms.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu - ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cu - ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/kaminpar_partitioner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cpp ) # C and Python adapter files diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp deleted file mode 100644 index 02150f6e61..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.hpp +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -namespace cuopt::linear_programming::detail { - -// Multi-threaded k-way partitioner backed by KaMinPar. Builds a -// constraint/variable bipartite graph and runs the -// shared-memory parallel KaMinPar kernel so partitioning scales across all CPU -// cores of a node -template -class kaminpar_partitioner_t : public partitioner_i { - public: - std::vector partition(partitioner_input_t const& input) const override; -}; - -} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp similarity index 64% rename from cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp rename to cpp/src/pdlp/distributed_pdlp/partitioner.cpp index 41d0eb34b5..71bddb538e 100644 --- a/cpp/src/pdlp/distributed_pdlp/kaminpar_partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp @@ -3,10 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -// Plain C++ translation unit (not .cu): KaMinPar's public header is C++20 host code -// and pulls in TBB; keeping it out of nvcc avoids device-compiler friction. +// Plain C++ translation unit (not .cu): this file contains no device code, and +// KaMinPar's public header () is C++20 host code that pulls in TBB. +// Keeping the whole partitioner implementation out of nvcc avoids +// device-compiler friction. -#include #include #include @@ -15,6 +16,7 @@ #include +#include #include #include #include @@ -23,6 +25,31 @@ namespace cuopt::linear_programming::detail { +template +std::vector dummy_partitioner_t::partition( + partitioner_input_t const& input) const +{ + cuopt_expects(input.nb_parts > 0, + error_type_t::ValidationError, + "dummy_partitioner: nb_parts must be positive"); + cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, + error_type_t::ValidationError, + "dummy_partitioner: invalid problem dimensions"); + + const std::size_t nvtx = + static_cast(input.nb_cstr) + static_cast(input.nb_vars); + std::vector parts(nvtx); + for (std::size_t i = 0; i < nvtx; ++i) { + parts[i] = static_cast(i % static_cast(input.nb_parts)); + } + validate_partition(parts, + static_cast(input.nb_cstr), + static_cast(input.nb_vars), + static_cast(input.nb_parts), + "dummy_partitioner"); + return parts; +} + // Builds the bipartite constraint/variable graph induced by A and runs the // multi-threaded KaMinPar k-way kernel. // * nodes [0, nb_cstr) : constraint nodes @@ -83,7 +110,7 @@ std::vector kaminpar_partitioner_t::partition( std::vector adjncy(2 * static_cast(nnz)); // CSR already represents an adjency list of cstr -> variables. - // Adding the transpose to represent the var -> cstr edges. + // Adding the transpose to represent the var -> cstr edges. // Casting the types to KaMinPar friendly types for (i_t i = 0; i <= nb_cstr; ++i) { xadj[i] = static_cast(A_offsets[i]); @@ -113,11 +140,11 @@ std::vector kaminpar_partitioner_t::partition( // The actual partition computation auto t0 = std::chrono::high_resolution_clock::now(); - + const kaminpar::shm::EdgeWeight edge_cut = engine.compute_partition(std::span(block_of)); - - auto t1 = std::chrono::high_resolution_clock::now(); + + auto t1 = std::chrono::high_resolution_clock::now(); const double dt = std::chrono::duration(t1 - t0).count(); CUOPT_LOG_INFO( @@ -143,6 +170,49 @@ std::vector kaminpar_partitioner_t::partition( return parts; } +void validate_partition( + std::vector const& parts, int nb_cstr, int nb_vars, int nb_parts, char const* context) +{ + const std::size_t expected = + static_cast(nb_cstr) + static_cast(nb_vars); + cuopt_expects(parts.size() == expected, + error_type_t::ValidationError, + "%s: expected %zu part entries (cstrs + vars), got %zu", + context, + expected, + parts.size()); + cuopt_expects( + nb_parts > 0, error_type_t::ValidationError, "%s: nb_parts must be positive", context); + if (parts.empty()) { return; } + const auto [min_it, max_it] = std::minmax_element(parts.begin(), parts.end()); + cuopt_expects(*min_it >= 0, + error_type_t::ValidationError, + "%s: partition ids must be non-negative (min=%d)", + context, + static_cast(*min_it)); + cuopt_expects(*max_it < nb_parts, + error_type_t::ValidationError, + "%s: partition ids must be in [0, %d) (max=%d)", + context, + static_cast(nb_parts), + static_cast(*max_it)); +} + +template +std::unique_ptr> make_partitioner(partitioner_kind_t kind) +{ + switch (kind) { + case partitioner_kind_t::Dummy: return std::make_unique>(); + case partitioner_kind_t::KaMinPar: return std::make_unique>(); + } + cuopt_expects( + false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); + return nullptr; +} + +template class dummy_partitioner_t; template class kaminpar_partitioner_t; +template std::unique_ptr> make_partitioner( + partitioner_kind_t); } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cu b/cpp/src/pdlp/distributed_pdlp/partitioner.cu deleted file mode 100644 index 093c2cb23b..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cu +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include - -#include - -#include -#include - -namespace cuopt::linear_programming::detail { - -template -std::vector dummy_partitioner_t::partition( - partitioner_input_t const& input) const -{ - cuopt_expects(input.nb_parts > 0, - error_type_t::ValidationError, - "dummy_partitioner: nb_parts must be positive"); - cuopt_expects(input.nb_cstr >= 0 && input.nb_vars >= 0, - error_type_t::ValidationError, - "dummy_partitioner: invalid problem dimensions"); - - const std::size_t nvtx = - static_cast(input.nb_cstr) + static_cast(input.nb_vars); - std::vector parts(nvtx); - for (std::size_t i = 0; i < nvtx; ++i) { - parts[i] = static_cast(i % static_cast(input.nb_parts)); - } - validate_partition(parts, - static_cast(input.nb_cstr), - static_cast(input.nb_vars), - static_cast(input.nb_parts), - "dummy_partitioner"); - return parts; -} - -void validate_partition( - std::vector const& parts, int nb_cstr, int nb_vars, int nb_parts, char const* context) -{ - const std::size_t expected = - static_cast(nb_cstr) + static_cast(nb_vars); - cuopt_expects(parts.size() == expected, - error_type_t::ValidationError, - "%s: expected %zu part entries (cstrs + vars), got %zu", - context, - expected, - parts.size()); - cuopt_expects( - nb_parts > 0, error_type_t::ValidationError, "%s: nb_parts must be positive", context); - if (parts.empty()) { return; } - const auto [min_it, max_it] = std::minmax_element(parts.begin(), parts.end()); - cuopt_expects(*min_it >= 0, - error_type_t::ValidationError, - "%s: partition ids must be non-negative (min=%d)", - context, - static_cast(*min_it)); - cuopt_expects(*max_it < nb_parts, - error_type_t::ValidationError, - "%s: partition ids must be in [0, %d) (max=%d)", - context, - static_cast(nb_parts), - static_cast(*max_it)); -} - -template -std::unique_ptr> make_partitioner(partitioner_kind_t kind) -{ - switch (kind) { - case partitioner_kind_t::Dummy: return std::make_unique>(); - case partitioner_kind_t::KaMinPar: return std::make_unique>(); - } - cuopt_expects( - false, error_type_t::RuntimeError, "make_partitioner: unsupported partitioner kind"); - return nullptr; -} - -template class dummy_partitioner_t; -template std::unique_ptr> make_partitioner( - partitioner_kind_t); - -} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index 07db06fe3b..5f0c50c8f7 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -30,13 +30,11 @@ struct partitioner_input_t { i_t nb_vars{0}; i_t nb_parts{0}; // Number of CPU threads the partitioner may use. Only honored by the - // multi-threaded KaMinPar backend; <= 0 means "auto" (all hardware threads). - // Serial backend (Dummy) ignore it. + // multi-threaded KaMinPar backend. <= 0 means "auto" i_t nb_threads{0}; // Constraint matrix A (rows = constraints, cols = variables). csr_host_view_t A{}; - // Transpose A_t (rows = variables, cols = constraints). Optional for partitioners - // that build a bipartite graph (e.g. KaMinPar); dummy partitioner ignores both matrices. + // Transpose A_t (rows = variables, cols = constraints) csr_host_view_t A_t{}; }; @@ -57,6 +55,15 @@ class dummy_partitioner_t : public partitioner_i { std::vector partition(partitioner_input_t const& input) const override; }; +// Multi-threaded k-way partitioner backed by KaMinPar. Builds a +// constraint/variable bipartite graph and runs the shared-memory parallel +// KaMinPar kernel so partitioning scales across all CPU cores of a node. +template +class kaminpar_partitioner_t : public partitioner_i { + public: + std::vector partition(partitioner_input_t const& input) const override; +}; + void validate_partition(std::vector const& parts, int nb_cstr, int nb_vars, From 349a1272864a19a190e3ba719fd257006283b0a1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 12:19:01 +0200 Subject: [PATCH 108/258] review ready pdlp/distributed_pdlp --- cpp/src/pdlp/distributed_pdlp/rank_data.hpp | 1 + cpp/src/pdlp/distributed_pdlp/shard.cu | 57 +++++---------------- cpp/src/pdlp/distributed_pdlp/shard.hpp | 4 +- 3 files changed, 16 insertions(+), 46 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp index 006c52749f..e9c701567a 100644 --- a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp +++ b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp @@ -9,6 +9,7 @@ #include namespace cuopt::linear_programming::detail { +// Pure data class representing most of the distributed data needed for operatiosn template struct rank_data_t { rank_data_t(std::size_t nb_parts) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index aadca28089..1a4f169194 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -18,6 +18,7 @@ namespace cuopt::linear_programming::detail { // This must be done in .cu file because the pdlp_solver_t is not already complete in the hpp file +// This is caused by the problematic cyclic include of pdlp_solver_t template pdlp_shard_t::~pdlp_shard_t() = default; @@ -78,19 +79,6 @@ pdlp_shard_t::pdlp_shard_t(int device_id, h_cstr_upper[i] = g_cstr_upper[g]; } - // Identity scaling: cumulative scaling factors are 1 and the bound / - // objective rescaling scalars are 1. The "scaled" arrays injected below are - // just the unscaled slices. - const std::vector& h_obj_scaled = h_obj; - const std::vector& h_var_lower_scaled = h_var_lower; - const std::vector& h_var_upper_scaled = h_var_upper; - const std::vector& h_cstr_lower_scaled = h_cstr_lower; - const std::vector& h_cstr_upper_scaled = h_cstr_upper; - const std::vector h_cstr_scaling_local(rank_data.total_cstr_size, f_t{1}); - const std::vector h_var_scaling_local(rank_data.total_var_size, f_t{1}); - const f_t h_bound_rescaling = f_t{1}; - const f_t h_objective_rescaling = f_t{1}; - // ---- 2. Build optimization_problem_t on this shard's device (UNSCALED). ---- opt_problem.emplace(&handle); opt_problem->set_csr_constraint_matrix(rank_data.h_A_values.data(), @@ -114,7 +102,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, opt_problem->set_objective_scaling_factor(objective_scaling_factor); opt_problem->set_problem_category(problem_category_t::LP); - // ---- 3. Build problem_t from opt_problem (still UNSCALED). ---- + // ---- 3. Build problem_t from opt_problem (UNSCALED). ---- sub_problem.emplace(*opt_problem); // ---- 4. Override reverse_* with the real local A_T from rank_data. ---- @@ -145,9 +133,8 @@ pdlp_shard_t::pdlp_shard_t(int device_id, sub_pdlp->pdhg_solver_.set_is_multi_gpu(true); - // Re-inject master-scaled buffers inside sub_pdlp. - // Need to also re-inject the offsets and variables arrays to revert - // the csrsort done by problem_t's constructor. + // Inject this shard's unscaled buffers into op_problem_scaled (distributed + // scaling runs later and will scale them). auto& scaled = sub_pdlp->get_op_problem_scaled(); raft::copy(scaled.offsets.data(), rank_data.h_A_row_offsets.data(), @@ -161,48 +148,32 @@ pdlp_shard_t::pdlp_shard_t(int device_id, rank_data.h_A_values.data(), rank_data.h_A_values.size(), stream_view); - // A_T side: all three arrays were already overridden together from - // rank_data on sub_problem (see step 4 above) and deep-copied into the - // scaled problem, so reverse_offsets / reverse_constraints already match - // h_A_t_values's order. At this stage distributed initial scaling starts from - // identity, so the matrix values are injected from the unscaled host slices. raft::copy(scaled.reverse_coefficients.data(), rank_data.h_A_t_values.data(), rank_data.h_A_t_values.size(), stream_view); + raft::copy(scaled.objective_coefficients.data(), h_obj.data(), h_obj.size(), stream_view); raft::copy( - scaled.objective_coefficients.data(), h_obj_scaled.data(), h_obj_scaled.size(), stream_view); - raft::copy(scaled.constraint_lower_bounds.data(), - h_cstr_lower_scaled.data(), - h_cstr_lower_scaled.size(), - stream_view); - raft::copy(scaled.constraint_upper_bounds.data(), - h_cstr_upper_scaled.data(), - h_cstr_upper_scaled.size(), - stream_view); + scaled.constraint_lower_bounds.data(), h_cstr_lower.data(), h_cstr_lower.size(), stream_view); + raft::copy( + scaled.constraint_upper_bounds.data(), h_cstr_upper.data(), h_cstr_upper.size(), stream_view); using f_t2 = typename type_2::type; - std::vector h_var_bounds_scaled_packed(rank_data.total_var_size); + std::vector h_var_bounds_packed(rank_data.total_var_size); for (i_t i = 0; i < rank_data.total_var_size; ++i) { - h_var_bounds_scaled_packed[i].x = h_var_lower_scaled[i]; - h_var_bounds_scaled_packed[i].y = h_var_upper_scaled[i]; + h_var_bounds_packed[i].x = h_var_lower[i]; + h_var_bounds_packed[i].y = h_var_upper[i]; } raft::copy(scaled.variable_bounds.data(), - h_var_bounds_scaled_packed.data(), - h_var_bounds_scaled_packed.size(), + h_var_bounds_packed.data(), + h_var_bounds_packed.size(), stream_view); combine_constraint_bounds(scaled, scaled.combined_bounds); - // Inject master-scaled buffers inside sub_pdlp.initil_strategy - auto& scaling = sub_pdlp->get_initial_scaling_strategy(); - scaling.set_cummulative_scaling(h_cstr_scaling_local, h_var_scaling_local); - scaling.set_h_bound_rescaling(h_bound_rescaling); - scaling.set_h_objective_rescaling(h_objective_rescaling); - sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( /* is_reflected */ true); - // ---- 6. Build per-peer halo-exchange plans (ported from metis_tests). ---- + // ---- 6. Build per-peer halo-exchange plans ---- // For each peer p, we precompute: // send_indices_d[p] : local indices to gather (uploaded from host send plan) // send_buf_d[p] : f_t staging buffer sized to match diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index e78516eb36..8749e286ef 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -57,7 +57,7 @@ struct pdlp_shard_t { pdlp_shard_t(const pdlp_shard_t&) = delete; pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; - // Move ops are implicitly deleted (user-declared dtor + deleted copy). + // Move ops are implicitly deleted // Intentional: shard owns device-affine resources and must never move. // Store as std::unique_ptr in any container. @@ -70,8 +70,6 @@ struct pdlp_shard_t { std::optional> sub_problem; std::unique_ptr> sub_pdlp; - // Per-peer halo-exchange state. Inner index = peer rank. - // Slot for self (peer == this rank) is present but unused (size 0). // var_send_indices_d[peer] : local indices into primal vector to gather and ncclSend // var_send_buf_d [peer] : staging buffer for outgoing variable values // cstr_send_indices_d/cstr_send_buf_d : same, for dual vector From f40bcfed336e1338cd5d25f1b77bc787e78d9bc8 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 12:26:12 +0200 Subject: [PATCH 109/258] updated solver settings comments --- .../cuopt/linear_programming/pdlp/solver_settings.hpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index 12c29f681b..c38ff7f026 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -313,15 +313,12 @@ class pdlp_solver_settings_t { std::string multi_gpu_partition_file{""}; // If non-empty, the partition computed for distributed PDLP is written to this // path (one part-id per line) right after partitioning. The file can be fed - // back via multi_gpu_partition_file. Exposed as the multi_gpu_export_partition_file - // parameter (CLI: --multi-gpu-export-partition-file ). + // back via multi_gpu_partition_file. std::string multi_gpu_export_partition_file{""}; // Which graph partitioner distributed PDLP uses. One of: // "auto" - 1 GPU => Dummy; otherwise KaMinPar // "dummy" - round-robin, no graph (trivial) // "kaminpar" - multi-threaded KaMinPar - // Exposed as the distributed_pdlp_partitioner parameter - // (CLI: --distributed-pdlp-partitioner ). std::string distributed_pdlp_partitioner{"auto"}; // Set to true inside the shards bool is_distributed_sub_pdlp{false}; From 710365564947f892947ef05b2d5af597be195fc0 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 12:29:34 +0200 Subject: [PATCH 110/258] removed mgpu_trace --- cpp/src/pdlp/utilities/mgpu_trace.cuh | 52 --------------------------- 1 file changed, 52 deletions(-) delete mode 100644 cpp/src/pdlp/utilities/mgpu_trace.cuh diff --git a/cpp/src/pdlp/utilities/mgpu_trace.cuh b/cpp/src/pdlp/utilities/mgpu_trace.cuh deleted file mode 100644 index d9975d3202..0000000000 --- a/cpp/src/pdlp/utilities/mgpu_trace.cuh +++ /dev/null @@ -1,52 +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 - -// Lightweight env-gated tracing for multi-GPU PDLP diagnosis. -// -// Enable by setting CUOPT_MGPU_TRACE=1 in the environment. -// All prints go to stderr (line-buffered + explicit flush) so they survive -// a CUDA hang and interleave with cuOpt's normal output. -// -// Usage: -// MGPU_TRACE("entering compute_At_y"); -// MGPU_TRACE_FMT("shard %d nnz=%lld", r, (long long)nnz); -// -// The guard reads the env var once on first use (thread-safe via static -// initialization) and the cost when disabled is a single load + branch. - -#include -#include - -namespace cuopt::linear_programming::detail { - -inline bool mgpu_trace_enabled() -{ - static const bool enabled = []() { - const char* v = std::getenv("CUOPT_MGPU_TRACE"); - return v != nullptr && v[0] != '\0' && v[0] != '0'; - }(); - return enabled; -} - -} // namespace cuopt::linear_programming::detail - -#define MGPU_TRACE(msg) \ - do { \ - if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ - std::fprintf(stderr, "[mgpu %s:%d] %s\n", __func__, __LINE__, (msg)); \ - std::fflush(stderr); \ - } \ - } while (0) - -#define MGPU_TRACE_FMT(fmt, ...) \ - do { \ - if (::cuopt::linear_programming::detail::mgpu_trace_enabled()) { \ - std::fprintf(stderr, "[mgpu %s:%d] " fmt "\n", __func__, __LINE__, __VA_ARGS__); \ - std::fflush(stderr); \ - } \ - } while (0) From 80fb18812ef12a1c304ea96edc2c290076a31c93 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 12:29:48 +0200 Subject: [PATCH 111/258] remove comment in saddle_point.cu --- cpp/src/pdlp/saddle_point.cu | 3 --- 1 file changed, 3 deletions(-) diff --git a/cpp/src/pdlp/saddle_point.cu b/cpp/src/pdlp/saddle_point.cu index 07a5d0146e..6e0a836c4c 100644 --- a/cpp/src/pdlp/saddle_point.cu +++ b/cpp/src/pdlp/saddle_point.cu @@ -38,9 +38,6 @@ saddle_point_state_t::saddle_point_state_t( current_AtY_{batch_size * primal_size, handle_ptr->get_stream()}, next_AtY_{batch_size * primal_size, handle_ptr->get_stream()} { - // >= 0 (not > 0): distributed PDLP builds the master pdlp_solver_t from a - // shape-0 placeholder problem so the master never materializes per-variable - // / per-constraint vectors; size-0 device_uvectors are valid throughout. EXE_CUOPT_EXPECTS(primal_size >= 0, "Size of the primal problem must be non-negative"); EXE_CUOPT_EXPECTS(dual_size >= 0, "Size of the dual problem must be non-negative"); From e680422adf3fa5081f828e8434aa52b1b61832e2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 15:04:11 +0200 Subject: [PATCH 112/258] cleaned pdhg.hpp and removed is_multi_gpu flag --- cpp/src/pdlp/distributed_pdlp/shard.cu | 4 +-- cpp/src/pdlp/pdhg.hpp | 34 +++++-------------- .../convergence_information.cu | 15 +++----- 3 files changed, 14 insertions(+), 39 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 1a4f169194..3a8257a3eb 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -127,12 +127,10 @@ pdlp_shard_t::pdlp_shard_t(int device_id, stream_view); handle.sync_stream(stream_view); - // ---- 5. Build sub_pdlp (single-GPU mode; multi_gpu flags cleared by caller). ---- + // ---- 5. Build sub_pdlp (single-GPU mode). ---- sub_pdlp = std::make_unique>( *sub_problem, settings, /*is_legacy_batch_mode=*/false); - sub_pdlp->pdhg_solver_.set_is_multi_gpu(true); - // Inject this shard's unscaled buffers into op_problem_scaled (distributed // scaling runs later and will scale them). auto& scaled = sub_pdlp->get_op_problem_scaled(); diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index e4d16360a7..d9f03735f5 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -83,8 +83,7 @@ class pdhg_solver_t { void refine_initial_primal_projection(const rmm::device_uvector& bound_rescaling); // SpMV primitives. Public so the multi-GPU engine can drive them per-shard - // after halo-exchanging the relevant vector. Single-GPU PDLP still calls - // them internally via take_step / compute_next_*. + // after halo-exchanging the relevant vector. // // If set_multi_gpu_engine() has been called, these dispatch to the engine // (halo exchange + per-shard SpMV). Otherwise they run the single-GPU @@ -95,38 +94,24 @@ class pdhg_solver_t { void spmvop_A_x(); // Parameterized SpMVs used by the multi-GPU engine. - // Both temporarily hijack a canonical input descriptor in cusparse_view_ - // (cv.dual_solution for At, cv.reflected_primal_solution for A) to point at - // `in_buf.data()`, run the local SpMV into `out_desc`, then restore the + // Both temporarily hijack a canonical input descriptor in cusparse_view_,run the local SpMV into `out_desc`, then restore the // descriptor to its original buffer so other code on this shard is unaffected. // No multi-GPU dispatch inside — the engine is the orchestrator. void spmv_At_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); void spmv_A_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); - // Pure cub-transform extractions. Each one is byte-identical to the inline - // cub call it replaces — no platform dispatch inside. Callers handle the - // single-GPU vs per-shard branching at the call site (see the - // "if (mgpu_engine_) for shard..." blocks in compute_next_*). + // Pure cub-transform extractions. Allows for clearer containment of the calls and ensures + // the single-GPU vs distributed-GPU uses the same calls void primal_reflected_major_projection_transform(rmm::device_uvector& primal_step_size); void dual_reflected_major_projection_transform(rmm::device_uvector& dual_step_size); void primal_reflected_projection_transform(rmm::device_uvector& primal_step_size); void dual_reflected_projection_transform(rmm::device_uvector& dual_step_size); // Master PDLP wires up the engine pointer here after the engine is built. - // Shards' pdhg_solver_ leaves this null so each shard runs single-GPU SpMV - // on its local matrix. Also flips is_multi_gpu_ — convenience flag that any - // pdhg participating in a distributed run (master OR shard) carries true. - void set_multi_gpu_engine(multi_gpu_engine_t* engine) - { - mgpu_engine_ = engine; - is_multi_gpu_ = (engine != nullptr); - } - - // Mark a shard's pdhg_solver_ as part of a distributed run without giving it - // an engine (shards don't orchestrate; they only run local SpMV on owned - // rows). Called from shard.cu right after sub_pdlp is constructed. - void set_is_multi_gpu(bool v) { is_multi_gpu_ = v; } - bool is_multi_gpu() const { return is_multi_gpu_; } + // Only the master's pdhg_solver_ holds a non-null engine; shards leave it + // null and run single-GPU SpMV on their local matrix. The engine pointer is + // the single source of truth for wether the code is distributed PDLP or not + void set_multi_gpu_engine(multi_gpu_engine_t* engine) { mgpu_engine_ = engine; } multi_gpu_engine_t* get_mgpu_engine() const { return mgpu_engine_; } i_t total_pdhg_iterations_; @@ -148,7 +133,6 @@ class pdhg_solver_t { void compute_primal_projection(rmm::device_uvector& primal_step_size); bool batch_mode_{false}; - bool is_multi_gpu_{false}; raft::handle_t const* handle_ptr_{nullptr}; rmm::cuda_stream_view stream_view_; @@ -202,7 +186,7 @@ class pdhg_solver_t { cuda::fast_mod_div batch_size_divisor_; // Non-owning. Set on the master pdhg_solver_ in distributed mode; null - // (default) means single-GPU path. See compute_At_y / compute_A_x. + // (default) means single-GPU path. multi_gpu_engine_t* mgpu_engine_{nullptr}; }; diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 1dfc8229da..355d1b6c4c 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -491,10 +491,7 @@ void convergence_information_t::compute_convergence_information( print("dual_slack", dual_slack); #endif - if (current_pdhg_solver.is_multi_gpu()) { - auto* engine = current_pdhg_solver.get_mgpu_engine(); - cuopt_assert(engine != nullptr, - "mGPU branch reached but current_pdhg_solver has no engine (shard pdhg?)"); + if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { cuopt_expects(!settings.per_constraint_residual, error_type_t::ValidationError, "per_constraint_residual is not yet supported in multi-GPU mode"); @@ -547,8 +544,7 @@ void convergence_information_t::compute_convergence_information( #endif // L2 Norm - if (current_pdhg_solver.is_multi_gpu()) { - auto* engine = current_pdhg_solver.get_mgpu_engine(); + if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { engine->distributed_l2_norm( [](pdlp_solver_t& sp) -> rmm::device_uvector& { return sp.get_current_termination_strategy().get_convergence_information().primal_residual_; @@ -613,9 +609,7 @@ void convergence_information_t::compute_convergence_information( std::numeric_limits::lowest()); } - if (current_pdhg_solver.is_multi_gpu()) { - auto* engine = current_pdhg_solver.get_mgpu_engine(); - + if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { // 1) Halo-exchange potential_next_dual_solution on every shard so the // A_T_shard @ y SpMV inside compute_dual_residual reads correct values // in the cstr halo region. The SpMV is driven through the eval view's @@ -687,10 +681,9 @@ void convergence_information_t::compute_convergence_information( print("Dual Residual", dual_residual_); #endif - if (current_pdhg_solver.is_multi_gpu()) { + if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { // Multi-GPU dual residual L2 norm: same pattern as the primal L2 above, // but the dual residual is var-shaped so we clip to owned_var_size. - auto* engine = current_pdhg_solver.get_mgpu_engine(); engine->distributed_l2_norm( [](pdlp_solver_t& sp) -> rmm::device_uvector& { return sp.get_current_termination_strategy().get_convergence_information().dual_residual_; From 6000b7594ed8a635eb659b8e46d80bcc1695f84a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 15:29:41 +0200 Subject: [PATCH 113/258] cleaner distributed handling --- cpp/src/pdlp/pdhg.cu | 24 ++++++++++++------------ cpp/src/pdlp/pdhg.hpp | 13 +++++++++---- cpp/src/pdlp/pdlp.cu | 16 ++++++++-------- cpp/src/pdlp/pdlp.cuh | 5 +++++ 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index b1f1a59ada..e4a466bd43 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -515,7 +515,7 @@ void pdhg_solver_t::compute_At_y() // exchange + per-shard SpMV via the engine. Shards' pdhg_solver_ have no // engine pointer set, so their compute_At_y falls through to the cusparse // path below on each shard's local A_t. - if (mgpu_engine_ != nullptr) { + if (is_distributed_master()) { mgpu_engine_->distributed_compute_At_y(); return; } @@ -573,7 +573,7 @@ void pdhg_solver_t::compute_A_x() // Multi-GPU dispatch: see compute_At_y. The engine halo-updates the // reflected_primal vector (the buffer this SpMV reads) and then drives // per-shard local cusparse SpMV. - if (mgpu_engine_ != nullptr) { + if (is_distributed_master()) { mgpu_engine_->distributed_compute_A_x(); return; } @@ -1245,7 +1245,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( using f_t2 = typename type_2::type; - if (mgpu_engine_ != nullptr) { mgpu_engine_->sync_await_shards(stream_view_); } + if (is_distributed_master()) { mgpu_engine_->sync_await_shards(stream_view_); } // Compute next primal solution reflected. @@ -1257,10 +1257,10 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // the capture or run outside the graph, leaving the captured graph // empty (or broken) -- which produces the cycling/stall behavior we // observed on larger problems. Mirrors metis_tests bench.cu fork/join. - if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } + if (is_distributed_master()) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } compute_At_y(); - if (mgpu_engine_ != nullptr) { + if (is_distributed_master()) { for (auto& shard : mgpu_engine_->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; @@ -1327,7 +1327,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // Compute next dual compute_A_x(); - if (mgpu_engine_ != nullptr) { + if (is_distributed_master()) { for (auto& shard : mgpu_engine_->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; @@ -1360,12 +1360,12 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // Multi-GPU: close the fork by joining every shard stream back into // the master stream so cudaStreamEndCapture sees a single graph // spanning all streams. - if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_join_from_shards(stream_view_); } + if (is_distributed_master()) { mgpu_engine_->graph_capture_join_from_shards(stream_view_); } }); } else { graph_all.run(should_major, [&]() { - if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } + if (is_distributed_master()) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } // Compute next primal compute_At_y(); @@ -1378,7 +1378,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( current_saddle_point_state_.get_current_AtY()); #endif - if (mgpu_engine_ != nullptr) { + if (is_distributed_master()) { for (auto& shard : mgpu_engine_->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; @@ -1446,7 +1446,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( // Compute next dual compute_A_x(); - if (mgpu_engine_ != nullptr) { + if (is_distributed_master()) { for (auto& shard : mgpu_engine_->shards) { raft::device_setter guard(shard->device_id); auto& sub_pdlp = *shard->sub_pdlp; @@ -1472,12 +1472,12 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( print("reflected_dual_", reflected_dual_); #endif - if (mgpu_engine_ != nullptr) { mgpu_engine_->graph_capture_join_from_shards(stream_view_); } + if (is_distributed_master()) { mgpu_engine_->graph_capture_join_from_shards(stream_view_); } }); } // sync to master stream after the graph is captured - if (mgpu_engine_ != nullptr) { mgpu_engine_->sync_await_master(stream_view_); } + if (is_distributed_master()) { mgpu_engine_->sync_await_master(stream_view_); } } template diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index d9f03735f5..547a0da504 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -107,13 +107,18 @@ class pdhg_solver_t { void primal_reflected_projection_transform(rmm::device_uvector& primal_step_size); void dual_reflected_projection_transform(rmm::device_uvector& dual_step_size); - // Master PDLP wires up the engine pointer here after the engine is built. - // Only the master's pdhg_solver_ holds a non-null engine; shards leave it - // null and run single-GPU SpMV on their local matrix. The engine pointer is - // the single source of truth for wether the code is distributed PDLP or not + // Master PDLP wires the engine pointer here after the engine is built. Only + // the master's pdhg_solver_ holds a non-null engine; shards leave it null and + // run single-GPU SpMV on their local matrix. void set_multi_gpu_engine(multi_gpu_engine_t* engine) { mgpu_engine_ = engine; } multi_gpu_engine_t* get_mgpu_engine() const { return mgpu_engine_; } + // True only on the master pdhg of a distributed run (the one wired to the + // engine, which orchestrates the shards). + // Shards report false. + // Single-GPU PDHG reports false. + bool is_distributed_master() const { return mgpu_engine_ != nullptr; } + i_t total_pdhg_iterations_; private: diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 29bace13aa..66528df10f 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2331,7 +2331,7 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte // Computing the deltas // TODO batch mdoe: this only works if everyone restarts - if (multi_gpu_engine) { + if (is_distributed_master()) { // Go faire une fonction compute_delta_primal, compute_delta primal ? for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); @@ -2367,7 +2367,7 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte auto& cusparse_view = pdhg_solver_.get_cusparse_view(); - if (multi_gpu_engine) { + if (is_distributed_master()) { // SpMV is the first operation in compute_interaction_and_movement so we can do halo before and // call it naturally we then reduce the local dot products multi_gpu_engine->halo_exchange_cstr( @@ -2468,7 +2468,7 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); // Put back, already done in multi-gpu side - if (!multi_gpu_engine) { + if (!is_distributed_master()) { RAFT_CUSPARSE_TRY( cusparseDnVecSetValues(cusparse_view.potential_next_dual_solution, (void*)pdhg_solver_.get_potential_next_dual_solution().data())); @@ -3000,7 +3000,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co // 1. At the very beginning of the solver, when no steps have been taken yet // 2. After a single step, since average of one step is the same step if (internal_solver_iterations_ <= 1) { - cuopt_expects(!multi_gpu_engine.has_value(), + cuopt_expects(!is_distributed_master(), error_type_t::RuntimeError, "Distributed PDLP does not support average restart; run with " "never_restart_to_average = true."); @@ -3057,7 +3057,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co initial_scaling_strategy_.unscale_solutions(pdhg_solver_.get_primal_solution(), pdhg_solver_.get_dual_solution()); } else { - if (multi_gpu_engine) { + if (is_distributed_master()) { // The only branch in cuPDLPx (Stable3) multi_gpu_engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; @@ -3103,7 +3103,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co initial_scaling_strategy_.scale_solutions(pdhg_solver_.get_primal_solution(), pdhg_solver_.get_dual_solution()); } else { - if (multi_gpu_engine) { + if (is_distributed_master()) { // The only branch in cuPDLPx (Stable3) multi_gpu_engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; @@ -3215,7 +3215,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co transpose_problem_fields(/*to_row=*/true); } } - if (multi_gpu_engine) { + if (is_distributed_master()) { multi_gpu_engine->for_each_shard([&](auto& shard) { shard.sub_pdlp->halpern_update(); }); } else { halpern_update(); @@ -3226,7 +3226,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co ++internal_solver_iterations_; if (settings_.hyper_params.never_restart_to_average) { restart_strategy_.increment_iteration_since_last_restart(); - if (multi_gpu_engine) { + if (is_distributed_master()) { multi_gpu_engine->for_each_shard([&](auto& shard) { shard.sub_pdlp->restart_strategy_.increment_iteration_since_last_restart(); }); diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 3544de89fa..3e8c10299d 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -215,6 +215,11 @@ class pdlp_solver_t { detail::pdhg_solver_t pdhg_solver_; void halpern_update(); + // This solver is the distributed-PDLP master orchestrator iff it owns the + // multi-GPU engine. Shards (sub-solvers) leave the optional empty -> false. + // Single-GPU PDLP reports false. + bool is_distributed_master() const { return multi_gpu_engine.has_value(); } + private: void compute_fixed_error(std::vector& has_restarted); From 8c67f900bfc1e0ef940d1469426f1e5e4106b800 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 15:39:00 +0200 Subject: [PATCH 114/258] removed unused disrtibuted spmv in multi_gpu_engine --- .../distributed_pdlp/multi_gpu_engine.hpp | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index a312cb1327..76c7b9a835 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -310,42 +310,9 @@ struct multi_gpu_engine_t { }); } - // -------- Generic distributed SpMVs ------------------------------------- - // distributed_spmv_A : halo-update the var-shaped input buffer returned by - // `in_buf(pdhg)`, then per-shard A @ in_buf -> out_desc. - // distributed_spmv_At: halo-update the cstr-shaped input buffer returned by - // `in_buf(pdhg)`, then per-shard A_T @ in_buf -> out_desc. - // - // Accessor signatures: - // in_buf (pdhg_solver_t&) -> rmm::device_uvector& - // out_desc(pdhg_solver_t&) -> cusparseDnVecDescr_t - template - void distributed_spmv_A(InBufAccess&& in_buf, OutDescAccess&& out_desc) - { - halo_exchange_var(in_buf); - for_each_shard([&](auto& shard) { - auto& sub_pdhg = shard.sub_pdlp->pdhg_solver_; - sub_pdhg.spmv_A_into(in_buf(sub_pdhg), out_desc(sub_pdhg)); - }); - } - - template - void distributed_spmv_At(InBufAccess&& in_buf, OutDescAccess&& out_desc) - { - halo_exchange_cstr(in_buf); - for_each_shard([&](auto& shard) { - auto& sub_pdhg = shard.sub_pdlp->pdhg_solver_; - sub_pdhg.spmv_At_into(in_buf(sub_pdhg), out_desc(sub_pdhg)); - }); - } - // -------- High-level: A @ x and A_T @ y --------------------------------- - // Distributed counterpart to pdhg_solver_t::compute_A_x() - // We don't use distributed_spmv_A() because we are using SpMVOp rather than SpMV + // Distributed counterpart to pdhg_solver_t::compute_A_x() / compute_At_y(). void distributed_compute_A_x(); - - // Distributed counterpart to pdhg_solver_t::compute_At_y() - // We don't use distributed_spmv_At() because we are using SpMVOp rather than SpMV void distributed_compute_At_y(); // Engine-level stream for fork/join orchestration (master side). From 1f42904f90077bceb320bf496a524da9e738ddfc Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 15:43:08 +0200 Subject: [PATCH 115/258] cleaned pdhg a bit more --- cpp/src/pdlp/pdhg.cu | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index e4a466bd43..8915a859d4 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -5,9 +5,6 @@ */ /* clang-format on */ #include -// pdlp.cuh defines pdlp_solver_t which the engine's compute_A_x/compute_At_y -// template bodies dereference via shard.sub_pdlp->pdhg_solver_. Must be a -// complete type at the point of template instantiation below. #include #include #include @@ -512,9 +509,7 @@ void pdhg_solver_t::compute_At_y() // A_t @ y // Multi-GPU dispatch: when the master pdhg has an engine, drive halo - // exchange + per-shard SpMV via the engine. Shards' pdhg_solver_ have no - // engine pointer set, so their compute_At_y falls through to the cusparse - // path below on each shard's local A_t. + // exchange + per-shard SpMV via the engine. if (is_distributed_master()) { mgpu_engine_->distributed_compute_At_y(); return; @@ -623,48 +618,48 @@ void pdhg_solver_t::compute_A_x() } } +// out_desc = A^T @ in_buf, on this shard's local matrix. in_buf is an arbitrary +// caller-owned (constraint/dual-shaped) buffer; we wrap it in a throwaway dense +// descriptor instead of hijacking a canonical solution descriptor, so no shared +// cusparse_view_ state is mutated. Used by the distributed max-singular-value +// power iteration to SpMV scratch buffers. template void pdhg_solver_t::spmv_At_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc) { - RAFT_CUSPARSE_TRY(cusparseDnVecSetValues(cusparse_view_.dual_solution, in_buf.data())); + cusparse_dn_vec_descr_wrapper_t in_vec; + in_vec.create(in_buf.size(), in_buf.data()); RAFT_CUSPARSE_TRY(raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), CUSPARSE_OPERATION_NON_TRANSPOSE, reusable_device_scalar_value_1_.data(), cusparse_view_.A_T, - cusparse_view_.dual_solution, + in_vec, reusable_device_scalar_value_0_.data(), out_desc, CUSPARSE_SPMV_CSR_ALG2, (f_t*)cusparse_view_.buffer_transpose.data(), stream_view_)); - // Restore the canonical binding so subsequent code on this shard that reads - // cv.dual_solution sees the dual_solution_ buffer it was constructed with. - RAFT_CUSPARSE_TRY(cusparseDnVecSetValues(cusparse_view_.dual_solution, - current_saddle_point_state_.get_dual_solution().data())); } +// out_desc = A @ in_buf, the spmv_A_into counterpart of spmv_At_into: wraps the +// arbitrary (variable/primal-shaped) in_buf in a throwaway descriptor. template void pdhg_solver_t::spmv_A_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc) { - RAFT_CUSPARSE_TRY( - cusparseDnVecSetValues(cusparse_view_.reflected_primal_solution, in_buf.data())); + cusparse_dn_vec_descr_wrapper_t in_vec; + in_vec.create(in_buf.size(), in_buf.data()); RAFT_CUSPARSE_TRY( raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), CUSPARSE_OPERATION_NON_TRANSPOSE, reusable_device_scalar_value_1_.data(), cusparse_view_.A, - cusparse_view_.reflected_primal_solution, + in_vec, reusable_device_scalar_value_0_.data(), out_desc, CUSPARSE_SPMV_CSR_ALG2, (f_t*)cusparse_view_.buffer_non_transpose.data(), stream_view_)); - // Restore the canonical binding so subsequent code on this shard that reads - // cv.reflected_primal_solution sees the reflected_primal_ buffer. - RAFT_CUSPARSE_TRY( - cusparseDnVecSetValues(cusparse_view_.reflected_primal_solution, reflected_primal_.data())); } template From 30319a040e396fbd630f310385be704ac32ba0e3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 15:48:17 +0200 Subject: [PATCH 116/258] pdhg review ready --- cpp/src/pdlp/pdhg.cu | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index 8915a859d4..27c89f6f04 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -756,10 +756,6 @@ struct primal_reflected_major_projection { const f_t* scalar_; }; -// Pure cub-transform extract — body byte-identical to the non-batch inline -// path in compute_next_primal_dual_solution_reflected. The platform dispatch -// (single-GPU vs per-shard fan-out) lives at the call site, not here. -// Placed after primal_reflected_major_projection so the functor is visible. template void pdhg_solver_t::primal_reflected_major_projection_transform( rmm::device_uvector& primal_step_size) @@ -1246,22 +1242,17 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( if (should_major) { graph_all.run(should_major, [&]() { - // Multi-GPU: splice shard streams into the capture so their kernels and - // NCCL collectives are recorded into the same graph. Without this, work - // issued on shard.stream from inside this lambda would either invalidate - // the capture or run outside the graph, leaving the captured graph - // empty (or broken) -- which produces the cycling/stall behavior we - // observed on larger problems. Mirrors metis_tests bench.cu fork/join. + // Adds all the shards streams into the graph capture if (is_distributed_master()) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } compute_At_y(); + if (is_distributed_master()) { - for (auto& shard : mgpu_engine_->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdlp = *shard->sub_pdlp; + mgpu_engine_->for_each_shard([](auto& shard) { + auto& sub_pdlp = *shard.sub_pdlp; sub_pdlp.pdhg_solver_.primal_reflected_major_projection_transform( sub_pdlp.get_primal_step_size()); - } + }); } else if (!batch_mode_) { primal_reflected_major_projection_transform(primal_step_size); } else { @@ -1323,12 +1314,11 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( compute_A_x(); if (is_distributed_master()) { - for (auto& shard : mgpu_engine_->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdlp = *shard->sub_pdlp; + mgpu_engine_->for_each_shard([](auto& shard) { + auto& sub_pdlp = *shard.sub_pdlp; sub_pdlp.pdhg_solver_.dual_reflected_major_projection_transform( sub_pdlp.get_dual_step_size()); - } + }); } else if (!batch_mode_) { dual_reflected_major_projection_transform(dual_step_size); } else { @@ -1360,6 +1350,7 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( } else { graph_all.run(should_major, [&]() { + // Same reason as above, adds all the shards streams into the graph capture if (is_distributed_master()) { mgpu_engine_->graph_capture_fork_to_shards(stream_view_); } // Compute next primal @@ -1374,12 +1365,11 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( #endif if (is_distributed_master()) { - for (auto& shard : mgpu_engine_->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdlp = *shard->sub_pdlp; + mgpu_engine_->for_each_shard([](auto& shard) { + auto& sub_pdlp = *shard.sub_pdlp; sub_pdlp.pdhg_solver_.primal_reflected_projection_transform( sub_pdlp.get_primal_step_size()); - } + }); } else if (!batch_mode_) { primal_reflected_projection_transform(primal_step_size); } else { @@ -1442,11 +1432,10 @@ void pdhg_solver_t::compute_next_primal_dual_solution_reflected( compute_A_x(); if (is_distributed_master()) { - for (auto& shard : mgpu_engine_->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdlp = *shard->sub_pdlp; + mgpu_engine_->for_each_shard([](auto& shard) { + auto& sub_pdlp = *shard.sub_pdlp; sub_pdlp.pdhg_solver_.dual_reflected_projection_transform(sub_pdlp.get_dual_step_size()); - } + }); } else if (!batch_mode_) { dual_reflected_projection_transform(dual_step_size); } else { From 2a14a4d306584a65bb57ba3a8df52dd203a9f04c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 15:51:21 +0200 Subject: [PATCH 117/258] finished cuopt_cli --- cpp/cuopt_cli.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 068a76dce1..f788b36012 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -179,8 +179,6 @@ int run_single_file(const std::string& file_path, auto& lp_settings = settings.get_pdlp_settings(); if (lp_settings.hyper_params.use_distributed_pdlp) { - // handle_ptr is only created for the GPU memory backend (see above). Distributed PDLP is - // GPU-only. cuopt::cuopt_expects( handle_ptr != nullptr, cuopt::error_type_t::ValidationError, @@ -434,10 +432,9 @@ int main(int argc, char* argv[]) std::vector memory_resources; if (memory_backend == cuopt::linear_programming::memory_backend_t::GPU) { - // Distributed PDLP scales one shard per GPU and uses its own knob; everything else - // (concurrent, batch, MIP) uses num_gpus which is capped at 2. - // For distributed PDLP, -1 means "auto-detect": resolve to the visible device - // count so the RMM memory pools match what solve.cu will eventually dispatch. + // Get the right number of GPUs + // Distributed PDLP uses its own knob: distributed_pdlp_num_gpus + // Everything else uses num_gpus which is capped at 2 const bool use_distributed_pdlp = settings.get_parameter(CUOPT_USE_DISTRIBUTED_PDLP); int requested_gpus = use_distributed_pdlp ? settings.get_parameter(CUOPT_DISTRIBUTED_PDLP_NUM_GPUS) From 9052e3167a25b42fc28fb8b28b2f600cf0f1bd64 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 16:02:39 +0200 Subject: [PATCH 118/258] cleaned initial_scaling --- cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 2ca7a5fc23..65a7880dcf 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -211,13 +211,7 @@ __global__ void inf_norm_row_kernel( } } -// Column inf-norm of the scaled matrix, over the TRANSPOSE A_T: each column is -// reduced from its own nonzeros. Computing it from A_T (instead of the -// row-major matrix) makes every OWNED column complete in distributed PDLP -// without any cross-shard reduction (halo columns have no A_T rows -> 0), -// mirroring pock_chambolle_scaling_kernel_col. The scaled value uses the same -// operands and order as the row kernel, so results are identical to the fused -// single-GPU computation (max is exact in floating point). +// Column inf-norm of the scaled matrix template __global__ void inf_norm_col_kernel( const typename problem_t::view_t op_problem, From 26d7f9e94373b8ae892d97464aa76dab94a2d028 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 16:55:27 +0200 Subject: [PATCH 119/258] pdlp and solve.cu --- cpp/src/pdlp/pdlp.cu | 63 +++++++++++++++++++----------------------- cpp/src/pdlp/solve.cuh | 44 +++++++++++++++-------------- 2 files changed, 52 insertions(+), 55 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 66528df10f..31bf5d9420 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -568,8 +568,7 @@ pdlp_solver_t::pdlp_solver_t( } // Distributed scaling. Each pass keeps the halo copies of both cumulative - // scalings refreshed internally (owner -> halo broadcast), so no extra halo - // push is needed here. + // scalings refreshed internally (owner -> halo broadcast) if (settings_.hyper_params.do_ruiz_scaling) { distributed_ruiz_inf_scaling( *multi_gpu_engine, settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); @@ -2303,6 +2302,26 @@ void pdlp_solver_t::resize_and_swap_all_context_loop( RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); } +// delta = reflected - current, for both primal and dual, written into the +// saddle-point delta buffers. Shared by the single-GPU and per-shard +// (distributed) paths so the two only differ by which pdhg/stream they pass. +template +static void compute_primal_dual_deltas(pdhg_solver_t& pdhg, rmm::cuda_stream_view stream) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(pdhg.get_reflected_primal().data(), pdhg.get_primal_solution().data()), + pdhg.get_saddle_point_state().get_delta_primal().data(), + pdhg.get_primal_solution().size(), + cuda::std::minus{}, + stream); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(pdhg.get_reflected_dual().data(), pdhg.get_dual_solution().data()), + pdhg.get_saddle_point_state().get_delta_dual().data(), + pdhg.get_dual_solution().size(), + cuda::std::minus{}, + stream); +} + template void pdlp_solver_t::compute_fixed_error(std::vector& has_restarted) { @@ -2329,44 +2348,19 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte dual_size_h_ * climber_strategies_.size(), "delta_dual_ size mismatch"); - // Computing the deltas + // Computing the deltas (delta = reflected - current) // TODO batch mdoe: this only works if everyone restarts if (is_distributed_master()) { - // Go faire une fonction compute_delta_primal, compute_delta primal ? - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdhg = shard->sub_pdlp->pdhg_solver_; - cub::DeviceTransform::Transform(cuda::std::make_tuple(sub_pdhg.get_reflected_primal().data(), - sub_pdhg.get_primal_solution().data()), - sub_pdhg.get_saddle_point_state().get_delta_primal().data(), - sub_pdhg.get_primal_solution().size(), - cuda::std::minus{}, - shard->stream.view()); - cub::DeviceTransform::Transform(cuda::std::make_tuple(sub_pdhg.get_reflected_dual().data(), - sub_pdhg.get_dual_solution().data()), - sub_pdhg.get_saddle_point_state().get_delta_dual().data(), - sub_pdhg.get_dual_solution().size(), - cuda::std::minus{}, - shard->stream.view()); - } + multi_gpu_engine->for_each_shard([](auto& shard) { + compute_primal_dual_deltas(shard.sub_pdlp->pdhg_solver_, shard.stream.view()); + }); } else { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(pdhg_solver_.get_reflected_primal().data(), - pdhg_solver_.get_primal_solution().data()), - pdhg_solver_.get_saddle_point_state().get_delta_primal().data(), - pdhg_solver_.get_primal_solution().size(), - cuda::std::minus{}, - stream_view_.value()); - cub::DeviceTransform::Transform(cuda::std::make_tuple(pdhg_solver_.get_reflected_dual().data(), - pdhg_solver_.get_dual_solution().data()), - pdhg_solver_.get_saddle_point_state().get_delta_dual().data(), - pdhg_solver_.get_dual_solution().size(), - cuda::std::minus{}, - stream_view_.value()); + compute_primal_dual_deltas(pdhg_solver_, stream_view_); } auto& cusparse_view = pdhg_solver_.get_cusparse_view(); + // Distributed compute_fixed_error second part if (is_distributed_master()) { // SpMV is the first operation in compute_interaction_and_movement so we can do halo before and // call it naturally we then reduce the local dot products @@ -2382,7 +2376,6 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte cusparseDnVecSetValues(sub_cv.potential_next_dual_solution, (void*)sub_pdlp.pdhg_solver_.get_reflected_dual().data())); - // Ensure norm is on owned size sub_pdlp.step_size_strategy_.compute_interaction_and_movement( sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), sub_cv, @@ -2713,7 +2706,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co bool warm_start_was_given = settings_.get_pdlp_warm_start_data().is_populated(); - // In distributed mode, skip all setup, it is done before + // In distributed mode, skip all setup, it is already done if (!settings_.hyper_params.use_distributed_pdlp) { // TODO handle that properly if (settings_.hyper_params.compute_initial_step_size_before_scaling && diff --git a/cpp/src/pdlp/solve.cuh b/cpp/src/pdlp/solve.cuh index 265f2ed2a2..da9e96e437 100644 --- a/cpp/src/pdlp/solve.cuh +++ b/cpp/src/pdlp/solve.cuh @@ -34,34 +34,38 @@ cuopt::linear_programming::optimization_problem_solution_t solve_lp_wi /** * @brief Distributed-PDLP entry point that consumes the host MPS data model - * directly, without ever materializing the full problem on a single - * (master) GPU. + * directly, partitioning it across GPUs without ever materializing the + * full problem on a single (master) GPU. * - * This is the entry point intended for problems whose `nnz` exceeds the memory - * of a single device. Today (Step 1 of the mGPU memory refactor) it is a thin - * routing shim: it resolves `distributed_pdlp_num_gpus == -1` against the - * visible-device count and delegates to the legacy - * `mps_data_model_to_optimization_problem(...)` + device-side `solve_lp(...)` - * pipeline, which still allocates the full problem on master. The shim exists - * so the public-facing call site is already in place; subsequent commits will - * replace the body with: - * 1. host-side graph partitioning straight off the MPS CSR - * 2. per-shard host CSR slicing - * 3. construction of an mGPU-native pdlp_solver_t whose master only holds - * scalar metadata + gather buffers (no full A / A^T / scaled copies). + * Intended for problems whose `nnz` exceeds the memory of a single device. The + * master `pdlp_solver_t` is constructed from a shape-0 placeholder problem; the + * real work happens inside it, built straight from the host `mps_data_model`: + * 1. host-side graph partitioning off the MPS CSR, + * 2. per-shard host CSR slicing, + * 3. one shard pdlp_solver_t per GPU, while master holds only scalar metadata + * + gather buffers (no full A / A^T / scaled copies). + * It then runs the solver, gathers the solution to master, applies the + * maximization sign-flip on the dual / reduced cost when the sense is maximize, + * and returns the gathered solution. * - * Until then, behaviour and memory footprint are identical to the legacy path. + * Resolves the `distributed_pdlp_num_gpus == -1` sentinel against the + * visible-device count and propagates `pdlp_disable_graph` to the CUDA-graph + * flag. Several configurations are rejected up front (see @pre). * - * @param handle_ptr Master raft handle (its stream owns the gather buffers - * and any master-side aggregator allocations). + * @param handle_ptr Master raft handle (its stream owns the gather buffers and + * any master-side aggregator allocations). Must be non-null. * @param mps_data_model Host-resident MPS data (CPU vectors only). * @param settings User-supplied PDLP solver settings; the * `distributed_pdlp_num_gpus == -1` sentinel is resolved * here against the visible-device count. - * @param problem_checking Forwarded to the eventual solver. - * @param use_pdlp_solver_mode Forwarded to the eventual solver. + * @param problem_checking Currently unused on this path (accepted for + * signature parity with the device-side entry points). + * @param use_pdlp_solver_mode When true, applies set_pdlp_solver_mode() to the + * resolved settings before solving. * - * @pre `settings.hyper_params.use_distributed_pdlp == true`. + * @pre `settings.hyper_params.use_distributed_pdlp == true`, `method == PDLP`, + * `presolver == None`, `pdlp_precision == DefaultPrecision`, not inside + * MIP, and no initial primal/dual or warm-start data. */ template cuopt::linear_programming::optimization_problem_solution_t solve_lp_distributed_from_mps( From 733334e5d0bc784a7d335dae3daa1aa30da3ccb1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 17:08:04 +0200 Subject: [PATCH 120/258] cnvergence info v2 --- .../convergence_information.cu | 296 +++++++++--------- .../convergence_information.hpp | 14 + 2 files changed, 166 insertions(+), 144 deletions(-) diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 355d1b6c4c..8eab20dc64 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -457,6 +457,154 @@ __global__ void compute_remaining_stats_kernel( raft::abs(convergence_information_view.dual_objective[idx]); } +template +template +void convergence_information_t::copy_scalar_from_shard0( + multi_gpu_engine_t& engine, Pick&& pick) +{ + // Sync shards with master stream to avoid race conditions before reading. + engine.sync_await_shards(stream_view_); + auto& s0 = *engine.shards[0]; + raft::device_setter guard(s0.device_id); + auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); + raft::copy(pick(*this), pick(s0_conv), 1, stream_view_); +} + +template +void convergence_information_t::distributed_compute_primal_residual_and_objective( + multi_gpu_engine_t& engine, const pdlp_solver_settings_t& settings) +{ + cuopt_expects(!settings.per_constraint_residual, + error_type_t::ValidationError, + "per_constraint_residual is not yet supported in multi-GPU mode"); + cuopt_assert(!batch_mode_, "multi-GPU PDLP is not supported in batch mode"); + + // Prepare halo values in potential_next_primal_solution. + engine.halo_exchange_var([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { + return pdhg.get_potential_next_primal_solution(); + }); + + // Per-shard primal residual + partial (owned) primal objective. + engine.for_each_shard([](auto& shard) { + auto& sub_pdlp = *shard.sub_pdlp; + auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); + sub_conv.compute_primal_residual(sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_dual_tmp_resource(), + sub_pdlp.pdhg_solver_.get_potential_next_dual_solution()); + sub_conv.compute_primal_objective_owned_partial( + sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), shard.rank_data.owned_var_size); + }); + + // Reduce partial primal objectives across shards, mirror the result from + // shard 0 to master, then apply scaling/offset once on the reduced value + // (applying it per-shard would over-count the offset Nshards times). + engine.allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .get_primal_objective() + .data(); + }); + copy_scalar_from_shard0( + engine, [](convergence_information_t& c) -> f_t* { return c.primal_objective_.data(); }); + apply_primal_objective_scaling_and_offset(); +} + +template +void convergence_information_t::distributed_compute_primal_residual_l2_norm( + multi_gpu_engine_t& engine) +{ + engine.distributed_l2_norm( + [](pdlp_solver_t& sp) -> rmm::device_uvector& { + return sp.get_current_termination_strategy().get_convergence_information().primal_residual_; + }, + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_primal_residual_.data(); + }, + [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_cstr_size; }); + copy_scalar_from_shard0(engine, [](convergence_information_t& c) -> f_t* { + return c.l2_primal_residual_.data(); + }); +} + +template +void convergence_information_t::distributed_compute_dual_residual_and_objective( + multi_gpu_engine_t& engine) +{ + // 1) Halo-exchange potential_next_dual_solution on every shard so the + // A_T_shard @ y SpMV inside compute_dual_residual reads correct values + // in the cstr halo region. The SpMV is driven through the eval view's + // cv.dual_solution descriptor, which (cuPDLPx, see + // cusparse_view.cu:931-937) is bound to _potential_next_dual -- not to + // current.dual_solution. So we must halo-exchange the same buffer. + engine.halo_exchange_cstr([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { + return pdhg.get_potential_next_dual_solution(); + }); + + // 2-3) Per-shard: + // - compute_dual_residual: shard.dual_residual_ has owned-var entries + // correct, halo var entries garbage (their A_T row isn't on this + // shard). + // - compute_dual_objective_owned_partial: writes a *partial* + // dot(slack[0:nv], x[0:nv]) + Σ primal_slack[0:nc] into + // shard.dual_objective_, with NO scaling/offset. Relies on + // primal_slack_ already populated by the per-shard + // compute_primal_residual above. + // + // Same primal_iterate fix as the primal block above: use the shard's + // (fresh, unscaled) potential_next_primal_solution, matching single-GPU + // cuPDLPx (pdlp.cu:1190-1203). The previous code's get_primal_solution() + // would mix scaled x with unscaled dual_slack in the dual_objective + // cublasdot. + engine.for_each_shard([](auto& shard) { + auto& sub_pdlp = *shard.sub_pdlp; + auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); + sub_conv.compute_dual_residual(sub_conv.op_problem_cusparse_view_, + sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), + sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), + sub_pdlp.pdhg_solver_.get_dual_slack()); + sub_conv.compute_dual_objective_owned_partial( + sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), + sub_pdlp.pdhg_solver_.get_dual_slack(), + shard.rank_data.owned_var_size, + shard.rank_data.owned_cstr_size); + }); + + // 4) Allreduce dual_objective_ across shards (sum, in place), mirror to + // master, then apply offset/scaling once (per-shard would over-count it). + engine.allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .get_dual_objective() + .data(); + }); + copy_scalar_from_shard0( + engine, [](convergence_information_t& c) -> f_t* { return c.dual_objective_.data(); }); + apply_dual_objective_scaling_and_offset(); +} + +template +void convergence_information_t::distributed_compute_dual_residual_l2_norm( + multi_gpu_engine_t& engine) +{ + // Same pattern as the primal L2 above, but the dual residual is var-shaped + // so we clip to owned_var_size. + engine.distributed_l2_norm( + [](pdlp_solver_t& sp) -> rmm::device_uvector& { + return sp.get_current_termination_strategy().get_convergence_information().dual_residual_; + }, + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_current_termination_strategy() + .get_convergence_information() + .l2_dual_residual_.data(); + }, + [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_var_size; }); + copy_scalar_from_shard0(engine, [](convergence_information_t& c) -> f_t* { + return c.l2_dual_residual_.data(); + }); +} + template void convergence_information_t::compute_convergence_information( pdhg_solver_t& current_pdhg_solver, @@ -492,47 +640,7 @@ void convergence_information_t::compute_convergence_information( #endif if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - cuopt_expects(!settings.per_constraint_residual, - error_type_t::ValidationError, - "per_constraint_residual is not yet supported in multi-GPU mode"); - - // Prepares halo values in potential_next_primal_solution - - engine->halo_exchange_var([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_potential_next_primal_solution(); - }); - - for (auto& shard : engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdlp = *shard->sub_pdlp; - auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); - sub_conv.compute_primal_residual(sub_conv.op_problem_cusparse_view_, - sub_pdlp.pdhg_solver_.get_dual_tmp_resource(), - sub_pdlp.pdhg_solver_.get_potential_next_dual_solution()); - sub_conv.compute_primal_objective_owned_partial( - sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), - shard->rank_data.owned_var_size); - } - - // Reduce all primal objectives across shards - cuopt_assert(!batch_mode_, "multi-GPU PDLP is not supported in batch mode"); - engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .get_primal_objective() - .data(); - }); - - // Get the reduced primal objective from the shard[0] (arbitrary) - // Sync shards with master stream to avoid race conditions - engine->sync_await_shards(stream_view_); - { - auto& s0 = *engine->shards[0]; - raft::device_setter guard(s0.device_id); - auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); - raft::copy(primal_objective_.data(), s0_conv.get_primal_objective().data(), 1, stream_view_); - } - apply_primal_objective_scaling_and_offset(); + distributed_compute_primal_residual_and_objective(*engine, settings); } else { compute_primal_residual( op_problem_cusparse_view_, current_pdhg_solver.get_dual_tmp_resource(), dual_iterate); @@ -545,27 +653,7 @@ void convergence_information_t::compute_convergence_information( // L2 Norm if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - engine->distributed_l2_norm( - [](pdlp_solver_t& sp) -> rmm::device_uvector& { - return sp.get_current_termination_strategy().get_convergence_information().primal_residual_; - }, - [](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .l2_primal_residual_.data(); - }, - [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_cstr_size; }); - - // distributed L2 norm before copying scalar data out of shard 0. - engine->sync_await_shards(stream_view_); - auto& s0 = *engine->shards[0]; - raft::device_setter guard(s0.device_id); - raft::copy(l2_primal_residual_.data(), - s0.sub_pdlp->get_current_termination_strategy() - .get_convergence_information() - .l2_primal_residual_.data(), - 1, - stream_view_); + distributed_compute_primal_residual_l2_norm(*engine); } else if (!batch_mode_) { my_l2_norm(primal_residual_, l2_primal_residual_, handle_ptr_); } else { @@ -610,65 +698,7 @@ void convergence_information_t::compute_convergence_information( } if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - // 1) Halo-exchange potential_next_dual_solution on every shard so the - // A_T_shard @ y SpMV inside compute_dual_residual reads correct values - // in the cstr halo region. The SpMV is driven through the eval view's - // cv.dual_solution descriptor, which (cuPDLPx, see - // cusparse_view.cu:931-937) is bound to _potential_next_dual -- not to - // current.dual_solution. So we must halo-exchange the same buffer. - engine->halo_exchange_cstr([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_potential_next_dual_solution(); - }); - - // 2-3) Per-shard: - // - compute_dual_residual: shard.dual_residual_ has owned-var entries - // correct, halo var entries garbage (their A_T row isn't on this - // shard). - // - compute_dual_objective_owned_partial: writes a *partial* - // dot(slack[0:nv], x[0:nv]) + Σ primal_slack[0:nc] into - // shard.dual_objective_, with NO scaling/offset. Relies on - // primal_slack_ already populated by the per-shard - // compute_primal_residual above. - // - // Same primal_iterate fix as the primal block above: use the shard's - // (fresh, unscaled) potential_next_primal_solution, matching single-GPU - // cuPDLPx (pdlp.cu:1190-1203). The previous code's get_primal_solution() - // would mix scaled x with unscaled dual_slack in the dual_objective - // cublasdot. - for (auto& shard : engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdlp = *shard->sub_pdlp; - auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); - sub_conv.compute_dual_residual(sub_conv.op_problem_cusparse_view_, - sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), - sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), - sub_pdlp.pdhg_solver_.get_dual_slack()); - sub_conv.compute_dual_objective_owned_partial( - sub_pdlp.pdhg_solver_.get_potential_next_primal_solution(), - sub_pdlp.pdhg_solver_.get_dual_slack(), - shard->rank_data.owned_var_size, - shard->rank_data.owned_cstr_size); - } - - // 4) Allreduce dual_objective_ across shards (sum, in place). Same - // offset/scaling-after-allreduce reasoning as primal: applying offset - // per-shard would over-count it Nshards times. - engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .get_dual_objective() - .data(); - }); - - // Sync shards with master stream to avoid race conditions - engine->sync_await_shards(stream_view_); - { - auto& s0 = *engine->shards[0]; - raft::device_setter guard(s0.device_id); - auto& s0_conv = s0.sub_pdlp->get_current_termination_strategy().get_convergence_information(); - raft::copy(dual_objective_.data(), s0_conv.get_dual_objective().data(), 1, stream_view_); - } - apply_dual_objective_scaling_and_offset(); + distributed_compute_dual_residual_and_objective(*engine); } else { compute_dual_residual(op_problem_cusparse_view_, current_pdhg_solver.get_primal_tmp_resource(), @@ -682,29 +712,7 @@ void convergence_information_t::compute_convergence_information( #endif if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - // Multi-GPU dual residual L2 norm: same pattern as the primal L2 above, - // but the dual residual is var-shaped so we clip to owned_var_size. - engine->distributed_l2_norm( - [](pdlp_solver_t& sp) -> rmm::device_uvector& { - return sp.get_current_termination_strategy().get_convergence_information().dual_residual_; - }, - [](pdlp_solver_t& sp) -> f_t* { - return sp.get_current_termination_strategy() - .get_convergence_information() - .l2_dual_residual_.data(); - }, - [](pdlp_shard_t& shard) -> i_t { return shard.rank_data.owned_var_size; }); - - // distributed L2 norm before copying scalar data out of shard 0. - engine->sync_await_shards(stream_view_); - auto& s0 = *engine->shards[0]; - raft::device_setter guard(s0.device_id); - raft::copy(l2_dual_residual_.data(), - s0.sub_pdlp->get_current_termination_strategy() - .get_convergence_information() - .l2_dual_residual_.data(), - 1, - stream_view_); + distributed_compute_dual_residual_l2_norm(*engine); } else if (!batch_mode_) { my_l2_norm(dual_residual_, l2_dual_residual_, handle_ptr_); } else { diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.hpp b/cpp/src/pdlp/termination_strategy/convergence_information.hpp index 7ff45e46f0..b3d6811306 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.hpp +++ b/cpp/src/pdlp/termination_strategy/convergence_information.hpp @@ -179,6 +179,20 @@ class convergence_information_t { void compute_reduced_costs_dual_objective_contribution(); + // ----- Distributed-PDLP sub-steps of compute_convergence_information ----- + // Each is the multi-GPU equivalent of the single-GPU block it replaces + void distributed_compute_primal_residual_and_objective( + multi_gpu_engine_t& engine, const pdlp_solver_settings_t& settings); + void distributed_compute_primal_residual_l2_norm(multi_gpu_engine_t& engine); + void distributed_compute_dual_residual_and_objective(multi_gpu_engine_t& engine); + void distributed_compute_dual_residual_l2_norm(multi_gpu_engine_t& engine); + + // Mirror one per-shard convergence scalar from shard 0 to master (after a + // cross-shard reduction has made every shard agree). `pick(conv)` selects the + // f_t* field; it is applied to both master (*this) and shard 0's instance. + template + void copy_scalar_from_shard0(multi_gpu_engine_t& engine, Pick&& pick); + // Ctor helpers — each handles both batch and non-batch internally. void init_objective_offsets(); void init_l2_norms(); From 56d5580caaab59f77368e0421c7513f979790328 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 17:14:43 +0200 Subject: [PATCH 121/258] FINISHED --- .../convergence_information.cu | 39 +++++++------------ .../convergence_information.hpp | 3 +- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 8eab20dc64..e92a455549 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -534,29 +534,15 @@ void convergence_information_t::distributed_compute_dual_residual_and_ { // 1) Halo-exchange potential_next_dual_solution on every shard so the // A_T_shard @ y SpMV inside compute_dual_residual reads correct values - // in the cstr halo region. The SpMV is driven through the eval view's - // cv.dual_solution descriptor, which (cuPDLPx, see - // cusparse_view.cu:931-937) is bound to _potential_next_dual -- not to - // current.dual_solution. So we must halo-exchange the same buffer. + // in the cstr halo region engine.halo_exchange_cstr([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { return pdhg.get_potential_next_dual_solution(); }); - // 2-3) Per-shard: - // - compute_dual_residual: shard.dual_residual_ has owned-var entries - // correct, halo var entries garbage (their A_T row isn't on this - // shard). - // - compute_dual_objective_owned_partial: writes a *partial* - // dot(slack[0:nv], x[0:nv]) + Σ primal_slack[0:nc] into - // shard.dual_objective_, with NO scaling/offset. Relies on - // primal_slack_ already populated by the per-shard - // compute_primal_residual above. - // + // Same primal_iterate fix as the primal block above: use the shard's // (fresh, unscaled) potential_next_primal_solution, matching single-GPU - // cuPDLPx (pdlp.cu:1190-1203). The previous code's get_primal_solution() - // would mix scaled x with unscaled dual_slack in the dual_objective - // cublasdot. + // cuPDLPx. engine.for_each_shard([](auto& shard) { auto& sub_pdlp = *shard.sub_pdlp; auto& sub_conv = sub_pdlp.get_current_termination_strategy().get_convergence_information(); @@ -572,7 +558,7 @@ void convergence_information_t::distributed_compute_dual_residual_and_ }); // 4) Allreduce dual_objective_ across shards (sum, in place), mirror to - // master, then apply offset/scaling once (per-shard would over-count it). + // master, then apply offset/scaling once engine.allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { return sp.get_current_termination_strategy() .get_convergence_information() @@ -639,8 +625,9 @@ void convergence_information_t::compute_convergence_information( print("dual_slack", dual_slack); #endif - if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - distributed_compute_primal_residual_and_objective(*engine, settings); + if (current_pdhg_solver.is_distributed_master()) { + distributed_compute_primal_residual_and_objective(*current_pdhg_solver.get_mgpu_engine(), + settings); } else { compute_primal_residual( op_problem_cusparse_view_, current_pdhg_solver.get_dual_tmp_resource(), dual_iterate); @@ -652,8 +639,8 @@ void convergence_information_t::compute_convergence_information( #endif // L2 Norm - if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - distributed_compute_primal_residual_l2_norm(*engine); + if (current_pdhg_solver.is_distributed_master()) { + distributed_compute_primal_residual_l2_norm(*current_pdhg_solver.get_mgpu_engine()); } else if (!batch_mode_) { my_l2_norm(primal_residual_, l2_primal_residual_, handle_ptr_); } else { @@ -697,8 +684,8 @@ void convergence_information_t::compute_convergence_information( std::numeric_limits::lowest()); } - if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - distributed_compute_dual_residual_and_objective(*engine); + if (current_pdhg_solver.is_distributed_master()) { + distributed_compute_dual_residual_and_objective(*current_pdhg_solver.get_mgpu_engine()); } else { compute_dual_residual(op_problem_cusparse_view_, current_pdhg_solver.get_primal_tmp_resource(), @@ -711,8 +698,8 @@ void convergence_information_t::compute_convergence_information( print("Dual Residual", dual_residual_); #endif - if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - distributed_compute_dual_residual_l2_norm(*engine); + if (current_pdhg_solver.is_distributed_master()) { + distributed_compute_dual_residual_l2_norm(*current_pdhg_solver.get_mgpu_engine()); } else if (!batch_mode_) { my_l2_norm(dual_residual_, l2_dual_residual_, handle_ptr_); } else { diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.hpp b/cpp/src/pdlp/termination_strategy/convergence_information.hpp index b3d6811306..d915ff1404 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.hpp +++ b/cpp/src/pdlp/termination_strategy/convergence_information.hpp @@ -134,8 +134,7 @@ class convergence_information_t { // Multi-GPU shard helper: writes a partial dot(c[0:n_owned], x[0:n_owned]) // into primal_objective_ (no scaling, no offset). Master is responsible for // allreduce SUM across shards and then applying scaling + offset once on the - // reduced value. n_owned must be <= primal_size_h_; pass owned_var_size on - // each shard. + // reduced value void compute_primal_objective_owned_partial(rmm::device_uvector& primal_solution, i_t n_owned); From e2a36abb5e141aad2207bcb7e8a55717e2e79e1a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 17 Jun 2026 19:19:54 +0200 Subject: [PATCH 122/258] style --- .../distributed_algorithms.cu | 34 +++++++++++-------- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 3 +- .../distributed_pdlp/multi_gpu_engine.hpp | 3 +- .../initial_scaling.cu | 13 +++---- cpp/src/pdlp/pdhg.cu | 2 +- cpp/src/pdlp/pdhg.hpp | 8 ++--- cpp/src/pdlp/pdlp.cu | 4 +-- .../convergence_information.cu | 11 +++--- 8 files changed, 42 insertions(+), 36 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 7fe3a89126..924e235dd2 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -112,8 +112,7 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng h_dual.data(), total_cstrs, engine.stream.view()); - raft::copy( - master_reduced_cost.data(), h_reduced_cost.data(), total_vars, engine.stream.view()); + raft::copy(master_reduced_cost.data(), h_reduced_cost.data(), total_vars, engine.stream.view()); RAFT_CUDA_TRY(cudaStreamSynchronize(engine.stream.view().value())); } @@ -177,8 +176,13 @@ void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); - ncclAllReduce( - sq[r].data(), sq[r].data(), 2, nccl_data_type(), ncclSum, s.comm.get(), s.stream.view().value()); + ncclAllReduce(sq[r].data(), + sq[r].data(), + 2, + nccl_data_type(), + ncclSum, + s.comm.get(), + s.stream.view().value()); } ncclGroupEnd(); @@ -610,22 +614,24 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, // ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- #define INSTANTIATE(F_TYPE) \ template void broadcast_constraint_scaling_to_halo( \ - multi_gpu_engine_t& engine); \ - template void broadcast_variable_scaling_to_halo( \ - multi_gpu_engine_t& engine); \ + multi_gpu_engine_t & engine); \ + template void broadcast_variable_scaling_to_halo(multi_gpu_engine_t & \ + engine); \ template void distributed_bound_objective_rescaling( \ - multi_gpu_engine_t& engine, F_TYPE c_scaling_weight); \ + multi_gpu_engine_t & engine, F_TYPE c_scaling_weight); \ template void distributed_ruiz_inf_scaling( \ - multi_gpu_engine_t& engine, int num_iter, int n_global_vars); \ + multi_gpu_engine_t & engine, int num_iter, int n_global_vars); \ template void distributed_pock_chambolle_scaling( \ - multi_gpu_engine_t& engine, F_TYPE alpha, int n_global_vars); \ + multi_gpu_engine_t & engine, F_TYPE alpha, int n_global_vars); \ template F_TYPE distributed_max_singular_value( \ - multi_gpu_engine_t& engine, int n_global_cstrs, int max_iterations, \ + multi_gpu_engine_t & engine, \ + int n_global_cstrs, \ + int max_iterations, \ F_TYPE tolerance); \ template void gather_potential_next_solutions_to_master( \ - multi_gpu_engine_t& engine, \ - pdhg_solver_t& master_pdhg, \ - rmm::device_uvector& master_reduced_cost); + multi_gpu_engine_t & engine, \ + pdhg_solver_t & master_pdhg, \ + rmm::device_uvector & master_reduced_cost); INSTANTIATE(double) INSTANTIATE(float) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index b99ffbad16..c7e3aaa7bd 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -94,8 +94,7 @@ void multi_gpu_engine_t::distributed_compute_At_y() // -------- Cross-stream fork / join / sync --------------------------------- template -void multi_gpu_engine_t::graph_capture_fork_to_shards( - rmm::cuda_stream_view master_stream) +void multi_gpu_engine_t::graph_capture_fork_to_shards(rmm::cuda_stream_view master_stream) { graph_master_ready_event_->record(master_stream); for (auto& s : shards) { diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 76c7b9a835..06221124ae 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -261,7 +261,8 @@ struct multi_gpu_engine_t { for (auto& s : shards) { raft::device_setter guard(s->device_id); f_t* buf = ptr_access(*s->sub_pdlp); - ncclAllReduce(buf, buf, count, nccl_data_type(), ncclSum, s->comm.get(), s->stream.view().value()); + ncclAllReduce( + buf, buf, count, nccl_data_type(), ncclSum, s->comm.get(), s->stream.view().value()); } ncclGroupEnd(); } diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 65a7880dcf..d2cca605ad 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -254,12 +254,13 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_compute_local_iteratio op_problem_scaled_.view(), this->view()); RAFT_CUDA_TRY(cudaPeekAtLastError()); - inf_norm_col_kernel<<>>( - op_problem_scaled_.view(), - this->view(), - A_T_.data(), - A_T_offsets_.data(), - A_T_indices_.data()); + inf_norm_col_kernel + <<>>( + op_problem_scaled_.view(), + this->view(), + A_T_.data(), + A_T_offsets_.data(), + A_T_indices_.data()); RAFT_CUDA_TRY(cudaPeekAtLastError()); if (running_mip_) { reset_integer_variables(); } diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index 27c89f6f04..980fe63d2c 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ -#include #include +#include #include #include #include diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index 547a0da504..6c435a96e6 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -94,13 +94,13 @@ class pdhg_solver_t { void spmvop_A_x(); // Parameterized SpMVs used by the multi-GPU engine. - // Both temporarily hijack a canonical input descriptor in cusparse_view_,run the local SpMV into `out_desc`, then restore the - // descriptor to its original buffer so other code on this shard is unaffected. - // No multi-GPU dispatch inside — the engine is the orchestrator. + // Both temporarily hijack a canonical input descriptor in cusparse_view_,run the local SpMV into + // `out_desc`, then restore the descriptor to its original buffer so other code on this shard is + // unaffected. No multi-GPU dispatch inside — the engine is the orchestrator. void spmv_At_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); void spmv_A_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); - // Pure cub-transform extractions. Allows for clearer containment of the calls and ensures + // Pure cub-transform extractions. Allows for clearer containment of the calls and ensures // the single-GPU vs distributed-GPU uses the same calls void primal_reflected_major_projection_transform(rmm::device_uvector& primal_step_size); void dual_reflected_major_projection_transform(rmm::device_uvector& dual_step_size); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 31bf5d9420..223b2c7669 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -433,7 +433,6 @@ pdlp_solver_t::pdlp_solver_t( error_type_t::ValidationError, "mps variable_upper_bounds size must equal n_variables"); - // A (CSR) — mutable copies for the engine + partitioner consumers below. std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); @@ -599,8 +598,7 @@ pdlp_solver_t::pdlp_solver_t( // Global bound/objective rescaling: allreduce the owned partial squared-norms if (settings_.hyper_params.bound_objective_rescaling && !inside_mip_) { distributed_bound_objective_rescaling( - *multi_gpu_engine, - static_cast(settings_.hyper_params.initial_primal_weight_c_scaling)); + *multi_gpu_engine, static_cast(settings_.hyper_params.initial_primal_weight_c_scaling)); } // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index e92a455549..40b71fd255 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -504,8 +504,9 @@ void convergence_information_t::distributed_compute_primal_residual_an .get_primal_objective() .data(); }); - copy_scalar_from_shard0( - engine, [](convergence_information_t& c) -> f_t* { return c.primal_objective_.data(); }); + copy_scalar_from_shard0(engine, [](convergence_information_t& c) -> f_t* { + return c.primal_objective_.data(); + }); apply_primal_objective_scaling_and_offset(); } @@ -539,7 +540,6 @@ void convergence_information_t::distributed_compute_dual_residual_and_ return pdhg.get_potential_next_dual_solution(); }); - // Same primal_iterate fix as the primal block above: use the shard's // (fresh, unscaled) potential_next_primal_solution, matching single-GPU // cuPDLPx. @@ -565,8 +565,9 @@ void convergence_information_t::distributed_compute_dual_residual_and_ .get_dual_objective() .data(); }); - copy_scalar_from_shard0( - engine, [](convergence_information_t& c) -> f_t* { return c.dual_objective_.data(); }); + copy_scalar_from_shard0(engine, [](convergence_information_t& c) -> f_t* { + return c.dual_objective_.data(); + }); apply_dual_objective_scaling_and_offset(); } From 45556da3f1b5dda1bd328087015444841561145b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 18 Jun 2026 03:52:25 -0700 Subject: [PATCH 123/258] =?UTF-8?q?moved=20mgpu=20engine=20up=20so=20graph?= =?UTF-8?q?=20gets=20destroyed=20before=20the=20mgpuengine=20communicators?= =?UTF-8?q?=20emoji=20troll=20=F0=9F=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cpp/src/pdlp/pdlp.cuh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 3e8c10299d..d92d61d63c 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -211,6 +211,11 @@ class pdlp_solver_t { detail::adaptive_step_size_strategy_t step_size_strategy_; public: + // std::optional because multi_gpu_engine_t is non-default-constructible + // (collectively bootstraps NCCL, owns RMM resources). Stays nullopt in + // single-GPU mode; emplaced by the multi-GPU ctor. + std::optional> multi_gpu_engine; + // Inner solver detail::pdhg_solver_t pdhg_solver_; void halpern_update(); @@ -279,10 +284,6 @@ class pdlp_solver_t { // Flag to indicate if solver is being called from MIP. No logging is done in this case. bool inside_mip_{false}; - // std::optional because multi_gpu_engine_t is non-default-constructible - // (collectively bootstraps NCCL, owns RMM resources). Stays nullopt in - // single-GPU mode; emplaced by the multi-GPU ctor. - std::optional> multi_gpu_engine; }; } // namespace cuopt::linear_programming::detail From d5f1ce05e3d03438f6f80ebaa1f69c8822ee0653 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 18 Jun 2026 03:58:35 -0700 Subject: [PATCH 124/258] removed a useless inclyde and moved an assert up for coderabbit --- cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 1 - cpp/src/pdlp/pdlp.cu | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index 5f0c50c8f7..6ab529d285 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -6,7 +6,6 @@ #pragma once #include -#include #include namespace cuopt::linear_programming::detail { diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 223b2c7669..d06c5c4c11 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -397,6 +397,9 @@ pdlp_solver_t::pdlp_solver_t( error_type_t::ValidationError, "Distributed mGPU pdlp_solver_t ctor requires a shape-0 " "placeholder problem (n_variables == n_constraints == nnz == 0)"); + cuopt_expects(settings.hyper_params.never_restart_to_average, + error_type_t::ValidationError, + "Distributed PDLP requires never_restart_to_average = true"); const int distributed_pdlp_num_gpus = settings.distributed_pdlp_num_gpus; CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU (mps direct path)", distributed_pdlp_num_gpus); @@ -2991,10 +2994,6 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co // 1. At the very beginning of the solver, when no steps have been taken yet // 2. After a single step, since average of one step is the same step if (internal_solver_iterations_ <= 1) { - cuopt_expects(!is_distributed_master(), - error_type_t::RuntimeError, - "Distributed PDLP does not support average restart; run with " - "never_restart_to_average = true."); raft::copy(unscaled_primal_avg_solution_.data(), pdhg_solver_.get_primal_solution().data(), primal_size_h_, From 9c1e345d8c7f8a59d942848980d9b5a061609601 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 18 Jun 2026 04:03:22 -0700 Subject: [PATCH 125/258] added nccl_try everywhere --- .../distributed_algorithms.cu | 110 +++++++++--------- .../distributed_pdlp/multi_gpu_engine.hpp | 65 ++++++----- .../pdlp/distributed_pdlp/nccl_helpers.hpp | 26 +++++ 3 files changed, 114 insertions(+), 87 deletions(-) create mode 100644 cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 924e235dd2..f48cb659a1 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -172,19 +172,19 @@ void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, // 2) NCCL allreduce SUM (both scalars at once) -> every shard holds the // global squared norms. - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); - ncclAllReduce(sq[r].data(), - sq[r].data(), - 2, - nccl_data_type(), - ncclSum, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclAllReduce(sq[r].data(), + sq[r].data(), + 2, + nccl_data_type(), + ncclSum, + s.comm.get(), + s.stream.view().value())); } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); // 3) derive the identical scalars and apply on every shard. for (int r = 0; r < nb; ++r) { @@ -379,18 +379,18 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, s.cstr_send_buf_d[peer].begin()); } } - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; - ncclSend(s.cstr_send_buf_d[peer].data(), - s.cstr_send_buf_d[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclSend(s.cstr_send_buf_d[peer].data(), + s.cstr_send_buf_d[peer].size(), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } for (int r = 0; r < nb; ++r) { @@ -401,15 +401,15 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; - ncclRecv(recv_ptr, - static_cast(rd.cstr_recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclRecv(recv_ptr, + static_cast(rd.cstr_recv_counts[peer]), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); }; auto halo_exchange_var_bufs = [&](std::vector>& bufs) { for (int r = 0; r < nb; ++r) { @@ -426,18 +426,18 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, s.var_send_buf_d[peer].begin()); } } - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; - ncclSend(s.var_send_buf_d[peer].data(), - s.var_send_buf_d[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclSend(s.var_send_buf_d[peer].data(), + s.var_send_buf_d[peer].size(), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } for (int r = 0; r < nb; ++r) { @@ -448,15 +448,15 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; - ncclRecv(recv_ptr, - static_cast(rd.var_recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclRecv(recv_ptr, + static_cast(rd.var_recv_counts[peer]), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); }; // Per-shard partial reductions over the OWNED cstr slice + NCCL allreduce. @@ -477,19 +477,19 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, out[r].data(), s.stream.view().value())); } - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); - ncclAllReduce(out[r].data(), - out[r].data(), - 1, - nccl_data_type(), - ncclSum, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclAllReduce(out[r].data(), + out[r].data(), + 1, + nccl_data_type(), + ncclSum, + s.comm.get(), + s.stream.view().value())); } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); @@ -513,19 +513,19 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, out[r].data(), s.stream.view().value())); } - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); - ncclAllReduce(out[r].data(), - out[r].data(), - 1, - nccl_data_type(), - ncclSum, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclAllReduce(out[r].data(), + out[r].data(), + 1, + nccl_data_type(), + ncclSum, + s.comm.get(), + s.stream.view().value())); } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); }; // ===== Power iteration ===== diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 06221124ae..1ce7a9889b 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -4,6 +4,7 @@ */ #pragma once +#include #include #include #include @@ -148,18 +149,18 @@ struct multi_gpu_engine_t { } // Step 2: matched send / recv across the whole topology in one NCCL group. - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; - ncclSend(s.var_send_buf_d[peer].data(), - s.var_send_buf_d[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclSend(s.var_send_buf_d[peer].data(), + s.var_send_buf_d[peer].size(), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } for (int r = 0; r < nb; ++r) { @@ -170,15 +171,15 @@ struct multi_gpu_engine_t { for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; - ncclRecv(recv_ptr, - static_cast(rd.var_recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclRecv(recv_ptr, + static_cast(rd.var_recv_counts[peer]), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); } // -------- Halo exchange (constraints / y) ------------------------------- @@ -215,18 +216,18 @@ struct multi_gpu_engine_t { } } - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; - ncclSend(s.cstr_send_buf_d[peer].data(), - s.cstr_send_buf_d[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclSend(s.cstr_send_buf_d[peer].data(), + s.cstr_send_buf_d[peer].size(), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } for (int r = 0; r < nb; ++r) { @@ -237,15 +238,15 @@ struct multi_gpu_engine_t { for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; - ncclRecv(recv_ptr, - static_cast(rd.cstr_recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value()); + CUOPT_NCCL_TRY(ncclRecv(recv_ptr, + static_cast(rd.cstr_recv_counts[peer]), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); } } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); } // -------- NCCL allreduce (sum, in place) -------------------------------- @@ -257,14 +258,14 @@ struct multi_gpu_engine_t { template void allreduce_sum_inplace(PtrAccess&& ptr_access, size_t count = 1) { - ncclGroupStart(); + CUOPT_NCCL_TRY(ncclGroupStart()); for (auto& s : shards) { raft::device_setter guard(s->device_id); f_t* buf = ptr_access(*s->sub_pdlp); - ncclAllReduce( - buf, buf, count, nccl_data_type(), ncclSum, s->comm.get(), s->stream.view().value()); + CUOPT_NCCL_TRY(ncclAllReduce( + buf, buf, count, nccl_data_type(), ncclSum, s->comm.get(), s->stream.view().value())); } - ncclGroupEnd(); + CUOPT_NCCL_TRY(ncclGroupEnd()); } // -------- Distributed L2 norm ------------------------------------------ diff --git a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp new file mode 100644 index 0000000000..8cab81a69a --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include + +namespace cuopt::linear_programming::detail { + +// Wraps a NCCL call and throws cuopt::logic_error (RuntimeError) on any non- +// ncclSuccess return code. +#define CUOPT_NCCL_TRY(call) \ + do { \ + ::ncclResult_t const _cuopt_nccl_status = (call); \ + ::cuopt::cuopt_expects(_cuopt_nccl_status == ncclSuccess, \ + ::cuopt::error_type_t::RuntimeError, \ + "NCCL error at %s:%d: %s", \ + __FILE__, \ + __LINE__, \ + ::ncclGetErrorString(_cuopt_nccl_status)); \ + } while (0) + +} // namespace cuopt::linear_programming::detail From 34196b14dd09fce6f4f60fd3faa4c0d68f987997 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 18 Jun 2026 15:13:19 +0200 Subject: [PATCH 126/258] style --- cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp | 18 +++++++++--------- cpp/src/pdlp/pdlp.cuh | 1 - 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp index 8cab81a69a..b9a6c3ac86 100644 --- a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp +++ b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp @@ -12,15 +12,15 @@ namespace cuopt::linear_programming::detail { // Wraps a NCCL call and throws cuopt::logic_error (RuntimeError) on any non- // ncclSuccess return code. -#define CUOPT_NCCL_TRY(call) \ - do { \ - ::ncclResult_t const _cuopt_nccl_status = (call); \ - ::cuopt::cuopt_expects(_cuopt_nccl_status == ncclSuccess, \ - ::cuopt::error_type_t::RuntimeError, \ - "NCCL error at %s:%d: %s", \ - __FILE__, \ - __LINE__, \ - ::ncclGetErrorString(_cuopt_nccl_status)); \ +#define CUOPT_NCCL_TRY(call) \ + do { \ + ::ncclResult_t const _cuopt_nccl_status = (call); \ + ::cuopt::cuopt_expects(_cuopt_nccl_status == ncclSuccess, \ + ::cuopt::error_type_t::RuntimeError, \ + "NCCL error at %s:%d: %s", \ + __FILE__, \ + __LINE__, \ + ::ncclGetErrorString(_cuopt_nccl_status)); \ } while (0) } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index d92d61d63c..f8934ca832 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -283,7 +283,6 @@ class pdlp_solver_t { primal_quality_adapter_t best_primal_quality_so_far_; // Flag to indicate if solver is being called from MIP. No logging is done in this case. bool inside_mip_{false}; - }; } // namespace cuopt::linear_programming::detail From 380fd26b3dca6dea9e8af57bf16d9b78d16d834e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 23 Jun 2026 17:13:01 +0200 Subject: [PATCH 127/258] updated dependencies --- conda/environments/all_cuda-129_arch-aarch64.yaml | 1 + conda/environments/all_cuda-129_arch-x86_64.yaml | 1 + conda/environments/all_cuda-133_arch-aarch64.yaml | 1 + conda/environments/all_cuda-133_arch-x86_64.yaml | 1 + conda/recipes/libcuopt/recipe.yaml | 4 ++++ dependencies.yaml | 1 + 6 files changed, 9 insertions(+) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index ee92dd9586..c27c7a6143 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -43,6 +43,7 @@ dependencies: - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 - myst-parser +- nccl >=2.19 - ninja - notebook - numba-cuda>=0.22.1 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 778d7b46ba..e8dbca1b41 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -43,6 +43,7 @@ dependencies: - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 - myst-parser +- nccl >=2.19 - ninja - notebook - numba-cuda>=0.22.1 diff --git a/conda/environments/all_cuda-133_arch-aarch64.yaml b/conda/environments/all_cuda-133_arch-aarch64.yaml index 6d1b554d6b..75bcf69ec7 100644 --- a/conda/environments/all_cuda-133_arch-aarch64.yaml +++ b/conda/environments/all_cuda-133_arch-aarch64.yaml @@ -43,6 +43,7 @@ dependencies: - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 - myst-parser +- nccl >=2.19 - ninja - notebook - numba-cuda>=0.22.1 diff --git a/conda/environments/all_cuda-133_arch-x86_64.yaml b/conda/environments/all_cuda-133_arch-x86_64.yaml index b8f0d8b05b..93c805f15b 100644 --- a/conda/environments/all_cuda-133_arch-x86_64.yaml +++ b/conda/environments/all_cuda-133_arch-x86_64.yaml @@ -43,6 +43,7 @@ dependencies: - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 - myst-parser +- nccl >=2.19 - ninja - notebook - numba-cuda>=0.22.1 diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 77c957eade..f7c8c244db 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -89,6 +89,7 @@ cache: - libcusparse-dev - libnvjitlink-dev - libboost-devel + - nccl >=2.19 - tbb-devel - zlib - bzip2 @@ -127,6 +128,7 @@ outputs: - libcudss-dev >=0.7,<0.8 - libcusparse-dev - libnvjitlink-dev + - nccl >=2.19 - openssl - c-ares - libuuid @@ -141,6 +143,7 @@ outputs: - librmm =${{ minor_version }} - cuda-nvrtc - libcudss >=0.7,<0.8 + - nccl >=2.19 - openssl - c-ares - libuuid @@ -189,6 +192,7 @@ outputs: - libcublas - libcudss-dev >=0.7,<0.8 - libcusparse-dev + - nccl >=2.19 - openssl - c-ares - libgrpc >=1.78.0,<1.80.0a0 diff --git a/dependencies.yaml b/dependencies.yaml index 55777a04f0..fad92f1230 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -266,6 +266,7 @@ dependencies: - libboost-devel - cpp-argparse - &pyyaml pyyaml>=6.0.0 + - nccl >=2.19 - tbb-devel - zlib - bzip2 From a77d81bb00e53d228662876474691c8f0627c959 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 24 Jun 2026 15:54:10 +0200 Subject: [PATCH 128/258] removed pdlp_disable graph knob and moved use_distributed_pdlp to settings rather than hyper_params --- cpp/cuopt_cli.cpp | 2 +- .../cuopt/linear_programming/constants.h | 1 - .../pdlp/pdlp_hyper_params.cuh | 5 ----- .../linear_programming/pdlp/solver_settings.hpp | 2 ++ cpp/src/math_optimization/solver_settings.cu | 3 +-- cpp/src/pdlp/pdlp.cu | 2 +- cpp/src/pdlp/solve.cu | 14 ++++---------- cpp/src/pdlp/solve.cuh | 6 +++--- cpp/src/pdlp/utilities/ping_pong_graph.cuh | 17 +---------------- cpp/tests/linear_programming/pdlp_test.cu | 7 ++----- 10 files changed, 15 insertions(+), 44 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index f788b36012..7435d2a8f2 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -178,7 +178,7 @@ int run_single_file(const std::string& file_path, } else { auto& lp_settings = settings.get_pdlp_settings(); - if (lp_settings.hyper_params.use_distributed_pdlp) { + if (lp_settings.use_distributed_pdlp) { cuopt::cuopt_expects( handle_ptr != nullptr, cuopt::error_type_t::ValidationError, diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 85c75f83b4..05413954c5 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -90,7 +90,6 @@ #define CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE "multi_gpu_export_partition_file" #define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" #define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" -#define CUOPT_PDLP_DISABLE_GRAPH "pdlp_disable_graph" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" #define CUOPT_PRESOLVE_FILE "presolve_file" #define CUOPT_RANDOM_SEED "random_seed" diff --git a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh index fc81d05d99..899ce7c2a4 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh +++ b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh @@ -47,11 +47,6 @@ struct pdlp_hyper_params_t { bool bound_objective_rescaling = true; bool use_reflected_primal_dual = true; bool use_fixed_point_error = true; - bool use_distributed_pdlp = false; - // Debug/diagnostic knob: when true, PDLP bypasses CUDA-graph capture in - // ping_pong_graph_t and executes each iteration eagerly - // Useful for cuD-PDLP in current state - bool pdlp_disable_graph = false; double reflection_coefficient = 1.0; double restart_k_p = 0.99; double restart_k_i = 0.01; diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index c38ff7f026..fb3aa29e33 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -307,6 +307,8 @@ class pdlp_solver_settings_t { presolver_t presolver{presolver_t::Default}; bool dual_postsolve{true}; int num_gpus{1}; + // Dispatch the LP to the multi-GPU distributed PDLP engine + bool use_distributed_pdlp{false}; // Number of GPUs to use specifically for distributed PDLP (use_distributed_pdlp=true). // -1 means auto-detect int distributed_pdlp_num_gpus{-1}; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 353ff76978..85a9189a3f 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -192,8 +192,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_DUAL_POSTSOLVE, &pdlp_settings.dual_postsolve, true}, {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, {CUOPT_MIP_PROBING, &mip_settings.probing, true}, - {CUOPT_USE_DISTRIBUTED_PDLP, &pdlp_settings.hyper_params.use_distributed_pdlp, false}, - {CUOPT_PDLP_DISABLE_GRAPH, &pdlp_settings.hyper_params.pdlp_disable_graph, false}, + {CUOPT_USE_DISTRIBUTED_PDLP, &pdlp_settings.use_distributed_pdlp, false}, // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") {CUOPT_MIP_HYPER_DIVING_SHOW_TYPE, &mip_settings.diving_params.show_type, false, "log diving heuristic type when it finds a new incumbent"}, }; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index d06c5c4c11..292b5bcbfc 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2708,7 +2708,7 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co bool warm_start_was_given = settings_.get_pdlp_warm_start_data().is_populated(); // In distributed mode, skip all setup, it is already done - if (!settings_.hyper_params.use_distributed_pdlp) { + if (!settings_.use_distributed_pdlp) { // TODO handle that properly if (settings_.hyper_params.compute_initial_step_size_before_scaling && !settings_.get_initial_step_size().has_value()) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 3bfbd1f494..937346ed4b 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -761,13 +761,11 @@ static optimization_problem_solution_t run_pdlp_solver( const timer_t& timer, bool is_batch_mode) { - cuopt_expects(!settings.hyper_params.use_distributed_pdlp, + cuopt_expects(!settings.use_distributed_pdlp, error_type_t::ValidationError, "Distributed PDLP must be entered via solve_lp(mps_data_model, ...) " "so the master GPU never materializes the full problem. Call sites " "with a problem_t cannot dispatch to distributed mode."); - detail::pdlp_graph_disabled_flag().store(settings.hyper_params.pdlp_disable_graph, - std::memory_order_relaxed); if (problem.n_constraints == 0) { CUOPT_LOG_CONDITIONAL_INFO( @@ -2221,7 +2219,7 @@ optimization_problem_solution_t solve_lp( bool problem_checking, bool use_pdlp_solver_mode) { - if (settings.hyper_params.use_distributed_pdlp) { + if (settings.use_distributed_pdlp) { return solve_lp_distributed_from_mps( handle_ptr, mps_data_model, settings, problem_checking, use_pdlp_solver_mode); } @@ -2240,10 +2238,9 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( cuopt_expects(handle_ptr != nullptr, error_type_t::ValidationError, "solve_lp_distributed_from_mps: handle_ptr must not be null"); - cuopt_expects(settings.hyper_params.use_distributed_pdlp, + cuopt_expects(settings.use_distributed_pdlp, error_type_t::ValidationError, - "solve_lp_distributed_from_mps: settings.hyper_params.use_distributed_pdlp " - "must be true"); + "solve_lp_distributed_from_mps: settings.use_distributed_pdlp must be true"); cuopt_expects(settings.presolver == cuopt::linear_programming::presolver_t::None, error_type_t::ValidationError, "solve_lp_distributed_from_mps: presolve is not yet supported with " @@ -2255,9 +2252,6 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( "Distributed MPS solve currently supports only method_t::PDLP"); if (use_pdlp_solver_mode) { set_pdlp_solver_mode(settings_resolved); } - detail::pdlp_graph_disabled_flag().store(settings_resolved.hyper_params.pdlp_disable_graph, - std::memory_order_relaxed); - if (settings_resolved.distributed_pdlp_num_gpus == -1) { settings_resolved.distributed_pdlp_num_gpus = raft::device_setter::get_device_count(); CUOPT_LOG_INFO( diff --git a/cpp/src/pdlp/solve.cuh b/cpp/src/pdlp/solve.cuh index da9e96e437..654fd56b18 100644 --- a/cpp/src/pdlp/solve.cuh +++ b/cpp/src/pdlp/solve.cuh @@ -49,8 +49,8 @@ cuopt::linear_programming::optimization_problem_solution_t solve_lp_wi * and returns the gathered solution. * * Resolves the `distributed_pdlp_num_gpus == -1` sentinel against the - * visible-device count and propagates `pdlp_disable_graph` to the CUDA-graph - * flag. Several configurations are rejected up front (see @pre). + * visible-device count. Several configurations are rejected up front + * (see @pre). * * @param handle_ptr Master raft handle (its stream owns the gather buffers and * any master-side aggregator allocations). Must be non-null. @@ -63,7 +63,7 @@ cuopt::linear_programming::optimization_problem_solution_t solve_lp_wi * @param use_pdlp_solver_mode When true, applies set_pdlp_solver_mode() to the * resolved settings before solving. * - * @pre `settings.hyper_params.use_distributed_pdlp == true`, `method == PDLP`, + * @pre `settings.use_distributed_pdlp == true`, `method == PDLP`, * `presolver == None`, `pdlp_precision == DefaultPrecision`, not inside * MIP, and no initial primal/dual or warm-start data. */ diff --git a/cpp/src/pdlp/utilities/ping_pong_graph.cuh b/cpp/src/pdlp/utilities/ping_pong_graph.cuh index 6b527f81b2..dbc8fe5828 100644 --- a/cpp/src/pdlp/utilities/ping_pong_graph.cuh +++ b/cpp/src/pdlp/utilities/ping_pong_graph.cuh @@ -12,25 +12,10 @@ #include -#include #include namespace cuopt::linear_programming::detail { -// Debug/diagnostic toggle: when set, ping_pong_graph_t::run() bypasses CUDA -// graph capture and executes its work eagerly on every iteration. Useful for -// for debugging -inline std::atomic& pdlp_graph_disabled_flag() -{ - static std::atomic s_flag{false}; - return s_flag; -} - -inline bool pdlp_graph_disabled() -{ - return pdlp_graph_disabled_flag().load(std::memory_order_relaxed); -} - // Two-slot CUDA-graph cache for PDLP. PDLP swaps pointers (rather than // copying vectors) at the end of adaptive pdhg step, so the captured graph // topology alternates between two layouts depending on iteration parity. @@ -64,7 +49,7 @@ class ping_pong_graph_t { #ifdef CUPDLP_DEBUG_MODE work(); #else - if (is_legacy_batch_mode_ || pdlp_graph_disabled()) { + if (is_legacy_batch_mode_) { work(); return; } diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index f879f962c8..d2584a0810 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -188,12 +188,9 @@ void expect_distributed_matches_base(raft::handle_t const& handle, // ----- distributed PDLP (identical settings, only the distributed flags flipped) ----- // testing on only one gpu is allowed and is already a great test for the distributed path pdlp_solver_settings_t dist_settings = base_settings; - dist_settings.hyper_params.use_distributed_pdlp = true; + dist_settings.use_distributed_pdlp = true; dist_settings.distributed_pdlp_num_gpus = -1; - // Disable CUDA-graph capture for the distributed run (execute each iteration - // eagerly); KaMinPar graph partitioning is left enabled. - dist_settings.hyper_params.pdlp_disable_graph = true; - auto dist = solve_lp(&handle, problem, dist_settings); + auto dist = solve_lp(&handle, problem, dist_settings); // ----- termination status ----- ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) From cb55b7b104bb32cca3c7bbb53e740f4b42b155e8 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 24 Jun 2026 16:07:38 +0200 Subject: [PATCH 129/258] added warning if not fully nvlink-connected --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index c7e3aaa7bd..0bdd196ec2 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -14,11 +14,51 @@ #include #include +#include #include namespace cuopt::linear_programming::detail { +namespace { + +// return true if the two GPUs are NVLink connected +bool pair_is_nvlink(int a, int b) +{ + int can = 0, rank = 0; + if (cudaDeviceCanAccessPeer(&can, a, b) != cudaSuccess || !can) return false; + if (cudaDeviceGetP2PAttribute(&rank, cudaDevP2PAttrPerformanceRank, a, b) != cudaSuccess) { + return false; + } + return rank >= 1; +} + +// Distributed PDLP iterations are dominated by NCCL halo exchanges. +// Warn if the GPU set isn't a fully-meshed NVLink topology so the +// user knows why iterations may be slow. +void warn_if_not_fully_nvlink(std::vector const& devices) +{ + if (devices.size() <= 1) return; + + std::string bad; + for (size_t a = 0; a < devices.size(); ++a) { + for (size_t b = a + 1; b < devices.size(); ++b) { + if (!pair_is_nvlink(devices[a], devices[b]) || !pair_is_nvlink(devices[b], devices[a])) { + bad += " (" + std::to_string(devices[a]) + "," + std::to_string(devices[b]) + ")"; + } + } + } + if (bad.empty()) return; + + CUOPT_LOG_WARN( + "distributed_pdlp: GPU pair(s) not NVLink-class:%s. NCCL halo exchanges " + "will be bottlenecked by the slowest link; for best performance use a " + "system with full NVLink (e.g. NVSwitch on DGX). See `nvidia-smi topo -m`.", + bad.c_str()); +} + +} // namespace + template multi_gpu_engine_t::multi_gpu_engine_t( std::vector>&& rank_data, @@ -34,6 +74,8 @@ multi_gpu_engine_t::multi_gpu_engine_t( std::vector devices(nb_parts); std::iota(devices.begin(), devices.end(), 0); + warn_if_not_fully_nvlink(devices); + // Create NCCL Comms, then immediately wrap each in a RAII owner so they are // destroyed on any exception (e.g. a shard ctor throwing) before being // handed off to a shard. From 1a5b941ecc71c06c2c0b91a63597f339c6da118b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 24 Jun 2026 16:07:54 +0200 Subject: [PATCH 130/258] style --- .../linear_programming/pdlp/pdlp_hyper_params.cuh | 12 ++++++------ cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu | 2 +- cpp/tests/linear_programming/pdlp_test.cu | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh index 899ce7c2a4..282e91d7ef 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh +++ b/cpp/include/cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh @@ -47,12 +47,12 @@ struct pdlp_hyper_params_t { bool bound_objective_rescaling = true; bool use_reflected_primal_dual = true; bool use_fixed_point_error = true; - double reflection_coefficient = 1.0; - double restart_k_p = 0.99; - double restart_k_i = 0.01; - double restart_k_d = 0.0; - double restart_i_smooth = 0.3; - bool use_conditional_major = true; + double reflection_coefficient = 1.0; + double restart_k_p = 0.99; + double restart_k_i = 0.01; + double restart_k_d = 0.0; + double restart_i_smooth = 0.3; + bool use_conditional_major = true; }; // TODO most likely we want to get rid of pdlp_solver_mode and just have prebuilt diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 0bdd196ec2..5595aab9cb 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -33,7 +33,7 @@ bool pair_is_nvlink(int a, int b) return rank >= 1; } -// Distributed PDLP iterations are dominated by NCCL halo exchanges. +// Distributed PDLP iterations are dominated by NCCL halo exchanges. // Warn if the GPU set isn't a fully-meshed NVLink topology so the // user knows why iterations may be slow. void warn_if_not_fully_nvlink(std::vector const& devices) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index d2584a0810..f746b9339b 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -190,7 +190,7 @@ void expect_distributed_matches_base(raft::handle_t const& handle, pdlp_solver_settings_t dist_settings = base_settings; dist_settings.use_distributed_pdlp = true; dist_settings.distributed_pdlp_num_gpus = -1; - auto dist = solve_lp(&handle, problem, dist_settings); + auto dist = solve_lp(&handle, problem, dist_settings); // ----- termination status ----- ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) From 368b3b322c96debe9c4313887c90b33c6c89502a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 25 Jun 2026 18:14:07 +0200 Subject: [PATCH 131/258] removed not-working warning on non-nvlink system --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 5595aab9cb..c7e3aaa7bd 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -14,51 +14,11 @@ #include #include -#include #include namespace cuopt::linear_programming::detail { -namespace { - -// return true if the two GPUs are NVLink connected -bool pair_is_nvlink(int a, int b) -{ - int can = 0, rank = 0; - if (cudaDeviceCanAccessPeer(&can, a, b) != cudaSuccess || !can) return false; - if (cudaDeviceGetP2PAttribute(&rank, cudaDevP2PAttrPerformanceRank, a, b) != cudaSuccess) { - return false; - } - return rank >= 1; -} - -// Distributed PDLP iterations are dominated by NCCL halo exchanges. -// Warn if the GPU set isn't a fully-meshed NVLink topology so the -// user knows why iterations may be slow. -void warn_if_not_fully_nvlink(std::vector const& devices) -{ - if (devices.size() <= 1) return; - - std::string bad; - for (size_t a = 0; a < devices.size(); ++a) { - for (size_t b = a + 1; b < devices.size(); ++b) { - if (!pair_is_nvlink(devices[a], devices[b]) || !pair_is_nvlink(devices[b], devices[a])) { - bad += " (" + std::to_string(devices[a]) + "," + std::to_string(devices[b]) + ")"; - } - } - } - if (bad.empty()) return; - - CUOPT_LOG_WARN( - "distributed_pdlp: GPU pair(s) not NVLink-class:%s. NCCL halo exchanges " - "will be bottlenecked by the slowest link; for best performance use a " - "system with full NVLink (e.g. NVSwitch on DGX). See `nvidia-smi topo -m`.", - bad.c_str()); -} - -} // namespace - template multi_gpu_engine_t::multi_gpu_engine_t( std::vector>&& rank_data, @@ -74,8 +34,6 @@ multi_gpu_engine_t::multi_gpu_engine_t( std::vector devices(nb_parts); std::iota(devices.begin(), devices.end(), 0); - warn_if_not_fully_nvlink(devices); - // Create NCCL Comms, then immediately wrap each in a RAII owner so they are // destroyed on any exception (e.g. a shard ctor throwing) before being // handed off to a shard. From 6948bc5fe38df0f7660385aa2b31ec7cca4fd25b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 25 Jun 2026 18:17:00 +0200 Subject: [PATCH 132/258] style --- skills/cuopt-multi-objective-exploration/skill-card.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/cuopt-multi-objective-exploration/skill-card.md b/skills/cuopt-multi-objective-exploration/skill-card.md index 36ec7726d1..b76846c100 100644 --- a/skills/cuopt-multi-objective-exploration/skill-card.md +++ b/skills/cuopt-multi-objective-exploration/skill-card.md @@ -16,7 +16,7 @@ Global
## Requirements / Dependencies:
**Requires API Key or External Credential:** [Not Specified]
-**Credential Type(s):** [None identified]
+**Credential Type(s):** [None identified]
Do not include secrets in prompts/logs/output; use least-privilege credentials; rotate keys as appropriate.
From 1563cdca10e14f74de0a3c86b3f7ab309444cfd9 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 25 Jun 2026 22:59:36 +0200 Subject: [PATCH 133/258] updated assert to allow skeletal master problem to build --- cpp/src/pdlp/cusparse_view.cu | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/cusparse_view.cu b/cpp/src/pdlp/cusparse_view.cu index a789d405be..f838e40ba5 100644 --- a/cpp/src/pdlp/cusparse_view.cu +++ b/cpp/src/pdlp/cusparse_view.cu @@ -613,7 +613,9 @@ cusparse_view_t::cusparse_view_t( op_problem_scaled.n_variables, current_saddle_point_state.get_next_AtY().data(), CUSPARSE_ORDER_COL); - cuopt_assert(_reflected_primal_solution.size() > 0, "Reflected primal solution empty"); + cuopt_assert(_reflected_primal_solution.size() >= + static_cast(op_problem_scaled.n_variables) * climber_strategies.size(), + "Reflected primal solution undersized"); batch_reflected_primal_solutions.create(op_problem_scaled.n_variables, climber_strategies.size(), climber_strategies.size(), @@ -664,7 +666,9 @@ cusparse_view_t::cusparse_view_t( tmp_primal.create(op_problem_scaled.n_variables, _tmp_primal.data()); tmp_dual.create(op_problem_scaled.n_constraints, _tmp_dual.data()); if (hyper_params.use_reflected_primal_dual) { - cuopt_assert(_reflected_primal_solution.size() > 0, "Reflected primal solution empty"); + cuopt_assert(_reflected_primal_solution.size() >= + static_cast(op_problem_scaled.n_variables), + "Reflected primal solution undersized"); reflected_primal_solution.create(op_problem_scaled.n_variables, _reflected_primal_solution.data()); } From b14ffcc0deadf3a8c7205b65f2c834e1c3bd2caf Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 26 Jun 2026 10:27:46 +0200 Subject: [PATCH 134/258] style --- cpp/src/pdlp/cusparse_view.cu | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/pdlp/cusparse_view.cu b/cpp/src/pdlp/cusparse_view.cu index f838e40ba5..3cbb4539fc 100644 --- a/cpp/src/pdlp/cusparse_view.cu +++ b/cpp/src/pdlp/cusparse_view.cu @@ -666,9 +666,9 @@ cusparse_view_t::cusparse_view_t( tmp_primal.create(op_problem_scaled.n_variables, _tmp_primal.data()); tmp_dual.create(op_problem_scaled.n_constraints, _tmp_dual.data()); if (hyper_params.use_reflected_primal_dual) { - cuopt_assert(_reflected_primal_solution.size() >= - static_cast(op_problem_scaled.n_variables), - "Reflected primal solution undersized"); + cuopt_assert( + _reflected_primal_solution.size() >= static_cast(op_problem_scaled.n_variables), + "Reflected primal solution undersized"); reflected_primal_solution.create(op_problem_scaled.n_variables, _reflected_primal_solution.data()); } From 7bb6945954b63f19726598f8e9f54558f32a26e3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 26 Jun 2026 14:01:35 +0200 Subject: [PATCH 135/258] better compile option for kaminpar --- cpp/cmake/thirdparty/get_kaminpar.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cpp/cmake/thirdparty/get_kaminpar.cmake b/cpp/cmake/thirdparty/get_kaminpar.cmake index 8e060544a2..c20880b332 100644 --- a/cpp/cmake/thirdparty/get_kaminpar.cmake +++ b/cpp/cmake/thirdparty/get_kaminpar.cmake @@ -31,6 +31,13 @@ function(find_and_configure_kaminpar) "KAMINPAR_BUILD_BENCHMARKS OFF" "KAMINPAR_BUILD_EXAMPLES OFF" "KAMINPAR_BUILD_DISTRIBUTED OFF" + # KaMinPar defaults this to ON, which injects `-march=native -mtune=native` + # (and propagates the same flag to its bundled KaHyPar / KaGen via + # KAHYPAR_ENABLE_ARCH_COMPILE_OPTIMIZATIONS / KAGEN_USE_MARCH_NATIVE). + # That bakes in whatever ISA the build host happens to support + # (e.g. AVX-512 on Sapphire Rapids), which then SIGILLs on any test + # host with a leaner CPU. Force generic codegen instead. + "KAMINPAR_BUILD_WITH_MTUNE_NATIVE OFF" # Timers use global state and force single-threaded use of the library # interface; disable so cuOpt can call the partitioner freely. "KAMINPAR_ENABLE_TIMERS OFF" From 81aa2349b595af6e112d7488da5201f4857b3581 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 29 Jun 2026 19:24:00 +0200 Subject: [PATCH 136/258] and update code to compile --- .../pdlp/distributed_pdlp/distributed_algorithms.cu | 4 ++-- .../pdlp/distributed_pdlp/distributed_algorithms.hpp | 4 ++-- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu | 4 ++-- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp | 8 ++++---- cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp | 4 ++-- cpp/src/pdlp/distributed_pdlp/partition_loader.cu | 4 ++-- cpp/src/pdlp/distributed_pdlp/partition_loader.hpp | 4 ++-- cpp/src/pdlp/distributed_pdlp/partitioner.cpp | 4 ++-- cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 4 ++-- cpp/src/pdlp/distributed_pdlp/rank_data.hpp | 4 ++-- cpp/src/pdlp/distributed_pdlp/shard.cu | 4 ++-- cpp/src/pdlp/distributed_pdlp/shard.hpp | 12 ++++++------ cpp/src/pdlp/pdlp.cu | 6 +++--- cpp/src/pdlp/pdlp.cuh | 10 +++++----- cpp/src/pdlp/solve.cu | 10 +++++----- cpp/src/pdlp/solve.cuh | 5 +++-- cpp/tests/linear_programming/pdlp_test.cu | 8 ++++---- 17 files changed, 50 insertions(+), 49 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index f48cb659a1..6f6deb873a 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -16,7 +16,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { // -------- Broadcast owned constraint (row) scaling into halo -------------- template @@ -638,4 +638,4 @@ INSTANTIATE(float) #undef INSTANTIATE -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp index 9a00f83de8..7578acbe85 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp @@ -7,7 +7,7 @@ #include // Algorithm-level distributed PDLP -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { template struct multi_gpu_engine_t; @@ -65,4 +65,4 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng pdhg_solver_t& master_pdhg, rmm::device_uvector& master_reduced_cost); -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index c7e3aaa7bd..1a9e064022 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -17,7 +17,7 @@ #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { template multi_gpu_engine_t::multi_gpu_engine_t( @@ -143,4 +143,4 @@ void multi_gpu_engine_t::sync_await_shards(rmm::cuda_stream_view maste template struct multi_gpu_engine_t; template struct multi_gpu_engine_t; -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 1ce7a9889b..0cdad94aa1 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -11,8 +11,8 @@ #include #include -#include -#include +#include +#include #include #include @@ -39,7 +39,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { // Maps the solver floating-point type to the matching NCCL datatype so that // halo exchanges / all-reduces transfer the correct element size for both @@ -348,4 +348,4 @@ struct multi_gpu_engine_t { void sync_await_shards(rmm::cuda_stream_view master_stream); }; -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp index b9a6c3ac86..eec034bd6d 100644 --- a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp +++ b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp @@ -8,7 +8,7 @@ #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { // Wraps a NCCL call and throws cuopt::logic_error (RuntimeError) on any non- // ncclSuccess return code. @@ -23,4 +23,4 @@ namespace cuopt::linear_programming::detail { ::ncclGetErrorString(_cuopt_nccl_status)); \ } while (0) -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index a6af208deb..7eed272cc5 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -11,7 +11,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { template std::vector partition_loader_t::parse_distributed_pdlp_partition_file( @@ -294,4 +294,4 @@ std::vector> partition_loader_t::create_rank_dat template struct partition_loader_t; -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index d498940e09..27712cfbfe 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -10,7 +10,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { template struct partition_loader_t { @@ -40,4 +40,4 @@ struct partition_loader_t { i_t nnz); }; -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp index 71bddb538e..0b2f6dd6f9 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp @@ -23,7 +23,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { template std::vector dummy_partitioner_t::partition( @@ -215,4 +215,4 @@ template class kaminpar_partitioner_t; template std::unique_ptr> make_partitioner( partitioner_kind_t); -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index 6ab529d285..64e9de3a32 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -8,7 +8,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { // Non-owning view of a host CSR matrix (A or A_t). template @@ -72,4 +72,4 @@ void validate_partition(std::vector const& parts, template std::unique_ptr> make_partitioner(partitioner_kind_t kind); -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp index e9c701567a..5e55f76480 100644 --- a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp +++ b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp @@ -8,7 +8,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { // Pure data class representing most of the distributed data needed for operatiosn template struct rank_data_t { @@ -57,4 +57,4 @@ struct rank_data_t { std::vector h_A_t_col_indices; std::vector h_A_t_values; }; -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 3a8257a3eb..3a7a436945 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -15,7 +15,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { // This must be done in .cu file because the pdlp_solver_t is not already complete in the hpp file // This is caused by the problematic cyclic include of pdlp_solver_t @@ -199,4 +199,4 @@ pdlp_shard_t::pdlp_shard_t(int device_id, template struct pdlp_shard_t; template struct pdlp_shard_t; -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 8749e286ef..caa9139c10 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -6,9 +6,9 @@ #include -#include -#include -#include +#include +#include +#include #include #include @@ -22,7 +22,7 @@ #include #include -namespace cuopt::linear_programming::detail { +namespace cuopt::mathematical_optimization::pdlp { // Forward-declare to break the cyclic include with pdlp.cuh // (pdlp.cuh -> multi_gpu_engine.hpp -> shard.hpp -> pdlp.cuh). @@ -67,7 +67,7 @@ struct pdlp_shard_t { nccl_comm_unique_ptr_t comm; rank_data_t rank_data; std::optional> opt_problem; - std::optional> sub_problem; + std::optional> sub_problem; std::unique_ptr> sub_pdlp; // var_send_indices_d[peer] : local indices into primal vector to gather and ncclSend @@ -79,4 +79,4 @@ struct pdlp_shard_t { std::vector> cstr_send_buf_d; }; -} // namespace cuopt::linear_programming::detail +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 6056007b38..e3f180f729 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -386,8 +386,8 @@ pdlp_solver_t::pdlp_solver_t(mip::problem_t& op_problem, // builds the engine from the mps_data_model template pdlp_solver_t::pdlp_solver_t( - problem_t& placeholder_problem, - cuopt::linear_programming::io::mps_data_model_t const& mps, + mip::problem_t& placeholder_problem, + cuopt::mathematical_optimization::io::mps_data_model_t const& mps, pdlp_solver_settings_t const& settings) // Makes all inner feilds of master 0 size : pdlp_solver_t(placeholder_problem, settings, /*is_legacy_batch_mode=*/false) @@ -445,7 +445,7 @@ pdlp_solver_t::pdlp_solver_t( // CSC(A) and CSR(A^T) share the same memory layout, so the CSC produced // by dual_simplex::csr_matrix_t::to_compressed_col IS the CSR of A^T. // O(nnz + n_vars) counting sort, same as problem_t::compute_transpose. - namespace ds = cuopt::linear_programming::dual_simplex; + namespace ds = cuopt::mathematical_optimization::simplex; ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); A_csr.row_start = h_A_row_offsets; A_csr.j = h_A_col_indices; diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index b6d5e5e7d7..d649e8532e 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -65,8 +65,8 @@ class pdlp_solver_t { bool is_batch_mode = false); // Distributed Solver Constructor - pdlp_solver_t(problem_t& placeholder_problem, - cuopt::linear_programming::io::mps_data_model_t const& mps, + pdlp_solver_t(mip::problem_t& placeholder_problem, + cuopt::mathematical_optimization::io::mps_data_model_t const& mps, pdlp_solver_settings_t const& settings); optimization_problem_solution_t run_solver(const timer_t& timer); @@ -106,12 +106,12 @@ class pdlp_solver_t { void compute_initial_primal_weight(); // Needed by multi-GPU to mutate them - problem_t& get_op_problem_scaled() { return op_problem_scaled_; } - detail::pdlp_initial_scaling_strategy_t& get_initial_scaling_strategy() + mip::problem_t& get_op_problem_scaled() { return op_problem_scaled_; } + pdlp_initial_scaling_strategy_t& get_initial_scaling_strategy() { return initial_scaling_strategy_; } - detail::pdlp_restart_strategy_t& get_restart_strategy() { return restart_strategy_; } + pdlp_restart_strategy_t& get_restart_strategy() { return restart_strategy_; } // Per-shard primal/dual step sizes are private state on pdlp_solver_t but // are needed inside the multi-GPU dispatch paths that fan out a master cub diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 9c84d9f119..6133d3ca2a 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2221,7 +2221,7 @@ optimization_problem_solution_t solve_lp( template optimization_problem_solution_t solve_lp_distributed_from_mps( raft::handle_t const* handle_ptr, - const cuopt::linear_programming::io::mps_data_model_t& mps_data_model, + const cuopt::mathematical_optimization::io::mps_data_model_t& mps_data_model, pdlp_solver_settings_t const& settings, bool problem_checking, bool use_pdlp_solver_mode) @@ -2232,7 +2232,7 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( cuopt_expects(settings.use_distributed_pdlp, error_type_t::ValidationError, "solve_lp_distributed_from_mps: settings.use_distributed_pdlp must be true"); - cuopt_expects(settings.presolver == cuopt::linear_programming::presolver_t::None, + cuopt_expects(settings.presolver == cuopt::mathematical_optimization::presolver_t::None, error_type_t::ValidationError, "solve_lp_distributed_from_mps: presolve is not yet supported with " "use_distributed_pdlp; please set settings.presolver = presolver_t::None"); @@ -2284,15 +2284,15 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( auto lp_timer = cuopt::timer_t(settings_resolved.time_limit); // Shape-0 placeholder: needed to build an empty pdlp_solver - cuopt::linear_programming::optimization_problem_t placeholder_op(handle_ptr); + cuopt::mathematical_optimization::optimization_problem_t placeholder_op(handle_ptr); { std::vector empty_offsets = {0}; placeholder_op.set_csr_constraint_matrix( nullptr, 0, nullptr, 0, empty_offsets.data(), static_cast(empty_offsets.size())); } - detail::problem_t placeholder_problem(placeholder_op); + mip::problem_t placeholder_problem(placeholder_op); - detail::pdlp_solver_t solver(placeholder_problem, mps_data_model, settings_resolved); + pdlp::pdlp_solver_t solver(placeholder_problem, mps_data_model, settings_resolved); auto sol = solver.run_solver(lp_timer); diff --git a/cpp/src/pdlp/solve.cuh b/cpp/src/pdlp/solve.cuh index 0cdb298dd8..9250245079 100644 --- a/cpp/src/pdlp/solve.cuh +++ b/cpp/src/pdlp/solve.cuh @@ -69,9 +69,10 @@ cuopt::mathematical_optimization::optimization_problem_solution_t solv * MIP, and no initial primal/dual or warm-start data. */ template -cuopt::linear_programming::optimization_problem_solution_t solve_lp_distributed_from_mps( +cuopt::mathematical_optimization::optimization_problem_solution_t +solve_lp_distributed_from_mps( raft::handle_t const* handle_ptr, - const cuopt::linear_programming::io::mps_data_model_t& mps_data_model, + const cuopt::mathematical_optimization::io::mps_data_model_t& mps_data_model, pdlp_solver_settings_t const& settings, bool problem_checking, bool use_pdlp_solver_mode); diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 5dad25674f..f92171b2bf 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -103,12 +103,12 @@ TEST(pdlp_class, run_double) // vector is identical to what the partitioner produced. TEST(pdlp_class, distributed_partition_kaminpar_export_import_roundtrip) { - using namespace cuopt::linear_programming::detail; - namespace ds = cuopt::linear_programming::dual_simplex; + using namespace cuopt::mathematical_optimization::pdlp; + namespace ds = cuopt::mathematical_optimization::simplex; auto path = make_path_absolute("linear_programming/afiro_original.mps"); - cuopt::linear_programming::io::mps_data_model_t mps = - cuopt::linear_programming::io::read_mps(path, true); + cuopt::mathematical_optimization::io::mps_data_model_t mps = + cuopt::mathematical_optimization::io::read_mps(path, true); const int n_vars = static_cast(mps.get_objective_coefficients().size()); const int n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); From 58cb7741aaa7caddee147b96d127bff4c50b1627 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 29 Jun 2026 20:41:33 +0200 Subject: [PATCH 137/258] updated asserts to match skeletal data --- cpp/src/pdlp/pdlp.cu | 51 ++++++++++++------- .../convergence_information.cu | 29 ++++++----- .../termination_strategy.cu | 16 +++--- 3 files changed, 61 insertions(+), 35 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index e3f180f729..e125a9069f 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2331,23 +2331,40 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte #ifdef CUPDLP_DEBUG_MODE printf("Computing compute_fixed_point_error \n"); #endif - cuopt_assert( - pdhg_solver_.get_reflected_primal().size() == primal_size_h_ * climber_strategies_.size(), - "reflected_primal_ size mismatch"); - cuopt_assert( - pdhg_solver_.get_reflected_dual().size() == dual_size_h_ * climber_strategies_.size(), - "reflected_dual_ size mismatch"); - cuopt_assert( - pdhg_solver_.get_primal_solution().size() == primal_size_h_ * climber_strategies_.size(), - "primal_solution_ size mismatch"); - cuopt_assert(pdhg_solver_.get_dual_solution().size() == dual_size_h_ * climber_strategies_.size(), - "dual_solution_ size mismatch"); - cuopt_assert(pdhg_solver_.get_saddle_point_state().get_delta_primal().size() == - primal_size_h_ * climber_strategies_.size(), - "delta_primal_ size mismatch"); - cuopt_assert(pdhg_solver_.get_saddle_point_state().get_delta_dual().size() == - dual_size_h_ * climber_strategies_.size(), - "delta_dual_ size mismatch"); + if (is_distributed_master()) { + // The master's own pdhg_solver_ buffers are built from a shape-0 placeholder and must be empty + cuopt_assert(pdhg_solver_.get_reflected_primal().size() == 0, + "master must not own a reflected_primal_ buffer (shards compute it)"); + cuopt_assert(pdhg_solver_.get_reflected_dual().size() == 0, + "master must not own a reflected_dual_ buffer (shards compute it)"); + cuopt_assert(pdhg_solver_.get_primal_solution().size() == 0, + "master must not own a primal_solution_ buffer (shards compute it)"); + cuopt_assert(pdhg_solver_.get_dual_solution().size() == 0, + "master must not own a dual_solution_ buffer (shards compute it)"); + cuopt_assert(pdhg_solver_.get_saddle_point_state().get_delta_primal().size() == 0, + "master must not own a delta_primal_ buffer (shards compute it)"); + cuopt_assert(pdhg_solver_.get_saddle_point_state().get_delta_dual().size() == 0, + "master must not own a delta_dual_ buffer (shards compute it)"); + } else { + cuopt_assert( + pdhg_solver_.get_reflected_primal().size() == primal_size_h_ * climber_strategies_.size(), + "reflected_primal_ size mismatch"); + cuopt_assert( + pdhg_solver_.get_reflected_dual().size() == dual_size_h_ * climber_strategies_.size(), + "reflected_dual_ size mismatch"); + cuopt_assert( + pdhg_solver_.get_primal_solution().size() == primal_size_h_ * climber_strategies_.size(), + "primal_solution_ size mismatch"); + cuopt_assert( + pdhg_solver_.get_dual_solution().size() == dual_size_h_ * climber_strategies_.size(), + "dual_solution_ size mismatch"); + cuopt_assert(pdhg_solver_.get_saddle_point_state().get_delta_primal().size() == + primal_size_h_ * climber_strategies_.size(), + "delta_primal_ size mismatch"); + cuopt_assert(pdhg_solver_.get_saddle_point_state().get_delta_dual().size() == + dual_size_h_ * climber_strategies_.size(), + "delta_dual_ size mismatch"); + } // Computing the deltas (delta = reflected - current) // TODO batch mdoe: this only works if everyone restarts diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index b2fc8f9a7c..4468a9b7ff 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -602,22 +602,27 @@ void convergence_information_t::compute_convergence_information( const rmm::device_uvector& objective_coefficients, const pdlp_solver_settings_t& settings) { - cuopt_assert( - primal_residual_.size() == dual_size_h_ * climber_strategies_.size(), - "primal_residual_ size must be equal to primal_size_h_ * climber_strategies_.size()"); - cuopt_assert(primal_iterate.size() == primal_size_h_ * climber_strategies_.size(), - "primal_iterate size must be equal to primal_size_h_ * climber_strategies_.size()"); - cuopt_assert(dual_residual_.size() == primal_size_h_ * climber_strategies_.size(), - "dual_residual_ size must be equal to primal_size_h_ * climber_strategies_.size()"); - cuopt_assert(dual_iterate.size() == dual_size_h_ * climber_strategies_.size(), - "dual_iterate size must be equal to dual_size_h_ * climber_strategies_.size()"); - cuopt_assert(l2_primal_residual_.size() == climber_strategies_.size(), - "l2_primal_residual_ size must be equal to climber_strategies_.size()"); + // Per-climber L2-norm scalars are uniform across single-GPU, master, and shards. cuopt_assert(l2_primal_residual_.size() == climber_strategies_.size(), "l2_primal_residual_ size must be equal to climber_strategies_.size()"); cuopt_assert(l2_dual_residual_.size() == climber_strategies_.size(), "l2_dual_residual_ size must be equal to climber_strategies_.size()"); - + if (current_pdhg_solver.is_distributed_master()) { + cuopt_assert(primal_residual_.size() == 0, + "master must not own a primal_residual_ buffer (shards compute it)"); + cuopt_assert(dual_residual_.size() == 0, + "master must not own a dual_residual_ buffer (shards compute it)"); + } else { + cuopt_assert(primal_iterate.size() == primal_size_h_ * climber_strategies_.size(), + "primal_iterate size must be equal to primal_size_h_ * climber_strategies_.size()"); + cuopt_assert(dual_iterate.size() == dual_size_h_ * climber_strategies_.size(), + "dual_iterate size must be equal to dual_size_h_ * climber_strategies_.size()"); + cuopt_assert( + primal_residual_.size() == dual_size_h_ * climber_strategies_.size(), + "primal_residual_ size must be equal to dual_size_h_ * climber_strategies_.size()"); + cuopt_assert(dual_residual_.size() == primal_size_h_ * climber_strategies_.size(), + "dual_residual_ size must be equal to primal_size_h_ * climber_strategies_.size()"); + } raft::common::nvtx::range fun_scope("compute_convergence_information"); #ifdef CUPDLP_DEBUG_MODE diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index 4cbf4c9d67..56e681b94c 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -556,12 +556,16 @@ pdlp_termination_strategy_t::fill_return_problem_solution( std::vector&& termination_status, bool deep_copy) { - cuopt_assert( - primal_iterate.size() == current_pdhg_solver.get_primal_size() * termination_status.size(), - "Primal iterate size mismatch"); - cuopt_assert( - dual_iterate.size() == current_pdhg_solver.get_dual_size() * termination_status.size(), - "Dual iterate size mismatch"); + // Skip the size checks on the distributed master: its pdhg_solver_ is built from a + // shape-0 placeholder while termination_strategy is built from the full problem size + if (!current_pdhg_solver.is_distributed_master()) { + cuopt_assert( + primal_iterate.size() == current_pdhg_solver.get_primal_size() * termination_status.size(), + "Primal iterate size mismatch"); + cuopt_assert( + dual_iterate.size() == current_pdhg_solver.get_dual_size() * termination_status.size(), + "Dual iterate size mismatch"); + } // In distributed PDLP, gather solutions to master here if (!deep_copy) { From 21cdcccd7abe197806bf5f01237a3a73fd395449 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 29 Jun 2026 20:41:54 +0200 Subject: [PATCH 138/258] style --- cpp/cuopt_cli.cpp | 4 ++-- .../termination_strategy/convergence_information.cu | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 1f1a187c82..6e7058117e 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -435,8 +435,8 @@ int main(int argc, char* argv[]) auto memory_backend = cuopt::mathematical_optimization::get_memory_backend_type(); std::vector memory_resources; -if (memory_backend == cuopt::mathematical_optimization::memory_backend_t::GPU) { - // Get the right number of GPUs + if (memory_backend == cuopt::mathematical_optimization::memory_backend_t::GPU) { + // Get the right number of GPUs // Distributed PDLP uses its own knob: distributed_pdlp_num_gpus // Everything else uses num_gpus which is capped at 2 const bool use_distributed_pdlp = settings.get_parameter(CUOPT_USE_DISTRIBUTED_PDLP); diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 4468a9b7ff..37242d8c34 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -613,15 +613,17 @@ void convergence_information_t::compute_convergence_information( cuopt_assert(dual_residual_.size() == 0, "master must not own a dual_residual_ buffer (shards compute it)"); } else { - cuopt_assert(primal_iterate.size() == primal_size_h_ * climber_strategies_.size(), - "primal_iterate size must be equal to primal_size_h_ * climber_strategies_.size()"); + cuopt_assert( + primal_iterate.size() == primal_size_h_ * climber_strategies_.size(), + "primal_iterate size must be equal to primal_size_h_ * climber_strategies_.size()"); cuopt_assert(dual_iterate.size() == dual_size_h_ * climber_strategies_.size(), "dual_iterate size must be equal to dual_size_h_ * climber_strategies_.size()"); cuopt_assert( primal_residual_.size() == dual_size_h_ * climber_strategies_.size(), "primal_residual_ size must be equal to dual_size_h_ * climber_strategies_.size()"); - cuopt_assert(dual_residual_.size() == primal_size_h_ * climber_strategies_.size(), - "dual_residual_ size must be equal to primal_size_h_ * climber_strategies_.size()"); + cuopt_assert( + dual_residual_.size() == primal_size_h_ * climber_strategies_.size(), + "dual_residual_ size must be equal to primal_size_h_ * climber_strategies_.size()"); } raft::common::nvtx::range fun_scope("compute_convergence_information"); From b7d4d91a93ad846229b1fcdc89ceb4ae1f6fba1b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 30 Jun 2026 08:19:27 -0700 Subject: [PATCH 139/258] updated moved includes --- cpp/src/pdlp/pdlp.cu | 6 +++--- cpp/tests/linear_programming/pdlp_test.cu | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index e125a9069f..dce932f2be 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -17,7 +17,7 @@ #include #include -#include +#include #include #include "cuopt/mathematical_optimization/pdlp/solver_solution.hpp" #include "distributed_pdlp/distributed_algorithms.hpp" @@ -443,9 +443,9 @@ pdlp_solver_t::pdlp_solver_t( // ----- 2. Transpose A -> A^T on the host (one-shot CSR transpose) ----- // CSC(A) and CSR(A^T) share the same memory layout, so the CSC produced - // by dual_simplex::csr_matrix_t::to_compressed_col IS the CSR of A^T. + // by csr_matrix_t::to_compressed_col IS the CSR of A^T. // O(nnz + n_vars) counting sort, same as problem_t::compute_transpose. - namespace ds = cuopt::mathematical_optimization::simplex; + namespace ds = cuopt::mathematical_optimization; ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); A_csr.row_start = h_A_row_offsets; A_csr.j = h_A_col_indices; diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index f92171b2bf..3b8edaa4fb 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -16,7 +16,7 @@ #include #include -#include +#include #include "utilities/pdlp_test_utilities.cuh" @@ -104,7 +104,7 @@ TEST(pdlp_class, run_double) TEST(pdlp_class, distributed_partition_kaminpar_export_import_roundtrip) { using namespace cuopt::mathematical_optimization::pdlp; - namespace ds = cuopt::mathematical_optimization::simplex; + namespace ds = cuopt::mathematical_optimization; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::mathematical_optimization::io::mps_data_model_t mps = From cdd9a4da025bf8079021f72ae50f367a4c8c0e8e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 30 Jun 2026 08:24:04 -0700 Subject: [PATCH 140/258] added host undo presolve --- .../presolve/third_party_presolve.cpp | 135 ++++++++++-------- .../presolve/third_party_presolve.hpp | 23 ++- 2 files changed, 97 insertions(+), 61 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 08bd7779a0..4aed6f12b4 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -768,32 +768,39 @@ third_party_presolve_result_t third_party_presolve_t::apply( } template -void third_party_presolve_t::undo(rmm::device_uvector& primal_solution, - rmm::device_uvector& dual_solution, - rmm::device_uvector& reduced_costs, - problem_category_t category, - bool status_to_skip, - bool dual_postsolve, - rmm::cuda_stream_view stream_view) +void third_party_presolve_t::undo_pslp_host(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs) { - if (presolver_ == cuopt::mathematical_optimization::presolver_t::PSLP) { - undo_pslp(primal_solution, dual_solution, reduced_costs, stream_view); - return; - } + if constexpr (std::is_same_v) { + // PSLP postsolve reads from the passed-in host buffers and writes the + // uncrushed solution into pslp_presolver_->sol->{x, y, z}. + postsolve( + pslp_presolver_, primal_solution.data(), dual_solution.data(), reduced_costs.data()); - if (status_to_skip) { return; } + auto uncrushed_sol = pslp_presolver_->sol; + const int n_cols = uncrushed_sol->dim_x; + const int n_rows = uncrushed_sol->dim_y; - std::vector primal_sol_vec_h(primal_solution.size()); - raft::copy(primal_sol_vec_h.data(), primal_solution.data(), primal_solution.size(), stream_view); - std::vector dual_sol_vec_h(dual_solution.size()); - raft::copy(dual_sol_vec_h.data(), dual_solution.data(), dual_solution.size(), stream_view); - std::vector reduced_costs_vec_h(reduced_costs.size()); - raft::copy(reduced_costs_vec_h.data(), reduced_costs.data(), reduced_costs.size(), stream_view); + primal_solution.assign(uncrushed_sol->x, uncrushed_sol->x + n_cols); + dual_solution.assign(uncrushed_sol->y, uncrushed_sol->y + n_rows); + reduced_costs.assign(uncrushed_sol->z, uncrushed_sol->z + n_cols); + } else { + cuopt_expects( + false, error_type_t::ValidationError, "PSLP postsolve only supports double precision"); + } +} - papilo::Solution reduced_sol(primal_sol_vec_h); +template +void third_party_presolve_t::undo_papilo_host(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + bool dual_postsolve) +{ + papilo::Solution reduced_sol(primal_solution); if (dual_postsolve) { - reduced_sol.dual = dual_sol_vec_h; - reduced_sol.reducedCosts = reduced_costs_vec_h; + reduced_sol.dual = dual_solution; + reduced_sol.reducedCosts = reduced_costs; reduced_sol.type = papilo::SolutionType::kPrimalDual; } papilo::Solution full_sol; @@ -806,50 +813,64 @@ void third_party_presolve_t::undo(rmm::device_uvector& primal_sol auto status = post_solver.undo(reduced_sol, full_sol, *papilo_post_solve_storage_, is_optimal); check_postsolve_status(status); - primal_solution.resize(full_sol.primal.size(), stream_view); - dual_solution.resize(full_sol.dual.size(), stream_view); - reduced_costs.resize(full_sol.reducedCosts.size(), stream_view); - raft::copy(primal_solution.data(), full_sol.primal.data(), full_sol.primal.size(), stream_view); - raft::copy(dual_solution.data(), full_sol.dual.data(), full_sol.dual.size(), stream_view); - raft::copy( - reduced_costs.data(), full_sol.reducedCosts.data(), full_sol.reducedCosts.size(), stream_view); + primal_solution = std::move(full_sol.primal); + dual_solution = std::move(full_sol.dual); + reduced_costs = std::move(full_sol.reducedCosts); } template -void third_party_presolve_t::undo_pslp(rmm::device_uvector& primal_solution, - rmm::device_uvector& dual_solution, - rmm::device_uvector& reduced_costs, - rmm::cuda_stream_view stream_view) +void third_party_presolve_t::undo_host(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + problem_category_t /*category*/, + bool status_to_skip, + bool dual_postsolve) { - if constexpr (std::is_same_v) { - // PSLP uses double internally, so we can use the data directly - std::vector h_primal_solution(primal_solution.size()); - std::vector h_dual_solution(dual_solution.size()); - std::vector h_reduced_costs(reduced_costs.size()); - raft::copy( - h_primal_solution.data(), primal_solution.data(), primal_solution.size(), stream_view); - raft::copy(h_dual_solution.data(), dual_solution.data(), dual_solution.size(), stream_view); - raft::copy(h_reduced_costs.data(), reduced_costs.data(), reduced_costs.size(), stream_view); - stream_view.synchronize(); + // Matches apply()'s dispatch: PSLP is the only branch that's special-cased; + // every other value of `presolver_` (Papilo / Default / None) runs the + // Papilo postsolve, which is a no-op short-circuit on status_to_skip. + if (presolver_ == cuopt::mathematical_optimization::presolver_t::PSLP) { + undo_pslp_host(primal_solution, dual_solution, reduced_costs); + return; + } - postsolve( - pslp_presolver_, h_primal_solution.data(), h_dual_solution.data(), h_reduced_costs.data()); + if (status_to_skip) { return; } + undo_papilo_host(primal_solution, dual_solution, reduced_costs, dual_postsolve); +} - auto uncrushed_sol = pslp_presolver_->sol; - int n_cols = uncrushed_sol->dim_x; - int n_rows = uncrushed_sol->dim_y; - - primal_solution.resize(n_cols, stream_view); - dual_solution.resize(n_rows, stream_view); - reduced_costs.resize(n_cols, stream_view); - raft::copy(primal_solution.data(), uncrushed_sol->x, n_cols, stream_view); - raft::copy(dual_solution.data(), uncrushed_sol->y, n_rows, stream_view); - raft::copy(reduced_costs.data(), uncrushed_sol->z, n_cols, stream_view); - } else { - cuopt_expects( - false, error_type_t::ValidationError, "PSLP postsolve only supports double precision"); +template +void third_party_presolve_t::undo(rmm::device_uvector& primal_solution, + rmm::device_uvector& dual_solution, + rmm::device_uvector& reduced_costs, + problem_category_t category, + bool status_to_skip, + bool dual_postsolve, + rmm::cuda_stream_view stream_view) +{ + // PSLP path always runs (it owns the lifted solution in-place); the Papilo + // path (used for Papilo/Default/None) is allowed to short-circuit via + // status_to_skip without touching the data. Mirror that here so we don't pay + // for unnecessary D->H->D copies. + if (status_to_skip && presolver_ != cuopt::mathematical_optimization::presolver_t::PSLP) { + return; } + std::vector h_primal(primal_solution.size()); + std::vector h_dual(dual_solution.size()); + std::vector h_rc(reduced_costs.size()); + raft::copy(h_primal.data(), primal_solution.data(), primal_solution.size(), stream_view); + raft::copy(h_dual.data(), dual_solution.data(), dual_solution.size(), stream_view); + raft::copy(h_rc.data(), reduced_costs.data(), reduced_costs.size(), stream_view); + stream_view.synchronize(); + + undo_host(h_primal, h_dual, h_rc, category, status_to_skip, dual_postsolve); + + primal_solution.resize(h_primal.size(), stream_view); + dual_solution.resize(h_dual.size(), stream_view); + reduced_costs.resize(h_rc.size(), stream_view); + raft::copy(primal_solution.data(), h_primal.data(), h_primal.size(), stream_view); + raft::copy(dual_solution.data(), h_dual.data(), h_dual.size(), stream_view); + raft::copy(reduced_costs.data(), h_rc.data(), h_rc.size(), stream_view); stream_view.synchronize(); } diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 3b6b92a40d..34d9bbfef3 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -76,6 +76,15 @@ class third_party_presolve_t { bool dual_postsolve, rmm::cuda_stream_view stream_view); + // Host-only postsolve. Resizes the vectors to original-problem dimensions. + // The device-side `undo` above is a thin shim around this method. + void undo_host(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + problem_category_t category, + bool status_to_skip, + bool dual_postsolve); + void uncrush_primal_solution(const std::vector& reduced_primal, std::vector& full_primal) const; const std::vector& get_reduced_to_original_map() const { return reduced_to_original_map_; } @@ -87,10 +96,16 @@ class third_party_presolve_t { third_party_presolve_result_t apply_pslp( optimization_problem_t const& op_problem, const double time_limit); - void undo_pslp(rmm::device_uvector& primal_solution, - rmm::device_uvector& dual_solution, - rmm::device_uvector& reduced_costs, - rmm::cuda_stream_view stream_view); + // Host-only per-backend postsolve helpers. Both resize their vector args + // to original-problem dimensions. + void undo_pslp_host(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs); + + void undo_papilo_host(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + bool dual_postsolve); bool maximize_ = false; cuopt::mathematical_optimization::presolver_t presolver_ = From 2531c24624d1f5873cecfde2729561265c589a3c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 30 Jun 2026 14:26:08 -0700 Subject: [PATCH 141/258] cleaned presolve and distributed works with presolve --- .../cuopt/mathematical_optimization/solve.hpp | 9 + .../presolve/third_party_presolve.cpp | 706 +++++++++--------- .../presolve/third_party_presolve.hpp | 68 +- cpp/src/mip_heuristics/solve.cu | 19 +- cpp/src/pdlp/solve.cu | 285 ++++++- cpp/src/pdlp/solve.cuh | 4 +- cpp/tests/mip/presolve_test.cu | 17 +- 7 files changed, 731 insertions(+), 377 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/solve.hpp b/cpp/include/cuopt/mathematical_optimization/solve.hpp index 961fcd29b6..15ca990941 100644 --- a/cpp/include/cuopt/mathematical_optimization/solve.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solve.hpp @@ -148,6 +148,15 @@ optimization_problem_t mps_data_model_to_optimization_problem( raft::handle_t const* handle_ptr, const cuopt::mathematical_optimization::io::mps_data_model_t& data_model); +// Device->host projection of an optimization_problem_t onto an +// mps_data_model_t. The inverse of mps_data_model_to_optimization_problem +// above. Copies every vector buffer (CSR, var/row bounds, types, optional +// row_types + RHS) and the relevant metadata (objective offset/scale, names, +// sense). One stream sync at the end. +template +cuopt::mathematical_optimization::io::mps_data_model_t +op_problem_to_mps_data_model(const optimization_problem_t& op_problem); + // ============================================================================ // CPU problem overloads (convert to GPU, solve, convert solution back) // ============================================================================ diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 4aed6f12b4..ab2f42e988 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -35,6 +35,9 @@ #else #pragma GCC diagnostic pop #endif +#include +#include +#include #include #include #include @@ -46,68 +49,98 @@ namespace cuopt::mathematical_optimization::mip { +// Host-only gather + input normalisation for PSLP from an mps_data_model. +// Single source of truth for PSLP input construction — the op_problem path +// reaches this via op_problem_to_mps_data_model first. template -papilo::Problem build_papilo_problem(const optimization_problem_t& op_problem, - problem_category_t category, - bool maximize) +pslp_input_t build_pslp_host_arrays_from_mps_data( + const io::mps_data_model_t& mps, bool maximize) { - raft::common::nvtx::range fun_scope("Build papilo problem"); - // Build papilo problem from optimization problem + raft::common::nvtx::range fun_scope("Build PSLP host arrays from mps_data_model"); + + pslp_input_t arrays; + arrays.n_cols = mps.get_n_variables(); + arrays.n_rows = mps.get_n_constraints(); + arrays.nnz = mps.get_nnz(); + + // Copy (mps's getters return by const-ref; we own these vectors so we can + // mutate them during normalisation). + arrays.coefficients = mps.get_constraint_matrix_values(); + arrays.indices = mps.get_constraint_matrix_indices(); + arrays.offsets = mps.get_constraint_matrix_offsets(); + arrays.obj_coeffs = mps.get_objective_coefficients(); + arrays.var_lb = mps.get_variable_lower_bounds(); + arrays.var_ub = mps.get_variable_upper_bounds(); + arrays.constr_lb = mps.get_constraint_lower_bounds(); + arrays.constr_ub = mps.get_constraint_upper_bounds(); + + const auto& h_bounds = mps.get_constraint_bounds(); + const auto& h_row_types = mps.get_row_types(); + + if (maximize) { + for (auto& c : arrays.obj_coeffs) c = -c; + } + + if (arrays.constr_lb.empty() && arrays.constr_ub.empty()) { + for (size_t i = 0; i < h_row_types.size(); ++i) { + if (h_row_types[i] == 'L') { + arrays.constr_lb.push_back(-std::numeric_limits::infinity()); + arrays.constr_ub.push_back(h_bounds[i]); + } else if (h_row_types[i] == 'G') { + arrays.constr_lb.push_back(h_bounds[i]); + arrays.constr_ub.push_back(std::numeric_limits::infinity()); + } else if (h_row_types[i] == 'E') { + arrays.constr_lb.push_back(h_bounds[i]); + arrays.constr_ub.push_back(h_bounds[i]); + } + } + } + + if (arrays.var_lb.empty()) { + arrays.var_lb.assign(arrays.n_cols, -std::numeric_limits::infinity()); + } + if (arrays.var_ub.empty()) { + arrays.var_ub.assign(arrays.n_cols, std::numeric_limits::infinity()); + } + + return arrays; +} + +// Host-only gather + papilo::Problem construction from an mps_data_model. +// Single source of truth for papilo input construction, the op_problem path +// reaches this via op_problem_to_mps_data_model first. +template +papilo::Problem build_papilo_problem_from_mps_data( + const io::mps_data_model_t& mps, problem_category_t category, bool maximize) +{ + raft::common::nvtx::range fun_scope("Build papilo problem from mps_data_model"); papilo::ProblemBuilder builder; - // Get problem dimensions - const i_t num_cols = op_problem.get_n_variables(); - const i_t num_rows = op_problem.get_n_constraints(); - const i_t nnz = op_problem.get_nnz(); + const i_t num_cols = mps.get_n_variables(); + const i_t num_rows = mps.get_n_constraints(); + const i_t nnz = mps.get_nnz(); builder.reserve(nnz, num_rows, num_cols); - // Get problem data from optimization problem - const auto& coefficients = op_problem.get_constraint_matrix_values(); - const auto& offsets = op_problem.get_constraint_matrix_offsets(); - const auto& variables = op_problem.get_constraint_matrix_indices(); - const auto& obj_coeffs = op_problem.get_objective_coefficients(); - const auto& var_lb = op_problem.get_variable_lower_bounds(); - const auto& var_ub = op_problem.get_variable_upper_bounds(); - const auto& bounds = op_problem.get_constraint_bounds(); - const auto& row_types = op_problem.get_row_types(); - const auto& constr_lb = op_problem.get_constraint_lower_bounds(); - const auto& constr_ub = op_problem.get_constraint_upper_bounds(); - const auto& var_types = op_problem.get_variable_types(); - - // Copy data to host - std::vector h_coefficients(coefficients.size()); - auto stream_view = op_problem.get_handle_ptr()->get_stream(); - raft::copy(h_coefficients.data(), coefficients.data(), coefficients.size(), stream_view); - std::vector h_offsets(offsets.size()); - raft::copy(h_offsets.data(), offsets.data(), offsets.size(), stream_view); - std::vector h_variables(variables.size()); - raft::copy(h_variables.data(), variables.data(), variables.size(), stream_view); - std::vector h_obj_coeffs(obj_coeffs.size()); - raft::copy(h_obj_coeffs.data(), obj_coeffs.data(), obj_coeffs.size(), stream_view); - std::vector h_var_lb(var_lb.size()); - raft::copy(h_var_lb.data(), var_lb.data(), var_lb.size(), stream_view); - std::vector h_var_ub(var_ub.size()); - raft::copy(h_var_ub.data(), var_ub.data(), var_ub.size(), stream_view); - std::vector h_bounds(bounds.size()); - raft::copy(h_bounds.data(), bounds.data(), bounds.size(), stream_view); - std::vector h_row_types(row_types.size()); - raft::copy(h_row_types.data(), row_types.data(), row_types.size(), stream_view); - std::vector h_constr_lb(constr_lb.size()); - raft::copy(h_constr_lb.data(), constr_lb.data(), constr_lb.size(), stream_view); - std::vector h_constr_ub(constr_ub.size()); - raft::copy(h_constr_ub.data(), constr_ub.data(), constr_ub.size(), stream_view); - std::vector h_var_types(var_types.size()); - raft::copy(h_var_types.data(), var_types.data(), var_types.size(), stream_view); + // Local mutable copies (we sign-flip / fill empties). + auto h_coefficients = mps.get_constraint_matrix_values(); + auto h_offsets = mps.get_constraint_matrix_offsets(); + auto h_variables = mps.get_constraint_matrix_indices(); + auto h_obj_coeffs = mps.get_objective_coefficients(); + auto h_var_lb = mps.get_variable_lower_bounds(); + auto h_var_ub = mps.get_variable_upper_bounds(); + auto h_constr_lb = mps.get_constraint_lower_bounds(); + auto h_constr_ub = mps.get_constraint_upper_bounds(); + + const auto& h_bounds = mps.get_constraint_bounds(); + const auto& h_row_types = mps.get_row_types(); + const auto& h_var_types = mps.get_variable_types(); if (maximize) { - for (size_t i = 0; i < h_obj_coeffs.size(); ++i) { - h_obj_coeffs[i] = -h_obj_coeffs[i]; - } + for (auto& c : h_obj_coeffs) c = -c; } - auto constr_bounds_empty = h_constr_lb.empty() && h_constr_ub.empty(); - if (constr_bounds_empty) { + if (h_constr_lb.empty() && h_constr_ub.empty()) { for (size_t i = 0; i < h_row_types.size(); ++i) { if (h_row_types[i] == 'L') { h_constr_lb.push_back(-std::numeric_limits::infinity()); @@ -126,19 +159,20 @@ papilo::Problem build_papilo_problem(const optimization_problem_t builder.setNumRows(num_rows); builder.setObjAll(h_obj_coeffs); - builder.setObjOffset(maximize ? -op_problem.get_objective_offset() - : op_problem.get_objective_offset()); + builder.setObjOffset(maximize ? -mps.get_objective_offset() : mps.get_objective_offset()); if (!h_var_lb.empty() && !h_var_ub.empty()) { builder.setColLbAll(h_var_lb); builder.setColUbAll(h_var_ub); - if (op_problem.get_variable_names().size() == h_var_lb.size()) { - builder.setColNameAll(op_problem.get_variable_names()); + if (mps.get_variable_names().size() == static_cast(num_cols)) { + builder.setColNameAll(mps.get_variable_names()); } } + // mps_data_model uses 'I' / 'C' for integer / continuous; absence means + // continuous. for (size_t i = 0; i < h_var_types.size(); ++i) { - builder.setColIntegral(i, h_var_types[i] == var_t::INTEGER); + builder.setColIntegral(i, h_var_types[i] == 'I'); } if (!h_constr_lb.empty() && !h_constr_ub.empty()) { @@ -148,9 +182,7 @@ papilo::Problem build_papilo_problem(const optimization_problem_t std::vector h_row_flags(h_constr_lb.size()); std::vector> h_entries; - // Add constraints row by row for (size_t i = 0; i < h_constr_lb.size(); ++i) { - // Get row entries i_t row_start = h_offsets[i]; i_t row_end = h_offsets[i + 1]; i_t num_entries = row_end - row_start; @@ -185,10 +217,9 @@ papilo::Problem build_papilo_problem(const optimization_problem_t if (h_entries.size()) { auto constexpr const sorted_entries = true; - // MIP reductions like clique merging and substituition require more fillin - const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; - const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; - auto csr_storage = papilo::SparseStorage( + const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; + const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; + auto csr_storage = papilo::SparseStorage( h_entries, num_rows, num_cols, sorted_entries, spare_ratio, min_inter_row_space); problem.setConstraintMatrix(csr_storage, h_constr_lb, h_constr_ub, h_row_flags); @@ -204,215 +235,91 @@ papilo::Problem build_papilo_problem(const optimization_problem_t return problem; } -struct PSLPContext { - Presolver* presolver = nullptr; - Settings* settings = nullptr; - PresolveStatus status = PresolveStatus_::UNCHANGED; -}; - +// Host-only result builder: write a (reduced) PSLP presolver state into a +// fresh mps_data_model_t. template -PSLPContext build_and_run_pslp_presolver(const optimization_problem_t& op_problem, - bool maximize, - const double time_limit) +io::mps_data_model_t build_mps_data_from_pslp(Presolver* pslp_presolver, + bool maximize, + f_t original_obj_offset) { - PSLPContext ctx; - raft::common::nvtx::range fun_scope("Build and run PSLP presolver"); - - // Get problem dimensions - const i_t num_cols = op_problem.get_n_variables(); - const i_t num_rows = op_problem.get_n_constraints(); - const i_t nnz = op_problem.get_nnz(); - - // Get problem data from optimization problem - const auto& coefficients = op_problem.get_constraint_matrix_values(); - const auto& offsets = op_problem.get_constraint_matrix_offsets(); - const auto& variables = op_problem.get_constraint_matrix_indices(); - const auto& obj_coeffs = op_problem.get_objective_coefficients(); - const auto& var_lb = op_problem.get_variable_lower_bounds(); - const auto& var_ub = op_problem.get_variable_upper_bounds(); - const auto& bounds = op_problem.get_constraint_bounds(); - const auto& row_types = op_problem.get_row_types(); - const auto& constr_lb = op_problem.get_constraint_lower_bounds(); - const auto& constr_ub = op_problem.get_constraint_upper_bounds(); - const auto& var_types = op_problem.get_variable_types(); - - // Copy data to host - std::vector h_coefficients(coefficients.size()); - auto stream_view = op_problem.get_handle_ptr()->get_stream(); - raft::copy(h_coefficients.data(), coefficients.data(), coefficients.size(), stream_view); - std::vector h_offsets(offsets.size()); - raft::copy(h_offsets.data(), offsets.data(), offsets.size(), stream_view); - std::vector h_variables(variables.size()); - raft::copy(h_variables.data(), variables.data(), variables.size(), stream_view); - std::vector h_obj_coeffs(obj_coeffs.size()); - raft::copy(h_obj_coeffs.data(), obj_coeffs.data(), obj_coeffs.size(), stream_view); - std::vector h_var_lb(var_lb.size()); - raft::copy(h_var_lb.data(), var_lb.data(), var_lb.size(), stream_view); - std::vector h_var_ub(var_ub.size()); - raft::copy(h_var_ub.data(), var_ub.data(), var_ub.size(), stream_view); - std::vector h_bounds(bounds.size()); - raft::copy(h_bounds.data(), bounds.data(), bounds.size(), stream_view); - std::vector h_row_types(row_types.size()); - raft::copy(h_row_types.data(), row_types.data(), row_types.size(), stream_view); - std::vector h_constr_lb(constr_lb.size()); - raft::copy(h_constr_lb.data(), constr_lb.data(), constr_lb.size(), stream_view); - std::vector h_constr_ub(constr_ub.size()); - raft::copy(h_constr_ub.data(), constr_ub.data(), constr_ub.size(), stream_view); - std::vector h_var_types(var_types.size()); - raft::copy(h_var_types.data(), var_types.data(), var_types.size(), stream_view); + raft::common::nvtx::range fun_scope("Build mps_data_model from PSLP"); + io::mps_data_model_t mps; - stream_view.synchronize(); - if (maximize) { - for (size_t i = 0; i < h_obj_coeffs.size(); ++i) { - h_obj_coeffs[i] = -h_obj_coeffs[i]; - } - } - - auto constr_bounds_empty = h_constr_lb.empty() && h_constr_ub.empty(); - if (constr_bounds_empty) { - for (size_t i = 0; i < h_row_types.size(); ++i) { - if (h_row_types[i] == 'L') { - h_constr_lb.push_back(-std::numeric_limits::infinity()); - h_constr_ub.push_back(h_bounds[i]); - } else if (h_row_types[i] == 'G') { - h_constr_lb.push_back(h_bounds[i]); - h_constr_ub.push_back(std::numeric_limits::infinity()); - } else if (h_row_types[i] == 'E') { - h_constr_lb.push_back(h_bounds[i]); - h_constr_ub.push_back(h_bounds[i]); - } + if constexpr (std::is_same_v) { + cuopt_expects(pslp_presolver != nullptr && pslp_presolver->reduced_prob != nullptr, + error_type_t::RuntimeError, + "PSLP presolver is not initialized"); + auto reduced_prob = pslp_presolver->reduced_prob; + const i_t n_rows = reduced_prob->m; + const i_t n_cols = reduced_prob->n; + const i_t nnz = reduced_prob->nnz; + f_t obj_offset = reduced_prob->obj_offset; + + obj_offset = maximize ? -obj_offset : obj_offset; + // PSLP does not allow setting an objective offset, so we fold the + // original input's offset into the reduced one. + obj_offset += original_obj_offset; + mps.set_objective_offset(obj_offset); + mps.set_maximize(maximize); + + if (n_cols == 0 && n_rows == 0) { + std::vector empty_offsets = {0}; + mps.set_csr_constraint_matrix( + {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); + return mps; } - } - // handle empty variable bounds - if (h_var_lb.empty()) { - h_var_lb = std::vector(num_cols, -std::numeric_limits::infinity()); - } - if (h_var_ub.empty()) { - h_var_ub = std::vector(num_cols, std::numeric_limits::infinity()); - } - - // Call PSLP presolver - ctx.settings = default_settings(); - ctx.settings->verbose = false; - ctx.settings->max_time = time_limit; - auto start_time = std::chrono::high_resolution_clock::now(); - ctx.presolver = new_presolver(h_coefficients.data(), - h_variables.data(), - h_offsets.data(), - num_rows, - num_cols, - nnz, - h_constr_lb.data(), - h_constr_ub.data(), - h_var_lb.data(), - h_var_ub.data(), - h_obj_coeffs.data(), - ctx.settings); - - assert(ctx.presolver != nullptr && "Presolver initialization failed"); - ctx.status = run_presolver(ctx.presolver); - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - CUOPT_LOG_DEBUG("PSLP presolver time: %d milliseconds", duration.count()); - CUOPT_LOG_INFO("PSLP Presolved problem: %d constraints, %d variables, %d non-zeros", - ctx.presolver->stats->n_rows_reduced, - ctx.presolver->stats->n_cols_reduced, - ctx.presolver->stats->nnz_reduced); - - return ctx; -} + mps.set_csr_constraint_matrix( + std::span(reduced_prob->Ax, static_cast(nnz)), + std::span(reduced_prob->Ai, static_cast(nnz)), + std::span(reduced_prob->Ap, static_cast(n_rows + 1))); -template -optimization_problem_t build_optimization_problem_from_pslp( - Presolver* pslp_presolver, - raft::handle_t const* handle_ptr, - bool maximize, - f_t original_obj_offset) -{ - raft::common::nvtx::range fun_scope("Build optimization problem from PSLP"); - cuopt_expects(pslp_presolver != nullptr && pslp_presolver->reduced_prob != nullptr, - error_type_t::RuntimeError, - "PSLP presolver is not initialized"); - auto reduced_prob = pslp_presolver->reduced_prob; - int n_rows = reduced_prob->m; - int n_cols = reduced_prob->n; - int nnz = reduced_prob->nnz; - double obj_offset = reduced_prob->obj_offset; - - optimization_problem_t op_problem(handle_ptr); - - obj_offset = maximize ? -obj_offset : obj_offset; - - // PSLP does not allow setting the objective offset, so we add the original objective offset to - // the reduced objective offset - obj_offset += original_obj_offset; - op_problem.set_objective_offset(obj_offset); - op_problem.set_maximize(maximize); - op_problem.set_problem_category(problem_category_t::LP); - - // Handle empty reduced problem (presolve completely solved it) - if (n_cols == 0 && n_rows == 0) { - // Set empty constraint matrix with proper offsets - std::vector empty_offsets = {0}; - op_problem.set_csr_constraint_matrix(nullptr, 0, nullptr, 0, empty_offsets.data(), 1); - return op_problem; - } - - op_problem.set_csr_constraint_matrix( - reduced_prob->Ax, nnz, reduced_prob->Ai, nnz, reduced_prob->Ap, n_rows + 1); - - std::vector h_obj_coeffs(n_cols); - std::copy(reduced_prob->c, reduced_prob->c + n_cols, h_obj_coeffs.begin()); - if (maximize) { - for (size_t i = 0; i < n_cols; ++i) { - h_obj_coeffs[i] = -h_obj_coeffs[i]; + std::vector h_obj_coeffs(reduced_prob->c, reduced_prob->c + n_cols); + if (maximize) { + for (auto& c : h_obj_coeffs) c = -c; } + mps.set_objective_coefficients( + std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); + mps.set_constraint_lower_bounds( + std::span(reduced_prob->lhs, static_cast(n_rows))); + mps.set_constraint_upper_bounds( + std::span(reduced_prob->rhs, static_cast(n_rows))); + mps.set_variable_lower_bounds( + std::span(reduced_prob->lbs, static_cast(n_cols))); + mps.set_variable_upper_bounds( + std::span(reduced_prob->ubs, static_cast(n_cols))); + } else { + cuopt_expects( + false, error_type_t::ValidationError, "PSLP only supports double precision"); } - op_problem.set_objective_coefficients(h_obj_coeffs.data(), n_cols); - op_problem.set_constraint_lower_bounds(reduced_prob->lhs, n_rows); - op_problem.set_constraint_upper_bounds(reduced_prob->rhs, n_rows); - op_problem.set_variable_lower_bounds(reduced_prob->lbs, n_cols); - op_problem.set_variable_upper_bounds(reduced_prob->ubs, n_cols); - - return op_problem; + return mps; } +// Host-only result builder: write the (reduced) papilo::Problem into a fresh +// mps_data_model_t. template -optimization_problem_t build_optimization_problem( - papilo::Problem const& papilo_problem, - raft::handle_t const* handle_ptr, - problem_category_t category, - bool maximize) +io::mps_data_model_t build_mps_data_from_papilo(papilo::Problem const& papilo_problem, + bool maximize) { - raft::common::nvtx::range fun_scope("Build optimization problem"); - optimization_problem_t op_problem(handle_ptr); + raft::common::nvtx::range fun_scope("Build mps_data_model from papilo"); + io::mps_data_model_t mps; auto obj = papilo_problem.getObjective(); - op_problem.set_objective_offset(maximize ? -obj.offset : obj.offset); - op_problem.set_maximize(maximize); - op_problem.set_problem_category(category); + mps.set_objective_offset(maximize ? -obj.offset : obj.offset); + mps.set_maximize(maximize); if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { - // FIXME: Shouldn't need to set offsets std::vector h_offsets{0}; - std::vector h_indices{}; - std::vector h_values{}; - op_problem.set_csr_constraint_matrix(h_values.data(), - h_values.size(), - h_indices.data(), - h_indices.size(), - h_offsets.data(), - h_offsets.size()); - - return op_problem; + mps.set_csr_constraint_matrix({}, {}, std::span(h_offsets.data(), h_offsets.size())); + return mps; } if (maximize) { for (size_t i = 0; i < obj.coefficients.size(); ++i) { obj.coefficients[i] = -obj.coefficients[i]; } } - op_problem.set_objective_coefficients(obj.coefficients.data(), obj.coefficients.size()); + mps.set_objective_coefficients( + std::span(obj.coefficients.data(), obj.coefficients.size())); auto& constraint_matrix = papilo_problem.getConstraintMatrix(); auto row_lower = constraint_matrix.getLeftHandSides(); @@ -430,13 +337,11 @@ optimization_problem_t build_optimization_problem( } } - op_problem.set_constraint_lower_bounds(row_lower.data(), row_lower.size()); - op_problem.set_constraint_upper_bounds(row_upper.data(), row_upper.size()); + mps.set_constraint_lower_bounds(std::span(row_lower.data(), row_lower.size())); + mps.set_constraint_upper_bounds(std::span(row_upper.data(), row_upper.size())); auto [index_range, nrows] = constraint_matrix.getRangeInfo(); - std::vector offsets(nrows + 1); - // papilo indices do not start from 0 after presolve size_t start = index_range[0].start; for (i_t i = 0; i < nrows; i++) { offsets[i] = index_range[i].start - start; @@ -449,20 +354,14 @@ optimization_problem_t build_optimization_problem( const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); - i_t ncols = papilo_problem.getNCols(); - cuopt_assert( - std::all_of( - cols + start, cols + start + nnz, [ncols](i_t col) { return col >= 0 && col < ncols; }), - "Papilo produced invalid column indices in presolved matrix"); - - op_problem.set_csr_constraint_matrix( - &(coeffs[start]), nnz, &(cols[start]), nnz, offsets.data(), nrows + 1); + mps.set_csr_constraint_matrix(std::span(&coeffs[start], static_cast(nnz)), + std::span(&cols[start], static_cast(nnz)), + std::span(offsets.data(), offsets.size())); auto col_flags = papilo_problem.getColFlags(); - std::vector var_types(col_flags.size()); + std::vector var_types(col_flags.size()); for (size_t i = 0; i < col_flags.size(); i++) { - var_types[i] = - col_flags[i].test(papilo::ColFlag::kIntegral) ? var_t::INTEGER : var_t::CONTINUOUS; + var_types[i] = col_flags[i].test(papilo::ColFlag::kIntegral) ? 'I' : 'C'; if (col_flags[i].test(papilo::ColFlag::kLbInf)) { col_lower[i] = -std::numeric_limits::infinity(); } @@ -471,11 +370,11 @@ optimization_problem_t build_optimization_problem( } } - op_problem.set_variable_lower_bounds(col_lower.data(), col_lower.size()); - op_problem.set_variable_upper_bounds(col_upper.data(), col_upper.size()); - op_problem.set_variable_types(var_types.data(), var_types.size()); + mps.set_variable_lower_bounds(std::span(col_lower.data(), col_lower.size())); + mps.set_variable_upper_bounds(std::span(col_upper.data(), col_upper.size())); + mps.set_variable_types(var_types); - return op_problem; + return mps; } void check_presolve_status(const papilo::PresolveStatus& status) @@ -623,71 +522,77 @@ void set_presolve_parameters(papilo::Presolve& presolver, } template -third_party_presolve_result_t third_party_presolve_t::apply_pslp( - optimization_problem_t const& op_problem, const double time_limit) +third_party_presolve_status_t third_party_presolve_t::apply_pslp( + pslp_input_t& arrays, double time_limit) { if constexpr (std::is_same_v) { - double original_obj_offset = op_problem.get_objective_offset(); - auto ctx = build_and_run_pslp_presolver(op_problem, maximize_, time_limit); - - // Free previously allocated presolver and settings if they exist + raft::common::nvtx::range fun_scope("Apply PSLP presolver on host"); + + Settings* settings = default_settings(); + settings->verbose = false; + settings->max_time = time_limit; + + auto start_time = std::chrono::high_resolution_clock::now(); + Presolver* presolver = new_presolver(arrays.coefficients.data(), + arrays.indices.data(), + arrays.offsets.data(), + arrays.n_rows, + arrays.n_cols, + arrays.nnz, + arrays.constr_lb.data(), + arrays.constr_ub.data(), + arrays.var_lb.data(), + arrays.var_ub.data(), + arrays.obj_coeffs.data(), + settings); + assert(presolver != nullptr && "Presolver initialization failed"); + const PresolveStatus pslp_status = run_presolver(presolver); + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + CUOPT_LOG_DEBUG("PSLP presolver time: %d milliseconds", duration.count()); + CUOPT_LOG_INFO("PSLP Presolved problem: %d constraints, %d variables, %d non-zeros", + presolver->stats->n_rows_reduced, + presolver->stats->n_cols_reduced, + presolver->stats->nnz_reduced); + + // Free previously allocated presolver and settings (if any) and stash the + // new ones so undo_pslp_host / build_mps_data_from_pslp can find them + // later. if (pslp_presolver_ != nullptr) { free_presolver(pslp_presolver_); } if (pslp_stgs_ != nullptr) { free_settings(pslp_stgs_); } + pslp_presolver_ = presolver; + pslp_stgs_ = settings; - pslp_presolver_ = ctx.presolver; - pslp_stgs_ = ctx.settings; - - auto status = convert_pslp_presolve_status_to_third_party_presolve_status(ctx.status); - if (ctx.status == PresolveStatus_::INFEASIBLE || ctx.status == PresolveStatus_::UNBNDORINFEAS) { - optimization_problem_t empty_problem(op_problem.get_handle_ptr()); - return third_party_presolve_result_t{status, std::move(empty_problem), {}, {}, {}}; - } - - auto opt_problem = build_optimization_problem_from_pslp( - pslp_presolver_, op_problem.get_handle_ptr(), maximize_, original_obj_offset); - return third_party_presolve_result_t{status, std::move(opt_problem), {}, {}, {}}; + return convert_pslp_presolve_status_to_third_party_presolve_status(pslp_status); } else { cuopt_expects( false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); - return third_party_presolve_result_t{ - third_party_presolve_status_t::UNCHANGED, - optimization_problem_t(op_problem.get_handle_ptr()), - {}, - {}, - {}}; + return third_party_presolve_status_t::UNCHANGED; // unreachable } } template -third_party_presolve_result_t third_party_presolve_t::apply( - optimization_problem_t const& op_problem, +third_party_presolve_status_t third_party_presolve_t::apply_papilo( + papilo::Problem& papilo_problem, problem_category_t category, - cuopt::mathematical_optimization::presolver_t presolver, bool dual_postsolve, f_t absolute_tolerance, f_t relative_tolerance, double time_limit, i_t num_cpu_threads) { - presolver_ = presolver; - maximize_ = op_problem.get_sense(); - if (category == problem_category_t::MIP && - presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { - cuopt_expects( - false, error_type_t::RuntimeError, "PSLP presolver is not supported for MIP problems"); - } + raft::common::nvtx::range fun_scope("Apply Papilo presolve on host"); - if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { - return apply_pslp(op_problem, time_limit); - } - - papilo::Problem papilo_problem = build_papilo_problem(op_problem, category, maximize_); + // Capture original dimensions before papilo.apply() mutates papilo_problem + // in place into its reduced form. + const i_t original_n_vars = static_cast(papilo_problem.getNCols()); + const i_t original_n_cons = static_cast(papilo_problem.getNRows()); + const i_t original_nnz = static_cast(papilo_problem.getConstraintMatrix().getNnz()); CUOPT_LOG_DEBUG("Original problem: %d constraints, %d variables, %d nonzeros", - papilo_problem.getNRows(), - papilo_problem.getNCols(), - papilo_problem.getConstraintMatrix().getNnz()); - + original_n_cons, + original_n_vars, + original_nnz); CUOPT_LOG_INFO("\nRunning Papilo presolve (git hash %s)", PAPILO_GITHASH); if (category == problem_category_t::MIP) { dual_postsolve = false; } papilo::Presolve papilo_presolver; @@ -699,10 +604,7 @@ third_party_presolve_result_t third_party_presolve_t::apply( time_limit, dual_postsolve, num_cpu_threads); - set_presolve_parameters( - papilo_presolver, category, op_problem.get_n_constraints(), op_problem.get_n_variables()); - - // Disable papilo logs + set_presolve_parameters(papilo_presolver, category, original_n_cons, original_n_vars); papilo_presolver.setVerbosityLevel(papilo::VerbosityLevel::kQuiet); auto result = papilo_presolver.apply(papilo_problem); @@ -711,19 +613,18 @@ third_party_presolve_result_t third_party_presolve_t::apply( if (result.status == papilo::PresolveStatus::kInfeasible || result.status == papilo::PresolveStatus::kUnbndOrInfeas || result.status == papilo::PresolveStatus::kUnbounded) { - optimization_problem_t empty_problem(op_problem.get_handle_ptr()); - return third_party_presolve_result_t{status, std::move(empty_problem), {}, {}, {}}; + return status; } papilo_post_solve_storage_.reset(new papilo::PostsolveStorage(result.postsolve)); CUOPT_LOG_INFO("Presolve removed: %d constraints, %d variables, %d nonzeros", - op_problem.get_n_constraints() - papilo_problem.getNRows(), - op_problem.get_n_variables() - papilo_problem.getNCols(), - op_problem.get_nnz() - papilo_problem.getConstraintMatrix().getNnz()); + original_n_cons - papilo_problem.getNRows(), + original_n_vars - papilo_problem.getNCols(), + original_nnz - papilo_problem.getConstraintMatrix().getNnz()); i_t n_integer = 0; { auto col_flags = papilo_problem.getColFlags(); - for (size_t i = 0; i < col_flags.size(); i++) { + for (size_t i = 0; i < col_flags.size(); ++i) { if (col_flags[i].test(papilo::ColFlag::kIntegral)) n_integer++; } } @@ -733,38 +634,165 @@ third_party_presolve_result_t third_party_presolve_t::apply( n_integer, papilo_problem.getConstraintMatrix().getNnz()); - // Check if presolve found the optimal solution (problem fully reduced) if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { status = third_party_presolve_status_t::OPTIMAL; } - auto opt_problem = build_optimization_problem( - papilo_problem, op_problem.get_handle_ptr(), category, maximize_); - // metadata from original optimization problem that is not filled - opt_problem.set_problem_name(op_problem.get_problem_name()); - opt_problem.set_objective_scaling_factor(op_problem.get_objective_scaling_factor()); - // when an objective offset outside (e.g. from mps file), handle accordingly - auto col_flags = papilo_problem.getColFlags(); - std::vector implied_integer_indices; - for (size_t i = 0; i < col_flags.size(); i++) { - if (col_flags[i].test(papilo::ColFlag::kImplInt)) implied_integer_indices.push_back(i); - } - auto const& col_map = result.postsolve.origcol_mapping; reduced_to_original_map_.assign(col_map.begin(), col_map.end()); - original_to_reduced_map_.assign(op_problem.get_n_variables(), -1); + original_to_reduced_map_.assign(original_n_vars, -1); for (size_t i = 0; i < reduced_to_original_map_.size(); ++i) { auto original_idx = reduced_to_original_map_[i]; if (original_idx >= 0 && static_cast(original_idx) < original_to_reduced_map_.size()) { original_to_reduced_map_[original_idx] = static_cast(i); } } + return status; +} + +// Project to mps_data_model and apply presolve on host +// and rebuild optimization_problem_t on device +template +third_party_presolve_device_result_t +third_party_presolve_t::apply_presolve_from_op_problem( + optimization_problem_t const& op_problem, + problem_category_t category, + cuopt::mathematical_optimization::presolver_t presolver, + bool dual_postsolve, + f_t absolute_tolerance, + f_t relative_tolerance, + double time_limit, + i_t num_cpu_threads) +{ + auto* handle = op_problem.get_handle_ptr(); + auto mps = op_problem_to_mps_data_model(op_problem); + + auto host_res = apply_presolve_from_mps_data(mps, + category, + presolver, + dual_postsolve, + absolute_tolerance, + relative_tolerance, + time_limit, + num_cpu_threads); + + if (host_res.status == third_party_presolve_status_t::INFEASIBLE || + host_res.status == third_party_presolve_status_t::UNBOUNDED || + host_res.status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_device_result_t{ + host_res.status, optimization_problem_t(handle), {}, {}, {}}; + } + + // H->D: rebuild a device optimization_problem from the reduced mps_data_model. + // mps_data_model doesn't carry problem_category, so we restore it here. + auto reduced_opt = + mps_data_model_to_optimization_problem(handle, host_res.reduced_problem); + reduced_opt.set_problem_category(category); + + return third_party_presolve_device_result_t{ + host_res.status, + std::move(reduced_opt), + std::move(host_res.implied_integer_indices), + std::move(host_res.reduced_to_original_map), + std::move(host_res.original_to_reduced_map)}; +} - return third_party_presolve_result_t{status, - std::move(opt_problem), - implied_integer_indices, - reduced_to_original_map_, - original_to_reduced_map_}; +template +third_party_presolve_host_result_t +third_party_presolve_t::apply_presolve_from_mps_data( + io::mps_data_model_t const& mps, + problem_category_t category, + cuopt::mathematical_optimization::presolver_t presolver, + bool dual_postsolve, + f_t absolute_tolerance, + f_t relative_tolerance, + double time_limit, + i_t num_cpu_threads) +{ + presolver_ = presolver; + maximize_ = mps.get_sense(); + + cuopt_expects(!(category == problem_category_t::MIP && + presolver == cuopt::mathematical_optimization::presolver_t::PSLP), + error_type_t::RuntimeError, + "PSLP presolver is not supported for MIP problems"); + + // Neither PSLP nor Papilo handle quadratic objective / constraints. + cuopt_expects(!mps.has_quadratic_objective(), + error_type_t::ValidationError, + "Presolve does not support mps_data_models with a quadratic objective"); + cuopt_expects(!mps.has_quadratic_constraints(), + error_type_t::ValidationError, + "Presolve does not support mps_data_models with quadratic constraints"); + + // PSLP branch: host gather -> apply_pslp (host) -> host build + if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { + if constexpr (std::is_same_v) { + const f_t original_obj_offset = mps.get_objective_offset(); + auto arrays = build_pslp_host_arrays_from_mps_data(mps, maximize_); + auto status = apply_pslp(arrays, time_limit); + + if (status == third_party_presolve_status_t::INFEASIBLE || + status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_host_result_t{ + status, io::mps_data_model_t{}, {}, {}, {}}; + } + + auto reduced_mps = build_mps_data_from_pslp( + pslp_presolver_, maximize_, original_obj_offset); + reduced_mps.set_problem_name(mps.get_problem_name()); + reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); + return third_party_presolve_host_result_t{ + status, std::move(reduced_mps), {}, {}, {}}; + } else { + cuopt_expects( + false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); + return third_party_presolve_host_result_t{ + third_party_presolve_status_t::UNCHANGED, + io::mps_data_model_t{}, + {}, + {}, + {}}; // unreachable + } + } else { + // Papilo branch: host gather -> apply_papilo (host) -> host build + auto papilo_problem = build_papilo_problem_from_mps_data(mps, category, maximize_); + auto status = apply_papilo(papilo_problem, + category, + dual_postsolve, + absolute_tolerance, + relative_tolerance, + time_limit, + num_cpu_threads); + + if (status == third_party_presolve_status_t::INFEASIBLE || + status == third_party_presolve_status_t::UNBOUNDED || + status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_host_result_t{ + status, io::mps_data_model_t{}, {}, {}, {}}; + } + + auto reduced_mps = build_mps_data_from_papilo(papilo_problem, maximize_); + reduced_mps.set_problem_name(mps.get_problem_name()); + reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); + + std::vector implied_integer_indices; + { + auto col_flags = papilo_problem.getColFlags(); + for (size_t i = 0; i < col_flags.size(); ++i) { + if (col_flags[i].test(papilo::ColFlag::kImplInt)) { + implied_integer_indices.push_back(static_cast(i)); + } + } + } + + return third_party_presolve_host_result_t{ + status, + std::move(reduced_mps), + std::move(implied_integer_indices), + reduced_to_original_map_, + original_to_reduced_map_}; + } } template diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 34d9bbfef3..06a8544520 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -18,6 +18,16 @@ namespace papilo { template class PostsolveStorage; + +// Forward declaration for Papilo Problem class +template +class Problem; +} + +// Forward declaration for mps_data_model_t +namespace cuopt::mathematical_optimization::io { +template +class mps_data_model_t; } namespace cuopt::mathematical_optimization::mip { @@ -37,7 +47,7 @@ enum class third_party_presolve_status_t { }; template -struct third_party_presolve_result_t { +struct third_party_presolve_device_result_t { third_party_presolve_status_t status; optimization_problem_t reduced_problem; std::vector implied_integer_indices; @@ -46,6 +56,34 @@ struct third_party_presolve_result_t { // clique info, etc... }; +// Host counterpart of third_party_presolve_device_result_t: the reduced +// problem is an mps_data_model_t (host) instead of an optimization_problem_t +// (device). Produced by apply_presolve_from_mps_data. +template +struct third_party_presolve_host_result_t { + third_party_presolve_status_t status; + io::mps_data_model_t reduced_problem; + std::vector implied_integer_indices; + std::vector reduced_to_original_map; + std::vector original_to_reduced_map; +}; + +// Host-side PSLP input: every buffer PSLP's C API needs, plus dimensions. +template +struct pslp_input_t { + std::vector coefficients; + std::vector indices; + std::vector offsets; + std::vector obj_coeffs; + std::vector var_lb; + std::vector var_ub; + std::vector constr_lb; + std::vector constr_ub; + i_t n_rows{0}; + i_t n_cols{0}; + i_t nnz{0}; +}; + template class third_party_presolve_t { public: @@ -58,7 +96,10 @@ class third_party_presolve_t { third_party_presolve_t(third_party_presolve_t&&) = delete; third_party_presolve_t& operator=(third_party_presolve_t&&) = delete; - third_party_presolve_result_t apply( + // Device entry: takes an optimization_problem_t and returns a device-side + // reduced optimization_problem_t. Internally a thin shim over + // apply_presolve_from_mps_data + third_party_presolve_device_result_t apply_presolve_from_op_problem( optimization_problem_t const& op_problem, problem_category_t category, cuopt::mathematical_optimization::presolver_t presolver, @@ -68,6 +109,18 @@ class third_party_presolve_t { double time_limit, i_t num_cpu_threads = 0); + // Host entry: takes an mps_data_model_t and returns a host-side reduced + // mps_data_model_t. Pure-host throughout + third_party_presolve_host_result_t apply_presolve_from_mps_data( + io::mps_data_model_t const& mps_problem, + problem_category_t category, + cuopt::mathematical_optimization::presolver_t presolver, + bool dual_postsolve, + f_t absolute_tolerance, + f_t relative_tolerance, + double time_limit, + i_t num_cpu_threads = 0); + void undo(rmm::device_uvector& primal_solution, rmm::device_uvector& dual_solution, rmm::device_uvector& reduced_costs, @@ -93,8 +146,15 @@ class third_party_presolve_t { ~third_party_presolve_t(); private: - third_party_presolve_result_t apply_pslp( - optimization_problem_t const& op_problem, const double time_limit); + third_party_presolve_status_t apply_pslp(pslp_input_t& arrays, double time_limit); + + third_party_presolve_status_t apply_papilo(papilo::Problem& papilo_problem, + problem_category_t category, + bool dual_postsolve, + f_t absolute_tolerance, + f_t relative_tolerance, + double time_limit, + i_t num_cpu_threads); // Host-only per-backend postsolve helpers. Both resize their vector args // to original-problem dimensions. diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 80f68ee500..4d8adba04c 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -448,7 +448,7 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p } double presolve_time = 0.0; std::unique_ptr> presolver; - std::optional> presolve_result_opt; + std::optional> presolve_result_opt; mip::problem_t problem( op_problem, settings.get_tolerances(), settings.determinism_mode == CUOPT_MODE_DETERMINISTIC); @@ -555,14 +555,15 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p presolve_time_limit = std::numeric_limits::infinity(); } presolver = std::make_unique>(); - auto result = presolver->apply(op_problem, - cuopt::mathematical_optimization::problem_category_t::MIP, - settings.presolver, - dual_postsolve, - settings.tolerances.absolute_tolerance, - settings.tolerances.relative_tolerance, - presolve_time_limit, - settings.num_cpu_threads); + auto result = presolver->apply_presolve_from_op_problem( + op_problem, + cuopt::mathematical_optimization::problem_category_t::MIP, + settings.presolver, + dual_postsolve, + settings.tolerances.absolute_tolerance, + settings.tolerances.relative_tolerance, + presolve_time_limit, + settings.num_cpu_threads); if (result.status == mip::third_party_presolve_status_t::INFEASIBLE) { return mip_solution_t(mip_termination_status_t::Infeasible, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 2d159f818f..25382f17bb 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1950,7 +1950,7 @@ optimization_problem_solution_t solve_lp( // Declare result at outer scope so that result.reduced_problem (which may be // referenced by problem.original_problem_ptr) remains alive through the solve. - std::optional> result; + std::optional> result; if (run_presolve) { sort_csr(op_problem); @@ -1960,13 +1960,14 @@ optimization_problem_solution_t solve_lp( const double presolve_time_limit = std::max(1.0, std::min(0.1 * lp_timer.remaining_time(), 60.0)); presolver = std::make_unique>(); - result = presolver->apply(op_problem, - cuopt::mathematical_optimization::problem_category_t::LP, - settings.presolver, - settings.dual_postsolve, - settings.tolerances.absolute_primal_tolerance, - settings.tolerances.relative_primal_tolerance, - presolve_time_limit); + result = presolver->apply_presolve_from_op_problem( + op_problem, + cuopt::mathematical_optimization::problem_category_t::LP, + settings.presolver, + settings.dual_postsolve, + settings.tolerances.absolute_primal_tolerance, + settings.tolerances.relative_primal_tolerance, + presolve_time_limit); if (result->status == mip::third_party_presolve_status_t::INFEASIBLE) { return optimization_problem_solution_t( pdlp_termination_status_t::PrimalInfeasible, op_problem.get_handle_ptr()->get_stream()); @@ -2209,6 +2210,111 @@ mps_data_model_to_optimization_problem( return op_problem; } +template +cuopt::mathematical_optimization::io::mps_data_model_t op_problem_to_mps_data_model( + const optimization_problem_t& op_problem) +{ + raft::common::nvtx::range fun_scope("op_problem -> mps_data_model (D->H)"); + cuopt::mathematical_optimization::io::mps_data_model_t mps; + + mps.set_maximize(op_problem.get_sense()); + mps.set_objective_scaling_factor(op_problem.get_objective_scaling_factor()); + mps.set_objective_offset(op_problem.get_objective_offset()); + + if (!op_problem.get_problem_name().empty()) { + mps.set_problem_name(op_problem.get_problem_name()); + } + if (!op_problem.get_objective_name().empty()) { + mps.set_objective_name(op_problem.get_objective_name()); + } + if (!op_problem.get_variable_names().empty()) { + mps.set_variable_names(op_problem.get_variable_names()); + } + if (!op_problem.get_row_names().empty()) { mps.set_row_names(op_problem.get_row_names()); } + + const auto& d_coefficients = op_problem.get_constraint_matrix_values(); + const auto& d_offsets = op_problem.get_constraint_matrix_offsets(); + const auto& d_indices = op_problem.get_constraint_matrix_indices(); + const auto& d_obj_coeffs = op_problem.get_objective_coefficients(); + const auto& d_var_lb = op_problem.get_variable_lower_bounds(); + const auto& d_var_ub = op_problem.get_variable_upper_bounds(); + const auto& d_bounds = op_problem.get_constraint_bounds(); + const auto& d_row_types = op_problem.get_row_types(); + const auto& d_constr_lb = op_problem.get_constraint_lower_bounds(); + const auto& d_constr_ub = op_problem.get_constraint_upper_bounds(); + const auto& d_var_types = op_problem.get_variable_types(); + + std::vector h_coefficients(d_coefficients.size()); + std::vector h_offsets(d_offsets.size()); + std::vector h_indices(d_indices.size()); + std::vector h_obj_coeffs(d_obj_coeffs.size()); + std::vector h_var_lb(d_var_lb.size()); + std::vector h_var_ub(d_var_ub.size()); + std::vector h_bounds(d_bounds.size()); + std::vector h_row_types(d_row_types.size()); + std::vector h_constr_lb(d_constr_lb.size()); + std::vector h_constr_ub(d_constr_ub.size()); + std::vector h_var_types_enum(d_var_types.size()); + + auto stream = op_problem.get_handle_ptr()->get_stream(); + raft::copy(h_coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); + raft::copy(h_offsets.data(), d_offsets.data(), d_offsets.size(), stream); + raft::copy(h_indices.data(), d_indices.data(), d_indices.size(), stream); + raft::copy(h_obj_coeffs.data(), d_obj_coeffs.data(), d_obj_coeffs.size(), stream); + raft::copy(h_var_lb.data(), d_var_lb.data(), d_var_lb.size(), stream); + raft::copy(h_var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); + raft::copy(h_bounds.data(), d_bounds.data(), d_bounds.size(), stream); + raft::copy(h_row_types.data(), d_row_types.data(), d_row_types.size(), stream); + raft::copy(h_constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); + raft::copy(h_constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); + raft::copy(h_var_types_enum.data(), d_var_types.data(), d_var_types.size(), stream); + stream.synchronize(); + + if (!h_offsets.empty()) { + mps.set_csr_constraint_matrix(std::span(h_coefficients.data(), h_coefficients.size()), + std::span(h_indices.data(), h_indices.size()), + std::span(h_offsets.data(), h_offsets.size())); + } else { + // set_csr_constraint_matrix rejects empty offsets — synthesize the [0] + // sentinel that downstream consumers expect for a zero-row problem. + std::vector empty_offsets{0}; + mps.set_csr_constraint_matrix( + {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); + } + + if (!h_obj_coeffs.empty()) { + mps.set_objective_coefficients(std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); + } + if (!h_var_lb.empty()) { + mps.set_variable_lower_bounds(std::span(h_var_lb.data(), h_var_lb.size())); + } + if (!h_var_ub.empty()) { + mps.set_variable_upper_bounds(std::span(h_var_ub.data(), h_var_ub.size())); + } + if (!h_bounds.empty()) { + mps.set_constraint_bounds(std::span(h_bounds.data(), h_bounds.size())); + } + if (!h_row_types.empty()) { + mps.set_row_types(std::span(h_row_types.data(), h_row_types.size())); + } + if (!h_constr_lb.empty()) { + mps.set_constraint_lower_bounds(std::span(h_constr_lb.data(), h_constr_lb.size())); + } + if (!h_constr_ub.empty()) { + mps.set_constraint_upper_bounds(std::span(h_constr_ub.data(), h_constr_ub.size())); + } + if (!h_var_types_enum.empty()) { + std::vector h_var_types_char(h_var_types_enum.size()); + std::transform(h_var_types_enum.begin(), + h_var_types_enum.end(), + h_var_types_char.begin(), + var_type_to_char); + mps.set_variable_types(h_var_types_char); + } + + return mps; +} + template optimization_problem_solution_t solve_lp( raft::handle_t const* handle_ptr, @@ -2239,10 +2345,6 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( cuopt_expects(settings.use_distributed_pdlp, error_type_t::ValidationError, "solve_lp_distributed_from_mps: settings.use_distributed_pdlp must be true"); - cuopt_expects(settings.presolver == cuopt::mathematical_optimization::presolver_t::None, - error_type_t::ValidationError, - "solve_lp_distributed_from_mps: presolve is not yet supported with " - "use_distributed_pdlp; please set settings.presolver = presolver_t::None"); pdlp_solver_settings_t settings_resolved = settings; cuopt_expects(settings_resolved.method == method_t::PDLP, @@ -2290,6 +2392,109 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( auto lp_timer = cuopt::timer_t(settings_resolved.time_limit); + if (settings_resolved.presolver == presolver_t::Default) { + settings_resolved.presolver = presolver_t::PSLP; + CUOPT_LOG_INFO("Using PSLP presolver"); + } + const bool run_presolve = settings_resolved.presolver != presolver_t::None; + + std::unique_ptr> presolver_ptr; + std::optional> host_res; + [[maybe_unused]] double presolve_time = 0.0; + + if (run_presolve) { + // mirroring single-GPU solve.cu + const double presolve_time_limit = + std::max(1.0, std::min(0.1 * lp_timer.remaining_time(), 60.0)); + + presolver_ptr = std::make_unique>(); + host_res = presolver_ptr->apply_presolve_from_mps_data( + mps_data_model, + cuopt::mathematical_optimization::problem_category_t::LP, + settings_resolved.presolver, + settings_resolved.dual_postsolve, + settings_resolved.tolerances.absolute_primal_tolerance, + settings_resolved.tolerances.relative_primal_tolerance, + presolve_time_limit); + + if (host_res->status == mip::third_party_presolve_status_t::INFEASIBLE) { + return optimization_problem_solution_t( + pdlp_termination_status_t::PrimalInfeasible, handle_ptr->get_stream()); + } + if (host_res->status == mip::third_party_presolve_status_t::UNBNDORINFEAS) { + return optimization_problem_solution_t( + pdlp_termination_status_t::UnboundedOrInfeasible, handle_ptr->get_stream()); + } + if (host_res->status == mip::third_party_presolve_status_t::UNBOUNDED) { + return optimization_problem_solution_t( + pdlp_termination_status_t::DualInfeasible, handle_ptr->get_stream()); + } + + // Presolve completely solved the problem. Mirroring single-GPU solve + if (host_res->reduced_problem.get_n_variables() == 0 && + host_res->reduced_problem.get_n_constraints() == 0) { + CUOPT_LOG_INFO("Presolve completely solved the problem"); + presolve_time = lp_timer.elapsed_time(); + CUOPT_LOG_INFO("%s presolve time: %.2fs", + settings_resolved.presolver == presolver_t::PSLP ? "PSLP" : "Papilo", + presolve_time); + + std::vector h_primal, h_dual, h_rc; + presolver_ptr->undo_host(h_primal, + h_dual, + h_rc, + cuopt::mathematical_optimization::problem_category_t::LP, + /*status_to_skip=*/false, + settings_resolved.dual_postsolve); + + auto primal_uv = cuopt::device_copy(h_primal, handle_ptr->get_stream()); + auto dual_uv = cuopt::device_copy(h_dual, handle_ptr->get_stream()); + auto rc_uv = cuopt::device_copy(h_rc, handle_ptr->get_stream()); + handle_ptr->sync_stream(); + + typename optimization_problem_solution_t::additional_termination_information_t + term_info; + term_info.primal_objective = host_res->reduced_problem.get_objective_offset(); + term_info.dual_objective = host_res->reduced_problem.get_objective_offset(); + term_info.number_of_steps_taken = 0; + term_info.solve_time = presolve_time; + term_info.l2_primal_residual = 0.0; + term_info.l2_dual_residual = 0.0; + term_info.gap = 0.0; + + std::vector< + typename optimization_problem_solution_t::additional_termination_information_t> + term_vec{term_info}; + std::vector status_vec{pdlp_termination_status_t::Optimal}; + + CUOPT_LOG_INFO("Status: Optimal Objective: %f", term_info.primal_objective); + return optimization_problem_solution_t(primal_uv, + dual_uv, + rc_uv, + mps_data_model.get_objective_name(), + mps_data_model.get_variable_names(), + mps_data_model.get_row_names(), + std::move(term_vec), + std::move(status_vec)); + } + + presolve_time = lp_timer.elapsed_time(); + CUOPT_LOG_INFO("%s presolve time: %.2fs", + settings_resolved.presolver == presolver_t::PSLP ? "PSLP" : "Papilo", + presolve_time); + CUOPT_LOG_INFO( + "Distributed-solving reduced problem: %d constraints, %d variables, %d nonzeros", + host_res->reduced_problem.get_n_constraints(), + host_res->reduced_problem.get_n_variables(), + host_res->reduced_problem.get_nnz()); + } + + // mps_for_solver is what the distributed solver actually sees. + // the reduced + // problem when we ran presolve, the original otherwise. No data transits through device + const auto& mps_for_solver = run_presolve ? host_res->reduced_problem : mps_data_model; + + // -------------------------- DISTRIBUTED SOLVE -------------------------- // Shape-0 placeholder: needed to build an empty pdlp_solver cuopt::mathematical_optimization::optimization_problem_t placeholder_op(handle_ptr); { @@ -2299,19 +2504,52 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( } mip::problem_t placeholder_problem(placeholder_op); - pdlp::pdlp_solver_t solver(placeholder_problem, mps_data_model, settings_resolved); + pdlp::pdlp_solver_t solver(placeholder_problem, mps_for_solver, settings_resolved); auto sol = solver.run_solver(lp_timer); - // Maximization post-processing (matches run_pdlp at solve.cu:835-839): + // Maximization post-processing (matches run_pdlp): // PDLP internally solves the negated objective, so flip dual / reduced // cost signs on the gathered solution before returning. - if (mps_data_model.get_sense()) { + if (mps_for_solver.get_sense()) { adjust_dual_solution_and_reduced_cost( sol.get_dual_solution(), sol.get_reduced_cost(), handle_ptr->get_stream()); handle_ptr->sync_stream(); } + // postsolve + if (run_presolve) { + auto h_primal = cuopt::host_copy(sol.get_primal_solution(), handle_ptr->get_stream()); + auto h_dual = cuopt::host_copy(sol.get_dual_solution(), handle_ptr->get_stream()); + auto h_rc = cuopt::host_copy(sol.get_reduced_cost(), handle_ptr->get_stream()); + handle_ptr->sync_stream(); + + presolver_ptr->undo_host(h_primal, + h_dual, + h_rc, + cuopt::mathematical_optimization::problem_category_t::LP, + /*status_to_skip=*/false, + settings_resolved.dual_postsolve); + + auto primal_uv = cuopt::device_copy(h_primal, handle_ptr->get_stream()); + auto dual_uv = cuopt::device_copy(h_dual, handle_ptr->get_stream()); + auto rc_uv = cuopt::device_copy(h_rc, handle_ptr->get_stream()); + handle_ptr->sync_stream(); + + auto term_vec = sol.get_additional_termination_informations(); + auto status_vec = sol.get_terminations_status(); + + sol = optimization_problem_solution_t(primal_uv, + dual_uv, + rc_uv, + std::move(sol.get_pdlp_warm_start_data()), + mps_data_model.get_objective_name(), + mps_data_model.get_variable_names(), + mps_data_model.get_row_names(), + std::move(term_vec), + std::move(status_vec)); + } + sol.set_solve_time(lp_timer.elapsed_time()); CUOPT_LOG_INFO("PDLP finished"); if (sol.get_termination_status() != pdlp_termination_status_t::ConcurrentLimit) { @@ -2471,6 +2709,9 @@ std::unique_ptr> solve_lp( raft::handle_t const* handle_ptr, \ const cuopt::mathematical_optimization::io::mps_data_model_t& data_model); \ \ + template cuopt::mathematical_optimization::io::mps_data_model_t \ + op_problem_to_mps_data_model(const optimization_problem_t& op_problem); \ + \ template optimization_problem_solution_t solve_lp_distributed_from_mps( \ raft::handle_t const* handle_ptr, \ const cuopt::mathematical_optimization::io::mps_data_model_t& mps_data_model, \ @@ -2488,4 +2729,18 @@ INSTANTIATE(float) INSTANTIATE(double) #endif +// third_party_presolve_t (in mip_heuristics/presolve/) is built +// whenever PDLP_INSTANTIATE_FLOAT is on and depends on the float overloads of +// mps_data_model_to_optimization_problem and op_problem_to_mps_data_model. +// Make sure both symbols exist in PDLP-only float builds where +// MIP_INSTANTIATE_FLOAT is off. +#if PDLP_INSTANTIATE_FLOAT && !MIP_INSTANTIATE_FLOAT +template optimization_problem_t mps_data_model_to_optimization_problem( + raft::handle_t const* handle_ptr, + const cuopt::mathematical_optimization::io::mps_data_model_t& data_model); + +template cuopt::mathematical_optimization::io::mps_data_model_t +op_problem_to_mps_data_model(const optimization_problem_t& op_problem); +#endif + } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solve.cuh b/cpp/src/pdlp/solve.cuh index 9250245079..aed7a70ac2 100644 --- a/cpp/src/pdlp/solve.cuh +++ b/cpp/src/pdlp/solve.cuh @@ -65,8 +65,8 @@ cuopt::mathematical_optimization::optimization_problem_solution_t solv * resolved settings before solving. * * @pre `settings.use_distributed_pdlp == true`, `method == PDLP`, - * `presolver == None`, `pdlp_precision == DefaultPrecision`, not inside - * MIP, and no initial primal/dual or warm-start data. + * `pdlp_precision == DefaultPrecision`, not inside MIP, and no initial + * primal/dual or warm-start data. */ template cuopt::mathematical_optimization::optimization_problem_solution_t diff --git a/cpp/tests/mip/presolve_test.cu b/cpp/tests/mip/presolve_test.cu index 5d82dcec0a..500f602c2f 100644 --- a/cpp/tests/mip/presolve_test.cu +++ b/cpp/tests/mip/presolve_test.cu @@ -37,14 +37,15 @@ TEST(problem, find_implied_integers) auto mps_data_model = cuopt::mathematical_optimization::io::read_mps(path, false); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_data_model); auto presolver = std::make_unique>(); - auto result = presolver->apply(op_problem, - cuopt::mathematical_optimization::problem_category_t::MIP, - cuopt::mathematical_optimization::presolver_t::Papilo, - false, - 1e-6, - 1e-12, - 20, - 1); + auto result = presolver->apply_presolve_from_op_problem( + op_problem, + cuopt::mathematical_optimization::problem_category_t::MIP, + cuopt::mathematical_optimization::presolver_t::Papilo, + false, + 1e-6, + 1e-12, + 20, + 1); ASSERT_NE(result.status, mip::third_party_presolve_status_t::INFEASIBLE); ASSERT_NE(result.status, mip::third_party_presolve_status_t::UNBNDORINFEAS); From 89c88787bd8b8c57febf52b264836e59ae74c929 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 30 Jun 2026 14:27:50 -0700 Subject: [PATCH 142/258] re-enabled pre solver in pdlp__test --- cpp/tests/linear_programming/pdlp_test.cu | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 3b8edaa4fb..4fc916406e 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -175,11 +175,9 @@ void expect_distributed_matches_base(raft::handle_t const& handle, auto path = make_path_absolute(mps_rel_path); io::mps_data_model_t problem = io::read_mps(path, fixed_mps_format); - // Shared settings: PDLP, no presolve (distributed requires presolver == None, so the - // base run must match to keep the two problems identical). + // Shared settings: method is PDLP pdlp_solver_settings_t base_settings{}; base_settings.method = method_t::PDLP; - base_settings.presolver = presolver_t::None; // ----- base: single-GPU PDLP (materialize the full problem on one GPU) ----- auto base_op = mps_data_model_to_optimization_problem(&handle, problem); From 2fc3addfc00dfb327015e28c75e495779fa84e07 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 30 Jun 2026 23:55:24 +0200 Subject: [PATCH 143/258] style --- .../cuopt/mathematical_optimization/solve.hpp | 4 +- .../presolve/third_party_presolve.cpp | 63 +++++++++---------- .../presolve/third_party_presolve.hpp | 2 +- cpp/src/pdlp/solve.cu | 30 +++++---- cpp/tests/linear_programming/pdlp_test.cu | 2 +- 5 files changed, 49 insertions(+), 52 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/solve.hpp b/cpp/include/cuopt/mathematical_optimization/solve.hpp index 15ca990941..f7f138ed53 100644 --- a/cpp/include/cuopt/mathematical_optimization/solve.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solve.hpp @@ -154,8 +154,8 @@ optimization_problem_t mps_data_model_to_optimization_problem( // row_types + RHS) and the relevant metadata (objective offset/scale, names, // sense). One stream sync at the end. template -cuopt::mathematical_optimization::io::mps_data_model_t -op_problem_to_mps_data_model(const optimization_problem_t& op_problem); +cuopt::mathematical_optimization::io::mps_data_model_t op_problem_to_mps_data_model( + const optimization_problem_t& op_problem); // ============================================================================ // CPU problem overloads (convert to GPU, solve, convert solution back) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index ab2f42e988..90fe4a10bd 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -78,7 +78,8 @@ pslp_input_t build_pslp_host_arrays_from_mps_data( const auto& h_row_types = mps.get_row_types(); if (maximize) { - for (auto& c : arrays.obj_coeffs) c = -c; + for (auto& c : arrays.obj_coeffs) + c = -c; } if (arrays.constr_lb.empty() && arrays.constr_ub.empty()) { @@ -110,8 +111,9 @@ pslp_input_t build_pslp_host_arrays_from_mps_data( // Single source of truth for papilo input construction, the op_problem path // reaches this via op_problem_to_mps_data_model first. template -papilo::Problem build_papilo_problem_from_mps_data( - const io::mps_data_model_t& mps, problem_category_t category, bool maximize) +papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model_t& mps, + problem_category_t category, + bool maximize) { raft::common::nvtx::range fun_scope("Build papilo problem from mps_data_model"); papilo::ProblemBuilder builder; @@ -137,7 +139,8 @@ papilo::Problem build_papilo_problem_from_mps_data( const auto& h_var_types = mps.get_variable_types(); if (maximize) { - for (auto& c : h_obj_coeffs) c = -c; + for (auto& c : h_obj_coeffs) + c = -c; } if (h_constr_lb.empty() && h_constr_ub.empty()) { @@ -276,10 +279,10 @@ io::mps_data_model_t build_mps_data_from_pslp(Presolver* pslp_presolve std::vector h_obj_coeffs(reduced_prob->c, reduced_prob->c + n_cols); if (maximize) { - for (auto& c : h_obj_coeffs) c = -c; + for (auto& c : h_obj_coeffs) + c = -c; } - mps.set_objective_coefficients( - std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); + mps.set_objective_coefficients(std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); mps.set_constraint_lower_bounds( std::span(reduced_prob->lhs, static_cast(n_rows))); mps.set_constraint_upper_bounds( @@ -289,8 +292,7 @@ io::mps_data_model_t build_mps_data_from_pslp(Presolver* pslp_presolve mps.set_variable_upper_bounds( std::span(reduced_prob->ubs, static_cast(n_cols))); } else { - cuopt_expects( - false, error_type_t::ValidationError, "PSLP only supports double precision"); + cuopt_expects(false, error_type_t::ValidationError, "PSLP only supports double precision"); } return mps; } @@ -298,8 +300,8 @@ io::mps_data_model_t build_mps_data_from_pslp(Presolver* pslp_presolve // Host-only result builder: write the (reduced) papilo::Problem into a fresh // mps_data_model_t. template -io::mps_data_model_t build_mps_data_from_papilo(papilo::Problem const& papilo_problem, - bool maximize) +io::mps_data_model_t build_mps_data_from_papilo( + papilo::Problem const& papilo_problem, bool maximize) { raft::common::nvtx::range fun_scope("Build mps_data_model from papilo"); io::mps_data_model_t mps; @@ -532,7 +534,7 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( settings->verbose = false; settings->max_time = time_limit; - auto start_time = std::chrono::high_resolution_clock::now(); + auto start_time = std::chrono::high_resolution_clock::now(); Presolver* presolver = new_presolver(arrays.coefficients.data(), arrays.indices.data(), arrays.offsets.data(), @@ -547,8 +549,8 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( settings); assert(presolver != nullptr && "Presolver initialization failed"); const PresolveStatus pslp_status = run_presolver(presolver); - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); CUOPT_LOG_DEBUG("PSLP presolver time: %d milliseconds", duration.count()); CUOPT_LOG_INFO("PSLP Presolved problem: %d constraints, %d variables, %d non-zeros", presolver->stats->n_rows_reduced, @@ -729,8 +731,8 @@ third_party_presolve_t::apply_presolve_from_mps_data( if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { if constexpr (std::is_same_v) { const f_t original_obj_offset = mps.get_objective_offset(); - auto arrays = build_pslp_host_arrays_from_mps_data(mps, maximize_); - auto status = apply_pslp(arrays, time_limit); + auto arrays = build_pslp_host_arrays_from_mps_data(mps, maximize_); + auto status = apply_pslp(arrays, time_limit); if (status == third_party_presolve_status_t::INFEASIBLE || status == third_party_presolve_status_t::UNBNDORINFEAS) { @@ -738,8 +740,8 @@ third_party_presolve_t::apply_presolve_from_mps_data( status, io::mps_data_model_t{}, {}, {}, {}}; } - auto reduced_mps = build_mps_data_from_pslp( - pslp_presolver_, maximize_, original_obj_offset); + auto reduced_mps = + build_mps_data_from_pslp(pslp_presolver_, maximize_, original_obj_offset); reduced_mps.set_problem_name(mps.get_problem_name()); reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); return third_party_presolve_host_result_t{ @@ -747,12 +749,11 @@ third_party_presolve_t::apply_presolve_from_mps_data( } else { cuopt_expects( false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); - return third_party_presolve_host_result_t{ - third_party_presolve_status_t::UNCHANGED, - io::mps_data_model_t{}, - {}, - {}, - {}}; // unreachable + return third_party_presolve_host_result_t{third_party_presolve_status_t::UNCHANGED, + io::mps_data_model_t{}, + {}, + {}, + {}}; // unreachable } } else { // Papilo branch: host gather -> apply_papilo (host) -> host build @@ -786,12 +787,11 @@ third_party_presolve_t::apply_presolve_from_mps_data( } } - return third_party_presolve_host_result_t{ - status, - std::move(reduced_mps), - std::move(implied_integer_indices), - reduced_to_original_map_, - original_to_reduced_map_}; + return third_party_presolve_host_result_t{status, + std::move(reduced_mps), + std::move(implied_integer_indices), + reduced_to_original_map_, + original_to_reduced_map_}; } } @@ -803,8 +803,7 @@ void third_party_presolve_t::undo_pslp_host(std::vector& primal_s if constexpr (std::is_same_v) { // PSLP postsolve reads from the passed-in host buffers and writes the // uncrushed solution into pslp_presolver_->sol->{x, y, z}. - postsolve( - pslp_presolver_, primal_solution.data(), dual_solution.data(), reduced_costs.data()); + postsolve(pslp_presolver_, primal_solution.data(), dual_solution.data(), reduced_costs.data()); auto uncrushed_sol = pslp_presolver_->sol; const int n_cols = uncrushed_sol->dim_x; diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 06a8544520..80c518cf3a 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -22,7 +22,7 @@ class PostsolveStorage; // Forward declaration for Papilo Problem class template class Problem; -} +} // namespace papilo // Forward declaration for mps_data_model_t namespace cuopt::mathematical_optimization::io { diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 25382f17bb..e0cda71d41 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2271,9 +2271,10 @@ cuopt::mathematical_optimization::io::mps_data_model_t op_problem_to_m stream.synchronize(); if (!h_offsets.empty()) { - mps.set_csr_constraint_matrix(std::span(h_coefficients.data(), h_coefficients.size()), - std::span(h_indices.data(), h_indices.size()), - std::span(h_offsets.data(), h_offsets.size())); + mps.set_csr_constraint_matrix( + std::span(h_coefficients.data(), h_coefficients.size()), + std::span(h_indices.data(), h_indices.size()), + std::span(h_offsets.data(), h_offsets.size())); } else { // set_csr_constraint_matrix rejects empty offsets — synthesize the [0] // sentinel that downstream consumers expect for a zero-row problem. @@ -2305,10 +2306,8 @@ cuopt::mathematical_optimization::io::mps_data_model_t op_problem_to_m } if (!h_var_types_enum.empty()) { std::vector h_var_types_char(h_var_types_enum.size()); - std::transform(h_var_types_enum.begin(), - h_var_types_enum.end(), - h_var_types_char.begin(), - var_type_to_char); + std::transform( + h_var_types_enum.begin(), h_var_types_enum.end(), h_var_types_char.begin(), var_type_to_char); mps.set_variable_types(h_var_types_char); } @@ -2418,16 +2417,16 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( presolve_time_limit); if (host_res->status == mip::third_party_presolve_status_t::INFEASIBLE) { - return optimization_problem_solution_t( - pdlp_termination_status_t::PrimalInfeasible, handle_ptr->get_stream()); + return optimization_problem_solution_t(pdlp_termination_status_t::PrimalInfeasible, + handle_ptr->get_stream()); } if (host_res->status == mip::third_party_presolve_status_t::UNBNDORINFEAS) { return optimization_problem_solution_t( pdlp_termination_status_t::UnboundedOrInfeasible, handle_ptr->get_stream()); } if (host_res->status == mip::third_party_presolve_status_t::UNBOUNDED) { - return optimization_problem_solution_t( - pdlp_termination_status_t::DualInfeasible, handle_ptr->get_stream()); + return optimization_problem_solution_t(pdlp_termination_status_t::DualInfeasible, + handle_ptr->get_stream()); } // Presolve completely solved the problem. Mirroring single-GPU solve @@ -2482,11 +2481,10 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( CUOPT_LOG_INFO("%s presolve time: %.2fs", settings_resolved.presolver == presolver_t::PSLP ? "PSLP" : "Papilo", presolve_time); - CUOPT_LOG_INFO( - "Distributed-solving reduced problem: %d constraints, %d variables, %d nonzeros", - host_res->reduced_problem.get_n_constraints(), - host_res->reduced_problem.get_n_variables(), - host_res->reduced_problem.get_nnz()); + CUOPT_LOG_INFO("Distributed-solving reduced problem: %d constraints, %d variables, %d nonzeros", + host_res->reduced_problem.get_n_constraints(), + host_res->reduced_problem.get_n_variables(), + host_res->reduced_problem.get_nnz()); } // mps_for_solver is what the distributed solver actually sees. diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 4fc916406e..e4b7d9c715 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -177,7 +177,7 @@ void expect_distributed_matches_base(raft::handle_t const& handle, // Shared settings: method is PDLP pdlp_solver_settings_t base_settings{}; - base_settings.method = method_t::PDLP; + base_settings.method = method_t::PDLP; // ----- base: single-GPU PDLP (materialize the full problem on one GPU) ----- auto base_op = mps_data_model_to_optimization_problem(&handle, problem); From c5a4c5c76f4ef12de6a89b5cc9baaa1a75f4d2ee Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 1 Jul 2026 17:50:08 +0200 Subject: [PATCH 144/258] silence kaminpar warnings --- cpp/cmake/thirdparty/get_kaminpar.cmake | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cpp/cmake/thirdparty/get_kaminpar.cmake b/cpp/cmake/thirdparty/get_kaminpar.cmake index c20880b332..0ea104b360 100644 --- a/cpp/cmake/thirdparty/get_kaminpar.cmake +++ b/cpp/cmake/thirdparty/get_kaminpar.cmake @@ -12,6 +12,16 @@ function(find_and_configure_kaminpar) set(oneValueArgs VERSION PINNED_TAG) cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN}) + # Silence the (many) warnings from KaMinPar and its bundled third-party + # dependencies (KaHyPar, KaGen, growt, ...). They are compiled from pinned + # upstream source we do not maintain, so their diagnostics only drown out + # cuOpt's own build output. Appending -w here is function-scoped: the CPM + # add_subdirectory below inherits these flags, but cuOpt's targets (compiled + # in the parent scope with -Wall -Werror) are unaffected. A system/conda + # install of KaMinPar builds nothing, so the flag is simply unused there. + string(APPEND CMAKE_C_FLAGS " -w") + string(APPEND CMAKE_CXX_FLAGS " -w") + # NOTE: KaMinPar is intentionally NOT added to cuopt's BUILD/INSTALL export sets. # It is a from-source static dependency that is fully embedded into libcuopt.so and # never installed (INSTALL_KAMINPAR OFF below). Registering it in cuopt-exports would From 999b42397d4ddd608a3be5344b3679eabe80fdac Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 3 Jul 2026 19:21:26 +0200 Subject: [PATCH 145/258] added char_to_var_type(h_var_types[i] in presolve. needed for compilation --- cpp/src/mip_heuristics/presolve/third_party_presolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index e707921342..cfb429e016 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -176,7 +176,7 @@ papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model if (category == problem_category_t::MIP) { for (size_t i = 0; i < h_var_types.size(); ++i) { - builder.setColIntegral(i, h_var_types[i] == var_t::INTEGER); + builder.setColIntegral(i, char_to_var_type(h_var_types[i]) == var_t::INTEGER); } } From 20b9c6e687729312834ad3ecad437187cfcd38dc Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 3 Jul 2026 19:22:31 +0200 Subject: [PATCH 146/258] replace apply() with apply_presolve_from_op_problem() in presolve_test.cu --- cpp/tests/linear_programming/unit_tests/presolve_test.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/tests/linear_programming/unit_tests/presolve_test.cu b/cpp/tests/linear_programming/unit_tests/presolve_test.cu index 0c76de192a..2987f52a6a 100644 --- a/cpp/tests/linear_programming/unit_tests/presolve_test.cu +++ b/cpp/tests/linear_programming/unit_tests/presolve_test.cu @@ -515,7 +515,7 @@ TEST_P(dual_crush_round_trip, kkt_check) // Step 1: Presolve with a single presolver instance (same one used for crush later) sort_csr(op_problem); mip::third_party_presolve_t presolver; - auto result = presolver.apply(op_problem, + auto result = presolver.apply_presolve_from_op_problem(op_problem, problem_category_t::LP, presolver_t::Papilo, /*dual_postsolve=*/true, @@ -772,7 +772,7 @@ TEST_P(crush_warmstart, round_trip) // Step 1: Presolve sort_csr(op_problem); mip::third_party_presolve_t presolver; - auto result = presolver.apply(op_problem, + auto result = presolver.apply_presolve_from_op_problem(op_problem, problem_category_t::LP, presolver_t::Papilo, /*dual_postsolve=*/true, From cb4d5d6ab3b525c970f38abfd7e52b73835e604d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 3 Jul 2026 19:39:02 +0200 Subject: [PATCH 147/258] propagate objective offset --- cpp/src/pdlp/solve.cu | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index e0cda71d41..2fd341c6f7 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2500,6 +2500,9 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( placeholder_op.set_csr_constraint_matrix( nullptr, 0, nullptr, 0, empty_offsets.data(), static_cast(empty_offsets.size())); } + placeholder_op.set_objective_offset(mps_for_solver.get_objective_offset()); + placeholder_op.set_objective_scaling_factor(mps_for_solver.get_objective_scaling_factor()); + placeholder_op.set_maximize(mps_for_solver.get_sense()); mip::problem_t placeholder_problem(placeholder_op); pdlp::pdlp_solver_t solver(placeholder_problem, mps_for_solver, settings_resolved); From e6b37e427a73ad9c7e68ad98fa4124b8f464db4d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 3 Jul 2026 19:48:21 +0200 Subject: [PATCH 148/258] made pock_chambolle_scaling public and reduced the call in distributed_algorithm --- cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu | 7 +------ cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 6f6deb873a..8d463ca96d 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -260,12 +260,7 @@ void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, broadcast_constraint_scaling_to_halo(engine); engine.for_each_shard([alpha](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_compute_local_iteration_vectors( - alpha); - }); - - engine.for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_apply_cumulative_update(); + shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_scaling(alpha); }); // Final refresh for downstream consumers. diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index cc2a3627fe..a001dd7837 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -125,6 +125,7 @@ class pdlp_initial_scaling_strategy_t { void ruiz_iter_apply_cumulative_update(); void pock_chambolle_compute_local_iteration_vectors(f_t alpha); void pock_chambolle_apply_cumulative_update(); + void pock_chambolle_scaling(f_t alpha); rmm::device_uvector& get_iteration_variable_scaling() { return iteration_variable_scaling_; } // Restore the clean pre-scaling state for the distributed path. @@ -138,7 +139,6 @@ class pdlp_initial_scaling_strategy_t { private: void ruiz_inf_scaling(i_t number_of_ruiz_iterations); - void pock_chambolle_scaling(f_t alpha); void reset_integer_variables(); raft::handle_t const* handle_ptr_{nullptr}; From b799d2bd498c1da6a4586167aad3a8faeca0f066 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 3 Jul 2026 19:53:09 +0200 Subject: [PATCH 149/258] reverted back pock chambolle to initial state as the split was not necessary --- .../initial_scaling.cu | 18 +----------------- .../initial_scaling.cuh | 3 +-- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index f36fe61740..ac0f403e1b 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -420,12 +420,8 @@ __global__ void pock_chambolle_scaling_kernel_col( if (threadIdx.x == 0) initial_scaling_view.iteration_variable_scaling[col] = accumulated_value; } -// Local half of one Pock-Chambolle pass: writes the per-row and per-column -// sums-of-powers into iteration_constraint_matrix_scaling_ / -// iteration_variable_scaling_ template -void pdlp_initial_scaling_strategy_t::pock_chambolle_compute_local_iteration_vectors( - f_t alpha) +void pdlp_initial_scaling_strategy_t::pock_chambolle_scaling(f_t alpha) { // Reset the iteration_scaling vectors to all 0 RAFT_CUDA_TRY(cudaMemsetAsync( @@ -460,12 +456,7 @@ void pdlp_initial_scaling_strategy_t::pock_chambolle_compute_local_ite A_T_offsets_.data(), A_T_indices_.data()); RAFT_CUDA_TRY(cudaPeekAtLastError()); -} -// Fold half of one Pock-Chambolle pass: cumulative /= sqrt(iteration). -template -void pdlp_initial_scaling_strategy_t::pock_chambolle_apply_cumulative_update() -{ if (running_mip_) { reset_integer_variables(); } // divide the sqrt of the vectors of the sums from above to the respective scaling vectors @@ -484,13 +475,6 @@ void pdlp_initial_scaling_strategy_t::pock_chambolle_apply_cumulative_ stream_view_); } -template -void pdlp_initial_scaling_strategy_t::pock_chambolle_scaling(f_t alpha) -{ - pock_chambolle_compute_local_iteration_vectors(alpha); - pock_chambolle_apply_cumulative_update(); -} - template __global__ void scale_problem_kernel( const typename pdlp_initial_scaling_strategy_t::view_t initial_scaling_view, diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index a001dd7837..cb6e05b7ee 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -123,8 +123,7 @@ class pdlp_initial_scaling_strategy_t { void ruiz_iter_compute_local_iteration_vectors(); void ruiz_iter_apply_cumulative_update(); - void pock_chambolle_compute_local_iteration_vectors(f_t alpha); - void pock_chambolle_apply_cumulative_update(); + // Shard-local end-to-end Pock-Chambolle pass. Exposed for distributed PDLP: void pock_chambolle_scaling(f_t alpha); rmm::device_uvector& get_iteration_variable_scaling() { return iteration_variable_scaling_; } From e8f321d39f06ede4778f281ba2450d909ba3119e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 3 Jul 2026 20:05:54 +0200 Subject: [PATCH 150/258] cleaned up ruiz --- .../distributed_algorithms.cu | 15 +++++------ .../initial_scaling.cu | 27 ++++++++----------- .../initial_scaling.cuh | 6 +++-- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 8d463ca96d..e2a965d228 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -222,16 +222,13 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, broadcast_variable_scaling_to_halo(engine); broadcast_constraint_scaling_to_halo(engine); - // Per-shard local kernels: row inf-norm (owned rows, complete) + column - // inf-norm from A_T (owned columns, complete; halo columns -> 0). + // Shard-local Ruiz iteration + // rows: inf norm only over OWNED (full) rows from A + // cols: inf norm only over OWNED (full) cols from A_T + // Then fold into cumulative on owned entries (halo entries get refreshed by + // the next iteration's halo update) engine.for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_compute_local_iteration_vectors(); - }); - - // Fold into cumulative on owned entries (halo entries get refreshed by - // the next iteration's broadcast). - engine.for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_apply_cumulative_update(); + shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_local(); }); } diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index ac0f403e1b..4cd695e73b 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -236,17 +236,21 @@ __global__ void inf_norm_col_kernel( } } +// One iteration of Ruiz inf-norm scaling. +// Distributed PDLP calls this per outer iteration between halo broadcasts; template -void pdlp_initial_scaling_strategy_t::ruiz_iter_compute_local_iteration_vectors() +void pdlp_initial_scaling_strategy_t::ruiz_iter_local() { - // find inf norm over rows and columns of the scaled matrix in given iteration. - // Rows are reduced from the row-major matrix and columns from the transpose - // A_T (both kernels atomicMax into zero-initialized iteration vectors). RAFT_CUDA_TRY(cudaMemsetAsync( iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); RAFT_CUDA_TRY(cudaMemsetAsync( iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); + // Inf-norm over rows (owned rows, from row-major A) and columns (owned + // columns, from A_T). Split into two kernels so the distributed path can + // touch only owned entries. + // Reading cols data from A_t allows for better cache locality on the AtomicAdd + // than it would by reading cols data from A as it is csr-represented => scattered cols i_t number_of_blocks = op_problem_scaled_.n_constraints / block_size; if (op_problem_scaled_.n_constraints % block_size) number_of_blocks++; i_t number_of_threads = std::min(op_problem_scaled_.n_variables, (i_t)block_size); @@ -264,11 +268,9 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_compute_local_iteratio RAFT_CUDA_TRY(cudaPeekAtLastError()); if (running_mip_) { reset_integer_variables(); } -} -template -void pdlp_initial_scaling_strategy_t::ruiz_iter_apply_cumulative_update() -{ + // Fold this iteration's inf-norms into the cumulative scalings: + // cumulative /= sqrt(iteration). raft::linalg::binaryOp(cummulative_constraint_matrix_scaling_.data(), cummulative_constraint_matrix_scaling_.data(), iteration_constraint_matrix_scaling_.data(), @@ -282,12 +284,6 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_apply_cumulative_updat primal_size_h_, a_divides_sqrt_b_bounded(), stream_view_); - - // Reset the iteration_scaling vectors to all 0 - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_constraint_matrix_scaling_.data(), 0.0, sizeof(f_t) * dual_size_h_, stream_view_)); - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_variable_scaling_.data(), 0.0, sizeof(f_t) * primal_size_h_, stream_view_)); } template @@ -326,8 +322,7 @@ void pdlp_initial_scaling_strategy_t::ruiz_inf_scaling(i_t number_of_r std::cout << "Doing ruiz_inf_scaling" << std::endl; #endif for (int i = 0; i < number_of_ruiz_iterations; i++) { - ruiz_iter_compute_local_iteration_vectors(); - ruiz_iter_apply_cumulative_update(); + ruiz_iter_local(); } } diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index cb6e05b7ee..88c149f5b7 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -121,8 +121,10 @@ class pdlp_initial_scaling_strategy_t { // ----- Distributed-PDLP hooks ----- - void ruiz_iter_compute_local_iteration_vectors(); - void ruiz_iter_apply_cumulative_update(); + // One Ruiz iteration (compute iteration vectors + fold into + // cumulative). Exposed for distributed PDLP so the outer loop with halo + // broadcasts lives at the distributed level + void ruiz_iter_local(); // Shard-local end-to-end Pock-Chambolle pass. Exposed for distributed PDLP: void pock_chambolle_scaling(f_t alpha); rmm::device_uvector& get_iteration_variable_scaling() { return iteration_variable_scaling_; } From d108c1bf9a279bc9d752de5eaeb005f8b05191e9 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 4 Jul 2026 11:14:58 +0200 Subject: [PATCH 151/258] syle --- .../distributed_algorithms.cu | 5 ++-- .../initial_scaling.cu | 2 +- .../unit_tests/presolve_test.cu | 24 +++++++++---------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index e2a965d228..7352fb52d1 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -227,9 +227,8 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, // cols: inf norm only over OWNED (full) cols from A_T // Then fold into cumulative on owned entries (halo entries get refreshed by // the next iteration's halo update) - engine.for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_local(); - }); + engine.for_each_shard( + [](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_local(); }); } // Final refresh so downstream consumers (the scaled problem, the next diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 4cd695e73b..5ca6640b4d 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -248,7 +248,7 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_local() // Inf-norm over rows (owned rows, from row-major A) and columns (owned // columns, from A_T). Split into two kernels so the distributed path can - // touch only owned entries. + // touch only owned entries. // Reading cols data from A_t allows for better cache locality on the AtomicAdd // than it would by reading cols data from A as it is csr-represented => scattered cols i_t number_of_blocks = op_problem_scaled_.n_constraints / block_size; diff --git a/cpp/tests/linear_programming/unit_tests/presolve_test.cu b/cpp/tests/linear_programming/unit_tests/presolve_test.cu index 2987f52a6a..977d856c9e 100644 --- a/cpp/tests/linear_programming/unit_tests/presolve_test.cu +++ b/cpp/tests/linear_programming/unit_tests/presolve_test.cu @@ -516,12 +516,12 @@ TEST_P(dual_crush_round_trip, kkt_check) sort_csr(op_problem); mip::third_party_presolve_t presolver; auto result = presolver.apply_presolve_from_op_problem(op_problem, - problem_category_t::LP, - presolver_t::Papilo, - /*dual_postsolve=*/true, - /*abs_tol=*/1e-6, - /*rel_tol=*/1e-9, - /*time_limit=*/60.0); + problem_category_t::LP, + presolver_t::Papilo, + /*dual_postsolve=*/true, + /*abs_tol=*/1e-6, + /*rel_tol=*/1e-9, + /*time_limit=*/60.0); ASSERT_TRUE(result.status == mip::third_party_presolve_status_t::REDUCED || result.status == mip::third_party_presolve_status_t::UNCHANGED); @@ -773,12 +773,12 @@ TEST_P(crush_warmstart, round_trip) sort_csr(op_problem); mip::third_party_presolve_t presolver; auto result = presolver.apply_presolve_from_op_problem(op_problem, - problem_category_t::LP, - presolver_t::Papilo, - /*dual_postsolve=*/true, - /*abs_tol=*/1e-6, - /*rel_tol=*/1e-9, - /*time_limit=*/60.0); + problem_category_t::LP, + presolver_t::Papilo, + /*dual_postsolve=*/true, + /*abs_tol=*/1e-6, + /*rel_tol=*/1e-9, + /*time_limit=*/60.0); ASSERT_TRUE(result.status == mip::third_party_presolve_status_t::REDUCED || result.status == mip::third_party_presolve_status_t::UNCHANGED); From 06fc37a8a3d4647ab5c39f2d7355db7d3cd495f7 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 4 Jul 2026 12:59:12 +0200 Subject: [PATCH 152/258] way better implementation of bound_objective_rescaling --- .../distributed_algorithms.cu | 123 ++++++--------- .../initial_scaling.cu | 146 +++++++----------- .../initial_scaling.cuh | 9 +- cpp/src/pdlp/utils.cuh | 15 ++ 4 files changed, 118 insertions(+), 175 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 7352fb52d1..3ce604f58d 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -7,10 +7,16 @@ #include #include #include +#include #include +#include + #include +#include +#include +#include #include #include @@ -117,92 +123,53 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng } // -------- Distributed bound / objective rescaling ------------------------- +// + apply_bound_objective_rescaling_to_problem, unfused because we need a +// raw squared-sum on device to hand to NCCL AllReduce and the base version comptues tranform->reduce->transform in one cub call for efficiency template void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, f_t c_scaling_weight) { - const int nb = static_cast(engine.shards.size()); - - // Per-shard packed partial squared norms: [0] = bound (rhs) sq, [1] = obj sq. - std::vector> sq; - sq.reserve(nb); - - // 1) per-shard partial squared norms over owned entries only - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - sq.emplace_back(2, s.stream.view()); + raft::common::nvtx::range scope("distributed_bound_objective_rescaling"); + // 1) + 2) Local transform-reduce on each shard, accumulate the global + // squared L2 norms on host as we go. + f_t global_bound_sq = f_t(0); + f_t global_obj_sq = f_t(0); + engine.for_each_shard([&](auto& s) { const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); - const int n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); - const int n_owned_var = static_cast(s.rank_data.owned_var_size); - - // Squared-norm contribution of each constraint's [lower, upper] bound pair - // (mirrors rhs_sum_of_squares_t). - const f_t* upper = scaled.constraint_upper_bounds.data(); - auto bound_op = [upper] __device__(f_t lower, i_t i) { - const f_t u = upper[i]; - f_t sum = f_t(0); - if (isfinite(lower) && (lower != u)) sum += lower * lower; - if (isfinite(u)) sum += u * u; - return sum; - }; - raft::linalg::reduce(sq[r].data() + 0, - scaled.constraint_lower_bounds.data(), - n_owned_cstr, - 1, - f_t(0), - s.stream.view(), - false, - bound_op, - raft::Sum()); - - // Weighted sum of squares of the objective coefficients. - auto obj_op = [c_scaling_weight] __device__(f_t v, i_t) { return v * v * c_scaling_weight; }; - raft::linalg::reduce(sq[r].data() + 1, - scaled.objective_coefficients.data(), - n_owned_var, - 1, - f_t(0), - s.stream.view(), - false, - obj_op, - raft::Sum()); - } + const i_t n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); + const i_t n_owned_var = static_cast(s.rank_data.owned_var_size); + auto policy = rmm::exec_policy(s.stream.view()); + + auto bounds_begin = thrust::make_zip_iterator(scaled.constraint_lower_bounds.data(), + scaled.constraint_upper_bounds.data()); + global_bound_sq += thrust::transform_reduce(policy, + bounds_begin, + bounds_begin + n_owned_cstr, + rhs_sum_of_squares_t{}, + f_t(0), + thrust::plus{}); + global_obj_sq += thrust::transform_reduce(policy, + scaled.objective_coefficients.data(), + scaled.objective_coefficients.data() + n_owned_var, + weighted_square_op{c_scaling_weight}, + f_t(0), + thrust::plus{}); + }); - // 2) NCCL allreduce SUM (both scalars at once) -> every shard holds the - // global squared norms. - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - CUOPT_NCCL_TRY(ncclAllReduce(sq[r].data(), - sq[r].data(), - 2, - nccl_data_type(), - ncclSum, - s.comm.get(), - s.stream.view().value())); - } - CUOPT_NCCL_TRY(ncclGroupEnd()); + // 3) Host-side derivation of the (identical on every shard) scaling scalars. + const f_t bound_rescaling = rescaling_from_squared_norm_op{}(global_bound_sq); + const f_t obj_rescaling = rescaling_from_squared_norm_op{}(global_obj_sq); - // 3) derive the identical scalars and apply on every shard. - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - f_t h_sq[2] = {f_t(0), f_t(0)}; - raft::copy(h_sq, sq[r].data(), 2, s.stream.view()); - s.stream.synchronize(); - const f_t bound_rescaling = f_t(1) / (std::sqrt(h_sq[0]) + f_t(1)); - const f_t objective_rescaling = f_t(1) / (std::sqrt(h_sq[1]) + f_t(1)); - s.sub_pdlp->get_initial_scaling_strategy().apply_distributed_bound_objective_rescaling( - bound_rescaling, objective_rescaling); - } - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - s.stream.synchronize(); - } + // 4) Publish + apply on every shard via the shared helpers. + engine.for_each_shard([&](auto& s) { + auto& scaling = s.sub_pdlp->get_initial_scaling_strategy(); + scaling.set_h_bound_rescaling(bound_rescaling); + scaling.set_h_objective_rescaling(obj_rescaling); + scaling.apply_bound_objective_rescaling_to_problem(); + }); + + engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } // -------- Distributed Ruiz inf-scaling ------------------------------------ diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 5ca6640b4d..a9ba1b3d10 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -31,17 +31,6 @@ namespace cuopt::mathematical_optimization::pdlp { -template -struct weighted_square_op { - f_t weight; - HDI f_t operator()(f_t v) { return v * v * weight; } -}; - -template -struct rescaling_from_squared_norm_op { - HDI f_t operator()(f_t sum) { return f_t(1.0) / (raft::sqrt(sum) + f_t(1.0)); } -}; - template struct inverse_rescaling_op { HDI f_t operator()(f_t v) @@ -187,6 +176,57 @@ void pdlp_initial_scaling_strategy_t::bound_objective_rescaling() h_objective_rescaling_ = cuopt::host_copy(objective_rescaling_, stream_view_); } +// Apply the already-published bound_rescaling_ / objective_rescaling_ device +// vectors to the scaled problem's constraint bounds, variable bounds, and +// objective. Extracted from scale_problem() so distributed_bound_objective_rescaling +// can share the exact same three multiplies (both call sites go through this +// helper — no duplicated multiply loops). +template +void pdlp_initial_scaling_strategy_t::apply_bound_objective_rescaling_to_problem() +{ + using f_t2 = typename type_2::type; + + cub::DeviceTransform::Transform( + cuda::std::make_tuple(op_problem_scaled_.constraint_lower_bounds.data(), + op_problem_scaled_.constraint_upper_bounds.data(), + batch_wrapped_container(bound_rescaling_, dual_size_h_)), + thrust::make_zip_iterator(op_problem_scaled_.constraint_lower_bounds.data(), + op_problem_scaled_.constraint_upper_bounds.data()), + op_problem_scaled_.constraint_upper_bounds.size(), + [] __device__(f_t constraint_lower_bound, + f_t constraint_upper_bound, + f_t bound_rescaling) -> thrust::tuple { + return {constraint_lower_bound * bound_rescaling, constraint_upper_bound * bound_rescaling}; + }, + stream_view_.value()); + + // In batch mode we don't scale the variable bounds (here) because they are shared across + // climbers. While the variable bounds are the same across climbers, there can be different + // bound rescaling factors for each climber. One solution would be to have per climber variable + // bounds but its costly from a memory perspective and from a memory bandwidth perspective. + // Since the variable bounds are the same across climbers but only the scaling factor changes, + // we pass the scaling factor to PDHG later. In PDHG we act the (almost fully) scaled variable + // bounds and add this missing scaling factor. + if (original_batch_size_ == 1) { + cub::DeviceTransform::Transform( + op_problem_scaled_.variable_bounds.data(), + op_problem_scaled_.variable_bounds.data(), + op_problem_scaled_.variable_bounds.size(), + [bound_rescaling = bound_rescaling_.data()] __device__(f_t2 variable_bounds) -> f_t2 { + return {variable_bounds.x * *bound_rescaling, variable_bounds.y * *bound_rescaling}; + }, + stream_view_); + } + + cub::DeviceTransform::Transform( + cuda::std::make_tuple(op_problem_scaled_.objective_coefficients.data(), + batch_wrapped_container(objective_rescaling_, primal_size_h_)), + op_problem_scaled_.objective_coefficients.data(), + op_problem_scaled_.objective_coefficients.size(), + cuda::std::multiplies{}, + stream_view_.value()); +} + // Row inf-norm of the scaled matrix, over the row-major matrix: each row is // reduced from its own nonzeros. (Owns the complete row in distributed PDLP.) template @@ -668,45 +708,7 @@ void pdlp_initial_scaling_strategy_t::scale_problem() print("objective_rescaling", objective_rescaling_); #endif - cub::DeviceTransform::Transform( - cuda::std::make_tuple(op_problem_scaled_.constraint_lower_bounds.data(), - op_problem_scaled_.constraint_upper_bounds.data(), - batch_wrapped_container(bound_rescaling_, dual_size_h_)), - thrust::make_zip_iterator(op_problem_scaled_.constraint_lower_bounds.data(), - op_problem_scaled_.constraint_upper_bounds.data()), - op_problem_scaled_.constraint_upper_bounds.size(), - [] __device__(f_t constraint_lower_bound, - f_t constraint_upper_bound, - f_t bound_rescaling) -> thrust::tuple { - return {constraint_lower_bound * bound_rescaling, constraint_upper_bound * bound_rescaling}; - }, - stream_view_.value()); - - // In batch mode we don't scale the variable bounds (here) because they are shared across - // climbers. While the variable bounds are the same across climbers, there can be different - // bound rescaling factors for each climber. One solution would be to have per climber variable - // bounds but its costly from a memory perspective and from a memory bandwidth perspective. - // Since the variable bounds are the same across climbers but only the scaling factor changes, - // we pass the scaling factor to PDHG later. In PDHG we act the (almost fully) scaled variable - // bounds and add this missing scaling factor. - if (original_batch_size_ == 1) { - cub::DeviceTransform::Transform( - op_problem_scaled_.variable_bounds.data(), - op_problem_scaled_.variable_bounds.data(), - op_problem_scaled_.variable_bounds.size(), - [bound_rescaling = bound_rescaling_.data()] __device__(f_t2 variable_bounds) -> f_t2 { - return {variable_bounds.x * *bound_rescaling, variable_bounds.y * *bound_rescaling}; - }, - stream_view_); - } - - cub::DeviceTransform::Transform( - cuda::std::make_tuple(op_problem_scaled_.objective_coefficients.data(), - batch_wrapped_container(objective_rescaling_, primal_size_h_)), - op_problem_scaled_.objective_coefficients.data(), - op_problem_scaled_.objective_coefficients.size(), - cuda::std::multiplies{}, - stream_view_.value()); + apply_bound_objective_rescaling_to_problem(); } #ifdef CUPDLP_DEBUG_MODE @@ -972,50 +974,6 @@ const mip::problem_t& pdlp_initial_scaling_strategy_t::get_s return op_problem_scaled_; } -template -void pdlp_initial_scaling_strategy_t::apply_distributed_bound_objective_rescaling( - f_t bound_rescaling, f_t objective_rescaling) -{ - using f_t2 = typename type_2::type; - - // constraint bounds *= bound_rescaling (matches scale_problem() bound block) - cub::DeviceTransform::Transform( - cuda::std::make_tuple(op_problem_scaled_.constraint_lower_bounds.data(), - op_problem_scaled_.constraint_upper_bounds.data()), - thrust::make_zip_iterator(op_problem_scaled_.constraint_lower_bounds.data(), - op_problem_scaled_.constraint_upper_bounds.data()), - op_problem_scaled_.constraint_upper_bounds.size(), - [bound_rescaling] __device__(f_t lower, f_t upper) -> thrust::tuple { - return {lower * bound_rescaling, upper * bound_rescaling}; - }, - stream_view_.value()); - - // variable bounds *= bound_rescaling (batch-1 path only; distributed is batch 1) - cub::DeviceTransform::Transform( - op_problem_scaled_.variable_bounds.data(), - op_problem_scaled_.variable_bounds.data(), - op_problem_scaled_.variable_bounds.size(), - [bound_rescaling] __device__(f_t2 variable_bounds) -> f_t2 { - return {variable_bounds.x * bound_rescaling, variable_bounds.y * bound_rescaling}; - }, - stream_view_); - - // objective *= objective_rescaling - cub::DeviceTransform::Transform( - op_problem_scaled_.objective_coefficients.data(), - op_problem_scaled_.objective_coefficients.data(), - op_problem_scaled_.objective_coefficients.size(), - [objective_rescaling] __device__(f_t c) -> f_t { return c * objective_rescaling; }, - stream_view_); - - // Store the factors (sets both host copies and the device rescaling vectors) - // so unscale_solutions() / scale_solutions() apply them consistently. The flag - // hyper_params_.bound_objective_rescaling stays true on shards so those paths - // are active; only scale_problem()'s local recompute is skipped. - set_h_bound_rescaling(bound_rescaling); - set_h_objective_rescaling(objective_rescaling); -} - template const rmm::device_uvector& pdlp_initial_scaling_strategy_t::get_constraint_matrix_scaling_vector() const diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 88c149f5b7..270983104b 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -105,9 +105,12 @@ class pdlp_initial_scaling_strategy_t { void bound_objective_rescaling(); - // Distributed PDLP: apply an externally-computed GLOBAL bound / objective - // rescaling to the already-scaled problem. - void apply_distributed_bound_objective_rescaling(f_t bound_rescaling, f_t objective_rescaling); + // Apply the already-populated bound_rescaling_ / objective_rescaling_ + // device vectors to op_problem_scaled_ (constraint bounds, variable bounds, + // objective). Extracted from scale_problem() into a shared helper so + // distributed PDLP can apply its globally-reduced scalars via the same + // three multiplies. + void apply_bound_objective_rescaling_to_problem(); // Distributed PDLP: skip the LOCAL bound/objective rescaling inside // scale_problem() diff --git a/cpp/src/pdlp/utils.cuh b/cpp/src/pdlp/utils.cuh index adc1432b39..2f5fa89edc 100644 --- a/cpp/src/pdlp/utils.cuh +++ b/cpp/src/pdlp/utils.cuh @@ -264,6 +264,21 @@ struct rhs_sum_of_squares_t { } }; +// Per-element weighted square (used to compute the L2 norm of the weighted +// objective coefficients). +template +struct weighted_square_op { + f_t weight; + HDI f_t operator()(f_t v) const { return v * v * weight; } +}; + +// Convert a squared L2 norm to its bound/objective rescaling scalar, +// 1 / (sqrt(sum) + 1). Uses raft::sqrt which is __host__ __device__. +template +struct rescaling_from_squared_norm_op { + HDI f_t operator()(f_t sum) const { return f_t(1.0) / (raft::sqrt(sum) + f_t(1.0)); } +}; + template void inline combine_constraint_bounds(const mip::problem_t& op_problem, rmm::device_uvector& combined_bounds) From 96f2c3a918f048a398e81983c84dc842e431ae96 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 4 Jul 2026 12:59:26 +0200 Subject: [PATCH 153/258] style --- cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 3ce604f58d..78171277cf 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -124,7 +124,8 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng // -------- Distributed bound / objective rescaling ------------------------- // + apply_bound_objective_rescaling_to_problem, unfused because we need a -// raw squared-sum on device to hand to NCCL AllReduce and the base version comptues tranform->reduce->transform in one cub call for efficiency +// raw squared-sum on device to hand to NCCL AllReduce and the base version comptues +// tranform->reduce->transform in one cub call for efficiency template void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, f_t c_scaling_weight) From af6c4e3550572783f284c5fbf60358bc073190c2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 4 Jul 2026 23:08:56 +0200 Subject: [PATCH 154/258] updated a comment and added small fix to presolver->apply so it compiles --- cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu | 2 +- cpp/tests/mip/presolve_test.cu | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 78171277cf..1460493be2 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -123,7 +123,7 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng } // -------- Distributed bound / objective rescaling ------------------------- -// + apply_bound_objective_rescaling_to_problem, unfused because we need a +// compute and apply_bound_objective_rescaling_to_problem, unfused because we need a // raw squared-sum on device to hand to NCCL AllReduce and the base version comptues // tranform->reduce->transform in one cub call for efficiency template diff --git a/cpp/tests/mip/presolve_test.cu b/cpp/tests/mip/presolve_test.cu index e189acf878..7067479093 100644 --- a/cpp/tests/mip/presolve_test.cu +++ b/cpp/tests/mip/presolve_test.cu @@ -145,7 +145,7 @@ TEST(gf2_presolve, uses_compact_constraint_indices) problem.set_constraint_upper_bounds(constraint_ub.data(), constraint_ub.size()); auto presolver = std::make_unique>(); - auto result = presolver->apply( + auto result = presolver->apply_presolve_from_op_problem( problem, problem_category_t::MIP, presolver_t::Papilo, false, 1e-6, 1e-12, 20, 1); EXPECT_EQ(result.status, mip::third_party_presolve_status_t::REDUCED); From b137d1da686dbab5dcf63105a4b3f7e8d411160a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 00:06:11 +0200 Subject: [PATCH 155/258] concentrated scaling --- .../distributed_algorithms.cu | 50 +++++++++++++++++++ .../distributed_algorithms.hpp | 17 +++++++ .../initial_scaling.cu | 41 +++++++++------ .../initial_scaling.cuh | 20 ++++---- cpp/src/pdlp/pdlp.cu | 49 +++--------------- 5 files changed, 111 insertions(+), 66 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 1460493be2..6e1759e513 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -234,6 +234,52 @@ void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } +// -------- Distributed scaling orchestration ------------------------------ +// Mirrors what scale_problem() does in single-GPU by composing the +// individual distributed passes. See the header for the full pipeline. +template +void distributed_scaling(multi_gpu_engine_t& engine, + pdlp_hyper_params_t const& hyper_params, + i_t n_global_vars, + bool inside_mip) +{ + raft::common::nvtx::range scope("distributed_scaling"); + + // 1) Reset per-shard scaling state (cumulative row/col scalings back to 1), + // then sync so subsequent scaling passes start from a clean slate. + engine.for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().reset_scaling_state_for_distributed(); + }); + engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + + // 2) Matrix scaling passes populate the cumulative row/col scalings on + // every shard. Each pass keeps the halo copies refreshed internally. + if (hyper_params.do_ruiz_scaling) { + distributed_ruiz_inf_scaling( + engine, hyper_params.default_l_inf_ruiz_iterations, n_global_vars); + } + if (hyper_params.do_pock_chambolle_scaling) { + distributed_pock_chambolle_scaling( + engine, + static_cast(hyper_params.default_alpha_pock_chambolle_rescaling), + n_global_vars); + } + + // 3) Per-shard apply of the accumulated scaling to A, c, variable and + // constraint bounds. This is scale_problem() minus its local + // bound/objective rescaling; the equivalent global step happens in (4). + engine.for_each_shard([](auto& shard) { + shard.sub_pdlp->get_initial_scaling_strategy().apply_cummulative_scaling_to_problem(); + }); + engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + + // 4) Global bound/objective rescaling (all shards get the identical scalar). + if (hyper_params.bound_objective_rescaling) { + distributed_bound_objective_rescaling( + engine, static_cast(hyper_params.initial_primal_weight_c_scaling)); + } +} + // -------- Distributed sigma_max(A) via power iteration -------------------- // The function has to re-implement the multi_gpu_engine_t preimitives as the scratch buffers // are not associated with shards. @@ -582,6 +628,10 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, multi_gpu_engine_t & engine, int num_iter, int n_global_vars); \ template void distributed_pock_chambolle_scaling( \ multi_gpu_engine_t & engine, F_TYPE alpha, int n_global_vars); \ + template void distributed_scaling(multi_gpu_engine_t & engine, \ + pdlp_hyper_params_t const& hyper_params, \ + int n_global_vars, \ + bool inside_mip); \ template F_TYPE distributed_max_singular_value( \ multi_gpu_engine_t & engine, \ int n_global_cstrs, \ diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp index 7578acbe85..532db6bd8e 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp @@ -4,6 +4,8 @@ */ #pragma once +#include + #include // Algorithm-level distributed PDLP @@ -49,6 +51,21 @@ void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, f_t alpha, i_t n_global_vars); +// Full distributed scaling entry point. Mirrors what scale_problem() does in +// single-GPU by orchestrating: +// - reset per-shard scaling state +// - Ruiz inf-scaling -> populates cumulative row/col scalings +// - Pock-Chambolle scaling -> same +// - per-shard apply_cummulative_scaling_to_problem() to apply the cumulative +// scalings to A, c, variable and constraint bounds (this is scale_problem() +// minus its shard-local bound/objective rescaling) +// - global bound/objective rescaling via distributed_bound_objective_rescaling +template +void distributed_scaling(multi_gpu_engine_t& engine, + pdlp_hyper_params_t const& hyper_params, + i_t n_global_vars, + bool inside_mip); + // Distributed sigma_max(A) via power iteration (used to seed the initial // step size). Returns the largest singular value of the scaled constraint // matrix; identical on every shard. diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index a9ba1b3d10..1f2d77d8fd 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -626,9 +626,9 @@ void pdlp_initial_scaling_strategy_t::resize_context(i_t new_size) } template -void pdlp_initial_scaling_strategy_t::scale_problem() +void pdlp_initial_scaling_strategy_t::apply_cummulative_scaling_to_problem() { - raft::common::nvtx::range fun_scope("scale_problem"); + raft::common::nvtx::range fun_scope("apply_cummulative_scaling_to_problem"); // scale A i_t number_of_blocks = op_problem_scaled_.n_constraints / block_size; @@ -698,19 +698,6 @@ void pdlp_initial_scaling_strategy_t::scale_problem() cuda::std::multiplies{}, stream_view_); - if (hyper_params_.bound_objective_rescaling && !running_mip_ && - !skip_distributed_local_rescaling_) { - // Coefficients are computed on the already scaled values - bound_objective_rescaling(); - -#ifdef CUPDLP_DEBUG_MODE - print("bound_rescaling", bound_rescaling_); - print("objective_rescaling", objective_rescaling_); -#endif - - apply_bound_objective_rescaling_to_problem(); - } - #ifdef CUPDLP_DEBUG_MODE print("constraint_lower_bound", op_problem_scaled_.constraint_lower_bounds); print("constraint_upper_bound", op_problem_scaled_.constraint_upper_bounds); @@ -736,6 +723,30 @@ void pdlp_initial_scaling_strategy_t::scale_problem() } } +template +void pdlp_initial_scaling_strategy_t::scale_problem() +{ + raft::common::nvtx::range fun_scope("scale_problem"); + + apply_cummulative_scaling_to_problem(); + + // Local bound/objective rescaling. Distributed PDLP intentionally does NOT + // reach this code path - it calls apply_cummulative_scaling_to_problem() + // directly and then applies the GLOBAL (allreduced) bound/objective factors + // via distributed_bound_objective_rescaling() instead. + if (hyper_params_.bound_objective_rescaling && !running_mip_) { + // Coefficients are computed on the already scaled values + bound_objective_rescaling(); + +#ifdef CUPDLP_DEBUG_MODE + print("bound_rescaling", bound_rescaling_); + print("objective_rescaling", objective_rescaling_); +#endif + + apply_bound_objective_rescaling_to_problem(); + } +} + template void pdlp_initial_scaling_strategy_t::scale_solutions( rmm::device_uvector& primal_solution, diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 270983104b..7f7bf3c139 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -112,18 +112,21 @@ class pdlp_initial_scaling_strategy_t { // three multiplies. void apply_bound_objective_rescaling_to_problem(); - // Distributed PDLP: skip the LOCAL bound/objective rescaling inside - // scale_problem() - void set_skip_distributed_local_rescaling(bool value) - { - skip_distributed_local_rescaling_ = value; - } - // Public for distributed PDLP void compute_scaling_vectors(i_t number_of_ruiz_iterations, f_t alpha); // ----- Distributed-PDLP hooks ----- + // Apply the cumulative row/column scalings that Ruiz/Pock-Chambolle + // accumulated to A, A_T, c, variable bounds and constraint bounds, mark + // the problem as scaled and scale the seed primal/dual solutions. + // scale_problem() = apply_cummulative_scaling_to_problem() + local + // bound/objective rescaling. Distributed PDLP calls this directly and + // then invokes distributed_bound_objective_rescaling() to apply the + // GLOBAL (allreduced) bound/objective factors instead of the shard-local + // ones. + void apply_cummulative_scaling_to_problem(); + // One Ruiz iteration (compute iteration vectors + fold into // cumulative). Exposed for distributed PDLP so the outer loop with halo // broadcasts lives at the distributed level @@ -170,8 +173,5 @@ class pdlp_initial_scaling_strategy_t { rmm::device_uvector& A_T_indices_; const pdlp::pdlp_hyper_params_t& hyper_params_; bool running_mip_; - // Distributed PDLP: when true, scale_problem() skips its local - // bound/objective rescaling (the global factor is applied separately). - bool skip_distributed_local_rescaling_{false}; }; } // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index dce932f2be..1cd094788e 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -560,49 +560,16 @@ pdlp_solver_t::pdlp_solver_t( multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), mps, sub_pdlp_settings); // ----- 8 Distributed Scaling ----- - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->sub_pdlp->get_initial_scaling_strategy().reset_scaling_state_for_distributed(); - } - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->stream.synchronize(); - } + // Full distributed scaling pipeline (reset state + Ruiz + Pock-Chambolle + + // per-shard scale_problem with the local bound/obj step suppressed + global + // bound/objective rescaling). + distributed_scaling(*multi_gpu_engine, settings_.hyper_params, n_vars, inside_mip_); - // Distributed scaling. Each pass keeps the halo copies of both cumulative - // scalings refreshed internally (owner -> halo broadcast) - if (settings_.hyper_params.do_ruiz_scaling) { - distributed_ruiz_inf_scaling( - *multi_gpu_engine, settings_.hyper_params.default_l_inf_ruiz_iterations, n_vars); - } - if (settings_.hyper_params.do_pock_chambolle_scaling) { - distributed_pock_chambolle_scaling( - *multi_gpu_engine, - static_cast(settings_.hyper_params.default_alpha_pock_chambolle_rescaling), - n_vars); - } - - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& scaling = shard->sub_pdlp->get_initial_scaling_strategy(); - // Skip the per-shard local bound/objective rescaling; the global factor is - // applied below. Keeps the unscale path active (flag stays true). - scaling.set_skip_distributed_local_rescaling(true); - scaling.scale_problem(); - - shard->sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( + multi_gpu_engine->for_each_shard([&](auto& shard) { + shard.sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( /*is_reflected=*/settings_.hyper_params.use_reflected_primal_dual); - } - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->stream.synchronize(); - } - - // Global bound/objective rescaling: allreduce the owned partial squared-norms - if (settings_.hyper_params.bound_objective_rescaling && !inside_mip_) { - distributed_bound_objective_rescaling( - *multi_gpu_engine, static_cast(settings_.hyper_params.initial_primal_weight_c_scaling)); - } + }); + multi_gpu_engine->for_each_shard([](auto& shard) { shard.stream.synchronize(); }); // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- constexpr f_t kStepSizeScale = f_t{0.998}; From bf05a21ee002c266ec874c4e056c5785dee0f3c3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 00:14:55 +0200 Subject: [PATCH 156/258] style --- cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 6e1759e513..a37297f50c 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -255,14 +255,11 @@ void distributed_scaling(multi_gpu_engine_t& engine, // 2) Matrix scaling passes populate the cumulative row/col scalings on // every shard. Each pass keeps the halo copies refreshed internally. if (hyper_params.do_ruiz_scaling) { - distributed_ruiz_inf_scaling( - engine, hyper_params.default_l_inf_ruiz_iterations, n_global_vars); + distributed_ruiz_inf_scaling(engine, hyper_params.default_l_inf_ruiz_iterations, n_global_vars); } if (hyper_params.do_pock_chambolle_scaling) { distributed_pock_chambolle_scaling( - engine, - static_cast(hyper_params.default_alpha_pock_chambolle_rescaling), - n_global_vars); + engine, static_cast(hyper_params.default_alpha_pock_chambolle_rescaling), n_global_vars); } // 3) Per-shard apply of the accumulated scaling to A, c, variable and From 7e33f2f666e6fc2833a3ccee8be2ff0a28de6423 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 13:22:35 +0200 Subject: [PATCH 157/258] multi_gpu_engine primitives are now based on vectors of raft::device_span. also made max singular value a bit better --- .../distributed_algorithms.cu | 226 +++---------- .../distributed_pdlp/multi_gpu_engine.hpp | 315 +++++++++++++----- 2 files changed, 276 insertions(+), 265 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index a37297f50c..9e98d49d2f 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -278,8 +278,10 @@ void distributed_scaling(multi_gpu_engine_t& engine, } // -------- Distributed sigma_max(A) via power iteration -------------------- -// The function has to re-implement the multi_gpu_engine_t preimitives as the scratch buffers -// are not associated with shards. +// Owns per-shard scratch (q / z / atq / scalar reductions) and drives the +// iteration; every cross-shard operation goes through multi_gpu_engine_t's +// *_bufs helpers (halo_exchange_{cstr,var}_bufs, distributed_l2_norm_bufs, +// distributed_dot_bufs), so this function contains no NCCL calls directly. template f_t distributed_max_singular_value(multi_gpu_engine_t& engine, i_t n_global_cstrs, @@ -363,177 +365,42 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, s.stream.synchronize(); } - // Local halo-exchange helpers that work directly on per-shard external - // buffers (the engine's halo_exchange_var/cstr expect accessors that - // resolve through pdhg_solver_t, which doesn't see our scratch). - auto halo_exchange_cstr_bufs = [&](std::vector>& bufs) { - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - auto& y = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - if (s.cstr_send_indices_d[peer].size() == 0) continue; - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.cstr_send_indices_d[peer].begin(), - s.cstr_send_indices_d[peer].end(), - y.begin(), - s.cstr_send_buf_d[peer].begin()); - } - } - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - CUOPT_NCCL_TRY(ncclSend(s.cstr_send_buf_d[peer].data(), - s.cstr_send_buf_d[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - auto& rd = s.rank_data; - raft::device_setter guard(s.device_id); - auto& y = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; - CUOPT_NCCL_TRY(ncclRecv(recv_ptr, - static_cast(rd.cstr_recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - CUOPT_NCCL_TRY(ncclGroupEnd()); - }; - auto halo_exchange_var_bufs = [&](std::vector>& bufs) { - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - auto& x = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - if (s.var_send_indices_d[peer].size() == 0) continue; - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.var_send_indices_d[peer].begin(), - s.var_send_indices_d[peer].end(), - x.begin(), - s.var_send_buf_d[peer].begin()); - } - } - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - CUOPT_NCCL_TRY(ncclSend(s.var_send_buf_d[peer].data(), - s.var_send_buf_d[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - auto& rd = s.rank_data; - raft::device_setter guard(s.device_id); - auto& x = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; - CUOPT_NCCL_TRY(ncclRecv(recv_ptr, - static_cast(rd.var_recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - CUOPT_NCCL_TRY(ncclGroupEnd()); - }; - - // Per-shard partial reductions over the OWNED cstr slice + NCCL allreduce. - // For norm: out := sqrt(Σ_r ||bufs[r][0:owned_cstr]||²). - // For dot : out := Σ_r . - auto distributed_norm_owned_cstr = [&](std::vector>& bufs, - std::vector>& out) { - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - const i_t n_owned = s.rank_data.owned_cstr_size; - RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), - static_cast(n_owned), - bufs[r].data(), - 1, - bufs[r].data(), - 1, - out[r].data(), - s.stream.view().value())); - } - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - CUOPT_NCCL_TRY(ncclAllReduce(out[r].data(), - out[r].data(), - 1, - nccl_data_type(), - ncclSum, - s.comm.get(), - s.stream.view().value())); - } - CUOPT_NCCL_TRY(ncclGroupEnd()); - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - cub::DeviceTransform::Transform( - out[r].data(), out[r].data(), 1, sqrt_inplace_op_t{}, s.stream.view().value()); - } - }; - auto distributed_dot_owned_cstr = [&](std::vector>& a, - std::vector>& b, - std::vector>& out) { - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - const i_t n_owned = s.rank_data.owned_cstr_size; - RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), - static_cast(n_owned), - a[r].data(), - 1, - b[r].data(), - 1, - out[r].data(), - s.stream.view().value())); - } - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; - raft::device_setter guard(s.device_id); - CUOPT_NCCL_TRY(ncclAllReduce(out[r].data(), - out[r].data(), - 1, - nccl_data_type(), - ncclSum, - s.comm.get(), - s.stream.view().value())); - } - CUOPT_NCCL_TRY(ncclGroupEnd()); - }; + // Build the per-shard views used by the engine's *_bufs helpers. + // Built ONCE: rmm::device_uvector::data() is stable for the object's + // lifetime (no reallocation happens inside the loop). + // *_full : span over owned + halo tail (halo_exchange_{cstr,var}_bufs) + // *_owned : span over just the owned prefix (distributed_{l2_norm,dot}_bufs) + // *_scalar : device_scalar_view over a per-shard scalar output slot + std::vector> q_full, atq_full; + std::vector> q_owned, z_owned; + std::vector> norm_q_scalar, sigma_sq_scalar, residual_scalar; + q_full.reserve(nb); + atq_full.reserve(nb); + q_owned.reserve(nb); + z_owned.reserve(nb); + norm_q_scalar.reserve(nb); + sigma_sq_scalar.reserve(nb); + residual_scalar.reserve(nb); + for (int r = 0; r < nb; ++r) { + auto& s = *engine.shards[r]; + const std::size_t owned = static_cast(s.rank_data.owned_cstr_size); + q_full.emplace_back(q[r].data(), q[r].size()); + atq_full.emplace_back(atq[r].data(), atq[r].size()); + q_owned.emplace_back(q[r].data(), owned); + z_owned.emplace_back(z[r].data(), owned); + norm_q_scalar.emplace_back(raft::make_device_scalar_view(norm_q[r].data())); + sigma_sq_scalar.emplace_back(raft::make_device_scalar_view(sigma_sq[r].data())); + residual_scalar.emplace_back(raft::make_device_scalar_view(residual_norm[r].data())); + } // ===== Power iteration ===== - // Mirrors single-GPU compute_initial_step_size + // Mirrors single-GPU compute_initial_step_size. All cross-shard math and + // NCCL comms are delegated to multi_gpu_engine_t's *_bufs helpers; the + // only inline work is (a) the two elementwise transforms whose functor + // captures each shard's own scalar (norm_q[r], sigma_sq[r]) and (b) the + // per-shard SpMVs that call pdhg_solver_'s A_into / A_T_into. for (int it = 0; it < max_iterations; ++it) { - // q := z on the owned slice (the carried iterate), then normalize. + // q := z on the owned slice (the carried iterate). for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); @@ -542,9 +409,11 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, } // ||q||₂ over the global OWNED cstr slice (one allreduce-sum + sqrt). - distributed_norm_owned_cstr(q, norm_q); + engine.distributed_l2_norm_bufs(q_owned, norm_q_scalar); // q /= ||q||₂ on owned slice (halo gets refreshed by next exchange). + // Kept inline: the divisor differs per shard (each shard reads its own + // norm_q[r]) so a single shared functor won't do. for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); @@ -557,18 +426,16 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, s.stream.view().value()); } - // atq = A^T q : halo-exchange q, then per-shard SpMV. spmv_At_into - // rebinds the dual_solution dnvec to q[r].data() and restores the - // canonical binding after the call - halo_exchange_cstr_bufs(q); + // atq = A^T q : refresh halo of q, then per-shard SpMV. + engine.halo_exchange_cstr_bufs(q_full); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); s.sub_pdlp->pdhg_solver_.spmv_At_into(q[r], atq_dn[r]); } - // z = A atq : halo-exchange atq, then per-shard SpMV. - halo_exchange_var_bufs(atq); + // z = A atq : refresh halo of atq, then per-shard SpMV. + engine.halo_exchange_var_bufs(atq_full); for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); @@ -577,9 +444,10 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, // σ² = q · z over the global OWNED cstr slice (= q^T A A^T q = σ_max² // when q is the dominant left-singular vector). - distributed_dot_owned_cstr(q, z, sigma_sq); + engine.distributed_dot_bufs(q_owned, z_owned, sigma_sq_scalar); // q := -σ² q + z (owned slice) — residual of the eigen-equation. + // Kept inline for the same per-shard-scalar reason as normalize above. for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); @@ -593,7 +461,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, } // Convergence check via global residual norm. - distributed_norm_owned_cstr(q, residual_norm); + engine.distributed_l2_norm_bufs(q_owned, residual_scalar); auto& s0 = *engine.shards[0]; raft::device_setter guard0(s0.device_id); f_t h_res{}; diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 0cdad94aa1..f716fae54f 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -16,7 +16,9 @@ #include #include +#include #include +#include #include #include @@ -86,20 +88,72 @@ struct multi_gpu_engine_t { } } + // Core: launches cub::DeviceTransform on every shard using per-shard + // pre-resolved inputs / outputs / sizes. + // - in_tuples[r] is the tuple passed as cub input for shard r (any + // iterator-shaped types cub accepts: raw pointers, thrust iterators, ...) + // - outs[r] is the output iterator for shard r + // - sizes[r] is the element count for shard r + // All three must have size == shards.size(). The heterogeneous per-shard + // input types are captured by the caller into whatever container / element + // type is convenient (e.g. std::vector>). + template + void distributed_transform_bufs(std::vector const& in_tuples, + std::vector const& outs, + std::vector const& sizes, + Op op) + { + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(in_tuples.size()) == nb && + static_cast(outs.size()) == nb && + static_cast(sizes.size()) == nb, + error_type_t::RuntimeError, + "distributed_transform_bufs: in_tuples / outs / sizes must " + "all have size == shards.size()"); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + cub::DeviceTransform::Transform(in_tuples[r], outs[r], sizes[r], op, s.stream.view()); + } + } + + // Wrapper: accessor form. Resolves each shard's cub input_tuple / output / + // size via the provided accessors, then delegates to + // distributed_transform_bufs. template void distributed_transform(std::tuple in_accessors, OutAccess out, SizeAccess sz, Op op) { - for_each_shard([&](auto& shard) { - auto& sub = *shard.sub_pdlp; - // turns the Tuple of lambdas into a tuple of rmm::device_uvector - auto cub_inputs = std::apply( - [&sub](auto&... acc) { return cuda::std::make_tuple(acc(sub)...); }, in_accessors); - - cub::DeviceTransform::Transform(cub_inputs, out(sub), sz(sub), op, shard.stream.view()); - }); + cuopt_expects(!shards.empty(), + error_type_t::RuntimeError, + "distributed_transform: engine has no shards"); + + // Deduce per-shard tuple / output types from the accessors themselves so + // the runtime vector element types match cub's expectations exactly. + auto& sample_sub = *shards[0]->sub_pdlp; + using in_tuple_t = decltype(std::apply( + [&sample_sub](auto&... acc) { return cuda::std::make_tuple(acc(sample_sub)...); }, + in_accessors)); + using out_iter_t = decltype(out(sample_sub)); + + std::vector in_tuples; + std::vector outs; + std::vector sizes; + in_tuples.reserve(shards.size()); + outs.reserve(shards.size()); + sizes.reserve(shards.size()); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + auto& sub = *s->sub_pdlp; + // Turns a tuple of accessors into a tuple of values. + in_tuples.emplace_back(std::apply( + [&sub](auto&... acc) { return cuda::std::make_tuple(acc(sub)...); }, in_accessors)); + outs.emplace_back(out(sub)); + sizes.emplace_back(sz(sub)); + } + distributed_transform_bufs(in_tuples, outs, sizes, op); } // --- 2) convenience: single input accessor (delegates) --- @@ -111,39 +165,28 @@ struct multi_gpu_engine_t { } // -------- Halo exchange (variables / x) --------------------------------- - // Fills the halo slice [owned_var_size, total_var_size) of the per-shard - // input buffer returned by `buf_access(pdhg)` (the buffer A @ x will read). - template - void halo_exchange_var(BufAccess&& buf_access) - { - halo_exchange_var_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { - return buf_access(s.sub_pdlp->pdhg_solver_); - }); - } - - // Core variable halo exchange. ShardBufAccess maps a shard to the var-shaped - // device buffer to synchronize (owned slice [0, owned_var_size) followed by - // the per-peer halo tail). // Step 1: thrust::gather per-peer outgoing values into staging buffers. // Step 2: a single NCCL group with matched ncclSend / ncclRecv across all // (rank, peer) pairs, receiving into each shard's halo region. - template - void halo_exchange_var_shard(ShardBufAccess&& buf_access) + void halo_exchange_var_bufs(std::vector> const& bufs) { const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(bufs.size()) == nb, + error_type_t::RuntimeError, + "halo_exchange_var_bufs: bufs.size() must equal shards.size()"); // Step 1: gather owned values that each peer needs into per-peer staging. for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto& x = buf_access(s); + auto x = bufs[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; if (s.var_send_indices_d[peer].size() == 0) continue; thrust::gather(rmm::exec_policy_nosync(s.stream.view()), s.var_send_indices_d[peer].begin(), s.var_send_indices_d[peer].end(), - x.begin(), + x.data(), s.var_send_buf_d[peer].begin()); } } @@ -167,7 +210,7 @@ struct multi_gpu_engine_t { auto& s = *shards[r]; auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& x = buf_access(s); + auto x = bufs[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; @@ -182,36 +225,56 @@ struct multi_gpu_engine_t { CUOPT_NCCL_TRY(ncclGroupEnd()); } - // -------- Halo exchange (constraints / y) ------------------------------- - // Same as halo_exchange_var but for a constraint-shaped buffer (the input - // A_T @ y will read) and constraint halos. buf_access maps a - // pdhg_solver_t to the cstr-shaped buffer to exchange. + // Wrapper: pdhg_solver_t accessor. + // buf_access : pdhg_solver_t& -> rmm::device_uvector& template - void halo_exchange_cstr(BufAccess&& buf_access) + void halo_exchange_var(BufAccess&& buf_access) { - halo_exchange_cstr_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { + halo_exchange_var_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { return buf_access(s.sub_pdlp->pdhg_solver_); }); } - // Same as halo_exchange_var_shard for cstr + // Wrapper: pdlp_shard_t accessor. Resolves one uvector per shard into a + // vector of spans, then delegates to halo_exchange_var_bufs. + // buf_access : pdlp_shard_t& -> rmm::device_uvector& template - void halo_exchange_cstr_shard(ShardBufAccess&& buf_access) + void halo_exchange_var_shard(ShardBufAccess&& buf_access) + { + std::vector> bufs; + bufs.reserve(shards.size()); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + auto& x = buf_access(*s); + bufs.emplace_back(x.data(), x.size()); + } + halo_exchange_var_bufs(bufs); + } + + // -------- Halo exchange (constraints / y) ------------------------------- + // Cstr-halo counterpart of halo_exchange_var_bufs. Same structure: contains + // all the gather + NCCL send/recv logic; accessor overloads below are thin + // wrappers that resolve one buffer per shard and delegate. + // Requirements: bufs.size() == shards.size(); bufs[r] is shard r's owned + + // halo tail (contiguous) with total_cstr_size elements. + void halo_exchange_cstr_bufs(std::vector> const& bufs) { const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(bufs.size()) == nb, + error_type_t::RuntimeError, + "halo_exchange_cstr_bufs: bufs.size() must equal shards.size()"); - // Gather each owner's owned values that peers need. for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto& y = buf_access(s); + auto y = bufs[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; if (s.cstr_send_indices_d[peer].size() == 0) continue; thrust::gather(rmm::exec_policy_nosync(s.stream.view()), s.cstr_send_indices_d[peer].begin(), s.cstr_send_indices_d[peer].end(), - y.begin(), + y.data(), s.cstr_send_buf_d[peer].begin()); } } @@ -234,7 +297,7 @@ struct multi_gpu_engine_t { auto& s = *shards[r]; auto& rd = s.rank_data; raft::device_setter guard(s.device_id); - auto& y = buf_access(s); + auto y = bufs[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; @@ -249,67 +312,147 @@ struct multi_gpu_engine_t { CUOPT_NCCL_TRY(ncclGroupEnd()); } + // Wrapper: pdhg_solver_t accessor. + // buf_access : pdhg_solver_t& -> rmm::device_uvector& + template + void halo_exchange_cstr(BufAccess&& buf_access) + { + halo_exchange_cstr_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { + return buf_access(s.sub_pdlp->pdhg_solver_); + }); + } + + // Wrapper: pdlp_shard_t accessor. Resolves one uvector per shard into a + // vector of spans, then delegates to halo_exchange_cstr_bufs. + // buf_access : pdlp_shard_t& -> rmm::device_uvector& + template + void halo_exchange_cstr_shard(ShardBufAccess&& buf_access) + { + std::vector> bufs; + bufs.reserve(shards.size()); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + auto& y = buf_access(*s); + bufs.emplace_back(y.data(), y.size()); + } + halo_exchange_cstr_bufs(bufs); + } + // -------- NCCL allreduce (sum, in place) -------------------------------- - // Per-shard in-place sum-allreduce. Each shard's stream issues an - // ncclAllReduce(buf, buf, count, nccl_data_type(), ncclSum, ...) inside a single - // group. After this returns, every shard's buffer holds the global sum. - // - // PtrAccess: pdlp_solver_t& -> f_t* (e.g. into step_size_strategy_). - template - void allreduce_sum_inplace(PtrAccess&& ptr_access, size_t count = 1) + // Core: per-shard in-place sum-allreduce on a single f_t scalar viewed by + // scalars[r], wrapped in one NCCL group so it executes as a single + // collective. After this returns, every shard's scalar holds the global sum. + void allreduce_sum_inplace_bufs(std::vector> const& scalars) { + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(scalars.size()) == nb, + error_type_t::RuntimeError, + "allreduce_sum_inplace_bufs: scalars.size() must equal shards.size()"); + if (nb == 0) return; + CUOPT_NCCL_TRY(ncclGroupStart()); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + f_t* p = scalars[r].data_handle(); + CUOPT_NCCL_TRY(ncclAllReduce(p, + p, + /*count=*/1, + nccl_data_type(), + ncclSum, + s.comm.get(), + s.stream.view().value())); + } + CUOPT_NCCL_TRY(ncclGroupEnd()); + } + + // Wrapper: pdlp_solver_t accessor for a single per-shard scalar. + // ptr_access : pdlp_solver_t& -> f_t* (pointer to the scalar + // to reduce; one per shard) + template + void allreduce_sum_inplace(PtrAccess&& ptr_access) + { + std::vector> scalars; + scalars.reserve(shards.size()); for (auto& s : shards) { raft::device_setter guard(s->device_id); - f_t* buf = ptr_access(*s->sub_pdlp); - CUOPT_NCCL_TRY(ncclAllReduce( - buf, buf, count, nccl_data_type(), ncclSum, s->comm.get(), s->stream.view().value())); + scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s->sub_pdlp))); } - CUOPT_NCCL_TRY(ncclGroupEnd()); + allreduce_sum_inplace_bufs(scalars); } - // -------- Distributed L2 norm ------------------------------------------ - // Computes sqrt(Σ_k Σ_{i ∈ owned_k} buf_k[i]²) and writes the scalar into - // the buffer returned by `out_access` on EVERY shard. - // - // Algorithm: - // 1) per shard: out = cublasdot(buf[0:n_owned], buf[0:n_owned]) (partial Σ²) - // 2) NCCL allreduce SUM on out (count = 1) (global Σ²) - // 3) per shard: out = sqrt(out) - // - // The caller is responsible for clipping correctness via `size_access` - // (which picks `rank_data.owned_var_size` or `rank_data.owned_cstr_size` - // depending on the shape of the input buffer), and for mirroring the - // result back to master if downstream code needs it there. - // - // BufAccess : pdlp_solver_t& -> rmm::device_uvector& - // OutAccess : pdlp_solver_t& -> f_t* (single scalar in shard memory) - // SizeAccess : pdlp_shard_t& -> i_t (owned slice length) - template - void distributed_l2_norm(BufAccess&& buf_access, OutAccess&& out_access, SizeAccess&& size_access) + // -------- Distributed dot / L2 norm ------------------------------------- + // Computes the dot product of two vectors for each shard. Returns the global result in out_scalars. + void distributed_dot_bufs(std::vector> const& a_bufs, + std::vector> const& b_bufs, + std::vector> const& out_scalars) { - for_each_shard([&](auto& shard) { - auto& sub = *shard.sub_pdlp; - auto& buf = buf_access(sub); - const i_t n = size_access(shard); - f_t* out = out_access(sub); - RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(shard.handle.get_cublas_handle(), - static_cast(n), - buf.data(), + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(a_bufs.size()) == nb && + static_cast(b_bufs.size()) == nb && + static_cast(out_scalars.size()) == nb, + error_type_t::RuntimeError, + "distributed_dot_bufs: a_bufs / b_bufs / out_scalars must " + "all have size == shards.size()"); + + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + cuopt_expects(a_bufs[r].size() == b_bufs[r].size(), + error_type_t::RuntimeError, + "distributed_dot_bufs: a_bufs[r] and b_bufs[r] must have equal size"); + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), + static_cast(a_bufs[r].size()), + a_bufs[r].data(), 1, - buf.data(), + b_bufs[r].data(), 1, - out, - shard.stream.view().value())); - }); + out_scalars[r].data_handle(), + s.stream.view().value())); + } - allreduce_sum_inplace(out_access, /*count=*/1); + allreduce_sum_inplace_bufs(out_scalars); + } - for_each_shard([&](auto& shard) { - f_t* out = out_access(*shard.sub_pdlp); - cub::DeviceTransform::Transform( - out, out, 1, sqrt_inplace_op_t{}, shard.stream.view().value()); - }); + // Core L2 norm: writes sqrt(sum_r ||in_bufs[r]||_2^2) into every + // *out_scalars[r].data_handle(). Delegates to distributed_dot_bufs(in, in, + // out) then does a per-shard in-place sqrt on the resulting scalar. + void distributed_l2_norm_bufs(std::vector> const& in_bufs, + std::vector> const& out_scalars) + { + distributed_dot_bufs(in_bufs, in_bufs, out_scalars); + for (std::size_t r = 0; r < shards.size(); ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + cub::DeviceTransform::Transform(out_scalars[r].data_handle(), + out_scalars[r].data_handle(), + 1, + sqrt_inplace_op_t{}, + s.stream.view().value()); + } + } + + // Wrapper: accessor form. Resolves per-shard input / output / owned-length + // then delegates to distributed_l2_norm_bufs. + // BufAccess : pdlp_solver_t& -> rmm::device_uvector& + // OutAccess : pdlp_solver_t& -> f_t* (single scalar) + // SizeAccess : pdlp_shard_t& -> i_t (owned slice length) + template + void distributed_l2_norm(BufAccess&& buf_access, OutAccess&& out_access, SizeAccess&& size_access) + { + std::vector> in_bufs; + std::vector> out_scalars; + in_bufs.reserve(shards.size()); + out_scalars.reserve(shards.size()); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + auto& sub = *s->sub_pdlp; + auto& buf = buf_access(sub); + const i_t n = size_access(*s); + in_bufs.emplace_back(buf.data(), static_cast(n)); + out_scalars.emplace_back(raft::make_device_scalar_view(out_access(sub))); + } + distributed_l2_norm_bufs(in_bufs, out_scalars); } // -------- High-level: A @ x and A_T @ y --------------------------------- From dd3ac278d7be8c4f329e325c5b9262374564a78e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 21:38:09 +0200 Subject: [PATCH 158/258] removed specific broadcast_constraint_scaling_to_halo function to use an accessor base one in scaling functions --- .../distributed_algorithms.cu | 54 +++++++++---------- .../distributed_algorithms.hpp | 12 ----- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 9e98d49d2f..b36c97d146 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -24,24 +24,6 @@ namespace cuopt::mathematical_optimization::pdlp { -// -------- Broadcast owned constraint (row) scaling into halo -------------- -template -void broadcast_constraint_scaling_to_halo(multi_gpu_engine_t& engine) -{ - engine.halo_exchange_cstr_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); - }); -} - -// -------- Broadcast owned variable (column) scaling into halo ------------- -template -void broadcast_variable_scaling_to_halo(multi_gpu_engine_t& engine) -{ - engine.halo_exchange_var_shard([](pdlp_shard_t& s) -> rmm::device_uvector& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); - }); -} - // -------- Solution gather (shards -> master) ------------------------------ template void gather_potential_next_solutions_to_master(multi_gpu_engine_t& engine, @@ -187,8 +169,12 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, for (int it = 0; it < num_iter; ++it) { // Refresh halo copies of both cumulative scalings (owner -> halo) so the // per-shard kernels read correct opposite-axis factors on their halo. - broadcast_variable_scaling_to_halo(engine); - broadcast_constraint_scaling_to_halo(engine); + engine.halo_exchange_var_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + }); + engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + }); // Shard-local Ruiz iteration // rows: inf norm only over OWNED (full) rows from A @@ -201,8 +187,12 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, // Final refresh so downstream consumers (the scaled problem, the next // distributed_max_singular_value, etc.) see correct halo factors. - broadcast_variable_scaling_to_halo(engine); - broadcast_constraint_scaling_to_halo(engine); + engine.halo_exchange_var_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + }); + engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + }); engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } @@ -220,16 +210,24 @@ void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); // Refresh halo copies of both cumulative scalings - broadcast_variable_scaling_to_halo(engine); - broadcast_constraint_scaling_to_halo(engine); + engine.halo_exchange_var_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + }); + engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + }); engine.for_each_shard([alpha](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_scaling(alpha); }); // Final refresh for downstream consumers. - broadcast_variable_scaling_to_halo(engine); - broadcast_constraint_scaling_to_halo(engine); + engine.halo_exchange_var_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + }); + engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + }); engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } @@ -483,10 +481,6 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, // ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- #define INSTANTIATE(F_TYPE) \ - template void broadcast_constraint_scaling_to_halo( \ - multi_gpu_engine_t & engine); \ - template void broadcast_variable_scaling_to_halo(multi_gpu_engine_t & \ - engine); \ template void distributed_bound_objective_rescaling( \ multi_gpu_engine_t & engine, F_TYPE c_scaling_weight); \ template void distributed_ruiz_inf_scaling( \ diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp index 532db6bd8e..2aec0c7c68 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp @@ -17,18 +17,6 @@ struct multi_gpu_engine_t; template class pdhg_solver_t; -// Broadcast the owned constraint (row) cumulative scaling into every peer's -// halo copy. The factor is computed only on owned rows; this pushes each -// owner's values out so halo rows carry correct factors for the next pass. -template -void broadcast_constraint_scaling_to_halo(multi_gpu_engine_t& engine); - -// Broadcast the owned variable (column) cumulative scaling into every peer's -// halo copy, so the next scaling iteration's row / column inf-norm kernels read -// correct factors on their halo columns. -template -void broadcast_variable_scaling_to_halo(multi_gpu_engine_t& engine); - // Global bound/objective rescaling: allreduce the owned partial squared norms // of the constraint bounds and (weighted) objective, then apply the identical // scalar on every shard. From c27c8a7cae9e42254e4771d556f0a8c55159ff13 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 21:53:12 +0200 Subject: [PATCH 159/258] moved is_distributed_sub_pdlp to pdlp.cuh --- .../mathematical_optimization/pdlp/solver_settings.hpp | 2 -- cpp/src/pdlp/distributed_pdlp/shard.cu | 3 +++ cpp/src/pdlp/pdlp.cu | 1 - cpp/src/pdlp/pdlp.cuh | 7 +++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 0702d1c3c6..4f551889a5 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -322,8 +322,6 @@ class pdlp_solver_settings_t { // "dummy" - round-robin, no graph (trivial) // "kaminpar" - multi-threaded KaMinPar std::string distributed_pdlp_partitioner{"auto"}; - // Set to true inside the shards - bool is_distributed_sub_pdlp{false}; method_t method{method_t::Concurrent}; bool inside_mip{false}; // For concurrent termination diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 3a7a436945..1d122056d5 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -130,6 +130,9 @@ pdlp_shard_t::pdlp_shard_t(int device_id, // ---- 5. Build sub_pdlp (single-GPU mode). ---- sub_pdlp = std::make_unique>( *sub_problem, settings, /*is_legacy_batch_mode=*/false); + // The shard IS by construction a distributed sub-solver; mark it so any + // pdlp/pdhg/termination/restart codepath can query is_distributed_sub_pdlp() + sub_pdlp->set_distributed_sub_pdlp(); // Inject this shard's unscaled buffers into op_problem_scaled (distributed // scaling runs later and will scale them). diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 1cd094788e..a812c95304 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -552,7 +552,6 @@ pdlp_solver_t::pdlp_solver_t( sub_pdlp_settings.distributed_pdlp_num_gpus = 1; sub_pdlp_settings.multi_gpu_partition_file = ""; sub_pdlp_settings.multi_gpu_export_partition_file = ""; - sub_pdlp_settings.is_distributed_sub_pdlp = true; sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index d649e8532e..2b11354679 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -224,6 +224,11 @@ class pdlp_solver_t { // Single-GPU PDLP reports false. bool is_distributed_master() const { return multi_gpu_engine.has_value(); } + // Marked true by the owning pdlp_shard_t right after this pdlp_solver_t is + // constructed as a per-shard sub-solver. + void set_distributed_sub_pdlp(bool value = true) { is_distributed_sub_pdlp_ = value; } + bool is_distributed_sub_pdlp() const { return is_distributed_sub_pdlp_; } + private: void compute_fixed_error(std::vector& has_restarted); @@ -282,6 +287,8 @@ class pdlp_solver_t { primal_quality_adapter_t best_primal_quality_so_far_; // Flag to indicate if solver is being called from MIP. No logging is done in this case. bool inside_mip_{false}; + // See set_distributed_sub_pdlp() / is_distributed_sub_pdlp() above. + bool is_distributed_sub_pdlp_{false}; }; } // namespace cuopt::mathematical_optimization::pdlp From c082586b87fd0f99f0396bf4d1c55f43eeae76df Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 22:01:32 +0200 Subject: [PATCH 160/258] distributed_pdlp_partitioner now takes an int --- .../mathematical_optimization/constants.h | 8 +++++ .../pdlp/solver_settings.hpp | 22 +++++++++--- cpp/src/math_optimization/solver_settings.cu | 2 +- cpp/src/pdlp/pdlp.cu | 35 +++++++++---------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 30a98ac39c..36c32098ac 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -211,6 +211,14 @@ #define CUOPT_PRESOLVE_PAPILO 1 #define CUOPT_PRESOLVE_PSLP 2 +/* @brief distributed_pdlp_partitioner values. + * Auto: pick automatically (Dummy on 1 GPU, KaMinPar otherwise). + * KaMinPar: multi-threaded KaMinPar graph partitioner. + * Dummy: round-robin, no graph. */ +#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER_AUTO 0 +#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER_KAMINPAR 1 +#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER_DUMMY 2 + /* @brief MIP scaling mode constants */ #define CUOPT_MIP_SCALING_OFF 0 #define CUOPT_MIP_SCALING_ON 1 diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 4f551889a5..45fc68292f 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -96,6 +96,19 @@ enum pdlp_precision_t : int { MixedPrecision = CUOPT_PDLP_MIXED_PRECISION }; +/** + * @brief Which graph partitioner distributed PDLP uses. + * + * Auto: pick automatically (Dummy on 1 GPU, KaMinPar otherwise). + * KaMinPar: multi-threaded KaMinPar graph partitioner. + * Dummy: round-robin, no graph (trivial). + */ +enum distributed_pdlp_partitioner_t : int { + Auto = CUOPT_DISTRIBUTED_PDLP_PARTITIONER_AUTO, + KaMinPar = CUOPT_DISTRIBUTED_PDLP_PARTITIONER_KAMINPAR, + Dummy = CUOPT_DISTRIBUTED_PDLP_PARTITIONER_DUMMY, +}; + template class pdlp_solver_settings_t { public: @@ -317,11 +330,10 @@ class pdlp_solver_settings_t { // path (one part-id per line) right after partitioning. The file can be fed // back via multi_gpu_partition_file. std::string multi_gpu_export_partition_file{""}; - // Which graph partitioner distributed PDLP uses. One of: - // "auto" - 1 GPU => Dummy; otherwise KaMinPar - // "dummy" - round-robin, no graph (trivial) - // "kaminpar" - multi-threaded KaMinPar - std::string distributed_pdlp_partitioner{"auto"}; + // Which graph partitioner distributed PDLP uses. See + // distributed_pdlp_partitioner_t for the meaning of each value. + distributed_pdlp_partitioner_t distributed_pdlp_partitioner{ + distributed_pdlp_partitioner_t::Auto}; method_t method{method_t::Concurrent}; bool inside_mip{false}; // For concurrent termination diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 769d960b49..6790d85d54 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -151,6 +151,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT, &mip_settings.strong_branching_simplex_iteration_limit, -1,std::numeric_limits::max(), -1}, {CUOPT_PRESOLVE, reinterpret_cast(&pdlp_settings.presolver), CUOPT_PRESOLVE_DEFAULT, CUOPT_PRESOLVE_PSLP, CUOPT_PRESOLVE_DEFAULT}, {CUOPT_PRESOLVE, reinterpret_cast(&mip_settings.presolver), CUOPT_PRESOLVE_DEFAULT, CUOPT_PRESOLVE_PSLP, CUOPT_PRESOLVE_DEFAULT}, + {CUOPT_DISTRIBUTED_PDLP_PARTITIONER, reinterpret_cast(&pdlp_settings.distributed_pdlp_partitioner), CUOPT_DISTRIBUTED_PDLP_PARTITIONER_AUTO, CUOPT_DISTRIBUTED_PDLP_PARTITIONER_DUMMY, CUOPT_DISTRIBUTED_PDLP_PARTITIONER_AUTO}, {CUOPT_MIP_DETERMINISM_MODE, &mip_settings.determinism_mode, CUOPT_MODE_OPPORTUNISTIC, CUOPT_MODE_DETERMINISTIC, CUOPT_MODE_OPPORTUNISTIC}, {CUOPT_RANDOM_SEED, &mip_settings.seed, -1, std::numeric_limits::max(), -1}, {CUOPT_MIP_RELIABILITY_BRANCHING, &mip_settings.reliability_branching, -1, std::numeric_limits::max(), -1}, @@ -209,7 +210,6 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""}, {CUOPT_MULTI_GPU_PARTITION_FILE, &pdlp_settings.multi_gpu_partition_file, ""}, {CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE, &pdlp_settings.multi_gpu_export_partition_file, ""}, - {CUOPT_DISTRIBUTED_PDLP_PARTITIONER, &pdlp_settings.distributed_pdlp_partitioner, "auto"}, }; // clang-format on } diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index a812c95304..67cb7f75c9 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -468,26 +468,23 @@ pdlp_solver_t::pdlp_solver_t( partition_input.nb_vars = n_vars; partition_input.nb_parts = distributed_pdlp_num_gpus; - // Resolve which partitioner to use. - std::string partitioner_choice = settings.distributed_pdlp_partitioner; - std::transform(partitioner_choice.begin(), - partitioner_choice.end(), - partitioner_choice.begin(), - [](unsigned char c) { return std::tolower(c); }); + // Resolve which partitioner to use. The public enum is validated at the + // parameter-set layer (int_parameters min/max), so we only need to map + // the three known values to the backend selector. partitioner_kind_t kind; - if (partitioner_choice.empty() || partitioner_choice == "auto") { - kind = - (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy : partitioner_kind_t::KaMinPar; - } else if (partitioner_choice == "dummy") { - kind = partitioner_kind_t::Dummy; - } else if (partitioner_choice == "kaminpar") { - kind = partitioner_kind_t::KaMinPar; - } else { - cuopt_expects(false, - error_type_t::ValidationError, - "Unknown distributed_pdlp_partitioner '%s' (expected auto|dummy|kaminpar)", - settings.distributed_pdlp_partitioner.c_str()); - kind = partitioner_kind_t::Dummy; // unreachable; silences -Wmaybe-uninitialized + switch (settings.distributed_pdlp_partitioner) { + case distributed_pdlp_partitioner_t::Auto: + kind = (distributed_pdlp_num_gpus == 1) ? partitioner_kind_t::Dummy + : partitioner_kind_t::KaMinPar; + break; + case distributed_pdlp_partitioner_t::KaMinPar: kind = partitioner_kind_t::KaMinPar; break; + case distributed_pdlp_partitioner_t::Dummy: kind = partitioner_kind_t::Dummy; break; + default: + cuopt_expects(false, + error_type_t::ValidationError, + "Unknown distributed_pdlp_partitioner value %d", + static_cast(settings.distributed_pdlp_partitioner)); + kind = partitioner_kind_t::Dummy; // unreachable; silences -Wmaybe-uninitialized } const bool needs_graph = (kind == partitioner_kind_t::KaMinPar); if (needs_graph) { From b35588946bcce0d54ea83642e74cded63c03022c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 22:04:54 +0200 Subject: [PATCH 161/258] removed multi_gpu_partition_file support --- cpp/include/cuopt/mathematical_optimization/constants.h | 1 - .../mathematical_optimization/pdlp/solver_settings.hpp | 4 ---- cpp/src/math_optimization/solver_settings.cu | 1 - cpp/src/pdlp/pdlp.cu | 9 --------- 4 files changed, 15 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 36c32098ac..d203f21615 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -88,7 +88,6 @@ #define CUOPT_NUM_GPUS "num_gpus" #define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" #define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" -#define CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE "multi_gpu_export_partition_file" #define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" #define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 45fc68292f..70e2c5cdbd 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -326,10 +326,6 @@ class pdlp_solver_settings_t { // -1 means auto-detect int distributed_pdlp_num_gpus{-1}; std::string multi_gpu_partition_file{""}; - // If non-empty, the partition computed for distributed PDLP is written to this - // path (one part-id per line) right after partitioning. The file can be fed - // back via multi_gpu_partition_file. - std::string multi_gpu_export_partition_file{""}; // Which graph partitioner distributed PDLP uses. See // distributed_pdlp_partitioner_t for the meaning of each value. distributed_pdlp_partitioner_t distributed_pdlp_partitioner{ diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 6790d85d54..a6be2a6988 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -209,7 +209,6 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_PRESOLVE_FILE, &mip_settings.presolve_file, ""}, {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""}, {CUOPT_MULTI_GPU_PARTITION_FILE, &pdlp_settings.multi_gpu_partition_file, ""}, - {CUOPT_MULTI_GPU_EXPORT_PARTITION_FILE, &pdlp_settings.multi_gpu_export_partition_file, ""}, }; // clang-format on } diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 67cb7f75c9..45f5b66b68 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -515,14 +515,6 @@ pdlp_solver_t::pdlp_solver_t( parts = partitioner->partition(partition_input); } - // Optionally dump the partition right after computing it (one part-id per line). - if (!settings.multi_gpu_export_partition_file.empty()) { - partition_loader_t::export_distributed_pdlp_partition_file( - settings.multi_gpu_export_partition_file, parts); - std::cout << "Exported " << parts.size() << " part-ids to " - << settings.multi_gpu_export_partition_file << std::endl; - } - // ----- 4. Build per-rank data ----- CUOPT_LOG_INFO("distributed_pdlp: building rank_data for %d parts ...", settings.distributed_pdlp_num_gpus); @@ -548,7 +540,6 @@ pdlp_solver_t::pdlp_solver_t( sub_pdlp_settings.num_gpus = 1; sub_pdlp_settings.distributed_pdlp_num_gpus = 1; sub_pdlp_settings.multi_gpu_partition_file = ""; - sub_pdlp_settings.multi_gpu_export_partition_file = ""; sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; From 50205fa18af6ba6e56bbdece0b12ed71899fcfc0 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 23:03:17 +0200 Subject: [PATCH 162/258] tuned down CUOPT_DISTRIBUTED_PDLP_NUM_GPUS max to 72 --- cpp/src/math_optimization/solver_settings.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index a6be2a6988..319beebce0 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -145,7 +145,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1}, {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, 1, 2, 1}, {CUOPT_NUM_GPUS, &mip_settings.num_gpus, 1, 2, 1}, - {CUOPT_DISTRIBUTED_PDLP_NUM_GPUS, &pdlp_settings.distributed_pdlp_num_gpus, -1, 576, -1}, + {CUOPT_DISTRIBUTED_PDLP_NUM_GPUS, &pdlp_settings.distributed_pdlp_num_gpus, -1, 72, -1}, {CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING, &mip_settings.mip_batch_pdlp_strong_branching, 0, 2, 0}, {CUOPT_MIP_BATCH_PDLP_RELIABILITY_BRANCHING, &mip_settings.mip_batch_pdlp_reliability_branching, 0, 2, 0}, {CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT, &mip_settings.strong_branching_simplex_iteration_limit, -1,std::numeric_limits::max(), -1}, From e194d0968e2a391da9c9ecc5c1a3aba57cd73a0a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 23:13:34 +0200 Subject: [PATCH 163/258] removed all export/inport partition file logic --- .../mathematical_optimization/constants.h | 1 - .../pdlp/solver_settings.hpp | 1 - cpp/src/math_optimization/solver_settings.cu | 1 - .../pdlp/distributed_pdlp/partition_loader.cu | 45 -------------- .../distributed_pdlp/partition_loader.hpp | 11 ---- cpp/src/pdlp/pdlp.cu | 7 +-- cpp/tests/linear_programming/pdlp_test.cu | 58 ------------------- 7 files changed, 1 insertion(+), 123 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index d203f21615..513134ff8f 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -87,7 +87,6 @@ #define CUOPT_NUM_CPU_THREADS "num_cpu_threads" #define CUOPT_NUM_GPUS "num_gpus" #define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" -#define CUOPT_MULTI_GPU_PARTITION_FILE "multi_gpu_partition_file" #define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" #define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 70e2c5cdbd..72afcaf8e1 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -325,7 +325,6 @@ class pdlp_solver_settings_t { // Number of GPUs to use specifically for distributed PDLP (use_distributed_pdlp=true). // -1 means auto-detect int distributed_pdlp_num_gpus{-1}; - std::string multi_gpu_partition_file{""}; // Which graph partitioner distributed PDLP uses. See // distributed_pdlp_partitioner_t for the meaning of each value. distributed_pdlp_partitioner_t distributed_pdlp_partitioner{ diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 319beebce0..dd94aa2f39 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -208,7 +208,6 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_USER_PROBLEM_FILE, &pdlp_settings.user_problem_file, ""}, {CUOPT_PRESOLVE_FILE, &mip_settings.presolve_file, ""}, {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""}, - {CUOPT_MULTI_GPU_PARTITION_FILE, &pdlp_settings.multi_gpu_partition_file, ""}, }; // clang-format on } diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu index 7eed272cc5..91be56e95d 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.cu @@ -7,56 +7,11 @@ #include -#include #include #include namespace cuopt::mathematical_optimization::pdlp { -template -std::vector partition_loader_t::parse_distributed_pdlp_partition_file( - std::string const& file) -{ - std::ifstream part_file(file); - cuopt_expects(part_file.is_open(), - error_type_t::ValidationError, - "Failed to open partition file: %s", - file.c_str()); - - // One integer per line - std::vector parts; - i_t part = 0; - while (part_file >> part) { - parts.push_back(part); - } - - // We must have hit EOF cleanly; any other state means a malformed token. - cuopt_expects(part_file.eof(), - error_type_t::ValidationError, - "Malformed partition file (expected one integer per line): %s", - file.c_str()); - - return parts; -} - -template -void partition_loader_t::export_distributed_pdlp_partition_file( - std::string const& file, std::vector const& parts) -{ - std::ofstream part_file(file); - cuopt_expects(part_file.is_open(), - error_type_t::ValidationError, - "Failed to open partition file for export: %s", - file.c_str()); - for (auto const& part : parts) { - part_file << part << "\n"; - } - cuopt_expects(part_file.good(), - error_type_t::RuntimeError, - "Failed while writing partition file: %s", - file.c_str()); -} - template std::vector> partition_loader_t::create_rank_data_from_parts( const std::vector& parts, diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp index 27712cfbfe..b88e9c3225 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp @@ -14,17 +14,6 @@ namespace cuopt::mathematical_optimization::pdlp { template struct partition_loader_t { - // Read a partition file: one part-id per line (whitespace-tolerant), - // ASCII integers in [0, nb_parts). Returns a flat vector of length - // nb_cstr + nb_vars, indexed as in create_rank_data_from_parts (cstrs first, then vars). - static std::vector parse_distributed_pdlp_partition_file(std::string const& file); - - // Write a partition vector to file in the same format parse_... reads back: - // one part-id per line. Useful for inspecting / reusing a computed partition - // (e.g. CLI --distributed-pdlp-export-parts). - static void export_distributed_pdlp_partition_file(std::string const& file, - std::vector const& parts); - // Slices the data to prepare a split from graph partitioning with halo communication static std::vector> create_rank_data_from_parts( const std::vector& parts, diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 45f5b66b68..187b906203 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -458,11 +458,7 @@ pdlp_solver_t::pdlp_solver_t( // ----- 3. Partition ----- std::vector parts; - if (!settings.multi_gpu_partition_file.empty()) { - parts = partition_loader_t::parse_distributed_pdlp_partition_file( - settings.multi_gpu_partition_file); - validate_partition(parts, n_cstr, n_vars, distributed_pdlp_num_gpus, "partition file"); - } else { + { partitioner_input_t partition_input; partition_input.nb_cstr = n_cstr; partition_input.nb_vars = n_vars; @@ -539,7 +535,6 @@ pdlp_solver_t::pdlp_solver_t( pdlp_solver_settings_t sub_pdlp_settings = settings; sub_pdlp_settings.num_gpus = 1; sub_pdlp_settings.distributed_pdlp_num_gpus = 1; - sub_pdlp_settings.multi_gpu_partition_file = ""; sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index e4b7d9c715..570b412383 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -98,64 +98,6 @@ TEST(pdlp_class, run_double) afiro_primal_objective, solution.get_additional_termination_information().primal_objective)); } -// Distributed-PDLP partition round-trip: partition the afiro constraint/variable -// bipartite graph with KaMinPar, write it out, read it back, and confirm the parsed -// vector is identical to what the partitioner produced. -TEST(pdlp_class, distributed_partition_kaminpar_export_import_roundtrip) -{ - using namespace cuopt::mathematical_optimization::pdlp; - namespace ds = cuopt::mathematical_optimization; - - auto path = make_path_absolute("linear_programming/afiro_original.mps"); - cuopt::mathematical_optimization::io::mps_data_model_t mps = - cuopt::mathematical_optimization::io::read_mps(path, true); - - const int n_vars = static_cast(mps.get_objective_coefficients().size()); - const int n_cstr = static_cast(mps.get_constraint_lower_bounds().size()); - const int nnz = static_cast(mps.get_constraint_matrix_values().size()); - - std::vector h_A_row_offsets = mps.get_constraint_matrix_offsets(); - std::vector h_A_col_indices = mps.get_constraint_matrix_indices(); - std::vector h_A_values = mps.get_constraint_matrix_values(); - - // Transpose A -> A^T (CSR of A^T == CSC of A), mirroring solve_lp_distributed_from_mps. - ds::csr_matrix_t A_csr(n_cstr, n_vars, nnz); - A_csr.row_start = h_A_row_offsets; - A_csr.j = h_A_col_indices; - A_csr.x = h_A_values; - ds::csc_matrix_t AT_as_csc(n_vars, n_cstr, nnz); - A_csr.to_compressed_col(AT_as_csc); - std::vector h_A_t_row_offsets = AT_as_csc.col_start; - std::vector h_A_t_col_indices = AT_as_csc.i; - - partitioner_input_t input; - input.nb_cstr = n_cstr; - input.nb_vars = n_vars; - input.nb_parts = 2; - input.A.row_offsets = &h_A_row_offsets; - input.A.col_indices = &h_A_col_indices; - input.A.num_rows = n_cstr; - input.A.num_cols = n_vars; - input.A_t.row_offsets = &h_A_t_row_offsets; - input.A_t.col_indices = &h_A_t_col_indices; - input.A_t.num_rows = n_vars; - input.A_t.num_cols = n_cstr; - - auto partitioner = make_partitioner(partitioner_kind_t::KaMinPar); - std::vector parts = partitioner->partition(input); - ASSERT_EQ(parts.size(), static_cast(n_cstr + n_vars)); - - std::string dir = ::testing::TempDir(); - if (!dir.empty() && dir.back() != '/') { dir.push_back('/'); } - const std::string out_path = dir + "afiro_kaminpar_roundtrip.parts"; - - partition_loader_t::export_distributed_pdlp_partition_file(out_path, parts); - std::vector reloaded = - partition_loader_t::parse_distributed_pdlp_partition_file(out_path); - - EXPECT_EQ(parts, reloaded); -} - namespace { // Solve `mps_rel_path` with the single-GPU PDLP ("base") and with distributed PDLP From f14cc1b78d034c1ecf454276cc8f73d902a5945e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 23:18:05 +0200 Subject: [PATCH 164/258] replaced raft::sqrt with cuda::std::sqrt --- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index f716fae54f..5a09fff406 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -65,7 +66,7 @@ constexpr ncclDataType_t nccl_data_type() // invoked with closure accessors). template struct sqrt_inplace_op_t { - __host__ __device__ f_t operator()(f_t x) const { return raft::sqrt(x); } + __host__ __device__ f_t operator()(f_t x) const { return cuda::std::sqrt(x); } }; template From 4f3e897728930bf8e0703542bfba433b5dc1a561 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 5 Jul 2026 23:52:12 +0200 Subject: [PATCH 165/258] use spans in partitionner.hpp --- cpp/src/pdlp/distributed_pdlp/partitioner.cpp | 12 ++++++------ cpp/src/pdlp/distributed_pdlp/partitioner.hpp | 7 ++++--- cpp/src/pdlp/pdlp.cu | 12 ++++++------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp index 0b2f6dd6f9..1c2b200c38 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp @@ -71,17 +71,17 @@ std::vector kaminpar_partitioner_t::partition( CUOPT_LOG_INFO("KaMinPar: nb_parts == 1, returning trivial single-block partition"); return std::vector(static_cast(input.nb_cstr + input.nb_vars), i_t{0}); } - cuopt_expects(input.A.row_offsets != nullptr && input.A.col_indices != nullptr, + cuopt_expects(!input.A.row_offsets.empty() && !input.A.col_indices.empty(), error_type_t::ValidationError, "kaminpar_partitioner: A.row_offsets and A.col_indices are required"); - cuopt_expects(input.A_t.row_offsets != nullptr && input.A_t.col_indices != nullptr, + cuopt_expects(!input.A_t.row_offsets.empty() && !input.A_t.col_indices.empty(), error_type_t::ValidationError, "kaminpar_partitioner: A_t.row_offsets and A_t.col_indices are required"); - auto const& A_offsets = *input.A.row_offsets; - auto const& A_cols = *input.A.col_indices; - auto const& A_t_offsets = *input.A_t.row_offsets; - auto const& A_t_cols = *input.A_t.col_indices; + auto A_offsets = input.A.row_offsets; + auto A_cols = input.A.col_indices; + auto A_t_offsets = input.A_t.row_offsets; + auto A_t_cols = input.A_t.col_indices; cuopt_expects(static_cast(A_offsets.size()) == input.nb_cstr + 1, error_type_t::ValidationError, diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp index 64e9de3a32..93b7065137 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.hpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.hpp @@ -6,6 +6,7 @@ #pragma once #include +#include #include namespace cuopt::mathematical_optimization::pdlp { @@ -13,9 +14,9 @@ namespace cuopt::mathematical_optimization::pdlp { // Non-owning view of a host CSR matrix (A or A_t). template struct csr_host_view_t { - std::vector const* row_offsets{nullptr}; - std::vector const* col_indices{nullptr}; - std::vector const* values{nullptr}; // optional; unused by topology-only partitioners + std::span row_offsets{}; + std::span col_indices{}; + std::span values{}; // optional; unused by topology-only partitioners i_t num_rows{0}; i_t num_cols{0}; }; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 187b906203..4f9a80d9c1 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -484,14 +484,14 @@ pdlp_solver_t::pdlp_solver_t( } const bool needs_graph = (kind == partitioner_kind_t::KaMinPar); if (needs_graph) { - // partitioner_input_t holds non-const std::vector* pointers; we - // already have the data in our local mutable buffers above. - partition_input.A.row_offsets = &h_A_row_offsets; - partition_input.A.col_indices = &h_A_col_indices; + // csr_host_view_t members are std::span — an owning + // std::vector converts implicitly. + partition_input.A.row_offsets = h_A_row_offsets; + partition_input.A.col_indices = h_A_col_indices; partition_input.A.num_rows = n_cstr; partition_input.A.num_cols = n_vars; - partition_input.A_t.row_offsets = &h_A_t_row_offsets; - partition_input.A_t.col_indices = &h_A_t_col_indices; + partition_input.A_t.row_offsets = h_A_t_row_offsets; + partition_input.A_t.col_indices = h_A_t_col_indices; partition_input.A_t.num_rows = n_vars; partition_input.A_t.num_cols = n_cstr; // 0 => KaMinPar auto-detects and uses all hardware threads. From 91b1be359905c2f99e8eb7c228af0b5b528682b3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 10:25:53 +0200 Subject: [PATCH 166/258] update comments in distr_max_sing_value --- .../distributed_pdlp/distributed_algorithms.cu | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index b36c97d146..222b83644b 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -302,14 +302,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, } } - // Per-shard scratch lives on each shard's device. We use total (owned + - // halo) sizes for q/z/atq because they're SpMV inputs that need halo - // space. Norms / dot are scalars. - // We use size-1 rmm::device_uvector instead of rmm::device_scalar for the - // per-shard scratch scalars: nvcc + libcudacxx fail the - // copy_constructible concept check when device_scalar appears in a - // std::vector (the check transitively touches rmm::cuda_stream, which is - // non-copyable). device_uvector avoids that path. + // Per-shard scratch lives on each shard's device.] std::vector> q; std::vector> z; std::vector> atq; @@ -327,6 +320,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, norm_q.reserve(nb); residual_norm.reserve(nb); + // Scatter z according to partition for (int r = 0; r < nb; ++r) { auto& s = *engine.shards[r]; raft::device_setter guard(s.device_id); @@ -363,12 +357,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, s.stream.synchronize(); } - // Build the per-shard views used by the engine's *_bufs helpers. - // Built ONCE: rmm::device_uvector::data() is stable for the object's - // lifetime (no reallocation happens inside the loop). - // *_full : span over owned + halo tail (halo_exchange_{cstr,var}_bufs) - // *_owned : span over just the owned prefix (distributed_{l2_norm,dot}_bufs) - // *_scalar : device_scalar_view over a per-shard scalar output slot + // Build the per-shard views (spans) used by the engine's *_bufs helpers. std::vector> q_full, atq_full; std::vector> q_owned, z_owned; std::vector> norm_q_scalar, sigma_sq_scalar, residual_scalar; From 1a6161685c8e30eb2056fe400e585d0489a2e747 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 11:18:16 +0200 Subject: [PATCH 167/258] added simple distributed_compute_primal_weight --- .../distributed_algorithms.cu | 28 +++++++++++++++++++ .../distributed_algorithms.hpp | 11 ++++++++ cpp/src/pdlp/pdlp.cu | 15 +++++----- cpp/src/pdlp/solve.cu | 20 +++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 222b83644b..e4f1eecf01 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -468,6 +468,32 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, return std::sqrt(std::max(sigma_sq_h, f_t(0))); } +// -------- Distributed initial primal weight ------------------------------ +// Distributed PDLP is currently restricted to the Stable3-shaped hyper-param +// profile (validated up front in solve_lp_distributed_from_mps, and defended +// again here). In that regime, single-GPU compute_initial_primal_weight +// short-circuits to primal_weight = 1 without touching the norms (see +// pdlp.cu: +// !initial_primal_weight_combined_bounds && bound_objective_rescaling +// -> uninitialized_fill(primal_weight_ / best_primal_weight_, 1); return +// ). Match that exactly, at zero communication cost. +template +f_t distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, + pdlp_hyper_params_t const& hyper_params) +{ + raft::common::nvtx::range scope("distributed_compute_initial_primal_weight"); + (void)engine; // Kept in the signature so the shape stays compatible with + // the eventual full implementation. + cuopt_expects(!hyper_params.initial_primal_weight_combined_bounds && + hyper_params.bound_objective_rescaling, + error_type_t::ValidationError, + "distributed_compute_initial_primal_weight: only the Stable3-shaped " + "short-circuit is supported (initial_primal_weight_combined_bounds=false " + "and bound_objective_rescaling=true). This should have been rejected " + "earlier in solve_lp_distributed_from_mps."); + return f_t(1); +} + // ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- #define INSTANTIATE(F_TYPE) \ template void distributed_bound_objective_rescaling( \ @@ -485,6 +511,8 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, int n_global_cstrs, \ int max_iterations, \ F_TYPE tolerance); \ + template F_TYPE distributed_compute_initial_primal_weight( \ + multi_gpu_engine_t & engine, pdlp_hyper_params_t const& hyper_params); \ template void gather_potential_next_solutions_to_master( \ multi_gpu_engine_t & engine, \ pdhg_solver_t & master_pdhg, \ diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp index 2aec0c7c68..e51b7f152a 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp @@ -63,6 +63,17 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, int max_iterations = 5000, f_t tolerance = 1e-4); +// Distributed counterpart of pdlp_solver_t::compute_initial_primal_weight +// (pdlp.cu). Returns the global initial primal weight. +// +// Today distributed only supports the Stable3-shaped hyper-param profile +// (validated at entry in solve_lp_distributed_from_mps), in which single-GPU +// short-circuits primal_weight to 1.0 without touching any norm. This +// function mirrors that short-circuit exactly. +template +f_t distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, + pdlp_hyper_params_t const& hyper_params); + // Gather the global potential_next primal/dual solutions and the reduced cost // onto the master from the owned slices distributed across shards. template diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 4f9a80d9c1..ff3f6c390e 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -556,13 +556,14 @@ pdlp_solver_t::pdlp_solver_t( // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- constexpr f_t kStepSizeScale = f_t{0.998}; const f_t sigma_max = distributed_max_singular_value(*multi_gpu_engine, n_cstr); - const f_t h_primal_weight = f_t{1}; - const f_t h_step_size = (sigma_max > f_t{0}) ? kStepSizeScale / sigma_max : f_t{1}; - // With primal_weight = 1 the adaptive step-size strategy collapses to - // primal_step_size = step_size / primal_weight = step_size - // dual_step_size = step_size * primal_weight = step_size. - const f_t h_primal_step_size = h_step_size; - const f_t h_dual_step_size = h_step_size; + const f_t h_primal_weight = + distributed_compute_initial_primal_weight(*multi_gpu_engine, settings_.hyper_params); + const f_t h_step_size = (sigma_max > f_t{0}) ? kStepSizeScale / sigma_max : f_t{1}; + // PDLP parameterization: + // primal_step_size = step_size / primal_weight + // dual_step_size = step_size * primal_weight + const f_t h_primal_step_size = h_step_size / h_primal_weight; + const f_t h_dual_step_size = h_step_size * h_primal_weight; // Put the values on master raft::copy(step_size_.data(), &h_step_size, 1, stream_view_); diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 2fd341c6f7..88a64f359f 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2373,6 +2373,26 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( error_type_t::ValidationError, "Distributed PDLP does not support initial primal/dual solutions or warm-start " "data."); + // Distributed PDLP today only supports the Stable3-shaped hyper-param profile: + // - initial_step_size_max_singular_value = true (matches the sigma_max seeding + // driven by distributed_max_singular_value in the setup), + // - initial_primal_weight_combined_bounds = false and bound_objective_rescaling = true + // (this is the profile where single-GPU compute_initial_primal_weight + // short-circuits to primal_weight = 1, which distributed_compute_initial_primal_weight + // mirrors verbatim). + // Any other profile would leave distributed setup silently divergent from + // single-GPU (either the step size or the primal weight would be seeded from + // a different formula than what single-GPU would compute). + // Fail early + cuopt_expects( + settings_resolved.hyper_params.initial_step_size_max_singular_value && + !settings_resolved.hyper_params.initial_primal_weight_combined_bounds && + settings_resolved.hyper_params.bound_objective_rescaling, + error_type_t::ValidationError, + "Distributed PDLP currently only supports the Stable3-shaped hyper-param profile " + "(initial_step_size_max_singular_value=true, initial_primal_weight_combined_bounds=false, " + "bound_objective_rescaling=true). Set pdlp_solver_mode = Stable3 (the default) or adjust " + "the hyper-params to match."); init_logger_t log(settings_resolved.log_file, settings_resolved.log_to_console); print_version_info(); From aac76a22b8e10bb9fc6b178b254a8e32742aad80 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 14:54:45 +0200 Subject: [PATCH 168/258] updated scaling, step size and primal weight to run in the same call for single and distributed, in run_solver() --- .../distributed_algorithms.cu | 76 +++++++- .../distributed_algorithms.hpp | 29 ++- cpp/src/pdlp/pdlp.cu | 183 +++++++++++------- cpp/src/pdlp/pdlp.cuh | 6 + 4 files changed, 202 insertions(+), 92 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index e4f1eecf01..fad81e494c 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -468,6 +468,44 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, return std::sqrt(std::max(sigma_sq_h, f_t(0))); } +// -------- Distributed initial step size --------------------------------- +// Sigma_max(A) via the shared power-iteration primitive. +// +// This function mirrors single-GPU's compute_initial_step_size exactly +// and broadcasts the result to every shard +template +void distributed_compute_initial_step_size(multi_gpu_engine_t& engine, + pdlp_solver_t& master, + pdlp_hyper_params_t const& hyper_params, + i_t n_global_cstrs, + f_t scaling_factor, + int max_iterations, + f_t tolerance) +{ + raft::common::nvtx::range scope("distributed_compute_initial_step_size"); + cuopt_expects(hyper_params.initial_step_size_max_singular_value, + error_type_t::ValidationError, + "distributed_compute_initial_step_size requires " + "initial_step_size_max_singular_value = true; the max-abs-value " + "of A fallback is single-GPU only. This should have been rejected " + "earlier in solve_lp_distributed_from_mps."); + + const f_t sigma_max = + distributed_max_singular_value(engine, n_global_cstrs, max_iterations, tolerance); + + auto* handle_ptr = master.get_handle_ptr(); + auto stream_view = handle_ptr->get_stream(); + + const f_t h_step_size = (sigma_max > f_t{0}) ? scaling_factor / sigma_max : f_t{1}; + + raft::copy(master.get_step_size().data(), &h_step_size, 1, stream_view); + engine.for_each_shard([&](auto& shard) { + raft::copy(shard.sub_pdlp->get_step_size().data(), &h_step_size, 1, shard.stream); + }); + engine.sync_await_shards(stream_view); + handle_ptr->sync_stream(stream_view); +} + // -------- Distributed initial primal weight ------------------------------ // Distributed PDLP is currently restricted to the Stable3-shaped hyper-param // profile (validated up front in solve_lp_distributed_from_mps, and defended @@ -476,14 +514,13 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, // pdlp.cu: // !initial_primal_weight_combined_bounds && bound_objective_rescaling // -> uninitialized_fill(primal_weight_ / best_primal_weight_, 1); return -// ). Match that exactly, at zero communication cost. +// This function also fills the shards and masters primal_weight / step_size buffers template -f_t distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, - pdlp_hyper_params_t const& hyper_params) +void distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, + pdlp_solver_t& master, + pdlp_hyper_params_t const& hyper_params) { raft::common::nvtx::range scope("distributed_compute_initial_primal_weight"); - (void)engine; // Kept in the signature so the shape stays compatible with - // the eventual full implementation. cuopt_expects(!hyper_params.initial_primal_weight_combined_bounds && hyper_params.bound_objective_rescaling, error_type_t::ValidationError, @@ -491,7 +528,20 @@ f_t distributed_compute_initial_primal_weight(multi_gpu_engine_t& engi "short-circuit is supported (initial_primal_weight_combined_bounds=false " "and bound_objective_rescaling=true). This should have been rejected " "earlier in solve_lp_distributed_from_mps."); - return f_t(1); + const f_t h_primal_weight = f_t(1); + + auto* handle_ptr = master.get_handle_ptr(); + auto stream_view = handle_ptr->get_stream(); + + raft::copy(master.get_primal_weight().data(), &h_primal_weight, 1, stream_view); + raft::copy(master.get_best_primal_weight().data(), &h_primal_weight, 1, stream_view); + engine.for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + raft::copy(sub.get_primal_weight().data(), &h_primal_weight, 1, shard.stream); + raft::copy(sub.get_best_primal_weight().data(), &h_primal_weight, 1, shard.stream); + }); + engine.sync_await_shards(stream_view); + handle_ptr->sync_stream(stream_view); } // ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- @@ -511,8 +561,18 @@ f_t distributed_compute_initial_primal_weight(multi_gpu_engine_t& engi int n_global_cstrs, \ int max_iterations, \ F_TYPE tolerance); \ - template F_TYPE distributed_compute_initial_primal_weight( \ - multi_gpu_engine_t & engine, pdlp_hyper_params_t const& hyper_params); \ + template void distributed_compute_initial_step_size( \ + multi_gpu_engine_t & engine, \ + pdlp_solver_t & master, \ + pdlp_hyper_params_t const& hyper_params, \ + int n_global_cstrs, \ + F_TYPE scaling_factor, \ + int max_iterations, \ + F_TYPE tolerance); \ + template void distributed_compute_initial_primal_weight( \ + multi_gpu_engine_t & engine, \ + pdlp_solver_t & master, \ + pdlp_hyper_params_t const& hyper_params); \ template void gather_potential_next_solutions_to_master( \ multi_gpu_engine_t & engine, \ pdhg_solver_t & master_pdhg, \ diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp index e51b7f152a..48f995b92d 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp @@ -17,6 +17,9 @@ struct multi_gpu_engine_t; template class pdhg_solver_t; +template +class pdlp_solver_t; + // Global bound/objective rescaling: allreduce the owned partial squared norms // of the constraint bounds and (weighted) objective, then apply the identical // scalar on every shard. @@ -63,16 +66,24 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, int max_iterations = 5000, f_t tolerance = 1e-4); -// Distributed counterpart of pdlp_solver_t::compute_initial_primal_weight -// (pdlp.cu). Returns the global initial primal weight. -// -// Today distributed only supports the Stable3-shaped hyper-param profile -// (validated at entry in solve_lp_distributed_from_mps), in which single-GPU -// short-circuits primal_weight to 1.0 without touching any norm. This -// function mirrors that short-circuit exactly. +// Distributed counterpart of pdlp_solver_t::compute_initial_step_size. +template +void distributed_compute_initial_step_size(multi_gpu_engine_t& engine, + pdlp_solver_t& master, + pdlp_hyper_params_t const& hyper_params, + i_t n_global_cstrs, + f_t scaling_factor, + int max_iterations, + f_t tolerance); + +// Distributed counterpart of pdlp_solver_t::compute_initial_primal_weight. +// Writes primal_weight = best_primal_weight = 1 onto master + every shard, +// mirroring the Stable3-shaped short-circuit +// (!initial_primal_weight_combined_bounds && bound_objective_rescaling). template -f_t distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, - pdlp_hyper_params_t const& hyper_params); +void distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, + pdlp_solver_t& master, + pdlp_hyper_params_t const& hyper_params); // Gather the global potential_next primal/dual solutions and the reduced cost // onto the master from the owned slices distributed across shards. diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index ff3f6c390e..674b6477a1 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -541,53 +541,6 @@ pdlp_solver_t::pdlp_solver_t( // ----- 6. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), mps, sub_pdlp_settings); - // ----- 8 Distributed Scaling ----- - // Full distributed scaling pipeline (reset state + Ruiz + Pock-Chambolle + - // per-shard scale_problem with the local bound/obj step suppressed + global - // bound/objective rescaling). - distributed_scaling(*multi_gpu_engine, settings_.hyper_params, n_vars, inside_mip_); - - multi_gpu_engine->for_each_shard([&](auto& shard) { - shard.sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( - /*is_reflected=*/settings_.hyper_params.use_reflected_primal_dual); - }); - multi_gpu_engine->for_each_shard([](auto& shard) { shard.stream.synchronize(); }); - - // ----- 8b. Seed initial step-size / primal-weight (distributed, scales to N shards) ----- - constexpr f_t kStepSizeScale = f_t{0.998}; - const f_t sigma_max = distributed_max_singular_value(*multi_gpu_engine, n_cstr); - const f_t h_primal_weight = - distributed_compute_initial_primal_weight(*multi_gpu_engine, settings_.hyper_params); - const f_t h_step_size = (sigma_max > f_t{0}) ? kStepSizeScale / sigma_max : f_t{1}; - // PDLP parameterization: - // primal_step_size = step_size / primal_weight - // dual_step_size = step_size * primal_weight - const f_t h_primal_step_size = h_step_size / h_primal_weight; - const f_t h_dual_step_size = h_step_size * h_primal_weight; - - // Put the values on master - raft::copy(step_size_.data(), &h_step_size, 1, stream_view_); - raft::copy(primal_weight_.data(), &h_primal_weight, 1, stream_view_); - raft::copy(best_primal_weight_.data(), &h_primal_weight, 1, stream_view_); - raft::copy(primal_step_size_.data(), &h_primal_step_size, 1, stream_view_); - raft::copy(dual_step_size_.data(), &h_dual_step_size, 1, stream_view_); - handle_ptr_->sync_stream(stream_view_); - - // put the values on each shard - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub = *shard->sub_pdlp; - raft::copy(sub.step_size_.data(), &h_step_size, 1, shard->stream); - raft::copy(sub.primal_weight_.data(), &h_primal_weight, 1, shard->stream); - raft::copy(sub.best_primal_weight_.data(), &h_primal_weight, 1, shard->stream); - raft::copy(sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard->stream); - raft::copy(sub.get_dual_step_size().data(), &h_dual_step_size, 1, shard->stream); - } - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->stream.synchronize(); - } - // Wire the engine into master's pdhg_solver_; shards keep mgpu_engine_ == nullptr. pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); @@ -2674,24 +2627,23 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co bool warm_start_was_given = settings_.get_pdlp_warm_start_data().is_populated(); - // In distributed mode, skip all setup, it is already done + // The four setup calls (compute_initial_step_size, compute_initial_primal_weight, + // scale_problem, create_spmv_op_plans) run unconditionally here. Each of them + // branches on is_distributed_master() at entry. + if (settings_.hyper_params.compute_initial_step_size_before_scaling && + !settings_.get_initial_step_size().has_value()) + compute_initial_step_size(); + if (settings_.hyper_params.compute_initial_primal_weight_before_scaling && + !settings_.get_initial_primal_weight().has_value()) + compute_initial_primal_weight(); + + scale_problem(); + create_spmv_op_plans(); + + // Post-scaling cleanup is single-GPU only: distributed shards' cusparse + // views are set up per-shard and don't share the master's problem_ptr / + // op_problem_scaled_. if (!settings_.use_distributed_pdlp) { - // TODO handle that properly - if (settings_.hyper_params.compute_initial_step_size_before_scaling && - !settings_.get_initial_step_size().has_value()) - compute_initial_step_size(); - if (settings_.hyper_params.compute_initial_primal_weight_before_scaling && - !settings_.get_initial_primal_weight().has_value()) - compute_initial_primal_weight(); - - initial_scaling_strategy_.scale_problem(); - if constexpr (std::is_same_v) { - if (!batch_mode_ && !pdhg_solver_.get_cusparse_view().mixed_precision_enabled_) { - pdhg_solver_.get_cusparse_view().create_spmv_op_plans( - settings_.hyper_params.use_reflected_primal_dual); - } - } - // Update FP32 matrix copies for mixed precision SpMV after scaling pdhg_solver_.get_cusparse_view().update_mixed_precision_matrices(); @@ -2703,14 +2655,34 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co op_problem_scaled_.offsets.resize(0, stream_view_); op_problem_scaled_.reverse_constraints.resize(0, stream_view_); op_problem_scaled_.reverse_offsets.resize(0, stream_view_); + } - if (!settings_.hyper_params.compute_initial_step_size_before_scaling && - !settings_.get_initial_step_size().has_value()) - compute_initial_step_size(); - if (!settings_.hyper_params.compute_initial_primal_weight_before_scaling && - !settings_.get_initial_primal_weight().has_value()) - compute_initial_primal_weight(); + if (!settings_.hyper_params.compute_initial_step_size_before_scaling && + !settings_.get_initial_step_size().has_value()) + compute_initial_step_size(); + if (!settings_.hyper_params.compute_initial_primal_weight_before_scaling && + !settings_.get_initial_primal_weight().has_value()) + compute_initial_primal_weight(); + // Distributed counterpart of the single-GPU + // step_size_strategy_.get_primal_and_dual_stepsizes() + if (settings_.use_distributed_pdlp) { + step_size_strategy_.get_primal_and_dual_stepsizes(primal_step_size_, dual_step_size_); + multi_gpu_engine->for_each_shard([&](auto& shard) { + auto& sub = *shard.sub_pdlp; + sub.step_size_strategy_.get_primal_and_dual_stepsizes(sub.primal_step_size_, + sub.dual_step_size_); + }); + multi_gpu_engine->sync_await_shards(stream_view_); + handle_ptr_->sync_stream(stream_view_); + } + + // Everything below (seed-from-settings, initial_k, get_primal_and_dual_stepsizes, + // initial primal/dual, projection, transpose, verbose prints, log header) + // still runs single-GPU only. Distributed rejects + // has_initial_{primal,dual}_solution() and warm-start data up front, and + // its per-shard primal/dual step sizes were derived above + if (!settings_.use_distributed_pdlp) { #ifdef PDLP_DEBUG_MODE std::cout << "Initial Scaling done" << std::endl; #endif @@ -3328,11 +3300,66 @@ void pdlp_solver_t::take_step([[maybe_unused]] i_t total_pdlp_iteratio // print("potential next dual", pdhg_solver_.get_potential_next_dual_solution()); } +template +void pdlp_solver_t::scale_problem() +{ + raft::common::nvtx::range fun_scope("pdlp_solver_t::scale_problem"); + if (is_distributed_master()) { + distributed_scaling(*multi_gpu_engine, settings_.hyper_params, primal_size_h_, inside_mip_); + } + else { + initial_scaling_strategy_.scale_problem(); + } +} + +template +void pdlp_solver_t::create_spmv_op_plans() +{ + raft::common::nvtx::range fun_scope("pdlp_solver_t::create_spmv_op_plans"); + if (is_distributed_master()) { + // Distributed path: fan out the same per-shard cusparse_view call the + // single-GPU path would make. + multi_gpu_engine->for_each_shard([&](auto& shard) { + shard.sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( + settings_.hyper_params.use_reflected_primal_dual); + }); + multi_gpu_engine->sync_await_shards(stream_view_); + handle_ptr_->sync_stream(stream_view_); + return; + } + if constexpr (std::is_same_v) { + if (!batch_mode_ && !pdhg_solver_.get_cusparse_view().mixed_precision_enabled_) { + pdhg_solver_.get_cusparse_view().create_spmv_op_plans( + settings_.hyper_params.use_reflected_primal_dual); + } + } +} + template void pdlp_solver_t::compute_initial_step_size() { raft::common::nvtx::range fun_scope("compute_initial_step_size"); + // Shared knobs for the power-iteration path (both single-GPU and + // distributed) + constexpr f_t scaling_factor = f_t{0.998}; + constexpr int max_iterations = 5000; + constexpr f_t tolerance = f_t{1e-4}; + + if (is_distributed_master()) { + // Distributed dispatch: everything (sigma_max, deriving primal/dual + // step sizes from master's current primal_weight_, seeding master + + // all shards, syncs) lives inside distributed_compute_initial_step_size. + distributed_compute_initial_step_size(*multi_gpu_engine, + *this, + settings_.hyper_params, + dual_size_h_, + scaling_factor, + max_iterations, + tolerance); + return; + } + if (!settings_.hyper_params.initial_step_size_max_singular_value) { // set stepsize relative to maximum absolute value of A rmm::device_scalar abs_max_element{0.0, stream_view_}; @@ -3365,9 +3392,6 @@ void pdlp_solver_t::compute_initial_step_size() // Sync since we are using local variable RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); } else { - constexpr i_t max_iterations = 5000; - constexpr f_t tolerance = 1e-4; - i_t m = op_problem_scaled_.n_constraints; i_t n = op_problem_scaled_.n_variables; @@ -3470,8 +3494,7 @@ void pdlp_solver_t::compute_initial_step_size() printf("iter_count %d\n", sing_iters); #endif - constexpr f_t scaling_factor = 0.998; - const f_t step_size = scaling_factor / std::sqrt(sigma_max_sq.value(stream_view_)); + const f_t step_size = scaling_factor / std::sqrt(sigma_max_sq.value(stream_view_)); thrust::uninitialized_fill( handle_ptr_->get_thrust_policy(), step_size_.begin(), step_size_.end(), step_size); @@ -3521,6 +3544,16 @@ void pdlp_solver_t::compute_initial_primal_weight() { raft::common::nvtx::range fun_scope("compute_initial_primal_weight"); + if (is_distributed_master()) { + // Distributed dispatch: + // - short-circuit -> 1 + // - primal/dual step sizes from master's current step_size_, seeding + // master + all shards, syncs) + distributed_compute_initial_primal_weight( + *multi_gpu_engine, *this, settings_.hyper_params); + return; + } + // Here we use the combined bounds of the op_problem_scaled which may or may not be scaled yet // based on pdlp config pdlp::combine_constraint_bounds(op_problem_scaled_, op_problem_scaled_.combined_bounds); diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index 2b11354679..bcad89ca63 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -104,6 +104,10 @@ class pdlp_solver_t { void compute_initial_step_size(); void compute_initial_primal_weight(); + // Thin dispatch wrappers used by run_solver so single-GPU and distributed + // callers hit the same call sites. + void scale_problem(); + void create_spmv_op_plans(); // Needed by multi-GPU to mutate them mip::problem_t& get_op_problem_scaled() { return op_problem_scaled_; } @@ -123,6 +127,8 @@ class pdlp_solver_t { // downstream shard-side restart machinery stays in sync with master. rmm::device_uvector& get_primal_weight() { return primal_weight_; } rmm::device_uvector& get_best_primal_weight() { return best_primal_weight_; } + rmm::device_uvector& get_step_size() { return step_size_; } + raft::handle_t const* get_handle_ptr() const { return handle_ptr_; } private: void print_termination_criteria(const timer_t& timer, bool is_average = false); From 1f4707489d7eb1a1eeb2b59dce1f76bd3f6546b2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 14:55:00 +0200 Subject: [PATCH 169/258] style --- .../mathematical_optimization/constants.h | 22 +++--- .../pdlp/solver_settings.hpp | 3 +- .../distributed_algorithms.cu | 76 +++++++++---------- .../distributed_pdlp/multi_gpu_engine.hpp | 18 ++--- cpp/src/pdlp/pdlp.cu | 10 +-- 5 files changed, 62 insertions(+), 67 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 513134ff8f..c6bb26e872 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -83,17 +83,17 @@ #define CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT \ "mip_strong_branching_simplex_iteration_limit" -#define CUOPT_SOLUTION_FILE "solution_file" -#define CUOPT_NUM_CPU_THREADS "num_cpu_threads" -#define CUOPT_NUM_GPUS "num_gpus" -#define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" -#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" -#define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" -#define CUOPT_USER_PROBLEM_FILE "user_problem_file" -#define CUOPT_PRESOLVE_FILE "presolve_file" -#define CUOPT_RANDOM_SEED "random_seed" -#define CUOPT_PDLP_PRECISION "pdlp_precision" -#define CUOPT_MIP_SEMICONTINUOUS_BIG_M "mip_semi_continuous_big_m" +#define CUOPT_SOLUTION_FILE "solution_file" +#define CUOPT_NUM_CPU_THREADS "num_cpu_threads" +#define CUOPT_NUM_GPUS "num_gpus" +#define CUOPT_DISTRIBUTED_PDLP_NUM_GPUS "distributed_pdlp_num_gpus" +#define CUOPT_DISTRIBUTED_PDLP_PARTITIONER "distributed_pdlp_partitioner" +#define CUOPT_USE_DISTRIBUTED_PDLP "use_distributed_pdlp" +#define CUOPT_USER_PROBLEM_FILE "user_problem_file" +#define CUOPT_PRESOLVE_FILE "presolve_file" +#define CUOPT_RANDOM_SEED "random_seed" +#define CUOPT_PDLP_PRECISION "pdlp_precision" +#define CUOPT_MIP_SEMICONTINUOUS_BIG_M "mip_semi_continuous_big_m" #define CUOPT_MIP_HYPER_HEURISTIC_POPULATION_SIZE "mip_hyper_heuristic_population_size" #define CUOPT_MIP_HYPER_HEURISTIC_NUM_CPUFJ_THREADS "mip_hyper_heuristic_num_cpufj_threads" diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 72afcaf8e1..5162ae8c4b 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -327,8 +327,7 @@ class pdlp_solver_settings_t { int distributed_pdlp_num_gpus{-1}; // Which graph partitioner distributed PDLP uses. See // distributed_pdlp_partitioner_t for the meaning of each value. - distributed_pdlp_partitioner_t distributed_pdlp_partitioner{ - distributed_pdlp_partitioner_t::Auto}; + distributed_pdlp_partitioner_t distributed_pdlp_partitioner{distributed_pdlp_partitioner_t::Auto}; method_t method{method_t::Concurrent}; bool inside_mip{false}; // For concurrent termination diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index fad81e494c..35f2aaa83f 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -521,13 +521,13 @@ void distributed_compute_initial_primal_weight(multi_gpu_engine_t& eng pdlp_hyper_params_t const& hyper_params) { raft::common::nvtx::range scope("distributed_compute_initial_primal_weight"); - cuopt_expects(!hyper_params.initial_primal_weight_combined_bounds && - hyper_params.bound_objective_rescaling, - error_type_t::ValidationError, - "distributed_compute_initial_primal_weight: only the Stable3-shaped " - "short-circuit is supported (initial_primal_weight_combined_bounds=false " - "and bound_objective_rescaling=true). This should have been rejected " - "earlier in solve_lp_distributed_from_mps."); + cuopt_expects( + !hyper_params.initial_primal_weight_combined_bounds && hyper_params.bound_objective_rescaling, + error_type_t::ValidationError, + "distributed_compute_initial_primal_weight: only the Stable3-shaped " + "short-circuit is supported (initial_primal_weight_combined_bounds=false " + "and bound_objective_rescaling=true). This should have been rejected " + "earlier in solve_lp_distributed_from_mps."); const f_t h_primal_weight = f_t(1); auto* handle_ptr = master.get_handle_ptr(); @@ -545,37 +545,37 @@ void distributed_compute_initial_primal_weight(multi_gpu_engine_t& eng } // ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- -#define INSTANTIATE(F_TYPE) \ - template void distributed_bound_objective_rescaling( \ - multi_gpu_engine_t & engine, F_TYPE c_scaling_weight); \ - template void distributed_ruiz_inf_scaling( \ - multi_gpu_engine_t & engine, int num_iter, int n_global_vars); \ - template void distributed_pock_chambolle_scaling( \ - multi_gpu_engine_t & engine, F_TYPE alpha, int n_global_vars); \ - template void distributed_scaling(multi_gpu_engine_t & engine, \ - pdlp_hyper_params_t const& hyper_params, \ - int n_global_vars, \ - bool inside_mip); \ - template F_TYPE distributed_max_singular_value( \ - multi_gpu_engine_t & engine, \ - int n_global_cstrs, \ - int max_iterations, \ - F_TYPE tolerance); \ - template void distributed_compute_initial_step_size( \ - multi_gpu_engine_t & engine, \ - pdlp_solver_t & master, \ - pdlp_hyper_params_t const& hyper_params, \ - int n_global_cstrs, \ - F_TYPE scaling_factor, \ - int max_iterations, \ - F_TYPE tolerance); \ - template void distributed_compute_initial_primal_weight( \ - multi_gpu_engine_t & engine, \ - pdlp_solver_t & master, \ - pdlp_hyper_params_t const& hyper_params); \ - template void gather_potential_next_solutions_to_master( \ - multi_gpu_engine_t & engine, \ - pdhg_solver_t & master_pdhg, \ +#define INSTANTIATE(F_TYPE) \ + template void distributed_bound_objective_rescaling( \ + multi_gpu_engine_t & engine, F_TYPE c_scaling_weight); \ + template void distributed_ruiz_inf_scaling( \ + multi_gpu_engine_t & engine, int num_iter, int n_global_vars); \ + template void distributed_pock_chambolle_scaling( \ + multi_gpu_engine_t & engine, F_TYPE alpha, int n_global_vars); \ + template void distributed_scaling(multi_gpu_engine_t & engine, \ + pdlp_hyper_params_t const& hyper_params, \ + int n_global_vars, \ + bool inside_mip); \ + template F_TYPE distributed_max_singular_value( \ + multi_gpu_engine_t & engine, \ + int n_global_cstrs, \ + int max_iterations, \ + F_TYPE tolerance); \ + template void distributed_compute_initial_step_size( \ + multi_gpu_engine_t & engine, \ + pdlp_solver_t & master, \ + pdlp_hyper_params_t const& hyper_params, \ + int n_global_cstrs, \ + F_TYPE scaling_factor, \ + int max_iterations, \ + F_TYPE tolerance); \ + template void distributed_compute_initial_primal_weight( \ + multi_gpu_engine_t & engine, \ + pdlp_solver_t & master, \ + pdlp_hyper_params_t const& hyper_params); \ + template void gather_potential_next_solutions_to_master( \ + multi_gpu_engine_t & engine, \ + pdhg_solver_t & master_pdhg, \ rmm::device_uvector & master_reduced_cost); INSTANTIATE(double) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 5a09fff406..0b609f0947 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -105,8 +105,7 @@ struct multi_gpu_engine_t { Op op) { const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(in_tuples.size()) == nb && - static_cast(outs.size()) == nb && + cuopt_expects(static_cast(in_tuples.size()) == nb && static_cast(outs.size()) == nb && static_cast(sizes.size()) == nb, error_type_t::RuntimeError, "distributed_transform_bufs: in_tuples / outs / sizes must " @@ -120,16 +119,15 @@ struct multi_gpu_engine_t { // Wrapper: accessor form. Resolves each shard's cub input_tuple / output / // size via the provided accessors, then delegates to - // distributed_transform_bufs. + // distributed_transform_bufs. template void distributed_transform(std::tuple in_accessors, OutAccess out, SizeAccess sz, Op op) { - cuopt_expects(!shards.empty(), - error_type_t::RuntimeError, - "distributed_transform: engine has no shards"); + cuopt_expects( + !shards.empty(), error_type_t::RuntimeError, "distributed_transform: engine has no shards"); // Deduce per-shard tuple / output types from the accessors themselves so // the runtime vector element types match cub's expectations exactly. @@ -353,7 +351,7 @@ struct multi_gpu_engine_t { CUOPT_NCCL_TRY(ncclGroupStart()); for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); f_t* p = scalars[r].data_handle(); CUOPT_NCCL_TRY(ncclAllReduce(p, @@ -383,14 +381,14 @@ struct multi_gpu_engine_t { } // -------- Distributed dot / L2 norm ------------------------------------- - // Computes the dot product of two vectors for each shard. Returns the global result in out_scalars. + // Computes the dot product of two vectors for each shard. Returns the global result in + // out_scalars. void distributed_dot_bufs(std::vector> const& a_bufs, std::vector> const& b_bufs, std::vector> const& out_scalars) { const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(a_bufs.size()) == nb && - static_cast(b_bufs.size()) == nb && + cuopt_expects(static_cast(a_bufs.size()) == nb && static_cast(b_bufs.size()) == nb && static_cast(out_scalars.size()) == nb, error_type_t::RuntimeError, "distributed_dot_bufs: a_bufs / b_bufs / out_scalars must " diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 674b6477a1..20809b13e0 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3306,8 +3306,7 @@ void pdlp_solver_t::scale_problem() raft::common::nvtx::range fun_scope("pdlp_solver_t::scale_problem"); if (is_distributed_master()) { distributed_scaling(*multi_gpu_engine, settings_.hyper_params, primal_size_h_, inside_mip_); - } - else { + } else { initial_scaling_strategy_.scale_problem(); } } @@ -3545,12 +3544,11 @@ void pdlp_solver_t::compute_initial_primal_weight() raft::common::nvtx::range fun_scope("compute_initial_primal_weight"); if (is_distributed_master()) { - // Distributed dispatch: + // Distributed dispatch: // - short-circuit -> 1 // - primal/dual step sizes from master's current step_size_, seeding - // master + all shards, syncs) - distributed_compute_initial_primal_weight( - *multi_gpu_engine, *this, settings_.hyper_params); + // master + all shards, syncs) + distributed_compute_initial_primal_weight(*multi_gpu_engine, *this, settings_.hyper_params); return; } From 1cd851d902ba549057ec1d7c8437d03f44ab7ce3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 14:56:50 +0200 Subject: [PATCH 170/258] update comment in pdlp.cu --- cpp/src/pdlp/pdlp.cu | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 20809b13e0..e7da2de559 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2640,9 +2640,8 @@ optimization_problem_solution_t pdlp_solver_t::run_solver(co scale_problem(); create_spmv_op_plans(); - // Post-scaling cleanup is single-GPU only: distributed shards' cusparse - // views are set up per-shard and don't share the master's problem_ptr / - // op_problem_scaled_. + // mixed precision and cusparse structure redirection are not supported in distributed + // as memory footprint is not currently a bottleneck in distributed if (!settings_.use_distributed_pdlp) { // Update FP32 matrix copies for mixed precision SpMV after scaling pdhg_solver_.get_cusparse_view().update_mixed_precision_matrices(); From 3e4bae05140a680de99109c5b91151c82da57437 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 15:39:50 +0200 Subject: [PATCH 171/258] change sync call in distributed restart for engine sync call --- cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index 6679ff14a7..8abb2d8b61 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -893,7 +893,7 @@ void pdlp_restart_strategy_t::cupdlpx_restart( should_restart.begin(), should_restart.end(), [](int restarted) { return restarted == 1; }), "If any, all should be true"); - // Computing the deltas + // Computing the distributed deltas if (auto* engine = pdhg_solver.get_mgpu_engine()) { engine->for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; @@ -921,11 +921,10 @@ void pdlp_restart_strategy_t::cupdlpx_restart( return sp.get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(); }); + // Event-sync master's stream on every shard stream so the D2D copies + // below see the allreduce writes without a CPU-blocking sync. + engine->sync_await_shards(stream_view_); auto& s0 = *engine->shards[0]; - { - raft::device_setter guard(s0.device_id); - RAFT_CUDA_TRY(cudaStreamSynchronize(s0.stream.view().value())); - } raft::copy(last_restart_duality_gap_.primal_distance_traveled_.data(), s0.sub_pdlp->get_restart_strategy() .last_restart_duality_gap_.primal_distance_traveled_.data(), From 57713c80efb860e5e3d4450546b4857585d083df Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 16:05:23 +0200 Subject: [PATCH 172/258] =?UTF-8?q?cleaner=20cupdlpx=20restart=20distribut?= =?UTF-8?q?ed=20and=20added=20allreduce=5Fsum=5Finplace=5Fto=5Fmaster,=20p?= =?UTF-8?q?articularly=20cool=20=F0=9F=A7=9C=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=F0=9F=A7=9C=E2=80=8D=E2=99=80=EF=B8=8F=F0=9F=A7=9C=E2=80=8D?= =?UTF-8?q?=E2=99=80=EF=B8=8F=F0=9F=A7=9C=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=F0=9F=A7=9C=E2=80=8D=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../distributed_pdlp/multi_gpu_engine.hpp | 40 +++++++++ cpp/src/pdlp/pdlp.cu | 39 +++----- .../restart_strategy/pdlp_restart_strategy.cu | 89 ++++++++----------- .../pdlp_restart_strategy.cuh | 8 ++ 4 files changed, 100 insertions(+), 76 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 0b609f0947..ececa67dac 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -380,6 +380,42 @@ struct multi_gpu_engine_t { allreduce_sum_inplace_bufs(scalars); } + // Core: same as allreduce_sum_inplace_bufs, plus after the collective the + // value is D2D-copied from shard 0 into master_dst + void allreduce_sum_inplace_to_master_buf( + std::vector> const& shard_scalars, + raft::device_scalar_view master_dst, + rmm::cuda_stream_view master_stream) + { + allreduce_sum_inplace_bufs(shard_scalars); + if (shards.empty()) return; + sync_await_shards(master_stream); + auto& s0 = *shards[0]; + raft::device_setter guard(s0.device_id); + raft::copy(master_dst.data_handle(), shard_scalars[0].data_handle(), 1, master_stream); + } + + // Wrapper: applies the ptr_access lambda to each shard's sub_pdlp to build + // the per-shard scalar views and to master_pdlp_ to obtain the master + // destination, then delegates to allreduce_sum_inplace_to_master_buf. + template + void allreduce_sum_inplace_to_master(PtrAccess&& ptr_access, + rmm::cuda_stream_view master_stream) + { + cuopt_assert(master_pdlp_ != nullptr, + "allreduce_sum_inplace_to_master requires set_master(...) to have been called"); + std::vector> shard_scalars; + shard_scalars.reserve(shards.size()); + for (auto& s : shards) { + raft::device_setter guard(s->device_id); + shard_scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s->sub_pdlp))); + } + allreduce_sum_inplace_to_master_buf( + shard_scalars, + raft::make_device_scalar_view(ptr_access(*master_pdlp_)), + master_stream); + } + // -------- Distributed dot / L2 norm ------------------------------------- // Computes the dot product of two vectors for each shard. Returns the global result in // out_scalars. @@ -466,6 +502,10 @@ struct multi_gpu_engine_t { // (owns device-affine resources: handle, NCCL comm, RMM buffers). std::vector>> shards; + // Non-owning back-pointer to the master pdlp_solver_t. + pdlp_solver_t* master_pdlp_ = nullptr; + void set_master(pdlp_solver_t* m) { master_pdlp_ = m; } + // ===== Cross-stream synchronization events ===== // two different events // capture_*_event_ are used inside graph capture diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index e7da2de559..c065c7fba3 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -541,6 +541,10 @@ pdlp_solver_t::pdlp_solver_t( // ----- 6. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), mps, sub_pdlp_settings); + // Non-owning back-pointer so engine helpers (e.g. allreduce_sum_inplace_to_master) + // can apply a single pdlp_solver_t-shaped accessor lambda to master too. + multi_gpu_engine->set_master(this); + // Wire the engine into master's pdhg_solver_; shards keep mgpu_engine_ == nullptr. pdhg_solver_.set_multi_gpu_engine(&*multi_gpu_engine); @@ -2309,32 +2313,15 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte (void*)sub_pdlp.pdhg_solver_.get_potential_next_dual_solution().data())); } - multi_gpu_engine->allreduce_sum_inplace( - [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }); - multi_gpu_engine->allreduce_sum_inplace([](auto& sp) -> f_t* { - return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); - }); - multi_gpu_engine->allreduce_sum_inplace( - [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }); - - auto& s0 = *multi_gpu_engine->shards[0]; - { - raft::device_setter guard(s0.device_id); - RAFT_CUDA_TRY(cudaStreamSynchronize(s0.stream.view().value())); - } - auto& src_sp = s0.sub_pdlp->step_size_strategy_; - raft::copy(step_size_strategy_.get_interaction().data(), - src_sp.get_interaction().data(), - 1, - stream_view_); - raft::copy(step_size_strategy_.get_norm_squared_delta_primal().data(), - src_sp.get_norm_squared_delta_primal().data(), - 1, - stream_view_); - raft::copy(step_size_strategy_.get_norm_squared_delta_dual().data(), - src_sp.get_norm_squared_delta_dual().data(), - 1, - stream_view_); + multi_gpu_engine->allreduce_sum_inplace_to_master( + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }, + stream_view_); + multi_gpu_engine->allreduce_sum_inplace_to_master( + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); }, + stream_view_); + multi_gpu_engine->allreduce_sum_inplace_to_master( + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }, + stream_view_); } else { // Sync to make sure all previous cuSparse operations are finished before setting the // potential_next_dual_solution diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index 8abb2d8b61..c349914272 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -896,60 +896,25 @@ void pdlp_restart_strategy_t::cupdlpx_restart( // Computing the distributed deltas if (auto* engine = pdhg_solver.get_mgpu_engine()) { engine->for_each_shard([&](auto& shard) { - auto& sub = *shard.sub_pdlp; - auto& sub_rest = sub.get_restart_strategy(); - sub_rest.distance_squared_moved_from_last_restart_period( - sub.pdhg_solver_.get_potential_next_primal_solution(), - sub_rest.last_restart_duality_gap_.primal_solution_, - sub.pdhg_solver_.get_primal_tmp_resource(), - shard.rank_data.owned_var_size, - 1, - sub_rest.last_restart_duality_gap_.primal_distance_traveled_); - sub_rest.distance_squared_moved_from_last_restart_period( - sub.pdhg_solver_.get_potential_next_dual_solution(), - sub_rest.last_restart_duality_gap_.dual_solution_, - sub.pdhg_solver_.get_dual_tmp_resource(), - shard.rank_data.owned_cstr_size, - 1, - sub_rest.last_restart_duality_gap_.dual_distance_traveled_); - }); - - engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { - return sp.get_restart_strategy().last_restart_duality_gap_.primal_distance_traveled_.data(); - }); - engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { - return sp.get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(); + auto& sub = *shard.sub_pdlp; + sub.get_restart_strategy().primal_dual_distance_squared_moved_from_last_restart_period( + sub.pdhg_solver_, shard.rank_data.owned_var_size, shard.rank_data.owned_cstr_size); }); - // Event-sync master's stream on every shard stream so the D2D copies - // below see the allreduce writes without a CPU-blocking sync. - engine->sync_await_shards(stream_view_); - auto& s0 = *engine->shards[0]; - raft::copy(last_restart_duality_gap_.primal_distance_traveled_.data(), - s0.sub_pdlp->get_restart_strategy() - .last_restart_duality_gap_.primal_distance_traveled_.data(), - 1, - stream_view_); - raft::copy( - last_restart_duality_gap_.dual_distance_traveled_.data(), - s0.sub_pdlp->get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(), - 1, + // Reduce across all shards + engine->allreduce_sum_inplace_to_master( + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_restart_strategy().last_restart_duality_gap_.primal_distance_traveled_.data(); + }, + stream_view_); + engine->allreduce_sum_inplace_to_master( + [](pdlp_solver_t& sp) -> f_t* { + return sp.get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(); + }, stream_view_); } else { - distance_squared_moved_from_last_restart_period( - pdhg_solver.get_potential_next_primal_solution(), - last_restart_duality_gap_.primal_solution_, - pdhg_solver.get_primal_tmp_resource(), - primal_size_h_, - 1, - last_restart_duality_gap_.primal_distance_traveled_); - distance_squared_moved_from_last_restart_period( - pdhg_solver.get_potential_next_dual_solution(), - last_restart_duality_gap_.dual_solution_, - pdhg_solver.get_dual_tmp_resource(), - dual_size_h_, - 1, - last_restart_duality_gap_.dual_distance_traveled_); + primal_dual_distance_squared_moved_from_last_restart_period( + pdhg_solver, primal_size_h_, dual_size_h_); } auto view = make_cupdlpx_restart_view(last_restart_duality_gap_.primal_distance_traveled_, @@ -1364,6 +1329,30 @@ void pdlp_restart_strategy_t::distance_squared_moved_from_last_restart } } +template +void pdlp_restart_strategy_t:: + primal_dual_distance_squared_moved_from_last_restart_period( + pdhg_solver_t& pdhg_solver, i_t primal_size, i_t dual_size) +{ + cuopt_assert(!batch_mode_, + "primal_dual_distance_squared_moved_from_last_restart_period hard-codes stride=1 " + "and is not batch-mode safe; batch call sites should use the underlying functions directly"); + distance_squared_moved_from_last_restart_period( + pdhg_solver.get_potential_next_primal_solution(), + last_restart_duality_gap_.primal_solution_, + pdhg_solver.get_primal_tmp_resource(), + primal_size, + 1, + last_restart_duality_gap_.primal_distance_traveled_); + distance_squared_moved_from_last_restart_period( + pdhg_solver.get_potential_next_dual_solution(), + last_restart_duality_gap_.dual_solution_, + pdhg_solver.get_dual_tmp_resource(), + dual_size, + 1, + last_restart_duality_gap_.dual_distance_traveled_); +} + template __global__ void compute_distance_traveled_last_restart_kernel( const typename localized_duality_gap_container_t::view_t duality_gap_view, diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cuh b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cuh index 03f9350314..68ef7503a0 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cuh +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cuh @@ -234,6 +234,14 @@ class pdlp_restart_strategy_t { i_t stride, rmm::device_uvector& distance_moved); + // Convenience wrapper: applies distance_squared_moved_from_last_restart_period + // to both primal and dual potential_next solutions of pdhg_solver, writing + // into last_restart_duality_gap_.primal_distance_traveled_ and + // last_restart_duality_gap_.dual_distance_traveled_. + // Used for distributed PDLP mirroring clarity + void primal_dual_distance_squared_moved_from_last_restart_period( + pdhg_solver_t& pdhg_solver, i_t primal_size, i_t dual_size); + void compute_primal_gradient(localized_duality_gap_container_t& duality_gap, cusparse_view_t& cusparse_view); From 5f919155ba65cd0244bf3c388ceb79681bbecee6 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 16:06:00 +0200 Subject: [PATCH 173/258] style --- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp | 7 ++----- cpp/src/pdlp/pdlp.cu | 4 +++- .../pdlp/restart_strategy/pdlp_restart_strategy.cu | 12 ++++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index ececa67dac..86ee91c881 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -399,8 +399,7 @@ struct multi_gpu_engine_t { // the per-shard scalar views and to master_pdlp_ to obtain the master // destination, then delegates to allreduce_sum_inplace_to_master_buf. template - void allreduce_sum_inplace_to_master(PtrAccess&& ptr_access, - rmm::cuda_stream_view master_stream) + void allreduce_sum_inplace_to_master(PtrAccess&& ptr_access, rmm::cuda_stream_view master_stream) { cuopt_assert(master_pdlp_ != nullptr, "allreduce_sum_inplace_to_master requires set_master(...) to have been called"); @@ -411,9 +410,7 @@ struct multi_gpu_engine_t { shard_scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s->sub_pdlp))); } allreduce_sum_inplace_to_master_buf( - shard_scalars, - raft::make_device_scalar_view(ptr_access(*master_pdlp_)), - master_stream); + shard_scalars, raft::make_device_scalar_view(ptr_access(*master_pdlp_)), master_stream); } // -------- Distributed dot / L2 norm ------------------------------------- diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index c065c7fba3..19fb56e53b 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2317,7 +2317,9 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }, stream_view_); multi_gpu_engine->allreduce_sum_inplace_to_master( - [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); }, + [](auto& sp) -> f_t* { + return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); + }, stream_view_); multi_gpu_engine->allreduce_sum_inplace_to_master( [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }, diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index c349914272..f5a87d1d41 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -1330,13 +1330,13 @@ void pdlp_restart_strategy_t::distance_squared_moved_from_last_restart } template -void pdlp_restart_strategy_t:: - primal_dual_distance_squared_moved_from_last_restart_period( - pdhg_solver_t& pdhg_solver, i_t primal_size, i_t dual_size) +void pdlp_restart_strategy_t::primal_dual_distance_squared_moved_from_last_restart_period( + pdhg_solver_t& pdhg_solver, i_t primal_size, i_t dual_size) { - cuopt_assert(!batch_mode_, - "primal_dual_distance_squared_moved_from_last_restart_period hard-codes stride=1 " - "and is not batch-mode safe; batch call sites should use the underlying functions directly"); + cuopt_assert( + !batch_mode_, + "primal_dual_distance_squared_moved_from_last_restart_period hard-codes stride=1 " + "and is not batch-mode safe; batch call sites should use the underlying functions directly"); distance_squared_moved_from_last_restart_period( pdhg_solver.get_potential_next_primal_solution(), last_restart_duality_gap_.primal_solution_, From 8fb04ea32b29e2be69f8da7e5ca7fd9b80663f58 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Mon, 6 Jul 2026 16:26:56 +0200 Subject: [PATCH 174/258] skip transpose check in distributed construction --- cpp/src/pdlp/distributed_pdlp/shard.cu | 8 ++++---- cpp/src/pdlp/pdlp.cu | 12 +++++++++--- cpp/src/pdlp/pdlp.cuh | 12 +++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 1d122056d5..7511f75b5e 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -128,11 +128,11 @@ pdlp_shard_t::pdlp_shard_t(int device_id, handle.sync_stream(stream_view); // ---- 5. Build sub_pdlp (single-GPU mode). ---- + // is_distributed_sub_pdlp=true tells the ctor to skip the CSR/CSC transpose + // validity check as A and A_T here are two independent local slices, not transposes + // A has all the owned rows and A_T has all the owned columns. sub_pdlp = std::make_unique>( - *sub_problem, settings, /*is_legacy_batch_mode=*/false); - // The shard IS by construction a distributed sub-solver; mark it so any - // pdlp/pdhg/termination/restart codepath can query is_distributed_sub_pdlp() - sub_pdlp->set_distributed_sub_pdlp(); + *sub_problem, settings, /*is_legacy_batch_mode=*/false, /*is_distributed_sub_pdlp=*/true); // Inject this shard's unscaled buffers into op_problem_scaled (distributed // scaling runs later and will scale them). diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 19fb56e53b..ee7a5ece22 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -162,7 +162,8 @@ static size_t batch_size_handler(const pdlp_solver_settings_t& setting template pdlp_solver_t::pdlp_solver_t(mip::problem_t& op_problem, pdlp_solver_settings_t const& settings, - bool is_legacy_batch_mode) + bool is_legacy_batch_mode, + bool is_distributed_sub_pdlp) : original_batch_size_(batch_size_handler(settings)), climber_strategies_(original_batch_size_), batch_mode_(climber_strategies_.size() > 1), @@ -267,7 +268,8 @@ pdlp_solver_t::pdlp_solver_t(mip::problem_t& op_problem, reusable_device_scalar_value_0_{f_t(0.0), stream_view_}, batch_solution_to_return_{pdlp_termination_status_t::TimeLimit, stream_view_}, best_primal_solution_so_far{pdlp_termination_status_t::TimeLimit, stream_view_}, - inside_mip_{false} + inside_mip_{false}, + is_distributed_sub_pdlp_{is_distributed_sub_pdlp} { cuopt_expects(!(settings_.first_primal_feasible && settings_.all_primal_feasible), error_type_t::ValidationError, @@ -356,7 +358,11 @@ pdlp_solver_t::pdlp_solver_t(mip::problem_t& op_problem, best_primal_quality_so_far_.primal_objective = (op_problem_scaled_.maximize) ? -std::numeric_limits::infinity() : std::numeric_limits::infinity(); - op_problem.check_problem_representation(true, false); + // On a distributed sub-solver, op_problem.coefficients (owned rows) and + // op_problem.reverse_coefficients (owned cols) are two independent slices + // of the global matrix, not transposes of each other; skip that check. + op_problem.check_problem_representation(/*check_transposed=*/!is_distributed_sub_pdlp_, + /*empty=*/false); if (batch_mode_) { batch_solution_to_return_.get_additional_termination_informations().resize( diff --git a/cpp/src/pdlp/pdlp.cuh b/cpp/src/pdlp/pdlp.cuh index bcad89ca63..492f03c610 100644 --- a/cpp/src/pdlp/pdlp.cuh +++ b/cpp/src/pdlp/pdlp.cuh @@ -59,10 +59,13 @@ class pdlp_solver_t { * * @param[in] op_problem An mip::problem_t object with a * representation of a linear program + * @param[in] is_distributed_sub_pdlp true when constructed as a shard of a + * distributed solve. */ pdlp_solver_t(mip::problem_t& op_problem, pdlp_solver_settings_t const& settings, - bool is_batch_mode = false); + bool is_batch_mode = false, + bool is_distributed_sub_pdlp = false); // Distributed Solver Constructor pdlp_solver_t(mip::problem_t& placeholder_problem, @@ -230,9 +233,8 @@ class pdlp_solver_t { // Single-GPU PDLP reports false. bool is_distributed_master() const { return multi_gpu_engine.has_value(); } - // Marked true by the owning pdlp_shard_t right after this pdlp_solver_t is - // constructed as a per-shard sub-solver. - void set_distributed_sub_pdlp(bool value = true) { is_distributed_sub_pdlp_ = value; } + // True when this pdlp_solver_t was constructed as a per-shard sub-solver + // of a distributed solve (set once at construction via the ctor arg). bool is_distributed_sub_pdlp() const { return is_distributed_sub_pdlp_; } private: @@ -293,7 +295,7 @@ class pdlp_solver_t { primal_quality_adapter_t best_primal_quality_so_far_; // Flag to indicate if solver is being called from MIP. No logging is done in this case. bool inside_mip_{false}; - // See set_distributed_sub_pdlp() / is_distributed_sub_pdlp() above. + // See is_distributed_sub_pdlp() above. Initialized from ctor arg. bool is_distributed_sub_pdlp_{false}; }; From 3b740b5bffc6c355aeb4838093cff806da401cdf Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 7 Jul 2026 11:19:42 +0200 Subject: [PATCH 175/258] deleted reset_scaling state for a more streamlined distributed scaling --- .../distributed_algorithms.cu | 10 +++--- .../initial_scaling.cu | 32 ------------------- .../initial_scaling.cuh | 10 ++++-- cpp/src/pdlp/pdlp.cu | 14 +++++--- 4 files changed, 22 insertions(+), 44 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 35f2aaa83f..6c853a47b4 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -243,12 +243,14 @@ void distributed_scaling(multi_gpu_engine_t& engine, { raft::common::nvtx::range scope("distributed_scaling"); - // 1) Reset per-shard scaling state (cumulative row/col scalings back to 1), - // then sync so subsequent scaling passes start from a clean slate. + // 1) Grow per-shard iteration_* scratch back to full size (the shard ctor + // released it after its no-op local pre-scaling pass) engine.for_each_shard([](auto& shard) { - shard.sub_pdlp->get_initial_scaling_strategy().reset_scaling_state_for_distributed(); + auto& scaling = shard.sub_pdlp->get_initial_scaling_strategy(); + auto& op = shard.sub_pdlp->get_op_problem_scaled(); + scaling.get_iteration_variable_scaling().resize(op.n_variables, shard.stream.view()); + scaling.get_iteration_constraint_matrix_scaling().resize(op.n_constraints, shard.stream.view()); }); - engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); // 2) Matrix scaling passes populate the cumulative row/col scalings on // every shard. Each pass keeps the halo copies refreshed internally. diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 1f2d77d8fd..6eabddf19c 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -131,10 +131,6 @@ void pdlp_initial_scaling_strategy_t::compute_scaling_vectors( { raft::common::nvtx::range fun_scope("compute_scaling_vectors"); - // Skip scaling entirely for a shape-0 problem (distributed PDLP builds the - // master pdlp_solver_t from a shape-0 placeholder) - if (primal_size_h_ == 0 || dual_size_h_ == 0) return; - if (hyper_params_.do_ruiz_scaling) { ruiz_inf_scaling(number_of_ruiz_iterations); } if (hyper_params_.do_pock_chambolle_scaling) { pock_chambolle_scaling(alpha); } } @@ -326,34 +322,6 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_local() stream_view_); } -template -void pdlp_initial_scaling_strategy_t::reset_scaling_state_for_distributed() -{ - if (primal_size_h_ == 0 || dual_size_h_ == 0) return; - - // Re-allocate the iteration vectors the ctor shrank to 0 and zero them. - iteration_constraint_matrix_scaling_.resize(static_cast(dual_size_h_), stream_view_); - iteration_variable_scaling_.resize(static_cast(primal_size_h_), stream_view_); - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); - - // Reset cumulative scaling + rescaling to identity (the ctor's stray - // Pock-Chambolle pass and shard.cu's set_cummulative_scaling left these in - // an arbitrary state; distributed scaling recomputes from a clean slate). - thrust::fill(handle_ptr_->get_thrust_policy(), - cummulative_constraint_matrix_scaling_.begin(), - cummulative_constraint_matrix_scaling_.end(), - f_t(1)); - thrust::fill(handle_ptr_->get_thrust_policy(), - cummulative_variable_scaling_.begin(), - cummulative_variable_scaling_.end(), - f_t(1)); - set_h_bound_rescaling(f_t(1)); - set_h_objective_rescaling(f_t(1)); -} - template void pdlp_initial_scaling_strategy_t::ruiz_inf_scaling(i_t number_of_ruiz_iterations) { diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 7f7bf3c139..0599121114 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -133,10 +133,14 @@ class pdlp_initial_scaling_strategy_t { void ruiz_iter_local(); // Shard-local end-to-end Pock-Chambolle pass. Exposed for distributed PDLP: void pock_chambolle_scaling(f_t alpha); + // Iteration_* scratch buffers used by ruiz_iter_local / + // pock_chambolle_scaling. Exposed mutably so distributed PDLP can grow + // them back to full size after the ctor's release (see distributed_scaling). rmm::device_uvector& get_iteration_variable_scaling() { return iteration_variable_scaling_; } - - // Restore the clean pre-scaling state for the distributed path. - void reset_scaling_state_for_distributed(); + rmm::device_uvector& get_iteration_constraint_matrix_scaling() + { + return iteration_constraint_matrix_scaling_; + } /** * @brief Gets the device-side view (with raw pointers), for ease of access diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index ee7a5ece22..6fc732fd21 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -538,11 +538,15 @@ pdlp_solver_t::pdlp_solver_t( std::chrono::duration(rank_data_t1 - rank_data_t0).count()); // ----- 5. Per-shard settings ----- - pdlp_solver_settings_t sub_pdlp_settings = settings; - sub_pdlp_settings.num_gpus = 1; - sub_pdlp_settings.distributed_pdlp_num_gpus = 1; - sub_pdlp_settings.hyper_params.default_l_inf_ruiz_iterations = 0; - sub_pdlp_settings.hyper_params.default_alpha_pock_chambolle_rescaling = 0.0; + pdlp_solver_settings_t sub_pdlp_settings = settings; + sub_pdlp_settings.num_gpus = 1; + sub_pdlp_settings.distributed_pdlp_num_gpus = 1; + // Disable automatic ruiz and pock-chambolle in intial_scaling ctor, as they need to be computed + // in distributed_scale_problem + // no need to disable bound_objective_rescaling as it does not get computed in initial_scaling_t + // ctor + sub_pdlp_settings.hyper_params.do_ruiz_scaling = false; + sub_pdlp_settings.hyper_params.do_pock_chambolle_scaling = false; // ----- 6. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), mps, sub_pdlp_settings); From 7b60523cec14d1af6d2654ddc50822cdf4e2ce11 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 7 Jul 2026 11:48:48 +0200 Subject: [PATCH 176/258] cleaned initial_scaling a bit --- .../initial_scaling.cu | 109 +++++++++--------- cpp/src/pdlp/pdlp.cu | 3 +- 2 files changed, 54 insertions(+), 58 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 6eabddf19c..c913b20eae 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -172,57 +172,6 @@ void pdlp_initial_scaling_strategy_t::bound_objective_rescaling() h_objective_rescaling_ = cuopt::host_copy(objective_rescaling_, stream_view_); } -// Apply the already-published bound_rescaling_ / objective_rescaling_ device -// vectors to the scaled problem's constraint bounds, variable bounds, and -// objective. Extracted from scale_problem() so distributed_bound_objective_rescaling -// can share the exact same three multiplies (both call sites go through this -// helper — no duplicated multiply loops). -template -void pdlp_initial_scaling_strategy_t::apply_bound_objective_rescaling_to_problem() -{ - using f_t2 = typename type_2::type; - - cub::DeviceTransform::Transform( - cuda::std::make_tuple(op_problem_scaled_.constraint_lower_bounds.data(), - op_problem_scaled_.constraint_upper_bounds.data(), - batch_wrapped_container(bound_rescaling_, dual_size_h_)), - thrust::make_zip_iterator(op_problem_scaled_.constraint_lower_bounds.data(), - op_problem_scaled_.constraint_upper_bounds.data()), - op_problem_scaled_.constraint_upper_bounds.size(), - [] __device__(f_t constraint_lower_bound, - f_t constraint_upper_bound, - f_t bound_rescaling) -> thrust::tuple { - return {constraint_lower_bound * bound_rescaling, constraint_upper_bound * bound_rescaling}; - }, - stream_view_.value()); - - // In batch mode we don't scale the variable bounds (here) because they are shared across - // climbers. While the variable bounds are the same across climbers, there can be different - // bound rescaling factors for each climber. One solution would be to have per climber variable - // bounds but its costly from a memory perspective and from a memory bandwidth perspective. - // Since the variable bounds are the same across climbers but only the scaling factor changes, - // we pass the scaling factor to PDHG later. In PDHG we act the (almost fully) scaled variable - // bounds and add this missing scaling factor. - if (original_batch_size_ == 1) { - cub::DeviceTransform::Transform( - op_problem_scaled_.variable_bounds.data(), - op_problem_scaled_.variable_bounds.data(), - op_problem_scaled_.variable_bounds.size(), - [bound_rescaling = bound_rescaling_.data()] __device__(f_t2 variable_bounds) -> f_t2 { - return {variable_bounds.x * *bound_rescaling, variable_bounds.y * *bound_rescaling}; - }, - stream_view_); - } - - cub::DeviceTransform::Transform( - cuda::std::make_tuple(op_problem_scaled_.objective_coefficients.data(), - batch_wrapped_container(objective_rescaling_, primal_size_h_)), - op_problem_scaled_.objective_coefficients.data(), - op_problem_scaled_.objective_coefficients.size(), - cuda::std::multiplies{}, - stream_view_.value()); -} - // Row inf-norm of the scaled matrix, over the row-major matrix: each row is // reduced from its own nonzeros. (Owns the complete row in distributed PDLP.) template @@ -277,11 +226,6 @@ __global__ void inf_norm_col_kernel( template void pdlp_initial_scaling_strategy_t::ruiz_iter_local() { - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); - // Inf-norm over rows (owned rows, from row-major A) and columns (owned // columns, from A_T). Split into two kernels so the distributed path can // touch only owned entries. @@ -320,6 +264,10 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_local() primal_size_h_, a_divides_sqrt_b_bounded(), stream_view_); + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); } template @@ -691,6 +639,55 @@ void pdlp_initial_scaling_strategy_t::apply_cummulative_scaling_to_pro } } +// Apply the already-published bound_rescaling_ / objective_rescaling_ device +// vectors to the scaled problem's constraint bounds, variable bounds, and +// objective. Used in both distributed and non-distributed PDLP. +template +void pdlp_initial_scaling_strategy_t::apply_bound_objective_rescaling_to_problem() +{ + using f_t2 = typename type_2::type; + + cub::DeviceTransform::Transform( + cuda::std::make_tuple(op_problem_scaled_.constraint_lower_bounds.data(), + op_problem_scaled_.constraint_upper_bounds.data(), + batch_wrapped_container(bound_rescaling_, dual_size_h_)), + thrust::make_zip_iterator(op_problem_scaled_.constraint_lower_bounds.data(), + op_problem_scaled_.constraint_upper_bounds.data()), + op_problem_scaled_.constraint_upper_bounds.size(), + [] __device__(f_t constraint_lower_bound, + f_t constraint_upper_bound, + f_t bound_rescaling) -> thrust::tuple { + return {constraint_lower_bound * bound_rescaling, constraint_upper_bound * bound_rescaling}; + }, + stream_view_.value()); + + // In batch mode we don't scale the variable bounds (here) because they are shared across + // climbers. While the variable bounds are the same across climbers, there can be different + // bound rescaling factors for each climber. One solution would be to have per climber variable + // bounds but its costly from a memory perspective and from a memory bandwidth perspective. + // Since the variable bounds are the same across climbers but only the scaling factor changes, + // we pass the scaling factor to PDHG later. In PDHG we act the (almost fully) scaled variable + // bounds and add this missing scaling factor. + if (original_batch_size_ == 1) { + cub::DeviceTransform::Transform( + op_problem_scaled_.variable_bounds.data(), + op_problem_scaled_.variable_bounds.data(), + op_problem_scaled_.variable_bounds.size(), + [bound_rescaling = bound_rescaling_.data()] __device__(f_t2 variable_bounds) -> f_t2 { + return {variable_bounds.x * *bound_rescaling, variable_bounds.y * *bound_rescaling}; + }, + stream_view_); + } + + cub::DeviceTransform::Transform( + cuda::std::make_tuple(op_problem_scaled_.objective_coefficients.data(), + batch_wrapped_container(objective_rescaling_, primal_size_h_)), + op_problem_scaled_.objective_coefficients.data(), + op_problem_scaled_.objective_coefficients.size(), + cuda::std::multiplies{}, + stream_view_.value()); +} + template void pdlp_initial_scaling_strategy_t::scale_problem() { diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 6fc732fd21..5158b286a2 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -543,10 +543,9 @@ pdlp_solver_t::pdlp_solver_t( sub_pdlp_settings.distributed_pdlp_num_gpus = 1; // Disable automatic ruiz and pock-chambolle in intial_scaling ctor, as they need to be computed // in distributed_scale_problem - // no need to disable bound_objective_rescaling as it does not get computed in initial_scaling_t - // ctor sub_pdlp_settings.hyper_params.do_ruiz_scaling = false; sub_pdlp_settings.hyper_params.do_pock_chambolle_scaling = false; + sub_pdlp_settings.hyper_params.bound_objective_rescaling = false; // ----- 6. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), mps, sub_pdlp_settings); From c1d7e9f07e5534d89449d7d8fe660ceb18b0bcf3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 7 Jul 2026 13:39:01 +0200 Subject: [PATCH 177/258] style --- cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index c913b20eae..c5994bfc71 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -265,9 +265,9 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_local() a_divides_sqrt_b_bounded(), stream_view_); RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); + iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); + iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); } template From 41ebbbc0322e1218669cf776fa1a653607e6b0ec Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 7 Jul 2026 14:38:58 +0200 Subject: [PATCH 178/258] updated cuopt_cli to actually not materialize the full problem on gpu --- cpp/cuopt_cli.cpp | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 6e7058117e..ccdf4ab495 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -139,6 +139,33 @@ int run_single_file(const std::string& file_path, std::make_unique>(); } + // Distributed PDLP is used for large problems that don't fit on a single GPU. + // We need to debranch before the problem_interface is created and tries to materialize the problem in device memory. + if (settings.get_pdlp_settings().use_distributed_pdlp) { + cuopt::cuopt_expects( + handle_ptr != nullptr, + cuopt::error_type_t::ValidationError, + "Distributed PDLP requires the GPU memory backend; no GPU handle is available for the " + "selected memory backend."); + cuopt::cuopt_expects( + !solve_relaxation, + cuopt::error_type_t::ValidationError, + "Solving the LP relaxation is not allowed for distributed PDLP."); + cuopt::cuopt_expects( + !initial_solution_file.empty(), + cuopt::error_type_t::ValidationError, + "Initial solution file is not allowed for distributed PDLP."); + const auto& var_type_chars = mps_data_model.get_variable_types(); + cuopt::cuopt_expects( + std::none_of( + var_type_chars.begin(), var_type_chars.end(), [](char t) { return t == 'I' || t == 'B'; }), + cuopt::error_type_t::ValidationError, + "Distributed PDLP does not support mixed integer problems."); + auto solution = + cuopt::mathematical_optimization::solve_lp(handle_ptr.get(), mps_data_model, settings.get_pdlp_settings()); + return solution; + } + cuopt::mathematical_optimization::populate_from_mps_data_model(problem_interface.get(), mps_data_model); @@ -178,20 +205,11 @@ int run_single_file(const std::string& file_path, auto solution = cuopt::mathematical_optimization::solve_mip(problem_interface.get(), mip_settings); } else { + // Distributed PDLP was handled by the early-exit branch above; this + // path is always single-GPU LP going through problem_interface. auto& lp_settings = settings.get_pdlp_settings(); - - if (lp_settings.use_distributed_pdlp) { - cuopt::cuopt_expects( - handle_ptr != nullptr, - cuopt::error_type_t::ValidationError, - "Distributed PDLP requires the GPU memory backend; no GPU handle is available for the " - "selected memory backend."); - auto solution = - cuopt::mathematical_optimization::solve_lp(handle_ptr.get(), mps_data_model, lp_settings); - } else { - auto solution = - cuopt::mathematical_optimization::solve_lp(problem_interface.get(), lp_settings); - } + auto solution = + cuopt::mathematical_optimization::solve_lp(problem_interface.get(), lp_settings); } } catch (const std::exception& e) { fprintf(stderr, "cuopt_cli error: %s\n", e.what()); From 2f36b83694a18b3b0ebd4d65513fa44f715e4aff Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 7 Jul 2026 14:39:19 +0200 Subject: [PATCH 179/258] style --- cpp/cuopt_cli.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index ccdf4ab495..0fe9f14b6d 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -140,29 +140,28 @@ int run_single_file(const std::string& file_path, } // Distributed PDLP is used for large problems that don't fit on a single GPU. - // We need to debranch before the problem_interface is created and tries to materialize the problem in device memory. + // We need to debranch before the problem_interface is created and tries to materialize the + // problem in device memory. if (settings.get_pdlp_settings().use_distributed_pdlp) { cuopt::cuopt_expects( handle_ptr != nullptr, cuopt::error_type_t::ValidationError, "Distributed PDLP requires the GPU memory backend; no GPU handle is available for the " "selected memory backend."); - cuopt::cuopt_expects( - !solve_relaxation, - cuopt::error_type_t::ValidationError, - "Solving the LP relaxation is not allowed for distributed PDLP."); - cuopt::cuopt_expects( - !initial_solution_file.empty(), - cuopt::error_type_t::ValidationError, - "Initial solution file is not allowed for distributed PDLP."); + cuopt::cuopt_expects(!solve_relaxation, + cuopt::error_type_t::ValidationError, + "Solving the LP relaxation is not allowed for distributed PDLP."); + cuopt::cuopt_expects(!initial_solution_file.empty(), + cuopt::error_type_t::ValidationError, + "Initial solution file is not allowed for distributed PDLP."); const auto& var_type_chars = mps_data_model.get_variable_types(); cuopt::cuopt_expects( std::none_of( var_type_chars.begin(), var_type_chars.end(), [](char t) { return t == 'I' || t == 'B'; }), cuopt::error_type_t::ValidationError, "Distributed PDLP does not support mixed integer problems."); - auto solution = - cuopt::mathematical_optimization::solve_lp(handle_ptr.get(), mps_data_model, settings.get_pdlp_settings()); + auto solution = cuopt::mathematical_optimization::solve_lp( + handle_ptr.get(), mps_data_model, settings.get_pdlp_settings()); return solution; } From ad1f9aa9aaa9d05c96e9a2a1b1c3257b2d61e3e2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 7 Jul 2026 14:41:11 +0200 Subject: [PATCH 180/258] return 0 in cuopt cli ! --- cpp/cuopt_cli.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 0fe9f14b6d..ac59a93281 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -162,7 +162,7 @@ int run_single_file(const std::string& file_path, "Distributed PDLP does not support mixed integer problems."); auto solution = cuopt::mathematical_optimization::solve_lp( handle_ptr.get(), mps_data_model, settings.get_pdlp_settings()); - return solution; + return 0; } cuopt::mathematical_optimization::populate_from_mps_data_model(problem_interface.get(), From 94b8be09e27134e5394e031eb8f57a30e26ec1ef Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Tue, 7 Jul 2026 14:46:03 +0200 Subject: [PATCH 181/258] use device setters in cuoptt_cli --- cpp/cuopt_cli.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index ac59a93281..9b923df18f 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -467,11 +467,10 @@ int main(int argc, char* argv[]) memory_resources.reserve(provisioned_gpus); for (int i = 0; i < provisioned_gpus; ++i) { - RAFT_CUDA_TRY(cudaSetDevice(i)); + raft::device_setter guard(i); memory_resources.emplace_back(); rmm::mr::set_per_device_resource(rmm::cuda_device_id{i}, memory_resources.back()); } - RAFT_CUDA_TRY(cudaSetDevice(0)); } return run_single_file(file_name, initial_solution_file, solve_relaxation, settings); From 47198855c2f3efc562c8a47067ac50df0a776a06 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 11:46:20 +0200 Subject: [PATCH 182/258] fixed initial_scaling --- .../initial_scaling_strategy/initial_scaling.cu | 13 +++++++++---- cpp/src/pdlp/pdlp.cu | 6 +++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index c5994bfc71..468138af30 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -131,6 +131,10 @@ void pdlp_initial_scaling_strategy_t::compute_scaling_vectors( { raft::common::nvtx::range fun_scope("compute_scaling_vectors"); + // Skip scaling entirely for a shape-0 problem (distributed PDLP builds the + // master pdlp_solver_t from a shape-0 placeholder) + if (primal_size_h_ == 0 || dual_size_h_ == 0) return; + if (hyper_params_.do_ruiz_scaling) { ruiz_inf_scaling(number_of_ruiz_iterations); } if (hyper_params_.do_pock_chambolle_scaling) { pock_chambolle_scaling(alpha); } } @@ -226,6 +230,11 @@ __global__ void inf_norm_col_kernel( template void pdlp_initial_scaling_strategy_t::ruiz_iter_local() { + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); + RAFT_CUDA_TRY(cudaMemsetAsync( + iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); + // Inf-norm over rows (owned rows, from row-major A) and columns (owned // columns, from A_T). Split into two kernels so the distributed path can // touch only owned entries. @@ -264,10 +273,6 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_local() primal_size_h_, a_divides_sqrt_b_bounded(), stream_view_); - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); - RAFT_CUDA_TRY(cudaMemsetAsync( - iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); } template diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 5158b286a2..c825c82dcd 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -541,11 +541,11 @@ pdlp_solver_t::pdlp_solver_t( pdlp_solver_settings_t sub_pdlp_settings = settings; sub_pdlp_settings.num_gpus = 1; sub_pdlp_settings.distributed_pdlp_num_gpus = 1; - // Disable automatic ruiz and pock-chambolle in intial_scaling ctor, as they need to be computed - // in distributed_scale_problem + // Disable automatic ruiz and pock-chambolle in the initial_scaling ctor: the + // distributed pipeline computes them via distributed_scaling using the + // GLOBAL problem. sub_pdlp_settings.hyper_params.do_ruiz_scaling = false; sub_pdlp_settings.hyper_params.do_pock_chambolle_scaling = false; - sub_pdlp_settings.hyper_params.bound_objective_rescaling = false; // ----- 6. Construct the engine: NCCL comms + per-shard pdlp_solver_t ----- multi_gpu_engine.emplace(std::move(sub_pdlp_rank_data), mps, sub_pdlp_settings); From 2daf4ba69241e04d6ce8715aadd0af5aeecd79a3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 11:47:20 +0200 Subject: [PATCH 183/258] added _MG do distributed test --- cpp/tests/linear_programming/pdlp_test.cu | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 570b412383..452fc40d7f 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -181,19 +181,19 @@ void expect_distributed_matches_base(raft::handle_t const& handle, } // namespace -TEST(pdlp_class, distributed_parity_afiro) +TEST(pdlp_class, distributed_parity_afiro_MG) { const raft::handle_t handle{}; expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); } -TEST(pdlp_class, distributed_parity_neos3) +TEST(pdlp_class, distributed_parity_neos3_MG) { const raft::handle_t handle{}; expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); } -TEST(pdlp_class, distributed_parity_a2864) +TEST(pdlp_class, distributed_parity_a2864_MG) { const raft::handle_t handle{}; expect_distributed_matches_base(handle, "linear_programming/a2864/a2864.mps"); From bb715696803cad59e0cc8dcaf25e82bd3829e987 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 12:16:16 +0200 Subject: [PATCH 184/258] cleaned rank_data.hpp --- cpp/src/pdlp/distributed_pdlp/rank_data.hpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp index 5e55f76480..ccc0430aa8 100644 --- a/cpp/src/pdlp/distributed_pdlp/rank_data.hpp +++ b/cpp/src/pdlp/distributed_pdlp/rank_data.hpp @@ -9,7 +9,7 @@ #include namespace cuopt::mathematical_optimization::pdlp { -// Pure data class representing most of the distributed data needed for operatiosn +// Pure data class representing most of the distributed data needed for mGPU operatiosn template struct rank_data_t { rank_data_t(std::size_t nb_parts) @@ -27,11 +27,11 @@ struct rank_data_t { i_t owned_cstr_size{0}; i_t total_cstr_size{0}; - // === Ownership === + // === Variable and Constraint indices owned by this shard, in global problem indices === std::vector owned_var_indices; std::vector owned_cstr_indices; - // === Send plan: per peer, indices to gather + send === + // === Send plan: each element is a vector of indices to send to associated peer === std::vector> var_send_per_peer; std::vector> cstr_send_per_peer; @@ -42,8 +42,10 @@ struct rank_data_t { std::vector cstr_recv_offsets; // === Mappings === + // global_to_local_* : full global problem indices to local shard problem indices std::unordered_map global_to_local_var; std::unordered_map global_to_local_cstr; + // local_to_global_* : local shard problem indices to full global problem indices std::vector local_to_global_var; std::vector local_to_global_cstr; From 31e627cd66bf4fe25f7b45c5c21ca719d0cec343 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 12:54:04 +0200 Subject: [PATCH 185/258] actually use _MG_TEST * --- cpp/tests/linear_programming/pdlp_test.cu | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 452fc40d7f..6517ded1d8 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -181,19 +181,19 @@ void expect_distributed_matches_base(raft::handle_t const& handle, } // namespace -TEST(pdlp_class, distributed_parity_afiro_MG) +TEST(pdlp_class, distributed_parity_afiro_MG_TEST) { const raft::handle_t handle{}; expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); } -TEST(pdlp_class, distributed_parity_neos3_MG) +TEST(pdlp_class, distributed_parity_neos3_MG_TEST) { const raft::handle_t handle{}; expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); } -TEST(pdlp_class, distributed_parity_a2864_MG) +TEST(pdlp_class, distributed_parity_a2864_MG_TEST) { const raft::handle_t handle{}; expect_distributed_matches_base(handle, "linear_programming/a2864/a2864.mps"); From fac853e97202319f4dcf7ff21a5d80d7956da6ee Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 13:14:37 +0200 Subject: [PATCH 186/258] moved paretitoin_loader to distributed_utils, and removed surrounding struct --- cpp/src/pdlp/CMakeLists.txt | 2 +- ...rtition_loader.cu => distributed_utils.cu} | 17 ++++++++-- .../distributed_pdlp/distributed_utils.hpp | 29 +++++++++++++++++ .../distributed_pdlp/partition_loader.hpp | 32 ------------------- cpp/src/pdlp/pdlp.cu | 24 +++++++------- cpp/tests/linear_programming/pdlp_test.cu | 2 +- 6 files changed, 57 insertions(+), 49 deletions(-) rename cpp/src/pdlp/distributed_pdlp/{partition_loader.cu => distributed_utils.cu} (94%) create mode 100644 cpp/src/pdlp/distributed_pdlp/distributed_utils.hpp delete mode 100644 cpp/src/pdlp/distributed_pdlp/partition_loader.hpp diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 1da0028eea..2f90f94872 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -32,7 +32,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/shard.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/multi_gpu_engine.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/distributed_algorithms.cu - ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partition_loader.cu + ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/distributed_utils.cu ${CMAKE_CURRENT_SOURCE_DIR}/distributed_pdlp/partitioner.cpp ) diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu b/cpp/src/pdlp/distributed_pdlp/distributed_utils.cu similarity index 94% rename from cpp/src/pdlp/distributed_pdlp/partition_loader.cu rename to cpp/src/pdlp/distributed_pdlp/distributed_utils.cu index 91be56e95d..c7c746449f 100644 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_utils.cu @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include +#include #include @@ -13,7 +13,7 @@ namespace cuopt::mathematical_optimization::pdlp { template -std::vector> partition_loader_t::create_rank_data_from_parts( +std::vector> create_rank_data_from_parts( const std::vector& parts, const std::vector& A_row_offsets, const std::vector& A_col_indices, @@ -247,6 +247,17 @@ std::vector> partition_loader_t::create_rank_dat return rank_data; } -template struct partition_loader_t; +template std::vector> create_rank_data_from_parts( + const std::vector& parts, + const std::vector& A_row_offsets, + const std::vector& A_col_indices, + const std::vector& A_values, + const std::vector& A_t_row_offsets, + const std::vector& A_t_col_indices, + const std::vector& A_t_values, + int nb_parts, + int nb_cstr, + int nb_vars, + int nnz); } // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_utils.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_utils.hpp new file mode 100644 index 0000000000..a96a5178f9 --- /dev/null +++ b/cpp/src/pdlp/distributed_pdlp/distributed_utils.hpp @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +namespace cuopt::mathematical_optimization::pdlp { + +// Slices the data to prepare a split from graph partitioning with halo communication. +template +std::vector> create_rank_data_from_parts( + const std::vector& parts, + const std::vector& A_row_offsets, + const std::vector& A_col_indices, + const std::vector& A_values, + const std::vector& A_t_row_offsets, + const std::vector& A_t_col_indices, + const std::vector& A_t_values, + i_t nb_parts, + i_t nb_cstr, + i_t nb_vars, + i_t nnz); + +} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp b/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp deleted file mode 100644 index b88e9c3225..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/partition_loader.hpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#include -#include - -namespace cuopt::mathematical_optimization::pdlp { - -template -struct partition_loader_t { - // Slices the data to prepare a split from graph partitioning with halo communication - static std::vector> create_rank_data_from_parts( - const std::vector& parts, - const std::vector& A_row_offsets, - const std::vector& A_col_indices, - const std::vector& A_values, - const std::vector& A_t_row_offsets, - const std::vector& A_t_col_indices, - const std::vector& A_t_values, - i_t nb_parts, - i_t nb_cstr, - i_t nb_vars, - i_t nnz); -}; - -} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index c825c82dcd..081773acb9 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -11,7 +11,7 @@ #include #include -#include +#include #include #include #include @@ -522,17 +522,17 @@ pdlp_solver_t::pdlp_solver_t( settings.distributed_pdlp_num_gpus); auto rank_data_t0 = std::chrono::high_resolution_clock::now(); std::vector> sub_pdlp_rank_data = - partition_loader_t::create_rank_data_from_parts(parts, - h_A_row_offsets, - h_A_col_indices, - h_A_values, - h_A_t_row_offsets, - h_A_t_col_indices, - h_A_t_values, - settings.distributed_pdlp_num_gpus, - n_cstr, - n_vars, - nnz); + create_rank_data_from_parts(parts, + h_A_row_offsets, + h_A_col_indices, + h_A_values, + h_A_t_row_offsets, + h_A_t_col_indices, + h_A_t_values, + settings.distributed_pdlp_num_gpus, + n_cstr, + n_vars, + nnz); auto rank_data_t1 = std::chrono::high_resolution_clock::now(); CUOPT_LOG_INFO("distributed_pdlp: rank_data build done in %.3f s", std::chrono::duration(rank_data_t1 - rank_data_t0).count()); diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 6517ded1d8..ae39923fab 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include #include From 38b56784e7fd968f0d56855549422b9e4757016f Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 17:37:50 +0200 Subject: [PATCH 187/258] created dedicated presolve for op_problem to ensure no regression --- .../presolve/third_party_presolve.cpp | 520 ++++++++++++++++-- .../presolve/third_party_presolve.hpp | 3 +- 2 files changed, 484 insertions(+), 39 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index cfb429e016..6f1fffcf22 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -52,8 +52,9 @@ namespace cuopt::mathematical_optimization::mip { // Host-only gather + input normalisation for PSLP from an mps_data_model. -// Single source of truth for PSLP input construction — the op_problem path -// reaches this via op_problem_to_mps_data_model first. +// Used by the mps-facing entry point (apply_presolve_from_mps_data). The +// op_problem-facing entry goes through build_pslp_host_arrays_from_op_problem +// instead (direct D->H, no mps roundtrip). template pslp_input_t build_pslp_host_arrays_from_mps_data( const io::mps_data_model_t& mps, bool maximize) @@ -110,8 +111,9 @@ pslp_input_t build_pslp_host_arrays_from_mps_data( } // Host-only gather + papilo::Problem construction from an mps_data_model. -// Single source of truth for papilo input construction, the op_problem path -// reaches this via op_problem_to_mps_data_model first. +// Used by the mps-facing entry point (apply_presolve_from_mps_data). The +// op_problem-facing entry goes through build_papilo_problem_from_op_problem +// instead (direct D->H, no mps roundtrip). template papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model_t& mps, problem_category_t category, @@ -381,6 +383,396 @@ io::mps_data_model_t build_mps_data_from_papilo( return mps; } +// D->H direct: gather op_problem's device buffers into a pslp_input_t +template +pslp_input_t build_pslp_host_arrays_from_op_problem( + const optimization_problem_t& op, bool maximize) +{ + raft::common::nvtx::range fun_scope("Build PSLP host arrays from op_problem (D->H)"); + + pslp_input_t arrays; + arrays.n_cols = op.get_n_variables(); + arrays.n_rows = op.get_n_constraints(); + arrays.nnz = op.get_nnz(); + + const auto& d_coefficients = op.get_constraint_matrix_values(); + const auto& d_indices = op.get_constraint_matrix_indices(); + const auto& d_offsets = op.get_constraint_matrix_offsets(); + const auto& d_obj_coeffs = op.get_objective_coefficients(); + const auto& d_var_lb = op.get_variable_lower_bounds(); + const auto& d_var_ub = op.get_variable_upper_bounds(); + const auto& d_constr_lb = op.get_constraint_lower_bounds(); + const auto& d_constr_ub = op.get_constraint_upper_bounds(); + const auto& d_bounds = op.get_constraint_bounds(); + const auto& d_row_types = op.get_row_types(); + + arrays.coefficients.resize(d_coefficients.size()); + arrays.indices.resize(d_indices.size()); + arrays.offsets.resize(d_offsets.size()); + arrays.obj_coeffs.resize(d_obj_coeffs.size()); + arrays.var_lb.resize(d_var_lb.size()); + arrays.var_ub.resize(d_var_ub.size()); + arrays.constr_lb.resize(d_constr_lb.size()); + arrays.constr_ub.resize(d_constr_ub.size()); + std::vector h_bounds(d_bounds.size()); + std::vector h_row_types(d_row_types.size()); + + auto stream = op.get_handle_ptr()->get_stream(); + raft::copy(arrays.coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); + raft::copy(arrays.indices.data(), d_indices.data(), d_indices.size(), stream); + raft::copy(arrays.offsets.data(), d_offsets.data(), d_offsets.size(), stream); + raft::copy(arrays.obj_coeffs.data(), d_obj_coeffs.data(), d_obj_coeffs.size(), stream); + raft::copy(arrays.var_lb.data(), d_var_lb.data(), d_var_lb.size(), stream); + raft::copy(arrays.var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); + raft::copy(arrays.constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); + raft::copy(arrays.constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); + raft::copy(h_bounds.data(), d_bounds.data(), d_bounds.size(), stream); + raft::copy(h_row_types.data(), d_row_types.data(), d_row_types.size(), stream); + stream.synchronize(); + + if (maximize) { + for (auto& c : arrays.obj_coeffs) + c = -c; + } + + if (arrays.constr_lb.empty() && arrays.constr_ub.empty()) { + for (size_t i = 0; i < h_row_types.size(); ++i) { + if (h_row_types[i] == 'L') { + arrays.constr_lb.push_back(-std::numeric_limits::infinity()); + arrays.constr_ub.push_back(h_bounds[i]); + } else if (h_row_types[i] == 'G') { + arrays.constr_lb.push_back(h_bounds[i]); + arrays.constr_ub.push_back(std::numeric_limits::infinity()); + } else if (h_row_types[i] == 'E') { + arrays.constr_lb.push_back(h_bounds[i]); + arrays.constr_ub.push_back(h_bounds[i]); + } + } + } + + if (arrays.var_lb.empty()) { + arrays.var_lb.assign(arrays.n_cols, -std::numeric_limits::infinity()); + } + if (arrays.var_ub.empty()) { + arrays.var_ub.assign(arrays.n_cols, std::numeric_limits::infinity()); + } + + return arrays; +} + +// D->H direct: gather op_problem's device buffers, then feed a +// papilo::ProblemBuilder. Mirrors build_papilo_problem_from_mps_data +// exactly (same normalisation and flag handling), but consumes op_problem's +// device-side arrays with a single stream sync — no mps_data_model roundtrip. +// var_types come out of op_problem as `var_t` enums so we skip the char <-> +// enum conversion the mps path needs. +template +papilo::Problem build_papilo_problem_from_op_problem( + const optimization_problem_t& op, problem_category_t category, bool maximize) +{ + raft::common::nvtx::range fun_scope("Build papilo problem from op_problem (D->H)"); + papilo::ProblemBuilder builder; + + const i_t num_cols = op.get_n_variables(); + const i_t num_rows = op.get_n_constraints(); + const i_t nnz = op.get_nnz(); + + builder.reserve(nnz, num_rows, num_cols); + + const auto& d_coefficients = op.get_constraint_matrix_values(); + const auto& d_indices = op.get_constraint_matrix_indices(); + const auto& d_offsets = op.get_constraint_matrix_offsets(); + const auto& d_obj_coeffs = op.get_objective_coefficients(); + const auto& d_var_lb = op.get_variable_lower_bounds(); + const auto& d_var_ub = op.get_variable_upper_bounds(); + const auto& d_constr_lb = op.get_constraint_lower_bounds(); + const auto& d_constr_ub = op.get_constraint_upper_bounds(); + const auto& d_bounds = op.get_constraint_bounds(); + const auto& d_row_types = op.get_row_types(); + const auto& d_var_types = op.get_variable_types(); + + std::vector h_coefficients(d_coefficients.size()); + std::vector h_variables(d_indices.size()); + std::vector h_offsets(d_offsets.size()); + std::vector h_obj_coeffs(d_obj_coeffs.size()); + std::vector h_var_lb(d_var_lb.size()); + std::vector h_var_ub(d_var_ub.size()); + std::vector h_constr_lb(d_constr_lb.size()); + std::vector h_constr_ub(d_constr_ub.size()); + std::vector h_bounds(d_bounds.size()); + std::vector h_row_types(d_row_types.size()); + std::vector h_var_types(d_var_types.size()); + + auto stream = op.get_handle_ptr()->get_stream(); + raft::copy(h_coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); + raft::copy(h_variables.data(), d_indices.data(), d_indices.size(), stream); + raft::copy(h_offsets.data(), d_offsets.data(), d_offsets.size(), stream); + raft::copy(h_obj_coeffs.data(), d_obj_coeffs.data(), d_obj_coeffs.size(), stream); + raft::copy(h_var_lb.data(), d_var_lb.data(), d_var_lb.size(), stream); + raft::copy(h_var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); + raft::copy(h_constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); + raft::copy(h_constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); + raft::copy(h_bounds.data(), d_bounds.data(), d_bounds.size(), stream); + raft::copy(h_row_types.data(), d_row_types.data(), d_row_types.size(), stream); + raft::copy(h_var_types.data(), d_var_types.data(), d_var_types.size(), stream); + stream.synchronize(); + + if (maximize) { + for (auto& c : h_obj_coeffs) + c = -c; + } + + if (h_constr_lb.empty() && h_constr_ub.empty()) { + for (size_t i = 0; i < h_row_types.size(); ++i) { + if (h_row_types[i] == 'L') { + h_constr_lb.push_back(-std::numeric_limits::infinity()); + h_constr_ub.push_back(h_bounds[i]); + } else if (h_row_types[i] == 'G') { + h_constr_lb.push_back(h_bounds[i]); + h_constr_ub.push_back(std::numeric_limits::infinity()); + } else if (h_row_types[i] == 'E') { + h_constr_lb.push_back(h_bounds[i]); + h_constr_ub.push_back(h_bounds[i]); + } + } + } + + builder.setNumCols(num_cols); + builder.setNumRows(num_rows); + + builder.setObjAll(h_obj_coeffs); + builder.setObjOffset(maximize ? -op.get_objective_offset() : op.get_objective_offset()); + + if (!h_var_lb.empty() && !h_var_ub.empty()) { + builder.setColLbAll(h_var_lb); + builder.setColUbAll(h_var_ub); + if (op.get_variable_names().size() == static_cast(num_cols)) { + builder.setColNameAll(op.get_variable_names()); + } + } + + if (category == problem_category_t::MIP) { + for (size_t i = 0; i < h_var_types.size(); ++i) { + builder.setColIntegral(i, h_var_types[i] == var_t::INTEGER); + } + } + + if (!h_constr_lb.empty() && !h_constr_ub.empty()) { + builder.setRowLhsAll(h_constr_lb); + builder.setRowRhsAll(h_constr_ub); + } + + std::vector h_row_flags(h_constr_lb.size()); + std::vector> h_entries; + for (size_t i = 0; i < h_constr_lb.size(); ++i) { + i_t row_start = h_offsets[i]; + i_t row_end = h_offsets[i + 1]; + i_t num_entries = row_end - row_start; + for (size_t j = 0; j < num_entries; ++j) { + h_entries.push_back( + std::make_tuple(i, h_variables[row_start + j], h_coefficients[row_start + j])); + } + + if (h_constr_lb[i] == -std::numeric_limits::infinity()) { + h_row_flags[i].set(papilo::RowFlag::kLhsInf); + } else { + h_row_flags[i].unset(papilo::RowFlag::kLhsInf); + } + if (h_constr_ub[i] == std::numeric_limits::infinity()) { + h_row_flags[i].set(papilo::RowFlag::kRhsInf); + } else { + h_row_flags[i].unset(papilo::RowFlag::kRhsInf); + } + + if (h_constr_lb[i] == -std::numeric_limits::infinity()) { h_constr_lb[i] = 0; } + if (h_constr_ub[i] == std::numeric_limits::infinity()) { h_constr_ub[i] = 0; } + } + + for (size_t i = 0; i < h_var_lb.size(); ++i) { + builder.setColLbInf(i, h_var_lb[i] == -std::numeric_limits::infinity()); + builder.setColUbInf(i, h_var_ub[i] == std::numeric_limits::infinity()); + if (h_var_lb[i] == -std::numeric_limits::infinity()) { builder.setColLb(i, 0); } + if (h_var_ub[i] == std::numeric_limits::infinity()) { builder.setColUb(i, 0); } + } + + auto problem = builder.build(); + + if (h_entries.size()) { + auto constexpr const sorted_entries = true; + const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; + const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; + auto csr_storage = papilo::SparseStorage( + h_entries, num_rows, num_cols, sorted_entries, spare_ratio, min_inter_row_space); + problem.setConstraintMatrix(csr_storage, h_constr_lb, h_constr_ub, h_row_flags); + + papilo::ConstraintMatrix& matrix = problem.getConstraintMatrix(); + for (int i = 0; i < problem.getNRows(); ++i) { + papilo::RowFlags rowFlag = matrix.getRowFlags()[i]; + if (!rowFlag.test(papilo::RowFlag::kRhsInf) && !rowFlag.test(papilo::RowFlag::kLhsInf) && + matrix.getLeftHandSides()[i] == matrix.getRightHandSides()[i]) + matrix.getRowFlags()[i].set(papilo::RowFlag::kEquation); + } + } + + return problem; +} + +// H->D direct: read PSLP's reduced host arrays and build a device +// optimization_problem_t on `handle`. Mirrors build_mps_data_from_pslp but +// skips the mps_data_model intermediate — arrays flow straight from PSLP into +// op_problem setters (which each perform an async raft::copy on handle's +// stream). We sync once at the end so the local sign-flipped obj_coeffs +// temp is safe to release. +template +optimization_problem_t build_op_problem_from_pslp(raft::handle_t const* handle, + Presolver* pslp_presolver, + bool maximize, + f_t original_obj_offset) +{ + raft::common::nvtx::range fun_scope("Build op_problem from PSLP (H->D)"); + optimization_problem_t op(handle); + + if constexpr (std::is_same_v) { + cuopt_expects(pslp_presolver != nullptr && pslp_presolver->reduced_prob != nullptr, + error_type_t::RuntimeError, + "PSLP presolver is not initialized"); + auto reduced_prob = pslp_presolver->reduced_prob; + const i_t n_rows = reduced_prob->m; + const i_t n_cols = reduced_prob->n; + const i_t nnz = reduced_prob->nnz; + f_t obj_offset = reduced_prob->obj_offset; + + obj_offset = maximize ? -obj_offset : obj_offset; + // PSLP does not allow setting an objective offset, so we fold the + // original input's offset into the reduced one. + obj_offset += original_obj_offset; + op.set_maximize(maximize); + op.set_objective_offset(obj_offset); + + if (n_cols == 0 && n_rows == 0) { + std::vector empty_offsets{0}; + op.set_csr_constraint_matrix(nullptr, 0, nullptr, 0, empty_offsets.data(), 1); + handle->sync_stream(); + return op; + } + + op.set_csr_constraint_matrix( + reduced_prob->Ax, nnz, reduced_prob->Ai, nnz, reduced_prob->Ap, n_rows + 1); + + std::vector h_obj_coeffs(reduced_prob->c, reduced_prob->c + n_cols); + if (maximize) { + for (auto& c : h_obj_coeffs) + c = -c; + } + op.set_objective_coefficients(h_obj_coeffs.data(), n_cols); + + op.set_constraint_lower_bounds(reduced_prob->lhs, n_rows); + op.set_constraint_upper_bounds(reduced_prob->rhs, n_rows); + op.set_variable_lower_bounds(reduced_prob->lbs, n_cols); + op.set_variable_upper_bounds(reduced_prob->ubs, n_cols); + + // set_* enqueued async raft::copy on the handle's stream; sync so the + // sign-flipped h_obj_coeffs temp lives long enough. + handle->sync_stream(); + } else { + cuopt_expects(false, error_type_t::ValidationError, "PSLP only supports double precision"); + } + return op; +} + +// H->D direct: read the reduced papilo::Problem and build a device +// optimization_problem_t on `handle`. Mirrors build_mps_data_from_papilo but +// skips the mps_data_model intermediate. var_types are produced as `var_t` +// enums (no char <-> enum round-trip). Single stream sync at the end so +// temporaries (var_types, bound arrays with inf substitution, sign-flipped +// obj_coeffs) live past the async H->D copies. +template +optimization_problem_t build_op_problem_from_papilo( + raft::handle_t const* handle, papilo::Problem const& papilo_problem, bool maximize) +{ + raft::common::nvtx::range fun_scope("Build op_problem from papilo (H->D)"); + optimization_problem_t op(handle); + + auto obj = papilo_problem.getObjective(); + op.set_maximize(maximize); + op.set_objective_offset(maximize ? -obj.offset : obj.offset); + + if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { + std::vector h_offsets{0}; + op.set_csr_constraint_matrix(nullptr, 0, nullptr, 0, h_offsets.data(), 1); + handle->sync_stream(); + return op; + } + + if (maximize) { + for (size_t i = 0; i < obj.coefficients.size(); ++i) { + obj.coefficients[i] = -obj.coefficients[i]; + } + } + op.set_objective_coefficients(obj.coefficients.data(), + static_cast(obj.coefficients.size())); + + auto& constraint_matrix = papilo_problem.getConstraintMatrix(); + auto row_lower = constraint_matrix.getLeftHandSides(); + auto row_upper = constraint_matrix.getRightHandSides(); + auto col_lower = papilo_problem.getLowerBounds(); + auto col_upper = papilo_problem.getUpperBounds(); + + auto row_flags = constraint_matrix.getRowFlags(); + for (size_t i = 0; i < row_flags.size(); i++) { + if (row_flags[i].test(papilo::RowFlag::kLhsInf)) { + row_lower[i] = -std::numeric_limits::infinity(); + } + if (row_flags[i].test(papilo::RowFlag::kRhsInf)) { + row_upper[i] = std::numeric_limits::infinity(); + } + } + + op.set_constraint_lower_bounds(row_lower.data(), static_cast(row_lower.size())); + op.set_constraint_upper_bounds(row_upper.data(), static_cast(row_upper.size())); + + auto [index_range, nrows] = constraint_matrix.getRangeInfo(); + std::vector offsets(nrows + 1); + size_t start = index_range[0].start; + for (i_t i = 0; i < nrows; i++) { + offsets[i] = index_range[i].start - start; + } + offsets[nrows] = index_range[nrows - 1].end - start; + + i_t nnz = constraint_matrix.getNnz(); + assert(offsets[nrows] == nnz); + + const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); + const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); + + op.set_csr_constraint_matrix( + &coeffs[start], nnz, &cols[start], nnz, offsets.data(), static_cast(offsets.size())); + + auto col_flags = papilo_problem.getColFlags(); + std::vector var_types(col_flags.size()); + for (size_t i = 0; i < col_flags.size(); i++) { + var_types[i] = + col_flags[i].test(papilo::ColFlag::kIntegral) ? var_t::INTEGER : var_t::CONTINUOUS; + if (col_flags[i].test(papilo::ColFlag::kLbInf)) { + col_lower[i] = -std::numeric_limits::infinity(); + } + if (col_flags[i].test(papilo::ColFlag::kUbInf)) { + col_upper[i] = std::numeric_limits::infinity(); + } + } + + op.set_variable_lower_bounds(col_lower.data(), static_cast(col_lower.size())); + op.set_variable_upper_bounds(col_upper.data(), static_cast(col_upper.size())); + op.set_variable_types(var_types.data(), static_cast(var_types.size())); + + // Every set_* above enqueued async raft::copy on handle's stream. Sync so + // the temporary vectors (var_types, adjusted lb/ub, offsets, sign-flipped + // obj coefficients) outlive the copies. + handle->sync_stream(); + + return op; +} + void check_presolve_status(const papilo::PresolveStatus& status) { switch (status) { @@ -563,8 +955,8 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( presolver->stats->nnz_reduced); // Free previously allocated presolver and settings (if any) and stash the - // new ones so undo_pslp_host / build_mps_data_from_pslp can find them - // later. + // new ones so undo_pslp_host / build_{mps_data,op_problem}_from_pslp can + // find them later. if (pslp_presolver_ != nullptr) { free_presolver(pslp_presolver_); } if (pslp_stgs_ != nullptr) { free_settings(pslp_stgs_); } pslp_presolver_ = presolver; @@ -657,8 +1049,7 @@ third_party_presolve_status_t third_party_presolve_t::apply_papilo( return status; } -// Project to mps_data_model and apply presolve on host -// and rebuild optimization_problem_t on device +// Direct op_problem -> presolver_input -> apply -> op_problem path template third_party_presolve_device_result_t third_party_presolve_t::apply_presolve_from_op_problem( @@ -672,36 +1063,91 @@ third_party_presolve_t::apply_presolve_from_op_problem( i_t num_cpu_threads) { auto* handle = op_problem.get_handle_ptr(); - auto mps = op_problem_to_mps_data_model(op_problem); - - auto host_res = apply_presolve_from_mps_data(mps, - category, - presolver, - dual_postsolve, - absolute_tolerance, - relative_tolerance, - time_limit, - num_cpu_threads); - - if (host_res.status == third_party_presolve_status_t::INFEASIBLE || - host_res.status == third_party_presolve_status_t::UNBOUNDED || - host_res.status == third_party_presolve_status_t::UNBNDORINFEAS) { - return third_party_presolve_device_result_t{ - host_res.status, optimization_problem_t(handle), {}, {}, {}}; - } + presolver_ = presolver; + maximize_ = op_problem.get_sense(); + + cuopt_expects(!(category == problem_category_t::MIP && + presolver == cuopt::mathematical_optimization::presolver_t::PSLP), + error_type_t::RuntimeError, + "PSLP presolver is not supported for MIP problems"); + + // Neither PSLP nor Papilo handle quadratic objective / constraints. + cuopt_expects(!op_problem.has_quadratic_objective(), + error_type_t::ValidationError, + "Presolve does not support optimization_problem with a quadratic objective"); + cuopt_expects(!op_problem.has_quadratic_constraints(), + error_type_t::ValidationError, + "Presolve does not support optimization_problem with quadratic constraints"); + + // PSLP branch: D->H gather -> apply_pslp (host) -> H->D build + if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { + if constexpr (std::is_same_v) { + const f_t original_obj_offset = op_problem.get_objective_offset(); + auto arrays = build_pslp_host_arrays_from_op_problem(op_problem, maximize_); + auto status = apply_pslp(arrays, time_limit); + + if (status == third_party_presolve_status_t::INFEASIBLE || + status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_device_result_t{ + status, optimization_problem_t(handle), {}, {}, {}}; + } + + auto reduced_opt = build_op_problem_from_pslp( + handle, pslp_presolver_, maximize_, original_obj_offset); + reduced_opt.set_problem_name(op_problem.get_problem_name()); + reduced_opt.set_objective_scaling_factor(op_problem.get_objective_scaling_factor()); + reduced_opt.set_problem_category(category); + return third_party_presolve_device_result_t{ + status, std::move(reduced_opt), {}, {}, {}}; + } else { + cuopt_expects( + false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); + return third_party_presolve_device_result_t{ + third_party_presolve_status_t::UNCHANGED, + optimization_problem_t(handle), + {}, + {}, + {}}; // unreachable + } + } else { + // Papilo branch: D->H gather -> apply_papilo (host) -> H->D build + auto papilo_problem = build_papilo_problem_from_op_problem(op_problem, category, maximize_); + auto status = apply_papilo(papilo_problem, + category, + dual_postsolve, + absolute_tolerance, + relative_tolerance, + time_limit, + num_cpu_threads); - // H->D: rebuild a device optimization_problem from the reduced mps_data_model. - // mps_data_model doesn't carry problem_category, so we restore it here. - auto reduced_opt = - mps_data_model_to_optimization_problem(handle, host_res.reduced_problem); - reduced_opt.set_problem_category(category); - - return third_party_presolve_device_result_t{ - host_res.status, - std::move(reduced_opt), - std::move(host_res.implied_integer_indices), - std::move(host_res.reduced_to_original_map), - std::move(host_res.original_to_reduced_map)}; + if (status == third_party_presolve_status_t::INFEASIBLE || + status == third_party_presolve_status_t::UNBOUNDED || + status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_device_result_t{ + status, optimization_problem_t(handle), {}, {}, {}}; + } + + auto reduced_opt = build_op_problem_from_papilo(handle, papilo_problem, maximize_); + reduced_opt.set_problem_name(op_problem.get_problem_name()); + reduced_opt.set_objective_scaling_factor(op_problem.get_objective_scaling_factor()); + reduced_opt.set_problem_category(category); + + std::vector implied_integer_indices; + { + auto col_flags = papilo_problem.getColFlags(); + for (size_t i = 0; i < col_flags.size(); ++i) { + if (col_flags[i].test(papilo::ColFlag::kImplInt)) { + implied_integer_indices.push_back(static_cast(i)); + } + } + } + + return third_party_presolve_device_result_t{status, + std::move(reduced_opt), + std::move(implied_integer_indices), + reduced_to_original_map_, + original_to_reduced_map_}; + } } template diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index f5c4921427..839e8b8f83 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -97,8 +97,7 @@ class third_party_presolve_t { third_party_presolve_t& operator=(third_party_presolve_t&&) = delete; // Device entry: takes an optimization_problem_t and returns a device-side - // reduced optimization_problem_t. Internally a thin shim over - // apply_presolve_from_mps_data + // reduced optimization_problem_t. third_party_presolve_device_result_t apply_presolve_from_op_problem( optimization_problem_t const& op_problem, problem_category_t category, From 390d66c85432c40dad94014dffa03f0da9c1d10b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 17:40:50 +0200 Subject: [PATCH 188/258] fixed small bug in cuopt cli --- cpp/cuopt_cli.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 9b923df18f..e02c12fade 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -151,7 +151,7 @@ int run_single_file(const std::string& file_path, cuopt::cuopt_expects(!solve_relaxation, cuopt::error_type_t::ValidationError, "Solving the LP relaxation is not allowed for distributed PDLP."); - cuopt::cuopt_expects(!initial_solution_file.empty(), + cuopt::cuopt_expects(initial_solution_file.empty(), cuopt::error_type_t::ValidationError, "Initial solution file is not allowed for distributed PDLP."); const auto& var_type_chars = mps_data_model.get_variable_types(); From 099adc3d293ebddc3a80e6a51c44688bc029ffae Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 21:10:56 +0200 Subject: [PATCH 189/258] moved mgpu tests to specific file to be ran in CI --- cpp/tests/linear_programming/CMakeLists.txt | 8 ++ .../pdlp_distributed_test.cu | 128 ++++++++++++++++++ cpp/tests/linear_programming/pdlp_test.cu | 101 -------------- 3 files changed, 136 insertions(+), 101 deletions(-) create mode 100644 cpp/tests/linear_programming/pdlp_distributed_test.cu diff --git a/cpp/tests/linear_programming/CMakeLists.txt b/cpp/tests/linear_programming/CMakeLists.txt index bc057db1e2..ea3292394d 100644 --- a/cpp/tests/linear_programming/CMakeLists.txt +++ b/cpp/tests/linear_programming/CMakeLists.txt @@ -15,6 +15,14 @@ ConfigureTest(PDLP_TEST ${CMAKE_CURRENT_SOURCE_DIR}/pdlp_test.cu LABELS numopt) +# Multi-GPU distributed PDLP tests. Binary name ends in _MG_TEST so that +# ci/test_cpp_multi_gpu.sh picks it up via its *_MG_TEST glob. Each test +# calls GTEST_SKIP when fewer than 2 GPUs are visible so the binary is +# harmless on single-GPU runners. +ConfigureTest(PDLP_MG_TEST + ${CMAKE_CURRENT_SOURCE_DIR}/pdlp_distributed_test.cu + LABELS numopt) + # ################################################################################################## # - MPS / LP parser tests -------------------------------------------------------------------------- ConfigureTest(MPS_PARSER_TEST diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu new file mode 100644 index 0000000000..f404298dda --- /dev/null +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -0,0 +1,128 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Multi-GPU distributed PDLP parity tests. +// Binary name PDLP_MG_TEST matches the *_MG_TEST glob in ci/test_cpp_multi_gpu.sh. + +#include "utilities/pdlp_test_utilities.cuh" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#include +#include + +namespace cuopt::mathematical_optimization::test { + +// Solve `mps_rel_path` with the single-GPU PDLP ("base") and with distributed PDLP +// (num_gpus = -1 => auto-detect), then assert the distributed run matches the base run on +// everything meaningful: termination status, step count (within 15%), primal/dual objective, +// and the full primal/dual solution vectors. All value comparisons use a loose relative tolerance. +static void expect_distributed_matches_base(raft::handle_t const& handle, + std::string const& mps_rel_path, + bool fixed_mps_format = false) +{ + constexpr double loose_rel = 1e-3; + auto near_rel = [](double a, double b, double rel) { + return std::fabs(a - b) <= rel * (1.0 + std::fabs(a)); + }; + + auto path = make_path_absolute(mps_rel_path); + io::mps_data_model_t problem = io::read_mps(path, fixed_mps_format); + + pdlp_solver_settings_t base_settings{}; + base_settings.method = method_t::PDLP; + + auto base_op = mps_data_model_to_optimization_problem(&handle, problem); + auto base = solve_lp(base_op, base_settings); + + pdlp_solver_settings_t dist_settings = base_settings; + dist_settings.use_distributed_pdlp = true; + dist_settings.distributed_pdlp_num_gpus = -1; + auto dist = solve_lp(&handle, problem, dist_settings); + + ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) + << mps_rel_path << ": base did not reach optimal"; + EXPECT_EQ(static_cast(dist.get_termination_status()), + static_cast(base.get_termination_status())) + << mps_rel_path << ": distributed termination status differs from base"; + + const auto& base_info = base.get_additional_termination_information(); + const auto& dist_info = dist.get_additional_termination_information(); + + EXPECT_TRUE(near_rel(base_info.primal_objective, dist_info.primal_objective, loose_rel)) + << mps_rel_path << ": primal objective base=" << base_info.primal_objective + << " distributed=" << dist_info.primal_objective; + EXPECT_TRUE(near_rel(base_info.dual_objective, dist_info.dual_objective, loose_rel)) + << mps_rel_path << ": dual objective base=" << base_info.dual_objective + << " distributed=" << dist_info.dual_objective; + + const int base_steps = base_info.number_of_steps_taken; + const int dist_steps = dist_info.number_of_steps_taken; + const int max_steps = std::max(base_steps, dist_steps); + const int step_diff = std::max(base_steps, dist_steps) - std::min(base_steps, dist_steps); + EXPECT_LE(static_cast(step_diff), 0.15 * max_steps) + << mps_rel_path << ": step counts differ by >15% (base=" << base_steps + << ", distributed=" << dist_steps << ")"; + + auto base_primal = cuopt::host_copy(base.get_primal_solution(), handle.get_stream()); + auto dist_primal = cuopt::host_copy(dist.get_primal_solution(), handle.get_stream()); + ASSERT_EQ(base_primal.size(), dist_primal.size()) << mps_rel_path << ": primal size mismatch"; + for (std::size_t i = 0; i < base_primal.size(); ++i) { + EXPECT_TRUE(near_rel(base_primal[i], dist_primal[i], loose_rel)) + << mps_rel_path << ": primal[" << i << "] base=" << base_primal[i] + << " distributed=" << dist_primal[i]; + } + + auto base_dual = cuopt::host_copy(base.get_dual_solution(), handle.get_stream()); + auto dist_dual = cuopt::host_copy(dist.get_dual_solution(), handle.get_stream()); + ASSERT_EQ(base_dual.size(), dist_dual.size()) << mps_rel_path << ": dual size mismatch"; + for (std::size_t i = 0; i < base_dual.size(); ++i) { + EXPECT_TRUE(near_rel(base_dual[i], dist_dual[i], loose_rel)) + << mps_rel_path << ": dual[" << i << "] base=" << base_dual[i] + << " distributed=" << dist_dual[i]; + } +} + +TEST(pdlp_class, distributed_parity_afiro_MG_TEST) +{ + if (raft::device_setter::get_device_count() < 2) { + GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); + } + const raft::handle_t handle{}; + expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); +} + +TEST(pdlp_class, distributed_parity_neos3_MG_TEST) +{ + if (raft::device_setter::get_device_count() < 2) { + GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); + } + const raft::handle_t handle{}; + expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); +} + +TEST(pdlp_class, distributed_parity_a2864_MG_TEST) +{ + if (raft::device_setter::get_device_count() < 2) { + GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); + } + const raft::handle_t handle{}; + expect_distributed_matches_base(handle, "linear_programming/a2864/a2864.mps"); +} + +} // namespace cuopt::mathematical_optimization::test diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index ae39923fab..95d4010ce7 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -98,107 +98,6 @@ TEST(pdlp_class, run_double) afiro_primal_objective, solution.get_additional_termination_information().primal_objective)); } -namespace { - -// Solve `mps_rel_path` with the single-GPU PDLP ("base") and with distributed PDLP -// (num_gpus = -1 => auto-detect; 1 GPU is fine), then assert the distributed run -// matches the base run on everything meaningful: termination status, step count -// (within 15%), primal/dual objective, and the full primal/dual solution vectors. -// All value comparisons use a loose relative tolerance. -void expect_distributed_matches_base(raft::handle_t const& handle, - std::string const& mps_rel_path, - bool fixed_mps_format = false) -{ - constexpr double loose_rel = 1e-3; - auto near_rel = [](double a, double b, double rel) { - return std::fabs(a - b) <= rel * (1.0 + std::fabs(a)); - }; - - auto path = make_path_absolute(mps_rel_path); - io::mps_data_model_t problem = io::read_mps(path, fixed_mps_format); - - // Shared settings: method is PDLP - pdlp_solver_settings_t base_settings{}; - base_settings.method = method_t::PDLP; - - // ----- base: single-GPU PDLP (materialize the full problem on one GPU) ----- - auto base_op = mps_data_model_to_optimization_problem(&handle, problem); - auto base = solve_lp(base_op, base_settings); - - // ----- distributed PDLP (identical settings, only the distributed flags flipped) ----- - // testing on only one gpu is allowed and is already a great test for the distributed path - pdlp_solver_settings_t dist_settings = base_settings; - dist_settings.use_distributed_pdlp = true; - dist_settings.distributed_pdlp_num_gpus = -1; - auto dist = solve_lp(&handle, problem, dist_settings); - - // ----- termination status ----- - ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) - << mps_rel_path << ": base did not reach optimal"; - EXPECT_EQ(static_cast(dist.get_termination_status()), - static_cast(base.get_termination_status())) - << mps_rel_path << ": distributed termination status differs from base"; - - const auto& base_info = base.get_additional_termination_information(); - const auto& dist_info = dist.get_additional_termination_information(); - - // ----- objectives ----- - EXPECT_TRUE(near_rel(base_info.primal_objective, dist_info.primal_objective, loose_rel)) - << mps_rel_path << ": primal objective base=" << base_info.primal_objective - << " distributed=" << dist_info.primal_objective; - EXPECT_TRUE(near_rel(base_info.dual_objective, dist_info.dual_objective, loose_rel)) - << mps_rel_path << ": dual objective base=" << base_info.dual_objective - << " distributed=" << dist_info.dual_objective; - - // ----- step count: within 15% of the larger of the two ----- - const int base_steps = base_info.number_of_steps_taken; - const int dist_steps = dist_info.number_of_steps_taken; - const int max_steps = std::max(base_steps, dist_steps); - const int step_diff = std::max(base_steps, dist_steps) - std::min(base_steps, dist_steps); - EXPECT_LE(static_cast(step_diff), 0.15 * max_steps) - << mps_rel_path << ": step counts differ by >15% (base=" << base_steps - << ", distributed=" << dist_steps << ")"; - - // ----- primal / dual solution vectors ----- - auto base_primal = cuopt::host_copy(base.get_primal_solution(), handle.get_stream()); - auto dist_primal = cuopt::host_copy(dist.get_primal_solution(), handle.get_stream()); - ASSERT_EQ(base_primal.size(), dist_primal.size()) << mps_rel_path << ": primal size mismatch"; - for (std::size_t i = 0; i < base_primal.size(); ++i) { - EXPECT_TRUE(near_rel(base_primal[i], dist_primal[i], loose_rel)) - << mps_rel_path << ": primal[" << i << "] base=" << base_primal[i] - << " distributed=" << dist_primal[i]; - } - - auto base_dual = cuopt::host_copy(base.get_dual_solution(), handle.get_stream()); - auto dist_dual = cuopt::host_copy(dist.get_dual_solution(), handle.get_stream()); - ASSERT_EQ(base_dual.size(), dist_dual.size()) << mps_rel_path << ": dual size mismatch"; - for (std::size_t i = 0; i < base_dual.size(); ++i) { - EXPECT_TRUE(near_rel(base_dual[i], dist_dual[i], loose_rel)) - << mps_rel_path << ": dual[" << i << "] base=" << base_dual[i] - << " distributed=" << dist_dual[i]; - } -} - -} // namespace - -TEST(pdlp_class, distributed_parity_afiro_MG_TEST) -{ - const raft::handle_t handle{}; - expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); -} - -TEST(pdlp_class, distributed_parity_neos3_MG_TEST) -{ - const raft::handle_t handle{}; - expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); -} - -TEST(pdlp_class, distributed_parity_a2864_MG_TEST) -{ - const raft::handle_t handle{}; - expect_distributed_matches_base(handle, "linear_programming/a2864/a2864.mps"); -} - TEST(pdlp_class, precision_mixed) { using namespace cuopt::mathematical_optimization::pdlp; From d9f5c4ac6c7c54ffe27bd0812712b51c277dc52c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 21:12:20 +0200 Subject: [PATCH 190/258] renamed distr tests --- cpp/tests/linear_programming/pdlp_distributed_test.cu | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu index f404298dda..63118bc625 100644 --- a/cpp/tests/linear_programming/pdlp_distributed_test.cu +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -98,7 +98,7 @@ static void expect_distributed_matches_base(raft::handle_t const& handle, } } -TEST(pdlp_class, distributed_parity_afiro_MG_TEST) +TEST(pdlp_class, distributed_parity_afiro) { if (raft::device_setter::get_device_count() < 2) { GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); @@ -107,7 +107,7 @@ TEST(pdlp_class, distributed_parity_afiro_MG_TEST) expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); } -TEST(pdlp_class, distributed_parity_neos3_MG_TEST) +TEST(pdlp_class, distributed_parity_neos3) { if (raft::device_setter::get_device_count() < 2) { GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); @@ -116,7 +116,7 @@ TEST(pdlp_class, distributed_parity_neos3_MG_TEST) expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); } -TEST(pdlp_class, distributed_parity_a2864_MG_TEST) +TEST(pdlp_class, distributed_parity_a2864) { if (raft::device_setter::get_device_count() < 2) { GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); From 37e27761e5607aad7051085142a5e78c505a41f2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 21:17:50 +0200 Subject: [PATCH 191/258] style --- .../mip_heuristics/presolve/third_party_presolve.cpp | 3 +-- .../mip_heuristics/presolve/third_party_presolve.hpp | 2 +- cpp/tests/linear_programming/pdlp_distributed_test.cu | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 6f1fffcf22..47f582a831 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -709,8 +709,7 @@ optimization_problem_t build_op_problem_from_papilo( obj.coefficients[i] = -obj.coefficients[i]; } } - op.set_objective_coefficients(obj.coefficients.data(), - static_cast(obj.coefficients.size())); + op.set_objective_coefficients(obj.coefficients.data(), static_cast(obj.coefficients.size())); auto& constraint_matrix = papilo_problem.getConstraintMatrix(); auto row_lower = constraint_matrix.getLeftHandSides(); diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 839e8b8f83..85081a7abc 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -97,7 +97,7 @@ class third_party_presolve_t { third_party_presolve_t& operator=(third_party_presolve_t&&) = delete; // Device entry: takes an optimization_problem_t and returns a device-side - // reduced optimization_problem_t. + // reduced optimization_problem_t. third_party_presolve_device_result_t apply_presolve_from_op_problem( optimization_problem_t const& op_problem, problem_category_t category, diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu index 63118bc625..7acb6b57cb 100644 --- a/cpp/tests/linear_programming/pdlp_distributed_test.cu +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -18,8 +18,8 @@ #include -#include #include +#include #include @@ -50,10 +50,10 @@ static void expect_distributed_matches_base(raft::handle_t const& handle, auto base_op = mps_data_model_to_optimization_problem(&handle, problem); auto base = solve_lp(base_op, base_settings); - pdlp_solver_settings_t dist_settings = base_settings; - dist_settings.use_distributed_pdlp = true; - dist_settings.distributed_pdlp_num_gpus = -1; - auto dist = solve_lp(&handle, problem, dist_settings); + pdlp_solver_settings_t dist_settings = base_settings; + dist_settings.use_distributed_pdlp = true; + dist_settings.distributed_pdlp_num_gpus = -1; + auto dist = solve_lp(&handle, problem, dist_settings); ASSERT_EQ(static_cast(base.get_termination_status()), CUOPT_TERMINATION_STATUS_OPTIMAL) << mps_rel_path << ": base did not reach optimal"; From 24713ce0f9dbfcca484a31e79709f2062e37ac0e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 21:46:24 +0200 Subject: [PATCH 192/258] removed forward dec in third_party presolve --- .../presolve/third_party_presolve.cpp | 16 --------- .../presolve/third_party_presolve.hpp | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 47f582a831..4c12094989 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -19,22 +19,6 @@ #include #include -#if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wc++11-narrowing" -#pragma clang diagnostic ignored "-Wimplicit-const-int-float-conversion" -#else -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wstringop-overflow" -#pragma GCC diagnostic ignored "-Wnarrowing" -#endif -#include -#include -#if defined(__clang__) -#pragma clang diagnostic pop -#else -#pragma GCC diagnostic pop -#endif #include #include #include diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 85081a7abc..95e292fc8a 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -11,25 +11,33 @@ #include #include +#include #include #include +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++11-narrowing" +#pragma clang diagnostic ignored "-Wimplicit-const-int-float-conversion" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-overflow" +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif +#include +#include +#if defined(__clang__) +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif + namespace papilo { template class PostsolveStorage; - -// Forward declaration for Papilo Problem class -template -class Problem; } // namespace papilo -// Forward declaration for mps_data_model_t -namespace cuopt::mathematical_optimization::io { -template -class mps_data_model_t; -} - namespace cuopt::mathematical_optimization::mip { template @@ -186,9 +194,8 @@ class third_party_presolve_t { Settings* pslp_stgs_{nullptr}; Presolver* pslp_presolver_{nullptr}; - // Necessary due to a nvcc bug due to papilo's constexpr functions - // Keep the papilo includes in the .cpp to avoid bringing them - // into any .cu context + // Necessary due to a nvcc bug due to papilo's constexpr functions. + // Keep heavier papilo includes in the .cpp; PostsolveStorage stays opaque here. std::unique_ptr, papilo_postsolve_deleter> papilo_post_solve_storage_; From ba2b96e7a85856c7efa0bda85504c4b242113cb7 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 21:48:50 +0200 Subject: [PATCH 193/258] unified third_party_presolve_result_t --- .../presolve/third_party_presolve.hpp | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 95e292fc8a..4f46174ad2 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -54,27 +54,23 @@ enum class third_party_presolve_status_t { UNCHANGED, }; -template -struct third_party_presolve_device_result_t { +template +struct third_party_presolve_result_t { third_party_presolve_status_t status; - optimization_problem_t reduced_problem; + ProblemT reduced_problem; std::vector implied_integer_indices; std::vector reduced_to_original_map; std::vector original_to_reduced_map; // clique info, etc... }; -// Host counterpart of third_party_presolve_device_result_t: the reduced -// problem is an mps_data_model_t (host) instead of an optimization_problem_t -// (device). Produced by apply_presolve_from_mps_data. template -struct third_party_presolve_host_result_t { - third_party_presolve_status_t status; - io::mps_data_model_t reduced_problem; - std::vector implied_integer_indices; - std::vector reduced_to_original_map; - std::vector original_to_reduced_map; -}; +using third_party_presolve_device_result_t = + third_party_presolve_result_t>; + +template +using third_party_presolve_host_result_t = + third_party_presolve_result_t>; // Host-side PSLP input: every buffer PSLP's C API needs, plus dimensions. template From 117d977e9a9ad3ef8cb67ae678b5e8e1d8af3de9 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 21:55:08 +0200 Subject: [PATCH 194/258] fixed a bug in restart_strategy that caused batch mode to break --- .../restart_strategy/pdlp_restart_strategy.cu | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index f5a87d1d41..3dd1dd7a51 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -913,8 +913,20 @@ void pdlp_restart_strategy_t::cupdlpx_restart( }, stream_view_); } else { - primal_dual_distance_squared_moved_from_last_restart_period( - pdhg_solver, primal_size_h_, dual_size_h_); + distance_squared_moved_from_last_restart_period( + pdhg_solver.get_potential_next_primal_solution(), + last_restart_duality_gap_.primal_solution_, + pdhg_solver.get_primal_tmp_resource(), + primal_size_h_, + 1, + last_restart_duality_gap_.primal_distance_traveled_); + distance_squared_moved_from_last_restart_period( + pdhg_solver.get_potential_next_dual_solution(), + last_restart_duality_gap_.dual_solution_, + pdhg_solver.get_dual_tmp_resource(), + dual_size_h_, + 1, + last_restart_duality_gap_.dual_distance_traveled_); } auto view = make_cupdlpx_restart_view(last_restart_duality_gap_.primal_distance_traveled_, From fa06593e723fb46d63ce322ea96e955bf3dcc02a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 21:58:49 +0200 Subject: [PATCH 195/258] added a print of topology in mgpu ci --- ci/test_cpp_multi_gpu.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index 2213f56dab..f3f60fa177 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -64,6 +64,10 @@ rapids-print-env rapids-logger "Check GPU usage" nvidia-smi +# Dump the GPU topology matrix +rapids-logger "Check GPU topology" +nvidia-smi topo -m + # Multi-GPU tests are meaningless on a single device — fail loudly rather than # passing a run that never exercised NCCL. GPU_COUNT=$(nvidia-smi -L | wc -l) From 99d1bd3a3a58d7a87ed32b9f030455a1ea488555 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 22:24:42 +0200 Subject: [PATCH 196/258] cleaned tpp a bit --- .../presolve/third_party_presolve.cpp | 128 +++++++----------- .../presolve/third_party_presolve.hpp | 11 +- 2 files changed, 57 insertions(+), 82 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 4c12094989..5c2376e918 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -19,9 +19,7 @@ #include #include -#include #include -#include #include #include #include @@ -31,17 +29,16 @@ #include +#include #include namespace cuopt::mathematical_optimization::mip { -// Host-only gather + input normalisation for PSLP from an mps_data_model. -// Used by the mps-facing entry point (apply_presolve_from_mps_data). The -// op_problem-facing entry goes through build_pslp_host_arrays_from_op_problem -// instead (direct D->H, no mps roundtrip). +// Host-only gather for PSLP from an mps_data_model. +// Used by the mps-facing entry point: apply_presolve_from_mps_data() template pslp_input_t build_pslp_host_arrays_from_mps_data( - const io::mps_data_model_t& mps, bool maximize) + const io::mps_data_model_t& mps) { raft::common::nvtx::range fun_scope("Build PSLP host arrays from mps_data_model"); @@ -50,54 +47,22 @@ pslp_input_t build_pslp_host_arrays_from_mps_data( arrays.n_rows = mps.get_n_constraints(); arrays.nnz = mps.get_nnz(); - // Copy (mps's getters return by const-ref; we own these vectors so we can - // mutate them during normalisation). arrays.coefficients = mps.get_constraint_matrix_values(); arrays.indices = mps.get_constraint_matrix_indices(); arrays.offsets = mps.get_constraint_matrix_offsets(); arrays.obj_coeffs = mps.get_objective_coefficients(); arrays.var_lb = mps.get_variable_lower_bounds(); arrays.var_ub = mps.get_variable_upper_bounds(); - arrays.constr_lb = mps.get_constraint_lower_bounds(); - arrays.constr_ub = mps.get_constraint_upper_bounds(); - - const auto& h_bounds = mps.get_constraint_bounds(); - const auto& h_row_types = mps.get_row_types(); - - if (maximize) { - for (auto& c : arrays.obj_coeffs) - c = -c; - } - - if (arrays.constr_lb.empty() && arrays.constr_ub.empty()) { - for (size_t i = 0; i < h_row_types.size(); ++i) { - if (h_row_types[i] == 'L') { - arrays.constr_lb.push_back(-std::numeric_limits::infinity()); - arrays.constr_ub.push_back(h_bounds[i]); - } else if (h_row_types[i] == 'G') { - arrays.constr_lb.push_back(h_bounds[i]); - arrays.constr_ub.push_back(std::numeric_limits::infinity()); - } else if (h_row_types[i] == 'E') { - arrays.constr_lb.push_back(h_bounds[i]); - arrays.constr_ub.push_back(h_bounds[i]); - } - } - } - - if (arrays.var_lb.empty()) { - arrays.var_lb.assign(arrays.n_cols, -std::numeric_limits::infinity()); - } - if (arrays.var_ub.empty()) { - arrays.var_ub.assign(arrays.n_cols, std::numeric_limits::infinity()); - } + arrays.constr_lb = mps.get_constraint_lower_bounds(); + arrays.constr_ub = mps.get_constraint_upper_bounds(); + arrays.constraint_bounds = mps.get_constraint_bounds(); + arrays.row_types = mps.get_row_types(); return arrays; } // Host-only gather + papilo::Problem construction from an mps_data_model. -// Used by the mps-facing entry point (apply_presolve_from_mps_data). The -// op_problem-facing entry goes through build_papilo_problem_from_op_problem -// instead (direct D->H, no mps roundtrip). +// Used by the mps-facing entry point: apply_presolve_from_mps_data() template papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model_t& mps, problem_category_t category, @@ -370,7 +335,7 @@ io::mps_data_model_t build_mps_data_from_papilo( // D->H direct: gather op_problem's device buffers into a pslp_input_t template pslp_input_t build_pslp_host_arrays_from_op_problem( - const optimization_problem_t& op, bool maximize) + const optimization_problem_t& op) { raft::common::nvtx::range fun_scope("Build PSLP host arrays from op_problem (D->H)"); @@ -398,8 +363,8 @@ pslp_input_t build_pslp_host_arrays_from_op_problem( arrays.var_ub.resize(d_var_ub.size()); arrays.constr_lb.resize(d_constr_lb.size()); arrays.constr_ub.resize(d_constr_ub.size()); - std::vector h_bounds(d_bounds.size()); - std::vector h_row_types(d_row_types.size()); + arrays.constraint_bounds.resize(d_bounds.size()); + arrays.row_types.resize(d_row_types.size()); auto stream = op.get_handle_ptr()->get_stream(); raft::copy(arrays.coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); @@ -410,37 +375,10 @@ pslp_input_t build_pslp_host_arrays_from_op_problem( raft::copy(arrays.var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); raft::copy(arrays.constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); raft::copy(arrays.constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); - raft::copy(h_bounds.data(), d_bounds.data(), d_bounds.size(), stream); - raft::copy(h_row_types.data(), d_row_types.data(), d_row_types.size(), stream); + raft::copy(arrays.constraint_bounds.data(), d_bounds.data(), d_bounds.size(), stream); + raft::copy(arrays.row_types.data(), d_row_types.data(), d_row_types.size(), stream); stream.synchronize(); - if (maximize) { - for (auto& c : arrays.obj_coeffs) - c = -c; - } - - if (arrays.constr_lb.empty() && arrays.constr_ub.empty()) { - for (size_t i = 0; i < h_row_types.size(); ++i) { - if (h_row_types[i] == 'L') { - arrays.constr_lb.push_back(-std::numeric_limits::infinity()); - arrays.constr_ub.push_back(h_bounds[i]); - } else if (h_row_types[i] == 'G') { - arrays.constr_lb.push_back(h_bounds[i]); - arrays.constr_ub.push_back(std::numeric_limits::infinity()); - } else if (h_row_types[i] == 'E') { - arrays.constr_lb.push_back(h_bounds[i]); - arrays.constr_ub.push_back(h_bounds[i]); - } - } - } - - if (arrays.var_lb.empty()) { - arrays.var_lb.assign(arrays.n_cols, -std::numeric_limits::infinity()); - } - if (arrays.var_ub.empty()) { - arrays.var_ub.assign(arrays.n_cols, std::numeric_limits::infinity()); - } - return arrays; } @@ -903,6 +841,38 @@ void set_presolve_parameters(papilo::Presolve& presolver, } } +template +void pslp_input_t::normalize_for_pslp(bool maximize) +{ + if (maximize) { + for (auto& c : obj_coeffs) { + c = -c; + } + } + + if (constr_lb.empty() && constr_ub.empty()) { + for (size_t i = 0; i < row_types.size(); ++i) { + if (row_types[i] == 'L') { + constr_lb.push_back(-std::numeric_limits::infinity()); + constr_ub.push_back(constraint_bounds[i]); + } else if (row_types[i] == 'G') { + constr_lb.push_back(constraint_bounds[i]); + constr_ub.push_back(std::numeric_limits::infinity()); + } else if (row_types[i] == 'E') { + constr_lb.push_back(constraint_bounds[i]); + constr_ub.push_back(constraint_bounds[i]); + } + } + } + + if (var_lb.empty()) { + var_lb.assign(n_cols, -std::numeric_limits::infinity()); + } + if (var_ub.empty()) { + var_ub.assign(n_cols, std::numeric_limits::infinity()); + } +} + template third_party_presolve_status_t third_party_presolve_t::apply_pslp( pslp_input_t& arrays, double time_limit) @@ -910,6 +880,8 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( if constexpr (std::is_same_v) { raft::common::nvtx::range fun_scope("Apply PSLP presolver on host"); + arrays.normalize_for_pslp(maximize_); + Settings* settings = default_settings(); settings->verbose = false; settings->max_time = time_limit; @@ -1066,7 +1038,7 @@ third_party_presolve_t::apply_presolve_from_op_problem( if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { if constexpr (std::is_same_v) { const f_t original_obj_offset = op_problem.get_objective_offset(); - auto arrays = build_pslp_host_arrays_from_op_problem(op_problem, maximize_); + auto arrays = build_pslp_host_arrays_from_op_problem(op_problem); auto status = apply_pslp(arrays, time_limit); if (status == third_party_presolve_status_t::INFEASIBLE || @@ -1165,7 +1137,7 @@ third_party_presolve_t::apply_presolve_from_mps_data( if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { if constexpr (std::is_same_v) { const f_t original_obj_offset = mps.get_objective_offset(); - auto arrays = build_pslp_host_arrays_from_mps_data(mps, maximize_); + auto arrays = build_pslp_host_arrays_from_mps_data(mps); auto status = apply_pslp(arrays, time_limit); if (status == third_party_presolve_status_t::INFEASIBLE || diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 4f46174ad2..f5ece6c82e 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -36,7 +36,7 @@ namespace papilo { template class PostsolveStorage; -} // namespace papilo +} namespace cuopt::mathematical_optimization::mip { @@ -83,9 +83,14 @@ struct pslp_input_t { std::vector var_ub; std::vector constr_lb; std::vector constr_ub; + std::vector constraint_bounds; + std::vector row_types; i_t n_rows{0}; i_t n_cols{0}; i_t nnz{0}; + + // Flip to minimise sense and materialise implicit bounds before PSLP runs. + void normalize_for_pslp(bool maximize); }; template @@ -133,7 +138,7 @@ class third_party_presolve_t { rmm::cuda_stream_view stream_view); // Host-only postsolve. Resizes the vectors to original-problem dimensions. - // The device-side `undo` above is a thin shim around this method. + // The device-side `undo` above is a thin wrapper around this method. void undo_host(std::vector& primal_solution, std::vector& dual_solution, std::vector& reduced_costs, @@ -190,8 +195,6 @@ class third_party_presolve_t { Settings* pslp_stgs_{nullptr}; Presolver* pslp_presolver_{nullptr}; - // Necessary due to a nvcc bug due to papilo's constexpr functions. - // Keep heavier papilo includes in the .cpp; PostsolveStorage stays opaque here. std::unique_ptr, papilo_postsolve_deleter> papilo_post_solve_storage_; From a85d9e13d5cf1c0275ae2319268683816e37f4c4 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 22:32:52 +0200 Subject: [PATCH 197/258] moved papilo include back to .cpp file and back to forward declaring Problem --- .../presolve/third_party_presolve.cpp | 17 ++++++++++++++ .../presolve/third_party_presolve.hpp | 22 ++++--------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 5c2376e918..1d9289d47f 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -19,6 +19,23 @@ #include #include +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++11-narrowing" +#pragma clang diagnostic ignored "-Wimplicit-const-int-float-conversion" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-overflow" +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif +#include +#include +#if defined(__clang__) +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif + #include #include #include diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index f5ece6c82e..2e9bcb93d3 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -16,27 +16,13 @@ #include -#if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wc++11-narrowing" -#pragma clang diagnostic ignored "-Wimplicit-const-int-float-conversion" -#else -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wstringop-overflow" -#pragma GCC diagnostic ignored "-Wnarrowing" -#endif -#include -#include -#if defined(__clang__) -#pragma clang diagnostic pop -#else -#pragma GCC diagnostic pop -#endif - namespace papilo { template class PostsolveStorage; -} + +template +class Problem; +} // namespace papilo namespace cuopt::mathematical_optimization::mip { From a3b7be79ea4c9719f044b8fd284b9dead534f5f8 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 22:33:07 +0200 Subject: [PATCH 198/258] style --- .../presolve/third_party_presolve.cpp | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 1d9289d47f..d25adebfe7 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -64,12 +64,12 @@ pslp_input_t build_pslp_host_arrays_from_mps_data( arrays.n_rows = mps.get_n_constraints(); arrays.nnz = mps.get_nnz(); - arrays.coefficients = mps.get_constraint_matrix_values(); - arrays.indices = mps.get_constraint_matrix_indices(); - arrays.offsets = mps.get_constraint_matrix_offsets(); - arrays.obj_coeffs = mps.get_objective_coefficients(); - arrays.var_lb = mps.get_variable_lower_bounds(); - arrays.var_ub = mps.get_variable_upper_bounds(); + arrays.coefficients = mps.get_constraint_matrix_values(); + arrays.indices = mps.get_constraint_matrix_indices(); + arrays.offsets = mps.get_constraint_matrix_offsets(); + arrays.obj_coeffs = mps.get_objective_coefficients(); + arrays.var_lb = mps.get_variable_lower_bounds(); + arrays.var_ub = mps.get_variable_upper_bounds(); arrays.constr_lb = mps.get_constraint_lower_bounds(); arrays.constr_ub = mps.get_constraint_upper_bounds(); arrays.constraint_bounds = mps.get_constraint_bounds(); @@ -882,12 +882,8 @@ void pslp_input_t::normalize_for_pslp(bool maximize) } } - if (var_lb.empty()) { - var_lb.assign(n_cols, -std::numeric_limits::infinity()); - } - if (var_ub.empty()) { - var_ub.assign(n_cols, std::numeric_limits::infinity()); - } + if (var_lb.empty()) { var_lb.assign(n_cols, -std::numeric_limits::infinity()); } + if (var_ub.empty()) { var_ub.assign(n_cols, std::numeric_limits::infinity()); } } template From 0d37018be714f508a344954081e4de5a4d7278d8 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 22:48:52 +0200 Subject: [PATCH 199/258] dedup code for papilo part --- .../presolve/third_party_presolve.cpp | 354 ++++++++---------- 1 file changed, 147 insertions(+), 207 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index d25adebfe7..8c248ec508 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -47,6 +47,8 @@ #include #include +#include +#include #include namespace cuopt::mathematical_optimization::mip { @@ -78,112 +80,116 @@ pslp_input_t build_pslp_host_arrays_from_mps_data( return arrays; } -// Host-only gather + papilo::Problem construction from an mps_data_model. -// Used by the mps-facing entry point: apply_presolve_from_mps_data() template -papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model_t& mps, - problem_category_t category, - bool maximize) -{ - raft::common::nvtx::range fun_scope("Build papilo problem from mps_data_model"); - papilo::ProblemBuilder builder; - - const i_t num_cols = mps.get_n_variables(); - const i_t num_rows = mps.get_n_constraints(); - const i_t nnz = mps.get_nnz(); - - builder.reserve(nnz, num_rows, num_cols); - - // Local mutable copies (we sign-flip / fill empties). - auto h_coefficients = mps.get_constraint_matrix_values(); - auto h_offsets = mps.get_constraint_matrix_offsets(); - auto h_variables = mps.get_constraint_matrix_indices(); - auto h_obj_coeffs = mps.get_objective_coefficients(); - auto h_var_lb = mps.get_variable_lower_bounds(); - auto h_var_ub = mps.get_variable_upper_bounds(); - auto h_constr_lb = mps.get_constraint_lower_bounds(); - auto h_constr_ub = mps.get_constraint_upper_bounds(); - - const auto& h_bounds = mps.get_constraint_bounds(); - const auto& h_row_types = mps.get_row_types(); - const auto& h_var_types = mps.get_variable_types(); - - if (maximize) { - for (auto& c : h_obj_coeffs) - c = -c; - } +struct papilo_problem_input_t { + i_t num_cols{0}; + i_t num_rows{0}; + i_t nnz{0}; + f_t objective_offset{0}; + std::vector coefficients; + std::vector offsets; + std::vector variables; + std::vector obj_coeffs; + std::vector var_lb; + std::vector var_ub; + std::vector constr_lb; + std::vector constr_ub; + std::vector constraint_bounds; + std::vector row_types; + std::vector var_types; + std::vector variable_names; + + void normalize(bool maximize) + { + if (maximize) { + for (auto& c : obj_coeffs) + c = -c; + objective_offset = -objective_offset; + } - if (h_constr_lb.empty() && h_constr_ub.empty()) { - for (size_t i = 0; i < h_row_types.size(); ++i) { - if (h_row_types[i] == 'L') { - h_constr_lb.push_back(-std::numeric_limits::infinity()); - h_constr_ub.push_back(h_bounds[i]); - } else if (h_row_types[i] == 'G') { - h_constr_lb.push_back(h_bounds[i]); - h_constr_ub.push_back(std::numeric_limits::infinity()); - } else if (h_row_types[i] == 'E') { - h_constr_lb.push_back(h_bounds[i]); - h_constr_ub.push_back(h_bounds[i]); + if (constr_lb.empty() && constr_ub.empty()) { + for (size_t i = 0; i < row_types.size(); ++i) { + if (row_types[i] == 'L') { + constr_lb.push_back(-std::numeric_limits::infinity()); + constr_ub.push_back(constraint_bounds[i]); + } else if (row_types[i] == 'G') { + constr_lb.push_back(constraint_bounds[i]); + constr_ub.push_back(std::numeric_limits::infinity()); + } else if (row_types[i] == 'E') { + constr_lb.push_back(constraint_bounds[i]); + constr_ub.push_back(constraint_bounds[i]); + } } } } +}; + +template +papilo::Problem build_papilo_problem_from_host(papilo_problem_input_t& input, + problem_category_t category, + bool maximize) +{ + papilo::ProblemBuilder builder; - builder.setNumCols(num_cols); - builder.setNumRows(num_rows); + input.normalize(maximize); - builder.setObjAll(h_obj_coeffs); - builder.setObjOffset(maximize ? -mps.get_objective_offset() : mps.get_objective_offset()); + builder.reserve(input.nnz, input.num_rows, input.num_cols); + builder.setNumCols(input.num_cols); + builder.setNumRows(input.num_rows); - if (!h_var_lb.empty() && !h_var_ub.empty()) { - builder.setColLbAll(h_var_lb); - builder.setColUbAll(h_var_ub); - if (mps.get_variable_names().size() == static_cast(num_cols)) { - builder.setColNameAll(mps.get_variable_names()); + builder.setObjAll(input.obj_coeffs); + builder.setObjOffset(input.objective_offset); + + if (!input.var_lb.empty() && !input.var_ub.empty()) { + builder.setColLbAll(input.var_lb); + builder.setColUbAll(input.var_ub); + if (input.variable_names.size() == static_cast(input.num_cols)) { + builder.setColNameAll(input.variable_names); } } if (category == problem_category_t::MIP) { - for (size_t i = 0; i < h_var_types.size(); ++i) { - builder.setColIntegral(i, char_to_var_type(h_var_types[i]) == var_t::INTEGER); + for (size_t i = 0; i < input.var_types.size(); ++i) { + builder.setColIntegral(i, input.var_types[i] == var_t::INTEGER); } } - if (!h_constr_lb.empty() && !h_constr_ub.empty()) { - builder.setRowLhsAll(h_constr_lb); - builder.setRowRhsAll(h_constr_ub); + if (!input.constr_lb.empty() && !input.constr_ub.empty()) { + builder.setRowLhsAll(input.constr_lb); + builder.setRowRhsAll(input.constr_ub); } - std::vector h_row_flags(h_constr_lb.size()); + std::vector h_row_flags(input.constr_lb.size()); std::vector> h_entries; - for (size_t i = 0; i < h_constr_lb.size(); ++i) { - i_t row_start = h_offsets[i]; - i_t row_end = h_offsets[i + 1]; + for (size_t i = 0; i < input.constr_lb.size(); ++i) { + i_t row_start = input.offsets[i]; + i_t row_end = input.offsets[i + 1]; i_t num_entries = row_end - row_start; for (size_t j = 0; j < num_entries; ++j) { h_entries.push_back( - std::make_tuple(i, h_variables[row_start + j], h_coefficients[row_start + j])); + std::make_tuple(i, input.variables[row_start + j], input.coefficients[row_start + j])); } - if (h_constr_lb[i] == -std::numeric_limits::infinity()) { + if (input.constr_lb[i] == -std::numeric_limits::infinity()) { h_row_flags[i].set(papilo::RowFlag::kLhsInf); } else { h_row_flags[i].unset(papilo::RowFlag::kLhsInf); } - if (h_constr_ub[i] == std::numeric_limits::infinity()) { + if (input.constr_ub[i] == std::numeric_limits::infinity()) { h_row_flags[i].set(papilo::RowFlag::kRhsInf); } else { h_row_flags[i].unset(papilo::RowFlag::kRhsInf); } - if (h_constr_lb[i] == -std::numeric_limits::infinity()) { h_constr_lb[i] = 0; } - if (h_constr_ub[i] == std::numeric_limits::infinity()) { h_constr_ub[i] = 0; } + if (input.constr_lb[i] == -std::numeric_limits::infinity()) { input.constr_lb[i] = 0; } + if (input.constr_ub[i] == std::numeric_limits::infinity()) { input.constr_ub[i] = 0; } } - for (size_t i = 0; i < h_var_lb.size(); ++i) { - builder.setColLbInf(i, h_var_lb[i] == -std::numeric_limits::infinity()); - builder.setColUbInf(i, h_var_ub[i] == std::numeric_limits::infinity()); - if (h_var_lb[i] == -std::numeric_limits::infinity()) { builder.setColLb(i, 0); } - if (h_var_ub[i] == std::numeric_limits::infinity()) { builder.setColUb(i, 0); } + for (size_t i = 0; i < input.var_lb.size(); ++i) { + builder.setColLbInf(i, input.var_lb[i] == -std::numeric_limits::infinity()); + builder.setColUbInf(i, input.var_ub[i] == std::numeric_limits::infinity()); + if (input.var_lb[i] == -std::numeric_limits::infinity()) { builder.setColLb(i, 0); } + if (input.var_ub[i] == std::numeric_limits::infinity()) { builder.setColUb(i, 0); } } auto problem = builder.build(); @@ -193,8 +199,8 @@ papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; auto csr_storage = papilo::SparseStorage( - h_entries, num_rows, num_cols, sorted_entries, spare_ratio, min_inter_row_space); - problem.setConstraintMatrix(csr_storage, h_constr_lb, h_constr_ub, h_row_flags); + h_entries, input.num_rows, input.num_cols, sorted_entries, spare_ratio, min_inter_row_space); + problem.setConstraintMatrix(csr_storage, input.constr_lb, input.constr_ub, h_row_flags); papilo::ConstraintMatrix& matrix = problem.getConstraintMatrix(); for (int i = 0; i < problem.getNRows(); ++i) { @@ -208,6 +214,41 @@ papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model return problem; } +// Host-only gather + papilo::Problem construction from an mps_data_model. +// Used by the mps-facing entry point: apply_presolve_from_mps_data() +template +papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model_t& mps, + problem_category_t category, + bool maximize) +{ + raft::common::nvtx::range fun_scope("Build papilo problem from mps_data_model"); + + papilo_problem_input_t input; + input.num_cols = mps.get_n_variables(); + input.num_rows = mps.get_n_constraints(); + input.nnz = mps.get_nnz(); + input.objective_offset = mps.get_objective_offset(); + input.coefficients = mps.get_constraint_matrix_values(); + input.offsets = mps.get_constraint_matrix_offsets(); + input.variables = mps.get_constraint_matrix_indices(); + input.obj_coeffs = mps.get_objective_coefficients(); + input.var_lb = mps.get_variable_lower_bounds(); + input.var_ub = mps.get_variable_upper_bounds(); + input.constr_lb = mps.get_constraint_lower_bounds(); + input.constr_ub = mps.get_constraint_upper_bounds(); + input.constraint_bounds = mps.get_constraint_bounds(); + input.row_types = mps.get_row_types(); + input.variable_names = mps.get_variable_names(); + + const auto& variable_types = mps.get_variable_types(); + input.var_types.reserve(variable_types.size()); + for (auto variable_type : variable_types) { + input.var_types.push_back(char_to_var_type(variable_type)); + } + + return build_papilo_problem_from_host(input, category, maximize); +} + // Host-only result builder: write a (reduced) PSLP presolver state into a // fresh mps_data_model_t. template @@ -399,24 +440,13 @@ pslp_input_t build_pslp_host_arrays_from_op_problem( return arrays; } -// D->H direct: gather op_problem's device buffers, then feed a -// papilo::ProblemBuilder. Mirrors build_papilo_problem_from_mps_data -// exactly (same normalisation and flag handling), but consumes op_problem's -// device-side arrays with a single stream sync — no mps_data_model roundtrip. -// var_types come out of op_problem as `var_t` enums so we skip the char <-> -// enum conversion the mps path needs. +// D->H direct: gather op_problem's device buffers, then feed the shared Papilo +// host builder. This avoids an mps_data_model roundtrip. template papilo::Problem build_papilo_problem_from_op_problem( const optimization_problem_t& op, problem_category_t category, bool maximize) { raft::common::nvtx::range fun_scope("Build papilo problem from op_problem (D->H)"); - papilo::ProblemBuilder builder; - - const i_t num_cols = op.get_n_variables(); - const i_t num_rows = op.get_n_constraints(); - const i_t nnz = op.get_nnz(); - - builder.reserve(nnz, num_rows, num_cols); const auto& d_coefficients = op.get_constraint_matrix_values(); const auto& d_indices = op.get_constraint_matrix_indices(); @@ -430,130 +460,40 @@ papilo::Problem build_papilo_problem_from_op_problem( const auto& d_row_types = op.get_row_types(); const auto& d_var_types = op.get_variable_types(); - std::vector h_coefficients(d_coefficients.size()); - std::vector h_variables(d_indices.size()); - std::vector h_offsets(d_offsets.size()); - std::vector h_obj_coeffs(d_obj_coeffs.size()); - std::vector h_var_lb(d_var_lb.size()); - std::vector h_var_ub(d_var_ub.size()); - std::vector h_constr_lb(d_constr_lb.size()); - std::vector h_constr_ub(d_constr_ub.size()); - std::vector h_bounds(d_bounds.size()); - std::vector h_row_types(d_row_types.size()); - std::vector h_var_types(d_var_types.size()); + papilo_problem_input_t input; + input.num_cols = op.get_n_variables(); + input.num_rows = op.get_n_constraints(); + input.nnz = op.get_nnz(); + input.objective_offset = op.get_objective_offset(); + input.variable_names = op.get_variable_names(); + + input.coefficients.resize(d_coefficients.size()); + input.variables.resize(d_indices.size()); + input.offsets.resize(d_offsets.size()); + input.obj_coeffs.resize(d_obj_coeffs.size()); + input.var_lb.resize(d_var_lb.size()); + input.var_ub.resize(d_var_ub.size()); + input.constr_lb.resize(d_constr_lb.size()); + input.constr_ub.resize(d_constr_ub.size()); + input.constraint_bounds.resize(d_bounds.size()); + input.row_types.resize(d_row_types.size()); + input.var_types.resize(d_var_types.size()); auto stream = op.get_handle_ptr()->get_stream(); - raft::copy(h_coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); - raft::copy(h_variables.data(), d_indices.data(), d_indices.size(), stream); - raft::copy(h_offsets.data(), d_offsets.data(), d_offsets.size(), stream); - raft::copy(h_obj_coeffs.data(), d_obj_coeffs.data(), d_obj_coeffs.size(), stream); - raft::copy(h_var_lb.data(), d_var_lb.data(), d_var_lb.size(), stream); - raft::copy(h_var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); - raft::copy(h_constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); - raft::copy(h_constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); - raft::copy(h_bounds.data(), d_bounds.data(), d_bounds.size(), stream); - raft::copy(h_row_types.data(), d_row_types.data(), d_row_types.size(), stream); - raft::copy(h_var_types.data(), d_var_types.data(), d_var_types.size(), stream); + raft::copy(input.coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); + raft::copy(input.variables.data(), d_indices.data(), d_indices.size(), stream); + raft::copy(input.offsets.data(), d_offsets.data(), d_offsets.size(), stream); + raft::copy(input.obj_coeffs.data(), d_obj_coeffs.data(), d_obj_coeffs.size(), stream); + raft::copy(input.var_lb.data(), d_var_lb.data(), d_var_lb.size(), stream); + raft::copy(input.var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); + raft::copy(input.constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); + raft::copy(input.constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); + raft::copy(input.constraint_bounds.data(), d_bounds.data(), d_bounds.size(), stream); + raft::copy(input.row_types.data(), d_row_types.data(), d_row_types.size(), stream); + raft::copy(input.var_types.data(), d_var_types.data(), d_var_types.size(), stream); stream.synchronize(); - if (maximize) { - for (auto& c : h_obj_coeffs) - c = -c; - } - - if (h_constr_lb.empty() && h_constr_ub.empty()) { - for (size_t i = 0; i < h_row_types.size(); ++i) { - if (h_row_types[i] == 'L') { - h_constr_lb.push_back(-std::numeric_limits::infinity()); - h_constr_ub.push_back(h_bounds[i]); - } else if (h_row_types[i] == 'G') { - h_constr_lb.push_back(h_bounds[i]); - h_constr_ub.push_back(std::numeric_limits::infinity()); - } else if (h_row_types[i] == 'E') { - h_constr_lb.push_back(h_bounds[i]); - h_constr_ub.push_back(h_bounds[i]); - } - } - } - - builder.setNumCols(num_cols); - builder.setNumRows(num_rows); - - builder.setObjAll(h_obj_coeffs); - builder.setObjOffset(maximize ? -op.get_objective_offset() : op.get_objective_offset()); - - if (!h_var_lb.empty() && !h_var_ub.empty()) { - builder.setColLbAll(h_var_lb); - builder.setColUbAll(h_var_ub); - if (op.get_variable_names().size() == static_cast(num_cols)) { - builder.setColNameAll(op.get_variable_names()); - } - } - - if (category == problem_category_t::MIP) { - for (size_t i = 0; i < h_var_types.size(); ++i) { - builder.setColIntegral(i, h_var_types[i] == var_t::INTEGER); - } - } - - if (!h_constr_lb.empty() && !h_constr_ub.empty()) { - builder.setRowLhsAll(h_constr_lb); - builder.setRowRhsAll(h_constr_ub); - } - - std::vector h_row_flags(h_constr_lb.size()); - std::vector> h_entries; - for (size_t i = 0; i < h_constr_lb.size(); ++i) { - i_t row_start = h_offsets[i]; - i_t row_end = h_offsets[i + 1]; - i_t num_entries = row_end - row_start; - for (size_t j = 0; j < num_entries; ++j) { - h_entries.push_back( - std::make_tuple(i, h_variables[row_start + j], h_coefficients[row_start + j])); - } - - if (h_constr_lb[i] == -std::numeric_limits::infinity()) { - h_row_flags[i].set(papilo::RowFlag::kLhsInf); - } else { - h_row_flags[i].unset(papilo::RowFlag::kLhsInf); - } - if (h_constr_ub[i] == std::numeric_limits::infinity()) { - h_row_flags[i].set(papilo::RowFlag::kRhsInf); - } else { - h_row_flags[i].unset(papilo::RowFlag::kRhsInf); - } - - if (h_constr_lb[i] == -std::numeric_limits::infinity()) { h_constr_lb[i] = 0; } - if (h_constr_ub[i] == std::numeric_limits::infinity()) { h_constr_ub[i] = 0; } - } - - for (size_t i = 0; i < h_var_lb.size(); ++i) { - builder.setColLbInf(i, h_var_lb[i] == -std::numeric_limits::infinity()); - builder.setColUbInf(i, h_var_ub[i] == std::numeric_limits::infinity()); - if (h_var_lb[i] == -std::numeric_limits::infinity()) { builder.setColLb(i, 0); } - if (h_var_ub[i] == std::numeric_limits::infinity()) { builder.setColUb(i, 0); } - } - - auto problem = builder.build(); - - if (h_entries.size()) { - auto constexpr const sorted_entries = true; - const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; - const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; - auto csr_storage = papilo::SparseStorage( - h_entries, num_rows, num_cols, sorted_entries, spare_ratio, min_inter_row_space); - problem.setConstraintMatrix(csr_storage, h_constr_lb, h_constr_ub, h_row_flags); - - papilo::ConstraintMatrix& matrix = problem.getConstraintMatrix(); - for (int i = 0; i < problem.getNRows(); ++i) { - papilo::RowFlags rowFlag = matrix.getRowFlags()[i]; - if (!rowFlag.test(papilo::RowFlag::kRhsInf) && !rowFlag.test(papilo::RowFlag::kLhsInf) && - matrix.getLeftHandSides()[i] == matrix.getRightHandSides()[i]) - matrix.getRowFlags()[i].set(papilo::RowFlag::kEquation); - } - } - - return problem; + return build_papilo_problem_from_host(input, category, maximize); } // H->D direct: read PSLP's reduced host arrays and build a device From e23aa6e4cce25cedb152bfcb924010eeffef0774 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Wed, 8 Jul 2026 23:24:00 +0200 Subject: [PATCH 200/258] disable nccl_p2p in ci for potentially more robustness --- ci/test_cpp_multi_gpu.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index f3f60fa177..5da8f22936 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -101,6 +101,8 @@ if [ "${#mg_tests[@]}" -eq 0 ]; then exit 0 fi +export NCCL_P2P_DISABLE=1 + EXITCODE=0 for gt in "${mg_tests[@]}"; do test_name=$(basename "${gt}") From 8966a907a703de82f84afd6accb0d8d9a6b2a816 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 10:40:51 +0200 Subject: [PATCH 201/258] run ci with NCCL_DEBUG=INFO --- ci/test_cpp_multi_gpu.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index 5da8f22936..298408feec 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -101,6 +101,9 @@ if [ "${#mg_tests[@]}" -eq 0 ]; then exit 0 fi +# NCCL diagnostics for CI logs when distributed PDLP halo exchange fails. +export NCCL_DEBUG=INFO +# PHB topology on the 2-GPU runner: disable direct GPU peer access (see topo above). export NCCL_P2P_DISABLE=1 EXITCODE=0 From 447963aff33b0c33028cd4fc1f0377928bcca76e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 14:38:34 +0200 Subject: [PATCH 202/258] download dataset in distributed --- ci/test_cpp_multi_gpu.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index 298408feec..0a53e4fd5d 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -78,6 +78,15 @@ if [ "${GPU_COUNT}" -lt 2 ]; then exit 1 fi +# Distributed PDLP parity tests read MPS instances (e.g. neos3, a2864) that are +# git-ignored and fetched from S3 — mirror ci/test_cpp.sh so the datasets exist +# and the tests resolve paths via RAPIDS_DATASET_ROOT_DIR. +rapids-logger "Download datasets" +./datasets/linear_programming/download_pdlp_test_dataset.sh + +RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)" +export RAPIDS_DATASET_ROOT_DIR + # Locate the installed gtest binaries (same search order as ci/run_ctests.sh). installed_test_location="${INSTALL_PREFIX:-${CONDA_PREFIX:-/usr}}/bin/gtests/libcuopt/" devcontainers_test_location="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../cpp/build/latest/gtests/libcuopt/" @@ -105,6 +114,12 @@ fi export NCCL_DEBUG=INFO # PHB topology on the 2-GPU runner: disable direct GPU peer access (see topo above). export NCCL_P2P_DISABLE=1 +# With P2P disabled NCCL falls back to the SHM transport, which allocates multi-MB +# buffers in /dev/shm. CI containers default to a 64 MB /dev/shm, so those +# allocations fail with "No space left on device". Disable SHM too and let NCCL +# use the socket transport for the intra-node exchange (functionally exercises the +# distributed PDLP communication path without depending on container --shm-size). +export NCCL_SHM_DISABLE=1 EXITCODE=0 for gt in "${mg_tests[@]}"; do From 9a290761e4256a47b9f52ead67fc4c269d8b2a21 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 15:16:37 +0200 Subject: [PATCH 203/258] mps based presolve --- .../presolve/third_party_presolve.cpp | 959 ++++++------------ .../presolve/third_party_presolve.hpp | 30 +- 2 files changed, 304 insertions(+), 685 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 8c248ec508..c55578791e 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -37,6 +37,7 @@ #endif #include +#include #include #include #include @@ -47,149 +48,144 @@ #include #include +#include #include #include #include namespace cuopt::mathematical_optimization::mip { -// Host-only gather for PSLP from an mps_data_model. -// Used by the mps-facing entry point: apply_presolve_from_mps_data() +// Backend-agnostic normalisation of the mutable presolve fields: +// * sign-flip `obj_coeffs` / `objective_offset` when maximise, +// * fill `var_lb` / `var_ub` with ±inf when the mps left them empty, +// * materialise ranged `constr_lb` / `constr_ub` from `row_types` + +// `constraint_bounds` when the ranged pair is absent from the mps. +// +// Precondition: the caller has already copy-initialised each vector from with mps data template -pslp_input_t build_pslp_host_arrays_from_mps_data( - const io::mps_data_model_t& mps) +void normalize_for_presolve(io::mps_data_model_t const& mps, + bool maximize, + std::vector& obj_coeffs, + f_t& objective_offset, + std::vector& var_lb, + std::vector& var_ub, + std::vector& constr_lb, + std::vector& constr_ub) { - raft::common::nvtx::range fun_scope("Build PSLP host arrays from mps_data_model"); - - pslp_input_t arrays; - arrays.n_cols = mps.get_n_variables(); - arrays.n_rows = mps.get_n_constraints(); - arrays.nnz = mps.get_nnz(); - - arrays.coefficients = mps.get_constraint_matrix_values(); - arrays.indices = mps.get_constraint_matrix_indices(); - arrays.offsets = mps.get_constraint_matrix_offsets(); - arrays.obj_coeffs = mps.get_objective_coefficients(); - arrays.var_lb = mps.get_variable_lower_bounds(); - arrays.var_ub = mps.get_variable_upper_bounds(); - arrays.constr_lb = mps.get_constraint_lower_bounds(); - arrays.constr_ub = mps.get_constraint_upper_bounds(); - arrays.constraint_bounds = mps.get_constraint_bounds(); - arrays.row_types = mps.get_row_types(); - - return arrays; -} - -template -struct papilo_problem_input_t { - i_t num_cols{0}; - i_t num_rows{0}; - i_t nnz{0}; - f_t objective_offset{0}; - std::vector coefficients; - std::vector offsets; - std::vector variables; - std::vector obj_coeffs; - std::vector var_lb; - std::vector var_ub; - std::vector constr_lb; - std::vector constr_ub; - std::vector constraint_bounds; - std::vector row_types; - std::vector var_types; - std::vector variable_names; - - void normalize(bool maximize) - { - if (maximize) { - for (auto& c : obj_coeffs) - c = -c; - objective_offset = -objective_offset; + if (maximize) { + for (auto& c : obj_coeffs) { + c = -c; } + objective_offset = -objective_offset; + } - if (constr_lb.empty() && constr_ub.empty()) { - for (size_t i = 0; i < row_types.size(); ++i) { - if (row_types[i] == 'L') { - constr_lb.push_back(-std::numeric_limits::infinity()); - constr_ub.push_back(constraint_bounds[i]); - } else if (row_types[i] == 'G') { - constr_lb.push_back(constraint_bounds[i]); - constr_ub.push_back(std::numeric_limits::infinity()); - } else if (row_types[i] == 'E') { - constr_lb.push_back(constraint_bounds[i]); - constr_ub.push_back(constraint_bounds[i]); - } + const i_t n_cols = mps.get_n_variables(); + if (var_lb.empty()) { var_lb.assign(n_cols, -std::numeric_limits::infinity()); } + if (var_ub.empty()) { var_ub.assign(n_cols, std::numeric_limits::infinity()); } + + if (constr_lb.empty() && constr_ub.empty()) { + const auto& row_types = mps.get_row_types(); + const auto& constraint_bounds = mps.get_constraint_bounds(); + for (size_t i = 0; i < row_types.size(); ++i) { + if (row_types[i] == 'L') { + constr_lb.push_back(-std::numeric_limits::infinity()); + constr_ub.push_back(constraint_bounds[i]); + } else if (row_types[i] == 'G') { + constr_lb.push_back(constraint_bounds[i]); + constr_ub.push_back(std::numeric_limits::infinity()); + } else if (row_types[i] == 'E') { + constr_lb.push_back(constraint_bounds[i]); + constr_ub.push_back(constraint_bounds[i]); } } } -}; +} +// Build a papilo::Problem template -papilo::Problem build_papilo_problem_from_host(papilo_problem_input_t& input, - problem_category_t category, - bool maximize) +papilo::Problem build_papilo_problem(io::mps_data_model_t const& mps, + bool maximize, + problem_category_t category) { - papilo::ProblemBuilder builder; + raft::common::nvtx::range fun_scope("Build papilo::Problem from mps_data_model"); + + const i_t n_cols = mps.get_n_variables(); + const i_t n_rows = mps.get_n_constraints(); + const i_t nnz = mps.get_nnz(); + + std::vector obj_coeffs(mps.get_objective_coefficients()); + std::vector var_lb(mps.get_variable_lower_bounds()); + std::vector var_ub(mps.get_variable_upper_bounds()); + std::vector constr_lb(mps.get_constraint_lower_bounds()); + std::vector constr_ub(mps.get_constraint_upper_bounds()); + f_t objective_offset = mps.get_objective_offset(); + normalize_for_presolve( + mps, maximize, obj_coeffs, objective_offset, var_lb, var_ub, constr_lb, constr_ub); + + const auto& coefficients = mps.get_constraint_matrix_values(); + const auto& indices = mps.get_constraint_matrix_indices(); + const auto& offsets = mps.get_constraint_matrix_offsets(); + const auto& variable_names = mps.get_variable_names(); + const auto& mps_var_types = mps.get_variable_types(); - input.normalize(maximize); - - builder.reserve(input.nnz, input.num_rows, input.num_cols); - builder.setNumCols(input.num_cols); - builder.setNumRows(input.num_rows); - - builder.setObjAll(input.obj_coeffs); - builder.setObjOffset(input.objective_offset); - - if (!input.var_lb.empty() && !input.var_ub.empty()) { - builder.setColLbAll(input.var_lb); - builder.setColUbAll(input.var_ub); - if (input.variable_names.size() == static_cast(input.num_cols)) { - builder.setColNameAll(input.variable_names); + papilo::ProblemBuilder builder; + builder.reserve(nnz, n_rows, n_cols); + builder.setNumCols(n_cols); + builder.setNumRows(n_rows); + builder.setObjAll(obj_coeffs); + builder.setObjOffset(objective_offset); + + if (!var_lb.empty() && !var_ub.empty()) { + builder.setColLbAll(var_lb); + builder.setColUbAll(var_ub); + if (variable_names.size() == static_cast(n_cols)) { + builder.setColNameAll(variable_names); } } if (category == problem_category_t::MIP) { - for (size_t i = 0; i < input.var_types.size(); ++i) { - builder.setColIntegral(i, input.var_types[i] == var_t::INTEGER); + for (size_t i = 0; i < mps_var_types.size(); ++i) { + builder.setColIntegral(i, char_to_var_type(mps_var_types[i]) == var_t::INTEGER); } } - if (!input.constr_lb.empty() && !input.constr_ub.empty()) { - builder.setRowLhsAll(input.constr_lb); - builder.setRowRhsAll(input.constr_ub); + if (!constr_lb.empty() && !constr_ub.empty()) { + builder.setRowLhsAll(constr_lb); + builder.setRowRhsAll(constr_ub); } - std::vector h_row_flags(input.constr_lb.size()); + std::vector h_row_flags(constr_lb.size()); std::vector> h_entries; - for (size_t i = 0; i < input.constr_lb.size(); ++i) { - i_t row_start = input.offsets[i]; - i_t row_end = input.offsets[i + 1]; - i_t num_entries = row_end - row_start; + for (size_t i = 0; i < constr_lb.size(); ++i) { + const i_t row_start = offsets[i]; + const i_t row_end = offsets[i + 1]; + const i_t num_entries = row_end - row_start; for (size_t j = 0; j < num_entries; ++j) { - h_entries.push_back( - std::make_tuple(i, input.variables[row_start + j], input.coefficients[row_start + j])); + h_entries.push_back(std::make_tuple(i, indices[row_start + j], coefficients[row_start + j])); } - if (input.constr_lb[i] == -std::numeric_limits::infinity()) { + if (constr_lb[i] == -std::numeric_limits::infinity()) { h_row_flags[i].set(papilo::RowFlag::kLhsInf); } else { h_row_flags[i].unset(papilo::RowFlag::kLhsInf); } - if (input.constr_ub[i] == std::numeric_limits::infinity()) { + if (constr_ub[i] == std::numeric_limits::infinity()) { h_row_flags[i].set(papilo::RowFlag::kRhsInf); } else { h_row_flags[i].unset(papilo::RowFlag::kRhsInf); } - if (input.constr_lb[i] == -std::numeric_limits::infinity()) { input.constr_lb[i] = 0; } - if (input.constr_ub[i] == std::numeric_limits::infinity()) { input.constr_ub[i] = 0; } + // Papilo stores finite dummies in place of ±inf; the flags above are the + // source of truth for infinity. Zero out the dummies before setConstraintMatrix. + if (constr_lb[i] == -std::numeric_limits::infinity()) { constr_lb[i] = 0; } + if (constr_ub[i] == std::numeric_limits::infinity()) { constr_ub[i] = 0; } } - for (size_t i = 0; i < input.var_lb.size(); ++i) { - builder.setColLbInf(i, input.var_lb[i] == -std::numeric_limits::infinity()); - builder.setColUbInf(i, input.var_ub[i] == std::numeric_limits::infinity()); - if (input.var_lb[i] == -std::numeric_limits::infinity()) { builder.setColLb(i, 0); } - if (input.var_ub[i] == std::numeric_limits::infinity()) { builder.setColUb(i, 0); } + for (size_t i = 0; i < var_lb.size(); ++i) { + builder.setColLbInf(i, var_lb[i] == -std::numeric_limits::infinity()); + builder.setColUbInf(i, var_ub[i] == std::numeric_limits::infinity()); + if (var_lb[i] == -std::numeric_limits::infinity()) { builder.setColLb(i, 0); } + if (var_ub[i] == std::numeric_limits::infinity()) { builder.setColUb(i, 0); } } auto problem = builder.build(); @@ -199,8 +195,8 @@ papilo::Problem build_papilo_problem_from_host(papilo_problem_input_t( - h_entries, input.num_rows, input.num_cols, sorted_entries, spare_ratio, min_inter_row_space); - problem.setConstraintMatrix(csr_storage, input.constr_lb, input.constr_ub, h_row_flags); + h_entries, n_rows, n_cols, sorted_entries, spare_ratio, min_inter_row_space); + problem.setConstraintMatrix(csr_storage, constr_lb, constr_ub, h_row_flags); papilo::ConstraintMatrix& matrix = problem.getConstraintMatrix(); for (int i = 0; i < problem.getNRows(); ++i) { @@ -214,132 +210,160 @@ papilo::Problem build_papilo_problem_from_host(papilo_problem_input_t -papilo::Problem build_papilo_problem_from_mps_data(const io::mps_data_model_t& mps, - problem_category_t category, - bool maximize) +third_party_presolve_status_t third_party_presolve_t::apply_pslp( + io::mps_data_model_t const& mps, double time_limit) { - raft::common::nvtx::range fun_scope("Build papilo problem from mps_data_model"); - - papilo_problem_input_t input; - input.num_cols = mps.get_n_variables(); - input.num_rows = mps.get_n_constraints(); - input.nnz = mps.get_nnz(); - input.objective_offset = mps.get_objective_offset(); - input.coefficients = mps.get_constraint_matrix_values(); - input.offsets = mps.get_constraint_matrix_offsets(); - input.variables = mps.get_constraint_matrix_indices(); - input.obj_coeffs = mps.get_objective_coefficients(); - input.var_lb = mps.get_variable_lower_bounds(); - input.var_ub = mps.get_variable_upper_bounds(); - input.constr_lb = mps.get_constraint_lower_bounds(); - input.constr_ub = mps.get_constraint_upper_bounds(); - input.constraint_bounds = mps.get_constraint_bounds(); - input.row_types = mps.get_row_types(); - input.variable_names = mps.get_variable_names(); - - const auto& variable_types = mps.get_variable_types(); - input.var_types.reserve(variable_types.size()); - for (auto variable_type : variable_types) { - input.var_types.push_back(char_to_var_type(variable_type)); - } + raft::common::nvtx::range fun_scope("Apply PSLP presolver on host"); - return build_papilo_problem_from_host(input, category, maximize); + cuopt_expects(std::is_same_v, + error_type_t::ValidationError, + "PSLP presolver only supports double precision"); + + const i_t n_cols = mps.get_n_variables(); + const i_t n_rows = mps.get_n_constraints(); + const i_t nnz = mps.get_nnz(); + + // Local owned copies of the fields that need mutation (sign flip on + // maximise / ±inf fill for empty bounds / row_types materialisation); + // matrix arrays are read straight from mps below (const&). + std::vector obj_coeffs(mps.get_objective_coefficients()); + std::vector var_lb(mps.get_variable_lower_bounds()); + std::vector var_ub(mps.get_variable_upper_bounds()); + std::vector constr_lb(mps.get_constraint_lower_bounds()); + std::vector constr_ub(mps.get_constraint_upper_bounds()); + f_t objective_offset = mps.get_objective_offset(); + normalize_for_presolve( + mps, maximize_, obj_coeffs, objective_offset, var_lb, var_ub, constr_lb, constr_ub); + + const auto& coefficients = mps.get_constraint_matrix_values(); + const auto& indices = mps.get_constraint_matrix_indices(); + const auto& offsets = mps.get_constraint_matrix_offsets(); + + Settings* settings = default_settings(); + settings->verbose = false; + settings->max_time = time_limit; + + auto start_time = std::chrono::high_resolution_clock::now(); + Presolver* presolver = new_presolver(coefficients.data(), + indices.data(), + offsets.data(), + n_rows, + n_cols, + nnz, + constr_lb.data(), + constr_ub.data(), + var_lb.data(), + var_ub.data(), + obj_coeffs.data(), + settings); + assert(presolver != nullptr && "Presolver initialization failed"); + const PresolveStatus pslp_status = run_presolver(presolver); + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + CUOPT_LOG_DEBUG("PSLP presolver time: %d milliseconds", duration.count()); + CUOPT_LOG_INFO("PSLP Presolved problem: %d constraints, %d variables, %d non-zeros", + presolver->stats->n_rows_reduced, + presolver->stats->n_cols_reduced, + presolver->stats->nnz_reduced); + + // Free previously allocated presolver and settings (if any) and stash the + // new ones so undo_pslp_host / build_reduced_mps_from_pslp can find them later. + if (pslp_presolver_ != nullptr) { free_presolver(pslp_presolver_); } + if (pslp_stgs_ != nullptr) { free_settings(pslp_stgs_); } + pslp_presolver_ = presolver; + pslp_stgs_ = settings; + + return convert_pslp_presolve_status_to_third_party_presolve_status(pslp_status); } -// Host-only result builder: write a (reduced) PSLP presolver state into a -// fresh mps_data_model_t. +// Reduced mps_data_model builders (one per backend). No intermediate view — +// each backend reads its own internal state and writes straight into a fresh +// mps_data_model_t. mps setters copy from the given spans/vectors into their +// internal owned storage, so the locals here can safely die on return. + template -io::mps_data_model_t build_mps_data_from_pslp(Presolver* pslp_presolver, - bool maximize, - f_t original_obj_offset) +io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_presolver, + bool maximize, + f_t original_obj_offset) { raft::common::nvtx::range fun_scope("Build mps_data_model from PSLP"); io::mps_data_model_t mps; - if constexpr (std::is_same_v) { - cuopt_expects(pslp_presolver != nullptr && pslp_presolver->reduced_prob != nullptr, - error_type_t::RuntimeError, - "PSLP presolver is not initialized"); - auto reduced_prob = pslp_presolver->reduced_prob; - const i_t n_rows = reduced_prob->m; - const i_t n_cols = reduced_prob->n; - const i_t nnz = reduced_prob->nnz; - f_t obj_offset = reduced_prob->obj_offset; - - obj_offset = maximize ? -obj_offset : obj_offset; - // PSLP does not allow setting an objective offset, so we fold the - // original input's offset into the reduced one. - obj_offset += original_obj_offset; - mps.set_objective_offset(obj_offset); + auto* reduced = pslp_presolver->reduced_prob; + const i_t n_rows = static_cast(reduced->m); + const i_t n_cols = static_cast(reduced->n); + const i_t nnz = static_cast(reduced->nnz); + // PSLP folds the sign flip into obj_offset for maximise problems, and does + // not track the original mps's objective offset — put both back. + const f_t obj_offset = + (maximize ? -reduced->obj_offset : reduced->obj_offset) + original_obj_offset; mps.set_maximize(maximize); + mps.set_objective_offset(obj_offset); if (n_cols == 0 && n_rows == 0) { - std::vector empty_offsets = {0}; + std::vector empty_offsets{0}; mps.set_csr_constraint_matrix( {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); return mps; } mps.set_csr_constraint_matrix( - std::span(reduced_prob->Ax, static_cast(nnz)), - std::span(reduced_prob->Ai, static_cast(nnz)), - std::span(reduced_prob->Ap, static_cast(n_rows + 1))); + std::span(reduced->Ax, static_cast(nnz)), + std::span(reduced->Ai, static_cast(nnz)), + std::span(reduced->Ap, static_cast(n_rows + 1))); - std::vector h_obj_coeffs(reduced_prob->c, reduced_prob->c + n_cols); if (maximize) { - for (auto& c : h_obj_coeffs) + std::vector h_obj_coeffs(reduced->c, reduced->c + n_cols); + for (auto& c : h_obj_coeffs) { c = -c; + } + mps.set_objective_coefficients( + std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); + } else { + mps.set_objective_coefficients(std::span(reduced->c, static_cast(n_cols))); } - mps.set_objective_coefficients(std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); mps.set_constraint_lower_bounds( - std::span(reduced_prob->lhs, static_cast(n_rows))); + std::span(reduced->lhs, static_cast(n_rows))); mps.set_constraint_upper_bounds( - std::span(reduced_prob->rhs, static_cast(n_rows))); - mps.set_variable_lower_bounds( - std::span(reduced_prob->lbs, static_cast(n_cols))); - mps.set_variable_upper_bounds( - std::span(reduced_prob->ubs, static_cast(n_cols))); - } else { - cuopt_expects(false, error_type_t::ValidationError, "PSLP only supports double precision"); - } + std::span(reduced->rhs, static_cast(n_rows))); + mps.set_variable_lower_bounds(std::span(reduced->lbs, static_cast(n_cols))); + mps.set_variable_upper_bounds(std::span(reduced->ubs, static_cast(n_cols))); + return mps; } -// Host-only result builder: write the (reduced) papilo::Problem into a fresh -// mps_data_model_t. template -io::mps_data_model_t build_mps_data_from_papilo( +io::mps_data_model_t build_reduced_mps_from_papilo( papilo::Problem const& papilo_problem, bool maximize) { - raft::common::nvtx::range fun_scope("Build mps_data_model from papilo"); + raft::common::nvtx::range fun_scope("Reduced mps <- Papilo"); io::mps_data_model_t mps; auto obj = papilo_problem.getObjective(); - mps.set_objective_offset(maximize ? -obj.offset : obj.offset); mps.set_maximize(maximize); + mps.set_objective_offset(maximize ? -obj.offset : obj.offset); if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { - std::vector h_offsets{0}; - mps.set_csr_constraint_matrix({}, {}, std::span(h_offsets.data(), h_offsets.size())); + std::vector empty_offsets{0}; + mps.set_csr_constraint_matrix( + {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); return mps; } + if (maximize) { - for (size_t i = 0; i < obj.coefficients.size(); ++i) { - obj.coefficients[i] = -obj.coefficients[i]; + for (auto& c : obj.coefficients) { + c = -c; } } mps.set_objective_coefficients( std::span(obj.coefficients.data(), obj.coefficients.size())); auto& constraint_matrix = papilo_problem.getConstraintMatrix(); - auto row_lower = constraint_matrix.getLeftHandSides(); - auto row_upper = constraint_matrix.getRightHandSides(); - auto col_lower = papilo_problem.getLowerBounds(); - auto col_upper = papilo_problem.getUpperBounds(); + // Row bounds: copy out (papilo returns by value) then substitute ±inf per flag. + auto row_lower = constraint_matrix.getLeftHandSides(); + auto row_upper = constraint_matrix.getRightHandSides(); auto row_flags = constraint_matrix.getRowFlags(); for (size_t i = 0; i < row_flags.size(); i++) { if (row_flags[i].test(papilo::RowFlag::kLhsInf)) { @@ -349,28 +373,29 @@ io::mps_data_model_t build_mps_data_from_papilo( row_upper[i] = std::numeric_limits::infinity(); } } - mps.set_constraint_lower_bounds(std::span(row_lower.data(), row_lower.size())); mps.set_constraint_upper_bounds(std::span(row_upper.data(), row_upper.size())); + // CSR offsets have to be synthesised from papilo's RangeInfo (non-contiguous + // in general); values and column indices are contiguous, so span in-place. auto [index_range, nrows] = constraint_matrix.getRangeInfo(); std::vector offsets(nrows + 1); - size_t start = index_range[0].start; + const size_t start = index_range[0].start; for (i_t i = 0; i < nrows; i++) { - offsets[i] = index_range[i].start - start; + offsets[i] = static_cast(index_range[i].start - start); } - offsets[nrows] = index_range[nrows - 1].end - start; - - i_t nnz = constraint_matrix.getNnz(); + offsets[nrows] = static_cast(index_range[nrows - 1].end - start); + const i_t nnz = static_cast(constraint_matrix.getNnz()); assert(offsets[nrows] == nnz); - const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); - mps.set_csr_constraint_matrix(std::span(&coeffs[start], static_cast(nnz)), std::span(&cols[start], static_cast(nnz)), std::span(offsets.data(), offsets.size())); + // Col bounds + var_types: same copy-then-fixup pattern. + auto col_lower = papilo_problem.getLowerBounds(); + auto col_upper = papilo_problem.getUpperBounds(); auto col_flags = papilo_problem.getColFlags(); std::vector var_types(col_flags.size()); for (size_t i = 0; i < col_flags.size(); i++) { @@ -382,7 +407,6 @@ io::mps_data_model_t build_mps_data_from_papilo( col_upper[i] = std::numeric_limits::infinity(); } } - mps.set_variable_lower_bounds(std::span(col_lower.data(), col_lower.size())); mps.set_variable_upper_bounds(std::span(col_upper.data(), col_upper.size())); mps.set_variable_types(var_types); @@ -390,267 +414,6 @@ io::mps_data_model_t build_mps_data_from_papilo( return mps; } -// D->H direct: gather op_problem's device buffers into a pslp_input_t -template -pslp_input_t build_pslp_host_arrays_from_op_problem( - const optimization_problem_t& op) -{ - raft::common::nvtx::range fun_scope("Build PSLP host arrays from op_problem (D->H)"); - - pslp_input_t arrays; - arrays.n_cols = op.get_n_variables(); - arrays.n_rows = op.get_n_constraints(); - arrays.nnz = op.get_nnz(); - - const auto& d_coefficients = op.get_constraint_matrix_values(); - const auto& d_indices = op.get_constraint_matrix_indices(); - const auto& d_offsets = op.get_constraint_matrix_offsets(); - const auto& d_obj_coeffs = op.get_objective_coefficients(); - const auto& d_var_lb = op.get_variable_lower_bounds(); - const auto& d_var_ub = op.get_variable_upper_bounds(); - const auto& d_constr_lb = op.get_constraint_lower_bounds(); - const auto& d_constr_ub = op.get_constraint_upper_bounds(); - const auto& d_bounds = op.get_constraint_bounds(); - const auto& d_row_types = op.get_row_types(); - - arrays.coefficients.resize(d_coefficients.size()); - arrays.indices.resize(d_indices.size()); - arrays.offsets.resize(d_offsets.size()); - arrays.obj_coeffs.resize(d_obj_coeffs.size()); - arrays.var_lb.resize(d_var_lb.size()); - arrays.var_ub.resize(d_var_ub.size()); - arrays.constr_lb.resize(d_constr_lb.size()); - arrays.constr_ub.resize(d_constr_ub.size()); - arrays.constraint_bounds.resize(d_bounds.size()); - arrays.row_types.resize(d_row_types.size()); - - auto stream = op.get_handle_ptr()->get_stream(); - raft::copy(arrays.coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); - raft::copy(arrays.indices.data(), d_indices.data(), d_indices.size(), stream); - raft::copy(arrays.offsets.data(), d_offsets.data(), d_offsets.size(), stream); - raft::copy(arrays.obj_coeffs.data(), d_obj_coeffs.data(), d_obj_coeffs.size(), stream); - raft::copy(arrays.var_lb.data(), d_var_lb.data(), d_var_lb.size(), stream); - raft::copy(arrays.var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); - raft::copy(arrays.constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); - raft::copy(arrays.constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); - raft::copy(arrays.constraint_bounds.data(), d_bounds.data(), d_bounds.size(), stream); - raft::copy(arrays.row_types.data(), d_row_types.data(), d_row_types.size(), stream); - stream.synchronize(); - - return arrays; -} - -// D->H direct: gather op_problem's device buffers, then feed the shared Papilo -// host builder. This avoids an mps_data_model roundtrip. -template -papilo::Problem build_papilo_problem_from_op_problem( - const optimization_problem_t& op, problem_category_t category, bool maximize) -{ - raft::common::nvtx::range fun_scope("Build papilo problem from op_problem (D->H)"); - - const auto& d_coefficients = op.get_constraint_matrix_values(); - const auto& d_indices = op.get_constraint_matrix_indices(); - const auto& d_offsets = op.get_constraint_matrix_offsets(); - const auto& d_obj_coeffs = op.get_objective_coefficients(); - const auto& d_var_lb = op.get_variable_lower_bounds(); - const auto& d_var_ub = op.get_variable_upper_bounds(); - const auto& d_constr_lb = op.get_constraint_lower_bounds(); - const auto& d_constr_ub = op.get_constraint_upper_bounds(); - const auto& d_bounds = op.get_constraint_bounds(); - const auto& d_row_types = op.get_row_types(); - const auto& d_var_types = op.get_variable_types(); - - papilo_problem_input_t input; - input.num_cols = op.get_n_variables(); - input.num_rows = op.get_n_constraints(); - input.nnz = op.get_nnz(); - input.objective_offset = op.get_objective_offset(); - input.variable_names = op.get_variable_names(); - - input.coefficients.resize(d_coefficients.size()); - input.variables.resize(d_indices.size()); - input.offsets.resize(d_offsets.size()); - input.obj_coeffs.resize(d_obj_coeffs.size()); - input.var_lb.resize(d_var_lb.size()); - input.var_ub.resize(d_var_ub.size()); - input.constr_lb.resize(d_constr_lb.size()); - input.constr_ub.resize(d_constr_ub.size()); - input.constraint_bounds.resize(d_bounds.size()); - input.row_types.resize(d_row_types.size()); - input.var_types.resize(d_var_types.size()); - - auto stream = op.get_handle_ptr()->get_stream(); - raft::copy(input.coefficients.data(), d_coefficients.data(), d_coefficients.size(), stream); - raft::copy(input.variables.data(), d_indices.data(), d_indices.size(), stream); - raft::copy(input.offsets.data(), d_offsets.data(), d_offsets.size(), stream); - raft::copy(input.obj_coeffs.data(), d_obj_coeffs.data(), d_obj_coeffs.size(), stream); - raft::copy(input.var_lb.data(), d_var_lb.data(), d_var_lb.size(), stream); - raft::copy(input.var_ub.data(), d_var_ub.data(), d_var_ub.size(), stream); - raft::copy(input.constr_lb.data(), d_constr_lb.data(), d_constr_lb.size(), stream); - raft::copy(input.constr_ub.data(), d_constr_ub.data(), d_constr_ub.size(), stream); - raft::copy(input.constraint_bounds.data(), d_bounds.data(), d_bounds.size(), stream); - raft::copy(input.row_types.data(), d_row_types.data(), d_row_types.size(), stream); - raft::copy(input.var_types.data(), d_var_types.data(), d_var_types.size(), stream); - stream.synchronize(); - - return build_papilo_problem_from_host(input, category, maximize); -} - -// H->D direct: read PSLP's reduced host arrays and build a device -// optimization_problem_t on `handle`. Mirrors build_mps_data_from_pslp but -// skips the mps_data_model intermediate — arrays flow straight from PSLP into -// op_problem setters (which each perform an async raft::copy on handle's -// stream). We sync once at the end so the local sign-flipped obj_coeffs -// temp is safe to release. -template -optimization_problem_t build_op_problem_from_pslp(raft::handle_t const* handle, - Presolver* pslp_presolver, - bool maximize, - f_t original_obj_offset) -{ - raft::common::nvtx::range fun_scope("Build op_problem from PSLP (H->D)"); - optimization_problem_t op(handle); - - if constexpr (std::is_same_v) { - cuopt_expects(pslp_presolver != nullptr && pslp_presolver->reduced_prob != nullptr, - error_type_t::RuntimeError, - "PSLP presolver is not initialized"); - auto reduced_prob = pslp_presolver->reduced_prob; - const i_t n_rows = reduced_prob->m; - const i_t n_cols = reduced_prob->n; - const i_t nnz = reduced_prob->nnz; - f_t obj_offset = reduced_prob->obj_offset; - - obj_offset = maximize ? -obj_offset : obj_offset; - // PSLP does not allow setting an objective offset, so we fold the - // original input's offset into the reduced one. - obj_offset += original_obj_offset; - op.set_maximize(maximize); - op.set_objective_offset(obj_offset); - - if (n_cols == 0 && n_rows == 0) { - std::vector empty_offsets{0}; - op.set_csr_constraint_matrix(nullptr, 0, nullptr, 0, empty_offsets.data(), 1); - handle->sync_stream(); - return op; - } - - op.set_csr_constraint_matrix( - reduced_prob->Ax, nnz, reduced_prob->Ai, nnz, reduced_prob->Ap, n_rows + 1); - - std::vector h_obj_coeffs(reduced_prob->c, reduced_prob->c + n_cols); - if (maximize) { - for (auto& c : h_obj_coeffs) - c = -c; - } - op.set_objective_coefficients(h_obj_coeffs.data(), n_cols); - - op.set_constraint_lower_bounds(reduced_prob->lhs, n_rows); - op.set_constraint_upper_bounds(reduced_prob->rhs, n_rows); - op.set_variable_lower_bounds(reduced_prob->lbs, n_cols); - op.set_variable_upper_bounds(reduced_prob->ubs, n_cols); - - // set_* enqueued async raft::copy on the handle's stream; sync so the - // sign-flipped h_obj_coeffs temp lives long enough. - handle->sync_stream(); - } else { - cuopt_expects(false, error_type_t::ValidationError, "PSLP only supports double precision"); - } - return op; -} - -// H->D direct: read the reduced papilo::Problem and build a device -// optimization_problem_t on `handle`. Mirrors build_mps_data_from_papilo but -// skips the mps_data_model intermediate. var_types are produced as `var_t` -// enums (no char <-> enum round-trip). Single stream sync at the end so -// temporaries (var_types, bound arrays with inf substitution, sign-flipped -// obj_coeffs) live past the async H->D copies. -template -optimization_problem_t build_op_problem_from_papilo( - raft::handle_t const* handle, papilo::Problem const& papilo_problem, bool maximize) -{ - raft::common::nvtx::range fun_scope("Build op_problem from papilo (H->D)"); - optimization_problem_t op(handle); - - auto obj = papilo_problem.getObjective(); - op.set_maximize(maximize); - op.set_objective_offset(maximize ? -obj.offset : obj.offset); - - if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { - std::vector h_offsets{0}; - op.set_csr_constraint_matrix(nullptr, 0, nullptr, 0, h_offsets.data(), 1); - handle->sync_stream(); - return op; - } - - if (maximize) { - for (size_t i = 0; i < obj.coefficients.size(); ++i) { - obj.coefficients[i] = -obj.coefficients[i]; - } - } - op.set_objective_coefficients(obj.coefficients.data(), static_cast(obj.coefficients.size())); - - auto& constraint_matrix = papilo_problem.getConstraintMatrix(); - auto row_lower = constraint_matrix.getLeftHandSides(); - auto row_upper = constraint_matrix.getRightHandSides(); - auto col_lower = papilo_problem.getLowerBounds(); - auto col_upper = papilo_problem.getUpperBounds(); - - auto row_flags = constraint_matrix.getRowFlags(); - for (size_t i = 0; i < row_flags.size(); i++) { - if (row_flags[i].test(papilo::RowFlag::kLhsInf)) { - row_lower[i] = -std::numeric_limits::infinity(); - } - if (row_flags[i].test(papilo::RowFlag::kRhsInf)) { - row_upper[i] = std::numeric_limits::infinity(); - } - } - - op.set_constraint_lower_bounds(row_lower.data(), static_cast(row_lower.size())); - op.set_constraint_upper_bounds(row_upper.data(), static_cast(row_upper.size())); - - auto [index_range, nrows] = constraint_matrix.getRangeInfo(); - std::vector offsets(nrows + 1); - size_t start = index_range[0].start; - for (i_t i = 0; i < nrows; i++) { - offsets[i] = index_range[i].start - start; - } - offsets[nrows] = index_range[nrows - 1].end - start; - - i_t nnz = constraint_matrix.getNnz(); - assert(offsets[nrows] == nnz); - - const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); - const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); - - op.set_csr_constraint_matrix( - &coeffs[start], nnz, &cols[start], nnz, offsets.data(), static_cast(offsets.size())); - - auto col_flags = papilo_problem.getColFlags(); - std::vector var_types(col_flags.size()); - for (size_t i = 0; i < col_flags.size(); i++) { - var_types[i] = - col_flags[i].test(papilo::ColFlag::kIntegral) ? var_t::INTEGER : var_t::CONTINUOUS; - if (col_flags[i].test(papilo::ColFlag::kLbInf)) { - col_lower[i] = -std::numeric_limits::infinity(); - } - if (col_flags[i].test(papilo::ColFlag::kUbInf)) { - col_upper[i] = std::numeric_limits::infinity(); - } - } - - op.set_variable_lower_bounds(col_lower.data(), static_cast(col_lower.size())); - op.set_variable_upper_bounds(col_upper.data(), static_cast(col_upper.size())); - op.set_variable_types(var_types.data(), static_cast(var_types.size())); - - // Every set_* above enqueued async raft::copy on handle's stream. Sync so - // the temporary vectors (var_types, adjusted lb/ub, offsets, sign-flipped - // obj coefficients) outlive the copies. - handle->sync_stream(); - - return op; -} - void check_presolve_status(const papilo::PresolveStatus& status) { switch (status) { @@ -798,86 +561,6 @@ void set_presolve_parameters(papilo::Presolve& presolver, } } -template -void pslp_input_t::normalize_for_pslp(bool maximize) -{ - if (maximize) { - for (auto& c : obj_coeffs) { - c = -c; - } - } - - if (constr_lb.empty() && constr_ub.empty()) { - for (size_t i = 0; i < row_types.size(); ++i) { - if (row_types[i] == 'L') { - constr_lb.push_back(-std::numeric_limits::infinity()); - constr_ub.push_back(constraint_bounds[i]); - } else if (row_types[i] == 'G') { - constr_lb.push_back(constraint_bounds[i]); - constr_ub.push_back(std::numeric_limits::infinity()); - } else if (row_types[i] == 'E') { - constr_lb.push_back(constraint_bounds[i]); - constr_ub.push_back(constraint_bounds[i]); - } - } - } - - if (var_lb.empty()) { var_lb.assign(n_cols, -std::numeric_limits::infinity()); } - if (var_ub.empty()) { var_ub.assign(n_cols, std::numeric_limits::infinity()); } -} - -template -third_party_presolve_status_t third_party_presolve_t::apply_pslp( - pslp_input_t& arrays, double time_limit) -{ - if constexpr (std::is_same_v) { - raft::common::nvtx::range fun_scope("Apply PSLP presolver on host"); - - arrays.normalize_for_pslp(maximize_); - - Settings* settings = default_settings(); - settings->verbose = false; - settings->max_time = time_limit; - - auto start_time = std::chrono::high_resolution_clock::now(); - Presolver* presolver = new_presolver(arrays.coefficients.data(), - arrays.indices.data(), - arrays.offsets.data(), - arrays.n_rows, - arrays.n_cols, - arrays.nnz, - arrays.constr_lb.data(), - arrays.constr_ub.data(), - arrays.var_lb.data(), - arrays.var_ub.data(), - arrays.obj_coeffs.data(), - settings); - assert(presolver != nullptr && "Presolver initialization failed"); - const PresolveStatus pslp_status = run_presolver(presolver); - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - CUOPT_LOG_DEBUG("PSLP presolver time: %d milliseconds", duration.count()); - CUOPT_LOG_INFO("PSLP Presolved problem: %d constraints, %d variables, %d non-zeros", - presolver->stats->n_rows_reduced, - presolver->stats->n_cols_reduced, - presolver->stats->nnz_reduced); - - // Free previously allocated presolver and settings (if any) and stash the - // new ones so undo_pslp_host / build_{mps_data,op_problem}_from_pslp can - // find them later. - if (pslp_presolver_ != nullptr) { free_presolver(pslp_presolver_); } - if (pslp_stgs_ != nullptr) { free_settings(pslp_stgs_); } - pslp_presolver_ = presolver; - pslp_stgs_ = settings; - - return convert_pslp_presolve_status_to_third_party_presolve_status(pslp_status); - } else { - cuopt_expects( - false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); - return third_party_presolve_status_t::UNCHANGED; // unreachable - } -} - template third_party_presolve_status_t third_party_presolve_t::apply_papilo( papilo::Problem& papilo_problem, @@ -957,7 +640,8 @@ third_party_presolve_status_t third_party_presolve_t::apply_papilo( return status; } -// Direct op_problem -> presolver_input -> apply -> op_problem path +// Wrapper around apply_presolve_from_mps_data +// Generates an mps_data, presolve it and turn it back into a reduced op_problem template third_party_presolve_device_result_t third_party_presolve_t::apply_presolve_from_op_problem( @@ -971,15 +655,7 @@ third_party_presolve_t::apply_presolve_from_op_problem( i_t num_cpu_threads) { auto* handle = op_problem.get_handle_ptr(); - presolver_ = presolver; - maximize_ = op_problem.get_sense(); - cuopt_expects(!(category == problem_category_t::MIP && - presolver == cuopt::mathematical_optimization::presolver_t::PSLP), - error_type_t::RuntimeError, - "PSLP presolver is not supported for MIP problems"); - - // Neither PSLP nor Papilo handle quadratic objective / constraints. cuopt_expects(!op_problem.has_quadratic_objective(), error_type_t::ValidationError, "Presolve does not support optimization_problem with a quadratic objective"); @@ -987,75 +663,43 @@ third_party_presolve_t::apply_presolve_from_op_problem( error_type_t::ValidationError, "Presolve does not support optimization_problem with quadratic constraints"); - // PSLP branch: D->H gather -> apply_pslp (host) -> H->D build - if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { - if constexpr (std::is_same_v) { - const f_t original_obj_offset = op_problem.get_objective_offset(); - auto arrays = build_pslp_host_arrays_from_op_problem(op_problem); - auto status = apply_pslp(arrays, time_limit); - - if (status == third_party_presolve_status_t::INFEASIBLE || - status == third_party_presolve_status_t::UNBNDORINFEAS) { - return third_party_presolve_device_result_t{ - status, optimization_problem_t(handle), {}, {}, {}}; - } - - auto reduced_opt = build_op_problem_from_pslp( - handle, pslp_presolver_, maximize_, original_obj_offset); - reduced_opt.set_problem_name(op_problem.get_problem_name()); - reduced_opt.set_objective_scaling_factor(op_problem.get_objective_scaling_factor()); - reduced_opt.set_problem_category(category); - return third_party_presolve_device_result_t{ - status, std::move(reduced_opt), {}, {}, {}}; - } else { - cuopt_expects( - false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); - return third_party_presolve_device_result_t{ - third_party_presolve_status_t::UNCHANGED, - optimization_problem_t(handle), - {}, - {}, - {}}; // unreachable - } - } else { - // Papilo branch: D->H gather -> apply_papilo (host) -> H->D build - auto papilo_problem = build_papilo_problem_from_op_problem(op_problem, category, maximize_); - auto status = apply_papilo(papilo_problem, - category, - dual_postsolve, - absolute_tolerance, - relative_tolerance, - time_limit, - num_cpu_threads); - - if (status == third_party_presolve_status_t::INFEASIBLE || - status == third_party_presolve_status_t::UNBOUNDED || - status == third_party_presolve_status_t::UNBNDORINFEAS) { - return third_party_presolve_device_result_t{ - status, optimization_problem_t(handle), {}, {}, {}}; - } - - auto reduced_opt = build_op_problem_from_papilo(handle, papilo_problem, maximize_); - reduced_opt.set_problem_name(op_problem.get_problem_name()); - reduced_opt.set_objective_scaling_factor(op_problem.get_objective_scaling_factor()); - reduced_opt.set_problem_category(category); - - std::vector implied_integer_indices; - { - auto col_flags = papilo_problem.getColFlags(); - for (size_t i = 0; i < col_flags.size(); ++i) { - if (col_flags[i].test(papilo::ColFlag::kImplInt)) { - implied_integer_indices.push_back(static_cast(i)); - } - } - } - - return third_party_presolve_device_result_t{status, - std::move(reduced_opt), - std::move(implied_integer_indices), - reduced_to_original_map_, - original_to_reduced_map_}; + auto mps = ::cuopt::mathematical_optimization::op_problem_to_mps_data_model(op_problem); + + auto host_res = apply_presolve_from_mps_data(mps, + category, + presolver, + dual_postsolve, + absolute_tolerance, + relative_tolerance, + time_limit, + num_cpu_threads); + + // On terminal statuses the mps entry returns an empty reduced problem; + // mirror that shape on the device side without going through H->D. + if (host_res.status == third_party_presolve_status_t::INFEASIBLE || + host_res.status == third_party_presolve_status_t::UNBOUNDED || + host_res.status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_device_result_t{ + host_res.status, + optimization_problem_t(handle), + std::move(host_res.implied_integer_indices), + std::move(host_res.reduced_to_original_map), + std::move(host_res.original_to_reduced_map)}; } + + auto reduced_opt = + ::cuopt::mathematical_optimization::mps_data_model_to_optimization_problem( + handle, host_res.reduced_problem); + // mps_data_model doesn't carry problem_category; plumb it here so the + // reduced op_problem matches the input's category + reduced_opt.set_problem_category(category); + + return third_party_presolve_device_result_t{ + host_res.status, + std::move(reduced_opt), + std::move(host_res.implied_integer_indices), + std::move(host_res.reduced_to_original_map), + std::move(host_res.original_to_reduced_map)}; } template @@ -1086,72 +730,63 @@ third_party_presolve_t::apply_presolve_from_mps_data( error_type_t::ValidationError, "Presolve does not support mps_data_models with quadratic constraints"); - // PSLP branch: host gather -> apply_pslp (host) -> host build + // PSLP branch: apply_pslp (host, reads mps directly) -> reduced mps. if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { - if constexpr (std::is_same_v) { - const f_t original_obj_offset = mps.get_objective_offset(); - auto arrays = build_pslp_host_arrays_from_mps_data(mps); - auto status = apply_pslp(arrays, time_limit); - - if (status == third_party_presolve_status_t::INFEASIBLE || - status == third_party_presolve_status_t::UNBNDORINFEAS) { - return third_party_presolve_host_result_t{ - status, io::mps_data_model_t{}, {}, {}, {}}; - } - - auto reduced_mps = - build_mps_data_from_pslp(pslp_presolver_, maximize_, original_obj_offset); - reduced_mps.set_problem_name(mps.get_problem_name()); - reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); - return third_party_presolve_host_result_t{ - status, std::move(reduced_mps), {}, {}, {}}; - } else { - cuopt_expects( - false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); - return third_party_presolve_host_result_t{third_party_presolve_status_t::UNCHANGED, - io::mps_data_model_t{}, - {}, - {}, - {}}; // unreachable - } - } else { - // Papilo branch: host gather -> apply_papilo (host) -> host build - auto papilo_problem = build_papilo_problem_from_mps_data(mps, category, maximize_); - auto status = apply_papilo(papilo_problem, - category, - dual_postsolve, - absolute_tolerance, - relative_tolerance, - time_limit, - num_cpu_threads); + const f_t original_obj_offset = mps.get_objective_offset(); + auto status = apply_pslp(mps, time_limit); if (status == third_party_presolve_status_t::INFEASIBLE || - status == third_party_presolve_status_t::UNBOUNDED || status == third_party_presolve_status_t::UNBNDORINFEAS) { return third_party_presolve_host_result_t{ status, io::mps_data_model_t{}, {}, {}, {}}; } - auto reduced_mps = build_mps_data_from_papilo(papilo_problem, maximize_); + auto reduced_mps = + build_reduced_mps_from_pslp(pslp_presolver_, maximize_, original_obj_offset); reduced_mps.set_problem_name(mps.get_problem_name()); reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); + return third_party_presolve_host_result_t{status, std::move(reduced_mps), {}, {}, {}}; + } +else +{ + // Papilo branch: build papilo::Problem (host, reads mps directly) -> + // apply_papilo -> reduced mps. + auto papilo_problem = build_papilo_problem(mps, maximize_, category); + auto status = apply_papilo(papilo_problem, + category, + dual_postsolve, + absolute_tolerance, + relative_tolerance, + time_limit, + num_cpu_threads); + + if (status == third_party_presolve_status_t::INFEASIBLE || + status == third_party_presolve_status_t::UNBOUNDED || + status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_host_result_t{ + status, io::mps_data_model_t{}, {}, {}, {}}; + } - std::vector implied_integer_indices; - { - auto col_flags = papilo_problem.getColFlags(); - for (size_t i = 0; i < col_flags.size(); ++i) { - if (col_flags[i].test(papilo::ColFlag::kImplInt)) { - implied_integer_indices.push_back(static_cast(i)); - } + auto reduced_mps = build_reduced_mps_from_papilo(papilo_problem, maximize_); + reduced_mps.set_problem_name(mps.get_problem_name()); + reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); + + std::vector implied_integer_indices; + { + auto col_flags = papilo_problem.getColFlags(); + for (size_t i = 0; i < col_flags.size(); ++i) { + if (col_flags[i].test(papilo::ColFlag::kImplInt)) { + implied_integer_indices.push_back(static_cast(i)); } } - - return third_party_presolve_host_result_t{status, - std::move(reduced_mps), - std::move(implied_integer_indices), - reduced_to_original_map_, - original_to_reduced_map_}; } + + return third_party_presolve_host_result_t{status, + std::move(reduced_mps), + std::move(implied_integer_indices), + reduced_to_original_map_, + original_to_reduced_map_}; +} } template diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 2e9bcb93d3..53632e673a 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -58,27 +59,6 @@ template using third_party_presolve_host_result_t = third_party_presolve_result_t>; -// Host-side PSLP input: every buffer PSLP's C API needs, plus dimensions. -template -struct pslp_input_t { - std::vector coefficients; - std::vector indices; - std::vector offsets; - std::vector obj_coeffs; - std::vector var_lb; - std::vector var_ub; - std::vector constr_lb; - std::vector constr_ub; - std::vector constraint_bounds; - std::vector row_types; - i_t n_rows{0}; - i_t n_cols{0}; - i_t nnz{0}; - - // Flip to minimise sense and materialise implicit bounds before PSLP runs. - void normalize_for_pslp(bool maximize); -}; - template class third_party_presolve_t { public: @@ -92,7 +72,7 @@ class third_party_presolve_t { third_party_presolve_t& operator=(third_party_presolve_t&&) = delete; // Device entry: takes an optimization_problem_t and returns a device-side - // reduced optimization_problem_t. + // reduced optimization_problem_t. This is a wrapper around apply_presolve_from_mps_data. third_party_presolve_device_result_t apply_presolve_from_op_problem( optimization_problem_t const& op_problem, problem_category_t category, @@ -153,7 +133,8 @@ class third_party_presolve_t { ~third_party_presolve_t(); private: - third_party_presolve_status_t apply_pslp(pslp_input_t& arrays, double time_limit); + third_party_presolve_status_t apply_pslp(io::mps_data_model_t const& mps, + double time_limit); third_party_presolve_status_t apply_papilo(papilo::Problem& papilo_problem, problem_category_t category, @@ -181,6 +162,9 @@ class third_party_presolve_t { Settings* pslp_stgs_{nullptr}; Presolver* pslp_presolver_{nullptr}; + // Necessary due to a nvcc bug due to papilo's constexpr functions + // Keep the papilo includes in the .cpp to avoid bringing them + // into any .cu context std::unique_ptr, papilo_postsolve_deleter> papilo_post_solve_storage_; From 7db5e8d1581e01642e95a087cfb3c8d722b482c7 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 15:30:25 +0200 Subject: [PATCH 204/258] moved helpers up and put back expects in constexpr --- .../presolve/third_party_presolve.cpp | 414 +++++++++--------- 1 file changed, 210 insertions(+), 204 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index c55578791e..c361d35077 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -210,210 +210,9 @@ papilo::Problem build_papilo_problem(io::mps_data_model_t const& return problem; } -template -third_party_presolve_status_t third_party_presolve_t::apply_pslp( - io::mps_data_model_t const& mps, double time_limit) -{ - raft::common::nvtx::range fun_scope("Apply PSLP presolver on host"); - - cuopt_expects(std::is_same_v, - error_type_t::ValidationError, - "PSLP presolver only supports double precision"); - - const i_t n_cols = mps.get_n_variables(); - const i_t n_rows = mps.get_n_constraints(); - const i_t nnz = mps.get_nnz(); - - // Local owned copies of the fields that need mutation (sign flip on - // maximise / ±inf fill for empty bounds / row_types materialisation); - // matrix arrays are read straight from mps below (const&). - std::vector obj_coeffs(mps.get_objective_coefficients()); - std::vector var_lb(mps.get_variable_lower_bounds()); - std::vector var_ub(mps.get_variable_upper_bounds()); - std::vector constr_lb(mps.get_constraint_lower_bounds()); - std::vector constr_ub(mps.get_constraint_upper_bounds()); - f_t objective_offset = mps.get_objective_offset(); - normalize_for_presolve( - mps, maximize_, obj_coeffs, objective_offset, var_lb, var_ub, constr_lb, constr_ub); - - const auto& coefficients = mps.get_constraint_matrix_values(); - const auto& indices = mps.get_constraint_matrix_indices(); - const auto& offsets = mps.get_constraint_matrix_offsets(); - - Settings* settings = default_settings(); - settings->verbose = false; - settings->max_time = time_limit; - - auto start_time = std::chrono::high_resolution_clock::now(); - Presolver* presolver = new_presolver(coefficients.data(), - indices.data(), - offsets.data(), - n_rows, - n_cols, - nnz, - constr_lb.data(), - constr_ub.data(), - var_lb.data(), - var_ub.data(), - obj_coeffs.data(), - settings); - assert(presolver != nullptr && "Presolver initialization failed"); - const PresolveStatus pslp_status = run_presolver(presolver); - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - CUOPT_LOG_DEBUG("PSLP presolver time: %d milliseconds", duration.count()); - CUOPT_LOG_INFO("PSLP Presolved problem: %d constraints, %d variables, %d non-zeros", - presolver->stats->n_rows_reduced, - presolver->stats->n_cols_reduced, - presolver->stats->nnz_reduced); - - // Free previously allocated presolver and settings (if any) and stash the - // new ones so undo_pslp_host / build_reduced_mps_from_pslp can find them later. - if (pslp_presolver_ != nullptr) { free_presolver(pslp_presolver_); } - if (pslp_stgs_ != nullptr) { free_settings(pslp_stgs_); } - pslp_presolver_ = presolver; - pslp_stgs_ = settings; - - return convert_pslp_presolve_status_to_third_party_presolve_status(pslp_status); -} - -// Reduced mps_data_model builders (one per backend). No intermediate view — -// each backend reads its own internal state and writes straight into a fresh -// mps_data_model_t. mps setters copy from the given spans/vectors into their -// internal owned storage, so the locals here can safely die on return. - -template -io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_presolver, - bool maximize, - f_t original_obj_offset) -{ - raft::common::nvtx::range fun_scope("Build mps_data_model from PSLP"); - io::mps_data_model_t mps; - - auto* reduced = pslp_presolver->reduced_prob; - const i_t n_rows = static_cast(reduced->m); - const i_t n_cols = static_cast(reduced->n); - const i_t nnz = static_cast(reduced->nnz); - // PSLP folds the sign flip into obj_offset for maximise problems, and does - // not track the original mps's objective offset — put both back. - const f_t obj_offset = - (maximize ? -reduced->obj_offset : reduced->obj_offset) + original_obj_offset; - mps.set_maximize(maximize); - mps.set_objective_offset(obj_offset); - - if (n_cols == 0 && n_rows == 0) { - std::vector empty_offsets{0}; - mps.set_csr_constraint_matrix( - {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); - return mps; - } - - mps.set_csr_constraint_matrix( - std::span(reduced->Ax, static_cast(nnz)), - std::span(reduced->Ai, static_cast(nnz)), - std::span(reduced->Ap, static_cast(n_rows + 1))); - - if (maximize) { - std::vector h_obj_coeffs(reduced->c, reduced->c + n_cols); - for (auto& c : h_obj_coeffs) { - c = -c; - } - mps.set_objective_coefficients( - std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); - } else { - mps.set_objective_coefficients(std::span(reduced->c, static_cast(n_cols))); - } - mps.set_constraint_lower_bounds( - std::span(reduced->lhs, static_cast(n_rows))); - mps.set_constraint_upper_bounds( - std::span(reduced->rhs, static_cast(n_rows))); - mps.set_variable_lower_bounds(std::span(reduced->lbs, static_cast(n_cols))); - mps.set_variable_upper_bounds(std::span(reduced->ubs, static_cast(n_cols))); - - return mps; -} - -template -io::mps_data_model_t build_reduced_mps_from_papilo( - papilo::Problem const& papilo_problem, bool maximize) -{ - raft::common::nvtx::range fun_scope("Reduced mps <- Papilo"); - io::mps_data_model_t mps; - - auto obj = papilo_problem.getObjective(); - mps.set_maximize(maximize); - mps.set_objective_offset(maximize ? -obj.offset : obj.offset); - - if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { - std::vector empty_offsets{0}; - mps.set_csr_constraint_matrix( - {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); - return mps; - } - - if (maximize) { - for (auto& c : obj.coefficients) { - c = -c; - } - } - mps.set_objective_coefficients( - std::span(obj.coefficients.data(), obj.coefficients.size())); - - auto& constraint_matrix = papilo_problem.getConstraintMatrix(); - - // Row bounds: copy out (papilo returns by value) then substitute ±inf per flag. - auto row_lower = constraint_matrix.getLeftHandSides(); - auto row_upper = constraint_matrix.getRightHandSides(); - auto row_flags = constraint_matrix.getRowFlags(); - for (size_t i = 0; i < row_flags.size(); i++) { - if (row_flags[i].test(papilo::RowFlag::kLhsInf)) { - row_lower[i] = -std::numeric_limits::infinity(); - } - if (row_flags[i].test(papilo::RowFlag::kRhsInf)) { - row_upper[i] = std::numeric_limits::infinity(); - } - } - mps.set_constraint_lower_bounds(std::span(row_lower.data(), row_lower.size())); - mps.set_constraint_upper_bounds(std::span(row_upper.data(), row_upper.size())); - - // CSR offsets have to be synthesised from papilo's RangeInfo (non-contiguous - // in general); values and column indices are contiguous, so span in-place. - auto [index_range, nrows] = constraint_matrix.getRangeInfo(); - std::vector offsets(nrows + 1); - const size_t start = index_range[0].start; - for (i_t i = 0; i < nrows; i++) { - offsets[i] = static_cast(index_range[i].start - start); - } - offsets[nrows] = static_cast(index_range[nrows - 1].end - start); - const i_t nnz = static_cast(constraint_matrix.getNnz()); - assert(offsets[nrows] == nnz); - const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); - const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); - mps.set_csr_constraint_matrix(std::span(&coeffs[start], static_cast(nnz)), - std::span(&cols[start], static_cast(nnz)), - std::span(offsets.data(), offsets.size())); - - // Col bounds + var_types: same copy-then-fixup pattern. - auto col_lower = papilo_problem.getLowerBounds(); - auto col_upper = papilo_problem.getUpperBounds(); - auto col_flags = papilo_problem.getColFlags(); - std::vector var_types(col_flags.size()); - for (size_t i = 0; i < col_flags.size(); i++) { - var_types[i] = col_flags[i].test(papilo::ColFlag::kIntegral) ? 'I' : 'C'; - if (col_flags[i].test(papilo::ColFlag::kLbInf)) { - col_lower[i] = -std::numeric_limits::infinity(); - } - if (col_flags[i].test(papilo::ColFlag::kUbInf)) { - col_upper[i] = std::numeric_limits::infinity(); - } - } - mps.set_variable_lower_bounds(std::span(col_lower.data(), col_lower.size())); - mps.set_variable_upper_bounds(std::span(col_upper.data(), col_upper.size())); - mps.set_variable_types(var_types); - - return mps; -} - +// Backend glue helpers (status logging / status enum conversion / papilo +// presolver configuration). Placed here so apply_pslp / apply_papilo can +// call them void check_presolve_status(const papilo::PresolveStatus& status) { switch (status) { @@ -561,6 +360,213 @@ void set_presolve_parameters(papilo::Presolve& presolver, } } +template +third_party_presolve_status_t third_party_presolve_t::apply_pslp( + io::mps_data_model_t const& mps, double time_limit) +{ + raft::common::nvtx::range fun_scope("Apply PSLP presolver on host"); + + if constexpr (std::is_same_v) { + const i_t n_cols = mps.get_n_variables(); + const i_t n_rows = mps.get_n_constraints(); + const i_t nnz = mps.get_nnz(); + + // Local owned copies of the fields that need mutation (sign flip on + // maximise / ±inf fill for empty bounds / row_types materialisation); + // matrix arrays are read straight from mps below (const&). + std::vector obj_coeffs(mps.get_objective_coefficients()); + std::vector var_lb(mps.get_variable_lower_bounds()); + std::vector var_ub(mps.get_variable_upper_bounds()); + std::vector constr_lb(mps.get_constraint_lower_bounds()); + std::vector constr_ub(mps.get_constraint_upper_bounds()); + f_t objective_offset = mps.get_objective_offset(); + normalize_for_presolve( + mps, maximize_, obj_coeffs, objective_offset, var_lb, var_ub, constr_lb, constr_ub); + + const auto& coefficients = mps.get_constraint_matrix_values(); + const auto& indices = mps.get_constraint_matrix_indices(); + const auto& offsets = mps.get_constraint_matrix_offsets(); + + Settings* settings = default_settings(); + settings->verbose = false; + settings->max_time = time_limit; + + auto start_time = std::chrono::high_resolution_clock::now(); + Presolver* presolver = new_presolver(coefficients.data(), + indices.data(), + offsets.data(), + n_rows, + n_cols, + nnz, + constr_lb.data(), + constr_ub.data(), + var_lb.data(), + var_ub.data(), + obj_coeffs.data(), + settings); + assert(presolver != nullptr && "Presolver initialization failed"); + const PresolveStatus pslp_status = run_presolver(presolver); + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + CUOPT_LOG_DEBUG("PSLP presolver time: %d milliseconds", duration.count()); + CUOPT_LOG_INFO("PSLP Presolved problem: %d constraints, %d variables, %d non-zeros", + presolver->stats->n_rows_reduced, + presolver->stats->n_cols_reduced, + presolver->stats->nnz_reduced); + + // Free previously allocated presolver and settings (if any) and stash the + // new ones so undo_pslp_host / build_reduced_mps_from_pslp can find them later. + if (pslp_presolver_ != nullptr) { free_presolver(pslp_presolver_); } + if (pslp_stgs_ != nullptr) { free_settings(pslp_stgs_); } + pslp_presolver_ = presolver; + pslp_stgs_ = settings; + + return convert_pslp_presolve_status_to_third_party_presolve_status(pslp_status); + } else { + cuopt_expects(false, + error_type_t::ValidationError, + "PSLP presolver only supports double precision"); + return third_party_presolve_status_t::UNCHANGED; // unreachable + } +} + +// Reduced mps_data_model builders (one per backend). No intermediate view — +// each backend reads its own internal state and writes straight into a fresh +// mps_data_model_t. mps setters copy from the given spans/vectors into their +// internal owned storage, so the locals here can safely die on return. + +template +io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_presolver, + bool maximize, + f_t original_obj_offset) +{ + raft::common::nvtx::range fun_scope("Build mps_data_model from PSLP"); + io::mps_data_model_t mps; + + auto* reduced = pslp_presolver->reduced_prob; + const i_t n_rows = static_cast(reduced->m); + const i_t n_cols = static_cast(reduced->n); + const i_t nnz = static_cast(reduced->nnz); + // PSLP folds the sign flip into obj_offset for maximise problems, and does + // not track the original mps's objective offset — put both back. + const f_t obj_offset = + (maximize ? -reduced->obj_offset : reduced->obj_offset) + original_obj_offset; + mps.set_maximize(maximize); + mps.set_objective_offset(obj_offset); + + if (n_cols == 0 && n_rows == 0) { + std::vector empty_offsets{0}; + mps.set_csr_constraint_matrix( + {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); + return mps; + } + + mps.set_csr_constraint_matrix( + std::span(reduced->Ax, static_cast(nnz)), + std::span(reduced->Ai, static_cast(nnz)), + std::span(reduced->Ap, static_cast(n_rows + 1))); + + if (maximize) { + std::vector h_obj_coeffs(reduced->c, reduced->c + n_cols); + for (auto& c : h_obj_coeffs) { + c = -c; + } + mps.set_objective_coefficients( + std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); + } else { + mps.set_objective_coefficients(std::span(reduced->c, static_cast(n_cols))); + } + mps.set_constraint_lower_bounds( + std::span(reduced->lhs, static_cast(n_rows))); + mps.set_constraint_upper_bounds( + std::span(reduced->rhs, static_cast(n_rows))); + mps.set_variable_lower_bounds(std::span(reduced->lbs, static_cast(n_cols))); + mps.set_variable_upper_bounds(std::span(reduced->ubs, static_cast(n_cols))); + + return mps; +} + +template +io::mps_data_model_t build_reduced_mps_from_papilo( + papilo::Problem const& papilo_problem, bool maximize) +{ + raft::common::nvtx::range fun_scope("Reduced mps <- Papilo"); + io::mps_data_model_t mps; + + auto obj = papilo_problem.getObjective(); + mps.set_maximize(maximize); + mps.set_objective_offset(maximize ? -obj.offset : obj.offset); + + if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { + std::vector empty_offsets{0}; + mps.set_csr_constraint_matrix( + {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); + return mps; + } + + if (maximize) { + for (auto& c : obj.coefficients) { + c = -c; + } + } + mps.set_objective_coefficients( + std::span(obj.coefficients.data(), obj.coefficients.size())); + + auto& constraint_matrix = papilo_problem.getConstraintMatrix(); + + // Row bounds: copy out (papilo returns by value) then substitute ±inf per flag. + auto row_lower = constraint_matrix.getLeftHandSides(); + auto row_upper = constraint_matrix.getRightHandSides(); + auto row_flags = constraint_matrix.getRowFlags(); + for (size_t i = 0; i < row_flags.size(); i++) { + if (row_flags[i].test(papilo::RowFlag::kLhsInf)) { + row_lower[i] = -std::numeric_limits::infinity(); + } + if (row_flags[i].test(papilo::RowFlag::kRhsInf)) { + row_upper[i] = std::numeric_limits::infinity(); + } + } + mps.set_constraint_lower_bounds(std::span(row_lower.data(), row_lower.size())); + mps.set_constraint_upper_bounds(std::span(row_upper.data(), row_upper.size())); + + // CSR offsets have to be synthesised from papilo's RangeInfo (non-contiguous + // in general); values and column indices are contiguous, so span in-place. + auto [index_range, nrows] = constraint_matrix.getRangeInfo(); + std::vector offsets(nrows + 1); + const size_t start = index_range[0].start; + for (i_t i = 0; i < nrows; i++) { + offsets[i] = static_cast(index_range[i].start - start); + } + offsets[nrows] = static_cast(index_range[nrows - 1].end - start); + const i_t nnz = static_cast(constraint_matrix.getNnz()); + assert(offsets[nrows] == nnz); + const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); + const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); + mps.set_csr_constraint_matrix(std::span(&coeffs[start], static_cast(nnz)), + std::span(&cols[start], static_cast(nnz)), + std::span(offsets.data(), offsets.size())); + + // Col bounds + var_types: same copy-then-fixup pattern. + auto col_lower = papilo_problem.getLowerBounds(); + auto col_upper = papilo_problem.getUpperBounds(); + auto col_flags = papilo_problem.getColFlags(); + std::vector var_types(col_flags.size()); + for (size_t i = 0; i < col_flags.size(); i++) { + var_types[i] = col_flags[i].test(papilo::ColFlag::kIntegral) ? 'I' : 'C'; + if (col_flags[i].test(papilo::ColFlag::kLbInf)) { + col_lower[i] = -std::numeric_limits::infinity(); + } + if (col_flags[i].test(papilo::ColFlag::kUbInf)) { + col_upper[i] = std::numeric_limits::infinity(); + } + } + mps.set_variable_lower_bounds(std::span(col_lower.data(), col_lower.size())); + mps.set_variable_upper_bounds(std::span(col_upper.data(), col_upper.size())); + mps.set_variable_types(var_types); + + return mps; +} + template third_party_presolve_status_t third_party_presolve_t::apply_papilo( papilo::Problem& papilo_problem, From 280991d30ebb0934989b026f38829f55df1c42f1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 15:33:13 +0200 Subject: [PATCH 205/258] put constexpr back for compile --- cpp/src/mip_heuristics/presolve/third_party_presolve.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index c361d35077..85f3914499 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -443,6 +443,7 @@ io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_preso raft::common::nvtx::range fun_scope("Build mps_data_model from PSLP"); io::mps_data_model_t mps; + if constexpr (std::is_same_v) { auto* reduced = pslp_presolver->reduced_prob; const i_t n_rows = static_cast(reduced->m); const i_t n_cols = static_cast(reduced->n); @@ -482,6 +483,9 @@ io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_preso std::span(reduced->rhs, static_cast(n_rows))); mps.set_variable_lower_bounds(std::span(reduced->lbs, static_cast(n_cols))); mps.set_variable_upper_bounds(std::span(reduced->ubs, static_cast(n_cols))); + } else { + cuopt_expects(false, error_type_t::ValidationError, "PSLP only supports double precision"); + } return mps; } From 6c3a3b8c39f80a22ecafeb8d54c7e832bc11defd Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 15:33:25 +0200 Subject: [PATCH 206/258] style --- .../presolve/third_party_presolve.cpp | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 85f3914499..3d696be5d2 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -423,9 +423,8 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( return convert_pslp_presolve_status_to_third_party_presolve_status(pslp_status); } else { - cuopt_expects(false, - error_type_t::ValidationError, - "PSLP presolver only supports double precision"); + cuopt_expects( + false, error_type_t::ValidationError, "PSLP presolver only supports double precision"); return third_party_presolve_status_t::UNCHANGED; // unreachable } } @@ -756,47 +755,45 @@ third_party_presolve_t::apply_presolve_from_mps_data( reduced_mps.set_problem_name(mps.get_problem_name()); reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); return third_party_presolve_host_result_t{status, std::move(reduced_mps), {}, {}, {}}; - } -else -{ - // Papilo branch: build papilo::Problem (host, reads mps directly) -> - // apply_papilo -> reduced mps. - auto papilo_problem = build_papilo_problem(mps, maximize_, category); - auto status = apply_papilo(papilo_problem, - category, - dual_postsolve, - absolute_tolerance, - relative_tolerance, - time_limit, - num_cpu_threads); - - if (status == third_party_presolve_status_t::INFEASIBLE || - status == third_party_presolve_status_t::UNBOUNDED || - status == third_party_presolve_status_t::UNBNDORINFEAS) { - return third_party_presolve_host_result_t{ - status, io::mps_data_model_t{}, {}, {}, {}}; - } + } else { + // Papilo branch: build papilo::Problem (host, reads mps directly) -> + // apply_papilo -> reduced mps. + auto papilo_problem = build_papilo_problem(mps, maximize_, category); + auto status = apply_papilo(papilo_problem, + category, + dual_postsolve, + absolute_tolerance, + relative_tolerance, + time_limit, + num_cpu_threads); - auto reduced_mps = build_reduced_mps_from_papilo(papilo_problem, maximize_); - reduced_mps.set_problem_name(mps.get_problem_name()); - reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); + if (status == third_party_presolve_status_t::INFEASIBLE || + status == third_party_presolve_status_t::UNBOUNDED || + status == third_party_presolve_status_t::UNBNDORINFEAS) { + return third_party_presolve_host_result_t{ + status, io::mps_data_model_t{}, {}, {}, {}}; + } - std::vector implied_integer_indices; - { - auto col_flags = papilo_problem.getColFlags(); - for (size_t i = 0; i < col_flags.size(); ++i) { - if (col_flags[i].test(papilo::ColFlag::kImplInt)) { - implied_integer_indices.push_back(static_cast(i)); + auto reduced_mps = build_reduced_mps_from_papilo(papilo_problem, maximize_); + reduced_mps.set_problem_name(mps.get_problem_name()); + reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); + + std::vector implied_integer_indices; + { + auto col_flags = papilo_problem.getColFlags(); + for (size_t i = 0; i < col_flags.size(); ++i) { + if (col_flags[i].test(papilo::ColFlag::kImplInt)) { + implied_integer_indices.push_back(static_cast(i)); + } } } - } - return third_party_presolve_host_result_t{status, - std::move(reduced_mps), - std::move(implied_integer_indices), - reduced_to_original_map_, - original_to_reduced_map_}; -} + return third_party_presolve_host_result_t{status, + std::move(reduced_mps), + std::move(implied_integer_indices), + reduced_to_original_map_, + original_to_reduced_map_}; + } } template From c40f3da6a302efdea26df312144e0eca3e947246 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 17:04:32 +0200 Subject: [PATCH 207/258] pulll different files for distributed ci --- ci/test_cpp_multi_gpu.sh | 16 +++++++++------- .../linear_programming/pdlp_distributed_test.cu | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index 0a53e4fd5d..9bf6679d6a 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -30,7 +30,7 @@ set -euo pipefail # container image (its tag bakes the value into the environment), so no CUDA # version needs to be pinned by the workflow. If it is unset we skip the # bootstrap and fall back to any gtests already present in the container/build -# tree — keeping the job non-breaking until the multi-GPU tests land. + # tree - keeping the job non-breaking until the multi-GPU tests land. if [ -n "${RAPIDS_CUDA_VERSION:-}" ]; then rapids-logger "Using CUDA ${RAPIDS_CUDA_VERSION} (inferred from container image)" rapids-logger "Configuring conda strict channel priority" @@ -68,7 +68,7 @@ nvidia-smi rapids-logger "Check GPU topology" nvidia-smi topo -m -# Multi-GPU tests are meaningless on a single device — fail loudly rather than +# Multi-GPU tests are meaningless on a single device - fail loudly rather than # passing a run that never exercised NCCL. GPU_COUNT=$(nvidia-smi -L | wc -l) rapids-logger "Detected ${GPU_COUNT} GPU(s)" @@ -78,11 +78,13 @@ if [ "${GPU_COUNT}" -lt 2 ]; then exit 1 fi -# Distributed PDLP parity tests read MPS instances (e.g. neos3, a2864) that are -# git-ignored and fetched from S3 — mirror ci/test_cpp.sh so the datasets exist -# and the tests resolve paths via RAPIDS_DATASET_ROOT_DIR. +# Distributed PDLP parity tests use a small set of git-ignored MPS instances. +# Download only those direct-MPS fixtures here: downloading the full PDLP suite +# falls back to netlib conversion when S3 credentials are not available, and that +# path needs gcc (not present in this test environment). rapids-logger "Download datasets" -./datasets/linear_programming/download_pdlp_test_dataset.sh +python benchmarks/linear_programming/utils/get_datasets.py -datasets graph40-40 +python benchmarks/linear_programming/utils/get_datasets.py -datasets ex10 RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)" export RAPIDS_DATASET_ROOT_DIR @@ -106,7 +108,7 @@ shopt -u nullglob if [ "${#mg_tests[@]}" -eq 0 ]; then rapids-logger "No multi-GPU gtest binaries (*_MG_TEST) found in ${GTEST_DIR}; nothing to run." - echo "::notice::No multi-GPU tests present yet — skipping. This job lights up once *_MG_TEST binaries land (distributed PDLP)." + echo "::notice::No multi-GPU tests present yet - skipping. This job lights up once *_MG_TEST binaries land (distributed PDLP)." exit 0 fi diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu index 7acb6b57cb..c39a9f1c92 100644 --- a/cpp/tests/linear_programming/pdlp_distributed_test.cu +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -107,22 +107,22 @@ TEST(pdlp_class, distributed_parity_afiro) expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); } -TEST(pdlp_class, distributed_parity_neos3) +TEST(pdlp_class, distributed_parity_graph40_40) { if (raft::device_setter::get_device_count() < 2) { GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); } const raft::handle_t handle{}; - expect_distributed_matches_base(handle, "linear_programming/neos3/neos3.mps"); + expect_distributed_matches_base(handle, "linear_programming/graph40-40/graph40-40.mps"); } -TEST(pdlp_class, distributed_parity_a2864) +TEST(pdlp_class, distributed_parity_ex10) { if (raft::device_setter::get_device_count() < 2) { GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); } const raft::handle_t handle{}; - expect_distributed_matches_base(handle, "linear_programming/a2864/a2864.mps"); + expect_distributed_matches_base(handle, "linear_programming/ex10/ex10.mps"); } } // namespace cuopt::mathematical_optimization::test From 777d5294cfc148759fd5101f85f48c1a81c6ad43 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 17:24:43 +0200 Subject: [PATCH 208/258] fix small bug in presolve --- cpp/src/mip_heuristics/presolve/third_party_presolve.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 3d696be5d2..90c7f6ff9f 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -57,7 +57,6 @@ namespace cuopt::mathematical_optimization::mip { // Backend-agnostic normalisation of the mutable presolve fields: // * sign-flip `obj_coeffs` / `objective_offset` when maximise, -// * fill `var_lb` / `var_ub` with ±inf when the mps left them empty, // * materialise ranged `constr_lb` / `constr_ub` from `row_types` + // `constraint_bounds` when the ranged pair is absent from the mps. // @@ -79,10 +78,6 @@ void normalize_for_presolve(io::mps_data_model_t const& mps, objective_offset = -objective_offset; } - const i_t n_cols = mps.get_n_variables(); - if (var_lb.empty()) { var_lb.assign(n_cols, -std::numeric_limits::infinity()); } - if (var_ub.empty()) { var_ub.assign(n_cols, std::numeric_limits::infinity()); } - if (constr_lb.empty() && constr_ub.empty()) { const auto& row_types = mps.get_row_types(); const auto& constraint_bounds = mps.get_constraint_bounds(); @@ -382,7 +377,8 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( f_t objective_offset = mps.get_objective_offset(); normalize_for_presolve( mps, maximize_, obj_coeffs, objective_offset, var_lb, var_ub, constr_lb, constr_ub); - + if (var_lb.empty()) { var_lb.assign(n_cols, -std::numeric_limits::infinity()); } + if (var_ub.empty()) { var_ub.assign(n_cols, std::numeric_limits::infinity()); } const auto& coefficients = mps.get_constraint_matrix_values(); const auto& indices = mps.get_constraint_matrix_indices(); const auto& offsets = mps.get_constraint_matrix_offsets(); From aa5ad5b6245f9e752f9af875317bdb1ea6ca62bc Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 18:02:53 +0200 Subject: [PATCH 209/258] allow 1GB for shm in CI distributed tests --- .github/workflows/multi_gpu_cpp_test.yaml | 8 ++++++++ ci/test_cpp_multi_gpu.sh | 8 ++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/multi_gpu_cpp_test.yaml b/.github/workflows/multi_gpu_cpp_test.yaml index 515be4a330..328bd97353 100644 --- a/.github/workflows/multi_gpu_cpp_test.yaml +++ b/.github/workflows/multi_gpu_cpp_test.yaml @@ -39,6 +39,13 @@ on: script: type: string default: "ci/test_cpp_multi_gpu.sh" + container-options: + description: | + Extra 'docker run' options. NCCL allocates its inter-GPU buffers in + /dev/shm; the container default of 64 MB is too small and makes NCCL + setup fail with "No space left on device", so bump it with --shm-size. + type: string + default: "--shm-size=1g" continue-on-error: description: "Treat failures as non-blocking (useful while the MG suite is maturing)." type: boolean @@ -63,5 +70,6 @@ jobs: node_type: ${{ inputs.node_type }} arch: ${{ inputs.arch }} container_image: ${{ inputs.container_image }} + container-options: ${{ inputs.container-options }} script: ${{ inputs.script }} continue-on-error: ${{ inputs.continue-on-error }} diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index 9bf6679d6a..8de2d6bfc9 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -115,13 +115,9 @@ fi # NCCL diagnostics for CI logs when distributed PDLP halo exchange fails. export NCCL_DEBUG=INFO # PHB topology on the 2-GPU runner: disable direct GPU peer access (see topo above). +# NCCL then uses the SHM transport, which needs a larger /dev/shm than the 64 MB +# container default — the workflow launches the container with --shm-size for this. export NCCL_P2P_DISABLE=1 -# With P2P disabled NCCL falls back to the SHM transport, which allocates multi-MB -# buffers in /dev/shm. CI containers default to a 64 MB /dev/shm, so those -# allocations fail with "No space left on device". Disable SHM too and let NCCL -# use the socket transport for the intra-node exchange (functionally exercises the -# distributed PDLP communication path without depending on container --shm-size). -export NCCL_SHM_DISABLE=1 EXITCODE=0 for gt in "${mg_tests[@]}"; do From 5d0cc104337c653a787e8b4351bb0de6ee40ba0c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 18:32:57 +0200 Subject: [PATCH 210/258] reordered presolve to have clearer diff --- .../presolve/third_party_presolve.cpp | 354 +++++++++--------- 1 file changed, 177 insertions(+), 177 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 90c7f6ff9f..bfb0364139 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -205,6 +205,147 @@ papilo::Problem build_papilo_problem(io::mps_data_model_t const& return problem; } +// Reduced mps_data_model builders (one per backend). No intermediate view — +// each backend reads its own internal state and writes straight into a fresh +// mps_data_model_t. mps setters copy from the given spans/vectors into their +// internal owned storage, so the locals here can safely die on return. + +template +io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_presolver, + bool maximize, + f_t original_obj_offset) +{ + raft::common::nvtx::range fun_scope("Build mps_data_model from PSLP"); + io::mps_data_model_t mps; + + if constexpr (std::is_same_v) { + auto* reduced = pslp_presolver->reduced_prob; + const i_t n_rows = static_cast(reduced->m); + const i_t n_cols = static_cast(reduced->n); + const i_t nnz = static_cast(reduced->nnz); + // PSLP folds the sign flip into obj_offset for maximise problems, and does + // not track the original mps's objective offset — put both back. + const f_t obj_offset = + (maximize ? -reduced->obj_offset : reduced->obj_offset) + original_obj_offset; + mps.set_maximize(maximize); + mps.set_objective_offset(obj_offset); + + if (n_cols == 0 && n_rows == 0) { + std::vector empty_offsets{0}; + mps.set_csr_constraint_matrix( + {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); + return mps; + } + + mps.set_csr_constraint_matrix( + std::span(reduced->Ax, static_cast(nnz)), + std::span(reduced->Ai, static_cast(nnz)), + std::span(reduced->Ap, static_cast(n_rows + 1))); + + if (maximize) { + std::vector h_obj_coeffs(reduced->c, reduced->c + n_cols); + for (auto& c : h_obj_coeffs) { + c = -c; + } + mps.set_objective_coefficients( + std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); + } else { + mps.set_objective_coefficients(std::span(reduced->c, static_cast(n_cols))); + } + mps.set_constraint_lower_bounds( + std::span(reduced->lhs, static_cast(n_rows))); + mps.set_constraint_upper_bounds( + std::span(reduced->rhs, static_cast(n_rows))); + mps.set_variable_lower_bounds(std::span(reduced->lbs, static_cast(n_cols))); + mps.set_variable_upper_bounds(std::span(reduced->ubs, static_cast(n_cols))); + } else { + cuopt_expects(false, error_type_t::ValidationError, "PSLP only supports double precision"); + } + + return mps; +} + +template +io::mps_data_model_t build_reduced_mps_from_papilo( + papilo::Problem const& papilo_problem, bool maximize) +{ + raft::common::nvtx::range fun_scope("Reduced mps <- Papilo"); + io::mps_data_model_t mps; + + auto obj = papilo_problem.getObjective(); + mps.set_maximize(maximize); + mps.set_objective_offset(maximize ? -obj.offset : obj.offset); + + if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { + std::vector empty_offsets{0}; + mps.set_csr_constraint_matrix( + {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); + return mps; + } + + if (maximize) { + for (auto& c : obj.coefficients) { + c = -c; + } + } + mps.set_objective_coefficients( + std::span(obj.coefficients.data(), obj.coefficients.size())); + + auto& constraint_matrix = papilo_problem.getConstraintMatrix(); + + // Row bounds: copy out (papilo returns by value) then substitute ±inf per flag. + auto row_lower = constraint_matrix.getLeftHandSides(); + auto row_upper = constraint_matrix.getRightHandSides(); + auto row_flags = constraint_matrix.getRowFlags(); + for (size_t i = 0; i < row_flags.size(); i++) { + if (row_flags[i].test(papilo::RowFlag::kLhsInf)) { + row_lower[i] = -std::numeric_limits::infinity(); + } + if (row_flags[i].test(papilo::RowFlag::kRhsInf)) { + row_upper[i] = std::numeric_limits::infinity(); + } + } + mps.set_constraint_lower_bounds(std::span(row_lower.data(), row_lower.size())); + mps.set_constraint_upper_bounds(std::span(row_upper.data(), row_upper.size())); + + // CSR offsets have to be synthesised from papilo's RangeInfo (non-contiguous + // in general); values and column indices are contiguous, so span in-place. + auto [index_range, nrows] = constraint_matrix.getRangeInfo(); + std::vector offsets(nrows + 1); + const size_t start = index_range[0].start; + for (i_t i = 0; i < nrows; i++) { + offsets[i] = static_cast(index_range[i].start - start); + } + offsets[nrows] = static_cast(index_range[nrows - 1].end - start); + const i_t nnz = static_cast(constraint_matrix.getNnz()); + assert(offsets[nrows] == nnz); + const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); + const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); + mps.set_csr_constraint_matrix(std::span(&coeffs[start], static_cast(nnz)), + std::span(&cols[start], static_cast(nnz)), + std::span(offsets.data(), offsets.size())); + + // Col bounds + var_types: same copy-then-fixup pattern. + auto col_lower = papilo_problem.getLowerBounds(); + auto col_upper = papilo_problem.getUpperBounds(); + auto col_flags = papilo_problem.getColFlags(); + std::vector var_types(col_flags.size()); + for (size_t i = 0; i < col_flags.size(); i++) { + var_types[i] = col_flags[i].test(papilo::ColFlag::kIntegral) ? 'I' : 'C'; + if (col_flags[i].test(papilo::ColFlag::kLbInf)) { + col_lower[i] = -std::numeric_limits::infinity(); + } + if (col_flags[i].test(papilo::ColFlag::kUbInf)) { + col_upper[i] = std::numeric_limits::infinity(); + } + } + mps.set_variable_lower_bounds(std::span(col_lower.data(), col_lower.size())); + mps.set_variable_upper_bounds(std::span(col_upper.data(), col_upper.size())); + mps.set_variable_types(var_types); + + return mps; +} + // Backend glue helpers (status logging / status enum conversion / papilo // presolver configuration). Placed here so apply_pslp / apply_papilo can // call them @@ -425,147 +566,6 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( } } -// Reduced mps_data_model builders (one per backend). No intermediate view — -// each backend reads its own internal state and writes straight into a fresh -// mps_data_model_t. mps setters copy from the given spans/vectors into their -// internal owned storage, so the locals here can safely die on return. - -template -io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_presolver, - bool maximize, - f_t original_obj_offset) -{ - raft::common::nvtx::range fun_scope("Build mps_data_model from PSLP"); - io::mps_data_model_t mps; - - if constexpr (std::is_same_v) { - auto* reduced = pslp_presolver->reduced_prob; - const i_t n_rows = static_cast(reduced->m); - const i_t n_cols = static_cast(reduced->n); - const i_t nnz = static_cast(reduced->nnz); - // PSLP folds the sign flip into obj_offset for maximise problems, and does - // not track the original mps's objective offset — put both back. - const f_t obj_offset = - (maximize ? -reduced->obj_offset : reduced->obj_offset) + original_obj_offset; - mps.set_maximize(maximize); - mps.set_objective_offset(obj_offset); - - if (n_cols == 0 && n_rows == 0) { - std::vector empty_offsets{0}; - mps.set_csr_constraint_matrix( - {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); - return mps; - } - - mps.set_csr_constraint_matrix( - std::span(reduced->Ax, static_cast(nnz)), - std::span(reduced->Ai, static_cast(nnz)), - std::span(reduced->Ap, static_cast(n_rows + 1))); - - if (maximize) { - std::vector h_obj_coeffs(reduced->c, reduced->c + n_cols); - for (auto& c : h_obj_coeffs) { - c = -c; - } - mps.set_objective_coefficients( - std::span(h_obj_coeffs.data(), h_obj_coeffs.size())); - } else { - mps.set_objective_coefficients(std::span(reduced->c, static_cast(n_cols))); - } - mps.set_constraint_lower_bounds( - std::span(reduced->lhs, static_cast(n_rows))); - mps.set_constraint_upper_bounds( - std::span(reduced->rhs, static_cast(n_rows))); - mps.set_variable_lower_bounds(std::span(reduced->lbs, static_cast(n_cols))); - mps.set_variable_upper_bounds(std::span(reduced->ubs, static_cast(n_cols))); - } else { - cuopt_expects(false, error_type_t::ValidationError, "PSLP only supports double precision"); - } - - return mps; -} - -template -io::mps_data_model_t build_reduced_mps_from_papilo( - papilo::Problem const& papilo_problem, bool maximize) -{ - raft::common::nvtx::range fun_scope("Reduced mps <- Papilo"); - io::mps_data_model_t mps; - - auto obj = papilo_problem.getObjective(); - mps.set_maximize(maximize); - mps.set_objective_offset(maximize ? -obj.offset : obj.offset); - - if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { - std::vector empty_offsets{0}; - mps.set_csr_constraint_matrix( - {}, {}, std::span(empty_offsets.data(), empty_offsets.size())); - return mps; - } - - if (maximize) { - for (auto& c : obj.coefficients) { - c = -c; - } - } - mps.set_objective_coefficients( - std::span(obj.coefficients.data(), obj.coefficients.size())); - - auto& constraint_matrix = papilo_problem.getConstraintMatrix(); - - // Row bounds: copy out (papilo returns by value) then substitute ±inf per flag. - auto row_lower = constraint_matrix.getLeftHandSides(); - auto row_upper = constraint_matrix.getRightHandSides(); - auto row_flags = constraint_matrix.getRowFlags(); - for (size_t i = 0; i < row_flags.size(); i++) { - if (row_flags[i].test(papilo::RowFlag::kLhsInf)) { - row_lower[i] = -std::numeric_limits::infinity(); - } - if (row_flags[i].test(papilo::RowFlag::kRhsInf)) { - row_upper[i] = std::numeric_limits::infinity(); - } - } - mps.set_constraint_lower_bounds(std::span(row_lower.data(), row_lower.size())); - mps.set_constraint_upper_bounds(std::span(row_upper.data(), row_upper.size())); - - // CSR offsets have to be synthesised from papilo's RangeInfo (non-contiguous - // in general); values and column indices are contiguous, so span in-place. - auto [index_range, nrows] = constraint_matrix.getRangeInfo(); - std::vector offsets(nrows + 1); - const size_t start = index_range[0].start; - for (i_t i = 0; i < nrows; i++) { - offsets[i] = static_cast(index_range[i].start - start); - } - offsets[nrows] = static_cast(index_range[nrows - 1].end - start); - const i_t nnz = static_cast(constraint_matrix.getNnz()); - assert(offsets[nrows] == nnz); - const int* cols = constraint_matrix.getConstraintMatrix().getColumns(); - const f_t* coeffs = constraint_matrix.getConstraintMatrix().getValues(); - mps.set_csr_constraint_matrix(std::span(&coeffs[start], static_cast(nnz)), - std::span(&cols[start], static_cast(nnz)), - std::span(offsets.data(), offsets.size())); - - // Col bounds + var_types: same copy-then-fixup pattern. - auto col_lower = papilo_problem.getLowerBounds(); - auto col_upper = papilo_problem.getUpperBounds(); - auto col_flags = papilo_problem.getColFlags(); - std::vector var_types(col_flags.size()); - for (size_t i = 0; i < col_flags.size(); i++) { - var_types[i] = col_flags[i].test(papilo::ColFlag::kIntegral) ? 'I' : 'C'; - if (col_flags[i].test(papilo::ColFlag::kLbInf)) { - col_lower[i] = -std::numeric_limits::infinity(); - } - if (col_flags[i].test(papilo::ColFlag::kUbInf)) { - col_upper[i] = std::numeric_limits::infinity(); - } - } - mps.set_variable_lower_bounds(std::span(col_lower.data(), col_lower.size())); - mps.set_variable_upper_bounds(std::span(col_upper.data(), col_upper.size())); - mps.set_variable_types(var_types); - - return mps; -} - template third_party_presolve_status_t third_party_presolve_t::apply_papilo( papilo::Problem& papilo_problem, @@ -792,6 +792,42 @@ third_party_presolve_t::apply_presolve_from_mps_data( } } +template +void third_party_presolve_t::undo(rmm::device_uvector& primal_solution, + rmm::device_uvector& dual_solution, + rmm::device_uvector& reduced_costs, + problem_category_t category, + bool status_to_skip, + bool dual_postsolve, + rmm::cuda_stream_view stream_view) +{ + // PSLP path always runs (it owns the lifted solution in-place); the Papilo + // path (used for Papilo/Default/None) is allowed to short-circuit via + // status_to_skip without touching the data. Mirror that here so we don't pay + // for unnecessary D->H->D copies. + if (status_to_skip && presolver_ != cuopt::mathematical_optimization::presolver_t::PSLP) { + return; + } + + std::vector h_primal(primal_solution.size()); + std::vector h_dual(dual_solution.size()); + std::vector h_rc(reduced_costs.size()); + raft::copy(h_primal.data(), primal_solution.data(), primal_solution.size(), stream_view); + raft::copy(h_dual.data(), dual_solution.data(), dual_solution.size(), stream_view); + raft::copy(h_rc.data(), reduced_costs.data(), reduced_costs.size(), stream_view); + stream_view.synchronize(); + + undo_host(h_primal, h_dual, h_rc, category, status_to_skip, dual_postsolve); + + primal_solution.resize(h_primal.size(), stream_view); + dual_solution.resize(h_dual.size(), stream_view); + reduced_costs.resize(h_rc.size(), stream_view); + raft::copy(primal_solution.data(), h_primal.data(), h_primal.size(), stream_view); + raft::copy(dual_solution.data(), h_dual.data(), h_dual.size(), stream_view); + raft::copy(reduced_costs.data(), h_rc.data(), h_rc.size(), stream_view); + stream_view.synchronize(); +} + template void third_party_presolve_t::undo_pslp_host(std::vector& primal_solution, std::vector& dual_solution, @@ -862,42 +898,6 @@ void third_party_presolve_t::undo_host(std::vector& primal_soluti undo_papilo_host(primal_solution, dual_solution, reduced_costs, dual_postsolve); } -template -void third_party_presolve_t::undo(rmm::device_uvector& primal_solution, - rmm::device_uvector& dual_solution, - rmm::device_uvector& reduced_costs, - problem_category_t category, - bool status_to_skip, - bool dual_postsolve, - rmm::cuda_stream_view stream_view) -{ - // PSLP path always runs (it owns the lifted solution in-place); the Papilo - // path (used for Papilo/Default/None) is allowed to short-circuit via - // status_to_skip without touching the data. Mirror that here so we don't pay - // for unnecessary D->H->D copies. - if (status_to_skip && presolver_ != cuopt::mathematical_optimization::presolver_t::PSLP) { - return; - } - - std::vector h_primal(primal_solution.size()); - std::vector h_dual(dual_solution.size()); - std::vector h_rc(reduced_costs.size()); - raft::copy(h_primal.data(), primal_solution.data(), primal_solution.size(), stream_view); - raft::copy(h_dual.data(), dual_solution.data(), dual_solution.size(), stream_view); - raft::copy(h_rc.data(), reduced_costs.data(), reduced_costs.size(), stream_view); - stream_view.synchronize(); - - undo_host(h_primal, h_dual, h_rc, category, status_to_skip, dual_postsolve); - - primal_solution.resize(h_primal.size(), stream_view); - dual_solution.resize(h_dual.size(), stream_view); - reduced_costs.resize(h_rc.size(), stream_view); - raft::copy(primal_solution.data(), h_primal.data(), h_primal.size(), stream_view); - raft::copy(dual_solution.data(), h_dual.data(), h_dual.size(), stream_view); - raft::copy(reduced_costs.data(), h_rc.data(), h_rc.size(), stream_view); - stream_view.synchronize(); -} - template void third_party_presolve_t::uncrush_primal_solution( const std::vector& reduced_primal, std::vector& full_primal) const From 70c0a4e23363dcc2afa7197f20262d60ba22fde3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 18:46:52 +0200 Subject: [PATCH 211/258] removed string include --- cpp/src/mip_heuristics/presolve/third_party_presolve.cpp | 1 - cpp/src/mip_heuristics/presolve/third_party_presolve.hpp | 1 - 2 files changed, 2 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index bfb0364139..4fd6aa4ed6 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -49,7 +49,6 @@ #include #include -#include #include #include diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 53632e673a..07e18b2a2c 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -9,7 +9,6 @@ #include #include -#include #include #include From 7f3d3c56e482b9b7fc42daea104c428a80e44bc3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 19:29:25 +0200 Subject: [PATCH 212/258] final cleanup of third_party presolve omg --- .../presolve/third_party_presolve.cpp | 90 ++++++++----------- .../presolve/third_party_presolve.hpp | 44 ++++----- cpp/src/mip_heuristics/solve.cu | 14 +-- cpp/src/pdlp/solve.cu | 52 +++++------ .../unit_tests/presolve_test.cu | 6 +- 5 files changed, 96 insertions(+), 110 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 4fd6aa4ed6..28df347d7e 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -186,6 +186,7 @@ papilo::Problem build_papilo_problem(io::mps_data_model_t const& if (h_entries.size()) { auto constexpr const sorted_entries = true; + // MIP reductions like clique merging and substituition require more fillin const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; auto csr_storage = papilo::SparseStorage( @@ -204,11 +205,7 @@ papilo::Problem build_papilo_problem(io::mps_data_model_t const& return problem; } -// Reduced mps_data_model builders (one per backend). No intermediate view — -// each backend reads its own internal state and writes straight into a fresh -// mps_data_model_t. mps setters copy from the given spans/vectors into their -// internal owned storage, so the locals here can safely die on return. - +// Presolved mps_data_model builder from PSLP template io::mps_data_model_t build_reduced_mps_from_pslp(Presolver* pslp_presolver, bool maximize, @@ -345,9 +342,6 @@ io::mps_data_model_t build_reduced_mps_from_papilo( return mps; } -// Backend glue helpers (status logging / status enum conversion / papilo -// presolver configuration). Placed here so apply_pslp / apply_papilo can -// call them void check_presolve_status(const papilo::PresolveStatus& status) { switch (status) { @@ -551,7 +545,7 @@ third_party_presolve_status_t third_party_presolve_t::apply_pslp( presolver->stats->nnz_reduced); // Free previously allocated presolver and settings (if any) and stash the - // new ones so undo_pslp_host / build_reduced_mps_from_pslp can find them later. + // new ones so undo_pslp / build_reduced_mps_from_pslp can find them later. if (pslp_presolver_ != nullptr) { free_presolver(pslp_presolver_); } if (pslp_stgs_ != nullptr) { free_settings(pslp_stgs_); } pslp_presolver_ = presolver; @@ -628,6 +622,7 @@ third_party_presolve_status_t third_party_presolve_t::apply_papilo( n_integer, papilo_problem.getConstraintMatrix().getNnz()); + // Check if presolve found the optimal solution (problem fully reduced) if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { status = third_party_presolve_status_t::OPTIMAL; } @@ -734,7 +729,7 @@ third_party_presolve_t::apply_presolve_from_mps_data( error_type_t::ValidationError, "Presolve does not support mps_data_models with quadratic constraints"); - // PSLP branch: apply_pslp (host, reads mps directly) -> reduced mps. + // PSLP branch: apply_pslp -> reduced mps. if (presolver == cuopt::mathematical_optimization::presolver_t::PSLP) { const f_t original_obj_offset = mps.get_objective_offset(); auto status = apply_pslp(mps, time_limit); @@ -751,7 +746,7 @@ third_party_presolve_t::apply_presolve_from_mps_data( reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); return third_party_presolve_host_result_t{status, std::move(reduced_mps), {}, {}, {}}; } else { - // Papilo branch: build papilo::Problem (host, reads mps directly) -> + // Papilo branch: build papilo::Problem -> // apply_papilo -> reduced mps. auto papilo_problem = build_papilo_problem(mps, maximize_, category); auto status = apply_papilo(papilo_problem, @@ -773,13 +768,11 @@ third_party_presolve_t::apply_presolve_from_mps_data( reduced_mps.set_problem_name(mps.get_problem_name()); reduced_mps.set_objective_scaling_factor(mps.get_objective_scaling_factor()); + auto col_flags = papilo_problem.getColFlags(); std::vector implied_integer_indices; - { - auto col_flags = papilo_problem.getColFlags(); - for (size_t i = 0; i < col_flags.size(); ++i) { - if (col_flags[i].test(papilo::ColFlag::kImplInt)) { - implied_integer_indices.push_back(static_cast(i)); - } + for (size_t i = 0; i < col_flags.size(); ++i) { + if (col_flags[i].test(papilo::ColFlag::kImplInt)) { + implied_integer_indices.push_back(static_cast(i)); } } @@ -792,22 +785,15 @@ third_party_presolve_t::apply_presolve_from_mps_data( } template -void third_party_presolve_t::undo(rmm::device_uvector& primal_solution, - rmm::device_uvector& dual_solution, - rmm::device_uvector& reduced_costs, - problem_category_t category, - bool status_to_skip, - bool dual_postsolve, - rmm::cuda_stream_view stream_view) +void third_party_presolve_t::undo_from_device( + rmm::device_uvector& primal_solution, + rmm::device_uvector& dual_solution, + rmm::device_uvector& reduced_costs, + problem_category_t category, + bool status_to_skip, + bool dual_postsolve, + rmm::cuda_stream_view stream_view) { - // PSLP path always runs (it owns the lifted solution in-place); the Papilo - // path (used for Papilo/Default/None) is allowed to short-circuit via - // status_to_skip without touching the data. Mirror that here so we don't pay - // for unnecessary D->H->D copies. - if (status_to_skip && presolver_ != cuopt::mathematical_optimization::presolver_t::PSLP) { - return; - } - std::vector h_primal(primal_solution.size()); std::vector h_dual(dual_solution.size()); std::vector h_rc(reduced_costs.size()); @@ -816,7 +802,7 @@ void third_party_presolve_t::undo(rmm::device_uvector& primal_sol raft::copy(h_rc.data(), reduced_costs.data(), reduced_costs.size(), stream_view); stream_view.synchronize(); - undo_host(h_primal, h_dual, h_rc, category, status_to_skip, dual_postsolve); + undo(h_primal, h_dual, h_rc, category, status_to_skip, dual_postsolve); primal_solution.resize(h_primal.size(), stream_view); dual_solution.resize(h_dual.size(), stream_view); @@ -828,9 +814,9 @@ void third_party_presolve_t::undo(rmm::device_uvector& primal_sol } template -void third_party_presolve_t::undo_pslp_host(std::vector& primal_solution, - std::vector& dual_solution, - std::vector& reduced_costs) +void third_party_presolve_t::undo_pslp(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs) { if constexpr (std::is_same_v) { // PSLP postsolve reads from the passed-in host buffers and writes the @@ -851,10 +837,10 @@ void third_party_presolve_t::undo_pslp_host(std::vector& primal_s } template -void third_party_presolve_t::undo_papilo_host(std::vector& primal_solution, - std::vector& dual_solution, - std::vector& reduced_costs, - bool dual_postsolve) +void third_party_presolve_t::undo_papilo(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + bool dual_postsolve) { papilo::Solution reduced_sol(primal_solution); if (dual_postsolve) { @@ -878,23 +864,21 @@ void third_party_presolve_t::undo_papilo_host(std::vector& primal } template -void third_party_presolve_t::undo_host(std::vector& primal_solution, - std::vector& dual_solution, - std::vector& reduced_costs, - problem_category_t /*category*/, - bool status_to_skip, - bool dual_postsolve) +void third_party_presolve_t::undo(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + problem_category_t /*category*/, + bool status_to_skip, + bool dual_postsolve) { - // Matches apply()'s dispatch: PSLP is the only branch that's special-cased; - // every other value of `presolver_` (Papilo / Default / None) runs the - // Papilo postsolve, which is a no-op short-circuit on status_to_skip. if (presolver_ == cuopt::mathematical_optimization::presolver_t::PSLP) { - undo_pslp_host(primal_solution, dual_solution, reduced_costs); + undo_pslp(primal_solution, dual_solution, reduced_costs); return; } - - if (status_to_skip) { return; } - undo_papilo_host(primal_solution, dual_solution, reduced_costs, dual_postsolve); + else { // Papilo branch + if (status_to_skip) { return; } + undo_papilo(primal_solution, dual_solution, reduced_costs, dual_postsolve); + } } template diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 07e18b2a2c..776a12b50e 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -94,22 +94,22 @@ class third_party_presolve_t { double time_limit, i_t num_cpu_threads = 0); - void undo(rmm::device_uvector& primal_solution, - rmm::device_uvector& dual_solution, - rmm::device_uvector& reduced_costs, - problem_category_t category, - bool status_to_skip, - bool dual_postsolve, - rmm::cuda_stream_view stream_view); + void undo_from_device(rmm::device_uvector& primal_solution, + rmm::device_uvector& dual_solution, + rmm::device_uvector& reduced_costs, + problem_category_t category, + bool status_to_skip, + bool dual_postsolve, + rmm::cuda_stream_view stream_view); // Host-only postsolve. Resizes the vectors to original-problem dimensions. - // The device-side `undo` above is a thin wrapper around this method. - void undo_host(std::vector& primal_solution, - std::vector& dual_solution, - std::vector& reduced_costs, - problem_category_t category, - bool status_to_skip, - bool dual_postsolve); + // The device-side `undo_from_device` above is a thin wrapper around this method. + void undo(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + problem_category_t category, + bool status_to_skip, + bool dual_postsolve); void uncrush_primal_solution(const std::vector& reduced_primal, std::vector& full_primal) const; @@ -145,14 +145,14 @@ class third_party_presolve_t { // Host-only per-backend postsolve helpers. Both resize their vector args // to original-problem dimensions. - void undo_pslp_host(std::vector& primal_solution, - std::vector& dual_solution, - std::vector& reduced_costs); - - void undo_papilo_host(std::vector& primal_solution, - std::vector& dual_solution, - std::vector& reduced_costs, - bool dual_postsolve); + void undo_pslp(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs); + + void undo_papilo(std::vector& primal_solution, + std::vector& dual_solution, + std::vector& reduced_costs, + bool dual_postsolve); bool maximize_ = false; cuopt::mathematical_optimization::presolver_t presolver_ = diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 7be38d7cb5..95786f3c5a 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -672,13 +672,13 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p cuopt::device_copy(sol.get_solution(), op_problem.get_handle_ptr()->get_stream()); rmm::device_uvector dual_solution(0, op_problem.get_handle_ptr()->get_stream()); rmm::device_uvector reduced_costs(0, op_problem.get_handle_ptr()->get_stream()); - presolver->undo(primal_solution, - dual_solution, - reduced_costs, - cuopt::mathematical_optimization::problem_category_t::MIP, - status_to_skip, - dual_postsolve, - op_problem.get_handle_ptr()->get_stream()); + presolver->undo_from_device(primal_solution, + dual_solution, + reduced_costs, + cuopt::mathematical_optimization::problem_category_t::MIP, + status_to_skip, + dual_postsolve, + op_problem.get_handle_ptr()->get_stream()); if (!status_to_skip) { thrust::fill(rmm::exec_policy(op_problem.get_handle_ptr()->get_stream()), dual_solution.data(), diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index a75543f411..ba8aeaf20e 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1998,13 +1998,13 @@ optimization_problem_solution_t solve_lp( rmm::device_uvector empty_reduced_costs(0, op_problem.get_handle_ptr()->get_stream()); // Run postsolve to get the full solution - presolver->undo(empty_primal, - empty_dual, - empty_reduced_costs, - cuopt::mathematical_optimization::problem_category_t::LP, - false, // status_to_skip - settings.dual_postsolve, - op_problem.get_handle_ptr()->get_stream()); + presolver->undo_from_device(empty_primal, + empty_dual, + empty_reduced_costs, + cuopt::mathematical_optimization::problem_category_t::LP, + false, // status_to_skip + settings.dual_postsolve, + op_problem.get_handle_ptr()->get_stream()); // Create termination info with the objective from presolve typename optimization_problem_solution_t::additional_termination_information_t @@ -2069,13 +2069,13 @@ optimization_problem_solution_t solve_lp( cuopt::device_copy(solution.get_reduced_cost(), op_problem.get_handle_ptr()->get_stream()); bool status_to_skip = false; - presolver->undo(primal_solution, - dual_solution, - reduced_costs, - cuopt::mathematical_optimization::problem_category_t::LP, - status_to_skip, - settings.dual_postsolve, - op_problem.get_handle_ptr()->get_stream()); + presolver->undo_from_device(primal_solution, + dual_solution, + reduced_costs, + cuopt::mathematical_optimization::problem_category_t::LP, + status_to_skip, + settings.dual_postsolve, + op_problem.get_handle_ptr()->get_stream()); std::vector< typename optimization_problem_solution_t::additional_termination_information_t> @@ -2459,12 +2459,12 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( presolve_time); std::vector h_primal, h_dual, h_rc; - presolver_ptr->undo_host(h_primal, - h_dual, - h_rc, - cuopt::mathematical_optimization::problem_category_t::LP, - /*status_to_skip=*/false, - settings_resolved.dual_postsolve); + presolver_ptr->undo(h_primal, + h_dual, + h_rc, + cuopt::mathematical_optimization::problem_category_t::LP, + /*status_to_skip=*/false, + settings_resolved.dual_postsolve); auto primal_uv = cuopt::device_copy(h_primal, handle_ptr->get_stream()); auto dual_uv = cuopt::device_copy(h_dual, handle_ptr->get_stream()); @@ -2545,12 +2545,12 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( auto h_rc = cuopt::host_copy(sol.get_reduced_cost(), handle_ptr->get_stream()); handle_ptr->sync_stream(); - presolver_ptr->undo_host(h_primal, - h_dual, - h_rc, - cuopt::mathematical_optimization::problem_category_t::LP, - /*status_to_skip=*/false, - settings_resolved.dual_postsolve); + presolver_ptr->undo(h_primal, + h_dual, + h_rc, + cuopt::mathematical_optimization::problem_category_t::LP, + /*status_to_skip=*/false, + settings_resolved.dual_postsolve); auto primal_uv = cuopt::device_copy(h_primal, handle_ptr->get_stream()); auto dual_uv = cuopt::device_copy(h_dual, handle_ptr->get_stream()); diff --git a/cpp/tests/linear_programming/unit_tests/presolve_test.cu b/cpp/tests/linear_programming/unit_tests/presolve_test.cu index 977d856c9e..3ca9b5833a 100644 --- a/cpp/tests/linear_programming/unit_tests/presolve_test.cu +++ b/cpp/tests/linear_programming/unit_tests/presolve_test.cu @@ -569,7 +569,8 @@ TEST_P(dual_crush_round_trip, kkt_check) rc_sol = cuopt::device_copy(reduced_solution.get_reduced_cost(), stream); } - presolver.undo(primal_sol, dual_sol, rc_sol, problem_category_t::LP, false, true, stream); + presolver.undo_from_device( + primal_sol, dual_sol, rc_sol, problem_category_t::LP, false, true, stream); auto x_orig = host_copy(primal_sol, stream); auto y_orig = host_copy(dual_sol, stream); @@ -817,7 +818,8 @@ TEST_P(crush_warmstart, round_trip) } auto rc_sol = cuopt::device_copy(z_red, stream); - presolver.undo(primal_sol, dual_sol, rc_sol, problem_category_t::LP, false, true, stream); + presolver.undo_from_device( + primal_sol, dual_sol, rc_sol, problem_category_t::LP, false, true, stream); auto x_orig = host_copy(primal_sol, stream); auto y_orig = host_copy(dual_sol, stream); From ad6e7a020fe7def30fe44de2214b69a2cfcbc422 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Thu, 9 Jul 2026 19:38:45 +0200 Subject: [PATCH 213/258] style --- .../presolve/third_party_presolve.cpp | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 28df347d7e..0e544b57f3 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -187,9 +187,9 @@ papilo::Problem build_papilo_problem(io::mps_data_model_t const& if (h_entries.size()) { auto constexpr const sorted_entries = true; // MIP reductions like clique merging and substituition require more fillin - const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; - const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; - auto csr_storage = papilo::SparseStorage( + const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0; + const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4; + auto csr_storage = papilo::SparseStorage( h_entries, n_rows, n_cols, sorted_entries, spare_ratio, min_inter_row_space); problem.setConstraintMatrix(csr_storage, constr_lb, constr_ub, h_row_flags); @@ -785,14 +785,13 @@ third_party_presolve_t::apply_presolve_from_mps_data( } template -void third_party_presolve_t::undo_from_device( - rmm::device_uvector& primal_solution, - rmm::device_uvector& dual_solution, - rmm::device_uvector& reduced_costs, - problem_category_t category, - bool status_to_skip, - bool dual_postsolve, - rmm::cuda_stream_view stream_view) +void third_party_presolve_t::undo_from_device(rmm::device_uvector& primal_solution, + rmm::device_uvector& dual_solution, + rmm::device_uvector& reduced_costs, + problem_category_t category, + bool status_to_skip, + bool dual_postsolve, + rmm::cuda_stream_view stream_view) { std::vector h_primal(primal_solution.size()); std::vector h_dual(dual_solution.size()); @@ -874,8 +873,7 @@ void third_party_presolve_t::undo(std::vector& primal_solution, if (presolver_ == cuopt::mathematical_optimization::presolver_t::PSLP) { undo_pslp(primal_solution, dual_solution, reduced_costs); return; - } - else { // Papilo branch + } else { // Papilo branch if (status_to_skip) { return; } undo_papilo(primal_solution, dual_solution, reduced_costs, dual_postsolve); } From 74483525ec2cbe4dc20a49098a644943200e8af3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 11:27:26 +0200 Subject: [PATCH 214/258] refactor dsitributed algorithms --- .../distributed_algorithms.cu | 215 +++++++++--------- .../distributed_algorithms.hpp | 95 -------- .../distributed_pdlp/multi_gpu_engine.hpp | 55 +++++ cpp/src/pdlp/pdlp.cu | 14 +- .../termination_strategy.cu | 5 +- 5 files changed, 166 insertions(+), 218 deletions(-) delete mode 100644 cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 6c853a47b4..b59827a409 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +// Out-of-line definitions of multi_gpu_engine_t's high-level algorithm methods +// used by the pdlp solver. #include -#include #include #include #include @@ -26,10 +27,13 @@ namespace cuopt::mathematical_optimization::pdlp { // -------- Solution gather (shards -> master) ------------------------------ template -void gather_potential_next_solutions_to_master(multi_gpu_engine_t& engine, - pdhg_solver_t& master_pdhg, - rmm::device_uvector& master_reduced_cost) +void multi_gpu_engine_t::gather_potential_next_solutions_to_master( + rmm::device_uvector& master_reduced_cost) { + cuopt_assert(master_pdlp_ != nullptr, + "gather_potential_next_solutions_to_master requires set_master(...)"); + auto& master_pdhg = master_pdlp_->pdhg_solver_; + const std::size_t total_vars = master_pdhg.get_potential_next_primal_solution().size(); const std::size_t total_cstrs = master_pdhg.get_potential_next_dual_solution().size(); @@ -37,7 +41,7 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng std::vector h_dual(total_cstrs); std::vector h_reduced_cost(total_vars); - for (auto& s_uptr : engine.shards) { + for (auto& s_uptr : shards) { auto& s = *s_uptr; raft::device_setter guard(s.device_id); const i_t nv = s.rank_data.owned_var_size; @@ -90,18 +94,18 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng } } - // Host -> master device. engine.stream lives on the master device + // Host -> master device. `stream` lives on the master device // (created at engine construction when master device was current). raft::copy(master_pdhg.get_potential_next_primal_solution().data(), h_primal.data(), total_vars, - engine.stream.view()); + stream.view()); raft::copy(master_pdhg.get_potential_next_dual_solution().data(), h_dual.data(), total_cstrs, - engine.stream.view()); - raft::copy(master_reduced_cost.data(), h_reduced_cost.data(), total_vars, engine.stream.view()); - RAFT_CUDA_TRY(cudaStreamSynchronize(engine.stream.view().value())); + stream.view()); + raft::copy(master_reduced_cost.data(), h_reduced_cost.data(), total_vars, stream.view()); + RAFT_CUDA_TRY(cudaStreamSynchronize(stream.view().value())); } // -------- Distributed bound / objective rescaling ------------------------- @@ -109,8 +113,7 @@ void gather_potential_next_solutions_to_master(multi_gpu_engine_t& eng // raw squared-sum on device to hand to NCCL AllReduce and the base version comptues // tranform->reduce->transform in one cub call for efficiency template -void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, - f_t c_scaling_weight) +void multi_gpu_engine_t::distributed_bound_objective_rescaling(f_t c_scaling_weight) { raft::common::nvtx::range scope("distributed_bound_objective_rescaling"); @@ -118,7 +121,7 @@ void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, // squared L2 norms on host as we go. f_t global_bound_sq = f_t(0); f_t global_obj_sq = f_t(0); - engine.for_each_shard([&](auto& s) { + for_each_shard([&](auto& s) { const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); const i_t n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); const i_t n_owned_var = static_cast(s.rank_data.owned_var_size); @@ -145,23 +148,21 @@ void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, const f_t obj_rescaling = rescaling_from_squared_norm_op{}(global_obj_sq); // 4) Publish + apply on every shard via the shared helpers. - engine.for_each_shard([&](auto& s) { + for_each_shard([&](auto& s) { auto& scaling = s.sub_pdlp->get_initial_scaling_strategy(); scaling.set_h_bound_rescaling(bound_rescaling); scaling.set_h_objective_rescaling(obj_rescaling); scaling.apply_bound_objective_rescaling_to_problem(); }); - engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } // -------- Distributed Ruiz inf-scaling ------------------------------------ // Each shard owns its rows AND its columns and stores both complete (h_A = // owned rows, h_A_t = owned columns) template -void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, - int num_iter, - i_t n_global_vars) +void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int num_iter, i_t n_global_vars) { if (num_iter <= 0 || n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_ruiz_inf_scaling"); @@ -169,10 +170,10 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, for (int it = 0; it < num_iter; ++it) { // Refresh halo copies of both cumulative scalings (owner -> halo) so the // per-shard kernels read correct opposite-axis factors on their halo. - engine.halo_exchange_var_shard([](auto& s) -> auto& { + halo_exchange_var_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + halo_exchange_cstr_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); @@ -181,20 +182,20 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, // cols: inf norm only over OWNED (full) cols from A_T // Then fold into cumulative on owned entries (halo entries get refreshed by // the next iteration's halo update) - engine.for_each_shard( + for_each_shard( [](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_local(); }); } // Final refresh so downstream consumers (the scaled problem, the next // distributed_max_singular_value, etc.) see correct halo factors. - engine.halo_exchange_var_shard([](auto& s) -> auto& { + halo_exchange_var_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + halo_exchange_cstr_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); - engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } // -------- Distributed Pock-Chambolle scaling ------------------------------ @@ -202,50 +203,48 @@ void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, // pock_chambolle_scaling. Row sum-of-powers come from the row-major matrix // (owned rows) and column sum-of-powers from A_T (owned columns). template -void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, - f_t alpha, - i_t n_global_vars) +void multi_gpu_engine_t::distributed_pock_chambolle_scaling(f_t alpha, + i_t n_global_vars) { if (n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); // Refresh halo copies of both cumulative scalings - engine.halo_exchange_var_shard([](auto& s) -> auto& { + halo_exchange_var_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + halo_exchange_cstr_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); - engine.for_each_shard([alpha](auto& shard) { + for_each_shard([alpha](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_scaling(alpha); }); // Final refresh for downstream consumers. - engine.halo_exchange_var_shard([](auto& s) -> auto& { + halo_exchange_var_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - engine.halo_exchange_cstr_shard([](auto& s) -> auto& { + halo_exchange_cstr_shard([](auto& s) -> auto& { return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); - engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); } // -------- Distributed scaling orchestration ------------------------------ // Mirrors what scale_problem() does in single-GPU by composing the // individual distributed passes. See the header for the full pipeline. template -void distributed_scaling(multi_gpu_engine_t& engine, - pdlp_hyper_params_t const& hyper_params, - i_t n_global_vars, - bool inside_mip) +void multi_gpu_engine_t::distributed_scaling(pdlp_hyper_params_t const& hyper_params, + i_t n_global_vars, + bool inside_mip) { raft::common::nvtx::range scope("distributed_scaling"); // 1) Grow per-shard iteration_* scratch back to full size (the shard ctor // released it after its no-op local pre-scaling pass) - engine.for_each_shard([](auto& shard) { + for_each_shard([](auto& shard) { auto& scaling = shard.sub_pdlp->get_initial_scaling_strategy(); auto& op = shard.sub_pdlp->get_op_problem_scaled(); scaling.get_iteration_variable_scaling().resize(op.n_variables, shard.stream.view()); @@ -255,25 +254,25 @@ void distributed_scaling(multi_gpu_engine_t& engine, // 2) Matrix scaling passes populate the cumulative row/col scalings on // every shard. Each pass keeps the halo copies refreshed internally. if (hyper_params.do_ruiz_scaling) { - distributed_ruiz_inf_scaling(engine, hyper_params.default_l_inf_ruiz_iterations, n_global_vars); + distributed_ruiz_inf_scaling(hyper_params.default_l_inf_ruiz_iterations, n_global_vars); } if (hyper_params.do_pock_chambolle_scaling) { distributed_pock_chambolle_scaling( - engine, static_cast(hyper_params.default_alpha_pock_chambolle_rescaling), n_global_vars); + static_cast(hyper_params.default_alpha_pock_chambolle_rescaling), n_global_vars); } // 3) Per-shard apply of the accumulated scaling to A, c, variable and // constraint bounds. This is scale_problem() minus its local // bound/objective rescaling; the equivalent global step happens in (4). - engine.for_each_shard([](auto& shard) { + for_each_shard([](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().apply_cummulative_scaling_to_problem(); }); - engine.for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + for_each_shard([](auto& shard) { shard.stream.synchronize(); }); // 4) Global bound/objective rescaling (all shards get the identical scalar). if (hyper_params.bound_objective_rescaling) { distributed_bound_objective_rescaling( - engine, static_cast(hyper_params.initial_primal_weight_c_scaling)); + static_cast(hyper_params.initial_primal_weight_c_scaling)); } } @@ -283,14 +282,13 @@ void distributed_scaling(multi_gpu_engine_t& engine, // *_bufs helpers (halo_exchange_{cstr,var}_bufs, distributed_l2_norm_bufs, // distributed_dot_bufs), so this function contains no NCCL calls directly. template -f_t distributed_max_singular_value(multi_gpu_engine_t& engine, - i_t n_global_cstrs, - int max_iterations, - f_t tolerance) +f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cstrs, + int max_iterations, + f_t tolerance) { raft::common::nvtx::range scope("distributed_max_singular_value"); - const int nb = static_cast(engine.shards.size()); + const int nb = static_cast(shards.size()); // Generate the GLOBAL z[] sequence in cstr-index order from a fresh // mt19937(1), once per call. It's m doubles regardless of N (cheap). @@ -324,7 +322,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, // Scatter z according to partition for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); const i_t cstr_total = s.rank_data.total_cstr_size; const i_t var_total = s.rank_data.total_var_size; @@ -371,7 +369,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, sigma_sq_scalar.reserve(nb); residual_scalar.reserve(nb); for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; + auto& s = *shards[r]; const std::size_t owned = static_cast(s.rank_data.owned_cstr_size); q_full.emplace_back(q[r].data(), q[r].size()); atq_full.emplace_back(atq[r].data(), atq[r].size()); @@ -391,20 +389,20 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, for (int it = 0; it < max_iterations; ++it) { // q := z on the owned slice (the carried iterate). for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); const i_t n_owned = s.rank_data.owned_cstr_size; raft::copy(q[r].data(), z[r].data(), n_owned, s.stream.view()); } // ||q||₂ over the global OWNED cstr slice (one allreduce-sum + sqrt). - engine.distributed_l2_norm_bufs(q_owned, norm_q_scalar); + distributed_l2_norm_bufs(q_owned, norm_q_scalar); // q /= ||q||₂ on owned slice (halo gets refreshed by next exchange). // Kept inline: the divisor differs per shard (each shard reads its own // norm_q[r]) so a single shared functor won't do. for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); const i_t n_owned = s.rank_data.owned_cstr_size; cub::DeviceTransform::Transform( @@ -416,29 +414,29 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, } // atq = A^T q : refresh halo of q, then per-shard SpMV. - engine.halo_exchange_cstr_bufs(q_full); + halo_exchange_cstr_bufs(q_full); for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); s.sub_pdlp->pdhg_solver_.spmv_At_into(q[r], atq_dn[r]); } // z = A atq : refresh halo of atq, then per-shard SpMV. - engine.halo_exchange_var_bufs(atq_full); + halo_exchange_var_bufs(atq_full); for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); s.sub_pdlp->pdhg_solver_.spmv_A_into(atq[r], z_dn[r]); } // σ² = q · z over the global OWNED cstr slice (= q^T A A^T q = σ_max² // when q is the dominant left-singular vector). - engine.distributed_dot_bufs(q_owned, z_owned, sigma_sq_scalar); + distributed_dot_bufs(q_owned, z_owned, sigma_sq_scalar); // q := -σ² q + z (owned slice) — residual of the eigen-equation. // Kept inline for the same per-shard-scalar reason as normalize above. for (int r = 0; r < nb; ++r) { - auto& s = *engine.shards[r]; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); const i_t n_owned = s.rank_data.owned_cstr_size; cub::DeviceTransform::Transform( @@ -450,8 +448,8 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, } // Convergence check via global residual norm. - engine.distributed_l2_norm_bufs(q_owned, residual_scalar); - auto& s0 = *engine.shards[0]; + distributed_l2_norm_bufs(q_owned, residual_scalar); + auto& s0 = *shards[0]; raft::device_setter guard0(s0.device_id); f_t h_res{}; raft::copy(&h_res, residual_norm[0].data(), 1, s0.stream.view()); @@ -460,7 +458,7 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, } // σ_max² is the same on every shard after the last allreduce. - auto& s0 = *engine.shards[0]; + auto& s0 = *shards[0]; raft::device_setter guard0(s0.device_id); f_t sigma_sq_h{}; raft::copy(&sigma_sq_h, sigma_sq[0].data(), 1, s0.stream.view()); @@ -476,15 +474,16 @@ f_t distributed_max_singular_value(multi_gpu_engine_t& engine, // This function mirrors single-GPU's compute_initial_step_size exactly // and broadcasts the result to every shard template -void distributed_compute_initial_step_size(multi_gpu_engine_t& engine, - pdlp_solver_t& master, - pdlp_hyper_params_t const& hyper_params, - i_t n_global_cstrs, - f_t scaling_factor, - int max_iterations, - f_t tolerance) +void multi_gpu_engine_t::distributed_compute_initial_step_size( + pdlp_hyper_params_t const& hyper_params, + i_t n_global_cstrs, + f_t scaling_factor, + int max_iterations, + f_t tolerance) { raft::common::nvtx::range scope("distributed_compute_initial_step_size"); + cuopt_assert(master_pdlp_ != nullptr, + "distributed_compute_initial_step_size requires set_master(...)"); cuopt_expects(hyper_params.initial_step_size_max_singular_value, error_type_t::ValidationError, "distributed_compute_initial_step_size requires " @@ -493,18 +492,19 @@ void distributed_compute_initial_step_size(multi_gpu_engine_t& engine, "earlier in solve_lp_distributed_from_mps."); const f_t sigma_max = - distributed_max_singular_value(engine, n_global_cstrs, max_iterations, tolerance); + distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); + auto& master = *master_pdlp_; auto* handle_ptr = master.get_handle_ptr(); auto stream_view = handle_ptr->get_stream(); const f_t h_step_size = (sigma_max > f_t{0}) ? scaling_factor / sigma_max : f_t{1}; raft::copy(master.get_step_size().data(), &h_step_size, 1, stream_view); - engine.for_each_shard([&](auto& shard) { + for_each_shard([&](auto& shard) { raft::copy(shard.sub_pdlp->get_step_size().data(), &h_step_size, 1, shard.stream); }); - engine.sync_await_shards(stream_view); + sync_await_shards(stream_view); handle_ptr->sync_stream(stream_view); } @@ -518,11 +518,12 @@ void distributed_compute_initial_step_size(multi_gpu_engine_t& engine, // -> uninitialized_fill(primal_weight_ / best_primal_weight_, 1); return // This function also fills the shards and masters primal_weight / step_size buffers template -void distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, - pdlp_solver_t& master, - pdlp_hyper_params_t const& hyper_params) +void multi_gpu_engine_t::distributed_compute_initial_primal_weight( + pdlp_hyper_params_t const& hyper_params) { raft::common::nvtx::range scope("distributed_compute_initial_primal_weight"); + cuopt_assert(master_pdlp_ != nullptr, + "distributed_compute_initial_primal_weight requires set_master(...)"); cuopt_expects( !hyper_params.initial_primal_weight_combined_bounds && hyper_params.bound_objective_rescaling, error_type_t::ValidationError, @@ -532,53 +533,47 @@ void distributed_compute_initial_primal_weight(multi_gpu_engine_t& eng "earlier in solve_lp_distributed_from_mps."); const f_t h_primal_weight = f_t(1); + auto& master = *master_pdlp_; auto* handle_ptr = master.get_handle_ptr(); auto stream_view = handle_ptr->get_stream(); raft::copy(master.get_primal_weight().data(), &h_primal_weight, 1, stream_view); raft::copy(master.get_best_primal_weight().data(), &h_primal_weight, 1, stream_view); - engine.for_each_shard([&](auto& shard) { + for_each_shard([&](auto& shard) { auto& sub = *shard.sub_pdlp; raft::copy(sub.get_primal_weight().data(), &h_primal_weight, 1, shard.stream); raft::copy(sub.get_best_primal_weight().data(), &h_primal_weight, 1, shard.stream); }); - engine.sync_await_shards(stream_view); + sync_await_shards(stream_view); handle_ptr->sync_stream(stream_view); } -// ----- Explicit instantiations (mirror multi_gpu_engine_t) ----- -#define INSTANTIATE(F_TYPE) \ - template void distributed_bound_objective_rescaling( \ - multi_gpu_engine_t & engine, F_TYPE c_scaling_weight); \ - template void distributed_ruiz_inf_scaling( \ - multi_gpu_engine_t & engine, int num_iter, int n_global_vars); \ - template void distributed_pock_chambolle_scaling( \ - multi_gpu_engine_t & engine, F_TYPE alpha, int n_global_vars); \ - template void distributed_scaling(multi_gpu_engine_t & engine, \ - pdlp_hyper_params_t const& hyper_params, \ - int n_global_vars, \ - bool inside_mip); \ - template F_TYPE distributed_max_singular_value( \ - multi_gpu_engine_t & engine, \ - int n_global_cstrs, \ - int max_iterations, \ - F_TYPE tolerance); \ - template void distributed_compute_initial_step_size( \ - multi_gpu_engine_t & engine, \ - pdlp_solver_t & master, \ - pdlp_hyper_params_t const& hyper_params, \ - int n_global_cstrs, \ - F_TYPE scaling_factor, \ - int max_iterations, \ - F_TYPE tolerance); \ - template void distributed_compute_initial_primal_weight( \ - multi_gpu_engine_t & engine, \ - pdlp_solver_t & master, \ - pdlp_hyper_params_t const& hyper_params); \ - template void gather_potential_next_solutions_to_master( \ - multi_gpu_engine_t & engine, \ - pdhg_solver_t & master_pdhg, \ - rmm::device_uvector & master_reduced_cost); +// ----- Explicit instantiations (member-by-member) -------------------------- +// The class template is instantiated in multi_gpu_engine.cu; here we only +// explicit-instantiate the out-of-line members defined in this TU. +#define INSTANTIATE(F_TYPE) \ + template void \ + multi_gpu_engine_t::gather_potential_next_solutions_to_master( \ + rmm::device_uvector&); \ + template void \ + multi_gpu_engine_t::distributed_bound_objective_rescaling( \ + F_TYPE); \ + template void \ + multi_gpu_engine_t::distributed_ruiz_inf_scaling(int, int); \ + template void \ + multi_gpu_engine_t::distributed_pock_chambolle_scaling( \ + F_TYPE, int); \ + template void multi_gpu_engine_t::distributed_scaling( \ + pdlp_hyper_params_t const&, int, bool); \ + template F_TYPE \ + multi_gpu_engine_t::distributed_max_singular_value( \ + int, int, F_TYPE); \ + template void \ + multi_gpu_engine_t::distributed_compute_initial_step_size( \ + pdlp_hyper_params_t const&, int, F_TYPE, int, F_TYPE); \ + template void \ + multi_gpu_engine_t::distributed_compute_initial_primal_weight( \ + pdlp_hyper_params_t const&); INSTANTIATE(double) INSTANTIATE(float) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp deleted file mode 100644 index 48f995b92d..0000000000 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.hpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -#pragma once - -#include - -#include - -// Algorithm-level distributed PDLP -namespace cuopt::mathematical_optimization::pdlp { - -template -struct multi_gpu_engine_t; - -template -class pdhg_solver_t; - -template -class pdlp_solver_t; - -// Global bound/objective rescaling: allreduce the owned partial squared norms -// of the constraint bounds and (weighted) objective, then apply the identical -// scalar on every shard. -template -void distributed_bound_objective_rescaling(multi_gpu_engine_t& engine, - f_t c_scaling_weight); - -// Distributed Ruiz inf-scaling (num_iter passes). Each shard computes both its -// owned-row and owned-column inf-norms locally; a per-iteration halo broadcast -// of both cumulative scalings is the only cross-shard communication. -template -void distributed_ruiz_inf_scaling(multi_gpu_engine_t& engine, - int num_iter, - i_t n_global_vars); - -// Distributed Pock-Chambolle scaling (one pass), mirroring the single-GPU -// pock_chambolle_scaling. -template -void distributed_pock_chambolle_scaling(multi_gpu_engine_t& engine, - f_t alpha, - i_t n_global_vars); - -// Full distributed scaling entry point. Mirrors what scale_problem() does in -// single-GPU by orchestrating: -// - reset per-shard scaling state -// - Ruiz inf-scaling -> populates cumulative row/col scalings -// - Pock-Chambolle scaling -> same -// - per-shard apply_cummulative_scaling_to_problem() to apply the cumulative -// scalings to A, c, variable and constraint bounds (this is scale_problem() -// minus its shard-local bound/objective rescaling) -// - global bound/objective rescaling via distributed_bound_objective_rescaling -template -void distributed_scaling(multi_gpu_engine_t& engine, - pdlp_hyper_params_t const& hyper_params, - i_t n_global_vars, - bool inside_mip); - -// Distributed sigma_max(A) via power iteration (used to seed the initial -// step size). Returns the largest singular value of the scaled constraint -// matrix; identical on every shard. -template -f_t distributed_max_singular_value(multi_gpu_engine_t& engine, - i_t n_global_cstrs, - int max_iterations = 5000, - f_t tolerance = 1e-4); - -// Distributed counterpart of pdlp_solver_t::compute_initial_step_size. -template -void distributed_compute_initial_step_size(multi_gpu_engine_t& engine, - pdlp_solver_t& master, - pdlp_hyper_params_t const& hyper_params, - i_t n_global_cstrs, - f_t scaling_factor, - int max_iterations, - f_t tolerance); - -// Distributed counterpart of pdlp_solver_t::compute_initial_primal_weight. -// Writes primal_weight = best_primal_weight = 1 onto master + every shard, -// mirroring the Stable3-shaped short-circuit -// (!initial_primal_weight_combined_bounds && bound_objective_rescaling). -template -void distributed_compute_initial_primal_weight(multi_gpu_engine_t& engine, - pdlp_solver_t& master, - pdlp_hyper_params_t const& hyper_params); - -// Gather the global potential_next primal/dual solutions and the reduced cost -// onto the master from the owned slices distributed across shards. -template -void gather_potential_next_solutions_to_master(multi_gpu_engine_t& engine, - pdhg_solver_t& master_pdhg, - rmm::device_uvector& master_reduced_cost); - -} // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 86ee91c881..25254ddaed 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -492,6 +492,61 @@ struct multi_gpu_engine_t { void distributed_compute_A_x(); void distributed_compute_At_y(); + // -------- High-level algorithms (defined in distributed_algorithms.cu) --- + // Global bound/objective rescaling: allreduce the owned partial squared norms + // of the constraint bounds and (weighted) objective, then apply the identical + // scalar on every shard. + void distributed_bound_objective_rescaling(f_t c_scaling_weight); + + // Distributed Ruiz inf-scaling (num_iter passes). Each shard computes both its + // owned-row and owned-column inf-norms locally; a per-iteration halo broadcast + // of both cumulative scalings is the only cross-shard communication. + void distributed_ruiz_inf_scaling(int num_iter, i_t n_global_vars); + + // Distributed Pock-Chambolle scaling (one pass), mirroring the single-GPU + // pock_chambolle_scaling. + void distributed_pock_chambolle_scaling(f_t alpha, i_t n_global_vars); + + // Full distributed scaling entry point. Mirrors what scale_problem() does in + // single-GPU by orchestrating: + // - reset per-shard scaling state + // - Ruiz inf-scaling -> populates cumulative row/col scalings + // - Pock-Chambolle scaling -> same + // - per-shard apply_cummulative_scaling_to_problem() to apply the cumulative + // scalings to A, c, variable and constraint bounds (this is scale_problem() + // minus its shard-local bound/objective rescaling) + // - global bound/objective rescaling via distributed_bound_objective_rescaling + void distributed_scaling(pdlp_hyper_params_t const& hyper_params, + i_t n_global_vars, + bool inside_mip); + + // Distributed sigma_max(A) via power iteration (used to seed the initial + // step size). Returns the largest singular value of the scaled constraint + // matrix; identical on every shard. + f_t distributed_max_singular_value(i_t n_global_cstrs, + int max_iterations = 5000, + f_t tolerance = 1e-4); + + // Distributed counterpart of pdlp_solver_t::compute_initial_step_size. + // Requires set_master(...) to have been called; writes onto *master_pdlp_. + void distributed_compute_initial_step_size(pdlp_hyper_params_t const& hyper_params, + i_t n_global_cstrs, + f_t scaling_factor, + int max_iterations, + f_t tolerance); + + // Distributed counterpart of pdlp_solver_t::compute_initial_primal_weight. + // Writes primal_weight = best_primal_weight = 1 onto master + every shard, + // mirroring the Stable3-shaped short-circuit + // (!initial_primal_weight_combined_bounds && bound_objective_rescaling). + // Requires set_master(...) to have been called. + void distributed_compute_initial_primal_weight(pdlp_hyper_params_t const& hyper_params); + + // Gather the global potential_next primal/dual solutions and the reduced cost + // onto the master from the owned slices distributed across shards. + // Requires set_master(...) to have been called; writes onto master_pdlp_->pdhg_solver_. + void gather_potential_next_solutions_to_master(rmm::device_uvector& master_reduced_cost); + // Engine-level stream for fork/join orchestration (master side). rmm::cuda_stream stream; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 081773acb9..89b18a0364 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -20,7 +20,6 @@ #include #include #include "cuopt/mathematical_optimization/pdlp/solver_solution.hpp" -#include "distributed_pdlp/distributed_algorithms.hpp" #include "distributed_pdlp/multi_gpu_engine.hpp" #include @@ -3302,7 +3301,7 @@ void pdlp_solver_t::scale_problem() { raft::common::nvtx::range fun_scope("pdlp_solver_t::scale_problem"); if (is_distributed_master()) { - distributed_scaling(*multi_gpu_engine, settings_.hyper_params, primal_size_h_, inside_mip_); + multi_gpu_engine->distributed_scaling(settings_.hyper_params, primal_size_h_, inside_mip_); } else { initial_scaling_strategy_.scale_problem(); } @@ -3346,13 +3345,8 @@ void pdlp_solver_t::compute_initial_step_size() // Distributed dispatch: everything (sigma_max, deriving primal/dual // step sizes from master's current primal_weight_, seeding master + // all shards, syncs) lives inside distributed_compute_initial_step_size. - distributed_compute_initial_step_size(*multi_gpu_engine, - *this, - settings_.hyper_params, - dual_size_h_, - scaling_factor, - max_iterations, - tolerance); + multi_gpu_engine->distributed_compute_initial_step_size( + settings_.hyper_params, dual_size_h_, scaling_factor, max_iterations, tolerance); return; } @@ -3545,7 +3539,7 @@ void pdlp_solver_t::compute_initial_primal_weight() // - short-circuit -> 1 // - primal/dual step sizes from master's current step_size_, seeding // master + all shards, syncs) - distributed_compute_initial_primal_weight(*multi_gpu_engine, *this, settings_.hyper_params); + multi_gpu_engine->distributed_compute_initial_primal_weight(settings_.hyper_params); return; } diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index 56e681b94c..af75d1c0ed 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -5,7 +5,6 @@ */ /* clang-format on */ -#include #include #include @@ -574,8 +573,8 @@ pdlp_termination_strategy_t::fill_return_problem_solution( (&primal_iterate == ¤t_pdhg_solver.get_potential_next_primal_solution()) || (&primal_iterate == ¤t_pdhg_solver.get_primal_solution()); if (is_current_live_iterate) { - gather_potential_next_solutions_to_master( - *engine, current_pdhg_solver, convergence_information_.get_reduced_cost()); + engine->gather_potential_next_solutions_to_master( + convergence_information_.get_reduced_cost()); } } } From fddcaba183b46d86070eac5f79260f24111f0297 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 11:34:57 +0200 Subject: [PATCH 215/258] remove nccl debug ingo --- ci/test_cpp_multi_gpu.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index 8de2d6bfc9..83772130fe 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -112,8 +112,6 @@ if [ "${#mg_tests[@]}" -eq 0 ]; then exit 0 fi -# NCCL diagnostics for CI logs when distributed PDLP halo exchange fails. -export NCCL_DEBUG=INFO # PHB topology on the 2-GPU runner: disable direct GPU peer access (see topo above). # NCCL then uses the SHM transport, which needs a larger /dev/shm than the 64 MB # container default — the workflow launches the container with --shm-size for this. From 109cfaaea0faa8637399d98c103d962034280a43 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 11:51:53 +0200 Subject: [PATCH 216/258] even better mgpu hehehehehe (implemented specific gather operations) --- .../distributed_algorithms.cu | 87 ++------------ .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 13 ++ .../distributed_pdlp/multi_gpu_engine.hpp | 112 +++++++++++++++++- .../termination_strategy.cu | 5 +- 4 files changed, 135 insertions(+), 82 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index b59827a409..5d5d9e2d35 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -26,86 +26,24 @@ namespace cuopt::mathematical_optimization::pdlp { // -------- Solution gather (shards -> master) ------------------------------ +// Gather the potential next primal/dual solutions and the reduced cost from shards to master. template -void multi_gpu_engine_t::gather_potential_next_solutions_to_master( - rmm::device_uvector& master_reduced_cost) +void multi_gpu_engine_t::gather_potential_next_solutions_to_master() { cuopt_assert(master_pdlp_ != nullptr, "gather_potential_next_solutions_to_master requires set_master(...)"); - auto& master_pdhg = master_pdlp_->pdhg_solver_; - const std::size_t total_vars = master_pdhg.get_potential_next_primal_solution().size(); - const std::size_t total_cstrs = master_pdhg.get_potential_next_dual_solution().size(); - - std::vector h_primal(total_vars); - std::vector h_dual(total_cstrs); - std::vector h_reduced_cost(total_vars); - - for (auto& s_uptr : shards) { - auto& s = *s_uptr; - raft::device_setter guard(s.device_id); - const i_t nv = s.rank_data.owned_var_size; - const i_t nc = s.rank_data.owned_cstr_size; - - std::vector tmp_primal(nv); - std::vector tmp_dual(nc); - std::vector tmp_reduced_cost(nv); - - auto& sub_reduced_cost = s.sub_pdlp->get_current_termination_strategy() - .get_convergence_information() - .get_reduced_cost(); + gather_owned_var_to_master([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_potential_next_primal_solution(); + }); - if (nv > 0) { - raft::copy(tmp_primal.data(), - s.sub_pdlp->pdhg_solver_.get_potential_next_primal_solution().data(), - static_cast(nv), - s.stream.view()); - raft::copy(tmp_reduced_cost.data(), - sub_reduced_cost.data(), - static_cast(nv), - s.stream.view()); - } - if (nc > 0) { - raft::copy(tmp_dual.data(), - s.sub_pdlp->pdhg_solver_.get_potential_next_dual_solution().data(), - static_cast(nc), - s.stream.view()); - } - RAFT_CUDA_TRY(cudaStreamSynchronize(s.stream.view().value())); - - if (nv > 0) { - thrust::scatter(thrust::host, - tmp_primal.begin(), - tmp_primal.end(), - s.rank_data.local_to_global_var.begin(), - h_primal.begin()); - thrust::scatter(thrust::host, - tmp_reduced_cost.begin(), - tmp_reduced_cost.end(), - s.rank_data.local_to_global_var.begin(), - h_reduced_cost.begin()); - } - if (nc > 0) { - thrust::scatter(thrust::host, - tmp_dual.begin(), - tmp_dual.end(), - s.rank_data.local_to_global_cstr.begin(), - h_dual.begin()); - } - } + gather_owned_cstr_to_master([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_potential_next_dual_solution(); + }); - // Host -> master device. `stream` lives on the master device - // (created at engine construction when master device was current). - raft::copy(master_pdhg.get_potential_next_primal_solution().data(), - h_primal.data(), - total_vars, - stream.view()); - raft::copy(master_pdhg.get_potential_next_dual_solution().data(), - h_dual.data(), - total_cstrs, - stream.view()); - raft::copy(master_reduced_cost.data(), h_reduced_cost.data(), total_vars, stream.view()); - RAFT_CUDA_TRY(cudaStreamSynchronize(stream.view().value())); + gather_owned_var_to_master([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.get_current_termination_strategy().get_convergence_information().get_reduced_cost(); + }); } // -------- Distributed bound / objective rescaling ------------------------- @@ -553,8 +491,7 @@ void multi_gpu_engine_t::distributed_compute_initial_primal_weight( // explicit-instantiate the out-of-line members defined in this TU. #define INSTANTIATE(F_TYPE) \ template void \ - multi_gpu_engine_t::gather_potential_next_solutions_to_master( \ - rmm::device_uvector&); \ + multi_gpu_engine_t::gather_potential_next_solutions_to_master();\ template void \ multi_gpu_engine_t::distributed_bound_objective_rescaling( \ F_TYPE); \ diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 1a9e064022..1e931b27a4 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -73,6 +73,19 @@ multi_gpu_engine_t::multi_gpu_engine_t( graph_shard_ready_events_.emplace_back(std::make_unique()); sync_shard_ready_events_.emplace_back(std::make_unique()); } + + // Cache per-shard partition metadata for gather_owned_*_to_master_bufs. + owned_var_sizes_.reserve(nb_parts); + owned_cstr_sizes_.reserve(nb_parts); + local_to_global_vars_.reserve(nb_parts); + local_to_global_cstrs_.reserve(nb_parts); + for (int r = 0; r < nb_parts; ++r) { + auto const& rd = shards[r]->rank_data; + owned_var_sizes_.push_back(static_cast(rd.owned_var_size)); + owned_cstr_sizes_.push_back(static_cast(rd.owned_cstr_size)); + local_to_global_vars_.push_back(rd.local_to_global_var); + local_to_global_cstrs_.push_back(rd.local_to_global_cstr); + } } // -------- High-level: A @ x and A_T @ y ----------------------------------- diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 25254ddaed..34fdc53642 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -37,7 +37,6 @@ #include #include -#include #include #include #include @@ -337,6 +336,103 @@ struct multi_gpu_engine_t { halo_exchange_cstr_bufs(bufs); } + // -------- Gather owned slices to master --------------------------------- + // Scatters data from each shard's owned slice to the master's buffer. + // var and cstr version share the same implementation. + void gather_owned_var_to_master_bufs( + std::vector> const& shard_owned, + raft::device_span master_buf) + { + gather_owned_to_master_bufs_impl( + shard_owned, master_buf, owned_var_sizes_, local_to_global_vars_); + } + + void gather_owned_cstr_to_master_bufs( + std::vector> const& shard_owned, + raft::device_span master_buf) + { + gather_owned_to_master_bufs_impl( + shard_owned, master_buf, owned_cstr_sizes_, local_to_global_cstrs_); + } + + // Wrappers around gather_owned_{var/cstr}_to_master_bufs using an accessor + // buf_access : pdlp_solver_t& -> rmm::device_uvector& + template + void gather_owned_var_to_master(BufAccess&& buf_access) + { + cuopt_assert(master_pdlp_ != nullptr, + "gather_owned_var_to_master requires set_master(...)"); + std::vector> shard_bufs; + shard_bufs.reserve(shards.size()); + for_each_shard([&](pdlp_shard_t& s) { + auto& x = buf_access(*s.sub_pdlp); + shard_bufs.emplace_back(x.data(), static_cast(s.rank_data.owned_var_size)); + }); + auto& m = buf_access(*master_pdlp_); + gather_owned_var_to_master_bufs(shard_bufs, raft::device_span{m.data(), m.size()}); + } + + template + void gather_owned_cstr_to_master(BufAccess&& buf_access) + { + cuopt_assert(master_pdlp_ != nullptr, + "gather_owned_cstr_to_master requires set_master(...)"); + std::vector> shard_bufs; + shard_bufs.reserve(shards.size()); + for_each_shard([&](pdlp_shard_t& s) { + auto& x = buf_access(*s.sub_pdlp); + shard_bufs.emplace_back(x.data(), static_cast(s.rank_data.owned_cstr_size)); + }); + auto& m = buf_access(*master_pdlp_); + gather_owned_cstr_to_master_bufs(shard_bufs, raft::device_span{m.data(), m.size()}); + } + + // Actual implementation. {var/cstr}-agnostic core shared by the two + // gather_owned_{var/cstr}_to_master_bufs entry points above. + // owned_sizes[r] and local_to_globals[r] are the axis-specific + // rank_data.owned_{var/cstr}_size and rank_data.local_to_global_{var/cstr} for shard r. + void gather_owned_to_master_bufs_impl( + std::vector> const& shard_owned, + raft::device_span master_buf, + std::vector const& owned_sizes, + std::vector> const& local_to_globals) + { + cuopt_assert(master_pdlp_ != nullptr, + "gather_owned_to_master_bufs_impl requires set_master(...)"); + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(shard_owned.size()) == nb && + static_cast(owned_sizes.size()) == nb && + static_cast(local_to_globals.size()) == nb, + error_type_t::RuntimeError, + "gather_owned_to_master_bufs_impl: shard_owned / owned_sizes / " + "local_to_globals must all have size == shards.size()"); + + // Assemble on host in global-index order. + std::vector h_master(master_buf.size()); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const std::size_t n_owned = owned_sizes[r]; + cuopt_expects(shard_owned[r].size() == n_owned, + error_type_t::RuntimeError, + "gather_owned_to_master_bufs_impl: shard_owned[r].size() must equal owned size"); + if (n_owned == 0) continue; + std::vector tmp(n_owned); + raft::copy(tmp.data(), shard_owned[r].data(), n_owned, s.stream.view()); + // Sync so tmp is populated before the host scatter (and stays valid). + s.stream.synchronize(); + thrust::scatter(thrust::host, + tmp.begin(), + tmp.end(), + local_to_globals[r].begin(), + h_master.begin()); + } + + // Single H->D onto master's device (`stream` lives on the master device). + raft::copy(master_buf.data(), h_master.data(), master_buf.size(), stream.view()); + stream.synchronize(); + } + // -------- NCCL allreduce (sum, in place) -------------------------------- // Core: per-shard in-place sum-allreduce on a single f_t scalar viewed by // scalars[r], wrapped in one NCCL group so it executes as a single @@ -544,8 +640,8 @@ struct multi_gpu_engine_t { // Gather the global potential_next primal/dual solutions and the reduced cost // onto the master from the owned slices distributed across shards. - // Requires set_master(...) to have been called; writes onto master_pdlp_->pdhg_solver_. - void gather_potential_next_solutions_to_master(rmm::device_uvector& master_reduced_cost); + // Requires set_master(...) to have been called; writes onto master_pdlp_. + void gather_potential_next_solutions_to_master(); // Engine-level stream for fork/join orchestration (master side). rmm::cuda_stream stream; @@ -554,6 +650,16 @@ struct multi_gpu_engine_t { // (owns device-affine resources: handle, NCCL comm, RMM buffers). std::vector>> shards; + // Cached per-shard partition metadata, populated once at construction from + // rank_data. Consumed by the gather_owned_{var/cstr}_to_master_bufs helpers so they + // don't rebuild these vectors on every call. + // owned_{var,cstr}_sizes_[r] == shards[r]->rank_data.owned_{var,cstr}_size + // local_to_global_{vars,cstrs}_[r] == shards[r]->rank_data.local_to_global_{var,cstr} + std::vector owned_var_sizes_; + std::vector owned_cstr_sizes_; + std::vector> local_to_global_vars_; + std::vector> local_to_global_cstrs_; + // Non-owning back-pointer to the master pdlp_solver_t. pdlp_solver_t* master_pdlp_ = nullptr; void set_master(pdlp_solver_t* m) { master_pdlp_ = m; } diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index af75d1c0ed..e2cacd01fd 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -572,10 +572,7 @@ pdlp_termination_strategy_t::fill_return_problem_solution( const bool is_current_live_iterate = (&primal_iterate == ¤t_pdhg_solver.get_potential_next_primal_solution()) || (&primal_iterate == ¤t_pdhg_solver.get_primal_solution()); - if (is_current_live_iterate) { - engine->gather_potential_next_solutions_to_master( - convergence_information_.get_reduced_cost()); - } + if (is_current_live_iterate) { engine->gather_potential_next_solutions_to_master(); } } } From 6af656eebaea93a94d605ed396b0d70f09bc5af2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 11:52:10 +0200 Subject: [PATCH 217/258] clean diff in test_cpp_mgpu.sh --- ci/test_cpp_multi_gpu.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index 83772130fe..da7ef714ef 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -30,8 +30,7 @@ set -euo pipefail # container image (its tag bakes the value into the environment), so no CUDA # version needs to be pinned by the workflow. If it is unset we skip the # bootstrap and fall back to any gtests already present in the container/build - # tree - keeping the job non-breaking until the multi-GPU tests land. -if [ -n "${RAPIDS_CUDA_VERSION:-}" ]; then +# tree — keeping the job non-breaking until the multi-GPU tests land.if [ -n "${RAPIDS_CUDA_VERSION:-}" ]; then rapids-logger "Using CUDA ${RAPIDS_CUDA_VERSION} (inferred from container image)" rapids-logger "Configuring conda strict channel priority" conda config --set channel_priority strict @@ -68,7 +67,7 @@ nvidia-smi rapids-logger "Check GPU topology" nvidia-smi topo -m -# Multi-GPU tests are meaningless on a single device - fail loudly rather than +# Multi-GPU tests are meaningless on a single device — fail loudly rather than # passing a run that never exercised NCCL. GPU_COUNT=$(nvidia-smi -L | wc -l) rapids-logger "Detected ${GPU_COUNT} GPU(s)" @@ -108,7 +107,7 @@ shopt -u nullglob if [ "${#mg_tests[@]}" -eq 0 ]; then rapids-logger "No multi-GPU gtest binaries (*_MG_TEST) found in ${GTEST_DIR}; nothing to run." - echo "::notice::No multi-GPU tests present yet - skipping. This job lights up once *_MG_TEST binaries land (distributed PDLP)." + echo "::notice::No multi-GPU tests present yet — skipping. This job lights up once *_MG_TEST binaries land (distributed PDLP)." exit 0 fi From 5436ea0c4693fc81f296cfaaeef97b98d53e819d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 12:15:51 +0200 Subject: [PATCH 218/258] cleaned halo exchange funcs --- .../distributed_algorithms.cu | 32 ++-- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 10 +- .../distributed_pdlp/multi_gpu_engine.hpp | 162 ++++++------------ cpp/src/pdlp/distributed_pdlp/shard.hpp | 27 +++ 4 files changed, 100 insertions(+), 131 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 5d5d9e2d35..a20ba18f40 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -108,11 +108,11 @@ void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int num_iter, i_ for (int it = 0; it < num_iter; ++it) { // Refresh halo copies of both cumulative scalings (owner -> halo) so the // per-shard kernels read correct opposite-axis factors on their halo. - halo_exchange_var_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + halo_exchange_var([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - halo_exchange_cstr_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); // Shard-local Ruiz iteration @@ -126,11 +126,11 @@ void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int num_iter, i_ // Final refresh so downstream consumers (the scaled problem, the next // distributed_max_singular_value, etc.) see correct halo factors. - halo_exchange_var_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + halo_exchange_var([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - halo_exchange_cstr_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); for_each_shard([](auto& shard) { shard.stream.synchronize(); }); @@ -148,11 +148,11 @@ void multi_gpu_engine_t::distributed_pock_chambolle_scaling(f_t alpha, raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); // Refresh halo copies of both cumulative scalings - halo_exchange_var_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + halo_exchange_var([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - halo_exchange_cstr_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); for_each_shard([alpha](auto& shard) { @@ -160,11 +160,11 @@ void multi_gpu_engine_t::distributed_pock_chambolle_scaling(f_t alpha, }); // Final refresh for downstream consumers. - halo_exchange_var_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_variable_scaling(); + halo_exchange_var([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); }); - halo_exchange_cstr_shard([](auto& s) -> auto& { - return s.sub_pdlp->get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); }); for_each_shard([](auto& shard) { shard.stream.synchronize(); }); diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 1e931b27a4..9bb9686d65 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -92,16 +92,18 @@ multi_gpu_engine_t::multi_gpu_engine_t( template void multi_gpu_engine_t::distributed_compute_A_x() { - halo_exchange_var( - [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_primal(); }); + halo_exchange_var([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_reflected_primal(); + }); for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_A_x(); }); } template void multi_gpu_engine_t::distributed_compute_At_y() { - halo_exchange_cstr( - [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_dual_solution(); }); + halo_exchange_cstr([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_dual_solution(); + }); for_each_shard([](auto& shard) { shard.sub_pdlp->pdhg_solver_.spmvop_At_y(); }); } diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 34fdc53642..03b47cfbb6 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -162,30 +162,34 @@ struct multi_gpu_engine_t { distributed_transform(std::make_tuple(in), out, sz, op); } - // -------- Halo exchange (variables / x) --------------------------------- - // Step 1: thrust::gather per-peer outgoing values into staging buffers. - // Step 2: a single NCCL group with matched ncclSend / ncclRecv across all - // (rank, peer) pairs, receiving into each shard's halo region. - void halo_exchange_var_bufs(std::vector> const& bufs) + // -------- Halo exchange (owner -> halo) --------------------------------- + // {var/cstr}-agnostic core: + // Step 1: thrust::gather per-peer outgoing values into staging buffers. + // Step 2: one NCCL group with matched ncclSend / ncclRecv across all + // (rank, peer) pairs, receiving into each shard's halo tail. + void halo_exchange_bufs_impl( + std::vector> const& bufs, + std::vector::halo_axis_t> const& axes) { const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(bufs.size()) == nb, + cuopt_expects(static_cast(bufs.size()) == nb && static_cast(axes.size()) == nb, error_type_t::RuntimeError, - "halo_exchange_var_bufs: bufs.size() must equal shards.size()"); + "halo_exchange_bufs_impl: bufs / axes must have size == shards.size()"); // Step 1: gather owned values that each peer needs into per-peer staging. for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto x = bufs[r]; + auto const& ax = axes[r]; + auto x = bufs[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; - if (s.var_send_indices_d[peer].size() == 0) continue; + if (ax.send_indices[peer].size() == 0) continue; thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.var_send_indices_d[peer].begin(), - s.var_send_indices_d[peer].end(), + ax.send_indices[peer].begin(), + ax.send_indices[peer].end(), x.data(), - s.var_send_buf_d[peer].begin()); + ax.send_buf[peer].begin()); } } @@ -194,10 +198,11 @@ struct multi_gpu_engine_t { for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); + auto const& ax = axes[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; - CUOPT_NCCL_TRY(ncclSend(s.var_send_buf_d[peer].data(), - s.var_send_buf_d[peer].size(), + CUOPT_NCCL_TRY(ncclSend(ax.send_buf[peer].data(), + ax.send_buf[peer].size(), nccl_data_type(), peer, s.comm.get(), @@ -205,15 +210,15 @@ struct multi_gpu_engine_t { } } for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - auto& rd = s.rank_data; + auto& s = *shards[r]; raft::device_setter guard(s.device_id); - auto x = bufs[r]; + auto const& ax = axes[r]; + auto x = bufs[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; - f_t* recv_ptr = x.data() + rd.owned_var_size + rd.var_recv_offsets[peer]; + f_t* recv_ptr = x.data() + ax.owned_size + ax.recv_offsets[peer]; CUOPT_NCCL_TRY(ncclRecv(recv_ptr, - static_cast(rd.var_recv_counts[peer]), + static_cast(ax.recv_counts[peer]), nccl_data_type(), peer, s.comm.get(), @@ -223,116 +228,51 @@ struct multi_gpu_engine_t { CUOPT_NCCL_TRY(ncclGroupEnd()); } - // Wrapper: pdhg_solver_t accessor. - // buf_access : pdhg_solver_t& -> rmm::device_uvector& - template - void halo_exchange_var(BufAccess&& buf_access) + // -------- Halo exchange (variables / x) --------------------------------- + void halo_exchange_var_bufs(std::vector> const& bufs) { - halo_exchange_var_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { - return buf_access(s.sub_pdlp->pdhg_solver_); - }); + std::vector::halo_axis_t> axes; + axes.reserve(shards.size()); + for (auto& s : shards) axes.push_back(s->var_halo_axis()); + halo_exchange_bufs_impl(bufs, axes); } - // Wrapper: pdlp_shard_t accessor. Resolves one uvector per shard into a + // Wrapper: pdlp_solver_t accessor. Resolves one uvector per shard into a // vector of spans, then delegates to halo_exchange_var_bufs. - // buf_access : pdlp_shard_t& -> rmm::device_uvector& - template - void halo_exchange_var_shard(ShardBufAccess&& buf_access) + // buf_access : pdlp_solver_t& -> rmm::device_uvector& + template + void halo_exchange_var(BufAccess&& buf_access) { std::vector> bufs; bufs.reserve(shards.size()); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - auto& x = buf_access(*s); + for_each_shard([&](pdlp_shard_t& s) { + auto& x = buf_access(*s.sub_pdlp); bufs.emplace_back(x.data(), x.size()); - } + }); halo_exchange_var_bufs(bufs); } // -------- Halo exchange (constraints / y) ------------------------------- - // Cstr-halo counterpart of halo_exchange_var_bufs. Same structure: contains - // all the gather + NCCL send/recv logic; accessor overloads below are thin - // wrappers that resolve one buffer per shard and delegate. - // Requirements: bufs.size() == shards.size(); bufs[r] is shard r's owned + - // halo tail (contiguous) with total_cstr_size elements. void halo_exchange_cstr_bufs(std::vector> const& bufs) { - const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(bufs.size()) == nb, - error_type_t::RuntimeError, - "halo_exchange_cstr_bufs: bufs.size() must equal shards.size()"); - - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto y = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - if (s.cstr_send_indices_d[peer].size() == 0) continue; - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - s.cstr_send_indices_d[peer].begin(), - s.cstr_send_indices_d[peer].end(), - y.data(), - s.cstr_send_buf_d[peer].begin()); - } - } - - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - CUOPT_NCCL_TRY(ncclSend(s.cstr_send_buf_d[peer].data(), - s.cstr_send_buf_d[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - auto& rd = s.rank_data; - raft::device_setter guard(s.device_id); - auto y = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - f_t* recv_ptr = y.data() + rd.owned_cstr_size + rd.cstr_recv_offsets[peer]; - CUOPT_NCCL_TRY(ncclRecv(recv_ptr, - static_cast(rd.cstr_recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - CUOPT_NCCL_TRY(ncclGroupEnd()); + std::vector::halo_axis_t> axes; + axes.reserve(shards.size()); + for (auto& s : shards) axes.push_back(s->cstr_halo_axis()); + halo_exchange_bufs_impl(bufs, axes); } - // Wrapper: pdhg_solver_t accessor. - // buf_access : pdhg_solver_t& -> rmm::device_uvector& + // Wrapper: pdlp_solver_t accessor. Resolves one uvector per shard into a + // vector of spans, then delegates to halo_exchange_cstr_bufs. + // buf_access : pdlp_solver_t& -> rmm::device_uvector& template void halo_exchange_cstr(BufAccess&& buf_access) - { - halo_exchange_cstr_shard([&](pdlp_shard_t& s) -> rmm::device_uvector& { - return buf_access(s.sub_pdlp->pdhg_solver_); - }); - } - - // Wrapper: pdlp_shard_t accessor. Resolves one uvector per shard into a - // vector of spans, then delegates to halo_exchange_cstr_bufs. - // buf_access : pdlp_shard_t& -> rmm::device_uvector& - template - void halo_exchange_cstr_shard(ShardBufAccess&& buf_access) { std::vector> bufs; bufs.reserve(shards.size()); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - auto& y = buf_access(*s); + for_each_shard([&](pdlp_shard_t& s) { + auto& y = buf_access(*s.sub_pdlp); bufs.emplace_back(y.data(), y.size()); - } + }); halo_exchange_cstr_bufs(bufs); } @@ -650,10 +590,10 @@ struct multi_gpu_engine_t { // (owns device-affine resources: handle, NCCL comm, RMM buffers). std::vector>> shards; - // Cached per-shard partition metadata, populated once at construction from - // rank_data. Consumed by the gather_owned_{var/cstr}_to_master_bufs helpers so they - // don't rebuild these vectors on every call. - // owned_{var,cstr}_sizes_[r] == shards[r]->rank_data.owned_{var,cstr}_size + // Cached per-shard partition metadata, populated once at construction. + // Consumed by gather_owned_*_to_master_bufs; caching avoids copying the + // sizeable local_to_global_* host vectors on every termination check. + // owned_{var,cstr}_sizes_[r] == shards[r]->rank_data.owned_{var,cstr}_size // local_to_global_{vars,cstrs}_[r] == shards[r]->rank_data.local_to_global_{var,cstr} std::vector owned_var_sizes_; std::vector owned_cstr_sizes_; diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index caa9139c10..983e655d1f 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -77,6 +77,33 @@ struct pdlp_shard_t { std::vector> var_send_buf_d; std::vector> cstr_send_indices_d; std::vector> cstr_send_buf_d; + + // Non-owning bundle of per-axis halo-exchange metadata, indexed by peer. + // Consumed by multi_gpu_engine_t::halo_exchange_bufs_impl (one axis vector + // per axis is cached in the engine at construction). + struct halo_axis_t { + std::vector>& send_indices; // [peer] + std::vector>& send_buf; // [peer] + i_t owned_size; + std::vector const& recv_offsets; // [peer] + std::vector const& recv_counts; // [peer] + }; + halo_axis_t var_halo_axis() + { + return {var_send_indices_d, + var_send_buf_d, + rank_data.owned_var_size, + rank_data.var_recv_offsets, + rank_data.var_recv_counts}; + } + halo_axis_t cstr_halo_axis() + { + return {cstr_send_indices_d, + cstr_send_buf_d, + rank_data.owned_cstr_size, + rank_data.cstr_recv_offsets, + rank_data.cstr_recv_counts}; + } }; } // namespace cuopt::mathematical_optimization::pdlp From 25e11584487e73559c24d305c578f6ff382b90a0 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 12:20:15 +0200 Subject: [PATCH 219/258] small fix for compilation --- cpp/src/pdlp/pdlp.cu | 5 +++-- .../pdlp/termination_strategy/convergence_information.cu | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 89b18a0364..db5c91a1ff 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2297,8 +2297,9 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte if (is_distributed_master()) { // SpMV is the first operation in compute_interaction_and_movement so we can do halo before and // call it naturally we then reduce the local dot products - multi_gpu_engine->halo_exchange_cstr( - [](auto& pdhg) -> rmm::device_uvector& { return pdhg.get_reflected_dual(); }); + multi_gpu_engine->halo_exchange_cstr([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_reflected_dual(); + }); for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); diff --git a/cpp/src/pdlp/termination_strategy/convergence_information.cu b/cpp/src/pdlp/termination_strategy/convergence_information.cu index 37242d8c34..27500d4c89 100644 --- a/cpp/src/pdlp/termination_strategy/convergence_information.cu +++ b/cpp/src/pdlp/termination_strategy/convergence_information.cu @@ -480,8 +480,8 @@ void convergence_information_t::distributed_compute_primal_residual_an cuopt_assert(!batch_mode_, "multi-GPU PDLP is not supported in batch mode"); // Prepare halo values in potential_next_primal_solution. - engine.halo_exchange_var([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_potential_next_primal_solution(); + engine.halo_exchange_var([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_potential_next_primal_solution(); }); // Per-shard primal residual + partial (owned) primal objective. @@ -536,8 +536,8 @@ void convergence_information_t::distributed_compute_dual_residual_and_ // 1) Halo-exchange potential_next_dual_solution on every shard so the // A_T_shard @ y SpMV inside compute_dual_residual reads correct values // in the cstr halo region - engine.halo_exchange_cstr([](pdhg_solver_t& pdhg) -> rmm::device_uvector& { - return pdhg.get_potential_next_dual_solution(); + engine.halo_exchange_cstr([](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_potential_next_dual_solution(); }); // Same primal_iterate fix as the primal block above: use the shard's From f1c6970e94895587bf35bfbfc31f5d945c079164 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 12:20:27 +0200 Subject: [PATCH 220/258] style --- .../distributed_algorithms.cu | 41 +++++++------------ .../distributed_pdlp/multi_gpu_engine.hpp | 34 +++++++-------- cpp/src/pdlp/distributed_pdlp/shard.hpp | 4 +- cpp/src/pdlp/pdlp.cu | 7 ++-- 4 files changed, 36 insertions(+), 50 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index a20ba18f40..01d570df41 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -141,8 +141,7 @@ void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int num_iter, i_ // pock_chambolle_scaling. Row sum-of-powers come from the row-major matrix // (owned rows) and column sum-of-powers from A_T (owned columns). template -void multi_gpu_engine_t::distributed_pock_chambolle_scaling(f_t alpha, - i_t n_global_vars) +void multi_gpu_engine_t::distributed_pock_chambolle_scaling(f_t alpha, i_t n_global_vars) { if (n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); @@ -429,8 +428,7 @@ void multi_gpu_engine_t::distributed_compute_initial_step_size( "of A fallback is single-GPU only. This should have been rejected " "earlier in solve_lp_distributed_from_mps."); - const f_t sigma_max = - distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); + const f_t sigma_max = distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); auto& master = *master_pdlp_; auto* handle_ptr = master.get_handle_ptr(); @@ -489,28 +487,19 @@ void multi_gpu_engine_t::distributed_compute_initial_primal_weight( // ----- Explicit instantiations (member-by-member) -------------------------- // The class template is instantiated in multi_gpu_engine.cu; here we only // explicit-instantiate the out-of-line members defined in this TU. -#define INSTANTIATE(F_TYPE) \ - template void \ - multi_gpu_engine_t::gather_potential_next_solutions_to_master();\ - template void \ - multi_gpu_engine_t::distributed_bound_objective_rescaling( \ - F_TYPE); \ - template void \ - multi_gpu_engine_t::distributed_ruiz_inf_scaling(int, int); \ - template void \ - multi_gpu_engine_t::distributed_pock_chambolle_scaling( \ - F_TYPE, int); \ - template void multi_gpu_engine_t::distributed_scaling( \ - pdlp_hyper_params_t const&, int, bool); \ - template F_TYPE \ - multi_gpu_engine_t::distributed_max_singular_value( \ - int, int, F_TYPE); \ - template void \ - multi_gpu_engine_t::distributed_compute_initial_step_size( \ - pdlp_hyper_params_t const&, int, F_TYPE, int, F_TYPE); \ - template void \ - multi_gpu_engine_t::distributed_compute_initial_primal_weight( \ - pdlp_hyper_params_t const&); +#define INSTANTIATE(F_TYPE) \ + template void multi_gpu_engine_t::gather_potential_next_solutions_to_master(); \ + template void multi_gpu_engine_t::distributed_bound_objective_rescaling(F_TYPE); \ + template void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int, int); \ + template void multi_gpu_engine_t::distributed_pock_chambolle_scaling(F_TYPE, int); \ + template void multi_gpu_engine_t::distributed_scaling( \ + pdlp_hyper_params_t const&, int, bool); \ + template F_TYPE multi_gpu_engine_t::distributed_max_singular_value( \ + int, int, F_TYPE); \ + template void multi_gpu_engine_t::distributed_compute_initial_step_size( \ + pdlp_hyper_params_t const&, int, F_TYPE, int, F_TYPE); \ + template void multi_gpu_engine_t::distributed_compute_initial_primal_weight( \ + pdlp_hyper_params_t const&); INSTANTIATE(double) INSTANTIATE(float) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 03b47cfbb6..a1fe72f96a 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -233,7 +233,8 @@ struct multi_gpu_engine_t { { std::vector::halo_axis_t> axes; axes.reserve(shards.size()); - for (auto& s : shards) axes.push_back(s->var_halo_axis()); + for (auto& s : shards) + axes.push_back(s->var_halo_axis()); halo_exchange_bufs_impl(bufs, axes); } @@ -257,7 +258,8 @@ struct multi_gpu_engine_t { { std::vector::halo_axis_t> axes; axes.reserve(shards.size()); - for (auto& s : shards) axes.push_back(s->cstr_halo_axis()); + for (auto& s : shards) + axes.push_back(s->cstr_halo_axis()); halo_exchange_bufs_impl(bufs, axes); } @@ -279,17 +281,15 @@ struct multi_gpu_engine_t { // -------- Gather owned slices to master --------------------------------- // Scatters data from each shard's owned slice to the master's buffer. // var and cstr version share the same implementation. - void gather_owned_var_to_master_bufs( - std::vector> const& shard_owned, - raft::device_span master_buf) + void gather_owned_var_to_master_bufs(std::vector> const& shard_owned, + raft::device_span master_buf) { gather_owned_to_master_bufs_impl( shard_owned, master_buf, owned_var_sizes_, local_to_global_vars_); } void gather_owned_cstr_to_master_bufs( - std::vector> const& shard_owned, - raft::device_span master_buf) + std::vector> const& shard_owned, raft::device_span master_buf) { gather_owned_to_master_bufs_impl( shard_owned, master_buf, owned_cstr_sizes_, local_to_global_cstrs_); @@ -300,8 +300,7 @@ struct multi_gpu_engine_t { template void gather_owned_var_to_master(BufAccess&& buf_access) { - cuopt_assert(master_pdlp_ != nullptr, - "gather_owned_var_to_master requires set_master(...)"); + cuopt_assert(master_pdlp_ != nullptr, "gather_owned_var_to_master requires set_master(...)"); std::vector> shard_bufs; shard_bufs.reserve(shards.size()); for_each_shard([&](pdlp_shard_t& s) { @@ -315,8 +314,7 @@ struct multi_gpu_engine_t { template void gather_owned_cstr_to_master(BufAccess&& buf_access) { - cuopt_assert(master_pdlp_ != nullptr, - "gather_owned_cstr_to_master requires set_master(...)"); + cuopt_assert(master_pdlp_ != nullptr, "gather_owned_cstr_to_master requires set_master(...)"); std::vector> shard_bufs; shard_bufs.reserve(shards.size()); for_each_shard([&](pdlp_shard_t& s) { @@ -353,19 +351,17 @@ struct multi_gpu_engine_t { auto& s = *shards[r]; raft::device_setter guard(s.device_id); const std::size_t n_owned = owned_sizes[r]; - cuopt_expects(shard_owned[r].size() == n_owned, - error_type_t::RuntimeError, - "gather_owned_to_master_bufs_impl: shard_owned[r].size() must equal owned size"); + cuopt_expects( + shard_owned[r].size() == n_owned, + error_type_t::RuntimeError, + "gather_owned_to_master_bufs_impl: shard_owned[r].size() must equal owned size"); if (n_owned == 0) continue; std::vector tmp(n_owned); raft::copy(tmp.data(), shard_owned[r].data(), n_owned, s.stream.view()); // Sync so tmp is populated before the host scatter (and stays valid). s.stream.synchronize(); - thrust::scatter(thrust::host, - tmp.begin(), - tmp.end(), - local_to_globals[r].begin(), - h_master.begin()); + thrust::scatter( + thrust::host, tmp.begin(), tmp.end(), local_to_globals[r].begin(), h_master.begin()); } // Single H->D onto master's device (`stream` lives on the master device). diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 983e655d1f..5bc76a4a64 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -85,8 +85,8 @@ struct pdlp_shard_t { std::vector>& send_indices; // [peer] std::vector>& send_buf; // [peer] i_t owned_size; - std::vector const& recv_offsets; // [peer] - std::vector const& recv_counts; // [peer] + std::vector const& recv_offsets; // [peer] + std::vector const& recv_counts; // [peer] }; halo_axis_t var_halo_axis() { diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index db5c91a1ff..be6dbcbbbd 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2297,9 +2297,10 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte if (is_distributed_master()) { // SpMV is the first operation in compute_interaction_and_movement so we can do halo before and // call it naturally we then reduce the local dot products - multi_gpu_engine->halo_exchange_cstr([](pdlp_solver_t& p) -> rmm::device_uvector& { - return p.pdhg_solver_.get_reflected_dual(); - }); + multi_gpu_engine->halo_exchange_cstr( + [](pdlp_solver_t& p) -> rmm::device_uvector& { + return p.pdhg_solver_.get_reflected_dual(); + }); for (auto& shard : multi_gpu_engine->shards) { raft::device_setter guard(shard->device_id); From dc6d9e4c02fc2818afb464512da004164002e1cb Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 12:23:06 +0200 Subject: [PATCH 221/258] small fix test_cpp_mgpu --- ci/test_cpp_multi_gpu.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/test_cpp_multi_gpu.sh b/ci/test_cpp_multi_gpu.sh index da7ef714ef..dc54637474 100755 --- a/ci/test_cpp_multi_gpu.sh +++ b/ci/test_cpp_multi_gpu.sh @@ -30,7 +30,8 @@ set -euo pipefail # container image (its tag bakes the value into the environment), so no CUDA # version needs to be pinned by the workflow. If it is unset we skip the # bootstrap and fall back to any gtests already present in the container/build -# tree — keeping the job non-breaking until the multi-GPU tests land.if [ -n "${RAPIDS_CUDA_VERSION:-}" ]; then +# tree — keeping the job non-breaking until the multi-GPU tests land. +if [ -n "${RAPIDS_CUDA_VERSION:-}" ]; then rapids-logger "Using CUDA ${RAPIDS_CUDA_VERSION} (inferred from container image)" rapids-logger "Configuring conda strict channel priority" conda config --set channel_priority strict From 1102f9b445ac8148396a3af6ed0fb53e9714980d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 12:36:59 +0200 Subject: [PATCH 222/258] added distributed_parity_good_max test --- .../linear_programming/pdlp_distributed_test.cu | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu index c39a9f1c92..d669c676c5 100644 --- a/cpp/tests/linear_programming/pdlp_distributed_test.cu +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -107,6 +107,19 @@ TEST(pdlp_class, distributed_parity_afiro) expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); } +// Maximization LP (OBJSENSE MAX in the MPS): exercises the sign-flipping paths +// through normalization / presolve / solution translation on the distributed side. +// The problem is tiny (3 vars, 1 constraint) so shard partitions are lopsided +// (one shard ends up with 0 owned constraints) — good coverage of that edge. +TEST(pdlp_class, distributed_parity_good_max) +{ + if (raft::device_setter::get_device_count() < 2) { + GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); + } + const raft::handle_t handle{}; + expect_distributed_matches_base(handle, "linear_programming/good-max.mps"); +} + TEST(pdlp_class, distributed_parity_graph40_40) { if (raft::device_setter::get_device_count() < 2) { From bd58cc706bb2a5e1edbff53eef6d555d308a2eac Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 12:52:33 +0200 Subject: [PATCH 223/258] remove support for save_best_primal_so_far in distributed_pdlp --- cpp/src/pdlp/solve.cu | 8 ++++++++ .../termination_strategy/termination_strategy.cu | 14 ++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index ba8aeaf20e..17a6336a24 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2373,6 +2373,14 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( error_type_t::ValidationError, "Distributed PDLP does not support initial primal/dual solutions or warm-start " "data."); + // save_best_primal_so_far snapshots the current-or-average iterate mid-solve via + // record_best_primal_so_far. That path reads shard-local buffers + // (pdhg_solver_.get_primal_solution() / unscaled_primal_avg_solution_) that are never + // coherently assembled on the master in distributed mode, so the resulting snapshot would + // be silently wrong. Fail loudly instead. + cuopt_expects(!settings_resolved.save_best_primal_so_far, + error_type_t::ValidationError, + "Distributed PDLP does not support save_best_primal_so_far."); // Distributed PDLP today only supports the Stable3-shaped hyper-param profile: // - initial_step_size_max_singular_value = true (matches the sigma_max seeding // driven by distributed_max_singular_value in the setup), diff --git a/cpp/src/pdlp/termination_strategy/termination_strategy.cu b/cpp/src/pdlp/termination_strategy/termination_strategy.cu index e2cacd01fd..13acee138c 100644 --- a/cpp/src/pdlp/termination_strategy/termination_strategy.cu +++ b/cpp/src/pdlp/termination_strategy/termination_strategy.cu @@ -566,14 +566,12 @@ pdlp_termination_strategy_t::fill_return_problem_solution( "Dual iterate size mismatch"); } - // In distributed PDLP, gather solutions to master here - if (!deep_copy) { - if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { - const bool is_current_live_iterate = - (&primal_iterate == ¤t_pdhg_solver.get_potential_next_primal_solution()) || - (&primal_iterate == ¤t_pdhg_solver.get_primal_solution()); - if (is_current_live_iterate) { engine->gather_potential_next_solutions_to_master(); } - } + // In distributed PDLP, gather solutions from the shards to the master. + if (auto* engine = current_pdhg_solver.get_mgpu_engine()) { + const bool is_current_live_iterate = + (&primal_iterate == ¤t_pdhg_solver.get_potential_next_primal_solution()) || + (&primal_iterate == ¤t_pdhg_solver.get_primal_solution()); + if (is_current_live_iterate) { engine->gather_potential_next_solutions_to_master(); } } typename convergence_information_t::view_t convergence_information_view = From 415b74431eaa3ac9c18aae5112fdaca2a16eeec1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 13:31:16 +0200 Subject: [PATCH 224/258] disable presolve for good max in test --- cpp/tests/linear_programming/pdlp_distributed_test.cu | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu index d669c676c5..8057e5deb3 100644 --- a/cpp/tests/linear_programming/pdlp_distributed_test.cu +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -32,9 +32,12 @@ namespace cuopt::mathematical_optimization::test { // (num_gpus = -1 => auto-detect), then assert the distributed run matches the base run on // everything meaningful: termination status, step count (within 15%), primal/dual objective, // and the full primal/dual solution vectors. All value comparisons use a loose relative tolerance. +// Pass disable_presolve=true for tiny instances that PSLP would solve entirely, which would +// otherwise bypass the distributed solver path we want to exercise. static void expect_distributed_matches_base(raft::handle_t const& handle, std::string const& mps_rel_path, - bool fixed_mps_format = false) + bool fixed_mps_format = false, + bool disable_presolve = false) { constexpr double loose_rel = 1e-3; auto near_rel = [](double a, double b, double rel) { @@ -46,6 +49,7 @@ static void expect_distributed_matches_base(raft::handle_t const& handle, pdlp_solver_settings_t base_settings{}; base_settings.method = method_t::PDLP; + if (disable_presolve) { base_settings.presolver = presolver_t::None; } auto base_op = mps_data_model_to_optimization_problem(&handle, problem); auto base = solve_lp(base_op, base_settings); @@ -117,7 +121,10 @@ TEST(pdlp_class, distributed_parity_good_max) GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); } const raft::handle_t handle{}; - expect_distributed_matches_base(handle, "linear_programming/good-max.mps"); + // Disable presolve: PSLP solves this 3-var/1-constraint problem entirely, bypassing + // the distributed solver path this test is meant to exercise. + expect_distributed_matches_base( + handle, "linear_programming/good-max.mps", /*fixed_mps_format=*/false, /*disable_presolve=*/true); } TEST(pdlp_class, distributed_parity_graph40_40) From 4091bc7ffe5b90f9ef7359d4244b78ba5eecfd1a Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 13:36:42 +0200 Subject: [PATCH 225/258] cleaned distributed_algorithms up to max_singular value --- .../distributed_algorithms.cu | 86 +++++++------------ .../distributed_pdlp/multi_gpu_engine.hpp | 11 ++- .../initial_scaling.cu | 8 +- cpp/src/pdlp/pdlp.cu | 12 +++ 4 files changed, 56 insertions(+), 61 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 01d570df41..923ca057f0 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -48,7 +48,7 @@ void multi_gpu_engine_t::gather_potential_next_solutions_to_master() // -------- Distributed bound / objective rescaling ------------------------- // compute and apply_bound_objective_rescaling_to_problem, unfused because we need a -// raw squared-sum on device to hand to NCCL AllReduce and the base version comptues +// raw squared-sum on device to reduce and the base version comptues // tranform->reduce->transform in one cub call for efficiency template void multi_gpu_engine_t::distributed_bound_objective_rescaling(f_t c_scaling_weight) @@ -61,21 +61,18 @@ void multi_gpu_engine_t::distributed_bound_objective_rescaling(f_t c_s f_t global_obj_sq = f_t(0); for_each_shard([&](auto& s) { const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); - const i_t n_owned_cstr = static_cast(s.rank_data.owned_cstr_size); - const i_t n_owned_var = static_cast(s.rank_data.owned_var_size); auto policy = rmm::exec_policy(s.stream.view()); - auto bounds_begin = thrust::make_zip_iterator(scaled.constraint_lower_bounds.data(), scaled.constraint_upper_bounds.data()); global_bound_sq += thrust::transform_reduce(policy, bounds_begin, - bounds_begin + n_owned_cstr, + bounds_begin + s.rank_data.owned_cstr_size, rhs_sum_of_squares_t{}, f_t(0), thrust::plus{}); global_obj_sq += thrust::transform_reduce(policy, scaled.objective_coefficients.data(), - scaled.objective_coefficients.data() + n_owned_var, + scaled.objective_coefficients.data() + s.rank_data.owned_var_size, weighted_square_op{c_scaling_weight}, f_t(0), thrust::plus{}); @@ -93,7 +90,21 @@ void multi_gpu_engine_t::distributed_bound_objective_rescaling(f_t c_s scaling.apply_bound_objective_rescaling_to_problem(); }); - for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + synchronize_shards(); +} + +// -------- Refresh halo of cumulative scalings ----------------------------- +// Refreshes the halo copies of the cumulative variable + constraint scalings on +// every shard. Called before and after each matrix-scaling pass in ruiz and pock-chambolle +template +void multi_gpu_engine_t::refresh_halo_cummulative_scalings() +{ + halo_exchange_var([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); + }); + halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { + return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); + }); } // -------- Distributed Ruiz inf-scaling ------------------------------------ @@ -106,14 +117,7 @@ void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int num_iter, i_ raft::common::nvtx::range scope("distributed_ruiz_inf_scaling"); for (int it = 0; it < num_iter; ++it) { - // Refresh halo copies of both cumulative scalings (owner -> halo) so the - // per-shard kernels read correct opposite-axis factors on their halo. - halo_exchange_var([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); - }); - halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); - }); + refresh_halo_cummulative_scalings(); // Shard-local Ruiz iteration // rows: inf norm only over OWNED (full) rows from A @@ -124,16 +128,10 @@ void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int num_iter, i_ [](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().ruiz_iter_local(); }); } - // Final refresh so downstream consumers (the scaled problem, the next - // distributed_max_singular_value, etc.) see correct halo factors. - halo_exchange_var([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); - }); - halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); - }); + // Final refresh after last iteration + refresh_halo_cummulative_scalings(); - for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + synchronize_shards(); } // -------- Distributed Pock-Chambolle scaling ------------------------------ @@ -146,32 +144,20 @@ void multi_gpu_engine_t::distributed_pock_chambolle_scaling(f_t alpha, if (n_global_vars <= 0) return; raft::common::nvtx::range scope("distributed_pock_chambolle_scaling"); - // Refresh halo copies of both cumulative scalings - halo_exchange_var([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); - }); - halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); - }); + refresh_halo_cummulative_scalings(); for_each_shard([alpha](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().pock_chambolle_scaling(alpha); }); // Final refresh for downstream consumers. - halo_exchange_var([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_variable_scaling(); - }); - halo_exchange_cstr([](pdlp_solver_t& p) -> auto& { - return p.get_initial_scaling_strategy().get_cummulative_constraint_matrix_scaling(); - }); + refresh_halo_cummulative_scalings(); - for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + synchronize_shards(); } // -------- Distributed scaling orchestration ------------------------------ -// Mirrors what scale_problem() does in single-GPU by composing the -// individual distributed passes. See the header for the full pipeline. +// Mirrors single GPU scaling template void multi_gpu_engine_t::distributed_scaling(pdlp_hyper_params_t const& hyper_params, i_t n_global_vars, @@ -179,16 +165,7 @@ void multi_gpu_engine_t::distributed_scaling(pdlp_hyper_params_t const { raft::common::nvtx::range scope("distributed_scaling"); - // 1) Grow per-shard iteration_* scratch back to full size (the shard ctor - // released it after its no-op local pre-scaling pass) - for_each_shard([](auto& shard) { - auto& scaling = shard.sub_pdlp->get_initial_scaling_strategy(); - auto& op = shard.sub_pdlp->get_op_problem_scaled(); - scaling.get_iteration_variable_scaling().resize(op.n_variables, shard.stream.view()); - scaling.get_iteration_constraint_matrix_scaling().resize(op.n_constraints, shard.stream.view()); - }); - - // 2) Matrix scaling passes populate the cumulative row/col scalings on + // 1) Matrix scaling passes populate the cumulative row/col scalings on // every shard. Each pass keeps the halo copies refreshed internally. if (hyper_params.do_ruiz_scaling) { distributed_ruiz_inf_scaling(hyper_params.default_l_inf_ruiz_iterations, n_global_vars); @@ -198,15 +175,15 @@ void multi_gpu_engine_t::distributed_scaling(pdlp_hyper_params_t const static_cast(hyper_params.default_alpha_pock_chambolle_rescaling), n_global_vars); } - // 3) Per-shard apply of the accumulated scaling to A, c, variable and + // 2) Per-shard apply of the accumulated scaling to A, c, variable and // constraint bounds. This is scale_problem() minus its local - // bound/objective rescaling; the equivalent global step happens in (4). + // bound/objective rescaling; the equivalent global step happens in (3). for_each_shard([](auto& shard) { shard.sub_pdlp->get_initial_scaling_strategy().apply_cummulative_scaling_to_problem(); }); - for_each_shard([](auto& shard) { shard.stream.synchronize(); }); + synchronize_shards(); - // 4) Global bound/objective rescaling (all shards get the identical scalar). + // 3) Global bound/objective rescaling (all shards get the identical scalar). if (hyper_params.bound_objective_rescaling) { distributed_bound_objective_rescaling( static_cast(hyper_params.initial_primal_weight_c_scaling)); @@ -489,6 +466,7 @@ void multi_gpu_engine_t::distributed_compute_initial_primal_weight( // explicit-instantiate the out-of-line members defined in this TU. #define INSTANTIATE(F_TYPE) \ template void multi_gpu_engine_t::gather_potential_next_solutions_to_master(); \ + template void multi_gpu_engine_t::refresh_halo_cummulative_scalings(); \ template void multi_gpu_engine_t::distributed_bound_objective_rescaling(F_TYPE); \ template void multi_gpu_engine_t::distributed_ruiz_inf_scaling(int, int); \ template void multi_gpu_engine_t::distributed_pock_chambolle_scaling(F_TYPE, int); \ diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index a1fe72f96a..65222718bc 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -88,6 +88,12 @@ struct multi_gpu_engine_t { } } + // Host-blocking barrier: waits until every shard stream has drained. + void synchronize_shards() + { + for_each_shard([](auto& s) { s.stream.synchronize(); }); + } + // Core: launches cub::DeviceTransform on every shard using per-shard // pre-resolved inputs / outputs / sizes. // - in_tuples[r] is the tuple passed as cub input for shard r (any @@ -525,6 +531,10 @@ struct multi_gpu_engine_t { void distributed_compute_At_y(); // -------- High-level algorithms (defined in distributed_algorithms.cu) --- + // Refreshes the halo copies of the cumulative variable + constraint scalings on + // every shard. Used by the matrix-scaling passes (Ruiz, Pock-Chambolle) + void refresh_halo_cummulative_scalings(); + // Global bound/objective rescaling: allreduce the owned partial squared norms // of the constraint bounds and (weighted) objective, then apply the identical // scalar on every shard. @@ -541,7 +551,6 @@ struct multi_gpu_engine_t { // Full distributed scaling entry point. Mirrors what scale_problem() does in // single-GPU by orchestrating: - // - reset per-shard scaling state // - Ruiz inf-scaling -> populates cumulative row/col scalings // - Pock-Chambolle scaling -> same // - per-shard apply_cummulative_scaling_to_problem() to apply the cumulative diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 468138af30..9daae756cc 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -120,9 +120,6 @@ pdlp_initial_scaling_strategy_t::pdlp_initial_scaling_strategy_t( f_t(1)); compute_scaling_vectors(number_of_ruiz_iterations, alpha); - - iteration_constraint_matrix_scaling_.resize(0, stream_view_); - iteration_variable_scaling_.resize(0, stream_view_); } template @@ -176,8 +173,7 @@ void pdlp_initial_scaling_strategy_t::bound_objective_rescaling() h_objective_rescaling_ = cuopt::host_copy(objective_rescaling_, stream_view_); } -// Row inf-norm of the scaled matrix, over the row-major matrix: each row is -// reduced from its own nonzeros. (Owns the complete row in distributed PDLP.) +// Row inf-norm of the scaled matrix, over the row-major matrix template __global__ void inf_norm_row_kernel( const typename mip::problem_t::view_t op_problem, @@ -200,7 +196,7 @@ __global__ void inf_norm_row_kernel( } } -// Column inf-norm of the scaled matrix +// Column inf-norm of the scaled matrix, over the column major matrix template __global__ void inf_norm_col_kernel( const typename mip::problem_t::view_t op_problem, diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index be6dbcbbbd..b58ee9e859 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3301,11 +3301,23 @@ void pdlp_solver_t::take_step([[maybe_unused]] i_t total_pdlp_iteratio template void pdlp_solver_t::scale_problem() { + // Scale problem then free scratch buffers raft::common::nvtx::range fun_scope("pdlp_solver_t::scale_problem"); if (is_distributed_master()) { multi_gpu_engine->distributed_scaling(settings_.hyper_params, primal_size_h_, inside_mip_); + + // Free per-shard scratch: no further scaling passes happen after this point. + multi_gpu_engine->for_each_shard([](auto& shard) { + auto& scaling = shard.sub_pdlp->get_initial_scaling_strategy(); + scaling.get_iteration_variable_scaling().resize(0, shard.stream.view()); + scaling.get_iteration_constraint_matrix_scaling().resize(0, shard.stream.view()); + }); } else { initial_scaling_strategy_.scale_problem(); + + // Free scratch: no further scaling passes happen after this point. + initial_scaling_strategy_.get_iteration_variable_scaling().resize(0, stream_view_); + initial_scaling_strategy_.get_iteration_constraint_matrix_scaling().resize(0, stream_view_); } } From 1f871429c5ba83c38d65e141fd9818446a3ec5b2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 16:49:25 +0200 Subject: [PATCH 226/258] better max_singular value --- .../distributed_algorithms.cu | 138 +++++++----------- .../distributed_pdlp/multi_gpu_engine.hpp | 64 +++++++- cpp/src/pdlp/pdlp.cu | 8 +- cpp/src/pdlp/utils.cuh | 17 +++ 4 files changed, 135 insertions(+), 92 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 923ca057f0..067f1eb65f 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -12,15 +12,16 @@ #include +#include #include #include #include +#include #include #include #include -#include #include namespace cuopt::mathematical_optimization::pdlp { @@ -205,94 +206,71 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs const int nb = static_cast(shards.size()); // Generate the GLOBAL z[] sequence in cstr-index order from a fresh - // mt19937(1), once per call. It's m doubles regardless of N (cheap). - // Each shard then keeps only z[global_idx_for_owned_local_i]. - std::vector h_global_z(static_cast(n_global_cstrs)); - { - std::mt19937 gen(1); - std::normal_distribution dist(f_t(0.0), f_t(1.0)); - for (i_t i = 0; i < n_global_cstrs; ++i) { - h_global_z[i] = dist(gen); - } - } + // Global z[] in cstr-index order — each shard keeps only + // z[global_idx_for_owned_local_i]. Shared with the single-GPU + // compute_initial_step_size path so both produce bit-identical seeds. + std::vector h_global_z = + make_singular_value_probe(static_cast(n_global_cstrs)); - // Per-shard scratch lives on each shard's device.] + // Per-shard scratch lives on each shard's device. std::vector> q; std::vector> z; std::vector> atq; - std::vector> sigma_sq; - std::vector> norm_q; - std::vector> residual_norm; - // RAII descriptors: created below per shard, destroyed automatically when - // the vectors go out of scope (no manual cusparseDestroyDnVec needed). + std::vector> sigma_sq; + std::vector> norm_q; + std::vector> residual_norm; + std::vector> z_dn(nb); std::vector> atq_dn(nb); - q.reserve(nb); - z.reserve(nb); - atq.reserve(nb); - sigma_sq.reserve(nb); - norm_q.reserve(nb); - residual_norm.reserve(nb); - - // Scatter z according to partition - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + + // Per-shard owned-slice spans consumed by the engine's *_bufs helpers. + // (Scalar outputs are passed as rmm::device_scalar directly via the + // scalar-owner overloads on distributed_{l2_norm,dot}_bufs.) + std::vector> q_owned, z_owned; + for (auto* v : {&q, &z, &atq}) v->reserve(nb); + for (auto* v : {&sigma_sq, &norm_q, &residual_norm}) v->reserve(nb); + for (auto* v : {&q_owned, &z_owned}) v->reserve(nb); + + // Allocate per-shard scratch, scatter z according to partition, and build + // the *_bufs views for the power iteration below. + for_each_shard([&](auto& s, int r) { const i_t cstr_total = s.rank_data.total_cstr_size; const i_t var_total = s.rank_data.total_var_size; + const i_t n_owned = s.rank_data.owned_cstr_size; + q.emplace_back(static_cast(cstr_total), s.stream.view()); z.emplace_back(static_cast(cstr_total), s.stream.view()); atq.emplace_back(static_cast(var_total), s.stream.view()); - sigma_sq.emplace_back(std::size_t{1}, s.stream.view()); - norm_q.emplace_back(std::size_t{1}, s.stream.view()); - residual_norm.emplace_back(std::size_t{1}, s.stream.view()); + sigma_sq.emplace_back(s.stream.view()); + norm_q.emplace_back(s.stream.view()); + residual_norm.emplace_back(s.stream.view()); z_dn[r].create(static_cast(cstr_total), z.back().data()); atq_dn[r].create(static_cast(var_total), atq.back().data()); - std::vector h_owned_z(static_cast(s.rank_data.owned_cstr_size)); - for (i_t i = 0; i < s.rank_data.owned_cstr_size; ++i) { - const i_t g = s.rank_data.local_to_global_cstr[i]; - h_owned_z[i] = h_global_z[g]; - } - if (s.rank_data.owned_cstr_size > 0) { - raft::copy(z.back().data(), - h_owned_z.data(), - static_cast(s.rank_data.owned_cstr_size), - s.stream.view()); + q_owned.emplace_back(q.back().data(), static_cast(n_owned)); + z_owned.emplace_back(z.back().data(), static_cast(n_owned)); + + // Scatter z according to partition + std::vector h_owned_z(static_cast(n_owned)); + thrust::gather(thrust::host, + s.rank_data.local_to_global_cstr.begin(), + s.rank_data.local_to_global_cstr.begin() + n_owned, + h_global_z.begin(), + h_owned_z.begin()); + if (n_owned > 0) { + raft::copy( + z.back().data(), h_owned_z.data(), static_cast(n_owned), s.stream.view()); } - if (cstr_total > s.rank_data.owned_cstr_size) { + if (cstr_total > n_owned) { thrust::fill(rmm::exec_policy_nosync(s.stream.view()), - z.back().data() + s.rank_data.owned_cstr_size, + z.back().data() + n_owned, z.back().data() + cstr_total, f_t(0)); } // Sync to ensure h_owned_z stays valid through the H2D copy (it goes // out of scope at end of this iteration of the per-shard loop). s.stream.synchronize(); - } - - // Build the per-shard views (spans) used by the engine's *_bufs helpers. - std::vector> q_full, atq_full; - std::vector> q_owned, z_owned; - std::vector> norm_q_scalar, sigma_sq_scalar, residual_scalar; - q_full.reserve(nb); - atq_full.reserve(nb); - q_owned.reserve(nb); - z_owned.reserve(nb); - norm_q_scalar.reserve(nb); - sigma_sq_scalar.reserve(nb); - residual_scalar.reserve(nb); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - const std::size_t owned = static_cast(s.rank_data.owned_cstr_size); - q_full.emplace_back(q[r].data(), q[r].size()); - atq_full.emplace_back(atq[r].data(), atq[r].size()); - q_owned.emplace_back(q[r].data(), owned); - z_owned.emplace_back(z[r].data(), owned); - norm_q_scalar.emplace_back(raft::make_device_scalar_view(norm_q[r].data())); - sigma_sq_scalar.emplace_back(raft::make_device_scalar_view(sigma_sq[r].data())); - residual_scalar.emplace_back(raft::make_device_scalar_view(residual_norm[r].data())); - } + }); // ===== Power iteration ===== // Mirrors single-GPU compute_initial_step_size. All cross-shard math and @@ -302,22 +280,18 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs // per-shard SpMVs that call pdhg_solver_'s A_into / A_T_into. for (int it = 0; it < max_iterations; ++it) { // q := z on the owned slice (the carried iterate). - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { const i_t n_owned = s.rank_data.owned_cstr_size; raft::copy(q[r].data(), z[r].data(), n_owned, s.stream.view()); - } + }); // ||q||₂ over the global OWNED cstr slice (one allreduce-sum + sqrt). - distributed_l2_norm_bufs(q_owned, norm_q_scalar); + distributed_l2_norm_bufs(q_owned, norm_q); // q /= ||q||₂ on owned slice (halo gets refreshed by next exchange). - // Kept inline: the divisor differs per shard (each shard reads its own - // norm_q[r]) so a single shared functor won't do. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + // Kept as per-shard cub launch: the divisor is a per-shard scalar + // captured in the device lambda, so a single shared functor won't do. + for_each_shard([&](auto& s, int r) { const i_t n_owned = s.rank_data.owned_cstr_size; cub::DeviceTransform::Transform( q[r].data(), @@ -325,10 +299,10 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs n_owned, [n = norm_q[r].data()] __device__(f_t v) { return v / *n; }, s.stream.view().value()); - } + }); // atq = A^T q : refresh halo of q, then per-shard SpMV. - halo_exchange_cstr_bufs(q_full); + halo_exchange_cstr_bufs(q); for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); @@ -336,7 +310,7 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs } // z = A atq : refresh halo of atq, then per-shard SpMV. - halo_exchange_var_bufs(atq_full); + halo_exchange_var_bufs(atq); for (int r = 0; r < nb; ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); @@ -345,7 +319,7 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs // σ² = q · z over the global OWNED cstr slice (= q^T A A^T q = σ_max² // when q is the dominant left-singular vector). - distributed_dot_bufs(q_owned, z_owned, sigma_sq_scalar); + distributed_dot_bufs(q_owned, z_owned, sigma_sq); // q := -σ² q + z (owned slice) — residual of the eigen-equation. // Kept inline for the same per-shard-scalar reason as normalize above. @@ -362,7 +336,7 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs } // Convergence check via global residual norm. - distributed_l2_norm_bufs(q_owned, residual_scalar); + distributed_l2_norm_bufs(q_owned, residual_norm); auto& s0 = *shards[0]; raft::device_setter guard0(s0.device_id); f_t h_res{}; diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 65222718bc..6264995aab 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -79,12 +80,22 @@ struct multi_gpu_engine_t { multi_gpu_engine_t(const multi_gpu_engine_t&) = delete; multi_gpu_engine_t& operator=(const multi_gpu_engine_t&) = delete; + // Invokes `fn` on every shard with the shard's device pre-set. `fn` may be + // (pdlp_shard_t&) or + // (pdlp_shard_t&, int r) — the second overload also gets the shard's rank. template void for_each_shard(Fn&& fn) { - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - fn(*s); + for (int r = 0; r < static_cast(shards.size()); ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + if constexpr (std::is_invocable_v&, int>) { + fn(s, r); + } else if constexpr (std::is_invocable_v&>) { + fn(s); + } else { + cuopt_expects(false, error_type_t::RuntimeError, "for_each_shard: invalid function signature"); + } } } @@ -244,6 +255,19 @@ struct multi_gpu_engine_t { halo_exchange_bufs_impl(bufs, axes); } + // Overload: accept the owning device_uvector directly (rmm doesn't provide + // an implicit conversion to raft::device_span, and std::vector doesn't + // convert to std::vector, so we adapt element-wise here). Non-const & + // because halo_exchange_bufs_impl writes the halo tail via ncclRecv, which + // requires a mutable pointer. + void halo_exchange_var_bufs(std::vector>& bufs) + { + std::vector> spans; + spans.reserve(bufs.size()); + for (auto& b : bufs) spans.emplace_back(b.data(), b.size()); + halo_exchange_var_bufs(spans); + } + // Wrapper: pdlp_solver_t accessor. Resolves one uvector per shard into a // vector of spans, then delegates to halo_exchange_var_bufs. // buf_access : pdlp_solver_t& -> rmm::device_uvector& @@ -269,6 +293,15 @@ struct multi_gpu_engine_t { halo_exchange_bufs_impl(bufs, axes); } + // Overload: same rationale as halo_exchange_var_bufs above. + void halo_exchange_cstr_bufs(std::vector>& bufs) + { + std::vector> spans; + spans.reserve(bufs.size()); + for (auto& b : bufs) spans.emplace_back(b.data(), b.size()); + halo_exchange_cstr_bufs(spans); + } + // Wrapper: pdlp_solver_t accessor. Resolves one uvector per shard into a // vector of spans, then delegates to halo_exchange_cstr_bufs. // buf_access : pdlp_solver_t& -> rmm::device_uvector& @@ -484,6 +517,20 @@ struct multi_gpu_engine_t { allreduce_sum_inplace_bufs(out_scalars); } + // Overload: accept owning device_scalar outputs directly. Wraps each into a + // scalar_view and delegates to the span-based core. Convenience for the typical + // case where per-shard outputs are rmm::device_scalar. + void distributed_dot_bufs(std::vector> const& a_bufs, + std::vector> const& b_bufs, + std::vector>& out_scalars) + { + std::vector> views; + views.reserve(out_scalars.size()); + for (auto& s : out_scalars) + views.emplace_back(raft::make_device_scalar_view(s.data())); + distributed_dot_bufs(a_bufs, b_bufs, views); + } + // Core L2 norm: writes sqrt(sum_r ||in_bufs[r]||_2^2) into every // *out_scalars[r].data_handle(). Delegates to distributed_dot_bufs(in, in, // out) then does a per-shard in-place sqrt on the resulting scalar. @@ -502,6 +549,17 @@ struct multi_gpu_engine_t { } } + // Overload: same rationale as distributed_dot_bufs above. + void distributed_l2_norm_bufs(std::vector> const& in_bufs, + std::vector>& out_scalars) + { + std::vector> views; + views.reserve(out_scalars.size()); + for (auto& s : out_scalars) + views.emplace_back(raft::make_device_scalar_view(s.data())); + distributed_l2_norm_bufs(in_bufs, views); + } + // Wrapper: accessor form. Resolves per-shard input / output / owned-length // then delegates to distributed_l2_norm_bufs. // BufAccess : pdlp_solver_t& -> rmm::device_uvector& diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index b58ee9e859..fb7fe21b1b 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3399,17 +3399,11 @@ void pdlp_solver_t::compute_initial_step_size() i_t m = op_problem_scaled_.n_constraints; i_t n = op_problem_scaled_.n_variables; - std::vector z(m); + std::vector z = make_singular_value_probe(static_cast(m)); rmm::device_uvector d_z(m, stream_view_); rmm::device_uvector d_q(m, stream_view_); rmm::device_uvector d_atq(n, stream_view_); - std::mt19937 gen(1); - std::normal_distribution dist(f_t(0.0), f_t(1.0)); - - for (int i = 0; i < m; ++i) - z[i] = dist(gen); - device_copy(d_z, z, stream_view_); rmm::device_scalar norm_q(stream_view_); diff --git a/cpp/src/pdlp/utils.cuh b/cpp/src/pdlp/utils.cuh index 2f5fa89edc..8bb87dcc47 100644 --- a/cpp/src/pdlp/utils.cuh +++ b/cpp/src/pdlp/utils.cuh @@ -12,6 +12,9 @@ #include #include +#include +#include + #include #include #include @@ -32,6 +35,20 @@ namespace cuopt::mathematical_optimization::pdlp { +// Host-side probe vector z ~ Normal(0, 1) that seeds the power iteration +// for sigma_max(A) in compute_initial_step_size (single-GPU) and +// distributed_max_singular_value. Fixed seed (1) so single-GPU and distributed +// runs on the same problem produce bit-identical initial iterates. +template +inline std::vector make_singular_value_probe(std::size_t size) +{ + std::vector out(size); + std::mt19937 gen(1); + std::normal_distribution dist(f_t(0.0), f_t(1.0)); + for (std::size_t i = 0; i < size; ++i) out[i] = dist(gen); + return out; +} + template DI f_t deterministic_block_reduce(raft::device_span shared, f_t val) { From 1ab99f03701785f8e086ffcca9986bbddc1c3162 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 17:32:44 +0200 Subject: [PATCH 227/258] change sig of spmv_a/t_into --- .../distributed_algorithms.cu | 106 +++++++----------- .../distributed_pdlp/multi_gpu_engine.hpp | 27 +++++ cpp/src/pdlp/pdhg.cu | 23 ++-- cpp/src/pdlp/pdhg.hpp | 10 +- cpp/src/pdlp/pdlp.cu | 25 ++--- cpp/src/pdlp/utils.cuh | 18 +++ 6 files changed, 111 insertions(+), 98 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 067f1eb65f..95c2dd8351 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -203,12 +203,13 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs { raft::common::nvtx::range scope("distributed_max_singular_value"); + // ┌──────────────────────────────────────────────────────────────┐ + // │ Setup │ + // └──────────────────────────────────────────────────────────────┘ + const int nb = static_cast(shards.size()); - - // Generate the GLOBAL z[] sequence in cstr-index order from a fresh - // Global z[] in cstr-index order — each shard keeps only - // z[global_idx_for_owned_local_i]. Shared with the single-GPU - // compute_initial_step_size path so both produce bit-identical seeds. + // Generate the GLOBAL z[] sequence in cstr-index order. + // Scatter it to the shards according to the partition. std::vector h_global_z = make_singular_value_probe(static_cast(n_global_cstrs)); @@ -220,12 +221,11 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs std::vector> norm_q; std::vector> residual_norm; + std::vector> q_dn(nb); std::vector> z_dn(nb); std::vector> atq_dn(nb); // Per-shard owned-slice spans consumed by the engine's *_bufs helpers. - // (Scalar outputs are passed as rmm::device_scalar directly via the - // scalar-owner overloads on distributed_{l2_norm,dot}_bufs.) std::vector> q_owned, z_owned; for (auto* v : {&q, &z, &atq}) v->reserve(nb); for (auto* v : {&sigma_sq, &norm_q, &residual_norm}) v->reserve(nb); @@ -244,6 +244,7 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs sigma_sq.emplace_back(s.stream.view()); norm_q.emplace_back(s.stream.view()); residual_norm.emplace_back(s.stream.view()); + q_dn[r].create(static_cast(cstr_total), q.back().data()); z_dn[r].create(static_cast(cstr_total), z.back().data()); atq_dn[r].create(static_cast(var_total), atq.back().data()); @@ -257,27 +258,24 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs s.rank_data.local_to_global_cstr.begin() + n_owned, h_global_z.begin(), h_owned_z.begin()); - if (n_owned > 0) { - raft::copy( - z.back().data(), h_owned_z.data(), static_cast(n_owned), s.stream.view()); - } - if (cstr_total > n_owned) { - thrust::fill(rmm::exec_policy_nosync(s.stream.view()), - z.back().data() + n_owned, - z.back().data() + cstr_total, - f_t(0)); - } + raft::copy( + z.back().data(), h_owned_z.data(), static_cast(n_owned), s.stream.view()); + thrust::fill(rmm::exec_policy_nosync(s.stream.view()), + z.back().data() + n_owned, + z.back().data() + cstr_total, + f_t(0)); + // Sync to ensure h_owned_z stays valid through the H2D copy (it goes // out of scope at end of this iteration of the per-shard loop). s.stream.synchronize(); }); - // ===== Power iteration ===== - // Mirrors single-GPU compute_initial_step_size. All cross-shard math and - // NCCL comms are delegated to multi_gpu_engine_t's *_bufs helpers; the - // only inline work is (a) the two elementwise transforms whose functor - // captures each shard's own scalar (norm_q[r], sigma_sq[r]) and (b) the - // per-shard SpMVs that call pdhg_solver_'s A_into / A_T_into. + // ┌──────────────────────────────────────────────────────────────┐ + // │ Power iteration │ + // └──────────────────────────────────────────────────────────────┘ + + // Mirrors single-GPU compute_initial_step_size. + // copy -> l2 norm -> transform -> SpMV -> SpMV -> dot -> transform -> norm -> convergence check for (int it = 0; it < max_iterations; ++it) { // q := z on the owned slice (the carried iterate). for_each_shard([&](auto& s, int r) { @@ -289,51 +287,35 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs distributed_l2_norm_bufs(q_owned, norm_q); // q /= ||q||₂ on owned slice (halo gets refreshed by next exchange). - // Kept as per-shard cub launch: the divisor is a per-shard scalar - // captured in the device lambda, so a single shared functor won't do. + // Kept as per-shard cub launch: the divisor is a per-shard scalar. for_each_shard([&](auto& s, int r) { const i_t n_owned = s.rank_data.owned_cstr_size; - cub::DeviceTransform::Transform( - q[r].data(), - q[r].data(), - n_owned, - [n = norm_q[r].data()] __device__(f_t v) { return v / *n; }, - s.stream.view().value()); + cub::DeviceTransform::Transform(q[r].data(), + q[r].data(), + n_owned, + divide_by_device_scalar_t{norm_q[r].data()}, + s.stream.view().value()); }); - // atq = A^T q : refresh halo of q, then per-shard SpMV. - halo_exchange_cstr_bufs(q); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - s.sub_pdlp->pdhg_solver_.spmv_At_into(q[r], atq_dn[r]); - } - - // z = A atq : refresh halo of atq, then per-shard SpMV. - halo_exchange_var_bufs(atq); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - s.sub_pdlp->pdhg_solver_.spmv_A_into(atq[r], z_dn[r]); - } + // atq = A^T q (fused halo-refresh of q + per-shard local SpMV). + distributed_spmv_At(q, q_dn, atq_dn); + + // z = A atq (fused halo-refresh of atq + per-shard local SpMV). + distributed_spmv_A(atq, atq_dn, z_dn); // σ² = q · z over the global OWNED cstr slice (= q^T A A^T q = σ_max² // when q is the dominant left-singular vector). distributed_dot_bufs(q_owned, z_owned, sigma_sq); // q := -σ² q + z (owned slice) — residual of the eigen-equation. - // Kept inline for the same per-shard-scalar reason as normalize above. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { const i_t n_owned = s.rank_data.owned_cstr_size; - cub::DeviceTransform::Transform( - cuda::std::make_tuple(q[r].data(), z[r].data()), - q[r].data(), - n_owned, - [s2 = sigma_sq[r].data()] __device__(f_t qv, f_t zv) { return -(*s2) * qv + zv; }, - s.stream.view().value()); - } + cub::DeviceTransform::Transform(cuda::std::make_tuple(q[r].data(), z[r].data()), + q[r].data(), + n_owned, + residual_fma_neg_scalar_t{sigma_sq[r].data()}, + s.stream.view().value()); + }); // Convergence check via global residual norm. distributed_l2_norm_bufs(q_owned, residual_norm); @@ -352,8 +334,7 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs raft::copy(&sigma_sq_h, sigma_sq[0].data(), 1, s0.stream.view()); s0.stream.synchronize(); - // z_dn / atq_dn descriptors are released by their RAII wrappers on return. - return std::sqrt(std::max(sigma_sq_h, f_t(0))); + return sigma_sq_h; } // -------- Distributed initial step size --------------------------------- @@ -379,13 +360,13 @@ void multi_gpu_engine_t::distributed_compute_initial_step_size( "of A fallback is single-GPU only. This should have been rejected " "earlier in solve_lp_distributed_from_mps."); - const f_t sigma_max = distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); + const f_t sigma_max_sq = distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); auto& master = *master_pdlp_; auto* handle_ptr = master.get_handle_ptr(); auto stream_view = handle_ptr->get_stream(); - const f_t h_step_size = (sigma_max > f_t{0}) ? scaling_factor / sigma_max : f_t{1}; + const f_t h_step_size = scaling_factor / std::sqrt(sigma_max_sq.value(stream_view_)); raft::copy(master.get_step_size().data(), &h_step_size, 1, stream_view); for_each_shard([&](auto& shard) { @@ -397,8 +378,7 @@ void multi_gpu_engine_t::distributed_compute_initial_step_size( // -------- Distributed initial primal weight ------------------------------ // Distributed PDLP is currently restricted to the Stable3-shaped hyper-param -// profile (validated up front in solve_lp_distributed_from_mps, and defended -// again here). In that regime, single-GPU compute_initial_primal_weight +// profile. Single-GPU compute_initial_primal_weight // short-circuits to primal_weight = 1 without touching the norms (see // pdlp.cu: // !initial_primal_weight_combined_bounds && bound_objective_rescaling diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 6264995aab..79d87b0426 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -588,6 +588,33 @@ struct multi_gpu_engine_t { void distributed_compute_A_x(); void distributed_compute_At_y(); + // Distributed A^T @ in on caller-owned scratch. Refreshes the halo of `in_bufs` + // (cstr axis, since the input is cstr-shaped), then dispatches each shard's + // local spmv_At_into that reads from in_descs[r] and writes into out_descs[r]. + void distributed_spmv_At(std::vector>& in_bufs, + std::vector>& in_descs, + std::vector>& out_descs) + { + halo_exchange_cstr_bufs(in_bufs); + for_each_shard([&](auto& s, int r) { + s.sub_pdlp->pdhg_solver_.spmv_At_into(in_descs[r], out_descs[r]); + }); + } + + // Distributed A @ in on caller-owned scratch. Refreshes the halo of `in_bufs` + // (var axis, since the input is var-shaped), then dispatches each shard's + // local spmv_A_into. Caller owns / sizes the descriptor vectors as above + // (in_descs to var_total, out_descs to cstr_total). + void distributed_spmv_A(std::vector>& in_bufs, + std::vector>& in_descs, + std::vector>& out_descs) + { + halo_exchange_var_bufs(in_bufs); + for_each_shard([&](auto& s, int r) { + s.sub_pdlp->pdhg_solver_.spmv_A_into(in_descs[r], out_descs[r]); + }); + } + // -------- High-level algorithms (defined in distributed_algorithms.cu) --- // Refreshes the halo copies of the cumulative variable + constraint scalings on // every shard. Used by the matrix-scaling passes (Ruiz, Pock-Chambolle) diff --git a/cpp/src/pdlp/pdhg.cu b/cpp/src/pdlp/pdhg.cu index f2a6133aa6..965d6d737f 100644 --- a/cpp/src/pdlp/pdhg.cu +++ b/cpp/src/pdlp/pdhg.cu @@ -618,22 +618,18 @@ void pdhg_solver_t::compute_A_x() } } -// out_desc = A^T @ in_buf, on this shard's local matrix. in_buf is an arbitrary -// caller-owned (constraint/dual-shaped) buffer; we wrap it in a throwaway dense -// descriptor instead of hijacking a canonical solution descriptor, so no shared -// cusparse_view_ state is mutated. Used by the distributed max-singular-value -// power iteration to SpMV scratch buffers. +// out_desc = A^T @ in_desc, on this shard's local matrix. Both descriptors are +// caller-owned and can point at arbitrary scratch buffers. Used by +// multi_gpu_engine_t::distributed_spmv_At. template -void pdhg_solver_t::spmv_At_into(rmm::device_uvector& in_buf, +void pdhg_solver_t::spmv_At_into(cusparseDnVecDescr_t in_desc, cusparseDnVecDescr_t out_desc) { - cusparse_dn_vec_descr_wrapper_t in_vec; - in_vec.create(in_buf.size(), in_buf.data()); RAFT_CUSPARSE_TRY(raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), CUSPARSE_OPERATION_NON_TRANSPOSE, reusable_device_scalar_value_1_.data(), cusparse_view_.A_T, - in_vec, + in_desc, reusable_device_scalar_value_0_.data(), out_desc, CUSPARSE_SPMV_CSR_ALG2, @@ -641,20 +637,17 @@ void pdhg_solver_t::spmv_At_into(rmm::device_uvector& in_buf, stream_view_)); } -// out_desc = A @ in_buf, the spmv_A_into counterpart of spmv_At_into: wraps the -// arbitrary (variable/primal-shaped) in_buf in a throwaway descriptor. +// out_desc = A @ in_desc, the counterpart of spmv_At_into on this shard's local A. template -void pdhg_solver_t::spmv_A_into(rmm::device_uvector& in_buf, +void pdhg_solver_t::spmv_A_into(cusparseDnVecDescr_t in_desc, cusparseDnVecDescr_t out_desc) { - cusparse_dn_vec_descr_wrapper_t in_vec; - in_vec.create(in_buf.size(), in_buf.data()); RAFT_CUSPARSE_TRY( raft::sparse::detail::cusparsespmv(handle_ptr_->get_cusparse_handle(), CUSPARSE_OPERATION_NON_TRANSPOSE, reusable_device_scalar_value_1_.data(), cusparse_view_.A, - in_vec, + in_desc, reusable_device_scalar_value_0_.data(), out_desc, CUSPARSE_SPMV_CSR_ALG2, diff --git a/cpp/src/pdlp/pdhg.hpp b/cpp/src/pdlp/pdhg.hpp index ed61fe28ee..83f3b5bde4 100644 --- a/cpp/src/pdlp/pdhg.hpp +++ b/cpp/src/pdlp/pdhg.hpp @@ -93,12 +93,10 @@ class pdhg_solver_t { void spmvop_At_y(); void spmvop_A_x(); - // Parameterized SpMVs used by the multi-GPU engine. - // Both temporarily hijack a canonical input descriptor in cusparse_view_,run the local SpMV into - // `out_desc`, then restore the descriptor to its original buffer so other code on this shard is - // unaffected. No multi-GPU dispatch inside — the engine is the orchestrator. - void spmv_At_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); - void spmv_A_into(rmm::device_uvector& in_buf, cusparseDnVecDescr_t out_desc); + // Parameterized SpMVs used by the multi-GPU engine. Thin wrappers around + // cusparsespmv on this shard's local A / A^T + void spmv_At_into(cusparseDnVecDescr_t in_desc, cusparseDnVecDescr_t out_desc); + void spmv_A_into(cusparseDnVecDescr_t in_desc, cusparseDnVecDescr_t out_desc); // Pure cub-transform extractions. Allows for clearer containment of the calls and ensures // the single-GPU vs distributed-GPU uses the same calls diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index fb7fe21b1b..2d3e3cef0b 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3433,12 +3433,11 @@ void pdlp_solver_t::compute_initial_step_size() cuopt_assert(norm_q.value(stream_view_) != f_t(0), "norm q can't be 0"); // d_q *= 1 / norm_q - cub::DeviceTransform::Transform( - d_q.data(), - d_q.data(), - d_q.size(), - [norm_q = norm_q.data()] __device__(f_t d_q) { return d_q / *norm_q; }, - stream_view_.value()); + cub::DeviceTransform::Transform(d_q.data(), + d_q.data(), + d_q.size(), + divide_by_device_scalar_t{norm_q.data()}, + stream_view_.value()); // A_t_q = A_t @ d_q RAFT_CUSPARSE_TRY( @@ -3475,14 +3474,12 @@ void pdlp_solver_t::compute_initial_step_size() sigma_max_sq.data(), stream_view_.value())); - cub::DeviceTransform::Transform( - cuda::std::make_tuple(d_q.data(), d_z.data()), - d_q.data(), - d_q.size(), - [sigma_max_sq = sigma_max_sq.data()] __device__(f_t d_q, f_t d_z) { - return d_q * -(*sigma_max_sq) + d_z; - }, - stream_view_.value()); + // d_q := -sigma_max_sq * d_q + d_z + cub::DeviceTransform::Transform(cuda::std::make_tuple(d_q.data(), d_z.data()), + d_q.data(), + d_q.size(), + residual_fma_neg_scalar_t{sigma_max_sq.data()}, + stream_view_.value()); my_l2_norm(d_q, residual_norm, handle_ptr_); diff --git a/cpp/src/pdlp/utils.cuh b/cpp/src/pdlp/utils.cuh index 8bb87dcc47..f404a45e6a 100644 --- a/cpp/src/pdlp/utils.cuh +++ b/cpp/src/pdlp/utils.cuh @@ -49,6 +49,24 @@ inline std::vector make_singular_value_probe(std::size_t size) return out; } +// Elementwise: v := v / *scalar. Named struct (rather than an extended HD +// lambda) so it can be used as a cub::DeviceTransform op inside templates whose +// template arguments are themselves local lambda types (which nvcc rejects). +template +struct divide_by_device_scalar_t { + f_t const* scalar; + __host__ __device__ f_t operator()(f_t v) const { return v / *scalar; } +}; + +// Elementwise: q := -*scalar * q + z. Used in the power-iteration residual +// update (single-GPU compute_initial_step_size and distributed +// distributed_max_singular_value). Same named-struct rationale as above. +template +struct residual_fma_neg_scalar_t { + f_t const* scalar; + __host__ __device__ f_t operator()(f_t q, f_t z) const { return -(*scalar) * q + z; } +}; + template DI f_t deterministic_block_reduce(raft::device_span shared, f_t val) { From 88273f4c6662e9e3f5527b1d9986078d501e09ea Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 17:33:04 +0200 Subject: [PATCH 228/258] style --- .../distributed_algorithms.cu | 39 +++++++++++-------- .../distributed_pdlp/multi_gpu_engine.hpp | 19 ++++----- cpp/src/pdlp/pdlp.cu | 4 +- cpp/src/pdlp/utils.cuh | 3 +- .../pdlp_distributed_test.cu | 6 ++- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 95c2dd8351..7803c6df43 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -61,9 +61,9 @@ void multi_gpu_engine_t::distributed_bound_objective_rescaling(f_t c_s f_t global_bound_sq = f_t(0); f_t global_obj_sq = f_t(0); for_each_shard([&](auto& s) { - const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); - auto policy = rmm::exec_policy(s.stream.view()); - auto bounds_begin = thrust::make_zip_iterator(scaled.constraint_lower_bounds.data(), + const auto& scaled = s.sub_pdlp->get_initial_scaling_strategy().get_scaled_op_problem(); + auto policy = rmm::exec_policy(s.stream.view()); + auto bounds_begin = thrust::make_zip_iterator(scaled.constraint_lower_bounds.data(), scaled.constraint_upper_bounds.data()); global_bound_sq += thrust::transform_reduce(policy, bounds_begin, @@ -71,12 +71,13 @@ void multi_gpu_engine_t::distributed_bound_objective_rescaling(f_t c_s rhs_sum_of_squares_t{}, f_t(0), thrust::plus{}); - global_obj_sq += thrust::transform_reduce(policy, - scaled.objective_coefficients.data(), - scaled.objective_coefficients.data() + s.rank_data.owned_var_size, - weighted_square_op{c_scaling_weight}, - f_t(0), - thrust::plus{}); + global_obj_sq += + thrust::transform_reduce(policy, + scaled.objective_coefficients.data(), + scaled.objective_coefficients.data() + s.rank_data.owned_var_size, + weighted_square_op{c_scaling_weight}, + f_t(0), + thrust::plus{}); }); // 3) Host-side derivation of the (identical on every shard) scaling scalars. @@ -206,7 +207,7 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs // ┌──────────────────────────────────────────────────────────────┐ // │ Setup │ // └──────────────────────────────────────────────────────────────┘ - + const int nb = static_cast(shards.size()); // Generate the GLOBAL z[] sequence in cstr-index order. // Scatter it to the shards according to the partition. @@ -227,9 +228,12 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs // Per-shard owned-slice spans consumed by the engine's *_bufs helpers. std::vector> q_owned, z_owned; - for (auto* v : {&q, &z, &atq}) v->reserve(nb); - for (auto* v : {&sigma_sq, &norm_q, &residual_norm}) v->reserve(nb); - for (auto* v : {&q_owned, &z_owned}) v->reserve(nb); + for (auto* v : {&q, &z, &atq}) + v->reserve(nb); + for (auto* v : {&sigma_sq, &norm_q, &residual_norm}) + v->reserve(nb); + for (auto* v : {&q_owned, &z_owned}) + v->reserve(nb); // Allocate per-shard scratch, scatter z according to partition, and build // the *_bufs views for the power iteration below. @@ -261,9 +265,9 @@ f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cs raft::copy( z.back().data(), h_owned_z.data(), static_cast(n_owned), s.stream.view()); thrust::fill(rmm::exec_policy_nosync(s.stream.view()), - z.back().data() + n_owned, - z.back().data() + cstr_total, - f_t(0)); + z.back().data() + n_owned, + z.back().data() + cstr_total, + f_t(0)); // Sync to ensure h_owned_z stays valid through the H2D copy (it goes // out of scope at end of this iteration of the per-shard loop). @@ -360,7 +364,8 @@ void multi_gpu_engine_t::distributed_compute_initial_step_size( "of A fallback is single-GPU only. This should have been rejected " "earlier in solve_lp_distributed_from_mps."); - const f_t sigma_max_sq = distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); + const f_t sigma_max_sq = + distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); auto& master = *master_pdlp_; auto* handle_ptr = master.get_handle_ptr(); diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 79d87b0426..b97d16bb85 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -94,7 +94,8 @@ struct multi_gpu_engine_t { } else if constexpr (std::is_invocable_v&>) { fn(s); } else { - cuopt_expects(false, error_type_t::RuntimeError, "for_each_shard: invalid function signature"); + cuopt_expects( + false, error_type_t::RuntimeError, "for_each_shard: invalid function signature"); } } } @@ -264,7 +265,8 @@ struct multi_gpu_engine_t { { std::vector> spans; spans.reserve(bufs.size()); - for (auto& b : bufs) spans.emplace_back(b.data(), b.size()); + for (auto& b : bufs) + spans.emplace_back(b.data(), b.size()); halo_exchange_var_bufs(spans); } @@ -298,7 +300,8 @@ struct multi_gpu_engine_t { { std::vector> spans; spans.reserve(bufs.size()); - for (auto& b : bufs) spans.emplace_back(b.data(), b.size()); + for (auto& b : bufs) + spans.emplace_back(b.data(), b.size()); halo_exchange_cstr_bufs(spans); } @@ -596,9 +599,8 @@ struct multi_gpu_engine_t { std::vector>& out_descs) { halo_exchange_cstr_bufs(in_bufs); - for_each_shard([&](auto& s, int r) { - s.sub_pdlp->pdhg_solver_.spmv_At_into(in_descs[r], out_descs[r]); - }); + for_each_shard( + [&](auto& s, int r) { s.sub_pdlp->pdhg_solver_.spmv_At_into(in_descs[r], out_descs[r]); }); } // Distributed A @ in on caller-owned scratch. Refreshes the halo of `in_bufs` @@ -610,9 +612,8 @@ struct multi_gpu_engine_t { std::vector>& out_descs) { halo_exchange_var_bufs(in_bufs); - for_each_shard([&](auto& s, int r) { - s.sub_pdlp->pdhg_solver_.spmv_A_into(in_descs[r], out_descs[r]); - }); + for_each_shard( + [&](auto& s, int r) { s.sub_pdlp->pdhg_solver_.spmv_A_into(in_descs[r], out_descs[r]); }); } // -------- High-level algorithms (defined in distributed_algorithms.cu) --- diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 2d3e3cef0b..b3b339a132 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -3305,7 +3305,7 @@ void pdlp_solver_t::scale_problem() raft::common::nvtx::range fun_scope("pdlp_solver_t::scale_problem"); if (is_distributed_master()) { multi_gpu_engine->distributed_scaling(settings_.hyper_params, primal_size_h_, inside_mip_); - + // Free per-shard scratch: no further scaling passes happen after this point. multi_gpu_engine->for_each_shard([](auto& shard) { auto& scaling = shard.sub_pdlp->get_initial_scaling_strategy(); @@ -3314,7 +3314,7 @@ void pdlp_solver_t::scale_problem() }); } else { initial_scaling_strategy_.scale_problem(); - + // Free scratch: no further scaling passes happen after this point. initial_scaling_strategy_.get_iteration_variable_scaling().resize(0, stream_view_); initial_scaling_strategy_.get_iteration_constraint_matrix_scaling().resize(0, stream_view_); diff --git a/cpp/src/pdlp/utils.cuh b/cpp/src/pdlp/utils.cuh index f404a45e6a..3210b05562 100644 --- a/cpp/src/pdlp/utils.cuh +++ b/cpp/src/pdlp/utils.cuh @@ -45,7 +45,8 @@ inline std::vector make_singular_value_probe(std::size_t size) std::vector out(size); std::mt19937 gen(1); std::normal_distribution dist(f_t(0.0), f_t(1.0)); - for (std::size_t i = 0; i < size; ++i) out[i] = dist(gen); + for (std::size_t i = 0; i < size; ++i) + out[i] = dist(gen); return out; } diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu index 8057e5deb3..da5d820cf1 100644 --- a/cpp/tests/linear_programming/pdlp_distributed_test.cu +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -123,8 +123,10 @@ TEST(pdlp_class, distributed_parity_good_max) const raft::handle_t handle{}; // Disable presolve: PSLP solves this 3-var/1-constraint problem entirely, bypassing // the distributed solver path this test is meant to exercise. - expect_distributed_matches_base( - handle, "linear_programming/good-max.mps", /*fixed_mps_format=*/false, /*disable_presolve=*/true); + expect_distributed_matches_base(handle, + "linear_programming/good-max.mps", + /*fixed_mps_format=*/false, + /*disable_presolve=*/true); } TEST(pdlp_class, distributed_parity_graph40_40) From 019e7314e2f6bb56bc53476807e92a73d2ae83b5 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 18:16:57 +0200 Subject: [PATCH 229/258] renamed max sing value to max sing value squared --- .../distributed_pdlp/distributed_algorithms.cu | 16 ++++++++-------- .../pdlp/distributed_pdlp/multi_gpu_engine.hpp | 12 ++++++------ cpp/src/pdlp/solve.cu | 2 +- cpp/src/pdlp/utils.cuh | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 7803c6df43..c52fab5940 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -192,17 +192,17 @@ void multi_gpu_engine_t::distributed_scaling(pdlp_hyper_params_t const } } -// -------- Distributed sigma_max(A) via power iteration -------------------- +// -------- Distributed sigma_max(A)^2 via power iteration ------------------ // Owns per-shard scratch (q / z / atq / scalar reductions) and drives the // iteration; every cross-shard operation goes through multi_gpu_engine_t's // *_bufs helpers (halo_exchange_{cstr,var}_bufs, distributed_l2_norm_bufs, // distributed_dot_bufs), so this function contains no NCCL calls directly. template -f_t multi_gpu_engine_t::distributed_max_singular_value(i_t n_global_cstrs, - int max_iterations, - f_t tolerance) +f_t multi_gpu_engine_t::distributed_max_singular_value_squared(i_t n_global_cstrs, + int max_iterations, + f_t tolerance) { - raft::common::nvtx::range scope("distributed_max_singular_value"); + raft::common::nvtx::range scope("distributed_max_singular_value_squared"); // ┌──────────────────────────────────────────────────────────────┐ // │ Setup │ @@ -365,13 +365,13 @@ void multi_gpu_engine_t::distributed_compute_initial_step_size( "earlier in solve_lp_distributed_from_mps."); const f_t sigma_max_sq = - distributed_max_singular_value(n_global_cstrs, max_iterations, tolerance); + distributed_max_singular_value_squared(n_global_cstrs, max_iterations, tolerance); auto& master = *master_pdlp_; auto* handle_ptr = master.get_handle_ptr(); auto stream_view = handle_ptr->get_stream(); - const f_t h_step_size = scaling_factor / std::sqrt(sigma_max_sq.value(stream_view_)); + const f_t h_step_size = scaling_factor / std::sqrt(sigma_max_sq); raft::copy(master.get_step_size().data(), &h_step_size, 1, stream_view); for_each_shard([&](auto& shard) { @@ -431,7 +431,7 @@ void multi_gpu_engine_t::distributed_compute_initial_primal_weight( template void multi_gpu_engine_t::distributed_pock_chambolle_scaling(F_TYPE, int); \ template void multi_gpu_engine_t::distributed_scaling( \ pdlp_hyper_params_t const&, int, bool); \ - template F_TYPE multi_gpu_engine_t::distributed_max_singular_value( \ + template F_TYPE multi_gpu_engine_t::distributed_max_singular_value_squared( \ int, int, F_TYPE); \ template void multi_gpu_engine_t::distributed_compute_initial_step_size( \ pdlp_hyper_params_t const&, int, F_TYPE, int, F_TYPE); \ diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index b97d16bb85..16007bfb01 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -647,12 +647,12 @@ struct multi_gpu_engine_t { i_t n_global_vars, bool inside_mip); - // Distributed sigma_max(A) via power iteration (used to seed the initial - // step size). Returns the largest singular value of the scaled constraint - // matrix; identical on every shard. - f_t distributed_max_singular_value(i_t n_global_cstrs, - int max_iterations = 5000, - f_t tolerance = 1e-4); + // Distributed sigma_max(A)^2 via power iteration (used to seed the initial + // step size). Returns the square of the largest singular value of the scaled + // constraint matrix. + f_t distributed_max_singular_value_squared(i_t n_global_cstrs, + int max_iterations = 5000, + f_t tolerance = 1e-4); // Distributed counterpart of pdlp_solver_t::compute_initial_step_size. // Requires set_master(...) to have been called; writes onto *master_pdlp_. diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 17a6336a24..a0f1a5961b 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2383,7 +2383,7 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( "Distributed PDLP does not support save_best_primal_so_far."); // Distributed PDLP today only supports the Stable3-shaped hyper-param profile: // - initial_step_size_max_singular_value = true (matches the sigma_max seeding - // driven by distributed_max_singular_value in the setup), + // driven by distributed_max_singular_value_squared in the setup), // - initial_primal_weight_combined_bounds = false and bound_objective_rescaling = true // (this is the profile where single-GPU compute_initial_primal_weight // short-circuits to primal_weight = 1, which distributed_compute_initial_primal_weight diff --git a/cpp/src/pdlp/utils.cuh b/cpp/src/pdlp/utils.cuh index 3210b05562..91c59be15b 100644 --- a/cpp/src/pdlp/utils.cuh +++ b/cpp/src/pdlp/utils.cuh @@ -37,8 +37,8 @@ namespace cuopt::mathematical_optimization::pdlp { // Host-side probe vector z ~ Normal(0, 1) that seeds the power iteration // for sigma_max(A) in compute_initial_step_size (single-GPU) and -// distributed_max_singular_value. Fixed seed (1) so single-GPU and distributed -// runs on the same problem produce bit-identical initial iterates. +// distributed_max_singular_value_squared. Fixed seed (1) so single-GPU and +// distributed runs on the same problem produce bit-identical initial iterates. template inline std::vector make_singular_value_probe(std::size_t size) { @@ -61,7 +61,7 @@ struct divide_by_device_scalar_t { // Elementwise: q := -*scalar * q + z. Used in the power-iteration residual // update (single-GPU compute_initial_step_size and distributed -// distributed_max_singular_value). Same named-struct rationale as above. +// distributed_max_singular_value_squared). Same named-struct rationale as above. template struct residual_fma_neg_scalar_t { f_t const* scalar; From 591fcd9e7265c62188fe162966474389f5cc84e6 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 18:17:08 +0200 Subject: [PATCH 230/258] style --- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 16007bfb01..84dba94ae3 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -649,7 +649,7 @@ struct multi_gpu_engine_t { // Distributed sigma_max(A)^2 via power iteration (used to seed the initial // step size). Returns the square of the largest singular value of the scaled - // constraint matrix. + // constraint matrix. f_t distributed_max_singular_value_squared(i_t n_global_cstrs, int max_iterations = 5000, f_t tolerance = 1e-4); From df87ba0982bd0033e89ad88b1fef8bff5cb650f1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 21:20:49 +0200 Subject: [PATCH 231/258] better asserts in problem.cu --- cpp/src/mip_heuristics/problem/problem.cu | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cpp/src/mip_heuristics/problem/problem.cu b/cpp/src/mip_heuristics/problem/problem.cu index e38e889495..9e7e737515 100644 --- a/cpp/src/mip_heuristics/problem/problem.cu +++ b/cpp/src/mip_heuristics/problem/problem.cu @@ -56,8 +56,13 @@ using simplex::variable_type_t; template void problem_t::op_problem_cstr_body(const optimization_problem_t& problem_) { - // Mark the problem as empty if the op_problem has an empty matrix. - if (problem_.get_constraint_matrix_values().is_empty()) { + // Mark the problem as empty only when there are no constraints AND no nnz. + // A distributed-PDLP shard may legitimately carry rows with zero local + // coefficients (e.g. a shard that owns 0 constraints but has halo rows for + // constraints owned by peers). Such a shard has n_constraints > 0 with an + // empty CSR values array, and must not be marked empty. + if (problem_.get_constraint_matrix_values().is_empty() && + problem_.get_n_constraints() == 0) { cuopt_assert(problem_.get_constraint_matrix_indices().is_empty(), "Problem is empty but constraint matrix indices are not empty."); cuopt_assert(problem_.get_constraint_matrix_offsets().size() == 1, @@ -566,7 +571,7 @@ void problem_t::check_problem_representation(bool check_transposed, } // Presolve reductions might trivially solve the problem to optimality/infeasibility. // In this case, it is exptected that the problem fields are empty. - if (!empty) { + if (!empty && nnz > 0) { // Check for empty fields cuopt_assert(!coefficients.is_empty(), "A_values must be set before calling the solver."); cuopt_assert(!variables.is_empty(), "A_indices must be set before calling the solver."); From 311bc62f5f84d1cf04f2eb6a36d72493afbe68fe Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 21:49:36 +0200 Subject: [PATCH 232/258] test on distributed_parity_cod105_max --- .../linear_programming/pdlp_distributed_test.cu | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_distributed_test.cu b/cpp/tests/linear_programming/pdlp_distributed_test.cu index da5d820cf1..74c35ddb34 100644 --- a/cpp/tests/linear_programming/pdlp_distributed_test.cu +++ b/cpp/tests/linear_programming/pdlp_distributed_test.cu @@ -111,22 +111,15 @@ TEST(pdlp_class, distributed_parity_afiro) expect_distributed_matches_base(handle, "linear_programming/afiro_original.mps", true); } -// Maximization LP (OBJSENSE MAX in the MPS): exercises the sign-flipping paths -// through normalization / presolve / solution translation on the distributed side. -// The problem is tiny (3 vars, 1 constraint) so shard partitions are lopsided -// (one shard ends up with 0 owned constraints) — good coverage of that edge. -TEST(pdlp_class, distributed_parity_good_max) +// Maximization LP: exercises the sign-flipping paths through normalization / +// presolve / solution translation on the distributed side. +TEST(pdlp_class, distributed_parity_cod105_max) { if (raft::device_setter::get_device_count() < 2) { GTEST_SKIP() << "Requires >=2 GPUs, found " << raft::device_setter::get_device_count(); } const raft::handle_t handle{}; - // Disable presolve: PSLP solves this 3-var/1-constraint problem entirely, bypassing - // the distributed solver path this test is meant to exercise. - expect_distributed_matches_base(handle, - "linear_programming/good-max.mps", - /*fixed_mps_format=*/false, - /*disable_presolve=*/true); + expect_distributed_matches_base(handle, "mip/cod105_max.mps"); } TEST(pdlp_class, distributed_parity_graph40_40) From fb8dcf6e0fc963fe3a859295fccfa316f152db34 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 22:00:11 +0200 Subject: [PATCH 233/258] better maximize support --- cpp/src/pdlp/distributed_pdlp/shard.cu | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 7511f75b5e..02430ac414 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -41,15 +41,12 @@ pdlp_shard_t::pdlp_shard_t(int device_id, "Right device must be set before building the shard"); // ---- 0. Problem-level scalars, taken straight from the global mps. ---- - // For a maximize problem we negate the objective (and offset / scaling - // factor), matching problem_helpers.cuh::convert_to_maximization_problem. - const bool maximize = mps.get_sense(); - f_t objective_offset = mps.get_objective_offset(); - f_t objective_scaling_factor = mps.get_objective_scaling_factor(); - if (maximize) { - objective_offset = -objective_offset; - objective_scaling_factor = -objective_scaling_factor; - } + // Objective coefficients / offset / scaling factor are passed through + // unchanged; the max -> min conversion (negating all three) happens once, + // in mip::problem_t's constructor via convert_to_maximization_problem + const bool maximize = mps.get_sense(); + const f_t objective_offset = mps.get_objective_offset(); + const f_t objective_scaling_factor = mps.get_objective_scaling_factor(); // Global (unpartitioned) host arrays, indexed by global var / cstr id. const std::vector& g_obj = mps.get_objective_coefficients(); @@ -69,7 +66,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, for (i_t i = 0; i < rank_data.owned_var_size; ++i) { const auto g = rank_data.local_to_global_var[i]; - h_obj[i] = maximize ? -g_obj[g] : g_obj[g]; + h_obj[i] = g_obj[g]; h_var_lower[i] = g_var_lower[g]; h_var_upper[i] = g_var_upper[g]; } @@ -153,7 +150,12 @@ pdlp_shard_t::pdlp_shard_t(int device_id, rank_data.h_A_t_values.data(), rank_data.h_A_t_values.size(), stream_view); - raft::copy(scaled.objective_coefficients.data(), h_obj.data(), h_obj.size(), stream_view); + // Use sub_problem's coefficients (already negated for max by + // convert_to_maximization_problem) so scaled matches the solver-facing form. + raft::copy(scaled.objective_coefficients.data(), + sub_problem->objective_coefficients.data(), + sub_problem->objective_coefficients.size(), + stream_view); raft::copy( scaled.constraint_lower_bounds.data(), h_cstr_lower.data(), h_cstr_lower.size(), stream_view); raft::copy( From eb3d0268d7159fd2b3ee7b2e38622a48fd911c9e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 22:03:58 +0200 Subject: [PATCH 234/258] rollback problem --- cpp/src/mip_heuristics/problem/problem.cu | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/cpp/src/mip_heuristics/problem/problem.cu b/cpp/src/mip_heuristics/problem/problem.cu index 9e7e737515..e38e889495 100644 --- a/cpp/src/mip_heuristics/problem/problem.cu +++ b/cpp/src/mip_heuristics/problem/problem.cu @@ -56,13 +56,8 @@ using simplex::variable_type_t; template void problem_t::op_problem_cstr_body(const optimization_problem_t& problem_) { - // Mark the problem as empty only when there are no constraints AND no nnz. - // A distributed-PDLP shard may legitimately carry rows with zero local - // coefficients (e.g. a shard that owns 0 constraints but has halo rows for - // constraints owned by peers). Such a shard has n_constraints > 0 with an - // empty CSR values array, and must not be marked empty. - if (problem_.get_constraint_matrix_values().is_empty() && - problem_.get_n_constraints() == 0) { + // Mark the problem as empty if the op_problem has an empty matrix. + if (problem_.get_constraint_matrix_values().is_empty()) { cuopt_assert(problem_.get_constraint_matrix_indices().is_empty(), "Problem is empty but constraint matrix indices are not empty."); cuopt_assert(problem_.get_constraint_matrix_offsets().size() == 1, @@ -571,7 +566,7 @@ void problem_t::check_problem_representation(bool check_transposed, } // Presolve reductions might trivially solve the problem to optimality/infeasibility. // In this case, it is exptected that the problem fields are empty. - if (!empty && nnz > 0) { + if (!empty) { // Check for empty fields cuopt_assert(!coefficients.is_empty(), "A_values must be set before calling the solver."); cuopt_assert(!variables.is_empty(), "A_indices must be set before calling the solver."); From 9410e5c4117df801c4a01e816c74dcce4314ad3c Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Fri, 10 Jul 2026 22:57:29 +0200 Subject: [PATCH 235/258] remove unused includes in pdlp_test --- cpp/tests/linear_programming/pdlp_test.cu | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 95d4010ce7..33ac914c35 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -8,16 +8,12 @@ #include #include #include -#include -#include #include #include #include #include #include -#include - #include "utilities/pdlp_test_utilities.cuh" #include "../mip/mip_utils.cuh" @@ -50,13 +46,11 @@ #include #include -#include #include #include #include #include #include -#include #include #include #include From cd8092c8114cec52700498998573ff2d3cd7a24b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 11:31:21 +0200 Subject: [PATCH 236/258] move non templated functions from mgpu.hpp to mgpu.cuh --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 303 ++++++++++++++++++ .../distributed_pdlp/multi_gpu_engine.hpp | 252 +-------------- 2 files changed, 320 insertions(+), 235 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 9bb9686d65..c71b5928d3 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -155,6 +155,309 @@ void multi_gpu_engine_t::sync_await_shards(rmm::cuda_stream_view maste } } +// -------- Halo exchange ----------------------------------------------------- +template +void multi_gpu_engine_t::halo_exchange_bufs_impl( + std::vector> const& bufs, + std::vector::halo_axis_t> const& axes) +{ + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(bufs.size()) == nb && static_cast(axes.size()) == nb, + error_type_t::RuntimeError, + "halo_exchange_bufs_impl: bufs / axes must have size == shards.size()"); + + // Step 1: gather owned values that each peer needs into per-peer staging. + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto const& ax = axes[r]; + auto x = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + if (ax.send_indices[peer].size() == 0) continue; + thrust::gather(rmm::exec_policy_nosync(s.stream.view()), + ax.send_indices[peer].begin(), + ax.send_indices[peer].end(), + x.data(), + ax.send_buf[peer].begin()); + } + } + + // Step 2: matched send / recv across the whole topology in one NCCL group. + CUOPT_NCCL_TRY(ncclGroupStart()); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto const& ax = axes[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + CUOPT_NCCL_TRY(ncclSend(ax.send_buf[peer].data(), + ax.send_buf[peer].size(), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); + } + } + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + auto const& ax = axes[r]; + auto x = bufs[r]; + for (int peer = 0; peer < nb; ++peer) { + if (peer == r) continue; + f_t* recv_ptr = x.data() + ax.owned_size + ax.recv_offsets[peer]; + CUOPT_NCCL_TRY(ncclRecv(recv_ptr, + static_cast(ax.recv_counts[peer]), + nccl_data_type(), + peer, + s.comm.get(), + s.stream.view().value())); + } + } + CUOPT_NCCL_TRY(ncclGroupEnd()); +} + +template +void multi_gpu_engine_t::halo_exchange_var_bufs( + std::vector> const& bufs) +{ + std::vector::halo_axis_t> axes; + axes.reserve(shards.size()); + for (auto& s : shards) + axes.push_back(s->var_halo_axis()); + halo_exchange_bufs_impl(bufs, axes); +} + +template +void multi_gpu_engine_t::halo_exchange_var_bufs( + std::vector>& bufs) +{ + std::vector> spans; + spans.reserve(bufs.size()); + for (auto& b : bufs) + spans.emplace_back(b.data(), b.size()); + halo_exchange_var_bufs(spans); +} + +template +void multi_gpu_engine_t::halo_exchange_cstr_bufs( + std::vector> const& bufs) +{ + std::vector::halo_axis_t> axes; + axes.reserve(shards.size()); + for (auto& s : shards) + axes.push_back(s->cstr_halo_axis()); + halo_exchange_bufs_impl(bufs, axes); +} + +template +void multi_gpu_engine_t::halo_exchange_cstr_bufs( + std::vector>& bufs) +{ + std::vector> spans; + spans.reserve(bufs.size()); + for (auto& b : bufs) + spans.emplace_back(b.data(), b.size()); + halo_exchange_cstr_bufs(spans); +} + +// -------- Gather owned slices to master ------------------------------------- +template +void multi_gpu_engine_t::gather_owned_var_to_master_bufs( + std::vector> const& shard_owned, raft::device_span master_buf) +{ + gather_owned_to_master_bufs_impl( + shard_owned, master_buf, owned_var_sizes_, local_to_global_vars_); +} + +template +void multi_gpu_engine_t::gather_owned_cstr_to_master_bufs( + std::vector> const& shard_owned, raft::device_span master_buf) +{ + gather_owned_to_master_bufs_impl( + shard_owned, master_buf, owned_cstr_sizes_, local_to_global_cstrs_); +} + +template +void multi_gpu_engine_t::gather_owned_to_master_bufs_impl( + std::vector> const& shard_owned, + raft::device_span master_buf, + std::vector const& owned_sizes, + std::vector> const& local_to_globals) +{ + cuopt_assert(master_pdlp_ != nullptr, + "gather_owned_to_master_bufs_impl requires set_master(...)"); + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(shard_owned.size()) == nb && + static_cast(owned_sizes.size()) == nb && + static_cast(local_to_globals.size()) == nb, + error_type_t::RuntimeError, + "gather_owned_to_master_bufs_impl: shard_owned / owned_sizes / " + "local_to_globals must all have size == shards.size()"); + + // Assemble on host in global-index order. + std::vector h_master(master_buf.size()); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + const std::size_t n_owned = owned_sizes[r]; + cuopt_expects(shard_owned[r].size() == n_owned, + error_type_t::RuntimeError, + "gather_owned_to_master_bufs_impl: shard_owned[r].size() must equal owned size"); + if (n_owned == 0) continue; + std::vector tmp(n_owned); + raft::copy(tmp.data(), shard_owned[r].data(), n_owned, s.stream.view()); + // Sync so tmp is populated before the host scatter (and stays valid). + s.stream.synchronize(); + thrust::scatter( + thrust::host, tmp.begin(), tmp.end(), local_to_globals[r].begin(), h_master.begin()); + } + + // Single H->D onto master's device (`stream` lives on the master device). + raft::copy(master_buf.data(), h_master.data(), master_buf.size(), stream.view()); + stream.synchronize(); +} + +// -------- NCCL allreduce (sum, in place) ------------------------------------ +template +void multi_gpu_engine_t::allreduce_sum_inplace_bufs( + std::vector> const& scalars) +{ + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(scalars.size()) == nb, + error_type_t::RuntimeError, + "allreduce_sum_inplace_bufs: scalars.size() must equal shards.size()"); + if (nb == 0) return; + + CUOPT_NCCL_TRY(ncclGroupStart()); + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + f_t* p = scalars[r].data_handle(); + CUOPT_NCCL_TRY(ncclAllReduce(p, + p, + /*count=*/1, + nccl_data_type(), + ncclSum, + s.comm.get(), + s.stream.view().value())); + } + CUOPT_NCCL_TRY(ncclGroupEnd()); +} + +template +void multi_gpu_engine_t::allreduce_sum_inplace_to_master_buf( + std::vector> const& shard_scalars, + raft::device_scalar_view master_dst, + rmm::cuda_stream_view master_stream) +{ + allreduce_sum_inplace_bufs(shard_scalars); + if (shards.empty()) return; + sync_await_shards(master_stream); + auto& s0 = *shards[0]; + raft::device_setter guard(s0.device_id); + raft::copy(master_dst.data_handle(), shard_scalars[0].data_handle(), 1, master_stream); +} + +// -------- Distributed dot / L2 norm ----------------------------------------- +template +void multi_gpu_engine_t::distributed_dot_bufs( + std::vector> const& a_bufs, + std::vector> const& b_bufs, + std::vector> const& out_scalars) +{ + const int nb = static_cast(shards.size()); + cuopt_expects(static_cast(a_bufs.size()) == nb && static_cast(b_bufs.size()) == nb && + static_cast(out_scalars.size()) == nb, + error_type_t::RuntimeError, + "distributed_dot_bufs: a_bufs / b_bufs / out_scalars must " + "all have size == shards.size()"); + + for (int r = 0; r < nb; ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + cuopt_expects(a_bufs[r].size() == b_bufs[r].size(), + error_type_t::RuntimeError, + "distributed_dot_bufs: a_bufs[r] and b_bufs[r] must have equal size"); + RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), + static_cast(a_bufs[r].size()), + a_bufs[r].data(), + 1, + b_bufs[r].data(), + 1, + out_scalars[r].data_handle(), + s.stream.view().value())); + } + + allreduce_sum_inplace_bufs(out_scalars); +} + +template +void multi_gpu_engine_t::distributed_dot_bufs( + std::vector> const& a_bufs, + std::vector> const& b_bufs, + std::vector>& out_scalars) +{ + std::vector> views; + views.reserve(out_scalars.size()); + for (auto& s : out_scalars) + views.emplace_back(raft::make_device_scalar_view(s.data())); + distributed_dot_bufs(a_bufs, b_bufs, views); +} + +template +void multi_gpu_engine_t::distributed_l2_norm_bufs( + std::vector> const& in_bufs, + std::vector> const& out_scalars) +{ + distributed_dot_bufs(in_bufs, in_bufs, out_scalars); + for (std::size_t r = 0; r < shards.size(); ++r) { + auto& s = *shards[r]; + raft::device_setter guard(s.device_id); + cub::DeviceTransform::Transform(out_scalars[r].data_handle(), + out_scalars[r].data_handle(), + 1, + sqrt_inplace_op_t{}, + s.stream.view().value()); + } +} + +template +void multi_gpu_engine_t::distributed_l2_norm_bufs( + std::vector> const& in_bufs, + std::vector>& out_scalars) +{ + std::vector> views; + views.reserve(out_scalars.size()); + for (auto& s : out_scalars) + views.emplace_back(raft::make_device_scalar_view(s.data())); + distributed_l2_norm_bufs(in_bufs, views); +} + +// -------- Fused halo-exchange + SpMV ---------------------------------------- +template +void multi_gpu_engine_t::distributed_spmv_At( + std::vector>& in_bufs, + std::vector>& in_descs, + std::vector>& out_descs) +{ + halo_exchange_cstr_bufs(in_bufs); + for_each_shard( + [&](auto& s, int r) { s.sub_pdlp->pdhg_solver_.spmv_At_into(in_descs[r], out_descs[r]); }); +} + +template +void multi_gpu_engine_t::distributed_spmv_A( + std::vector>& in_bufs, + std::vector>& in_descs, + std::vector>& out_descs) +{ + halo_exchange_var_bufs(in_bufs); + for_each_shard( + [&](auto& s, int r) { s.sub_pdlp->pdhg_solver_.spmv_A_into(in_descs[r], out_descs[r]); }); +} + template struct multi_gpu_engine_t; template struct multi_gpu_engine_t; diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 84dba94ae3..e279dd704c 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -187,88 +187,17 @@ struct multi_gpu_engine_t { // (rank, peer) pairs, receiving into each shard's halo tail. void halo_exchange_bufs_impl( std::vector> const& bufs, - std::vector::halo_axis_t> const& axes) - { - const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(bufs.size()) == nb && static_cast(axes.size()) == nb, - error_type_t::RuntimeError, - "halo_exchange_bufs_impl: bufs / axes must have size == shards.size()"); - - // Step 1: gather owned values that each peer needs into per-peer staging. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto const& ax = axes[r]; - auto x = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - if (ax.send_indices[peer].size() == 0) continue; - thrust::gather(rmm::exec_policy_nosync(s.stream.view()), - ax.send_indices[peer].begin(), - ax.send_indices[peer].end(), - x.data(), - ax.send_buf[peer].begin()); - } - } - - // Step 2: matched send / recv across the whole topology in one NCCL group. - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto const& ax = axes[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - CUOPT_NCCL_TRY(ncclSend(ax.send_buf[peer].data(), - ax.send_buf[peer].size(), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - auto const& ax = axes[r]; - auto x = bufs[r]; - for (int peer = 0; peer < nb; ++peer) { - if (peer == r) continue; - f_t* recv_ptr = x.data() + ax.owned_size + ax.recv_offsets[peer]; - CUOPT_NCCL_TRY(ncclRecv(recv_ptr, - static_cast(ax.recv_counts[peer]), - nccl_data_type(), - peer, - s.comm.get(), - s.stream.view().value())); - } - } - CUOPT_NCCL_TRY(ncclGroupEnd()); - } + std::vector::halo_axis_t> const& axes); // -------- Halo exchange (variables / x) --------------------------------- - void halo_exchange_var_bufs(std::vector> const& bufs) - { - std::vector::halo_axis_t> axes; - axes.reserve(shards.size()); - for (auto& s : shards) - axes.push_back(s->var_halo_axis()); - halo_exchange_bufs_impl(bufs, axes); - } + void halo_exchange_var_bufs(std::vector> const& bufs); // Overload: accept the owning device_uvector directly (rmm doesn't provide // an implicit conversion to raft::device_span, and std::vector doesn't // convert to std::vector, so we adapt element-wise here). Non-const & // because halo_exchange_bufs_impl writes the halo tail via ncclRecv, which // requires a mutable pointer. - void halo_exchange_var_bufs(std::vector>& bufs) - { - std::vector> spans; - spans.reserve(bufs.size()); - for (auto& b : bufs) - spans.emplace_back(b.data(), b.size()); - halo_exchange_var_bufs(spans); - } + void halo_exchange_var_bufs(std::vector>& bufs); // Wrapper: pdlp_solver_t accessor. Resolves one uvector per shard into a // vector of spans, then delegates to halo_exchange_var_bufs. @@ -286,24 +215,10 @@ struct multi_gpu_engine_t { } // -------- Halo exchange (constraints / y) ------------------------------- - void halo_exchange_cstr_bufs(std::vector> const& bufs) - { - std::vector::halo_axis_t> axes; - axes.reserve(shards.size()); - for (auto& s : shards) - axes.push_back(s->cstr_halo_axis()); - halo_exchange_bufs_impl(bufs, axes); - } + void halo_exchange_cstr_bufs(std::vector> const& bufs); // Overload: same rationale as halo_exchange_var_bufs above. - void halo_exchange_cstr_bufs(std::vector>& bufs) - { - std::vector> spans; - spans.reserve(bufs.size()); - for (auto& b : bufs) - spans.emplace_back(b.data(), b.size()); - halo_exchange_cstr_bufs(spans); - } + void halo_exchange_cstr_bufs(std::vector>& bufs); // Wrapper: pdlp_solver_t accessor. Resolves one uvector per shard into a // vector of spans, then delegates to halo_exchange_cstr_bufs. @@ -324,18 +239,11 @@ struct multi_gpu_engine_t { // Scatters data from each shard's owned slice to the master's buffer. // var and cstr version share the same implementation. void gather_owned_var_to_master_bufs(std::vector> const& shard_owned, - raft::device_span master_buf) - { - gather_owned_to_master_bufs_impl( - shard_owned, master_buf, owned_var_sizes_, local_to_global_vars_); - } + raft::device_span master_buf); void gather_owned_cstr_to_master_bufs( - std::vector> const& shard_owned, raft::device_span master_buf) - { - gather_owned_to_master_bufs_impl( - shard_owned, master_buf, owned_cstr_sizes_, local_to_global_cstrs_); - } + std::vector> const& shard_owned, + raft::device_span master_buf); // Wrappers around gather_owned_{var/cstr}_to_master_bufs using an accessor // buf_access : pdlp_solver_t& -> rmm::device_uvector& @@ -375,69 +283,13 @@ struct multi_gpu_engine_t { std::vector> const& shard_owned, raft::device_span master_buf, std::vector const& owned_sizes, - std::vector> const& local_to_globals) - { - cuopt_assert(master_pdlp_ != nullptr, - "gather_owned_to_master_bufs_impl requires set_master(...)"); - const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(shard_owned.size()) == nb && - static_cast(owned_sizes.size()) == nb && - static_cast(local_to_globals.size()) == nb, - error_type_t::RuntimeError, - "gather_owned_to_master_bufs_impl: shard_owned / owned_sizes / " - "local_to_globals must all have size == shards.size()"); - - // Assemble on host in global-index order. - std::vector h_master(master_buf.size()); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - const std::size_t n_owned = owned_sizes[r]; - cuopt_expects( - shard_owned[r].size() == n_owned, - error_type_t::RuntimeError, - "gather_owned_to_master_bufs_impl: shard_owned[r].size() must equal owned size"); - if (n_owned == 0) continue; - std::vector tmp(n_owned); - raft::copy(tmp.data(), shard_owned[r].data(), n_owned, s.stream.view()); - // Sync so tmp is populated before the host scatter (and stays valid). - s.stream.synchronize(); - thrust::scatter( - thrust::host, tmp.begin(), tmp.end(), local_to_globals[r].begin(), h_master.begin()); - } - - // Single H->D onto master's device (`stream` lives on the master device). - raft::copy(master_buf.data(), h_master.data(), master_buf.size(), stream.view()); - stream.synchronize(); - } + std::vector> const& local_to_globals); // -------- NCCL allreduce (sum, in place) -------------------------------- // Core: per-shard in-place sum-allreduce on a single f_t scalar viewed by // scalars[r], wrapped in one NCCL group so it executes as a single // collective. After this returns, every shard's scalar holds the global sum. - void allreduce_sum_inplace_bufs(std::vector> const& scalars) - { - const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(scalars.size()) == nb, - error_type_t::RuntimeError, - "allreduce_sum_inplace_bufs: scalars.size() must equal shards.size()"); - if (nb == 0) return; - - CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - f_t* p = scalars[r].data_handle(); - CUOPT_NCCL_TRY(ncclAllReduce(p, - p, - /*count=*/1, - nccl_data_type(), - ncclSum, - s.comm.get(), - s.stream.view().value())); - } - CUOPT_NCCL_TRY(ncclGroupEnd()); - } + void allreduce_sum_inplace_bufs(std::vector> const& scalars); // Wrapper: pdlp_solver_t accessor for a single per-shard scalar. // ptr_access : pdlp_solver_t& -> f_t* (pointer to the scalar @@ -459,15 +311,7 @@ struct multi_gpu_engine_t { void allreduce_sum_inplace_to_master_buf( std::vector> const& shard_scalars, raft::device_scalar_view master_dst, - rmm::cuda_stream_view master_stream) - { - allreduce_sum_inplace_bufs(shard_scalars); - if (shards.empty()) return; - sync_await_shards(master_stream); - auto& s0 = *shards[0]; - raft::device_setter guard(s0.device_id); - raft::copy(master_dst.data_handle(), shard_scalars[0].data_handle(), 1, master_stream); - } + rmm::cuda_stream_view master_stream); // Wrapper: applies the ptr_access lambda to each shard's sub_pdlp to build // the per-shard scalar views and to master_pdlp_ to obtain the master @@ -492,76 +336,24 @@ struct multi_gpu_engine_t { // out_scalars. void distributed_dot_bufs(std::vector> const& a_bufs, std::vector> const& b_bufs, - std::vector> const& out_scalars) - { - const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(a_bufs.size()) == nb && static_cast(b_bufs.size()) == nb && - static_cast(out_scalars.size()) == nb, - error_type_t::RuntimeError, - "distributed_dot_bufs: a_bufs / b_bufs / out_scalars must " - "all have size == shards.size()"); - - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - cuopt_expects(a_bufs[r].size() == b_bufs[r].size(), - error_type_t::RuntimeError, - "distributed_dot_bufs: a_bufs[r] and b_bufs[r] must have equal size"); - RAFT_CUBLAS_TRY(raft::linalg::detail::cublasdot(s.handle.get_cublas_handle(), - static_cast(a_bufs[r].size()), - a_bufs[r].data(), - 1, - b_bufs[r].data(), - 1, - out_scalars[r].data_handle(), - s.stream.view().value())); - } - - allreduce_sum_inplace_bufs(out_scalars); - } + std::vector> const& out_scalars); // Overload: accept owning device_scalar outputs directly. Wraps each into a // scalar_view and delegates to the span-based core. Convenience for the typical // case where per-shard outputs are rmm::device_scalar. void distributed_dot_bufs(std::vector> const& a_bufs, std::vector> const& b_bufs, - std::vector>& out_scalars) - { - std::vector> views; - views.reserve(out_scalars.size()); - for (auto& s : out_scalars) - views.emplace_back(raft::make_device_scalar_view(s.data())); - distributed_dot_bufs(a_bufs, b_bufs, views); - } + std::vector>& out_scalars); // Core L2 norm: writes sqrt(sum_r ||in_bufs[r]||_2^2) into every // *out_scalars[r].data_handle(). Delegates to distributed_dot_bufs(in, in, // out) then does a per-shard in-place sqrt on the resulting scalar. void distributed_l2_norm_bufs(std::vector> const& in_bufs, - std::vector> const& out_scalars) - { - distributed_dot_bufs(in_bufs, in_bufs, out_scalars); - for (std::size_t r = 0; r < shards.size(); ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); - cub::DeviceTransform::Transform(out_scalars[r].data_handle(), - out_scalars[r].data_handle(), - 1, - sqrt_inplace_op_t{}, - s.stream.view().value()); - } - } + std::vector> const& out_scalars); // Overload: same rationale as distributed_dot_bufs above. void distributed_l2_norm_bufs(std::vector> const& in_bufs, - std::vector>& out_scalars) - { - std::vector> views; - views.reserve(out_scalars.size()); - for (auto& s : out_scalars) - views.emplace_back(raft::make_device_scalar_view(s.data())); - distributed_l2_norm_bufs(in_bufs, views); - } + std::vector>& out_scalars); // Wrapper: accessor form. Resolves per-shard input / output / owned-length // then delegates to distributed_l2_norm_bufs. @@ -596,12 +388,7 @@ struct multi_gpu_engine_t { // local spmv_At_into that reads from in_descs[r] and writes into out_descs[r]. void distributed_spmv_At(std::vector>& in_bufs, std::vector>& in_descs, - std::vector>& out_descs) - { - halo_exchange_cstr_bufs(in_bufs); - for_each_shard( - [&](auto& s, int r) { s.sub_pdlp->pdhg_solver_.spmv_At_into(in_descs[r], out_descs[r]); }); - } + std::vector>& out_descs); // Distributed A @ in on caller-owned scratch. Refreshes the halo of `in_bufs` // (var axis, since the input is var-shaped), then dispatches each shard's @@ -609,12 +396,7 @@ struct multi_gpu_engine_t { // (in_descs to var_total, out_descs to cstr_total). void distributed_spmv_A(std::vector>& in_bufs, std::vector>& in_descs, - std::vector>& out_descs) - { - halo_exchange_var_bufs(in_bufs); - for_each_shard( - [&](auto& s, int r) { s.sub_pdlp->pdhg_solver_.spmv_A_into(in_descs[r], out_descs[r]); }); - } + std::vector>& out_descs); // -------- High-level algorithms (defined in distributed_algorithms.cu) --- // Refreshes the halo copies of the cumulative variable + constraint scalings on From 85e170e5c7abe93ea4181889f696436f9e158347 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 14:50:39 +0200 Subject: [PATCH 237/258] removed sqrt_inplace_op_t --- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu | 11 ++++++----- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp | 10 ---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index c71b5928d3..a4a90b0900 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -415,11 +415,12 @@ void multi_gpu_engine_t::distributed_l2_norm_bufs( for (std::size_t r = 0; r < shards.size(); ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - cub::DeviceTransform::Transform(out_scalars[r].data_handle(), - out_scalars[r].data_handle(), - 1, - sqrt_inplace_op_t{}, - s.stream.view().value()); + cub::DeviceTransform::Transform( + out_scalars[r].data_handle(), + out_scalars[r].data_handle(), + 1, + [] __device__(f_t x) { return cuda::std::sqrt(x); }, + s.stream.view().value()); } } diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index e279dd704c..bf8c723397 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -59,16 +59,6 @@ constexpr ncclDataType_t nccl_data_type() } } -// Element-wise sqrt functor. Defined at namespace scope (not as a local -// extended HD lambda) because nvcc disallows extended __host__ __device__ -// lambdas appearing inside templates whose template arguments are -// themselves local lambda types (which happens when distributed_l2_norm is -// invoked with closure accessors). -template -struct sqrt_inplace_op_t { - __host__ __device__ f_t operator()(f_t x) const { return cuda::std::sqrt(x); } -}; - template struct multi_gpu_engine_t { // Constructs shards from rank_data. The global (unpartitioned) problem is From d3de007fcaf07a9950d58ae1bb90ae4298e57805 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 14:59:13 +0200 Subject: [PATCH 238/258] replace many calls with simpler for_each --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 71 ++++++------------- .../distributed_pdlp/multi_gpu_engine.hpp | 43 +++++------ cpp/src/pdlp/pdlp.cu | 31 ++++---- 3 files changed, 54 insertions(+), 91 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index a4a90b0900..8061f2d8b5 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -68,11 +68,10 @@ multi_gpu_engine_t::multi_gpu_engine_t( sync_master_ready_event_ = std::make_unique(); graph_shard_ready_events_.reserve(nb_parts); sync_shard_ready_events_.reserve(nb_parts); - for (int r = 0; r < nb_parts; ++r) { - raft::device_setter guard(devices[r]); + for_each_shard([&](auto&) { graph_shard_ready_events_.emplace_back(std::make_unique()); sync_shard_ready_events_.emplace_back(std::make_unique()); - } + }); // Cache per-shard partition metadata for gather_owned_*_to_master_bufs. owned_var_sizes_.reserve(nb_parts); @@ -112,21 +111,14 @@ template void multi_gpu_engine_t::graph_capture_fork_to_shards(rmm::cuda_stream_view master_stream) { graph_master_ready_event_->record(master_stream); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - graph_master_ready_event_->stream_wait(s->stream.view()); - } + for_each_shard([&](auto& s) { graph_master_ready_event_->stream_wait(s.stream.view()); }); } template void multi_gpu_engine_t::graph_capture_join_from_shards( rmm::cuda_stream_view master_stream) { - const int nb = static_cast(shards.size()); - for (int r = 0; r < nb; ++r) { - raft::device_setter guard(shards[r]->device_id); - graph_shard_ready_events_[r]->record(shards[r]->stream.view()); - } + for_each_shard([&](auto& s, int r) { graph_shard_ready_events_[r]->record(s.stream.view()); }); for (auto& e : graph_shard_ready_events_) { e->stream_wait(master_stream); } @@ -136,20 +128,13 @@ template void multi_gpu_engine_t::sync_await_master(rmm::cuda_stream_view master_stream) { sync_master_ready_event_->record(master_stream); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - sync_master_ready_event_->stream_wait(s->stream.view()); - } + for_each_shard([&](auto& s) { sync_master_ready_event_->stream_wait(s.stream.view()); }); } template void multi_gpu_engine_t::sync_await_shards(rmm::cuda_stream_view master_stream) { - const int nb = static_cast(shards.size()); - for (int r = 0; r < nb; ++r) { - raft::device_setter guard(shards[r]->device_id); - sync_shard_ready_events_[r]->record(shards[r]->stream.view()); - } + for_each_shard([&](auto& s, int r) { sync_shard_ready_events_[r]->record(s.stream.view()); }); for (auto& e : sync_shard_ready_events_) { e->stream_wait(master_stream); } @@ -167,9 +152,7 @@ void multi_gpu_engine_t::halo_exchange_bufs_impl( "halo_exchange_bufs_impl: bufs / axes must have size == shards.size()"); // Step 1: gather owned values that each peer needs into per-peer staging. - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { auto const& ax = axes[r]; auto x = bufs[r]; for (int peer = 0; peer < nb; ++peer) { @@ -181,13 +164,11 @@ void multi_gpu_engine_t::halo_exchange_bufs_impl( x.data(), ax.send_buf[peer].begin()); } - } + }); // Step 2: matched send / recv across the whole topology in one NCCL group. CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { auto const& ax = axes[r]; for (int peer = 0; peer < nb; ++peer) { if (peer == r) continue; @@ -198,10 +179,8 @@ void multi_gpu_engine_t::halo_exchange_bufs_impl( s.comm.get(), s.stream.view().value())); } - } - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + }); + for_each_shard([&](auto& s, int r) { auto const& ax = axes[r]; auto x = bufs[r]; for (int peer = 0; peer < nb; ++peer) { @@ -214,7 +193,7 @@ void multi_gpu_engine_t::halo_exchange_bufs_impl( s.comm.get(), s.stream.view().value())); } - } + }); CUOPT_NCCL_TRY(ncclGroupEnd()); } @@ -298,21 +277,19 @@ void multi_gpu_engine_t::gather_owned_to_master_bufs_impl( // Assemble on host in global-index order. std::vector h_master(master_buf.size()); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { const std::size_t n_owned = owned_sizes[r]; cuopt_expects(shard_owned[r].size() == n_owned, error_type_t::RuntimeError, "gather_owned_to_master_bufs_impl: shard_owned[r].size() must equal owned size"); - if (n_owned == 0) continue; + if (n_owned == 0) return; std::vector tmp(n_owned); raft::copy(tmp.data(), shard_owned[r].data(), n_owned, s.stream.view()); // Sync so tmp is populated before the host scatter (and stays valid). s.stream.synchronize(); thrust::scatter( thrust::host, tmp.begin(), tmp.end(), local_to_globals[r].begin(), h_master.begin()); - } + }); // Single H->D onto master's device (`stream` lives on the master device). raft::copy(master_buf.data(), h_master.data(), master_buf.size(), stream.view()); @@ -331,9 +308,7 @@ void multi_gpu_engine_t::allreduce_sum_inplace_bufs( if (nb == 0) return; CUOPT_NCCL_TRY(ncclGroupStart()); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { f_t* p = scalars[r].data_handle(); CUOPT_NCCL_TRY(ncclAllReduce(p, p, @@ -342,7 +317,7 @@ void multi_gpu_engine_t::allreduce_sum_inplace_bufs( ncclSum, s.comm.get(), s.stream.view().value())); - } + }); CUOPT_NCCL_TRY(ncclGroupEnd()); } @@ -374,9 +349,7 @@ void multi_gpu_engine_t::distributed_dot_bufs( "distributed_dot_bufs: a_bufs / b_bufs / out_scalars must " "all have size == shards.size()"); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { cuopt_expects(a_bufs[r].size() == b_bufs[r].size(), error_type_t::RuntimeError, "distributed_dot_bufs: a_bufs[r] and b_bufs[r] must have equal size"); @@ -388,7 +361,7 @@ void multi_gpu_engine_t::distributed_dot_bufs( 1, out_scalars[r].data_handle(), s.stream.view().value())); - } + }); allreduce_sum_inplace_bufs(out_scalars); } @@ -412,16 +385,14 @@ void multi_gpu_engine_t::distributed_l2_norm_bufs( std::vector> const& out_scalars) { distributed_dot_bufs(in_bufs, in_bufs, out_scalars); - for (std::size_t r = 0; r < shards.size(); ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](pdlp_shard_t& s, int r) { cub::DeviceTransform::Transform( out_scalars[r].data_handle(), out_scalars[r].data_handle(), 1, [] __device__(f_t x) { return cuda::std::sqrt(x); }, s.stream.view().value()); - } + }); } template diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index bf8c723397..fba3b87293 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -79,11 +79,14 @@ struct multi_gpu_engine_t { for (int r = 0; r < static_cast(shards.size()); ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); + // If the function is invocable with a pdlp_shard_t& and an int, call it with the shard and the rank. if constexpr (std::is_invocable_v&, int>) { fn(s, r); + // If the function is invocable only with a pdlp_shard_t&, call it with the shard. } else if constexpr (std::is_invocable_v&>) { fn(s); } else { + // Otherwise, the function has an invalid signature. cuopt_expects( false, error_type_t::RuntimeError, "for_each_shard: invalid function signature"); } @@ -102,9 +105,7 @@ struct multi_gpu_engine_t { // iterator-shaped types cub accepts: raw pointers, thrust iterators, ...) // - outs[r] is the output iterator for shard r // - sizes[r] is the element count for shard r - // All three must have size == shards.size(). The heterogeneous per-shard - // input types are captured by the caller into whatever container / element - // type is convenient (e.g. std::vector>). + // All three must have size == shards.size(). template void distributed_transform_bufs(std::vector const& in_tuples, std::vector const& outs, @@ -117,11 +118,9 @@ struct multi_gpu_engine_t { error_type_t::RuntimeError, "distributed_transform_bufs: in_tuples / outs / sizes must " "all have size == shards.size()"); - for (int r = 0; r < nb; ++r) { - auto& s = *shards[r]; - raft::device_setter guard(s.device_id); + for_each_shard([&](auto& s, int r) { cub::DeviceTransform::Transform(in_tuples[r], outs[r], sizes[r], op, s.stream.view()); - } + }); } // Wrapper: accessor form. Resolves each shard's cub input_tuple / output / @@ -150,15 +149,14 @@ struct multi_gpu_engine_t { in_tuples.reserve(shards.size()); outs.reserve(shards.size()); sizes.reserve(shards.size()); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - auto& sub = *s->sub_pdlp; + for_each_shard([&](auto& s) { + auto& sub = *s.sub_pdlp; // Turns a tuple of accessors into a tuple of values. in_tuples.emplace_back(std::apply( [&sub](auto&... acc) { return cuda::std::make_tuple(acc(sub)...); }, in_accessors)); outs.emplace_back(out(sub)); sizes.emplace_back(sz(sub)); - } + }); distributed_transform_bufs(in_tuples, outs, sizes, op); } @@ -289,10 +287,9 @@ struct multi_gpu_engine_t { { std::vector> scalars; scalars.reserve(shards.size()); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s->sub_pdlp))); - } + for_each_shard([&](auto& s) { + scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s.sub_pdlp))); + }); allreduce_sum_inplace_bufs(scalars); } @@ -313,10 +310,9 @@ struct multi_gpu_engine_t { "allreduce_sum_inplace_to_master requires set_master(...) to have been called"); std::vector> shard_scalars; shard_scalars.reserve(shards.size()); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - shard_scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s->sub_pdlp))); - } + for_each_shard([&](auto& s) { + shard_scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s.sub_pdlp))); + }); allreduce_sum_inplace_to_master_buf( shard_scalars, raft::make_device_scalar_view(ptr_access(*master_pdlp_)), master_stream); } @@ -357,14 +353,13 @@ struct multi_gpu_engine_t { std::vector> out_scalars; in_bufs.reserve(shards.size()); out_scalars.reserve(shards.size()); - for (auto& s : shards) { - raft::device_setter guard(s->device_id); - auto& sub = *s->sub_pdlp; + for_each_shard([&](auto& s) { + auto& sub = *s.sub_pdlp; auto& buf = buf_access(sub); - const i_t n = size_access(*s); + const i_t n = size_access(s); in_bufs.emplace_back(buf.data(), static_cast(n)); out_scalars.emplace_back(raft::make_device_scalar_view(out_access(sub))); - } + }); distributed_l2_norm_bufs(in_bufs, out_scalars); } diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index b3b339a132..5808b6cb02 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -565,13 +565,12 @@ pdlp_solver_t::pdlp_solver_t( dual_size_h_ = n_cstr; // Distributed conergence_information::init_l2_norms - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->sub_pdlp->get_current_termination_strategy() + multi_gpu_engine->for_each_shard([](auto& shard) { + shard.sub_pdlp->get_current_termination_strategy() .get_convergence_information() - .compute_owned_reference_norm_partials(shard->rank_data.owned_var_size, - shard->rank_data.owned_cstr_size); - } + .compute_owned_reference_norm_partials(shard.rank_data.owned_var_size, + shard.rank_data.owned_cstr_size); + }); multi_gpu_engine->allreduce_sum_inplace([](pdlp_solver_t& sp) -> f_t* { return sp.get_current_termination_strategy() .get_convergence_information() @@ -582,13 +581,12 @@ pdlp_solver_t::pdlp_solver_t( .get_convergence_information() .l2_norm_primal_linear_objective_data(); }); - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - shard->sub_pdlp->get_current_termination_strategy() + multi_gpu_engine->for_each_shard([](auto& shard) { + shard.sub_pdlp->get_current_termination_strategy() .get_convergence_information() .sqrt_reference_norms_inplace(); - shard->stream.synchronize(); - } + shard.stream.synchronize(); + }); // Broadcast the values to the master { auto& s0 = *multi_gpu_engine->shards[0]; @@ -2302,9 +2300,8 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte return p.pdhg_solver_.get_reflected_dual(); }); - for (auto& shard : multi_gpu_engine->shards) { - raft::device_setter guard(shard->device_id); - auto& sub_pdlp = *shard->sub_pdlp; + multi_gpu_engine->for_each_shard([](auto& shard) { + auto& sub_pdlp = *shard.sub_pdlp; auto& sub_cv = sub_pdlp.pdhg_solver_.get_cusparse_view(); RAFT_CUSPARSE_TRY( @@ -2315,13 +2312,13 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte sub_pdlp.pdhg_solver_.get_primal_tmp_resource(), sub_cv, sub_pdlp.pdhg_solver_.get_saddle_point_state(), - shard->rank_data.owned_var_size, - shard->rank_data.owned_cstr_size); + shard.rank_data.owned_var_size, + shard.rank_data.owned_cstr_size); RAFT_CUSPARSE_TRY(cusparseDnVecSetValues( sub_cv.potential_next_dual_solution, (void*)sub_pdlp.pdhg_solver_.get_potential_next_dual_solution().data())); - } + }); multi_gpu_engine->allreduce_sum_inplace_to_master( [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }, From 0172220cbe1beab522c4c5623b2558dabd251a50 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 15:27:09 +0200 Subject: [PATCH 239/258] multi_gpu_engine.hppp fully reviewed !! updated coments and order for clarity --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 38 +++++----- .../distributed_pdlp/multi_gpu_engine.hpp | 71 +++++++++---------- cpp/src/pdlp/pdlp.cu | 14 ++-- .../restart_strategy/pdlp_restart_strategy.cu | 16 ++--- 4 files changed, 63 insertions(+), 76 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 8061f2d8b5..56b39d7cf6 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -242,22 +242,6 @@ void multi_gpu_engine_t::halo_exchange_cstr_bufs( } // -------- Gather owned slices to master ------------------------------------- -template -void multi_gpu_engine_t::gather_owned_var_to_master_bufs( - std::vector> const& shard_owned, raft::device_span master_buf) -{ - gather_owned_to_master_bufs_impl( - shard_owned, master_buf, owned_var_sizes_, local_to_global_vars_); -} - -template -void multi_gpu_engine_t::gather_owned_cstr_to_master_bufs( - std::vector> const& shard_owned, raft::device_span master_buf) -{ - gather_owned_to_master_bufs_impl( - shard_owned, master_buf, owned_cstr_sizes_, local_to_global_cstrs_); -} - template void multi_gpu_engine_t::gather_owned_to_master_bufs_impl( std::vector> const& shard_owned, @@ -296,6 +280,22 @@ void multi_gpu_engine_t::gather_owned_to_master_bufs_impl( stream.synchronize(); } +template +void multi_gpu_engine_t::gather_owned_var_to_master_bufs( + std::vector> const& shard_owned, raft::device_span master_buf) +{ + gather_owned_to_master_bufs_impl( + shard_owned, master_buf, owned_var_sizes_, local_to_global_vars_); +} + +template +void multi_gpu_engine_t::gather_owned_cstr_to_master_bufs( + std::vector> const& shard_owned, raft::device_span master_buf) +{ + gather_owned_to_master_bufs_impl( + shard_owned, master_buf, owned_cstr_sizes_, local_to_global_cstrs_); +} + // -------- NCCL allreduce (sum, in place) ------------------------------------ template void multi_gpu_engine_t::allreduce_sum_inplace_bufs( @@ -324,11 +324,13 @@ void multi_gpu_engine_t::allreduce_sum_inplace_bufs( template void multi_gpu_engine_t::allreduce_sum_inplace_to_master_buf( std::vector> const& shard_scalars, - raft::device_scalar_view master_dst, - rmm::cuda_stream_view master_stream) + raft::device_scalar_view master_dst) { + cuopt_assert(master_pdlp_ != nullptr, + "allreduce_sum_inplace_to_master_buf requires set_master(...)"); allreduce_sum_inplace_bufs(shard_scalars); if (shards.empty()) return; + auto master_stream = master_pdlp_->get_handle_ptr()->get_stream(); sync_await_shards(master_stream); auto& s0 = *shards[0]; raft::device_setter guard(s0.device_id); diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index fba3b87293..4a0666646e 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -136,7 +136,7 @@ struct multi_gpu_engine_t { !shards.empty(), error_type_t::RuntimeError, "distributed_transform: engine has no shards"); // Deduce per-shard tuple / output types from the accessors themselves so - // the runtime vector element types match cub's expectations exactly. + // the runtime vector doesn't complain auto& sample_sub = *shards[0]->sub_pdlp; using in_tuple_t = decltype(std::apply( [&sample_sub](auto&... acc) { return cuda::std::make_tuple(acc(sample_sub)...); }, @@ -149,9 +149,10 @@ struct multi_gpu_engine_t { in_tuples.reserve(shards.size()); outs.reserve(shards.size()); sizes.reserve(shards.size()); + for_each_shard([&](auto& s) { auto& sub = *s.sub_pdlp; - // Turns a tuple of accessors into a tuple of values. + // apply() = Turns a tuple of accessors into a tuple of values. in_tuples.emplace_back(std::apply( [&sub](auto&... acc) { return cuda::std::make_tuple(acc(sub)...); }, in_accessors)); outs.emplace_back(out(sub)); @@ -181,10 +182,7 @@ struct multi_gpu_engine_t { void halo_exchange_var_bufs(std::vector> const& bufs); // Overload: accept the owning device_uvector directly (rmm doesn't provide - // an implicit conversion to raft::device_span, and std::vector doesn't - // convert to std::vector, so we adapt element-wise here). Non-const & - // because halo_exchange_bufs_impl writes the halo tail via ncclRecv, which - // requires a mutable pointer. + // an implicit conversion to raft::device_span) void halo_exchange_var_bufs(std::vector>& bufs); // Wrapper: pdlp_solver_t accessor. Resolves one uvector per shard into a @@ -224,16 +222,22 @@ struct multi_gpu_engine_t { } // -------- Gather owned slices to master --------------------------------- - // Scatters data from each shard's owned slice to the master's buffer. - // var and cstr version share the same implementation. + // {var/cstr}-agnostic core: scatters each shard's owned slice into + // master_buf using local_to_globals[r] as the destination index list. + // owned_sizes[r] and local_to_globals[r] are the axis-specific + // rank_data.owned_{var/cstr}_size and rank_data.local_to_global_{var/cstr} for shard r. + void gather_owned_to_master_bufs_impl( + std::vector> const& shard_owned, + raft::device_span master_buf, + std::vector const& owned_sizes, + std::vector> const& local_to_globals); + + // -------- Gather (variables / x) ---------------------------------------- void gather_owned_var_to_master_bufs(std::vector> const& shard_owned, raft::device_span master_buf); - void gather_owned_cstr_to_master_bufs( - std::vector> const& shard_owned, - raft::device_span master_buf); - - // Wrappers around gather_owned_{var/cstr}_to_master_bufs using an accessor + // Wrapper: pdlp_solver_t accessor. Slices each shard's owned prefix and + // then delegates to gather_owned_var_to_master_bufs. // buf_access : pdlp_solver_t& -> rmm::device_uvector& template void gather_owned_var_to_master(BufAccess&& buf_access) @@ -249,6 +253,12 @@ struct multi_gpu_engine_t { gather_owned_var_to_master_bufs(shard_bufs, raft::device_span{m.data(), m.size()}); } + // -------- Gather (constraints / y) -------------------------------------- + void gather_owned_cstr_to_master_bufs( + std::vector> const& shard_owned, + raft::device_span master_buf); + + // Wrapper: same rationale as gather_owned_var_to_master. template void gather_owned_cstr_to_master(BufAccess&& buf_access) { @@ -263,16 +273,6 @@ struct multi_gpu_engine_t { gather_owned_cstr_to_master_bufs(shard_bufs, raft::device_span{m.data(), m.size()}); } - // Actual implementation. {var/cstr}-agnostic core shared by the two - // gather_owned_{var/cstr}_to_master_bufs entry points above. - // owned_sizes[r] and local_to_globals[r] are the axis-specific - // rank_data.owned_{var/cstr}_size and rank_data.local_to_global_{var/cstr} for shard r. - void gather_owned_to_master_bufs_impl( - std::vector> const& shard_owned, - raft::device_span master_buf, - std::vector const& owned_sizes, - std::vector> const& local_to_globals); - // -------- NCCL allreduce (sum, in place) -------------------------------- // Core: per-shard in-place sum-allreduce on a single f_t scalar viewed by // scalars[r], wrapped in one NCCL group so it executes as a single @@ -294,17 +294,16 @@ struct multi_gpu_engine_t { } // Core: same as allreduce_sum_inplace_bufs, plus after the collective the - // value is D2D-copied from shard 0 into master_dst + // value is D2D-copied from shard 0 into master_dst. On master's stream. void allreduce_sum_inplace_to_master_buf( std::vector> const& shard_scalars, - raft::device_scalar_view master_dst, - rmm::cuda_stream_view master_stream); + raft::device_scalar_view master_dst); // Wrapper: applies the ptr_access lambda to each shard's sub_pdlp to build // the per-shard scalar views and to master_pdlp_ to obtain the master // destination, then delegates to allreduce_sum_inplace_to_master_buf. template - void allreduce_sum_inplace_to_master(PtrAccess&& ptr_access, rmm::cuda_stream_view master_stream) + void allreduce_sum_inplace_to_master(PtrAccess&& ptr_access) { cuopt_assert(master_pdlp_ != nullptr, "allreduce_sum_inplace_to_master requires set_master(...) to have been called"); @@ -314,7 +313,7 @@ struct multi_gpu_engine_t { shard_scalars.emplace_back(raft::make_device_scalar_view(ptr_access(*s.sub_pdlp))); }); allreduce_sum_inplace_to_master_buf( - shard_scalars, raft::make_device_scalar_view(ptr_access(*master_pdlp_)), master_stream); + shard_scalars, raft::make_device_scalar_view(ptr_access(*master_pdlp_))); } // -------- Distributed dot / L2 norm ------------------------------------- @@ -337,7 +336,7 @@ struct multi_gpu_engine_t { void distributed_l2_norm_bufs(std::vector> const& in_bufs, std::vector> const& out_scalars); - // Overload: same rationale as distributed_dot_bufs above. + // Overload: same rationale as distributed_dot_bufs above. Allows to use rmm::device_scalar directly. void distributed_l2_norm_bufs(std::vector> const& in_bufs, std::vector>& out_scalars); @@ -394,8 +393,7 @@ struct multi_gpu_engine_t { void distributed_bound_objective_rescaling(f_t c_scaling_weight); // Distributed Ruiz inf-scaling (num_iter passes). Each shard computes both its - // owned-row and owned-column inf-norms locally; a per-iteration halo broadcast - // of both cumulative scalings is the only cross-shard communication. + // owned-row and owned-column inf-norms locally then broadcasts the cumulative scalings to all shards. void distributed_ruiz_inf_scaling(int num_iter, i_t n_global_vars); // Distributed Pock-Chambolle scaling (one pass), mirroring the single-GPU @@ -406,9 +404,7 @@ struct multi_gpu_engine_t { // single-GPU by orchestrating: // - Ruiz inf-scaling -> populates cumulative row/col scalings // - Pock-Chambolle scaling -> same - // - per-shard apply_cummulative_scaling_to_problem() to apply the cumulative - // scalings to A, c, variable and constraint bounds (this is scale_problem() - // minus its shard-local bound/objective rescaling) + // - per-shard apply_cummulative_scaling_to_problem() // - global bound/objective rescaling via distributed_bound_objective_rescaling void distributed_scaling(pdlp_hyper_params_t const& hyper_params, i_t n_global_vars, @@ -422,7 +418,6 @@ struct multi_gpu_engine_t { f_t tolerance = 1e-4); // Distributed counterpart of pdlp_solver_t::compute_initial_step_size. - // Requires set_master(...) to have been called; writes onto *master_pdlp_. void distributed_compute_initial_step_size(pdlp_hyper_params_t const& hyper_params, i_t n_global_cstrs, f_t scaling_factor, @@ -433,12 +428,10 @@ struct multi_gpu_engine_t { // Writes primal_weight = best_primal_weight = 1 onto master + every shard, // mirroring the Stable3-shaped short-circuit // (!initial_primal_weight_combined_bounds && bound_objective_rescaling). - // Requires set_master(...) to have been called. void distributed_compute_initial_primal_weight(pdlp_hyper_params_t const& hyper_params); // Gather the global potential_next primal/dual solutions and the reduced cost // onto the master from the owned slices distributed across shards. - // Requires set_master(...) to have been called; writes onto master_pdlp_. void gather_potential_next_solutions_to_master(); // Engine-level stream for fork/join orchestration (master side). @@ -464,8 +457,8 @@ struct multi_gpu_engine_t { // ===== Cross-stream synchronization events ===== // two different events - // capture_*_event_ are used inside graph capture - // ext_*_event_ are used when sync is needed outside of graph + // graph_*_event_ are used inside graph capture + // sync_*_event_ are used when sync is needed outside of graph std::unique_ptr graph_master_ready_event_; std::vector> graph_shard_ready_events_; std::unique_ptr sync_master_ready_event_; diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 5808b6cb02..6084bf222f 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -2321,16 +2321,12 @@ void pdlp_solver_t::compute_fixed_error(std::vector& has_restarte }); multi_gpu_engine->allreduce_sum_inplace_to_master( - [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }, - stream_view_); - multi_gpu_engine->allreduce_sum_inplace_to_master( - [](auto& sp) -> f_t* { - return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); - }, - stream_view_); + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_interaction().data(); }); + multi_gpu_engine->allreduce_sum_inplace_to_master([](auto& sp) -> f_t* { + return sp.step_size_strategy_.get_norm_squared_delta_primal().data(); + }); multi_gpu_engine->allreduce_sum_inplace_to_master( - [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }, - stream_view_); + [](auto& sp) -> f_t* { return sp.step_size_strategy_.get_norm_squared_delta_dual().data(); }); } else { // Sync to make sure all previous cuSparse operations are finished before setting the // potential_next_dual_solution diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index 3dd1dd7a51..460dc540ec 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -902,16 +902,12 @@ void pdlp_restart_strategy_t::cupdlpx_restart( }); // Reduce across all shards - engine->allreduce_sum_inplace_to_master( - [](pdlp_solver_t& sp) -> f_t* { - return sp.get_restart_strategy().last_restart_duality_gap_.primal_distance_traveled_.data(); - }, - stream_view_); - engine->allreduce_sum_inplace_to_master( - [](pdlp_solver_t& sp) -> f_t* { - return sp.get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(); - }, - stream_view_); + engine->allreduce_sum_inplace_to_master([](pdlp_solver_t& sp) -> f_t* { + return sp.get_restart_strategy().last_restart_duality_gap_.primal_distance_traveled_.data(); + }); + engine->allreduce_sum_inplace_to_master([](pdlp_solver_t& sp) -> f_t* { + return sp.get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(); + }); } else { distance_squared_moved_from_last_restart_period( pdhg_solver.get_potential_next_primal_solution(), From 6930cde7d18942b5cb01fa505ea6d01f57309938 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 20:47:59 +0200 Subject: [PATCH 240/258] cleaned mgpu_engine.cu --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 30 ++++++------------- .../distributed_pdlp/multi_gpu_engine.hpp | 8 ++--- cpp/src/pdlp/solve.cu | 4 --- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 56b39d7cf6..956e3f792f 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -50,16 +50,12 @@ multi_gpu_engine_t::multi_gpu_engine_t( // 3. Construct one shard per rank, pinned to its device. Ownership of each // communicator moves into its shard. - CUOPT_LOG_INFO("distributed_pdlp: building %d shard solver(s) ...", nb_parts); auto shard_build_t0 = std::chrono::high_resolution_clock::now(); for (int r = 0; r < nb_parts; ++r) { raft::device_setter guard(devices[r]); // shard ctor needs device set shards.emplace_back(std::make_unique>( devices[r], std::move(rank_data[r]), std::move(comms[r]), mps, sub_solver_settings)); } - auto shard_build_t1 = std::chrono::high_resolution_clock::now(); - CUOPT_LOG_INFO("distributed_pdlp: shard build done in %.3f s", - std::chrono::duration(shard_build_t1 - shard_build_t0).count()); // Two different events // capture_*_event_ are used inside graph capture @@ -74,14 +70,10 @@ multi_gpu_engine_t::multi_gpu_engine_t( }); // Cache per-shard partition metadata for gather_owned_*_to_master_bufs. - owned_var_sizes_.reserve(nb_parts); - owned_cstr_sizes_.reserve(nb_parts); local_to_global_vars_.reserve(nb_parts); local_to_global_cstrs_.reserve(nb_parts); for (int r = 0; r < nb_parts; ++r) { auto const& rd = shards[r]->rank_data; - owned_var_sizes_.push_back(static_cast(rd.owned_var_size)); - owned_cstr_sizes_.push_back(static_cast(rd.owned_cstr_size)); local_to_global_vars_.push_back(rd.local_to_global_var); local_to_global_cstrs_.push_back(rd.local_to_global_cstr); } @@ -140,7 +132,10 @@ void multi_gpu_engine_t::sync_await_shards(rmm::cuda_stream_view maste } } -// -------- Halo exchange ----------------------------------------------------- +// -------- Halo exchange ------------ +// typename pdlp_shard_t::halo_axis_t is the unified view of the halo exchange metadata for both variables and constraints. +// It contains the send and receive buffers for each peer, the owned size, and the receive offsets and counts for each peer. +// There is one for variables and one for constraints, on each shard. This allows to avoid duplicating the logic for variables and constraints. template void multi_gpu_engine_t::halo_exchange_bufs_impl( std::vector> const& bufs, @@ -246,26 +241,21 @@ template void multi_gpu_engine_t::gather_owned_to_master_bufs_impl( std::vector> const& shard_owned, raft::device_span master_buf, - std::vector const& owned_sizes, std::vector> const& local_to_globals) { cuopt_assert(master_pdlp_ != nullptr, "gather_owned_to_master_bufs_impl requires set_master(...)"); const int nb = static_cast(shards.size()); cuopt_expects(static_cast(shard_owned.size()) == nb && - static_cast(owned_sizes.size()) == nb && static_cast(local_to_globals.size()) == nb, error_type_t::RuntimeError, - "gather_owned_to_master_bufs_impl: shard_owned / owned_sizes / " - "local_to_globals must all have size == shards.size()"); + "gather_owned_to_master_bufs_impl: shard_owned / local_to_globals " + "must have size == shards.size()"); // Assemble on host in global-index order. std::vector h_master(master_buf.size()); for_each_shard([&](auto& s, int r) { - const std::size_t n_owned = owned_sizes[r]; - cuopt_expects(shard_owned[r].size() == n_owned, - error_type_t::RuntimeError, - "gather_owned_to_master_bufs_impl: shard_owned[r].size() must equal owned size"); + const std::size_t n_owned = shard_owned[r].size(); if (n_owned == 0) return; std::vector tmp(n_owned); raft::copy(tmp.data(), shard_owned[r].data(), n_owned, s.stream.view()); @@ -284,16 +274,14 @@ template void multi_gpu_engine_t::gather_owned_var_to_master_bufs( std::vector> const& shard_owned, raft::device_span master_buf) { - gather_owned_to_master_bufs_impl( - shard_owned, master_buf, owned_var_sizes_, local_to_global_vars_); + gather_owned_to_master_bufs_impl(shard_owned, master_buf, local_to_global_vars_); } template void multi_gpu_engine_t::gather_owned_cstr_to_master_bufs( std::vector> const& shard_owned, raft::device_span master_buf) { - gather_owned_to_master_bufs_impl( - shard_owned, master_buf, owned_cstr_sizes_, local_to_global_cstrs_); + gather_owned_to_master_bufs_impl(shard_owned, master_buf, local_to_global_cstrs_); } // -------- NCCL allreduce (sum, in place) ------------------------------------ diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index 4a0666646e..d93d41d3f7 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -224,12 +224,11 @@ struct multi_gpu_engine_t { // -------- Gather owned slices to master --------------------------------- // {var/cstr}-agnostic core: scatters each shard's owned slice into // master_buf using local_to_globals[r] as the destination index list. - // owned_sizes[r] and local_to_globals[r] are the axis-specific - // rank_data.owned_{var/cstr}_size and rank_data.local_to_global_{var/cstr} for shard r. + // local_to_globals[r] is + // the axis-specific rank_data.local_to_global_{var/cstr} for shard r. void gather_owned_to_master_bufs_impl( std::vector> const& shard_owned, raft::device_span master_buf, - std::vector const& owned_sizes, std::vector> const& local_to_globals); // -------- Gather (variables / x) ---------------------------------------- @@ -444,10 +443,7 @@ struct multi_gpu_engine_t { // Cached per-shard partition metadata, populated once at construction. // Consumed by gather_owned_*_to_master_bufs; caching avoids copying the // sizeable local_to_global_* host vectors on every termination check. - // owned_{var,cstr}_sizes_[r] == shards[r]->rank_data.owned_{var,cstr}_size // local_to_global_{vars,cstrs}_[r] == shards[r]->rank_data.local_to_global_{var,cstr} - std::vector owned_var_sizes_; - std::vector owned_cstr_sizes_; std::vector> local_to_global_vars_; std::vector> local_to_global_cstrs_; diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index a0f1a5961b..03fdaadfb5 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2509,10 +2509,6 @@ optimization_problem_solution_t solve_lp_distributed_from_mps( CUOPT_LOG_INFO("%s presolve time: %.2fs", settings_resolved.presolver == presolver_t::PSLP ? "PSLP" : "Papilo", presolve_time); - CUOPT_LOG_INFO("Distributed-solving reduced problem: %d constraints, %d variables, %d nonzeros", - host_res->reduced_problem.get_n_constraints(), - host_res->reduced_problem.get_n_variables(), - host_res->reduced_problem.get_nnz()); } // mps_for_solver is what the distributed solver actually sees. From f4ba2a74395f455276021e4d2fdbc31483c0c7a4 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 20:50:34 +0200 Subject: [PATCH 241/258] removed chrono --- cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 956e3f792f..4e268682e3 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -12,7 +12,6 @@ #include -#include #include #include @@ -50,7 +49,6 @@ multi_gpu_engine_t::multi_gpu_engine_t( // 3. Construct one shard per rank, pinned to its device. Ownership of each // communicator moves into its shard. - auto shard_build_t0 = std::chrono::high_resolution_clock::now(); for (int r = 0; r < nb_parts; ++r) { raft::device_setter guard(devices[r]); // shard ctor needs device set shards.emplace_back(std::make_unique>( From 046738b4c3ef401e3489886276b455d8620f200e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 22:35:26 +0200 Subject: [PATCH 242/258] removed logs in pdlp --- cpp/src/pdlp/pdlp.cu | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 6084bf222f..c7a660adf7 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -517,9 +517,6 @@ pdlp_solver_t::pdlp_solver_t( } // ----- 4. Build per-rank data ----- - CUOPT_LOG_INFO("distributed_pdlp: building rank_data for %d parts ...", - settings.distributed_pdlp_num_gpus); - auto rank_data_t0 = std::chrono::high_resolution_clock::now(); std::vector> sub_pdlp_rank_data = create_rank_data_from_parts(parts, h_A_row_offsets, @@ -532,9 +529,6 @@ pdlp_solver_t::pdlp_solver_t( n_cstr, n_vars, nnz); - auto rank_data_t1 = std::chrono::high_resolution_clock::now(); - CUOPT_LOG_INFO("distributed_pdlp: rank_data build done in %.3f s", - std::chrono::duration(rank_data_t1 - rank_data_t0).count()); // ----- 5. Per-shard settings ----- pdlp_solver_settings_t sub_pdlp_settings = settings; From 7201c4b814e119ca1245bf8d0bfa742dc4c3c5d9 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 23:17:58 +0200 Subject: [PATCH 243/258] updated comments and logs --- cpp/src/pdlp/distributed_pdlp/partitioner.cpp | 11 +++++++---- cpp/src/pdlp/pdlp.cu | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp index 1c2b200c38..37103edf6e 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp @@ -5,8 +5,6 @@ // Plain C++ translation unit (not .cu): this file contains no device code, and // KaMinPar's public header () is C++20 host code that pulls in TBB. -// Keeping the whole partitioner implementation out of nvcc avoids -// device-compiler friction. #include @@ -98,7 +96,8 @@ std::vector kaminpar_partitioner_t::partition( const i_t nnz = static_cast(A_cols.size()); const i_t nvtx = nb_cstr + nb_vars; - // Resolve thread count: <= 0 => all hardware threads (1 as a last resort). + // > 0: use the specified number of threads, + // <= 0: use all hardware threads (1 as a last resort). int nthreads = input.nb_threads > 0 ? static_cast(input.nb_threads) : 0; if (nthreads <= 0) { nthreads = static_cast(std::thread::hardware_concurrency()); @@ -112,17 +111,21 @@ std::vector kaminpar_partitioner_t::partition( // CSR already represents an adjency list of cstr -> variables. // Adding the transpose to represent the var -> cstr edges. // Casting the types to KaMinPar friendly types + // Put A in top right corner of adjency matrix + // Put A_t in bottom left corner of adjency matrix for (i_t i = 0; i <= nb_cstr; ++i) { xadj[i] = static_cast(A_offsets[i]); } for (i_t i = 0; i <= nb_vars; ++i) { xadj[nb_cstr + i] = - static_cast(A_t_offsets[i]) + static_cast(nnz); + static_cast(A_t_offsets[i]) + static_cast(nb_cstr); } + // cstr node/row has value in index J <=> link current cstr node with var node J for (i_t k = 0; k < nnz; ++k) { adjncy[k] = static_cast(A_cols[k]) + static_cast(nb_cstr); } + // same as right above but reversed for (i_t k = 0; k < nnz; ++k) { adjncy[nnz + k] = static_cast(A_t_cols[k]); } diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index c7a660adf7..56a5b1221c 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -406,7 +406,7 @@ pdlp_solver_t::pdlp_solver_t( error_type_t::ValidationError, "Distributed PDLP requires never_restart_to_average = true"); const int distributed_pdlp_num_gpus = settings.distributed_pdlp_num_gpus; - CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU (mps direct path)", + CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU.", distributed_pdlp_num_gpus); if constexpr (!std::is_same_v) { From 176de0c354954fad95186ae7ddcf244bb72381f7 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sat, 11 Jul 2026 23:21:55 +0200 Subject: [PATCH 244/258] style --- .../pdlp/distributed_pdlp/multi_gpu_engine.cu | 18 ++++++++++-------- .../pdlp/distributed_pdlp/multi_gpu_engine.hpp | 13 ++++++++----- cpp/src/pdlp/distributed_pdlp/partitioner.cpp | 6 +++--- cpp/src/pdlp/pdlp.cu | 3 +-- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu index 4e268682e3..f651c1cea9 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.cu @@ -131,9 +131,11 @@ void multi_gpu_engine_t::sync_await_shards(rmm::cuda_stream_view maste } // -------- Halo exchange ------------ -// typename pdlp_shard_t::halo_axis_t is the unified view of the halo exchange metadata for both variables and constraints. -// It contains the send and receive buffers for each peer, the owned size, and the receive offsets and counts for each peer. -// There is one for variables and one for constraints, on each shard. This allows to avoid duplicating the logic for variables and constraints. +// typename pdlp_shard_t::halo_axis_t is the unified view of the halo exchange metadata +// for both variables and constraints. It contains the send and receive buffers for each peer, the +// owned size, and the receive offsets and counts for each peer. There is one for variables and one +// for constraints, on each shard. This allows to avoid duplicating the logic for variables and +// constraints. template void multi_gpu_engine_t::halo_exchange_bufs_impl( std::vector> const& bufs, @@ -244,11 +246,11 @@ void multi_gpu_engine_t::gather_owned_to_master_bufs_impl( cuopt_assert(master_pdlp_ != nullptr, "gather_owned_to_master_bufs_impl requires set_master(...)"); const int nb = static_cast(shards.size()); - cuopt_expects(static_cast(shard_owned.size()) == nb && - static_cast(local_to_globals.size()) == nb, - error_type_t::RuntimeError, - "gather_owned_to_master_bufs_impl: shard_owned / local_to_globals " - "must have size == shards.size()"); + cuopt_expects( + static_cast(shard_owned.size()) == nb && static_cast(local_to_globals.size()) == nb, + error_type_t::RuntimeError, + "gather_owned_to_master_bufs_impl: shard_owned / local_to_globals " + "must have size == shards.size()"); // Assemble on host in global-index order. std::vector h_master(master_buf.size()); diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index d93d41d3f7..d07787bacd 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -79,14 +79,15 @@ struct multi_gpu_engine_t { for (int r = 0; r < static_cast(shards.size()); ++r) { auto& s = *shards[r]; raft::device_setter guard(s.device_id); - // If the function is invocable with a pdlp_shard_t& and an int, call it with the shard and the rank. + // If the function is invocable with a pdlp_shard_t& and an int, call it with the + // shard and the rank. if constexpr (std::is_invocable_v&, int>) { fn(s, r); - // If the function is invocable only with a pdlp_shard_t&, call it with the shard. + // If the function is invocable only with a pdlp_shard_t&, call it with the shard. } else if constexpr (std::is_invocable_v&>) { fn(s); } else { - // Otherwise, the function has an invalid signature. + // Otherwise, the function has an invalid signature. cuopt_expects( false, error_type_t::RuntimeError, "for_each_shard: invalid function signature"); } @@ -335,7 +336,8 @@ struct multi_gpu_engine_t { void distributed_l2_norm_bufs(std::vector> const& in_bufs, std::vector> const& out_scalars); - // Overload: same rationale as distributed_dot_bufs above. Allows to use rmm::device_scalar directly. + // Overload: same rationale as distributed_dot_bufs above. Allows to use rmm::device_scalar + // directly. void distributed_l2_norm_bufs(std::vector> const& in_bufs, std::vector>& out_scalars); @@ -392,7 +394,8 @@ struct multi_gpu_engine_t { void distributed_bound_objective_rescaling(f_t c_scaling_weight); // Distributed Ruiz inf-scaling (num_iter passes). Each shard computes both its - // owned-row and owned-column inf-norms locally then broadcasts the cumulative scalings to all shards. + // owned-row and owned-column inf-norms locally then broadcasts the cumulative scalings to all + // shards. void distributed_ruiz_inf_scaling(int num_iter, i_t n_global_vars); // Distributed Pock-Chambolle scaling (one pass), mirroring the single-GPU diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp index 37103edf6e..4a1dbf1194 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp @@ -117,10 +117,10 @@ std::vector kaminpar_partitioner_t::partition( xadj[i] = static_cast(A_offsets[i]); } for (i_t i = 0; i <= nb_vars; ++i) { - xadj[nb_cstr + i] = - static_cast(A_t_offsets[i]) + static_cast(nb_cstr); + xadj[nb_cstr + i] = static_cast(A_t_offsets[i]) + + static_cast(nb_cstr); } - // cstr node/row has value in index J <=> link current cstr node with var node J + // cstr node/row has value in index J <=> link current cstr node with var node J for (i_t k = 0; k < nnz; ++k) { adjncy[k] = static_cast(A_cols[k]) + static_cast(nb_cstr); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index 56a5b1221c..f8380d059d 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -406,8 +406,7 @@ pdlp_solver_t::pdlp_solver_t( error_type_t::ValidationError, "Distributed PDLP requires never_restart_to_average = true"); const int distributed_pdlp_num_gpus = settings.distributed_pdlp_num_gpus; - CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU.", - distributed_pdlp_num_gpus); + CUOPT_LOG_INFO("Solving with distributed PDLP on %d GPU.", distributed_pdlp_num_gpus); if constexpr (!std::is_same_v) { cuopt_expects( From 115b90dda6e132df5e0f22fddaa7bcf3ab34f6c1 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 15:01:54 +0200 Subject: [PATCH 245/258] added NCCL_TRY no throw --- cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp | 15 +++++++++++++++ cpp/src/pdlp/distributed_pdlp/shard.hpp | 6 ++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp index eec034bd6d..6676e43a50 100644 --- a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp +++ b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp @@ -5,6 +5,7 @@ #pragma once #include +#include #include @@ -23,4 +24,18 @@ namespace cuopt::mathematical_optimization::pdlp { ::ncclGetErrorString(_cuopt_nccl_status)); \ } while (0) +// Non-throwing variant: logs at ERROR level on failure and swallows the error. +// Intended for noexcept contexts (destructors, unique_ptr deleters, teardown) +// where throwing would call std::terminate. Mirrors RAFT_CUDA_TRY_NO_THROW. +#define CUOPT_NCCL_TRY_NO_THROW(call) \ + do { \ + ::ncclResult_t const _cuopt_nccl_status = (call); \ + if (_cuopt_nccl_status != ncclSuccess) { \ + CUOPT_LOG_ERROR("NCCL error at %s:%d: %s", \ + __FILE__, \ + __LINE__, \ + ::ncclGetErrorString(_cuopt_nccl_status)); \ + } \ + } while (0) + } // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 5bc76a4a64..7d6fc58736 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -4,6 +4,7 @@ */ #pragma once +#include #include #include @@ -37,7 +38,7 @@ struct nccl_comm_deleter_t { { if (comm == nullptr) return; raft::device_setter guard(device_id); - ncclCommDestroy(comm); + CUOPT_NCCL_TRY_NO_THROW(ncclCommDestroy(comm)); } }; using nccl_comm_unique_ptr_t = std::unique_ptr; @@ -57,9 +58,6 @@ struct pdlp_shard_t { pdlp_shard_t(const pdlp_shard_t&) = delete; pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; - // Move ops are implicitly deleted - // Intentional: shard owns device-affine resources and must never move. - // Store as std::unique_ptr in any container. int device_id; rmm::cuda_stream stream; From e7e129244b62b6292b8c51add21cc934b2fd26a3 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 15:35:07 +0200 Subject: [PATCH 246/258] move opt_problem out of optionnal --- cpp/src/pdlp/distributed_pdlp/shard.cu | 37 ++++++++++++------------- cpp/src/pdlp/distributed_pdlp/shard.hpp | 6 ++-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 02430ac414..2c06c5f975 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -33,7 +33,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, handle(stream.view()), comm(std::move(comm)), rank_data(std::move(rd)), - opt_problem(std::nullopt), + opt_problem(&handle), sub_problem(std::nullopt), sub_pdlp(nullptr) { @@ -76,31 +76,30 @@ pdlp_shard_t::pdlp_shard_t(int device_id, h_cstr_upper[i] = g_cstr_upper[g]; } - // ---- 2. Build optimization_problem_t on this shard's device (UNSCALED). ---- - opt_problem.emplace(&handle); - opt_problem->set_csr_constraint_matrix(rank_data.h_A_values.data(), - static_cast(rank_data.h_A_values.size()), - rank_data.h_A_col_indices.data(), - static_cast(rank_data.h_A_col_indices.size()), - rank_data.h_A_row_offsets.data(), - static_cast(rank_data.h_A_row_offsets.size())); + // ---- 2. Populate opt_problem (constructed in init list) on this shard's device (UNSCALED). ---- + opt_problem.set_csr_constraint_matrix(rank_data.h_A_values.data(), + static_cast(rank_data.h_A_values.size()), + rank_data.h_A_col_indices.data(), + static_cast(rank_data.h_A_col_indices.size()), + rank_data.h_A_row_offsets.data(), + static_cast(rank_data.h_A_row_offsets.size())); // Primal axis: TOTAL (owned + halo). Halo slots have neutral defaults. - opt_problem->set_objective_coefficients(h_obj.data(), rank_data.total_var_size); - opt_problem->set_variable_lower_bounds(h_var_lower.data(), rank_data.total_var_size); - opt_problem->set_variable_upper_bounds(h_var_upper.data(), rank_data.total_var_size); + opt_problem.set_objective_coefficients(h_obj.data(), rank_data.total_var_size); + opt_problem.set_variable_lower_bounds(h_var_lower.data(), rank_data.total_var_size); + opt_problem.set_variable_upper_bounds(h_var_upper.data(), rank_data.total_var_size); // Dual axis: TOTAL (owned + halo). Halo slots have ±inf so trivially satisfied. - opt_problem->set_constraint_lower_bounds(h_cstr_lower.data(), rank_data.total_cstr_size); - opt_problem->set_constraint_upper_bounds(h_cstr_upper.data(), rank_data.total_cstr_size); + opt_problem.set_constraint_lower_bounds(h_cstr_lower.data(), rank_data.total_cstr_size); + opt_problem.set_constraint_upper_bounds(h_cstr_upper.data(), rank_data.total_cstr_size); - opt_problem->set_maximize(maximize); - opt_problem->set_objective_offset(objective_offset); - opt_problem->set_objective_scaling_factor(objective_scaling_factor); - opt_problem->set_problem_category(problem_category_t::LP); + opt_problem.set_maximize(maximize); + opt_problem.set_objective_offset(objective_offset); + opt_problem.set_objective_scaling_factor(objective_scaling_factor); + opt_problem.set_problem_category(problem_category_t::LP); // ---- 3. Build problem_t from opt_problem (UNSCALED). ---- - sub_problem.emplace(*opt_problem); + sub_problem.emplace(opt_problem); // ---- 4. Override reverse_* with the real local A_T from rank_data. ---- // problem_t's ctor computes the transpose of the LOCAL A, which is wrong diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 7d6fc58736..7d11bdedf6 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -37,6 +38,7 @@ struct nccl_comm_deleter_t { void operator()(ncclComm* comm) const noexcept { if (comm == nullptr) return; + cuopt_assert(device_id >= 0, "nccl_comm_deleter_t: device_id not set"); raft::device_setter guard(device_id); CUOPT_NCCL_TRY_NO_THROW(ncclCommDestroy(comm)); } @@ -59,12 +61,12 @@ struct pdlp_shard_t { pdlp_shard_t(const pdlp_shard_t&) = delete; pdlp_shard_t& operator=(const pdlp_shard_t&) = delete; - int device_id; + int device_id{-1}; rmm::cuda_stream stream; raft::handle_t handle; nccl_comm_unique_ptr_t comm; rank_data_t rank_data; - std::optional> opt_problem; + optimization_problem_t opt_problem; std::optional> sub_problem; std::unique_ptr> sub_pdlp; From cdc0c6607157ef98cfa2f26ad9bd8097c5bfa93b Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 15:54:19 +0200 Subject: [PATCH 247/258] added sync stream in loop to prevent destruction before finish of rmm vector --- cpp/src/pdlp/distributed_pdlp/shard.cu | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 2c06c5f975..dce1bd9fd6 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -192,6 +192,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, } indices_d.emplace_back(std::move(idx)); buf_d.emplace_back(std::move(buf)); + handle.sync_stream(stream_view); } }; build_send_plan(rank_data.var_send_per_peer, var_send_indices_d, var_send_buf_d); From 0d3e9005789a0d9166b7a24061b1e7b1333f40ce Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 16:40:59 +0200 Subject: [PATCH 248/258] plumb down is_sub_pdlp to initial_scaling in order to disable scaling --- cpp/src/pdlp/distributed_pdlp/shard.cu | 54 +++---------------- .../initial_scaling.cu | 8 ++- .../initial_scaling.cuh | 7 ++- cpp/src/pdlp/pdlp.cu | 4 +- 4 files changed, 23 insertions(+), 50 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index dce1bd9fd6..96857b2134 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -124,55 +124,17 @@ pdlp_shard_t::pdlp_shard_t(int device_id, handle.sync_stream(stream_view); // ---- 5. Build sub_pdlp (single-GPU mode). ---- - // is_distributed_sub_pdlp=true tells the ctor to skip the CSR/CSC transpose - // validity check as A and A_T here are two independent local slices, not transposes - // A has all the owned rows and A_T has all the owned columns. + // is_distributed_sub_pdlp=true has two effects in pdlp_solver_t's ctor: + // * skip the CSR/CSC transpose validity check -- A and A_T here are two + // independent local slices, not transposes (A has all owned rows and + // A_T has all owned columns). + // * skip local Ruiz / Pock-Chambolle inside initial_scaling_strategy_'s + // ctor -- distributed scaling (multi_gpu_engine_t::distributed_scaling) + // runs a cross-shard-coherent scaling later. Local per-shard scaling + // would be incoherent across shards. sub_pdlp = std::make_unique>( *sub_problem, settings, /*is_legacy_batch_mode=*/false, /*is_distributed_sub_pdlp=*/true); - // Inject this shard's unscaled buffers into op_problem_scaled (distributed - // scaling runs later and will scale them). - auto& scaled = sub_pdlp->get_op_problem_scaled(); - raft::copy(scaled.offsets.data(), - rank_data.h_A_row_offsets.data(), - rank_data.h_A_row_offsets.size(), - stream_view); - raft::copy(scaled.variables.data(), - rank_data.h_A_col_indices.data(), - rank_data.h_A_col_indices.size(), - stream_view); - raft::copy(scaled.coefficients.data(), - rank_data.h_A_values.data(), - rank_data.h_A_values.size(), - stream_view); - raft::copy(scaled.reverse_coefficients.data(), - rank_data.h_A_t_values.data(), - rank_data.h_A_t_values.size(), - stream_view); - // Use sub_problem's coefficients (already negated for max by - // convert_to_maximization_problem) so scaled matches the solver-facing form. - raft::copy(scaled.objective_coefficients.data(), - sub_problem->objective_coefficients.data(), - sub_problem->objective_coefficients.size(), - stream_view); - raft::copy( - scaled.constraint_lower_bounds.data(), h_cstr_lower.data(), h_cstr_lower.size(), stream_view); - raft::copy( - scaled.constraint_upper_bounds.data(), h_cstr_upper.data(), h_cstr_upper.size(), stream_view); - - using f_t2 = typename type_2::type; - std::vector h_var_bounds_packed(rank_data.total_var_size); - for (i_t i = 0; i < rank_data.total_var_size; ++i) { - h_var_bounds_packed[i].x = h_var_lower[i]; - h_var_bounds_packed[i].y = h_var_upper[i]; - } - raft::copy(scaled.variable_bounds.data(), - h_var_bounds_packed.data(), - h_var_bounds_packed.size(), - stream_view); - - combine_constraint_bounds(scaled, scaled.combined_bounds); - sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( /* is_reflected */ true); // ---- 6. Build per-peer halo-exchange plans ---- diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 9daae756cc..c38350c569 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -68,7 +68,8 @@ pdlp_initial_scaling_strategy_t::pdlp_initial_scaling_strategy_t( pdhg_solver_t* pdhg_solver_ptr, const pdlp::pdlp_hyper_params_t& hyper_params, i_t original_batch_size, - bool running_mip) + bool running_mip, + bool skip_initial_scaling) : handle_ptr_(handle_ptr), stream_view_(handle_ptr_->get_stream()), primal_size_h_(op_problem_scaled.n_variables), @@ -119,7 +120,10 @@ pdlp_initial_scaling_strategy_t::pdlp_initial_scaling_strategy_t( objective_rescaling_.end(), f_t(1)); - compute_scaling_vectors(number_of_ruiz_iterations, alpha); + // Distributed PDLP shards defer scaling to multi_gpu_engine_t::distributed_scaling, + // which runs a cross-shard-coherent Ruiz. Local per-shard Ruiz would be incoherent + // across shards, so skip it here. + if (!skip_initial_scaling) { compute_scaling_vectors(number_of_ruiz_iterations, alpha); } } template diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 0599121114..e016ac6d2f 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -46,6 +46,10 @@ class pdlp_initial_scaling_strategy_t { raft::device_span cummulative_variable_scaling; }; // struct view_t + // skip_initial_scaling: when true, the ctor performs identity-initialization + // of the scaling vectors but does NOT run local Ruiz / Pock-Chambolle. Used + // by distributed PDLP shards, where cross-shard-coherent scaling is applied + // later by multi_gpu_engine_t::distributed_scaling. pdlp_initial_scaling_strategy_t(raft::handle_t const* handle_ptr, mip::problem_t& op_problem_scaled, i_t number_of_ruiz_iterations, @@ -56,7 +60,8 @@ class pdlp_initial_scaling_strategy_t { pdhg_solver_t* pdhg_solver_ptr, const pdlp::pdlp_hyper_params_t& hyper_params, i_t original_batch_size, - bool running_mip = false); + bool running_mip = false, + bool skip_initial_scaling = false); void scale_problem(); diff --git a/cpp/src/pdlp/pdlp.cu b/cpp/src/pdlp/pdlp.cu index f8380d059d..488a2bcc6b 100644 --- a/cpp/src/pdlp/pdlp.cu +++ b/cpp/src/pdlp/pdlp.cu @@ -205,7 +205,9 @@ pdlp_solver_t::pdlp_solver_t(mip::problem_t& op_problem, op_problem_scaled_.reverse_constraints, &pdhg_solver_, settings_.hyper_params, - static_cast(original_batch_size_)}, + static_cast(original_batch_size_), + /*running_mip=*/false, + /*skip_initial_scaling=*/is_distributed_sub_pdlp}, average_op_problem_evaluation_cusparse_view_{handle_ptr_, op_problem, unscaled_primal_avg_solution_, From 7e3e662045c46becf57d8196dece3d578b04f652 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 16:41:26 +0200 Subject: [PATCH 249/258] style --- cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp | 16 +++++++--------- .../initial_scaling_strategy/initial_scaling.cuh | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp index 6676e43a50..48d7bd8b66 100644 --- a/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp +++ b/cpp/src/pdlp/distributed_pdlp/nccl_helpers.hpp @@ -27,15 +27,13 @@ namespace cuopt::mathematical_optimization::pdlp { // Non-throwing variant: logs at ERROR level on failure and swallows the error. // Intended for noexcept contexts (destructors, unique_ptr deleters, teardown) // where throwing would call std::terminate. Mirrors RAFT_CUDA_TRY_NO_THROW. -#define CUOPT_NCCL_TRY_NO_THROW(call) \ - do { \ - ::ncclResult_t const _cuopt_nccl_status = (call); \ - if (_cuopt_nccl_status != ncclSuccess) { \ - CUOPT_LOG_ERROR("NCCL error at %s:%d: %s", \ - __FILE__, \ - __LINE__, \ - ::ncclGetErrorString(_cuopt_nccl_status)); \ - } \ +#define CUOPT_NCCL_TRY_NO_THROW(call) \ + do { \ + ::ncclResult_t const _cuopt_nccl_status = (call); \ + if (_cuopt_nccl_status != ncclSuccess) { \ + CUOPT_LOG_ERROR( \ + "NCCL error at %s:%d: %s", __FILE__, __LINE__, ::ncclGetErrorString(_cuopt_nccl_status)); \ + } \ } while (0) } // namespace cuopt::mathematical_optimization::pdlp diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index e016ac6d2f..26a6c6f5ec 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -60,7 +60,7 @@ class pdlp_initial_scaling_strategy_t { pdhg_solver_t* pdhg_solver_ptr, const pdlp::pdlp_hyper_params_t& hyper_params, i_t original_batch_size, - bool running_mip = false, + bool running_mip = false, bool skip_initial_scaling = false); void scale_problem(); From 6bb29b681d136644e00c04783d5cbe6cec811d09 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 17:07:19 +0200 Subject: [PATCH 250/258] final review of shard.* !!!! --- cpp/src/pdlp/distributed_pdlp/shard.cu | 26 ++++++++++++------------- cpp/src/pdlp/distributed_pdlp/shard.hpp | 3 +-- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 96857b2134..3618d0fb43 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -57,7 +57,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, // ---- 1. Gather per-shard host slices using rank_data's index maps. ---- // All vectors are sized to TOTAL (owned + halo). Owned slots get real - // values; halo slots keep defaults because they should not be accessed. + // values; halo slots keep defaults because they should not be accessed before getting filled. std::vector h_obj(rank_data.total_var_size, f_t{0}); std::vector h_var_lower(rank_data.total_var_size, -std::numeric_limits::infinity()); std::vector h_var_upper(rank_data.total_var_size, std::numeric_limits::infinity()); @@ -76,7 +76,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, h_cstr_upper[i] = g_cstr_upper[g]; } - // ---- 2. Populate opt_problem (constructed in init list) on this shard's device (UNSCALED). ---- + // ---- 2. Populate opt_problem (constructed in init list) on this shard's device. ---- opt_problem.set_csr_constraint_matrix(rank_data.h_A_values.data(), static_cast(rank_data.h_A_values.size()), rank_data.h_A_col_indices.data(), @@ -84,12 +84,12 @@ pdlp_shard_t::pdlp_shard_t(int device_id, rank_data.h_A_row_offsets.data(), static_cast(rank_data.h_A_row_offsets.size())); - // Primal axis: TOTAL (owned + halo). Halo slots have neutral defaults. + // Primal axis: TOTAL (owned + halo) opt_problem.set_objective_coefficients(h_obj.data(), rank_data.total_var_size); opt_problem.set_variable_lower_bounds(h_var_lower.data(), rank_data.total_var_size); opt_problem.set_variable_upper_bounds(h_var_upper.data(), rank_data.total_var_size); - // Dual axis: TOTAL (owned + halo). Halo slots have ±inf so trivially satisfied. + // Dual axis: TOTAL (owned + halo) opt_problem.set_constraint_lower_bounds(h_cstr_lower.data(), rank_data.total_cstr_size); opt_problem.set_constraint_upper_bounds(h_cstr_upper.data(), rank_data.total_cstr_size); @@ -135,26 +135,24 @@ pdlp_shard_t::pdlp_shard_t(int device_id, sub_pdlp = std::make_unique>( *sub_problem, settings, /*is_legacy_batch_mode=*/false, /*is_distributed_sub_pdlp=*/true); - sub_pdlp->pdhg_solver_.get_cusparse_view().create_spmv_op_plans( - /* is_reflected */ true); // ---- 6. Build per-peer halo-exchange plans ---- // For each peer p, we precompute: // send_indices_d[p] : local indices to gather (uploaded from host send plan) // send_buf_d[p] : f_t staging buffer sized to match - // Self-peer slot is present but empty (size 0). Used in engine halo exchange. - auto build_send_plan = [&](auto const& send_per_peer, auto& indices_d, auto& buf_d) { + // Self-peer slot is present but empty (size 0). + auto build_send_plan = [&](std::vector> const& send_per_peer, + std::vector>& indices_d, + std::vector>& buf_d) { const std::size_t n_peers = send_per_peer.size(); indices_d.reserve(n_peers); buf_d.reserve(n_peers); for (auto const& send_to_peer : send_per_peer) { - rmm::device_uvector idx(send_to_peer.size(), stream_view); - rmm::device_uvector buf(send_to_peer.size(), stream_view); + indices_d.emplace_back(send_to_peer.size(), stream_view); + buf_d.emplace_back(send_to_peer.size(), stream_view); if (!send_to_peer.empty()) { - raft::copy(idx.data(), send_to_peer.data(), send_to_peer.size(), stream_view); + raft::copy( + indices_d.back().data(), send_to_peer.data(), send_to_peer.size(), stream_view); } - indices_d.emplace_back(std::move(idx)); - buf_d.emplace_back(std::move(buf)); - handle.sync_stream(stream_view); } }; build_send_plan(rank_data.var_send_per_peer, var_send_indices_d, var_send_buf_d); diff --git a/cpp/src/pdlp/distributed_pdlp/shard.hpp b/cpp/src/pdlp/distributed_pdlp/shard.hpp index 7d11bdedf6..dcca723c3e 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.hpp +++ b/cpp/src/pdlp/distributed_pdlp/shard.hpp @@ -79,8 +79,7 @@ struct pdlp_shard_t { std::vector> cstr_send_buf_d; // Non-owning bundle of per-axis halo-exchange metadata, indexed by peer. - // Consumed by multi_gpu_engine_t::halo_exchange_bufs_impl (one axis vector - // per axis is cached in the engine at construction). + // Consumed by multi_gpu_engine_t::halo_exchange_bufs_impl struct halo_axis_t { std::vector>& send_indices; // [peer] std::vector>& send_buf; // [peer] From 0a06c200382008f79268082a7f3b00a0a543c1eb Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 17:10:16 +0200 Subject: [PATCH 251/258] style --- cpp/src/pdlp/distributed_pdlp/shard.cu | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/shard.cu b/cpp/src/pdlp/distributed_pdlp/shard.cu index 3618d0fb43..2bb4c5da6e 100644 --- a/cpp/src/pdlp/distributed_pdlp/shard.cu +++ b/cpp/src/pdlp/distributed_pdlp/shard.cu @@ -150,8 +150,7 @@ pdlp_shard_t::pdlp_shard_t(int device_id, indices_d.emplace_back(send_to_peer.size(), stream_view); buf_d.emplace_back(send_to_peer.size(), stream_view); if (!send_to_peer.empty()) { - raft::copy( - indices_d.back().data(), send_to_peer.data(), send_to_peer.size(), stream_view); + raft::copy(indices_d.back().data(), send_to_peer.data(), send_to_peer.size(), stream_view); } } }; From 410014cadf4841b8e1affacc51f9f28744572aab Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 17:38:50 +0200 Subject: [PATCH 252/258] removed dead code in initial scaling --- .../initial_scaling.cu | 42 +++++-------------- .../initial_scaling.cuh | 4 -- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index c38350c569..49baa56072 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -230,13 +230,13 @@ __global__ void inf_norm_col_kernel( template void pdlp_initial_scaling_strategy_t::ruiz_iter_local() { + // Reset the iteration_scaling vectors to all 0 RAFT_CUDA_TRY(cudaMemsetAsync( iteration_constraint_matrix_scaling_.data(), 0, sizeof(f_t) * dual_size_h_, stream_view_)); RAFT_CUDA_TRY(cudaMemsetAsync( iteration_variable_scaling_.data(), 0, sizeof(f_t) * primal_size_h_, stream_view_)); - // Inf-norm over rows (owned rows, from row-major A) and columns (owned - // columns, from A_T). Split into two kernels so the distributed path can + // Inf-norm over rows and columns. Split into two kernels so the distributed path can // touch only owned entries. // Reading cols data from A_t allows for better cache locality on the AtomicAdd // than it would by reading cols data from A as it is csr-represented => scattered cols @@ -247,13 +247,15 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_local() op_problem_scaled_.view(), this->view()); RAFT_CUDA_TRY(cudaPeekAtLastError()); - inf_norm_col_kernel - <<>>( - op_problem_scaled_.view(), - this->view(), - A_T_.data(), - A_T_offsets_.data(), - A_T_indices_.data()); + i_t number_of_blocks_col = op_problem_scaled_.n_variables / block_size; + if (op_problem_scaled_.n_variables % block_size) number_of_blocks_col++; + i_t number_of_threads_col = std::min(op_problem_scaled_.n_constraints, (i_t)block_size); + inf_norm_col_kernel<<>>( + op_problem_scaled_.view(), + this->view(), + A_T_.data(), + A_T_offsets_.data(), + A_T_indices_.data()); RAFT_CUDA_TRY(cudaPeekAtLastError()); if (running_mip_) { reset_integer_variables(); } @@ -969,28 +971,6 @@ pdlp_initial_scaling_strategy_t::get_variable_scaling_vector() const return cummulative_variable_scaling_; } -template -void pdlp_initial_scaling_strategy_t::set_cummulative_scaling( - const std::vector& h_cummulative_constraint_matrix_scaling, - const std::vector& h_cummulative_variable_scaling) -{ - cuopt_expects(static_cast(h_cummulative_constraint_matrix_scaling.size()) == dual_size_h_, - error_type_t::ValidationError, - "set_cummulative_scaling: host constraint scaling vector size mismatch"); - cuopt_expects(static_cast(h_cummulative_variable_scaling.size()) == primal_size_h_, - error_type_t::ValidationError, - "set_cummulative_scaling: host variable scaling vector size mismatch"); - - raft::copy(cummulative_constraint_matrix_scaling_.data(), - h_cummulative_constraint_matrix_scaling.data(), - h_cummulative_constraint_matrix_scaling.size(), - stream_view_); - raft::copy(cummulative_variable_scaling_.data(), - h_cummulative_variable_scaling.data(), - h_cummulative_variable_scaling.size(), - stream_view_); -} - template void pdlp_initial_scaling_strategy_t::set_h_bound_rescaling(f_t value) { diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 26a6c6f5ec..41b80041cc 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -101,10 +101,6 @@ class pdlp_initial_scaling_strategy_t { void swap_context(const thrust::universal_host_pinned_vector>& swap_pairs); void resize_context(i_t new_size); - // Inject scaling state computed by another pdlp_initial_scaling_strategy_t - // Needed by distributed PDLP - void set_cummulative_scaling(const std::vector& h_cummulative_constraint_matrix_scaling, - const std::vector& h_cummulative_variable_scaling); void set_h_bound_rescaling(f_t value); void set_h_objective_rescaling(f_t value); From 16c88e5e8b61bf57ef7b0a5c12169d3e691412d7 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 17:44:42 +0200 Subject: [PATCH 253/258] updated comments in initial scaling. done !! --- cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh index 41b80041cc..bcda2c6f93 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh @@ -122,10 +122,7 @@ class pdlp_initial_scaling_strategy_t { // accumulated to A, A_T, c, variable bounds and constraint bounds, mark // the problem as scaled and scale the seed primal/dual solutions. // scale_problem() = apply_cummulative_scaling_to_problem() + local - // bound/objective rescaling. Distributed PDLP calls this directly and - // then invokes distributed_bound_objective_rescaling() to apply the - // GLOBAL (allreduced) bound/objective factors instead of the shard-local - // ones. + // bound/objective rescaling void apply_cummulative_scaling_to_problem(); // One Ruiz iteration (compute iteration vectors + fold into From b41bd8093bb83393114a79b2150edb5f79d6a17e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 18:05:05 +0200 Subject: [PATCH 254/258] style --- cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu index 49baa56072..51ca2e305a 100644 --- a/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu +++ b/cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu @@ -251,11 +251,7 @@ void pdlp_initial_scaling_strategy_t::ruiz_iter_local() if (op_problem_scaled_.n_variables % block_size) number_of_blocks_col++; i_t number_of_threads_col = std::min(op_problem_scaled_.n_constraints, (i_t)block_size); inf_norm_col_kernel<<>>( - op_problem_scaled_.view(), - this->view(), - A_T_.data(), - A_T_offsets_.data(), - A_T_indices_.data()); + op_problem_scaled_.view(), this->view(), A_T_.data(), A_T_offsets_.data(), A_T_indices_.data()); RAFT_CUDA_TRY(cudaPeekAtLastError()); if (running_mip_) { reset_integer_variables(); } From 54555766c83940bef0dd1f3032564c49ba7ab16d Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 09:26:15 -0700 Subject: [PATCH 255/258] fixed PERNICIOUS bug in partition building --- cpp/src/pdlp/distributed_pdlp/partitioner.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp index 4a1dbf1194..cf5f4f7aca 100644 --- a/cpp/src/pdlp/distributed_pdlp/partitioner.cpp +++ b/cpp/src/pdlp/distributed_pdlp/partitioner.cpp @@ -117,8 +117,11 @@ std::vector kaminpar_partitioner_t::partition( xadj[i] = static_cast(A_offsets[i]); } for (i_t i = 0; i <= nb_vars; ++i) { + // A_t edges live in adjncy[nnz .. 2*nnz), so their CSR offsets start at nnz + // (NOT nb_cstr). Corrupting this made xadj[nvtx] = nnz + nb_cstr instead of + // 2*nnz, causing KaMinPar to under-allocate and stomp the heap. xadj[nb_cstr + i] = static_cast(A_t_offsets[i]) + - static_cast(nb_cstr); + static_cast(nnz); } // cstr node/row has value in index J <=> link current cstr node with var node J for (i_t k = 0; k < nnz; ++k) { From 2a3d2b0ef95441f1d441acad55581628f1cc892e Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 18:52:19 +0200 Subject: [PATCH 256/258] added set_scalar_on_master_and_shards for distr algo and restart strategy --- .../distributed_algorithms.cu | 29 +++---------- .../distributed_pdlp/multi_gpu_engine.hpp | 17 ++++++++ .../restart_strategy/pdlp_restart_strategy.cu | 43 +++++++------------ 3 files changed, 38 insertions(+), 51 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index c52fab5940..6c51bd92e2 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -367,18 +367,10 @@ void multi_gpu_engine_t::distributed_compute_initial_step_size( const f_t sigma_max_sq = distributed_max_singular_value_squared(n_global_cstrs, max_iterations, tolerance); - auto& master = *master_pdlp_; - auto* handle_ptr = master.get_handle_ptr(); - auto stream_view = handle_ptr->get_stream(); - const f_t h_step_size = scaling_factor / std::sqrt(sigma_max_sq); - raft::copy(master.get_step_size().data(), &h_step_size, 1, stream_view); - for_each_shard([&](auto& shard) { - raft::copy(shard.sub_pdlp->get_step_size().data(), &h_step_size, 1, shard.stream); - }); - sync_await_shards(stream_view); - handle_ptr->sync_stream(stream_view); + set_scalar_on_master_and_shards(h_step_size, + [](auto& sp) { return sp.get_step_size().data(); }); } // -------- Distributed initial primal weight ------------------------------ @@ -405,19 +397,10 @@ void multi_gpu_engine_t::distributed_compute_initial_primal_weight( "earlier in solve_lp_distributed_from_mps."); const f_t h_primal_weight = f_t(1); - auto& master = *master_pdlp_; - auto* handle_ptr = master.get_handle_ptr(); - auto stream_view = handle_ptr->get_stream(); - - raft::copy(master.get_primal_weight().data(), &h_primal_weight, 1, stream_view); - raft::copy(master.get_best_primal_weight().data(), &h_primal_weight, 1, stream_view); - for_each_shard([&](auto& shard) { - auto& sub = *shard.sub_pdlp; - raft::copy(sub.get_primal_weight().data(), &h_primal_weight, 1, shard.stream); - raft::copy(sub.get_best_primal_weight().data(), &h_primal_weight, 1, shard.stream); - }); - sync_await_shards(stream_view); - handle_ptr->sync_stream(stream_view); + set_scalar_on_master_and_shards(h_primal_weight, + [](auto& sp) { return sp.get_primal_weight().data(); }); + set_scalar_on_master_and_shards(h_primal_weight, + [](auto& sp) { return sp.get_best_primal_weight().data(); }); } // ----- Explicit instantiations (member-by-member) -------------------------- diff --git a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp index d07787bacd..5637576eee 100644 --- a/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp +++ b/cpp/src/pdlp/distributed_pdlp/multi_gpu_engine.hpp @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -316,6 +317,22 @@ struct multi_gpu_engine_t { shard_scalars, raft::make_device_scalar_view(ptr_access(*master_pdlp_))); } + // -------- Set a host scalar on master AND every shard ------------------- + // Writes a host-side value to master and every shard through `ptr_access`. + template + void set_scalar_on_master_and_shards(f_t value, PtrAccess&& ptr_access) + { + cuopt_assert(master_pdlp_ != nullptr, + "set_scalar_on_master_and_shards requires set_master(...) to have been called"); + auto master_stream = master_pdlp_->get_handle_ptr()->get_stream(); + raft::copy(ptr_access(*master_pdlp_), &value, 1, master_stream); + for_each_shard([&](auto& shard) { + raft::copy(ptr_access(*shard.sub_pdlp), &value, 1, shard.stream.view()); + }); + master_pdlp_->get_handle_ptr()->sync_stream(master_stream); + synchronize_shards(); + } + // -------- Distributed dot / L2 norm ------------------------------------- // Computes the dot product of two vectors for each shard. Returns the global result in // out_scalars. diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index 460dc540ec..92939686e2 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -970,36 +970,23 @@ void pdlp_restart_strategy_t::cupdlpx_restart( hyper_params_.restart_k_i, hyper_params_.restart_k_d, hyper_params_.restart_i_smooth); - primal_weight.set_element_async(0, primal_weight_value, stream_view_); - primal_step_size.set_element_async(0, primal_step_size_value, stream_view_); - dual_step_size.set_element_async(0, dual_step_size_value, stream_view_); - best_primal_weight.set_element_async(0, best_primal_weight_value, stream_view_); + if (auto* engine = pdhg_solver.get_mgpu_engine()) { + engine->set_scalar_on_master_and_shards( + primal_weight_value, [](auto& sp) { return sp.get_primal_weight().data(); }); + engine->set_scalar_on_master_and_shards( + primal_step_size_value, [](auto& sp) { return sp.get_primal_step_size().data(); }); + engine->set_scalar_on_master_and_shards( + dual_step_size_value, [](auto& sp) { return sp.get_dual_step_size().data(); }); + engine->set_scalar_on_master_and_shards( + best_primal_weight_value, [](auto& sp) { return sp.get_best_primal_weight().data(); }); + } else { + primal_weight.set_element_async(0, primal_weight_value, stream_view_); + primal_step_size.set_element_async(0, primal_step_size_value, stream_view_); + dual_step_size.set_element_async(0, dual_step_size_value, stream_view_); + best_primal_weight.set_element_async(0, best_primal_weight_value, stream_view_); + } } - // mGPU: Broadcast all primal-weight / step-size scalars updated by the cuPDLPx - // restart on the master to every shard so the restart-state on - // each shard stays in sync with master. - if (auto* engine = pdhg_solver.get_mgpu_engine()) { - RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); - - f_t h_primal_step_size{}, h_dual_step_size{}; - f_t h_primal_weight{}, h_best_primal_weight{}; - - raft::copy(&h_primal_step_size, primal_step_size.data(), 1, stream_view_); - raft::copy(&h_dual_step_size, dual_step_size.data(), 1, stream_view_); - raft::copy(&h_primal_weight, primal_weight.data(), 1, stream_view_); - raft::copy(&h_best_primal_weight, best_primal_weight.data(), 1, stream_view_); - RAFT_CUDA_TRY(cudaStreamSynchronize(stream_view_)); - - engine->for_each_shard([&](auto& shard) { - auto& sub = *shard.sub_pdlp; - raft::copy(sub.get_primal_step_size().data(), &h_primal_step_size, 1, shard.stream.view()); - raft::copy(sub.get_dual_step_size().data(), &h_dual_step_size, 1, shard.stream.view()); - raft::copy(sub.get_primal_weight().data(), &h_primal_weight, 1, shard.stream.view()); - raft::copy( - sub.get_best_primal_weight().data(), &h_best_primal_weight, 1, shard.stream.view()); - }); - } // TODO later batch mode: remove if you have per climber restart if (auto* engine = pdhg_solver.get_mgpu_engine()) { From 5c543d626cb14e9bc9a1777992a4680fab0e2417 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 18:52:40 +0200 Subject: [PATCH 257/258] style --- cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu index 6c51bd92e2..af9bef5571 100644 --- a/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu +++ b/cpp/src/pdlp/distributed_pdlp/distributed_algorithms.cu @@ -369,8 +369,7 @@ void multi_gpu_engine_t::distributed_compute_initial_step_size( const f_t h_step_size = scaling_factor / std::sqrt(sigma_max_sq); - set_scalar_on_master_and_shards(h_step_size, - [](auto& sp) { return sp.get_step_size().data(); }); + set_scalar_on_master_and_shards(h_step_size, [](auto& sp) { return sp.get_step_size().data(); }); } // -------- Distributed initial primal weight ------------------------------ From 33fc0711345bef11cdd90d1208642e2873495ef2 Mon Sep 17 00:00:00 2001 From: Bulle Mostovoi Date: Sun, 12 Jul 2026 19:01:23 +0200 Subject: [PATCH 258/258] better restart strategy --- .../restart_strategy/pdlp_restart_strategy.cu | 81 +++++++------------ 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index 92939686e2..5d3258502a 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -901,7 +901,6 @@ void pdlp_restart_strategy_t::cupdlpx_restart( sub.pdhg_solver_, shard.rank_data.owned_var_size, shard.rank_data.owned_cstr_size); }); - // Reduce across all shards engine->allreduce_sum_inplace_to_master([](pdlp_solver_t& sp) -> f_t* { return sp.get_restart_strategy().last_restart_duality_gap_.primal_distance_traveled_.data(); }); @@ -909,20 +908,8 @@ void pdlp_restart_strategy_t::cupdlpx_restart( return sp.get_restart_strategy().last_restart_duality_gap_.dual_distance_traveled_.data(); }); } else { - distance_squared_moved_from_last_restart_period( - pdhg_solver.get_potential_next_primal_solution(), - last_restart_duality_gap_.primal_solution_, - pdhg_solver.get_primal_tmp_resource(), - primal_size_h_, - 1, - last_restart_duality_gap_.primal_distance_traveled_); - distance_squared_moved_from_last_restart_period( - pdhg_solver.get_potential_next_dual_solution(), - last_restart_duality_gap_.dual_solution_, - pdhg_solver.get_dual_tmp_resource(), - dual_size_h_, - 1, - last_restart_duality_gap_.dual_distance_traveled_); + primal_dual_distance_squared_moved_from_last_restart_period( + pdhg_solver, primal_size_h_, dual_size_h_); } auto view = make_cupdlpx_restart_view(last_restart_duality_gap_.primal_distance_traveled_, @@ -989,44 +976,36 @@ void pdlp_restart_strategy_t::cupdlpx_restart( // TODO later batch mode: remove if you have per climber restart + // Small copy helper to use in both single-GPU and distributed paths. + auto commit_potential_next_as_last_restart = [](pdlp_restart_strategy_t& rest, + pdhg_solver_t& solver, + rmm::cuda_stream_view stream) { + raft::copy(rest.last_restart_duality_gap_.primal_solution_.data(), + solver.get_potential_next_primal_solution().data(), + rest.last_restart_duality_gap_.primal_solution_.size(), + stream); + raft::copy(solver.get_primal_solution().data(), + solver.get_potential_next_primal_solution().data(), + solver.get_primal_solution().size(), + stream); + raft::copy(rest.last_restart_duality_gap_.dual_solution_.data(), + solver.get_potential_next_dual_solution().data(), + rest.last_restart_duality_gap_.dual_solution_.size(), + stream); + raft::copy(solver.get_dual_solution().data(), + solver.get_potential_next_dual_solution().data(), + solver.get_dual_solution().size(), + stream); + }; + if (auto* engine = pdhg_solver.get_mgpu_engine()) { engine->for_each_shard([&](auto& shard) { - auto& sub = *shard.sub_pdlp; - auto& sub_rest = sub.get_restart_strategy(); - raft::copy(sub_rest.last_restart_duality_gap_.primal_solution_.data(), - sub.pdhg_solver_.get_potential_next_primal_solution().data(), - sub_rest.last_restart_duality_gap_.primal_solution_.size(), - shard.stream.view()); - raft::copy(sub.pdhg_solver_.get_primal_solution().data(), - sub.pdhg_solver_.get_potential_next_primal_solution().data(), - sub.pdhg_solver_.get_primal_solution().size(), - shard.stream.view()); - raft::copy(sub_rest.last_restart_duality_gap_.dual_solution_.data(), - sub.pdhg_solver_.get_potential_next_dual_solution().data(), - sub_rest.last_restart_duality_gap_.dual_solution_.size(), - shard.stream.view()); - raft::copy(sub.pdhg_solver_.get_dual_solution().data(), - sub.pdhg_solver_.get_potential_next_dual_solution().data(), - sub.pdhg_solver_.get_dual_solution().size(), - shard.stream.view()); + auto& sub = *shard.sub_pdlp; + commit_potential_next_as_last_restart( + sub.get_restart_strategy(), sub.pdhg_solver_, shard.stream.view()); }); } else { - raft::copy(last_restart_duality_gap_.primal_solution_.data(), - pdhg_solver.get_potential_next_primal_solution().data(), - last_restart_duality_gap_.primal_solution_.size(), - stream_view_); - raft::copy(pdhg_solver.get_primal_solution().data(), - pdhg_solver.get_potential_next_primal_solution().data(), - last_restart_duality_gap_.primal_solution_.size(), - stream_view_); - raft::copy(last_restart_duality_gap_.dual_solution_.data(), - pdhg_solver.get_potential_next_dual_solution().data(), - last_restart_duality_gap_.dual_solution_.size(), - stream_view_); - raft::copy(pdhg_solver.get_dual_solution().data(), - pdhg_solver.get_potential_next_dual_solution().data(), - last_restart_duality_gap_.dual_solution_.size(), - stream_view_); + commit_potential_next_as_last_restart(*this, pdhg_solver, stream_view_); } #ifdef CUPDLP_DEBUG_MODE @@ -1328,10 +1307,6 @@ template void pdlp_restart_strategy_t::primal_dual_distance_squared_moved_from_last_restart_period( pdhg_solver_t& pdhg_solver, i_t primal_size, i_t dual_size) { - cuopt_assert( - !batch_mode_, - "primal_dual_distance_squared_moved_from_last_restart_period hard-codes stride=1 " - "and is not batch-mode safe; batch call sites should use the underlying functions directly"); distance_squared_moved_from_last_restart_period( pdhg_solver.get_potential_next_primal_solution(), last_restart_duality_gap_.primal_solution_,