From b0104684e485e728a96e8d4f89d01ef37b53fd67 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Wed, 29 Jul 2026 20:57:44 -0700 Subject: [PATCH 1/5] Reduce concurrent LP host memory footprint Signed-off-by: Hugo Linsenmaier --- .../optimization_problem_utils.hpp | 10 +- cpp/src/dual_simplex/solve.cpp | 27 ++- cpp/src/dual_simplex/solve.hpp | 7 + cpp/src/pdlp/solve.cu | 154 +++++++++++------- cpp/src/pdlp/translate.hpp | 6 +- cpp/src/utilities/version_info.cpp | 46 ++++-- .../unit_tests/solution_interface_test.cu | 18 ++ cpp/tests/utilities/test_cli.cpp | 4 +- 8 files changed, 190 insertions(+), 82 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp index dab2ce9454..b1f81b8edb 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -139,12 +140,13 @@ void populate_from_mps_data_model(optimization_problem_interface_t* pr /** * @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays. * - * For GPU-backed problems this falls back to populate_from_mps_data_model (copy/H2D path). + * For GPU-backed problems this falls back to populate_from_mps_data_model (copy/H2D path), waits + * for the copies to complete, and releases the parsed host storage. * * @tparam i_t Integer type for indices * @tparam f_t Floating point type for values * @param[out] problem The optimization problem interface to populate - * @param[in] data_model Parsed model; moved-from on return for CPU adopt + * @param[in] data_model Parsed model; moved-from on return */ template void adopt_from_mps_data_model(optimization_problem_interface_t* problem, @@ -155,6 +157,10 @@ void adopt_from_mps_data_model(optimization_problem_interface_t* probl return; } populate_from_mps_data_model(problem, data_model); + if (auto* gpu_problem = dynamic_cast*>(problem)) { + gpu_problem->get_handle_ptr()->sync_stream(); + data_model = {}; + } } /** diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 697af9e869..3a7c8a22e4 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -351,10 +351,11 @@ template lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, f_t start_time, - lp_solution_t& solution) + lp_solution_t& solution, + const raft::handle_t* handle_ptr) { lp_status_t status = lp_status_t::UNSET; - lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + lp_problem_t original_lp(handle_ptr, 1, 1, 1); // Convert the user problem to a linear program with only equality constraints std::vector new_slacks; @@ -369,14 +370,14 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us // Presolve the linear program presolve_info_t presolve_info; - lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); + lp_problem_t presolved_lp(handle_ptr, 1, 1, 1); const i_t ok = presolve(original_lp, barrier_settings, presolved_lp, presolve_info); if (ok == CONCURRENT_HALT_RETURN) { return lp_status_t::CONCURRENT_LIMIT; } if (ok == TIME_LIMIT_RETURN) { return lp_status_t::TIME_LIMIT; } if (ok == -1) { return lp_status_t::INFEASIBLE; } // Apply columns scaling to the presolve LP - lp_problem_t barrier_lp(user_problem.handle_ptr, + lp_problem_t barrier_lp(handle_ptr, presolved_lp.num_rows, presolved_lp.num_cols, presolved_lp.A.col_start[presolved_lp.num_cols]); @@ -669,6 +670,16 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us return barrier_status; } +template +lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, + const simplex_solver_settings_t& settings, + f_t start_time, + lp_solution_t& solution) +{ + return solve_linear_program_with_barrier( + user_problem, settings, start_time, solution, user_problem.handle_ptr); +} + template lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, @@ -688,7 +699,6 @@ lp_status_t solve_linear_program(const user_problem_t& user_problem, std::vector new_slacks; dualize_info_t dualize_info; convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); - solution.resize(user_problem.num_rows, user_problem.num_cols); lp_solution_t lp_solution(original_lp.num_rows, original_lp.num_cols); std::vector vstatus; std::vector edge_norms; @@ -825,6 +835,13 @@ template lp_status_t solve_linear_program_with_barrier( double start_time, lp_solution_t& solution); +template lp_status_t solve_linear_program_with_barrier( + const user_problem_t& user_problem, + const simplex_solver_settings_t& settings, + double start_time, + lp_solution_t& solution, + const raft::handle_t* handle_ptr); + template lp_status_t solve_linear_program(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, lp_solution_t& solution); diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 7cc9a9f5cf..90c2dbd690 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -98,6 +98,13 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us f_t start_time, lp_solution_t& solution); +template +lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, + const simplex_solver_settings_t& settings, + f_t start_time, + lp_solution_t& solution, + const raft::handle_t* handle_ptr); + template lp_status_t solve_linear_program(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index a235d137c8..eb725476f6 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -62,6 +62,7 @@ #include #include #include +#include #include #include @@ -487,9 +488,10 @@ optimization_problem_solution_t convert_dual_simplex_sol( template std::tuple, simplex::lp_status_t, f_t, f_t, f_t> run_barrier( - simplex::user_problem_t& user_problem, + const simplex::user_problem_t& user_problem, pdlp_solver_settings_t const& settings, - const timer_t& timer) + const timer_t& timer, + const raft::handle_t* handle_ptr = nullptr) { f_t norm_user_objective = vector_norm2(user_problem.objective); f_t norm_rhs = vector_norm2(user_problem.rhs); @@ -521,11 +523,19 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t barrier_settings.log.log = false; } - simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); + simplex::lp_solution_t solution(0, 0); auto status = simplex::solve_linear_program_with_barrier( - user_problem, barrier_settings, timer.get_tic_start(), solution); + user_problem, + barrier_settings, + timer.get_tic_start(), + solution, + handle_ptr == nullptr ? user_problem.handle_ptr : handle_ptr); - barrier::project_barrier_solution_to_model_variables(user_problem, solution); + if (status == simplex::lp_status_t::OPTIMAL) { + barrier::project_barrier_solution_to_model_variables(user_problem, solution); + } else if (handle_ptr == nullptr) { + solution.resize(user_problem.num_rows, user_problem.num_cols); + } CUOPT_LOG_CONDITIONAL_INFO( !settings.inside_mip, "Barrier finished in %.2f seconds", timer.elapsed_time()); @@ -549,7 +559,7 @@ optimization_problem_solution_t run_barrier( { // Convert data structures to dual simplex format and back simplex::user_problem_t dual_simplex_problem = - cuopt_problem_to_user_problem(problem.handle_ptr, problem); + cuopt_problem_to_user_problem(problem.handle_ptr, problem, false); auto sol_dual_simplex = run_barrier(dual_simplex_problem, settings, timer); return convert_dual_simplex_sol(problem, std::get<0>(sol_dual_simplex), @@ -562,26 +572,28 @@ optimization_problem_solution_t run_barrier( template void run_barrier_thread( - simplex::user_problem_t& problem, + const simplex::user_problem_t& problem, pdlp_solver_settings_t const& settings, std::unique_ptr< std::tuple, simplex::lp_status_t, f_t, f_t, f_t>>& sol_ptr, - const timer_t& timer) + const timer_t& timer, + const raft::handle_t* handle_ptr) { // We will return the solution from the thread as a unique_ptr sol_ptr = std::make_unique< std::tuple, simplex::lp_status_t, f_t, f_t, f_t>>( - run_barrier(problem, settings, timer)); + run_barrier(problem, settings, timer, handle_ptr)); // Wait for barrier thread to finish - problem.handle_ptr->sync_stream(); + handle_ptr->sync_stream(); } template std::tuple, simplex::lp_status_t, f_t, f_t, f_t> run_dual_simplex( - simplex::user_problem_t& user_problem, + const simplex::user_problem_t& user_problem, pdlp_solver_settings_t const& settings, - const timer_t& timer) + const timer_t& timer, + bool preserve_solution_shape = true) { f_t norm_user_objective = vector_norm2(user_problem.objective); f_t norm_rhs = vector_norm2(user_problem.rhs); @@ -595,9 +607,14 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t dual_simplex_settings.log.log = false; } - simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); + simplex::lp_solution_t solution(0, 0); auto status = simplex::solve_linear_program( user_problem, dual_simplex_settings, timer.get_tic_start(), solution); + if (preserve_solution_shape && + (solution.x.size() != static_cast(user_problem.num_cols) || + solution.y.size() != static_cast(user_problem.num_rows))) { + solution.resize(user_problem.num_rows, user_problem.num_cols); + } CUOPT_LOG_CONDITIONAL_INFO( !settings.inside_mip, "Dual simplex finished in %.2f seconds", timer.elapsed_time()); @@ -621,7 +638,7 @@ optimization_problem_solution_t run_dual_simplex( { // Convert data structures to dual simplex format and back simplex::user_problem_t dual_simplex_problem = - cuopt_problem_to_user_problem(problem.handle_ptr, problem); + cuopt_problem_to_user_problem(problem.handle_ptr, problem, false); auto sol_dual_simplex = run_dual_simplex(dual_simplex_problem, settings, timer); return convert_dual_simplex_sol(problem, std::get<0>(sol_dual_simplex), @@ -1497,7 +1514,7 @@ optimization_problem_solution_t batch_pdlp_solve( template void run_dual_simplex_thread( - simplex::user_problem_t& problem, + const simplex::user_problem_t& problem, pdlp_solver_settings_t const& settings, std::unique_ptr< std::tuple, simplex::lp_status_t, f_t, f_t, f_t>>& sol_ptr, @@ -1506,7 +1523,7 @@ void run_dual_simplex_thread( // We will return the solution from the thread as a unique_ptr sol_ptr = std::make_unique< std::tuple, simplex::lp_status_t, f_t, f_t, f_t>>( - run_dual_simplex(problem, settings, timer)); + run_dual_simplex(problem, settings, timer, false)); } template @@ -1551,7 +1568,7 @@ optimization_problem_solution_t run_concurrent( // Otherwise, CUDA API calls to the problem stream may occur in both threads and throw graph // capture off simplex::user_problem_t dual_simplex_problem = - cuopt_problem_to_user_problem(problem.handle_ptr, problem); + cuopt_problem_to_user_problem(problem.handle_ptr, problem, false); // Dual simplex / barrier results — written by tasks, read after the taskgroup barrier. std::unique_ptr, simplex::lp_status_t, f_t, f_t, f_t>> sol_dual_simplex_ptr; @@ -1592,10 +1609,12 @@ optimization_problem_solution_t run_concurrent( try { auto call_barrier_thread = [&]() { rmm::cuda_stream_view barrier_stream = rmm::cuda_stream_per_thread; - barrier_handle_ptr = std::make_unique(barrier_stream); - auto barrier_problem = dual_simplex_problem; - barrier_problem.handle_ptr = barrier_handle_ptr.get(); - run_barrier_thread(barrier_problem, settings_pdlp, sol_barrier_ptr, timer); + barrier_handle_ptr = std::make_unique(barrier_stream); + run_barrier_thread(dual_simplex_problem, + settings_pdlp, + sol_barrier_ptr, + timer, + barrier_handle_ptr.get()); }; if (settings.num_gpus > 1) { problem.handle_ptr->sync_stream(); @@ -1679,38 +1698,32 @@ optimization_problem_solution_t run_concurrent( if (dual_simplex_exception) { std::rethrow_exception(dual_simplex_exception); } if (barrier_exception) { std::rethrow_exception(barrier_exception); } - // copy the dual simplex solution to the device - auto sol_dual_simplex = - !settings.inside_mip - ? convert_dual_simplex_sol(problem, - std::get<0>(*sol_dual_simplex_ptr), - std::get<1>(*sol_dual_simplex_ptr), - std::get<2>(*sol_dual_simplex_ptr), - std::get<3>(*sol_dual_simplex_ptr), - std::get<4>(*sol_dual_simplex_ptr), - method_t::DualSimplex) - : optimization_problem_solution_t{pdlp_termination_status_t::ConcurrentLimit, - problem.handle_ptr->get_stream()}; - - // copy the barrier solution to the device (sentinel when the barrier task was skipped). - auto sol_barrier = enable_barrier ? convert_dual_simplex_sol(problem, - std::get<0>(*sol_barrier_ptr), - std::get<1>(*sol_barrier_ptr), - std::get<2>(*sol_barrier_ptr), - std::get<3>(*sol_barrier_ptr), - std::get<4>(*sol_barrier_ptr), - method_t::Barrier) - : optimization_problem_solution_t{ - pdlp_termination_status_t::ConcurrentLimit, - problem.handle_ptr->get_stream()}; + // Both CPU solvers have joined, so release their shared host model before converting outputs. + dual_simplex_problem = simplex::user_problem_t(problem.handle_ptr); f_t end_time = timer.elapsed_time(); CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Concurrent time: %.3fs", end_time); - // Check status to see if we should return the pdlp solution or the dual simplex solution - if (!settings.inside_mip && - (sol_dual_simplex.get_termination_status() == pdlp_termination_status_t::Optimal || - sol_dual_simplex.get_termination_status() == pdlp_termination_status_t::PrimalInfeasible || - sol_dual_simplex.get_termination_status() == pdlp_termination_status_t::DualInfeasible)) { + + const auto dual_simplex_status = !settings.inside_mip ? std::get<1>(*sol_dual_simplex_ptr) + : simplex::lp_status_t::CONCURRENT_LIMIT; + const auto barrier_status = + enable_barrier ? std::get<1>(*sol_barrier_ptr) : simplex::lp_status_t::CONCURRENT_LIMIT; + const bool dual_simplex_solved = dual_simplex_status == simplex::lp_status_t::OPTIMAL || + dual_simplex_status == simplex::lp_status_t::INFEASIBLE || + dual_simplex_status == simplex::lp_status_t::UNBOUNDED; + + // Convert only the solution that will be returned. Each conversion copies three potentially + // large vectors to the device and duplicates the problem's row and variable names. + if (!settings.inside_mip && dual_simplex_solved) { + sol_barrier_ptr.reset(); + auto sol_dual_simplex = convert_dual_simplex_sol(problem, + std::get<0>(*sol_dual_simplex_ptr), + std::get<1>(*sol_dual_simplex_ptr), + std::get<2>(*sol_dual_simplex_ptr), + std::get<3>(*sol_dual_simplex_ptr), + std::get<4>(*sol_dual_simplex_ptr), + method_t::DualSimplex); + sol_dual_simplex_ptr.reset(); CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Solved with dual simplex"); sol_pdlp.copy_from(problem.handle_ptr, sol_dual_simplex); sol_pdlp.set_solve_time(end_time); @@ -1732,7 +1745,16 @@ optimization_problem_solution_t run_concurrent( sol_pdlp.get_additional_termination_information().l2_dual_residual, sol_pdlp.get_additional_termination_information().l2_relative_dual_residual); return sol_pdlp; - } else if (sol_barrier.get_termination_status() == pdlp_termination_status_t::Optimal) { + } else if (barrier_status == simplex::lp_status_t::OPTIMAL) { + sol_dual_simplex_ptr.reset(); + auto sol_barrier = convert_dual_simplex_sol(problem, + std::get<0>(*sol_barrier_ptr), + std::get<1>(*sol_barrier_ptr), + std::get<2>(*sol_barrier_ptr), + std::get<3>(*sol_barrier_ptr), + std::get<4>(*sol_barrier_ptr), + method_t::Barrier); + sol_barrier_ptr.reset(); CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Solved with barrier"); sol_pdlp.copy_from(problem.handle_ptr, sol_barrier); sol_pdlp.set_solve_time(end_time); @@ -1755,13 +1777,31 @@ optimization_problem_solution_t run_concurrent( sol_pdlp.get_additional_termination_information().l2_relative_dual_residual); return sol_pdlp; } else if (sol_pdlp.get_termination_status() == pdlp_termination_status_t::Optimal) { + sol_dual_simplex_ptr.reset(); + sol_barrier_ptr.reset(); CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Solved with PDLP"); return sol_pdlp; } else if (!settings.inside_mip && sol_pdlp.get_termination_status() == pdlp_termination_status_t::ConcurrentLimit) { + sol_barrier_ptr.reset(); + auto& dual_simplex_solution = std::get<0>(*sol_dual_simplex_ptr); + if (dual_simplex_solution.x.size() != static_cast(problem.n_variables) || + dual_simplex_solution.y.size() != static_cast(problem.n_constraints)) { + dual_simplex_solution.resize(problem.n_constraints, problem.n_variables); + } + auto sol_dual_simplex = convert_dual_simplex_sol(problem, + dual_simplex_solution, + std::get<1>(*sol_dual_simplex_ptr), + std::get<2>(*sol_dual_simplex_ptr), + std::get<3>(*sol_dual_simplex_ptr), + std::get<4>(*sol_dual_simplex_ptr), + method_t::DualSimplex); + sol_dual_simplex_ptr.reset(); CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Using dual simplex solve info"); return sol_dual_simplex; } else { + sol_dual_simplex_ptr.reset(); + sol_barrier_ptr.reset(); CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Using PDLP solve info"); return sol_pdlp; } @@ -2002,7 +2042,7 @@ optimization_problem_solution_t solve_lp( validate_new_bounds(op_problem, settings); auto lp_timer = cuopt::timer_t(settings.time_limit); - mip::problem_t problem(op_problem); + std::optional> problem; // handle default presolve if (settings.presolver == presolver_t::Default) { constexpr i_t presolve_nnz_threshold = 8000; @@ -2085,17 +2125,19 @@ optimization_problem_solution_t solve_lp( op_problem.get_row_names()); } - problem = mip::problem_t(result->reduced_problem); + problem.emplace(result->reduced_problem); presolve_time = lp_timer.elapsed_time(); CUOPT_LOG_INFO("%s presolve time: %.2fs", settings.presolver == presolver_t::PSLP ? "PSLP" : "Papilo", presolve_time); + } else { + problem.emplace(op_problem); } if (!settings_const.inside_mip) { CUOPT_LOG_INFO("Objective offset %f scaling_factor %f", - problem.presolve_data.objective_offset, - problem.presolve_data.objective_scaling_factor); + problem->presolve_data.objective_offset, + problem->presolve_data.objective_scaling_factor); } if (settings.user_problem_file != "") { @@ -2110,7 +2152,7 @@ optimization_problem_solution_t solve_lp( // Set the hyper-parameters based on the solver_settings if (use_pdlp_solver_mode) { set_pdlp_solver_mode(settings); } - auto solution = solve_lp_with_method(problem, settings, lp_timer, is_batch_mode); + auto solution = solve_lp_with_method(*problem, settings, lp_timer, is_batch_mode); if (run_presolve) { auto primal_solution = cuopt::device_copy(solution.get_primal_solution(), diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 6d374d43f3..b5d425abab 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -112,7 +112,7 @@ static simplex::user_problem_t cuopt_problem_to_user_problem( template static simplex::user_problem_t cuopt_problem_to_user_problem( - raft::handle_t const* handle_ptr, mip::problem_t& model) + raft::handle_t const* handle_ptr, mip::problem_t& model, bool copy_names = true) { simplex::user_problem_t user_problem(handle_ptr); @@ -165,7 +165,7 @@ static simplex::user_problem_t cuopt_problem_to_user_problem( std::tie(user_problem.lower, user_problem.upper) = extract_host_bounds(model.variable_bounds, handle_ptr); user_problem.problem_name = model.original_problem_ptr->get_problem_name(); - if (model.row_names.size() > 0) { + if (copy_names && model.row_names.size() > 0) { user_problem.row_names.resize(m); for (int i = 0; i < m; ++i) { if (i < (int)model.row_names.size()) { @@ -175,7 +175,7 @@ static simplex::user_problem_t cuopt_problem_to_user_problem( } } } - if (model.var_names.size() > 0) { + if (copy_names && model.var_names.size() > 0) { user_problem.col_names.resize(n); for (int j = 0; j < n; ++j) { if (j < (int)model.var_names.size()) { diff --git a/cpp/src/utilities/version_info.cpp b/cpp/src/utilities/version_info.cpp index dfab609cc5..8a748400e3 100644 --- a/cpp/src/utilities/version_info.cpp +++ b/cpp/src/utilities/version_info.cpp @@ -143,24 +143,37 @@ static std::string get_cpu_model() return "Unknown"; } -static double get_available_memory_gb() +struct host_memory_info_t { + double total_gb{}; + double available_gb{}; +}; + +static host_memory_info_t get_host_memory_info() { std::ifstream meminfo("/proc/meminfo"); - if (!meminfo.is_open()) return 0.0; + if (!meminfo.is_open()) return {}; std::string line; - long kb = 0; + long total_kb = 0; + long available_kb = 0; + long free_kb = 0; while (std::getline(meminfo, line)) { - if (line.find("MemAvailable:") == 0 || line.find("MemFree:") == 0) { - std::size_t pos = line.find_first_of("0123456789"); - if (pos != std::string::npos) { - kb = std::stol(line.substr(pos)); - break; - } + std::istringstream fields(line); + std::string key; + long value_kb = 0; + fields >> key >> value_kb; + if (key == "MemTotal:") { + total_kb = value_kb; + } else if (key == "MemAvailable:") { + available_kb = value_kb; + } else if (key == "MemFree:") { + free_kb = value_kb; } } - return kb / (1024.0 * 1024.0); // Convert KB to GB + if (available_kb == 0) { available_kb = free_kb; } + constexpr double kb_per_gib = 1024.0 * 1024.0; + return {total_kb / kb_per_gib, available_kb / kb_per_gib}; } void print_version_info(int num_devices) @@ -180,11 +193,14 @@ void print_version_info(int num_devices) CUOPT_GIT_COMMIT_HASH, CUOPT_CPU_ARCHITECTURE, CUOPT_CUDA_ARCHITECTURES); - CUOPT_LOG_INFO("CPU: %s, threads (physical/logical): %d/%d, RAM: %.2f GiB", - get_cpu_model().c_str(), - get_physical_cores(), - std::thread::hardware_concurrency(), - get_available_memory_gb()); + const auto memory = get_host_memory_info(); + CUOPT_LOG_INFO( + "CPU: %s, threads (physical/logical): %d/%d, RAM: %.2f GiB total, %.2f GiB available", + get_cpu_model().c_str(), + get_physical_cores(), + std::thread::hardware_concurrency(), + memory.total_gb, + memory.available_gb); for (int device_id = 0; device_id < num_devices; ++device_id) { cudaDeviceProp device_prop{}; diff --git a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu index 4628c225a0..b34faa88b4 100644 --- a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu @@ -515,6 +515,24 @@ End expect_adopt_matches_populate(model); } +TEST_F(SolutionInterfaceTest, gpu_adopt_consumes_mps_data_model) +{ + auto model = io::read_mps(lp_file_); + const auto n_variables = model.get_n_variables(); + const auto n_constraints = model.get_n_constraints(); + const auto nnz = model.get_nnz(); + + raft::handle_t handle; + optimization_problem_t problem(&handle); + adopt_from_mps_data_model(&problem, std::move(model)); + + EXPECT_EQ(model.get_n_variables(), 0); + EXPECT_EQ(model.get_n_constraints(), 0); + EXPECT_EQ(problem.get_n_variables(), n_variables); + EXPECT_EQ(problem.get_n_constraints(), n_constraints); + EXPECT_EQ(problem.get_nnz(), nnz); +} + // ============================================================================= // Solution conversion tests (hand-constructed, known values) // ============================================================================= diff --git a/cpp/tests/utilities/test_cli.cpp b/cpp/tests/utilities/test_cli.cpp index a3bbb06973..107fbc6291 100644 --- a/cpp/tests/utilities/test_cli.cpp +++ b/cpp/tests/utilities/test_cli.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -108,6 +108,8 @@ TEST_F(cli_test_t, basic_usage) std::string content((std::istreambuf_iterator(sol)), std::istreambuf_iterator()); EXPECT_TRUE(content.find("Status:") != std::string::npos); EXPECT_TRUE(content.find("Objective value:") != std::string::npos); + EXPECT_TRUE(output.find("GiB total") != std::string::npos); + EXPECT_TRUE(output.find("GiB available") != std::string::npos); } TEST_F(cli_test_t, with_initial_solution) From 37478239eae1a4bc723d4710b4e4a5c8a3a7c24e Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 30 Jul 2026 09:58:02 -0700 Subject: [PATCH 2/5] Address memory footprint review feedback Signed-off-by: Hugo Linsenmaier --- cpp/src/dual_simplex/solve.cpp | 1 + cpp/src/pdlp/solve.cu | 18 ++++-------------- cpp/src/utilities/version_info.cpp | 6 +++--- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 3a7c8a22e4..68382c6237 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -699,6 +699,7 @@ lp_status_t solve_linear_program(const user_problem_t& user_problem, std::vector new_slacks; dualize_info_t dualize_info; convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + solution.resize(user_problem.num_rows, user_problem.num_cols); lp_solution_t lp_solution(original_lp.num_rows, original_lp.num_cols); std::vector vstatus; std::vector edge_norms; diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index eb725476f6..cb0e10a7b8 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -592,8 +592,7 @@ template std::tuple, simplex::lp_status_t, f_t, f_t, f_t> run_dual_simplex( const simplex::user_problem_t& user_problem, pdlp_solver_settings_t const& settings, - const timer_t& timer, - bool preserve_solution_shape = true) + const timer_t& timer) { f_t norm_user_objective = vector_norm2(user_problem.objective); f_t norm_rhs = vector_norm2(user_problem.rhs); @@ -607,14 +606,9 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t dual_simplex_settings.log.log = false; } - simplex::lp_solution_t solution(0, 0); + simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); auto status = simplex::solve_linear_program( user_problem, dual_simplex_settings, timer.get_tic_start(), solution); - if (preserve_solution_shape && - (solution.x.size() != static_cast(user_problem.num_cols) || - solution.y.size() != static_cast(user_problem.num_rows))) { - solution.resize(user_problem.num_rows, user_problem.num_cols); - } CUOPT_LOG_CONDITIONAL_INFO( !settings.inside_mip, "Dual simplex finished in %.2f seconds", timer.elapsed_time()); @@ -1523,7 +1517,7 @@ void run_dual_simplex_thread( // We will return the solution from the thread as a unique_ptr sol_ptr = std::make_unique< std::tuple, simplex::lp_status_t, f_t, f_t, f_t>>( - run_dual_simplex(problem, settings, timer, false)); + run_dual_simplex(problem, settings, timer)); } template @@ -1785,11 +1779,7 @@ optimization_problem_solution_t run_concurrent( sol_pdlp.get_termination_status() == pdlp_termination_status_t::ConcurrentLimit) { sol_barrier_ptr.reset(); auto& dual_simplex_solution = std::get<0>(*sol_dual_simplex_ptr); - if (dual_simplex_solution.x.size() != static_cast(problem.n_variables) || - dual_simplex_solution.y.size() != static_cast(problem.n_constraints)) { - dual_simplex_solution.resize(problem.n_constraints, problem.n_variables); - } - auto sol_dual_simplex = convert_dual_simplex_sol(problem, + auto sol_dual_simplex = convert_dual_simplex_sol(problem, dual_simplex_solution, std::get<1>(*sol_dual_simplex_ptr), std::get<2>(*sol_dual_simplex_ptr), diff --git a/cpp/src/utilities/version_info.cpp b/cpp/src/utilities/version_info.cpp index 8a748400e3..71dfc20c22 100644 --- a/cpp/src/utilities/version_info.cpp +++ b/cpp/src/utilities/version_info.cpp @@ -195,12 +195,12 @@ void print_version_info(int num_devices) CUOPT_CUDA_ARCHITECTURES); const auto memory = get_host_memory_info(); CUOPT_LOG_INFO( - "CPU: %s, threads (physical/logical): %d/%d, RAM: %.2f GiB total, %.2f GiB available", + "CPU: %s, threads (physical/logical): %d/%d, RAM (available/total): %.2f / %.2f GiB", get_cpu_model().c_str(), get_physical_cores(), std::thread::hardware_concurrency(), - memory.total_gb, - memory.available_gb); + memory.available_gb, + memory.total_gb); for (int device_id = 0; device_id < num_devices; ++device_id) { cudaDeviceProp device_prop{}; From e2834e04494d19455b165e4520ec6c8079253df5 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 30 Jul 2026 11:28:21 -0700 Subject: [PATCH 3/5] Fix CLI memory info test expectation Signed-off-by: Hugo Linsenmaier --- cpp/tests/utilities/test_cli.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/tests/utilities/test_cli.cpp b/cpp/tests/utilities/test_cli.cpp index 107fbc6291..4a7c9b74f5 100644 --- a/cpp/tests/utilities/test_cli.cpp +++ b/cpp/tests/utilities/test_cli.cpp @@ -108,8 +108,7 @@ TEST_F(cli_test_t, basic_usage) std::string content((std::istreambuf_iterator(sol)), std::istreambuf_iterator()); EXPECT_TRUE(content.find("Status:") != std::string::npos); EXPECT_TRUE(content.find("Objective value:") != std::string::npos); - EXPECT_TRUE(output.find("GiB total") != std::string::npos); - EXPECT_TRUE(output.find("GiB available") != std::string::npos); + EXPECT_TRUE(output.find("RAM (available/total):") != std::string::npos); } TEST_F(cli_test_t, with_initial_solution) From 3f20c16121e828a12ada1b363972264e2daa9843 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Fri, 31 Jul 2026 11:50:00 -0700 Subject: [PATCH 4/5] Restore barrier solution sizing Signed-off-by: Hugo Linsenmaier --- cpp/src/pdlp/solve.cu | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index cb0e10a7b8..87dbcc033a 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -523,7 +523,7 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t barrier_settings.log.log = false; } - simplex::lp_solution_t solution(0, 0); + simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); auto status = simplex::solve_linear_program_with_barrier( user_problem, barrier_settings, @@ -533,8 +533,6 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t if (status == simplex::lp_status_t::OPTIMAL) { barrier::project_barrier_solution_to_model_variables(user_problem, solution); - } else if (handle_ptr == nullptr) { - solution.resize(user_problem.num_rows, user_problem.num_cols); } CUOPT_LOG_CONDITIONAL_INFO( From 7165c1be88c927600cda43ffea2e345a4574bdb2 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 3 Aug 2026 09:39:27 -0700 Subject: [PATCH 5/5] Make barrier handle explicit Signed-off-by: Hugo Linsenmaier --- cpp/src/pdlp/solve.cu | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 87dbcc033a..7dde5fe937 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -491,7 +491,7 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t const simplex::user_problem_t& user_problem, pdlp_solver_settings_t const& settings, const timer_t& timer, - const raft::handle_t* handle_ptr = nullptr) + const raft::handle_t* handle_ptr) { f_t norm_user_objective = vector_norm2(user_problem.objective); f_t norm_rhs = vector_norm2(user_problem.rhs); @@ -525,11 +525,7 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); auto status = simplex::solve_linear_program_with_barrier( - user_problem, - barrier_settings, - timer.get_tic_start(), - solution, - handle_ptr == nullptr ? user_problem.handle_ptr : handle_ptr); + user_problem, barrier_settings, timer.get_tic_start(), solution, handle_ptr); if (status == simplex::lp_status_t::OPTIMAL) { barrier::project_barrier_solution_to_model_variables(user_problem, solution); @@ -558,7 +554,7 @@ optimization_problem_solution_t run_barrier( // Convert data structures to dual simplex format and back simplex::user_problem_t dual_simplex_problem = cuopt_problem_to_user_problem(problem.handle_ptr, problem, false); - auto sol_dual_simplex = run_barrier(dual_simplex_problem, settings, timer); + auto sol_dual_simplex = run_barrier(dual_simplex_problem, settings, timer, problem.handle_ptr); return convert_dual_simplex_sol(problem, std::get<0>(sol_dual_simplex), std::get<1>(sol_dual_simplex), @@ -1875,8 +1871,9 @@ optimization_problem_solution_t solve_qcqp( // Convert data structures to dual simplex format and back simplex::user_problem_t dual_simplex_problem = cuopt_optimization_problem_to_user_problem(op_problem.get_handle_ptr(), op_problem); - auto sol_dual_simplex = run_barrier(dual_simplex_problem, settings, qcqp_timer); - auto solution = convert_dual_simplex_sol(op_problem, + auto sol_dual_simplex = + run_barrier(dual_simplex_problem, settings, qcqp_timer, op_problem.get_handle_ptr()); + auto solution = convert_dual_simplex_sol(op_problem, std::get<0>(sol_dual_simplex), std::get<1>(sol_dual_simplex), std::get<2>(sol_dual_simplex),