diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b2c9f85d2..fd6c0644a 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1288,6 +1288,11 @@ template lp_status_t branch_and_bound_t::solve_root_relaxation( simplex_solver_settings_t const& lp_settings) { + f_t start_time = tic(); + f_t user_objective = 0; + i_t iter = 0; + std::string solver_name = ""; + // Root node path lp_status_t root_status; std::future root_status_future; @@ -1338,24 +1343,41 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( root_crossover_soln_, crossover_vstatus_); - if (crossover_status == crossover_status_t::OPTIMAL) { - settings_.log.printf("Crossover status: %d\n", crossover_status); - } - // Check if crossover was stopped by dual simplex if (crossover_status == crossover_status_t::OPTIMAL) { set_root_concurrent_halt(1); // Stop dual simplex root_status = root_status_future.get(); + // Override the root relaxation solution with the crossover solution root_relax_soln_ = root_crossover_soln_; root_vstatus_ = crossover_vstatus_; root_status = lp_status_t::OPTIMAL; + user_objective = root_crossover_soln_.user_objective; + iter = root_crossover_soln_.iterations; + solver_name = "Barrier/PDLP and Crossover"; + } else { - root_status = root_status_future.get(); + root_status = root_status_future.get(); + user_objective = root_relax_soln_.user_objective; + iter = root_relax_soln_.iterations; + solver_name = "Dual Simplex"; } } else { - root_status = root_status_future.get(); + root_status = root_status_future.get(); + user_objective = root_relax_soln_.user_objective; + iter = root_relax_soln_.iterations; + solver_name = "Dual Simplex"; } + + settings_.log.printf("\n"); + settings_.log.printf("Root relaxation solution found in %d iterations and %.2fs by %s\n", + iter, + toc(start_time), + solver_name.c_str()); + settings_.log.printf("Root relaxation objective %+.8e\n", user_objective); + settings_.log.printf("\n"); + + is_root_solution_set = true; return root_status; } @@ -1414,14 +1436,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.resize(original_lp_.num_rows, original_lp_.num_cols); - settings_.log.printf("Solving LP root relaxation\n"); - lp_status_t root_status; simplex_solver_settings_t lp_settings = settings_; lp_settings.inside_mip = 1; lp_settings.concurrent_halt = get_root_concurrent_halt(); // RINS/SUBMIP path if (!enable_concurrent_lp_root_solve()) { + settings_.log.printf("\nSolving LP root relaxation with dual simplex\n"); root_status = solve_linear_program_advanced(original_lp_, exploration_stats_.start_time, lp_settings, @@ -1430,6 +1451,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut edge_norms_); } else { + settings_.log.printf("\nSolving LP root relaxation in concurrent mode\n"); root_status = solve_root_relaxation(lp_settings); } @@ -1540,7 +1562,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", + settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n\n", settings_.num_threads, settings_.num_bfs_workers, settings_.num_threads - settings_.num_bfs_workers); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index dac1ab393..327f99bc4 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -84,14 +84,16 @@ class branch_and_bound_t { f_t user_objective, i_t iterations) { - root_crossover_soln_.x = primal; - root_crossover_soln_.y = dual; - root_crossover_soln_.z = reduced_costs; - root_objective_ = objective; - root_crossover_soln_.objective = objective; - root_crossover_soln_.user_objective = user_objective; - root_crossover_soln_.iterations = iterations; - root_crossover_solution_set_.store(true, std::memory_order_release); + if (!is_root_solution_set) { + root_crossover_soln_.x = primal; + root_crossover_soln_.y = dual; + root_crossover_soln_.z = reduced_costs; + root_objective_ = objective; + root_crossover_soln_.objective = objective; + root_crossover_soln_.user_objective = user_objective; + root_crossover_soln_.iterations = iterations; + root_crossover_solution_set_.store(true, std::memory_order_release); + } } // Set a solution based on the user problem during the course of the solve @@ -162,6 +164,7 @@ class branch_and_bound_t { std::atomic root_crossover_solution_set_{false}; bool enable_concurrent_lp_root_solve_{false}; std::atomic root_concurrent_halt_{0}; + bool is_root_solution_set{false}; // Pseudocosts pseudo_costs_t pc_; diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 2bc00f636..a54101ec8 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2077,12 +2077,6 @@ void prepare_optimality(const lp_problem_t& lp, settings.log.printf("Primal infeasibility (abs): %.2e\n", primal_infeas); settings.log.printf("Dual infeasibility (abs): %.2e\n", dual_infeas); settings.log.printf("Perturbation: %.2e\n", perturbation); - } else { - settings.log.printf("\n"); - settings.log.printf( - "Root relaxation solution found in %d iterations and %.2fs\n", iter, toc(start_time)); - settings.log.printf("Root relaxation objective %+.8e\n", sol.user_objective); - settings.log.printf("\n"); } } } diff --git a/cpp/src/linear_programming/solve.cu b/cpp/src/linear_programming/solve.cu index d038ade72..829008651 100644 --- a/cpp/src/linear_programming/solve.cu +++ b/cpp/src/linear_programming/solve.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -42,6 +42,9 @@ #include // For std::thread +#define CUOPT_LOG_CONDITIONAL_INFO(condition, ...) \ + if ((condition)) { CUOPT_LOG_INFO(__VA_ARGS__); } + namespace cuopt::linear_programming { // This serves as both a warm up but also a mandatory initial call to setup cuSparse and cuBLAS @@ -417,7 +420,8 @@ run_barrier(dual_simplex::user_problem_t& user_problem, auto status = dual_simplex::solve_linear_program_with_barrier( user_problem, barrier_settings, solution); - CUOPT_LOG_INFO("Barrier finished in %.2f seconds", timer.elapsed_time()); + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, "Barrier finished in %.2f seconds", timer.elapsed_time()); if (settings.concurrent_halt != nullptr && (status == dual_simplex::lp_status_t::OPTIMAL || status == dual_simplex::lp_status_t::UNBOUNDED || @@ -489,9 +493,10 @@ run_dual_simplex(dual_simplex::user_problem_t& user_problem, auto status = dual_simplex::solve_linear_program(user_problem, dual_simplex_settings, solution); - CUOPT_LOG_INFO("Dual simplex finished in %.2f seconds, total time %.2f", - timer_dual_simplex.elapsed_time(), - timer.elapsed_time()); + CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, + "Dual simplex finished in %.2f seconds, total time %.2f", + timer_dual_simplex.elapsed_time(), + timer.elapsed_time()); if (settings.concurrent_halt != nullptr && (status == dual_simplex::lp_status_t::OPTIMAL || status == dual_simplex::lp_status_t::UNBOUNDED || @@ -530,7 +535,9 @@ static optimization_problem_solution_t run_pdlp_solver( bool is_batch_mode) { if (problem.n_constraints == 0) { - CUOPT_LOG_INFO("No constraints in the problem: PDLP can't be run, use Dual Simplex instead."); + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, + "No constraints in the problem: PDLP can't be run, use Dual Simplex instead."); return optimization_problem_solution_t{pdlp_termination_status_t::NumericalError, problem.handle_ptr->get_stream()}; } @@ -551,14 +558,16 @@ optimization_problem_solution_t run_pdlp(detail::problem_t& auto sol = run_pdlp_solver(problem, settings, timer, is_batch_mode); auto pdlp_solve_time = timer_pdlp.elapsed_time(); sol.set_solve_time(timer.elapsed_time()); - CUOPT_LOG_INFO("PDLP finished"); + CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "PDLP finished"); if (sol.get_termination_status() != pdlp_termination_status_t::ConcurrentLimit) { - CUOPT_LOG_INFO("Status: %s Objective: %.8e Iterations: %d Time: %.3fs, Total time %.3fs", - sol.get_termination_status_string().c_str(), - sol.get_objective_value(), - sol.get_additional_termination_information().number_of_steps_taken, - pdlp_solve_time, - sol.get_solve_time()); + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, + "Status: %s Objective: %.8e Iterations: %d Time: %.3fs, Total time %.3fs", + sol.get_termination_status_string().c_str(), + sol.get_objective_value(), + sol.get_additional_termination_information().number_of_steps_taken, + pdlp_solve_time, + sol.get_solve_time()); } const bool do_crossover = settings.crossover; @@ -620,12 +629,13 @@ optimization_problem_solution_t run_pdlp(detail::problem_t& info, termination_status); sol.copy_from(problem.handle_ptr, sol_crossover); - CUOPT_LOG_INFO("Crossover status %s", sol.get_termination_status_string().c_str()); + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, "Crossover status %s", sol.get_termination_status_string().c_str()); } if (settings.method == method_t::Concurrent && settings.concurrent_halt != nullptr && crossover_info == 0 && sol.get_termination_status() == pdlp_termination_status_t::Optimal) { // We finished. Tell dual simplex to stop if it is still running. - CUOPT_LOG_INFO("PDLP finished. Telling others to stop"); + CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "PDLP finished. Telling others to stop"); *settings.concurrent_halt = 1; } return sol; @@ -653,7 +663,7 @@ optimization_problem_solution_t run_concurrent( const timer_t& timer, bool is_batch_mode) { - CUOPT_LOG_INFO("Running concurrent\n"); + CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Running concurrent\n"); timer_t timer_concurrent(timer.remaining_time()); // Copy the settings so that we can set the concurrent halt pointer @@ -668,7 +678,8 @@ optimization_problem_solution_t run_concurrent( if (settings.num_gpus > 1) { int device_count = raft::device_setter::get_device_count(); - CUOPT_LOG_INFO("Running PDLP and Barrier on %d GPUs", device_count); + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, "Running PDLP and Barrier on %d GPUs", device_count); cuopt_expects( device_count > 1, error_type_t::RuntimeError, "Multi-GPU mode requires at least 2 GPUs"); } @@ -752,41 +763,47 @@ optimization_problem_solution_t run_concurrent( 1); f_t end_time = timer.elapsed_time(); - CUOPT_LOG_INFO( - "Concurrent time: %.3fs, total time %.3fs", timer_concurrent.elapsed_time(), end_time); + CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, + "Concurrent time: %.3fs, total time %.3fs", + timer_concurrent.elapsed_time(), + 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)) { - CUOPT_LOG_INFO("Solved with dual simplex"); + 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); - CUOPT_LOG_INFO("Status: %s Objective: %.8e Iterations: %d Time: %.3fs", - sol_pdlp.get_termination_status_string().c_str(), - sol_pdlp.get_objective_value(), - sol_pdlp.get_additional_termination_information().number_of_steps_taken, - end_time); + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, + "Status: %s Objective: %.8e Iterations: %d Time: %.3fs", + sol_pdlp.get_termination_status_string().c_str(), + sol_pdlp.get_objective_value(), + sol_pdlp.get_additional_termination_information().number_of_steps_taken, + end_time); return sol_pdlp; } else if (sol_barrier.get_termination_status() == pdlp_termination_status_t::Optimal) { - CUOPT_LOG_INFO("Solved with barrier"); + 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); - CUOPT_LOG_INFO("Status: %s Objective: %.8e Iterations: %d Time: %.3fs", - sol_pdlp.get_termination_status_string().c_str(), - sol_pdlp.get_objective_value(), - sol_pdlp.get_additional_termination_information().number_of_steps_taken, - end_time); + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, + "Status: %s Objective: %.8e Iterations: %d Time: %.3fs", + sol_pdlp.get_termination_status_string().c_str(), + sol_pdlp.get_objective_value(), + sol_pdlp.get_additional_termination_information().number_of_steps_taken, + end_time); return sol_pdlp; } else if (sol_pdlp.get_termination_status() == pdlp_termination_status_t::Optimal) { - CUOPT_LOG_INFO("Solved with PDLP"); + 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) { - CUOPT_LOG_INFO("Using dual simplex solve info"); + CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Using dual simplex solve info"); return sol_dual_simplex; } else { - CUOPT_LOG_INFO("Using PDLP solve info"); + CUOPT_LOG_CONDITIONAL_INFO(!settings.inside_mip, "Using PDLP solve info"); return sol_pdlp; } } diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index cfe9876de..dcca0b1cc 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -411,7 +411,7 @@ solution_t diversity_manager_t::run_solver() // to bring variables within the bounds } - // Send PDLP relaxed solution to branch and bound before it solves the root node + // Send PDLP relaxed solution to branch and bound if (problem_ptr->set_root_relaxation_solution_callback != nullptr) { auto& d_primal_solution = lp_result.get_primal_solution(); auto& d_dual_solution = lp_result.get_dual_solution(); @@ -434,15 +434,13 @@ solution_t diversity_manager_t::run_solver() problem_ptr->handle_ptr->get_stream()); problem_ptr->handle_ptr->sync_stream(); - auto user_obj = problem_ptr->get_user_obj_from_solver_obj(lp_result.get_objective_value()); + // PDLP returns user-space objective (it applies objective_scaling_factor internally) + auto user_obj = lp_result.get_objective_value(); + auto solver_obj = problem_ptr->get_solver_obj_from_user_obj(user_obj); auto iterations = lp_result.get_additional_termination_information().number_of_steps_taken; - // Set for the B&B - problem_ptr->set_root_relaxation_solution_callback(host_primal, - host_dual, - host_reduced_costs, - lp_result.get_objective_value(), - user_obj, - iterations); + // Set for the B&B (param4 expects solver space, param5 expects user space) + problem_ptr->set_root_relaxation_solution_callback( + host_primal, host_dual, host_reduced_costs, solver_obj, user_obj, iterations); } // in case the pdlp returned var boudns that are out of bounds diff --git a/cpp/src/mip/presolve/bounds_presolve.cuh b/cpp/src/mip/presolve/bounds_presolve.cuh index 54194b059..88a6c740f 100644 --- a/cpp/src/mip/presolve/bounds_presolve.cuh +++ b/cpp/src/mip/presolve/bounds_presolve.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -21,6 +21,8 @@ #include "bounds_update_data.cuh" #include "utils.cuh" +#include + namespace cuopt::linear_programming::detail { template @@ -32,6 +34,7 @@ class bound_presolve_t { struct settings_t { f_t time_limit{60.0}; i_t iteration_limit{std::numeric_limits::max()}; + i_t num_threads = -1; bool parallel_bounds_update{true}; }; diff --git a/cpp/src/mip/presolve/probing_cache.cu b/cpp/src/mip/presolve/probing_cache.cu index fc2d974e3..c0c9f2f0a 100644 --- a/cpp/src/mip/presolve/probing_cache.cu +++ b/cpp/src/mip/presolve/probing_cache.cu @@ -856,17 +856,18 @@ bool compute_probing_cache(bound_presolve_t& bound_presolve, bound_presolve.settings.iteration_limit = 50; bound_presolve.settings.time_limit = timer.remaining_time(); - // Set the number of threads - const size_t max_threads = 8; - omp_set_num_threads(max_threads); + size_t num_threads = bound_presolve.settings.num_threads < 0 + ? 0.2 * omp_get_max_threads() + : bound_presolve.settings.num_threads; + num_threads = std::clamp(num_threads, 1, 8); // Create a vector of multi_probe_t objects std::vector> multi_probe_presolve_pool; - std::vector>> modification_vector_pool(max_threads); - std::vector>> substitution_vector_pool(max_threads); + std::vector>> modification_vector_pool(num_threads); + std::vector>> substitution_vector_pool(num_threads); // Initialize multi_probe_presolve_pool - for (size_t i = 0; i < max_threads; i++) { + for (size_t i = 0; i < num_threads; i++) { multi_probe_presolve_pool.emplace_back(bound_presolve.context); multi_probe_presolve_pool[i].resize(problem); multi_probe_presolve_pool[i].compute_stats = true; @@ -879,13 +880,15 @@ bool compute_probing_cache(bound_presolve_t& bound_presolve, size_t last_it_implied_singletons = 0; bool early_exit = false; const size_t step_size = min((size_t)2048, priority_indices.size()); - for (size_t step_start = 0; step_start < priority_indices.size(); step_start += step_size) { - if (timer.check_time_limit() || early_exit || problem_is_infeasible.load()) { break; } - size_t step_end = std::min(step_start + step_size, priority_indices.size()); + // Main parallel loop -#pragma omp parallel - { -#pragma omp for schedule(static, 4) +#pragma omp parallel num_threads(num_threads) + { + for (size_t step_start = 0; step_start < priority_indices.size(); step_start += step_size) { + if (timer.check_time_limit() || early_exit || problem_is_infeasible.load()) { break; } + size_t step_end = std::min(step_start + step_size, priority_indices.size()); + +#pragma omp for for (size_t i = step_start; i < step_end; ++i) { auto var_idx = priority_indices[i]; if (timer.check_time_limit()) { continue; } diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 9e9b74a2e..8feaee523 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -1988,6 +1988,13 @@ void problem_t::get_host_user_problem( : cuopt::linear_programming::dual_simplex::variable_type_t::INTEGER; } } + +template +f_t problem_t::get_solver_obj_from_user_obj(f_t user_obj) const +{ + return (user_obj / presolve_data.objective_scaling_factor) - presolve_data.objective_offset; +} + template f_t problem_t::get_user_obj_from_solver_obj(f_t solver_obj) const { diff --git a/cpp/src/mip/problem/problem.cuh b/cpp/src/mip/problem/problem.cuh index 9719f0b54..910079916 100644 --- a/cpp/src/mip/problem/problem.cuh +++ b/cpp/src/mip/problem/problem.cuh @@ -91,6 +91,7 @@ class problem_t { void post_process_solution(solution_t& solution); void compute_transpose_of_problem(); f_t get_user_obj_from_solver_obj(f_t solver_obj) const; + f_t get_solver_obj_from_user_obj(f_t user_obj) const; bool is_objective_integral() const { return objective_is_integral; } void compute_integer_fixed_problem(); void fill_integer_fixed_problem(rmm::device_uvector& assignment,