Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions cpp/src/dual_simplex/branch_and_bound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,8 @@ lp_status_t branch_and_bound_t<i_t, f_t>::solve_root_relaxation(
}

if (root_crossover_solution_set_.load(std::memory_order_acquire)) {
settings_.log.printf("\nRunning crossover\n\n");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this print. I don't think we want to interrupt the dual simplex logs for the root solve, unless we know that crossover was successful.


// Crush the root relaxation solution on converted user problem
std::vector<f_t> crushed_root_x;
crush_primal_solution(
Expand Down Expand Up @@ -1338,23 +1340,24 @@ lp_status_t branch_and_bound_t<i_t, f_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) {
settings_.log.printf("\nCrossover found an optimal solution for the root relaxation\n\n");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we should indicate that PDLP/Barrier won. Can you use the is_pdlp_solution to determine which of them won and print it here?

Copy link
Contributor Author

@nguidotti nguidotti Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not find a way to get the winner solver from the optimization_problem_solution_t unless we change the API (#787). This object is user-facing, so it needs to be reflect on the Python side as well.

root_solver_type_ = root_solver_type_t::CROSSOVER;

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;
} else {
root_status = root_status_future.get();
root_status = root_status_future.get();
root_solver_type_ = root_solver_type_t::DUAL_SIMPLEX;
}
} else {
root_status = root_status_future.get();
root_status = root_status_future.get();
root_solver_type_ = root_solver_type_t::DUAL_SIMPLEX;
}
return root_status;
}
Expand Down Expand Up @@ -1414,14 +1417,13 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_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,
Expand All @@ -1430,6 +1432,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
edge_norms_);

} else {
settings_.log.printf("\nSolving LP root relaxation in concurrent mode\n");
root_status = solve_root_relaxation(lp_settings);
}

Expand Down Expand Up @@ -1540,10 +1543,11 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
original_lp_,
log);

settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n",
settings_.num_threads,
settings_.num_bfs_workers,
settings_.num_threads - settings_.num_bfs_workers);
settings_.log.printf(
"\nExploring 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);

exploration_stats_.nodes_explored = 0;
exploration_stats_.nodes_unexplored = 2;
Expand Down
30 changes: 21 additions & 9 deletions cpp/src/dual_simplex/branch_and_bound.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ struct bnb_stats_t {
template <typename i_t, typename f_t>
class branch_and_bound_t {
public:
// Specify which solver was used for solving the root LP relaxation
enum class root_solver_type_t { NONE = 0, CROSSOVER = 1, DUAL_SIMPLEX = 2 };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Can we just determine who won in solve_root_relaxation?


branch_and_bound_t(const user_problem_t<i_t, f_t>& user_problem,
const simplex_solver_settings_t<i_t, f_t>& solver_settings);

Expand All @@ -82,16 +85,24 @@ class branch_and_bound_t {
const std::vector<f_t>& reduced_costs,
f_t objective,
f_t user_objective,
i_t iterations)
i_t iterations,
f_t solve_time)
{
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 (root_solver_type_ == root_solver_type_t::NONE) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this printing to inside solve_root_relaxation and only print when crossover is successful.

Otherwise we could tell the user we have found a solution with PDLP/Barrier. And then the user could wait a long time only to have us use the dual simplex solution because we either: a) failed in crossover, or b) crossover took a very long time.

settings_.log.printf(
"\nRoot relaxation solution found in %d iterations and %.2fs by PDLP/Barrier\n",
iterations,
solve_time);
settings_.log.printf("Root relaxation objective = %+.8e\n", user_objective);
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
Expand Down Expand Up @@ -162,6 +173,7 @@ class branch_and_bound_t {
std::atomic<bool> root_crossover_solution_set_{false};
bool enable_concurrent_lp_root_solve_{false};
std::atomic<int> root_concurrent_halt_{0};
std::atomic<root_solver_type_t> root_solver_type_{root_solver_type_t::NONE};
Copy link
Contributor

@hlinsen hlinsen Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be an atomic? I think we can never have a race condition because we write to this var only when one of the solves is optimal which means the other solver returned with halt status.


// Pseudocosts
pseudo_costs_t<i_t, f_t> pc_;
Expand Down
85 changes: 51 additions & 34 deletions cpp/src/linear_programming/solve.cu
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -42,6 +42,9 @@

#include <thread> // 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
Expand Down Expand Up @@ -417,7 +420,8 @@ run_barrier(dual_simplex::user_problem_t<i_t, f_t>& user_problem,
auto status = dual_simplex::solve_linear_program_with_barrier<i_t, f_t>(
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 ||
Expand Down Expand Up @@ -489,9 +493,10 @@ run_dual_simplex(dual_simplex::user_problem_t<i_t, f_t>& user_problem,
auto status =
dual_simplex::solve_linear_program<i_t, f_t>(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 ||
Expand Down Expand Up @@ -530,7 +535,9 @@ static optimization_problem_solution_t<i_t, f_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<i_t, f_t>{pdlp_termination_status_t::NumericalError,
problem.handle_ptr->get_stream()};
}
Expand All @@ -551,14 +558,16 @@ optimization_problem_solution_t<i_t, f_t> run_pdlp(detail::problem_t<i_t, f_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;
Expand Down Expand Up @@ -620,12 +629,13 @@ optimization_problem_solution_t<i_t, f_t> run_pdlp(detail::problem_t<i_t, f_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;
Expand Down Expand Up @@ -653,7 +663,7 @@ optimization_problem_solution_t<i_t, f_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
Expand All @@ -668,7 +678,8 @@ optimization_problem_solution_t<i_t, f_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");
}
Expand Down Expand Up @@ -752,41 +763,47 @@ optimization_problem_solution_t<i_t, f_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;
}
}
Expand Down
17 changes: 8 additions & 9 deletions cpp/src/mip/diversity/diversity_manager.cu
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ solution_t<i_t, f_t> diversity_manager_t<i_t, f_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();
Expand All @@ -434,15 +434,14 @@ solution_t<i_t, f_t> diversity_manager_t<i_t, f_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);
auto solve_time = lp_result.get_additional_termination_information().solve_time;
// 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, solve_time);
}

// in case the pdlp returned var boudns that are out of bounds
Expand Down
5 changes: 4 additions & 1 deletion cpp/src/mip/presolve/bounds_presolve.cuh
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -21,6 +21,8 @@
#include "bounds_update_data.cuh"
#include "utils.cuh"

#include <omp.h>

namespace cuopt::linear_programming::detail {

template <typename i_t, typename f_t>
Expand All @@ -32,6 +34,7 @@ class bound_presolve_t {
struct settings_t {
f_t time_limit{60.0};
i_t iteration_limit{std::numeric_limits<i_t>::max()};
i_t num_threads = -1;
bool parallel_bounds_update{true};
};

Expand Down
Loading
Loading