From 7832d495ffdaf938eef0b77220c07b59d72eb839 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:35:38 +0100 Subject: [PATCH 01/22] calculate reduced cost fixing for every incumbent update. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 149 ++++++++++++++---- cpp/src/branch_and_bound/branch_and_bound.hpp | 18 ++- cpp/src/mip_heuristics/solver.cu | 3 +- 3 files changed, 131 insertions(+), 39 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index ba2b63983e..02bc7dad0e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -43,7 +43,6 @@ #include namespace cuopt::linear_programming::dual_simplex { - namespace { template @@ -383,13 +382,14 @@ void branch_and_bound_t::report( template i_t branch_and_bound_t::find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, - std::vector& upper_bounds) + std::vector& upper_bounds, + std::vector& bounds_changed) { - std::vector reduced_costs = root_relax_soln_.z; - lower_bounds = original_lp_.lower; - upper_bounds = original_lp_.upper; - std::vector bounds_changed(original_lp_.num_cols, false); - const f_t root_obj = compute_objective(original_lp_, root_relax_soln_.x); + std::vector& reduced_costs = root_relax_soln_.z; + lower_bounds = original_lp_.lower; + upper_bounds = original_lp_.upper; + bounds_changed.assign(original_lp_.num_cols, false); + const f_t threshold = 100.0 * settings_.integer_tol; const f_t weaken = settings_.integer_tol; const f_t fixed_tol = settings_.fixed_tol; @@ -401,7 +401,7 @@ i_t branch_and_bound_t::find_reduced_cost_fixings(f_t upper_bound, if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold) { const f_t lower_j = original_lp_.lower[j]; const f_t upper_j = original_lp_.upper[j]; - const f_t abs_gap = upper_bound - root_obj; + const f_t abs_gap = upper_bound - root_objective_; f_t reduced_cost_upper_bound = upper_j; f_t reduced_cost_lower_bound = lower_j; if (lower_j > -inf && reduced_costs[j] > 0) { @@ -439,6 +439,25 @@ i_t branch_and_bound_t::find_reduced_cost_fixings(f_t upper_bound, } return num_fixed; } +template +bool branch_and_bound_t::update_root_bounds(const std::vector& lower_bounds, + const std::vector& upper_bounds, + const std::vector& bounds_changed) +{ + std::vector row_sense; + bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); + + mutex_original_lp_.lock(); + original_lp_.lower = lower_bounds; + original_lp_.upper = upper_bounds; + bool feasible = node_presolve.bounds_strengthening( + settings_, bounds_changed, original_lp_.lower, original_lp_.upper); + mutex_original_lp_.unlock(); + + was_bounds_at_root_updated = true; + + return feasible; +} template void branch_and_bound_t::update_user_bound(f_t lower_bound) @@ -457,15 +476,18 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu "Solution size mismatch %ld %d\n", solution.size(), original_problem_.num_cols); } std::vector crushed_solution; + std::vector lower_bound; + std::vector upper_bound; + std::vector bounds_changed; crush_primal_solution( original_problem_, original_lp_, solution, new_slacks_, crushed_solution); f_t obj = compute_objective(original_lp_, crushed_solution); mutex_original_lp_.unlock(); - bool is_feasible = false; - bool attempt_repair = false; - mutex_upper_.lock(); + bool is_feasible = false; + bool attempt_repair = false; f_t current_upper_bound = upper_bound_; - mutex_upper_.unlock(); + i_t num_fixed = 0; + if (obj < current_upper_bound) { f_t primal_err; f_t bound_err; @@ -484,6 +506,14 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu if (is_feasible && obj < upper_bound_) { upper_bound_ = obj; incumbent_.set_incumbent_solution(obj, crushed_solution); + + if (settings_.reduced_cost_strengthening >= 3) { + num_fixed = find_reduced_cost_fixings(obj, lower_bound, upper_bound, bounds_changed); + settings_.log.printf("H: Applied reduced cost fixing. obj=%.10e. num_fixed=%d.\n", + compute_user_objective(original_lp_, obj), + num_fixed); + } + } else { attempt_repair = true; constexpr bool verbose = false; @@ -507,6 +537,14 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu repair_queue_.push_back(solution); mutex_repair_.unlock(); } + + if (num_fixed > 0) { + bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); + if (!feasible) { + settings_.log.printf("Bound strenghtening failed when updating the bounds at the root node!"); + solver_status_ = mip_status_t::NUMERICAL; + } + } } template @@ -635,7 +673,12 @@ void branch_and_bound_t::repair_heuristic_solutions() crush_primal_solution( original_problem_, original_lp_, uncrushed_solution, new_slacks_, crushed_solution); std::vector repaired_solution; + std::vector lower_bound; + std::vector upper_bound; + std::vector bounds_changed; + i_t num_fixed = 0; f_t repaired_obj; + bool is_feasible = repair_solution(edge_norms_, crushed_solution, repaired_obj, repaired_solution); if (is_feasible) { @@ -651,9 +694,27 @@ void branch_and_bound_t::repair_heuristic_solutions() uncrush_primal_solution(original_problem_, original_lp_, repaired_solution, original_x); settings_.solution_callback(original_x, repaired_obj); } + + if (settings_.reduced_cost_strengthening >= 3) { + num_fixed = find_reduced_cost_fixings( + upper_bound_.load(), lower_bound, upper_bound, bounds_changed); + settings_.log.printf( + "Repair H: Applied reduced cost fixing. obj=%.10e. num_fixed=%d.\n", + compute_user_objective(original_lp_, repaired_obj), + num_fixed); + } } mutex_upper_.unlock(); + + if (num_fixed > 0) { + bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); + if (!feasible) { + settings_.log.printf( + "Bound strenghtening failed when updating the bounds at the root node!"); + solver_status_ = mip_status_t::NUMERICAL; + } + } } } } @@ -778,6 +839,11 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, search_strategy_t thread_type) { bool send_solution = false; + i_t num_fixed = 0; + + std::vector lower_bound; + std::vector upper_bound; + std::vector bounds_changed; settings_.log.debug("%c found a feasible solution with obj=%.10e.\n", feasible_solution_symbol(thread_type), @@ -789,6 +855,15 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, upper_bound_ = leaf_objective; report(feasible_solution_symbol(thread_type), leaf_objective, get_lower_bound(), leaf_depth, 0); send_solution = true; + + if (settings_.reduced_cost_strengthening >= 3) { + num_fixed = + find_reduced_cost_fixings(leaf_objective, lower_bound, upper_bound, bounds_changed); + settings_.log.printf("%c: Applying reduced cost fixing. obj=%.10e. num_fixed=%d.\n", + feasible_solution_symbol(thread_type), + compute_user_objective(original_lp_, leaf_objective), + num_fixed); + } } if (send_solution && settings_.solution_callback != nullptr) { @@ -797,6 +872,14 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, settings_.solution_callback(original_x, upper_bound_); } mutex_upper_.unlock(); + + if (num_fixed > 0) { + bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); + if (!feasible) { + settings_.log.printf("Bound strenghtening failed when updating the bounds at the root node!"); + solver_status_ = mip_status_t::NUMERICAL; + } + } } // Martin's criteria for the preferred rounding direction (see [1]) @@ -1175,7 +1258,6 @@ std::pair branch_and_bound_t::upd dual::status_t lp_status, Policy& policy) { - constexpr f_t inf = std::numeric_limits::infinity(); const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; lp_problem_t& leaf_problem = worker->leaf_problem; lp_solution_t& leaf_solution = worker->leaf_solution; @@ -2275,32 +2357,35 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::NUMERICAL; } + std::vector bounds_changed; + std::vector new_lower; + std::vector new_upper; + if (settings_.reduced_cost_strengthening >= 1 && upper_bound_.load() < last_upper_bound) { mutex_upper_.lock(); last_upper_bound = upper_bound_.load(); - std::vector lower_bounds; - std::vector upper_bounds; - find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); + find_reduced_cost_fixings(upper_bound_.load(), new_lower, new_upper, bounds_changed); mutex_upper_.unlock(); mutex_original_lp_.lock(); - original_lp_.lower = lower_bounds; - original_lp_.upper = upper_bounds; + original_lp_.lower = new_lower; + original_lp_.upper = new_upper; mutex_original_lp_.unlock(); } - // Try to do bound strengthening - std::vector bounds_changed(original_lp_.num_cols, true); - std::vector row_sense; #ifdef CHECK_MATRICES settings_.log.printf("Before A check\n"); original_lp_.A.check_matrix(); #endif original_lp_.A.to_compressed_row(Arow_); + // Try to do bound strengthening + std::vector row_sense; f_t node_presolve_start_time = tic(); bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); - std::vector new_lower = original_lp_.lower; - std::vector new_upper = original_lp_.upper; + bounds_changed.assign(original_lp_.num_cols, true); + new_lower = original_lp_.lower; + new_upper = original_lp_.upper; + bool feasible = node_presolve.bounds_strengthening(settings_, bounds_changed, new_lower, new_upper); mutex_original_lp_.lock(); @@ -2470,19 +2555,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { std::vector lower_bounds; std::vector upper_bounds; - i_t num_fixed = find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); - if (num_fixed > 0) { - std::vector bounds_changed(original_lp_.num_cols, true); - std::vector row_sense; + std::vector bounds_changed; - bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); + mutex_upper_.lock(); + i_t num_fixed = + find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds, bounds_changed); + mutex_upper_.unlock(); - mutex_original_lp_.lock(); - original_lp_.lower = lower_bounds; - original_lp_.upper = upper_bounds; - bool feasible = node_presolve.bounds_strengthening( - settings_, bounds_changed, original_lp_.lower, original_lp_.upper); - mutex_original_lp_.unlock(); + if (num_fixed > 0) { + bool feasible = update_root_bounds(lower_bounds, upper_bounds, bounds_changed); if (!feasible) { settings_.log.printf("Bound strengthening failed\n"); return mip_status_t::NUMERICAL; // We had a feasible integer solution, but bound diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 98674b7f9e..b23d624cc3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -133,10 +133,6 @@ class branch_and_bound_t { std::vector& nonbasic_list, std::vector& edge_norms); - i_t find_reduced_cost_fixings(f_t upper_bound, - std::vector& lower_bounds, - std::vector& upper_bounds); - // The main entry routine. Returns the solver status and populates solution with the incumbent. mip_status_t solve(mip_solution_t& solution); @@ -176,6 +172,10 @@ class branch_and_bound_t { // when modifying the original LP. omp_mutex_t mutex_original_lp_; + // If the reduced cost strengthening modified the bounds of the root LP problem,, + // then all workers must also update its local bounds. + omp_atomic_t was_bounds_at_root_updated; + // Mutex for upper bound omp_mutex_t mutex_upper_; @@ -307,6 +307,16 @@ class branch_and_bound_t { dual::status_t lp_status, logger_t& log); + i_t find_reduced_cost_fixings(f_t upper_bound, + std::vector& lower_bounds, + std::vector& upper_bounds, + std::vector& bounds_changed); + bool update_root_bounds(const std::vector& lower_bounds, + const std::vector& upper_bounds, + const std::vector& bounds_changed); + + i_t apply_reduced_cost_fixings(); + // ============================================================================ // Deterministic BSP (Bulk Synchronous Parallel) methods for deterministic parallel B&B // ============================================================================ diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index f25c093afb..7c8d6747d9 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -224,7 +224,8 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening; + context.settings.reduced_cost_strengthening < 0 ? 3 + : context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; branch_and_bound_settings.mip_batch_pdlp_strong_branching = From 254de1582149004eb9f2118acd649da3d3ff2816 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:16:43 +0100 Subject: [PATCH 02/22] apply reduced cost fixing locally before branching --- cpp/src/branch_and_bound/branch_and_bound.cpp | 176 +++++++++--------- cpp/src/branch_and_bound/branch_and_bound.hpp | 15 +- .../branch_and_bound/reduced_cost_fixing.hpp | 79 ++++++++ .../dual_simplex/simplex_solver_settings.hpp | 15 +- cpp/src/mip_heuristics/solver.cu | 2 +- 5 files changed, 180 insertions(+), 107 deletions(-) create mode 100644 cpp/src/branch_and_bound/reduced_cost_fixing.hpp diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 02bc7dad0e..70a92017f1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -5,7 +5,9 @@ */ /* clang-format on */ +#include #include +#include #include #include @@ -35,11 +37,9 @@ #include #include #include -#include #include #include #include -#include #include namespace cuopt::linear_programming::dual_simplex { @@ -379,66 +379,6 @@ void branch_and_bound_t::report( } } -template -i_t branch_and_bound_t::find_reduced_cost_fixings(f_t upper_bound, - std::vector& lower_bounds, - std::vector& upper_bounds, - std::vector& bounds_changed) -{ - std::vector& reduced_costs = root_relax_soln_.z; - lower_bounds = original_lp_.lower; - upper_bounds = original_lp_.upper; - bounds_changed.assign(original_lp_.num_cols, false); - - const f_t threshold = 100.0 * settings_.integer_tol; - const f_t weaken = settings_.integer_tol; - const f_t fixed_tol = settings_.fixed_tol; - i_t num_improved = 0; - i_t num_fixed = 0; - i_t num_cols_to_check = reduced_costs.size(); // Reduced costs will be smaller than the original - // problem because we have added slacks for cuts - for (i_t j = 0; j < num_cols_to_check; j++) { - if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold) { - const f_t lower_j = original_lp_.lower[j]; - const f_t upper_j = original_lp_.upper[j]; - const f_t abs_gap = upper_bound - root_objective_; - f_t reduced_cost_upper_bound = upper_j; - f_t reduced_cost_lower_bound = lower_j; - if (lower_j > -inf && reduced_costs[j] > 0) { - const f_t new_upper_bound = lower_j + abs_gap / reduced_costs[j]; - reduced_cost_upper_bound = var_types_[j] == variable_type_t::INTEGER - ? std::floor(new_upper_bound + weaken) - : new_upper_bound; - if (reduced_cost_upper_bound < upper_j && var_types_[j] == variable_type_t::INTEGER) { - num_improved++; - upper_bounds[j] = reduced_cost_upper_bound; - bounds_changed[j] = true; - } - } - if (upper_j < inf && reduced_costs[j] < 0) { - const f_t new_lower_bound = upper_j + abs_gap / reduced_costs[j]; - reduced_cost_lower_bound = var_types_[j] == variable_type_t::INTEGER - ? std::ceil(new_lower_bound - weaken) - : new_lower_bound; - if (reduced_cost_lower_bound > lower_j && var_types_[j] == variable_type_t::INTEGER) { - num_improved++; - lower_bounds[j] = reduced_cost_lower_bound; - bounds_changed[j] = true; - } - } - if (var_types_[j] == variable_type_t::INTEGER && - reduced_cost_upper_bound <= reduced_cost_lower_bound + fixed_tol) { - num_fixed++; - } - } - } - - if (num_fixed > 0 || num_improved > 0) { - settings_.log.printf( - "Reduced costs: Found %d improved bounds and %d fixed variables\n", num_improved, num_fixed); - } - return num_fixed; -} template bool branch_and_bound_t::update_root_bounds(const std::vector& lower_bounds, const std::vector& upper_bounds, @@ -454,8 +394,6 @@ bool branch_and_bound_t::update_root_bounds(const std::vector& lo settings_, bounds_changed, original_lp_.lower, original_lp_.upper); mutex_original_lp_.unlock(); - was_bounds_at_root_updated = true; - return feasible; } @@ -508,10 +446,17 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu incumbent_.set_incumbent_solution(obj, crushed_solution); if (settings_.reduced_cost_strengthening >= 3) { - num_fixed = find_reduced_cost_fixings(obj, lower_bound, upper_bound, bounds_changed); - settings_.log.printf("H: Applied reduced cost fixing. obj=%.10e. num_fixed=%d.\n", - compute_user_objective(original_lp_, obj), - num_fixed); + lower_bound = original_lp_.lower; + upper_bound = original_lp_.upper; + num_fixed = find_reduced_cost_fixings(original_lp_, + root_relax_soln_.z, + var_types_, + root_objective_, + obj, + lower_bound, + upper_bound, + bounds_changed, + settings_); } } else { @@ -696,8 +641,17 @@ void branch_and_bound_t::repair_heuristic_solutions() } if (settings_.reduced_cost_strengthening >= 3) { - num_fixed = find_reduced_cost_fixings( - upper_bound_.load(), lower_bound, upper_bound, bounds_changed); + lower_bound = original_lp_.lower; + upper_bound = original_lp_.upper; + num_fixed = find_reduced_cost_fixings(original_lp_, + root_relax_soln_.z, + var_types_, + root_objective_, + repaired_obj, + lower_bound, + upper_bound, + bounds_changed, + settings_); settings_.log.printf( "Repair H: Applied reduced cost fixing. obj=%.10e. num_fixed=%d.\n", compute_user_objective(original_lp_, repaired_obj), @@ -857,12 +811,17 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, send_solution = true; if (settings_.reduced_cost_strengthening >= 3) { - num_fixed = - find_reduced_cost_fixings(leaf_objective, lower_bound, upper_bound, bounds_changed); - settings_.log.printf("%c: Applying reduced cost fixing. obj=%.10e. num_fixed=%d.\n", - feasible_solution_symbol(thread_type), - compute_user_objective(original_lp_, leaf_objective), - num_fixed); + lower_bound = original_lp_.lower; + upper_bound = original_lp_.upper; + num_fixed = find_reduced_cost_fixings(original_lp_, + root_relax_soln_.z, + var_types_, + root_objective_, + leaf_objective, + lower_bound, + upper_bound, + bounds_changed, + settings_); } } @@ -1265,6 +1224,9 @@ std::pair branch_and_bound_t::upd node_status_t status = node_status_t::PENDING; rounding_direction_t round_dir = rounding_direction_t::NONE; + worker->recompute_basis = true; + worker->recompute_bounds = true; + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { node_ptr->lower_bound = inf; policy.graphviz(search_tree, node_ptr, "infeasible", 0.0); @@ -1324,6 +1286,29 @@ std::pair branch_and_bound_t::upd assert(dir != rounding_direction_t::NONE); policy.update_objective_estimate(node_ptr, leaf_fractional, leaf_solution.x); + worker->recompute_basis = false; + worker->recompute_bounds = false; + + if (settings_.reduced_cost_strengthening >= 4) { + i_t num_fixed = find_reduced_cost_fixings(leaf_problem, + leaf_solution.z, + var_types_, + leaf_obj, + upper_bound, + leaf_problem.lower, + leaf_problem.upper, + worker->bounds_changed, + settings_); + if (num_fixed > 0) { + bool feasible = worker->node_presolver.bounds_strengthening( + settings_, worker->bounds_changed, leaf_problem.lower, leaf_problem.upper); + if (!feasible) { + // If bounds strenghtening failed after applying reduced cost fixing at the node, + // recompute the bounds from the beginning + worker->recompute_bounds = true; + } + } + } logger_t log; log.log = false; @@ -1563,10 +1548,6 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_trecompute_basis = node_status != node_status_t::HAS_CHILDREN; - worker->recompute_bounds = node_status != node_status_t::HAS_CHILDREN; - if (node_status == node_status_t::HAS_CHILDREN) { // The stack should only contain the children of the current parent. // If the stack size is greater than 0, @@ -1661,9 +1642,6 @@ void branch_and_bound_t::dive_with(branch_and_bound_worker_t auto [node_status, round_dir] = update_tree(node_ptr, dive_tree, worker, lp_status, log); - worker->recompute_basis = node_status != node_status_t::HAS_CHILDREN; - worker->recompute_bounds = node_status != node_status_t::HAS_CHILDREN; - if (node_status == node_status_t::HAS_CHILDREN) { if (round_dir == rounding_direction_t::UP) { stack.push_front(node_ptr->get_down_child()); @@ -2362,9 +2340,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::vector new_upper; if (settings_.reduced_cost_strengthening >= 1 && upper_bound_.load() < last_upper_bound) { + mutex_original_lp_.lock(); + new_lower = original_lp_.lower; + new_upper = original_lp_.upper; + mutex_original_lp_.unlock(); mutex_upper_.lock(); last_upper_bound = upper_bound_.load(); - find_reduced_cost_fixings(upper_bound_.load(), new_lower, new_upper, bounds_changed); + find_reduced_cost_fixings(original_lp_, + root_relax_soln_.z, + var_types_, + compute_objective(original_lp_, root_relax_soln_.x), + upper_bound_.load(), + new_lower, + new_upper, + bounds_changed, + settings_); mutex_upper_.unlock(); mutex_original_lp_.lock(); original_lp_.lower = new_lower; @@ -2553,13 +2543,23 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { - std::vector lower_bounds; - std::vector upper_bounds; std::vector bounds_changed; + mutex_original_lp_.lock(); + std::vector lower_bounds = original_lp_.lower; + std::vector upper_bounds = original_lp_.upper; + mutex_original_lp_.unlock(); + mutex_upper_.lock(); - i_t num_fixed = - find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds, bounds_changed); + i_t num_fixed = find_reduced_cost_fixings(original_lp_, + root_relax_soln_.z, + var_types_, + root_objective_, + upper_bound_.load(), + lower_bounds, + upper_bounds, + bounds_changed, + settings_); mutex_upper_.unlock(); if (num_fixed > 0) { diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index b23d624cc3..73124992be 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -30,8 +29,6 @@ #include #include -#include - #include #include #include @@ -172,10 +169,6 @@ class branch_and_bound_t { // when modifying the original LP. omp_mutex_t mutex_original_lp_; - // If the reduced cost strengthening modified the bounds of the root LP problem,, - // then all workers must also update its local bounds. - omp_atomic_t was_bounds_at_root_updated; - // Mutex for upper bound omp_mutex_t mutex_upper_; @@ -307,16 +300,10 @@ class branch_and_bound_t { dual::status_t lp_status, logger_t& log); - i_t find_reduced_cost_fixings(f_t upper_bound, - std::vector& lower_bounds, - std::vector& upper_bounds, - std::vector& bounds_changed); - bool update_root_bounds(const std::vector& lower_bounds, + bool update_root_bounds(const std::vector& lower_bounds, const std::vector& upper_bounds, const std::vector& bounds_changed); - i_t apply_reduced_cost_fixings(); - // ============================================================================ // Deterministic BSP (Bulk Synchronous Parallel) methods for deterministic parallel B&B // ============================================================================ diff --git a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp new file mode 100644 index 0000000000..511309b94f --- /dev/null +++ b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp @@ -0,0 +1,79 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +i_t find_reduced_cost_fixings(const lp_problem_t& lp, + const std::vector& reduced_costs, + const std::vector& var_types, + f_t obj, + f_t upper_bound, + std::vector& lower_bounds, + std::vector& upper_bounds, + std::vector& bounds_changed, + const simplex_solver_settings_t& settings) +{ + const f_t threshold = 100.0 * settings.integer_tol; + const f_t weaken = settings.integer_tol; + const f_t fixed_tol = settings.fixed_tol; + i_t num_improved = 0; + i_t num_fixed = 0; + i_t num_cols_to_check = reduced_costs.size(); // Reduced costs will be smaller than the original + // problem because we have added slacks for cuts + + bounds_changed.assign(lp.num_cols, false); + + for (i_t j = 0; j < num_cols_to_check; j++) { + if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold) { + const f_t lower_j = lp.lower[j]; + const f_t upper_j = lp.upper[j]; + const f_t abs_gap = upper_bound - obj; + f_t reduced_cost_upper_bound = upper_j; + f_t reduced_cost_lower_bound = lower_j; + if (lower_j > -inf && reduced_costs[j] > 0) { + const f_t new_upper_bound = lower_j + abs_gap / reduced_costs[j]; + reduced_cost_upper_bound = var_types[j] == variable_type_t::INTEGER + ? std::floor(new_upper_bound + weaken) + : new_upper_bound; + if (reduced_cost_upper_bound < upper_j && var_types[j] == variable_type_t::INTEGER) { + ++num_improved; + upper_bounds[j] = reduced_cost_upper_bound; + bounds_changed[j] = true; + } + } + if (upper_j < inf && reduced_costs[j] < 0) { + const f_t new_lower_bound = upper_j + abs_gap / reduced_costs[j]; + reduced_cost_lower_bound = var_types[j] == variable_type_t::INTEGER + ? std::ceil(new_lower_bound - weaken) + : new_lower_bound; + if (reduced_cost_lower_bound > lower_j && var_types[j] == variable_type_t::INTEGER) { + ++num_improved; + lower_bounds[j] = reduced_cost_lower_bound; + bounds_changed[j] = true; + } + } + if (var_types[j] == variable_type_t::INTEGER && + reduced_cost_upper_bound <= reduced_cost_lower_bound + fixed_tol) { + ++num_fixed; + } + } + } + + if (num_fixed > 0 || num_improved > 0) { + settings.log.debug( + "Reduced costs: Found %d improved bounds and %d fixed variables\n", num_improved, num_fixed); + } + return num_fixed; +} + +} // namespace cuopt::linear_programming::dual_simplex \ No newline at end of file diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index eadd93040c..9739e17e1c 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -183,10 +183,17 @@ struct simplex_solver_settings_t { i_t clique_cuts; // -1 automatic, 0 to disable, >0 to enable clique cuts i_t strong_chvatal_gomory_cuts; // -1 automatic, 0 to disable, >0 to enable strong Chvatal Gomory // cuts - i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost - // strengthening - f_t cut_change_threshold; // threshold for cut change - f_t cut_min_orthogonality; // minimum orthogonality for cuts + // >= 0 adds additional reduced cost strengthening passes at each level + // -1 - automatic + // 0 - disable + // 1 - apply to root after each cut pass + // 2 - apply to root after all cuts + // 3 - apply to root after each incumbent update + // 4 - apply to the current node before branching, if it generates children + i_t reduced_cost_strengthening; + // strengthening + f_t cut_change_threshold; // threshold for cut change + f_t cut_min_orthogonality; // minimum orthogonality for cuts i_t mip_batch_pdlp_strong_branching{0}; // 0 if not using batch PDLP for strong branching, 1 if // using batch PDLP for strong branching diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 7c8d6747d9..ac97acc32e 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -224,7 +224,7 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening < 0 ? 3 + context.settings.reduced_cost_strengthening < 0 ? 4 : context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; From 237fffdc5cbf9cc6a14a9732fe34cd3cefe3d083 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:46:57 +0100 Subject: [PATCH 03/22] propagate bound changes at the root to all active workers. split worker and worker pool in two files. removed local reduced cost fixing. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 76 ++++---- cpp/src/branch_and_bound/branch_and_bound.hpp | 5 +- .../deterministic_workers.hpp | 10 +- cpp/src/branch_and_bound/mip_node.hpp | 61 +++++- cpp/src/branch_and_bound/pseudo_costs.hpp | 2 +- ...branch_and_bound_worker.hpp => worker.hpp} | 181 ++++-------------- cpp/src/branch_and_bound/worker_pool.hpp | 141 ++++++++++++++ .../dual_simplex/simplex_solver_settings.hpp | 1 - 8 files changed, 285 insertions(+), 192 deletions(-) rename cpp/src/branch_and_bound/{branch_and_bound_worker.hpp => worker.hpp} (50%) create mode 100644 cpp/src/branch_and_bound/worker_pool.hpp diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 70a92017f1..fdbd636520 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -5,11 +5,11 @@ */ /* clang-format on */ -#include #include #include #include #include +#include #include #include @@ -394,6 +394,8 @@ bool branch_and_bound_t::update_root_bounds(const std::vector& lo settings_, bounds_changed, original_lp_.lower, original_lp_.upper); mutex_original_lp_.unlock(); + worker_pool_.broadcast_root_bounds_change(); + return feasible; } @@ -1289,27 +1291,6 @@ std::pair branch_and_bound_t::upd worker->recompute_basis = false; worker->recompute_bounds = false; - if (settings_.reduced_cost_strengthening >= 4) { - i_t num_fixed = find_reduced_cost_fixings(leaf_problem, - leaf_solution.z, - var_types_, - leaf_obj, - upper_bound, - leaf_problem.lower, - leaf_problem.upper, - worker->bounds_changed, - settings_); - if (num_fixed > 0) { - bool feasible = worker->node_presolver.bounds_strengthening( - settings_, worker->bounds_changed, leaf_problem.lower, leaf_problem.upper); - if (!feasible) { - // If bounds strenghtening failed after applying reduced cost fixing at the node, - // recompute the bounds from the beginning - worker->recompute_bounds = true; - } - } - } - logger_t log; log.log = false; search_tree.branch(node_ptr, @@ -1765,6 +1746,17 @@ void branch_and_bound_t::run_scheduler() continue; } + bool feasible = + start_node.value()->check_variable_bounds(original_lp_.lower, original_lp_.upper); + if (!feasible) { + // This node was put on the heap earlier but its variables bounds now violates the bounds + // at the root node + search_tree_.graphviz_node( + settings_.log, start_node.value(), "cutoff", start_node.value()->lower_bound); + search_tree_.update(start_node.value(), node_status_t::FATHOMED); + continue; + } + // Remove the worker from the idle list. worker_pool_.pop_idle_worker(); worker->init_best_first(start_node.value(), original_lp_); @@ -3164,17 +3156,22 @@ node_status_t branch_and_bound_t::solve_node_deterministic( raft::common::nvtx::range scope("BB::solve_node_deterministic"); double work_units_at_start = worker.work_context.global_work_units_elapsed; + bool feasible = true; std::fill(worker.bounds_changed.begin(), worker.bounds_changed.end(), false); if (worker.recompute_bounds_and_basis) { - worker.leaf_problem.lower = original_lp_.lower; - worker.leaf_problem.upper = original_lp_.upper; - node_ptr->get_variable_bounds( - worker.leaf_problem.lower, worker.leaf_problem.upper, worker.bounds_changed); + feasible = node_ptr->get_variable_bounds(original_lp_.lower, + original_lp_.upper, + worker.leaf_problem.lower, + worker.leaf_problem.upper, + worker.bounds_changed); } else { - node_ptr->update_branched_variable_bounds( - worker.leaf_problem.lower, worker.leaf_problem.upper, worker.bounds_changed); + feasible = node_ptr->update_branched_variable_bounds(original_lp_.lower, + original_lp_.upper, + worker.leaf_problem.lower, + worker.leaf_problem.upper, + worker.bounds_changed); } double remaining_time = settings_.time_limit - toc(exploration_stats_.start_time); @@ -3188,11 +3185,12 @@ node_status_t branch_and_bound_t::solve_node_deterministic( lp_settings.time_limit = remaining_time; lp_settings.scale_columns = false; - bool feasible = true; #ifndef DETERMINISM_DISABLE_BOUNDS_STRENGTHENING raft::common::nvtx::range scope_bs("BB::bound_strengthening"); - feasible = worker.node_presolver.bounds_strengthening( - lp_settings, worker.bounds_changed, worker.leaf_problem.lower, worker.leaf_problem.upper); + if (feasible) { + feasible = worker.node_presolver.bounds_strengthening( + lp_settings, worker.bounds_changed, worker.leaf_problem.lower, worker.leaf_problem.upper); + } if (settings_.deterministic) { // TEMP APPROXIMATION; @@ -3779,13 +3777,17 @@ void branch_and_bound_t::deterministic_dive( std::fill(worker.bounds_changed.begin(), worker.bounds_changed.end(), false); if (worker.recompute_bounds_and_basis) { - worker.leaf_problem.lower = worker.dive_lower; - worker.leaf_problem.upper = worker.dive_upper; - node_ptr->get_variable_bounds( - worker.leaf_problem.lower, worker.leaf_problem.upper, worker.bounds_changed); + node_ptr->get_variable_bounds(worker.dive_lower, + worker.dive_upper, + worker.leaf_problem.lower, + worker.leaf_problem.upper, + worker.bounds_changed); } else { - node_ptr->update_branched_variable_bounds( - worker.leaf_problem.lower, worker.leaf_problem.upper, worker.bounds_changed); + node_ptr->update_branched_variable_bounds(worker.dive_lower, + worker.dive_upper, + worker.leaf_problem.lower, + worker.leaf_problem.upper, + worker.bounds_changed); } double remaining_time = settings_.time_limit - toc(exploration_stats_.start_time); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 73124992be..3e3bcddd14 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -8,11 +8,12 @@ #pragma once #include -#include #include #include #include #include +#include +#include #include @@ -99,7 +100,7 @@ class branch_and_bound_t { } } - // Set a solution based on the user problem during the course of the solve + // Set a solution based on the user problem during solve time void set_new_solution(const std::vector& solution); // This queues the solution to be processed at the correct work unit timestamp diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index 7a074051c6..1fa71f66cd 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -8,9 +8,9 @@ #pragma once #include -#include #include #include +#include #include @@ -316,10 +316,12 @@ class deterministic_diving_worker_t void enqueue_dive_node(mip_node_t* node, const lp_problem_t& original_lp) { dive_queue_entry_t entry; - entry.resolved_lower = original_lp.lower; - entry.resolved_upper = original_lp.upper; std::vector bounds_changed(original_lp.num_cols, false); - node->get_variable_bounds(entry.resolved_lower, entry.resolved_upper, bounds_changed); + node->get_variable_bounds(original_lp.lower, + original_lp.upper, + entry.resolved_lower, + entry.resolved_upper, + bounds_changed); entry.node = node->detach_copy(); dive_queue.push_back(std::move(entry)); } diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 58e1a6f683..6b49a5f4a6 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -98,22 +98,62 @@ class mip_node_t { children[1] = nullptr; } - void get_variable_bounds(std::vector& lower, + // Check if no node in the path violates the initial bounds after branching on a given variable. + // The bound violation can happen after the initial bounds is changed via reduced cost + // strengthening and one of the node in the path was created based on the old values. + bool check_variable_bounds(const std::vector& start_lower, + const std::vector& start_upper) + { + if (branch_var_upper > start_upper[branch_var] || branch_var_lower < start_lower[branch_var]) { + return false; + } + + mip_node_t* parent_ptr = parent; + while (parent_ptr != nullptr && parent_ptr->node_id != 0) { + if (parent_ptr->branch_var_upper > start_upper[parent_ptr->branch_var] || + parent_ptr->branch_var_lower < start_lower[parent_ptr->branch_var]) { + return false; + } + parent_ptr = parent_ptr->parent; + } + + return true; + } + + // Get the variable bounds starting at the current node and then traversing it back until the + // the root node. The bounds are initially set based on the `start_lower` and `start_upper`. + // Return true if all bounds are valid (i.e., no node in the path violates the initial bounds + // after branching on a given variable). + bool get_variable_bounds(const std::vector& start_lower, + const std::vector& start_upper, + std::vector& lower, std::vector& upper, std::vector& bounds_changed) const { - update_branched_variable_bounds(lower, upper, bounds_changed); + lower = start_lower; + upper = start_upper; + + bool feasible = + update_branched_variable_bounds(start_lower, start_upper, lower, upper, bounds_changed); + if (!feasible) { return false; } - mip_node_t* parent_ptr = parent; + mip_node_t* parent_ptr = parent; while (parent_ptr != nullptr && parent_ptr->node_id != 0) { - parent_ptr->update_branched_variable_bounds(lower, upper, bounds_changed); + feasible = parent_ptr->update_branched_variable_bounds( + start_lower, start_upper, lower, upper, bounds_changed); + if (!feasible) { return false; } parent_ptr = parent_ptr->parent; } + + return true; } // Here we assume that we are traversing from the deepest node to the - // root of the tree - void update_branched_variable_bounds(std::vector& lower, + // root of the tree. Return true if no bounds were violated in this node + // considering the starting bounds + bool update_branched_variable_bounds(const std::vector& start_lower, + const std::vector& start_upper, + std::vector& lower, std::vector& upper, std::vector& bounds_changed) const { @@ -122,15 +162,22 @@ class mip_node_t { assert(upper.size() > branch_var); assert(bounds_changed.size() > branch_var); + // If the start bounds has changed (via reduced cost strengthening), check if the + // bounds in the node is still valid. + if (branch_var_upper > start_upper[branch_var] || branch_var_lower < start_lower[branch_var]) { + return false; + } + // If the bounds have already been updated on another node, // skip this node as it contains looser bounds, since we // are traversing up the tree toward the root - if (bounds_changed[branch_var]) { return; } + if (bounds_changed[branch_var]) { return true; } // Apply the bounds at the current node lower[branch_var] = branch_var_lower; upper[branch_var] = branch_var_upper; bounds_changed[branch_var] = true; + return true; } mip_node_t* get_down_child() const { return children[0].get(); } diff --git a/cpp/src/branch_and_bound/pseudo_costs.hpp b/cpp/src/branch_and_bound/pseudo_costs.hpp index 6b6c6917b6..08970c0fcf 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.hpp +++ b/cpp/src/branch_and_bound/pseudo_costs.hpp @@ -7,8 +7,8 @@ #pragma once -#include #include +#include #include #include diff --git a/cpp/src/branch_and_bound/branch_and_bound_worker.hpp b/cpp/src/branch_and_bound/worker.hpp similarity index 50% rename from cpp/src/branch_and_bound/branch_and_bound_worker.hpp rename to cpp/src/branch_and_bound/worker.hpp index 4de2b43cae..a7fa3d3ec2 100644 --- a/cpp/src/branch_and_bound/branch_and_bound_worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -67,14 +67,15 @@ class branch_and_bound_worker_t { bounds_strengthening_t node_presolver; std::vector bounds_changed; - std::vector start_lower; - std::vector start_upper; + std::vector* start_lower; + std::vector* start_upper; mip_node_t* start_node; pcgenerator_t rng; - bool recompute_basis = true; - bool recompute_bounds = true; + bool recompute_basis = true; + bool recompute_bounds = true; + omp_atomic_t start_bounds_updated = false; branch_and_bound_worker_t(i_t worker_id, const lp_problem_t& original_lp, @@ -92,17 +93,20 @@ class branch_and_bound_worker_t { nonbasic_list(), node_presolver(leaf_problem, Arow, {}, var_type), bounds_changed(original_lp.num_cols, false), + start_lower(nullptr), + start_upper(nullptr), + start_node(nullptr), rng(settings.random_seed + pcgenerator_t::default_seed + worker_id, pcgenerator_t::default_stream ^ worker_id) { } // Set the `start_node` for best-first search. - void init_best_first(mip_node_t* node, const lp_problem_t& original_lp) + void init_best_first(mip_node_t* node, lp_problem_t& original_lp) { start_node = node; - start_lower = original_lp.lower; - start_upper = original_lp.upper; + start_lower = &original_lp.lower; + start_upper = &original_lp.upper; search_strategy = BEST_FIRST; lower_bound = node->lower_bound; is_active = true; @@ -110,42 +114,53 @@ class branch_and_bound_worker_t { // Initialize the worker for diving, setting the `start_node`, `start_lower` and // `start_upper`. Returns `true` if the starting node is feasible via - // bounds propagation. - bool init_diving(mip_node_t* node, + // bounds propagation and no bounds were violated in any of the previous nodes + bool init_diving(mip_node_t* node_ptr, search_strategy_t type, const lp_problem_t& original_lp, const simplex_solver_settings_t& settings) { - internal_node = node->detach_copy(); - start_node = &internal_node; - - start_lower = original_lp.lower; - start_upper = original_lp.upper; + internal_node = node_ptr->detach_copy(); + start_node = &internal_node; + start_lower = &internal_start_lower; + start_upper = &internal_start_upper; search_strategy = type; - lower_bound = node->lower_bound; + lower_bound = node_ptr->lower_bound; is_active = true; - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - node->get_variable_bounds(start_lower, start_upper, bounds_changed); - return node_presolver.bounds_strengthening(settings, bounds_changed, start_lower, start_upper); + + bool feasible = node_ptr->get_variable_bounds( + original_lp.lower, original_lp.upper, *start_lower, *start_upper, bounds_changed); + if (!feasible) { return false; } + + return node_presolver.bounds_strengthening( + settings, bounds_changed, *start_lower, *start_upper); } - // Set the variables bounds for the LP relaxation of the current node. + // Set the variables bounds for the LP relaxation in the current node. bool set_lp_variable_bounds(mip_node_t* node_ptr, const simplex_solver_settings_t& settings) { + // If the starting bounds were updated via reduced cost fixing, then + // recompute the bounds from scratch + if (start_bounds_updated) { + recompute_bounds = true; + start_bounds_updated = false; + } + // Reset the bound_changed markers std::fill(bounds_changed.begin(), bounds_changed.end(), false); // Set the correct bounds for the leaf problem if (recompute_bounds) { - leaf_problem.lower = start_lower; - leaf_problem.upper = start_upper; - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); + bool feasible = node_ptr->get_variable_bounds( + *start_lower, *start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); + if (!feasible) { return false; } } else { - node_ptr->update_branched_variable_bounds( - leaf_problem.lower, leaf_problem.upper, bounds_changed); + bool feasible = node_ptr->update_branched_variable_bounds( + *start_lower, *start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); + if (!feasible) { return false; } } return node_presolver.bounds_strengthening( @@ -160,122 +175,8 @@ class branch_and_bound_worker_t { // will be pointed by `start_node`. // For exploration, this will not be used. mip_node_t internal_node; + std::vector internal_start_lower; + std::vector internal_start_upper; }; -template -class branch_and_bound_worker_pool_t { - public: - void init(i_t num_workers, - const lp_problem_t& original_lp, - const csr_matrix_t& Arow, - const std::vector& var_type, - const simplex_solver_settings_t& settings) - { - workers_.resize(num_workers); - num_idle_workers_ = num_workers; - for (i_t i = 0; i < num_workers; ++i) { - workers_[i] = std::make_unique>( - i, original_lp, Arow, var_type, settings); - idle_workers_.push_front(i); - } - - is_initialized = true; - } - - // Here, we are assuming that the scheduler is the only - // thread that can retrieve/pop an idle worker. - branch_and_bound_worker_t* get_idle_worker() - { - std::lock_guard lock(mutex_); - if (idle_workers_.empty()) { - return nullptr; - } else { - i_t idx = idle_workers_.front(); - return workers_[idx].get(); - } - } - - // Here, we are assuming that the scheduler is the only - // thread that can retrieve/pop an idle worker. - void pop_idle_worker() - { - std::lock_guard lock(mutex_); - if (!idle_workers_.empty()) { - idle_workers_.pop_front(); - num_idle_workers_--; - } - } - - void return_worker_to_pool(branch_and_bound_worker_t* worker) - { - worker->is_active = false; - std::lock_guard lock(mutex_); - idle_workers_.push_back(worker->worker_id); - num_idle_workers_++; - } - - f_t get_lower_bound() - { - f_t lower_bound = std::numeric_limits::infinity(); - - if (is_initialized) { - for (i_t i = 0; i < workers_.size(); ++i) { - if (workers_[i]->search_strategy == BEST_FIRST && workers_[i]->is_active) { - lower_bound = std::min(workers_[i]->lower_bound.load(), lower_bound); - } - } - } - - return lower_bound; - } - - i_t num_idle_workers() { return num_idle_workers_; } - - private: - // Worker pool - std::vector>> workers_; - bool is_initialized = false; - - omp_mutex_t mutex_; - std::deque idle_workers_; - omp_atomic_t num_idle_workers_; -}; - -template -std::vector get_search_strategies( - diving_heuristics_settings_t settings) -{ - std::vector types; - types.reserve(num_search_strategies); - types.push_back(BEST_FIRST); - if (settings.pseudocost_diving != 0) { types.push_back(PSEUDOCOST_DIVING); } - if (settings.line_search_diving != 0) { types.push_back(LINE_SEARCH_DIVING); } - if (settings.guided_diving != 0) { types.push_back(GUIDED_DIVING); } - if (settings.coefficient_diving != 0) { types.push_back(COEFFICIENT_DIVING); } - return types; -} - -template -std::array get_max_workers( - i_t num_workers, const std::vector& strategies) -{ - std::array max_num_workers; - max_num_workers.fill(0); - - i_t bfs_workers = std::max(strategies.size() == 1 ? num_workers : num_workers / 4, 1); - max_num_workers[BEST_FIRST] = bfs_workers; - - i_t diving_workers = (num_workers - bfs_workers); - i_t m = strategies.size() - 1; - - for (size_t i = 1, k = 0; i < strategies.size(); ++i) { - i_t start = (double)k * diving_workers / m; - i_t end = (double)(k + 1) * diving_workers / m; - max_num_workers[strategies[i]] = end - start; - ++k; - } - - return max_num_workers; -} - } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/worker_pool.hpp b/cpp/src/branch_and_bound/worker_pool.hpp new file mode 100644 index 0000000000..2396b88914 --- /dev/null +++ b/cpp/src/branch_and_bound/worker_pool.hpp @@ -0,0 +1,141 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +class branch_and_bound_worker_pool_t { + public: + void init(i_t num_workers, + const lp_problem_t& original_lp, + const csr_matrix_t& Arow, + const std::vector& var_type, + const simplex_solver_settings_t& settings) + { + workers_.resize(num_workers); + num_idle_workers_ = num_workers; + for (i_t i = 0; i < num_workers; ++i) { + workers_[i] = std::make_unique>( + i, original_lp, Arow, var_type, settings); + idle_workers_.push_front(i); + } + + is_initialized = true; + } + + // Here, we are assuming that the scheduler is the only + // thread that can retrieve/pop an idle worker. + branch_and_bound_worker_t* get_idle_worker() + { + std::lock_guard lock(mutex_); + if (idle_workers_.empty()) { + return nullptr; + } else { + i_t idx = idle_workers_.front(); + return workers_[idx].get(); + } + } + + // Here, we are assuming that the scheduler is the only + // thread that can retrieve/pop an idle worker. + void pop_idle_worker() + { + std::lock_guard lock(mutex_); + if (!idle_workers_.empty()) { + idle_workers_.pop_front(); + num_idle_workers_--; + } + } + + void return_worker_to_pool(branch_and_bound_worker_t* worker) + { + worker->is_active = false; + std::lock_guard lock(mutex_); + idle_workers_.push_back(worker->worker_id); + num_idle_workers_++; + } + + f_t get_lower_bound() + { + f_t lower_bound = std::numeric_limits::infinity(); + + if (is_initialized) { + for (i_t i = 0; i < workers_.size(); ++i) { + if (workers_[i]->search_strategy == BEST_FIRST && workers_[i]->is_active) { + lower_bound = std::min(workers_[i]->lower_bound.load(), lower_bound); + } + } + } + + return lower_bound; + } + + i_t num_idle_workers() { return num_idle_workers_; } + + void broadcast_root_bounds_change() + { + if (is_initialized) { + for (i_t i = 0; i < workers_.size(); ++i) { + if (workers_[i]->search_strategy == BEST_FIRST && workers_[i]->is_active) { + workers_[i]->start_bounds_updated = true; + } + } + } + } + + private: + // Worker pool + std::vector>> workers_; + bool is_initialized = false; + + omp_mutex_t mutex_; + std::deque idle_workers_; + omp_atomic_t num_idle_workers_; +}; + +template +std::vector get_search_strategies( + diving_heuristics_settings_t settings) +{ + std::vector types; + types.reserve(num_search_strategies); + types.push_back(BEST_FIRST); + if (settings.pseudocost_diving != 0) { types.push_back(PSEUDOCOST_DIVING); } + if (settings.line_search_diving != 0) { types.push_back(LINE_SEARCH_DIVING); } + if (settings.guided_diving != 0) { types.push_back(GUIDED_DIVING); } + if (settings.coefficient_diving != 0) { types.push_back(COEFFICIENT_DIVING); } + return types; +} + +template +std::array get_max_workers( + i_t num_workers, const std::vector& strategies) +{ + std::array max_num_workers; + max_num_workers.fill(0); + + i_t bfs_workers = std::max(strategies.size() == 1 ? num_workers : num_workers / 4, 1); + max_num_workers[BEST_FIRST] = bfs_workers; + + i_t diving_workers = (num_workers - bfs_workers); + i_t m = strategies.size() - 1; + + for (size_t i = 1, k = 0; i < strategies.size(); ++i) { + i_t start = (double)k * diving_workers / m; + i_t end = (double)(k + 1) * diving_workers / m; + max_num_workers[strategies[i]] = end - start; + ++k; + } + + return max_num_workers; +} + +} // namespace cuopt::linear_programming::dual_simplex \ No newline at end of file diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 9739e17e1c..8c9b3adef4 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -189,7 +189,6 @@ struct simplex_solver_settings_t { // 1 - apply to root after each cut pass // 2 - apply to root after all cuts // 3 - apply to root after each incumbent update - // 4 - apply to the current node before branching, if it generates children i_t reduced_cost_strengthening; // strengthening f_t cut_change_threshold; // threshold for cut change From 23b9bb97689642f47fde5502b0e7fc354235467e Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:53:13 +0100 Subject: [PATCH 04/22] added missing mutexes. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 13 ++++++++++--- cpp/src/mip_heuristics/solver.cu | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index fdbd636520..79b862bd0c 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -448,9 +448,11 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu incumbent_.set_incumbent_solution(obj, crushed_solution); if (settings_.reduced_cost_strengthening >= 3) { + mutex_original_lp_.lock(); lower_bound = original_lp_.lower; upper_bound = original_lp_.upper; - num_fixed = find_reduced_cost_fixings(original_lp_, + mutex_original_lp_.unlock(); + num_fixed = find_reduced_cost_fixings(original_lp_, root_relax_soln_.z, var_types_, root_objective_, @@ -643,9 +645,11 @@ void branch_and_bound_t::repair_heuristic_solutions() } if (settings_.reduced_cost_strengthening >= 3) { + mutex_original_lp_.lock(); lower_bound = original_lp_.lower; upper_bound = original_lp_.upper; - num_fixed = find_reduced_cost_fixings(original_lp_, + mutex_original_lp_.unlock(); + num_fixed = find_reduced_cost_fixings(original_lp_, root_relax_soln_.z, var_types_, root_objective_, @@ -813,9 +817,12 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, send_solution = true; if (settings_.reduced_cost_strengthening >= 3) { + mutex_original_lp_.lock(); lower_bound = original_lp_.lower; upper_bound = original_lp_.upper; - num_fixed = find_reduced_cost_fixings(original_lp_, + mutex_original_lp_.unlock(); + + num_fixed = find_reduced_cost_fixings(original_lp_, root_relax_soln_.z, var_types_, root_objective_, diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index ac97acc32e..19d8237f4b 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -224,8 +224,8 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening < 0 ? 4 - : context.settings.reduced_cost_strengthening; + context.settings.reduced_cost_strengthening >= 0 ? context.settings.reduced_cost_strengthening + : 3; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; branch_and_bound_settings.mip_batch_pdlp_strong_branching = From 56fa6449b906580dd72e9799b61b1607eb19ecda Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:05:48 +0100 Subject: [PATCH 05/22] extend reduced cost fixing to diving workers. fix possible race conditions. fix incorrect LP problem passed to the coefficient diving. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 233 ++++++++++-------- .../branch_and_bound/reduced_cost_fixing.hpp | 30 +-- cpp/src/branch_and_bound/worker.hpp | 54 ++-- cpp/src/branch_and_bound/worker_pool.hpp | 8 +- 4 files changed, 173 insertions(+), 152 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 79b862bd0c..935a843534 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -410,7 +410,6 @@ void branch_and_bound_t::update_user_bound(f_t lower_bound) template void branch_and_bound_t::set_new_solution(const std::vector& solution) { - mutex_original_lp_.lock(); if (solution.size() != original_problem_.num_cols) { settings_.log.printf( "Solution size mismatch %ld %d\n", solution.size(), original_problem_.num_cols); @@ -419,14 +418,16 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu std::vector lower_bound; std::vector upper_bound; std::vector bounds_changed; + + mutex_original_lp_.lock(); crush_primal_solution( original_problem_, original_lp_, solution, new_slacks_, crushed_solution); f_t obj = compute_objective(original_lp_, crushed_solution); mutex_original_lp_.unlock(); + bool is_feasible = false; bool attempt_repair = false; f_t current_upper_bound = upper_bound_; - i_t num_fixed = 0; if (obj < current_upper_bound) { f_t primal_err; @@ -452,15 +453,22 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu lower_bound = original_lp_.lower; upper_bound = original_lp_.upper; mutex_original_lp_.unlock(); - num_fixed = find_reduced_cost_fixings(original_lp_, - root_relax_soln_.z, - var_types_, - root_objective_, - obj, - lower_bound, - upper_bound, - bounds_changed, - settings_); + auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, + var_types_, + settings_, + root_objective_, + obj, + lower_bound, + upper_bound, + bounds_changed); + if (num_fixed > 0 || num_improved > 0) { + bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); + if (!feasible) { + settings_.log.printf( + "Bound strengthening failed when updating the bounds at the root node!\n"); + solver_status_ = mip_status_t::NUMERICAL; + } + } } } else { @@ -486,14 +494,6 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu repair_queue_.push_back(solution); mutex_repair_.unlock(); } - - if (num_fixed > 0) { - bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); - if (!feasible) { - settings_.log.printf("Bound strenghtening failed when updating the bounds at the root node!"); - solver_status_ = mip_status_t::NUMERICAL; - } - } } template @@ -619,13 +619,16 @@ void branch_and_bound_t::repair_heuristic_solutions() settings_.log.debug("Attempting to repair %ld injected solutions\n", to_repair.size()); for (const std::vector& uncrushed_solution : to_repair) { std::vector crushed_solution; + + mutex_original_lp_.lock(); crush_primal_solution( original_problem_, original_lp_, uncrushed_solution, new_slacks_, crushed_solution); + mutex_original_lp_.unlock(); + std::vector repaired_solution; std::vector lower_bound; std::vector upper_bound; std::vector bounds_changed; - i_t num_fixed = 0; f_t repaired_obj; bool is_feasible = @@ -649,32 +652,25 @@ void branch_and_bound_t::repair_heuristic_solutions() lower_bound = original_lp_.lower; upper_bound = original_lp_.upper; mutex_original_lp_.unlock(); - num_fixed = find_reduced_cost_fixings(original_lp_, - root_relax_soln_.z, - var_types_, - root_objective_, - repaired_obj, - lower_bound, - upper_bound, - bounds_changed, - settings_); - settings_.log.printf( - "Repair H: Applied reduced cost fixing. obj=%.10e. num_fixed=%d.\n", - compute_user_objective(original_lp_, repaired_obj), - num_fixed); + auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, + var_types_, + settings_, + root_objective_, + repaired_obj, + lower_bound, + upper_bound, + bounds_changed); + if (num_fixed > 0 || num_improved > 0) { + bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); + if (!feasible) { + settings_.log.printf( + "Bound strengthening failed when updating the bounds at the root node!\n"); + solver_status_ = mip_status_t::NUMERICAL; + } + } } } - mutex_upper_.unlock(); - - if (num_fixed > 0) { - bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); - if (!feasible) { - settings_.log.printf( - "Bound strenghtening failed when updating the bounds at the root node!"); - solver_status_ = mip_status_t::NUMERICAL; - } - } } } } @@ -822,15 +818,22 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, upper_bound = original_lp_.upper; mutex_original_lp_.unlock(); - num_fixed = find_reduced_cost_fixings(original_lp_, - root_relax_soln_.z, - var_types_, - root_objective_, - leaf_objective, - lower_bound, - upper_bound, - bounds_changed, - settings_); + auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, + var_types_, + settings_, + root_objective_, + leaf_objective, + lower_bound, + upper_bound, + bounds_changed); + if (num_fixed > 0 || num_improved > 0) { + bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); + if (!feasible) { + settings_.log.printf( + "Bound strengthening failed when updating the bounds at the root node!\n"); + solver_status_ = mip_status_t::NUMERICAL; + } + } } } @@ -840,14 +843,6 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, settings_.solution_callback(original_x, upper_bound_); } mutex_upper_.unlock(); - - if (num_fixed > 0) { - bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); - if (!feasible) { - settings_.log.printf("Bound strenghtening failed when updating the bounds at the root node!"); - solver_status_ = mip_status_t::NUMERICAL; - } - } } // Martin's criteria for the preferred rounding direction (see [1]) @@ -908,7 +903,7 @@ branch_variable_t branch_and_bound_t::variable_selection( case search_strategy_t::COEFFICIENT_DIVING: return coefficient_diving( - original_lp_, fractional, solution, var_up_locks_, var_down_locks_, log); + worker->leaf_problem, fractional, solution, var_up_locks_, var_down_locks_, log); case search_strategy_t::LINE_SEARCH_DIVING: return line_search_diving(fractional, solution, root_relax_soln_.x, log); @@ -1177,7 +1172,7 @@ struct deterministic_diving_policy_t case search_strategy_t::COEFFICIENT_DIVING: { logger_t log; log.log = false; - return coefficient_diving(this->bnb.original_lp_, + return coefficient_diving(this->worker.leaf_problem, fractional, x, this->bnb.var_up_locks_, @@ -1431,6 +1426,31 @@ dual::status_t branch_and_bound_t::solve_node_lp( node_ptr->vstatus[node_ptr->branch_var]); #endif + if (worker->start_bounds_updated) { + worker->start_bounds_updated = false; + worker->recompute_bounds = true; + + if (worker->search_strategy == BEST_FIRST) { + mutex_original_lp_.lock(); + worker->start_lower = original_lp_.lower; + worker->start_upper = original_lp_.upper; + mutex_original_lp_.unlock(); + } else { + // When diving, we are working on a separated subtree so we no longer can + // retrieve the bounds from the starting node until the root node of + // the main. Instead, we apply the reduced cost fixing directly + // to the starting bounds. + reduced_cost_fixing(root_relax_soln_.z, + var_types_, + settings_, + root_objective_, + upper_bound_.load(), + worker->start_lower, + worker->start_upper, + worker->bounds_changed); + } + } + bool feasible = worker->set_lp_variable_bounds(node_ptr, settings_); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; worker->leaf_edge_norms = edge_norms_; @@ -1497,8 +1517,8 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_tlower_bound = lower_bound; @@ -1662,7 +1682,9 @@ void branch_and_bound_t::run_scheduler() std::array max_num_workers_per_type = get_max_workers(num_workers, strategies); + mutex_original_lp_.lock(); worker_pool_.init(num_workers, original_lp_, Arow_, var_types_, settings_); + mutex_original_lp_.unlock(); active_workers_per_strategy_.fill(0); #ifdef CUOPT_LOG_DEBUG @@ -1753,11 +1775,10 @@ void branch_and_bound_t::run_scheduler() continue; } - bool feasible = - start_node.value()->check_variable_bounds(original_lp_.lower, original_lp_.upper); + bool feasible = worker->init_best_first(start_node.value(), original_lp_); if (!feasible) { - // This node was put on the heap earlier but its variables bounds now violates the bounds - // at the root node + // This node was put on the heap earlier but its variables bounds now violates the + // bounds at the root node search_tree_.graphviz_node( settings_.log, start_node.value(), "cutoff", start_node.value()->lower_bound); search_tree_.update(start_node.value(), node_status_t::FATHOMED); @@ -1766,7 +1787,6 @@ void branch_and_bound_t::run_scheduler() // Remove the worker from the idle list. worker_pool_.pop_idle_worker(); - worker->init_best_first(start_node.value(), original_lp_); last_node_depth = start_node.value()->depth; last_int_infeas = start_node.value()->integer_infeasible; active_workers_per_strategy_[strategy]++; @@ -1784,8 +1804,10 @@ void branch_and_bound_t::run_scheduler() continue; } + mutex_original_lp_.lock(); bool is_feasible = worker->init_diving(start_node.value(), strategy, original_lp_, settings_); + mutex_original_lp_.unlock(); if (!is_feasible) { continue; } // Remove the worker from the idle list. @@ -2196,7 +2218,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (num_fractional != 0 && settings_.max_cut_passes > 0) { settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node | " + " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node | " + " " "Gap " "| Time |\n"); } @@ -2345,15 +2368,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut mutex_original_lp_.unlock(); mutex_upper_.lock(); last_upper_bound = upper_bound_.load(); - find_reduced_cost_fixings(original_lp_, - root_relax_soln_.z, - var_types_, - compute_objective(original_lp_, root_relax_soln_.x), - upper_bound_.load(), - new_lower, - new_upper, - bounds_changed, - settings_); + reduced_cost_fixing(root_relax_soln_.z, + var_types_, + settings_, + root_objective_, + upper_bound_.load(), + new_lower, + new_upper, + bounds_changed); mutex_upper_.unlock(); mutex_original_lp_.lock(); original_lp_.lower = new_lower; @@ -2550,15 +2572,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut mutex_original_lp_.unlock(); mutex_upper_.lock(); - i_t num_fixed = find_reduced_cost_fixings(original_lp_, - root_relax_soln_.z, - var_types_, - root_objective_, - upper_bound_.load(), - lower_bounds, - upper_bounds, - bounds_changed, - settings_); + auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, + var_types_, + settings_, + root_objective_, + upper_bound_.load(), + lower_bounds, + upper_bounds, + bounds_changed); mutex_upper_.unlock(); if (num_fixed > 0) { @@ -2734,8 +2755,8 @@ Work Units: 0 0.5 1. ──────────────────────────────────────────────────────────────────────────────────────────► Work Unit Time -Legend: ▓▓▓ = actively working ░░░ = waiting at barrier [hash] = state hash for verification - wut = work unit timestamp PC = pseudo-costs snap = snapshot (local copy) +Legend: ▓▓▓ = actively working ░░░ = waiting at barrier [hash] = state hash for +verification wut = work unit timestamp PC = pseudo-costs snap = snapshot (local copy) */ @@ -2773,23 +2794,22 @@ Producer Sync: Producing solutions in the past would break determinism, therefore this unidirectional sync ensures no such thing can occur. Instrumentation Aggregator: Collects multiple instrument vectors into a single aggregation point for estimating work from memory operations. Worker Context: Object -representing the "context" (e.g.: the worker) that should register the amount of work recorded There -is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure they -remained synchronized together. Queued Integer Solutions: New integer solutions found within -horizons are queued with a work unit timestamp, in order to be sorted and played in order during the -sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is used -to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently +representing the "context" (e.g.: the worker) that should register the amount of work recorded +There is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure +they remained synchronized together. Queued Integer Solutions: New integer solutions found within +horizons are queued with a work unit timestamp, in order to be sorted and played in order during +the sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is +used to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently nondeterministic. To fix this, in deterministic mode, nodes are addressed by a tuple - where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential ID -local to the worker.\ This sequential ID is similar in principle to the global atomic ID sequence of -the nondeterminsitic mode but since it is local to each worker, it is updated serially and thus is -deterministic. worker IDs are unique, and sequence IDs are unique to their workers, therefor - is a globally unique node identifier. -Pseudocost Update: - Each worker updates its local pseudocosts when branching. These updates are queued within -horizons. During the horizon sync, these updates are all played in order, and the newly updated -global pseudocosts are broadcast to the worker's pseudocost snapshots for the coming horizon. + where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential +ID local to the worker.\ This sequential ID is similar in principle to the global atomic ID +sequence of the nondeterminsitic mode but since it is local to each worker, it is updated serially +and thus is deterministic. worker IDs are unique, and sequence IDs are unique to their workers, +therefor is a globally unique node identifier. Pseudocost Update: Each worker +updates its local pseudocosts when branching. These updates are queued within horizons. During the +horizon sync, these updates are all played in order, and the newly updated global pseudocosts are +broadcast to the worker's pseudocost snapshots for the coming horizon. */ @@ -2907,7 +2927,8 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri "Sync%% | NoWork\n"); settings_.log.printf( " " - "-------+---------+----------+--------+---------+--------+----------+----------+-------+-------" + "-------+---------+----------+--------+---------+--------+----------+----------+-------+-----" + "--" "\n"); for (const auto& worker : *deterministic_workers_) { double sync_time = worker.work_context.total_sync_time; diff --git a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp index 511309b94f..8b40eb03df 100644 --- a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp +++ b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp @@ -12,16 +12,17 @@ namespace cuopt::linear_programming::dual_simplex { +// Applies reduced cost fixing over the lower and bounds. Stores the bounds changes +// for applying bound strengthening later. Returns {num_fixed, num_improved}. template -i_t find_reduced_cost_fixings(const lp_problem_t& lp, - const std::vector& reduced_costs, - const std::vector& var_types, - f_t obj, - f_t upper_bound, - std::vector& lower_bounds, - std::vector& upper_bounds, - std::vector& bounds_changed, - const simplex_solver_settings_t& settings) +std::pair reduced_cost_fixing(const std::vector& reduced_costs, + const std::vector& var_types, + const simplex_solver_settings_t& settings, + f_t obj, + f_t upper_bound, + std::vector& lower_bounds, + std::vector& upper_bounds, + std::vector& bounds_changed) { const f_t threshold = 100.0 * settings.integer_tol; const f_t weaken = settings.integer_tol; @@ -31,12 +32,12 @@ i_t find_reduced_cost_fixings(const lp_problem_t& lp, i_t num_cols_to_check = reduced_costs.size(); // Reduced costs will be smaller than the original // problem because we have added slacks for cuts - bounds_changed.assign(lp.num_cols, false); + bounds_changed.assign(lower_bounds.size(), false); for (i_t j = 0; j < num_cols_to_check; j++) { if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold) { - const f_t lower_j = lp.lower[j]; - const f_t upper_j = lp.upper[j]; + const f_t lower_j = lower_bounds[j]; + const f_t upper_j = upper_bounds[j]; const f_t abs_gap = upper_bound - obj; f_t reduced_cost_upper_bound = upper_j; f_t reduced_cost_lower_bound = lower_j; @@ -73,7 +74,6 @@ i_t find_reduced_cost_fixings(const lp_problem_t& lp, settings.log.debug( "Reduced costs: Found %d improved bounds and %d fixed variables\n", num_improved, num_fixed); } - return num_fixed; + return {num_fixed, num_improved}; } - -} // namespace cuopt::linear_programming::dual_simplex \ No newline at end of file +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index a7fa3d3ec2..ba38b0e41e 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -67,9 +67,21 @@ class branch_and_bound_worker_t { bounds_strengthening_t node_presolver; std::vector bounds_changed; - std::vector* start_lower; - std::vector* start_upper; - mip_node_t* start_node; + // We need to maintain a copy in each worker for 2 reasons: + // + // - The root LP may be modified by multiple threads and + // require a mutex for accessing its variable bounds. + // Since we are maintain a copy here, we can access + // without having to acquire the mutex. Only if the + // bounds is modified, then we acquire the lock and update the copy. + // + // - When diving, we are working on a separated subtree. Hence, we cannot + // retrieve the bounds from the main tree. Instead, we copy the bounds until + // the starting node before it is detached from the main tree and use it + // as the starting bounds. + std::vector start_lower; + std::vector start_upper; + mip_node_t* start_node; pcgenerator_t rng; @@ -93,23 +105,23 @@ class branch_and_bound_worker_t { nonbasic_list(), node_presolver(leaf_problem, Arow, {}, var_type), bounds_changed(original_lp.num_cols, false), - start_lower(nullptr), - start_upper(nullptr), start_node(nullptr), rng(settings.random_seed + pcgenerator_t::default_seed + worker_id, pcgenerator_t::default_stream ^ worker_id) { } - // Set the `start_node` for best-first search. - void init_best_first(mip_node_t* node, lp_problem_t& original_lp) + // Initialize the worker for plunging, setting the `start_node`, `start_lower` and + // `start_upper`. Returns `true` if no bounds were violated in any of the previous nodes + bool init_best_first(mip_node_t* node, lp_problem_t& original_lp) { start_node = node; - start_lower = &original_lp.lower; - start_upper = &original_lp.upper; + start_lower = original_lp.lower; + start_upper = original_lp.upper; search_strategy = BEST_FIRST; lower_bound = node->lower_bound; is_active = true; + return node->check_variable_bounds(start_lower, start_upper); } // Initialize the worker for diving, setting the `start_node`, `start_lower` and @@ -122,44 +134,36 @@ class branch_and_bound_worker_t { { internal_node = node_ptr->detach_copy(); start_node = &internal_node; - start_lower = &internal_start_lower; - start_upper = &internal_start_upper; + start_lower = original_lp.lower; + start_upper = original_lp.upper; search_strategy = type; lower_bound = node_ptr->lower_bound; is_active = true; std::fill(bounds_changed.begin(), bounds_changed.end(), false); bool feasible = node_ptr->get_variable_bounds( - original_lp.lower, original_lp.upper, *start_lower, *start_upper, bounds_changed); + original_lp.lower, original_lp.upper, start_lower, start_upper, bounds_changed); if (!feasible) { return false; } - return node_presolver.bounds_strengthening( - settings, bounds_changed, *start_lower, *start_upper); + return node_presolver.bounds_strengthening(settings, bounds_changed, start_lower, start_upper); } // Set the variables bounds for the LP relaxation in the current node. bool set_lp_variable_bounds(mip_node_t* node_ptr, const simplex_solver_settings_t& settings) { - // If the starting bounds were updated via reduced cost fixing, then - // recompute the bounds from scratch - if (start_bounds_updated) { - recompute_bounds = true; - start_bounds_updated = false; - } - // Reset the bound_changed markers std::fill(bounds_changed.begin(), bounds_changed.end(), false); // Set the correct bounds for the leaf problem if (recompute_bounds) { bool feasible = node_ptr->get_variable_bounds( - *start_lower, *start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); + start_lower, start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); if (!feasible) { return false; } } else { bool feasible = node_ptr->update_branched_variable_bounds( - *start_lower, *start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); + start_lower, start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); if (!feasible) { return false; } } @@ -168,15 +172,13 @@ class branch_and_bound_worker_t { } private: - // For diving, we need to store the full node instead of + // For diving, we need to store the full node instead // of just a pointer, since it is not stored in the tree anymore. // To keep the same interface across all worker types, // this will be used as a temporary storage and // will be pointed by `start_node`. // For exploration, this will not be used. mip_node_t internal_node; - std::vector internal_start_lower; - std::vector internal_start_upper; }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/worker_pool.hpp b/cpp/src/branch_and_bound/worker_pool.hpp index 2396b88914..7ea1a0c184 100644 --- a/cpp/src/branch_and_bound/worker_pool.hpp +++ b/cpp/src/branch_and_bound/worker_pool.hpp @@ -84,9 +84,7 @@ class branch_and_bound_worker_pool_t { { if (is_initialized) { for (i_t i = 0; i < workers_.size(); ++i) { - if (workers_[i]->search_strategy == BEST_FIRST && workers_[i]->is_active) { - workers_[i]->start_bounds_updated = true; - } + if (workers_[i]->is_active) { workers_[i]->start_bounds_updated = true; } } } } @@ -101,7 +99,7 @@ class branch_and_bound_worker_pool_t { omp_atomic_t num_idle_workers_; }; -template +template std::vector get_search_strategies( diving_heuristics_settings_t settings) { @@ -138,4 +136,4 @@ std::array get_max_workers( return max_num_workers; } -} // namespace cuopt::linear_programming::dual_simplex \ No newline at end of file +} // namespace cuopt::linear_programming::dual_simplex From 1862e36a58f0388eb3350e5d5a2ea64c0d6f96da Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:19:23 +0100 Subject: [PATCH 06/22] fix worker initialization when the path is invalid after reduced cost strengthening. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 32 ++++++++----------- cpp/src/branch_and_bound/worker.hpp | 7 ++-- .../dual_simplex/simplex_solver_settings.hpp | 4 +-- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 935a843534..52bfd21194 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -414,12 +414,9 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu settings_.log.printf( "Solution size mismatch %ld %d\n", solution.size(), original_problem_.num_cols); } - std::vector crushed_solution; - std::vector lower_bound; - std::vector upper_bound; - std::vector bounds_changed; mutex_original_lp_.lock(); + std::vector crushed_solution; crush_primal_solution( original_problem_, original_lp_, solution, new_slacks_, crushed_solution); f_t obj = compute_objective(original_lp_, crushed_solution); @@ -450,9 +447,11 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu if (settings_.reduced_cost_strengthening >= 3) { mutex_original_lp_.lock(); - lower_bound = original_lp_.lower; - upper_bound = original_lp_.upper; + std::vector lower_bound = original_lp_.lower; + std::vector upper_bound = original_lp_.upper; + std::vector bounds_changed; mutex_original_lp_.unlock(); + auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, var_types_, settings_, @@ -461,6 +460,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu lower_bound, upper_bound, bounds_changed); + if (num_fixed > 0 || num_improved > 0) { bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); if (!feasible) { @@ -626,9 +626,6 @@ void branch_and_bound_t::repair_heuristic_solutions() mutex_original_lp_.unlock(); std::vector repaired_solution; - std::vector lower_bound; - std::vector upper_bound; - std::vector bounds_changed; f_t repaired_obj; bool is_feasible = @@ -649,9 +646,11 @@ void branch_and_bound_t::repair_heuristic_solutions() if (settings_.reduced_cost_strengthening >= 3) { mutex_original_lp_.lock(); - lower_bound = original_lp_.lower; - upper_bound = original_lp_.upper; + std::vector lower_bound = original_lp_.lower; + std::vector upper_bound = original_lp_.upper; + std::vector bounds_changed; mutex_original_lp_.unlock(); + auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, var_types_, settings_, @@ -795,11 +794,6 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, search_strategy_t thread_type) { bool send_solution = false; - i_t num_fixed = 0; - - std::vector lower_bound; - std::vector upper_bound; - std::vector bounds_changed; settings_.log.debug("%c found a feasible solution with obj=%.10e.\n", feasible_solution_symbol(thread_type), @@ -814,8 +808,9 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, if (settings_.reduced_cost_strengthening >= 3) { mutex_original_lp_.lock(); - lower_bound = original_lp_.lower; - upper_bound = original_lp_.upper; + std::vector lower_bound = original_lp_.lower; + std::vector upper_bound = original_lp_.upper; + std::vector bounds_changed; mutex_original_lp_.unlock(); auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, @@ -2702,6 +2697,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // Deterministic implementation // ============================================================================ +// TODO: reduced cost fixing is not enabled for the deterministic mode // The deterministic BSP model is based on letting independent workers execute during virtual time // intervals, and exchange data during serialized interval sync points. /* diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index ba38b0e41e..07112c7943 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -81,7 +81,7 @@ class branch_and_bound_worker_t { // as the starting bounds. std::vector start_lower; std::vector start_upper; - mip_node_t* start_node; + mip_node_t* start_node; pcgenerator_t rng; @@ -115,13 +115,16 @@ class branch_and_bound_worker_t { // `start_upper`. Returns `true` if no bounds were violated in any of the previous nodes bool init_best_first(mip_node_t* node, lp_problem_t& original_lp) { + bool feasible = node->check_variable_bounds(original_lp.lower, original_lp.upper); + if (!feasible) { return false; } + start_node = node; start_lower = original_lp.lower; start_upper = original_lp.upper; search_strategy = BEST_FIRST; lower_bound = node->lower_bound; is_active = true; - return node->check_variable_bounds(start_lower, start_upper); + return true; } // Initialize the worker for diving, setting the `start_node`, `start_lower` and diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 8c9b3adef4..0372771a3d 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -183,14 +183,14 @@ struct simplex_solver_settings_t { i_t clique_cuts; // -1 automatic, 0 to disable, >0 to enable clique cuts i_t strong_chvatal_gomory_cuts; // -1 automatic, 0 to disable, >0 to enable strong Chvatal Gomory // cuts - // >= 0 adds additional reduced cost strengthening passes at each level + // >= 0 adds additional reduced cost strengthening passes at each level // -1 - automatic // 0 - disable // 1 - apply to root after each cut pass // 2 - apply to root after all cuts // 3 - apply to root after each incumbent update i_t reduced_cost_strengthening; - // strengthening + f_t cut_change_threshold; // threshold for cut change f_t cut_min_orthogonality; // minimum orthogonality for cuts i_t mip_batch_pdlp_strong_branching{0}; // 0 if not using batch PDLP for strong branching, 1 if From a5fdf502bbca5d04e6957592457919411764e4a9 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:32:49 +0100 Subject: [PATCH 07/22] fix diving initialization --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 +- cpp/src/branch_and_bound/worker.hpp | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 52bfd21194..c1db3026f7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2577,7 +2577,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut bounds_changed); mutex_upper_.unlock(); - if (num_fixed > 0) { + if (num_fixed > 0 || num_improved > 0) { bool feasible = update_root_bounds(lower_bounds, upper_bounds, bounds_changed); if (!feasible) { settings_.log.printf("Bound strengthening failed\n"); diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 07112c7943..829b667400 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -141,37 +141,43 @@ class branch_and_bound_worker_t { start_upper = original_lp.upper; search_strategy = type; lower_bound = node_ptr->lower_bound; - is_active = true; std::fill(bounds_changed.begin(), bounds_changed.end(), false); bool feasible = node_ptr->get_variable_bounds( original_lp.lower, original_lp.upper, start_lower, start_upper, bounds_changed); - if (!feasible) { return false; } - - return node_presolver.bounds_strengthening(settings, bounds_changed, start_lower, start_upper); + if (feasible) { + feasible = + node_presolver.bounds_strengthening(settings, bounds_changed, start_lower, start_upper); + } + is_active = feasible; + return feasible; } // Set the variables bounds for the LP relaxation in the current node. bool set_lp_variable_bounds(mip_node_t* node_ptr, const simplex_solver_settings_t& settings) { + bool feasible = false; + // Reset the bound_changed markers std::fill(bounds_changed.begin(), bounds_changed.end(), false); // Set the correct bounds for the leaf problem if (recompute_bounds) { - bool feasible = node_ptr->get_variable_bounds( + feasible = node_ptr->get_variable_bounds( start_lower, start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); - if (!feasible) { return false; } } else { - bool feasible = node_ptr->update_branched_variable_bounds( + feasible = node_ptr->update_branched_variable_bounds( start_lower, start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); - if (!feasible) { return false; } } - return node_presolver.bounds_strengthening( - settings, bounds_changed, leaf_problem.lower, leaf_problem.upper); + if (feasible) { + feasible = node_presolver.bounds_strengthening( + settings, bounds_changed, leaf_problem.lower, leaf_problem.upper); + } + + return feasible; } private: From bd1ad236116b91d310c9f518c210f82539e649e1 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:33:46 +0100 Subject: [PATCH 08/22] add missing mutex --- cpp/src/branch_and_bound/branch_and_bound.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c1db3026f7..c08bb0b55b 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1770,7 +1770,10 @@ void branch_and_bound_t::run_scheduler() continue; } + mutex_original_lp_.lock(); bool feasible = worker->init_best_first(start_node.value(), original_lp_); + mutex_original_lp_.unlock(); + if (!feasible) { // This node was put on the heap earlier but its variables bounds now violates the // bounds at the root node From cc63dc6a42c9460050d4134d7513f9d07324e8cb Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:10:40 +0100 Subject: [PATCH 09/22] added comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 8 ++++++++ cpp/src/branch_and_bound/deterministic_workers.hpp | 11 ++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c08bb0b55b..3d62c0efc7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3187,6 +3187,14 @@ node_status_t branch_and_bound_t::solve_node_deterministic( std::fill(worker.bounds_changed.begin(), worker.bounds_changed.end(), false); + // FIXME: Add mutex protection for original_lp_ bounds access in solve_node_deterministic. + // The code reads original_lp_.lower and original_lp_.upper without mutex protection at lines + // 3171-3172 and 3177-3178. However, update_root_bounds (called from set_new_solution during + // solution processing) modifies these same bounds while holding mutex_original_lp_. This creates + // a data race: workers can read bounds while the main thread updates them via heuristic + // solutions. Acquire mutex_original_lp_ before accessing the bounds, or take a snapshot at the + // start of solve_node_deterministic to avoid repeated locking. + // (This fix is needed for deterministic mode + reduced cost strengthening) if (worker.recompute_bounds_and_basis) { feasible = node_ptr->get_variable_bounds(original_lp_.lower, original_lp_.upper, diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index 1fa71f66cd..50b8e905db 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -317,11 +317,12 @@ class deterministic_diving_worker_t { dive_queue_entry_t entry; std::vector bounds_changed(original_lp.num_cols, false); - node->get_variable_bounds(original_lp.lower, - original_lp.upper, - entry.resolved_lower, - entry.resolved_upper, - bounds_changed); + bool feasible = node->get_variable_bounds(original_lp.lower, + original_lp.upper, + entry.resolved_lower, + entry.resolved_upper, + bounds_changed); + if (!feasible) { return; } entry.node = node->detach_copy(); dive_queue.push_back(std::move(entry)); } From e5e7d26c57d2a92487c7e1cda28472a5afd4b057 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:23:13 +0100 Subject: [PATCH 10/22] unified reduced root fixing into a single routine. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 189 +++++++----------- cpp/src/branch_and_bound/branch_and_bound.hpp | 4 +- .../branch_and_bound/reduced_cost_fixing.hpp | 2 +- 3 files changed, 74 insertions(+), 121 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3d62c0efc7..c0f3e56b8d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -380,31 +380,51 @@ void branch_and_bound_t::report( } template -bool branch_and_bound_t::update_root_bounds(const std::vector& lower_bounds, - const std::vector& upper_bounds, - const std::vector& bounds_changed) +void branch_and_bound_t::update_user_bound(f_t lower_bound) { - std::vector row_sense; - bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); + if (user_bound_callback_ == nullptr) { return; } + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + user_bound_callback_(user_lower); +} + +template +void branch_and_bound_t::root_reduced_cost_fixing(f_t upper_bound) +{ + if (settings_.reduced_cost_strengthening < 3) { return; } mutex_original_lp_.lock(); - original_lp_.lower = lower_bounds; - original_lp_.upper = upper_bounds; - bool feasible = node_presolve.bounds_strengthening( - settings_, bounds_changed, original_lp_.lower, original_lp_.upper); + std::vector lower = original_lp_.lower; + std::vector upper = original_lp_.upper; + std::vector bounds_changed; mutex_original_lp_.unlock(); - worker_pool_.broadcast_root_bounds_change(); + auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, + var_types_, + settings_, + root_objective_, + upper_bound, + lower, + upper, + bounds_changed); + if (num_fixed > 0 || num_improved > 0) { + std::vector row_sense; + bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); - return feasible; -} + mutex_original_lp_.lock(); + original_lp_.lower = lower; + original_lp_.upper = upper; + bool feasible = node_presolve.bounds_strengthening( + settings_, bounds_changed, original_lp_.lower, original_lp_.upper); + mutex_original_lp_.unlock(); -template -void branch_and_bound_t::update_user_bound(f_t lower_bound) -{ - if (user_bound_callback_ == nullptr) { return; } - f_t user_lower = compute_user_objective(original_lp_, lower_bound); - user_bound_callback_(user_lower); + worker_pool_.broadcast_root_bounds_change(); + + if (!feasible) { + settings_.log.printf( + "Bound strengthening failed when updating the bounds at the root node!\n"); + solver_status_ = mip_status_t::NUMERICAL; + } + } } template @@ -444,33 +464,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu if (is_feasible && obj < upper_bound_) { upper_bound_ = obj; incumbent_.set_incumbent_solution(obj, crushed_solution); - - if (settings_.reduced_cost_strengthening >= 3) { - mutex_original_lp_.lock(); - std::vector lower_bound = original_lp_.lower; - std::vector upper_bound = original_lp_.upper; - std::vector bounds_changed; - mutex_original_lp_.unlock(); - - auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, - var_types_, - settings_, - root_objective_, - obj, - lower_bound, - upper_bound, - bounds_changed); - - if (num_fixed > 0 || num_improved > 0) { - bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); - if (!feasible) { - settings_.log.printf( - "Bound strengthening failed when updating the bounds at the root node!\n"); - solver_status_ = mip_status_t::NUMERICAL; - } - } - } - + root_reduced_cost_fixing(obj); } else { attempt_repair = true; constexpr bool verbose = false; @@ -644,30 +638,7 @@ void branch_and_bound_t::repair_heuristic_solutions() settings_.solution_callback(original_x, repaired_obj); } - if (settings_.reduced_cost_strengthening >= 3) { - mutex_original_lp_.lock(); - std::vector lower_bound = original_lp_.lower; - std::vector upper_bound = original_lp_.upper; - std::vector bounds_changed; - mutex_original_lp_.unlock(); - - auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, - var_types_, - settings_, - root_objective_, - repaired_obj, - lower_bound, - upper_bound, - bounds_changed); - if (num_fixed > 0 || num_improved > 0) { - bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); - if (!feasible) { - settings_.log.printf( - "Bound strengthening failed when updating the bounds at the root node!\n"); - solver_status_ = mip_status_t::NUMERICAL; - } - } - } + root_reduced_cost_fixing(repaired_obj); } mutex_upper_.unlock(); } @@ -804,32 +775,8 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, incumbent_.set_incumbent_solution(leaf_objective, leaf_solution); upper_bound_ = leaf_objective; report(feasible_solution_symbol(thread_type), leaf_objective, get_lower_bound(), leaf_depth, 0); + root_reduced_cost_fixing(leaf_objective); send_solution = true; - - if (settings_.reduced_cost_strengthening >= 3) { - mutex_original_lp_.lock(); - std::vector lower_bound = original_lp_.lower; - std::vector upper_bound = original_lp_.upper; - std::vector bounds_changed; - mutex_original_lp_.unlock(); - - auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z, - var_types_, - settings_, - root_objective_, - leaf_objective, - lower_bound, - upper_bound, - bounds_changed); - if (num_fixed > 0 || num_improved > 0) { - bool feasible = update_root_bounds(lower_bound, upper_bound, bounds_changed); - if (!feasible) { - settings_.log.printf( - "Bound strengthening failed when updating the bounds at the root node!\n"); - solver_status_ = mip_status_t::NUMERICAL; - } - } - } } if (send_solution && settings_.solution_callback != nullptr) { @@ -1432,8 +1379,8 @@ dual::status_t branch_and_bound_t::solve_node_lp( mutex_original_lp_.unlock(); } else { // When diving, we are working on a separated subtree so we no longer can - // retrieve the bounds from the starting node until the root node of - // the main. Instead, we apply the reduced cost fixing directly + // propagate the root bound changes from the root to the current node. + // Instead, we apply the reduced cost fixing directly // to the starting bounds. reduced_cost_fixing(root_relax_soln_.z, var_types_, @@ -2216,8 +2163,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (num_fractional != 0 && settings_.max_cut_passes > 0) { settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node | " - " " + " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node | " "Gap " "| Time |\n"); } @@ -2581,7 +2527,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut mutex_upper_.unlock(); if (num_fixed > 0 || num_improved > 0) { - bool feasible = update_root_bounds(lower_bounds, upper_bounds, bounds_changed); + std::vector row_sense; + bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); + + mutex_original_lp_.lock(); + original_lp_.lower = lower_bounds; + original_lp_.upper = upper_bounds; + bool feasible = node_presolve.bounds_strengthening( + settings_, bounds_changed, original_lp_.lower, original_lp_.upper); + mutex_original_lp_.unlock(); + if (!feasible) { settings_.log.printf("Bound strengthening failed\n"); return mip_status_t::NUMERICAL; // We had a feasible integer solution, but bound @@ -2754,8 +2709,8 @@ Work Units: 0 0.5 1. ──────────────────────────────────────────────────────────────────────────────────────────► Work Unit Time -Legend: ▓▓▓ = actively working ░░░ = waiting at barrier [hash] = state hash for -verification wut = work unit timestamp PC = pseudo-costs snap = snapshot (local copy) +Legend: ▓▓▓ = actively working ░░░ = waiting at barrier [hash] = state hash for verification + wut = work unit timestamp PC = pseudo-costs snap = snapshot (local copy) */ @@ -2793,22 +2748,23 @@ Producer Sync: Producing solutions in the past would break determinism, therefore this unidirectional sync ensures no such thing can occur. Instrumentation Aggregator: Collects multiple instrument vectors into a single aggregation point for estimating work from memory operations. Worker Context: Object -representing the "context" (e.g.: the worker) that should register the amount of work recorded -There is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure -they remained synchronized together. Queued Integer Solutions: New integer solutions found within -horizons are queued with a work unit timestamp, in order to be sorted and played in order during -the sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is -used to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently +representing the "context" (e.g.: the worker) that should register the amount of work recorded There +is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure they +remained synchronized together. Queued Integer Solutions: New integer solutions found within +horizons are queued with a work unit timestamp, in order to be sorted and played in order during the +sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is used +to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently nondeterministic. To fix this, in deterministic mode, nodes are addressed by a tuple - where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential -ID local to the worker.\ This sequential ID is similar in principle to the global atomic ID -sequence of the nondeterminsitic mode but since it is local to each worker, it is updated serially -and thus is deterministic. worker IDs are unique, and sequence IDs are unique to their workers, -therefor is a globally unique node identifier. Pseudocost Update: Each worker -updates its local pseudocosts when branching. These updates are queued within horizons. During the -horizon sync, these updates are all played in order, and the newly updated global pseudocosts are -broadcast to the worker's pseudocost snapshots for the coming horizon. + where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential ID +local to the worker.\ This sequential ID is similar in principle to the global atomic ID sequence of +the nondeterminsitic mode but since it is local to each worker, it is updated serially and thus is +deterministic. worker IDs are unique, and sequence IDs are unique to their workers, therefor + is a globally unique node identifier. +Pseudocost Update: + Each worker updates its local pseudocosts when branching. These updates are queued within +horizons. During the horizon sync, these updates are all played in order, and the newly updated +global pseudocosts are broadcast to the worker's pseudocost snapshots for the coming horizon. */ @@ -2926,8 +2882,7 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri "Sync%% | NoWork\n"); settings_.log.printf( " " - "-------+---------+----------+--------+---------+--------+----------+----------+-------+-----" - "--" + "-------+---------+----------+--------+---------+--------+----------+----------+-------+-------" "\n"); for (const auto& worker : *deterministic_workers_) { double sync_time = worker.work_context.total_sync_time; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 3e3bcddd14..da4ae53840 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -301,9 +301,7 @@ class branch_and_bound_t { dual::status_t lp_status, logger_t& log); - bool update_root_bounds(const std::vector& lower_bounds, - const std::vector& upper_bounds, - const std::vector& bounds_changed); + void root_reduced_cost_fixing(f_t upper_bound); // ============================================================================ // Deterministic BSP (Bulk Synchronous Parallel) methods for deterministic parallel B&B diff --git a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp index 8b40eb03df..53fd1737c4 100644 --- a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp +++ b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp @@ -71,7 +71,7 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, } if (num_fixed > 0 || num_improved > 0) { - settings.log.debug( + settings.log.printf( "Reduced costs: Found %d improved bounds and %d fixed variables\n", num_improved, num_fixed); } return {num_fixed, num_improved}; From 00576e62eddd15a07941cf504becdb3bcfd24391 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:47:35 +0100 Subject: [PATCH 11/22] added missing feasibility check. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c0f3e56b8d..a13d312049 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3765,19 +3765,20 @@ void branch_and_bound_t::deterministic_dive( // Setup bounds for this node std::fill(worker.bounds_changed.begin(), worker.bounds_changed.end(), false); + bool feasible = true; if (worker.recompute_bounds_and_basis) { - node_ptr->get_variable_bounds(worker.dive_lower, - worker.dive_upper, - worker.leaf_problem.lower, - worker.leaf_problem.upper, - worker.bounds_changed); + feasible = node_ptr->get_variable_bounds(worker.dive_lower, + worker.dive_upper, + worker.leaf_problem.lower, + worker.leaf_problem.upper, + worker.bounds_changed); } else { - node_ptr->update_branched_variable_bounds(worker.dive_lower, - worker.dive_upper, - worker.leaf_problem.lower, - worker.leaf_problem.upper, - worker.bounds_changed); + feasible = node_ptr->update_branched_variable_bounds(worker.dive_lower, + worker.dive_upper, + worker.leaf_problem.lower, + worker.leaf_problem.upper, + worker.bounds_changed); } double remaining_time = settings_.time_limit - toc(exploration_stats_.start_time); @@ -3792,8 +3793,10 @@ void branch_and_bound_t::deterministic_dive( lp_settings.scale_columns = false; #ifndef DETERMINISM_DISABLE_BOUNDS_STRENGTHENING - bool feasible = worker.node_presolver.bounds_strengthening( - lp_settings, worker.bounds_changed, worker.leaf_problem.lower, worker.leaf_problem.upper); + if (feasible) { + feasible = worker.node_presolver.bounds_strengthening( + lp_settings, worker.bounds_changed, worker.leaf_problem.lower, worker.leaf_problem.upper); + } if (settings_.deterministic) { // TEMP APPROXIMATION; From 5d8eebfff9a491dcdb4075437073bd73cc2530a9 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:34:38 +0100 Subject: [PATCH 12/22] Fixed duplicated settings --- cpp/src/mip_heuristics/solver.cu | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 19d8237f4b..b428940fb3 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -230,10 +230,6 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; branch_and_bound_settings.mip_batch_pdlp_strong_branching = context.settings.mip_batch_pdlp_strong_branching; - branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening == -1 - ? 2 - : context.settings.reduced_cost_strengthening; if (context.settings.num_cpu_threads < 0) { branch_and_bound_settings.num_threads = std::max(1, omp_get_max_threads() - 1); From 2aa7b9e5840145b6e3786fb8113ca6d8ef223965 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:35:20 +0100 Subject: [PATCH 13/22] the absolute gap for stopping B&B is now calculated in the user space. fix incorrect check for infeasible nodes in the tree. --- cpp/src/branch_and_bound/branch_and_bound.cpp | 40 ++++++++++++++----- cpp/src/branch_and_bound/mip_node.hpp | 8 ++-- cpp/src/branch_and_bound/worker.hpp | 2 - cpp/src/mip_heuristics/diversity/lns/rins.cu | 13 +++--- .../diversity/recombiners/sub_mip.cuh | 11 ++--- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index a13d312049..41ed408379 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -195,6 +195,16 @@ f_t user_relative_gap(const lp_problem_t& lp, f_t obj_value, f_t lower return user_mip_gap; } +template +f_t user_absolute_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) +{ + f_t user_obj = compute_user_objective(lp, obj_value); + f_t user_lower_bound = compute_user_objective(lp, lower_bound); + f_t user_mip_gap = std::abs(user_obj - user_lower_bound); + if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } + return user_mip_gap; +} + template std::string user_mip_gap(f_t obj_value, f_t lower_bound) { @@ -697,9 +707,9 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& settings_.heuristic_preemption_callback(); } - f_t gap = upper_bound_ - lower_bound; f_t obj = compute_user_objective(original_lp_, upper_bound_.load()); f_t user_bound = compute_user_objective(original_lp_, lower_bound); + f_t gap = std::abs(obj - user_bound); f_t gap_rel = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); bool is_maximization = original_lp_.obj_scale < 0.0; @@ -1454,7 +1464,6 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_tlower_bound; f_t upper_bound = upper_bound_; - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); // This is based on three assumptions: // - The stack only contains sibling nodes, i.e., the current node and it sibling (if @@ -1567,7 +1576,6 @@ void branch_and_bound_t::dive_with(branch_and_bound_worker_t f_t lower_bound = node_ptr->lower_bound; f_t upper_bound = upper_bound_; - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); worker->lower_bound = lower_bound; if (node_ptr->lower_bound > upper_bound) { @@ -1639,7 +1647,7 @@ void branch_and_bound_t::run_scheduler() #endif f_t lower_bound = get_lower_bound(); - f_t abs_gap = upper_bound_ - lower_bound; + f_t abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); i_t last_node_depth = 0; i_t last_int_infeas = 0; @@ -1649,7 +1657,7 @@ void branch_and_bound_t::run_scheduler() (active_workers_per_strategy_[0] > 0 || node_queue_.best_first_queue_size() > 0)) { bool launched_any_task = false; lower_bound = get_lower_bound(); - abs_gap = upper_bound_ - lower_bound; + abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); repair_heuristic_solutions(); @@ -1778,14 +1786,14 @@ void branch_and_bound_t::single_threaded_solve() branch_and_bound_worker_t worker(0, original_lp_, Arow_, var_types_, settings_); f_t lower_bound = get_lower_bound(); - f_t abs_gap = upper_bound_ - lower_bound; + f_t abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && node_queue_.best_first_queue_size() > 0) { bool launched_any_task = false; lower_bound = get_lower_bound(); - abs_gap = upper_bound_ - lower_bound; + abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); repair_heuristic_solutions(); @@ -1823,7 +1831,19 @@ void branch_and_bound_t::single_threaded_solve() continue; } - worker.init_best_first(start_node.value(), original_lp_); + mutex_original_lp_.lock(); + bool feasible = worker.init_best_first(start_node.value(), original_lp_); + mutex_original_lp_.unlock(); + + if (!feasible) { + // This node was put on the heap earlier but its variables bounds now violates the + // bounds at the root node + search_tree_.graphviz_node( + settings_.log, start_node.value(), "cutoff", start_node.value()->lower_bound); + search_tree_.update(start_node.value(), node_status_t::FATHOMED); + continue; + } + plunge_with(&worker); } } @@ -2450,7 +2470,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut report(' ', obj, root_objective_, 0, num_fractional); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); - f_t abs_gap = upper_bound_.load() - root_objective_; + f_t abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), root_objective_); if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { set_solution_at_root(solution, cut_info); set_final_solution(solution, root_objective_); @@ -3066,7 +3086,7 @@ void branch_and_bound_t::deterministic_sync_callback() f_t lower_bound = deterministic_compute_lower_bound(); f_t upper_bound = upper_bound_.load(); - f_t abs_gap = upper_bound - lower_bound; + f_t abs_gap = user_absolute_gap(original_lp_, upper_bound, lower_bound); f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); if (abs_gap <= settings_.absolute_mip_gap_tol || rel_gap <= settings_.relative_mip_gap_tol) { diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 6b49a5f4a6..ca41e73653 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -104,14 +104,14 @@ class mip_node_t { bool check_variable_bounds(const std::vector& start_lower, const std::vector& start_upper) { - if (branch_var_upper > start_upper[branch_var] || branch_var_lower < start_lower[branch_var]) { + if (branch_var_upper < start_lower[branch_var] || branch_var_lower > start_upper[branch_var]) { return false; } mip_node_t* parent_ptr = parent; while (parent_ptr != nullptr && parent_ptr->node_id != 0) { - if (parent_ptr->branch_var_upper > start_upper[parent_ptr->branch_var] || - parent_ptr->branch_var_lower < start_lower[parent_ptr->branch_var]) { + if (parent_ptr->branch_var_upper < start_lower[parent_ptr->branch_var] || + parent_ptr->branch_var_lower > start_upper[parent_ptr->branch_var]) { return false; } parent_ptr = parent_ptr->parent; @@ -164,7 +164,7 @@ class mip_node_t { // If the start bounds has changed (via reduced cost strengthening), check if the // bounds in the node is still valid. - if (branch_var_upper > start_upper[branch_var] || branch_var_lower < start_lower[branch_var]) { + if (branch_var_upper < start_lower[branch_var] || branch_var_lower > start_upper[branch_var]) { return false; } diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 829b667400..53ef5ee541 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -166,7 +166,6 @@ class branch_and_bound_worker_t { if (recompute_bounds) { feasible = node_ptr->get_variable_bounds( start_lower, start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); - } else { feasible = node_ptr->update_branched_variable_bounds( start_lower, start_upper, leaf_problem.lower, leaf_problem.upper, bounds_changed); @@ -176,7 +175,6 @@ class branch_and_bound_worker_t { feasible = node_presolver.bounds_strengthening( settings, bounds_changed, leaf_problem.lower, leaf_problem.upper); } - return feasible; } diff --git a/cpp/src/mip_heuristics/diversity/lns/rins.cu b/cpp/src/mip_heuristics/diversity/lns/rins.cu index d7d7601014..de4b2557fb 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cu +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cu @@ -264,12 +264,13 @@ void rins_t::run_rins() std::min(current_mip_gap, (f_t)settings.target_mip_gap); branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; branch_and_bound_settings.num_threads = 1; - branch_and_bound_settings.reliability_branching = 0; - branch_and_bound_settings.max_cut_passes = 0; - branch_and_bound_settings.clique_cuts = 0; - branch_and_bound_settings.sub_mip = 1; - branch_and_bound_settings.log.log = false; - branch_and_bound_settings.log.log_prefix = "[RINS] "; + branch_and_bound_settings.reliability_branching = 0; + branch_and_bound_settings.reduced_cost_strengthening = 0; + branch_and_bound_settings.max_cut_passes = 0; + branch_and_bound_settings.clique_cuts = 0; + branch_and_bound_settings.sub_mip = 1; + branch_and_bound_settings.log.log = false; + branch_and_bound_settings.log.log_prefix = "[RINS] "; branch_and_bound_settings.solution_callback = [&rins_solution_queue](std::vector& solution, f_t objective) { rins_solution_queue.push_back(solution); diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index a867141d0a..43aae14204 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -106,11 +106,12 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.relative_mip_gap_tol = context.settings.tolerances.relative_mip_gap; branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; branch_and_bound_settings.num_threads = 1; - branch_and_bound_settings.reliability_branching = 0; - branch_and_bound_settings.max_cut_passes = 0; - branch_and_bound_settings.clique_cuts = 0; - branch_and_bound_settings.sub_mip = 1; - branch_and_bound_settings.solution_callback = [this](std::vector& solution, + branch_and_bound_settings.reliability_branching = 0; + branch_and_bound_settings.reduced_cost_strengthening = 0; + branch_and_bound_settings.max_cut_passes = 0; + branch_and_bound_settings.clique_cuts = 0; + branch_and_bound_settings.sub_mip = 1; + branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); }; From ad0500673fb555006043bd29c402ea99cc1db68b Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:06:05 +0100 Subject: [PATCH 14/22] small change Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/mip_heuristics/solver.cu | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index b428940fb3..c5ea0b6b74 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -223,13 +223,14 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.clique_cuts = context.settings.clique_cuts; branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; - branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening >= 0 ? context.settings.reduced_cost_strengthening - : 3; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; branch_and_bound_settings.mip_batch_pdlp_strong_branching = context.settings.mip_batch_pdlp_strong_branching; + branch_and_bound_settings.reduced_cost_strengthening = + context.settings.reduced_cost_strengthening == -1 + ? 3 + : context.settings.reduced_cost_strengthening; if (context.settings.num_cpu_threads < 0) { branch_and_bound_settings.num_threads = std::max(1, omp_get_max_threads() - 1); From 22f626546d8897c0f10dba0af810e59f43044ec5 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:10:49 +0100 Subject: [PATCH 15/22] reduced verbosity for the reduced cost strengthening Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/branch_and_bound/reduced_cost_fixing.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp index 53fd1737c4..8b40eb03df 100644 --- a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp +++ b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp @@ -71,7 +71,7 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, } if (num_fixed > 0 || num_improved > 0) { - settings.log.printf( + settings.log.debug( "Reduced costs: Found %d improved bounds and %d fixed variables\n", num_improved, num_fixed); } return {num_fixed, num_improved}; From f9ae273298c01b8ab038af9f24cc83e16aa32f7d Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:28:46 +0100 Subject: [PATCH 16/22] fixed incorrect label and lower bound for the node when it violates the root bounds Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/branch_and_bound/branch_and_bound.cpp | 13 ++++++------- cpp/src/branch_and_bound/reduced_cost_fixing.hpp | 5 +++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 41ed408379..8ec5b6a16f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1732,9 +1732,9 @@ void branch_and_bound_t::run_scheduler() if (!feasible) { // This node was put on the heap earlier but its variables bounds now violates the // bounds at the root node - search_tree_.graphviz_node( - settings_.log, start_node.value(), "cutoff", start_node.value()->lower_bound); - search_tree_.update(start_node.value(), node_status_t::FATHOMED); + start_node.value()->lower_bound = inf; + search_tree_.graphviz_node(settings_.log, start_node.value(), "infeasible", 0.0); + search_tree_.update(start_node.value(), node_status_t::INFEASIBLE); continue; } @@ -1838,9 +1838,9 @@ void branch_and_bound_t::single_threaded_solve() if (!feasible) { // This node was put on the heap earlier but its variables bounds now violates the // bounds at the root node - search_tree_.graphviz_node( - settings_.log, start_node.value(), "cutoff", start_node.value()->lower_bound); - search_tree_.update(start_node.value(), node_status_t::FATHOMED); + start_node.value()->lower_bound = inf; + search_tree_.graphviz_node(settings_.log, start_node.value(), "infeasible", 0.0); + search_tree_.update(start_node.value(), node_status_t::INFEASIBLE); continue; } @@ -2654,7 +2654,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut search_tree_.graphviz_node( settings_.log, start_node.value(), "cutoff", start_node.value()->lower_bound); search_tree_.update(start_node.value(), node_status_t::FATHOMED); - continue; } else { node_queue_.push( start_node.value()); // Needed to ensure we don't lose the correct lower bound diff --git a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp index 8b40eb03df..6f85df9285 100644 --- a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp +++ b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp @@ -27,6 +27,7 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, const f_t threshold = 100.0 * settings.integer_tol; const f_t weaken = settings.integer_tol; const f_t fixed_tol = settings.fixed_tol; + const f_t abs_gap = upper_bound - obj; i_t num_improved = 0; i_t num_fixed = 0; i_t num_cols_to_check = reduced_costs.size(); // Reduced costs will be smaller than the original @@ -38,7 +39,6 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold) { const f_t lower_j = lower_bounds[j]; const f_t upper_j = upper_bounds[j]; - const f_t abs_gap = upper_bound - obj; f_t reduced_cost_upper_bound = upper_j; f_t reduced_cost_lower_bound = lower_j; if (lower_j > -inf && reduced_costs[j] > 0) { @@ -63,6 +63,7 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, bounds_changed[j] = true; } } + if (var_types[j] == variable_type_t::INTEGER && reduced_cost_upper_bound <= reduced_cost_lower_bound + fixed_tol) { ++num_fixed; @@ -71,7 +72,7 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, } if (num_fixed > 0 || num_improved > 0) { - settings.log.debug( + settings.log.printf( "Reduced costs: Found %d improved bounds and %d fixed variables\n", num_improved, num_fixed); } return {num_fixed, num_improved}; From fe687fb25d059eec66c19759a74890ce521da6d5 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:21:46 +0100 Subject: [PATCH 17/22] removed variable bounds check. extended reduced cost strengthening to continuous variables Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/branch_and_bound/branch_and_bound.cpp | 28 +------------- cpp/src/branch_and_bound/mip_node.hpp | 33 +++-------------- .../branch_and_bound/reduced_cost_fixing.hpp | 37 +++++++++---------- cpp/src/branch_and_bound/worker.hpp | 6 +-- 4 files changed, 26 insertions(+), 78 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 8ec5b6a16f..185202b68d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1725,21 +1725,9 @@ void branch_and_bound_t::run_scheduler() continue; } - mutex_original_lp_.lock(); - bool feasible = worker->init_best_first(start_node.value(), original_lp_); - mutex_original_lp_.unlock(); - - if (!feasible) { - // This node was put on the heap earlier but its variables bounds now violates the - // bounds at the root node - start_node.value()->lower_bound = inf; - search_tree_.graphviz_node(settings_.log, start_node.value(), "infeasible", 0.0); - search_tree_.update(start_node.value(), node_status_t::INFEASIBLE); - continue; - } - // Remove the worker from the idle list. worker_pool_.pop_idle_worker(); + worker->init_best_first(start_node.value(), original_lp_); last_node_depth = start_node.value()->depth; last_int_infeas = start_node.value()->integer_infeasible; active_workers_per_strategy_[strategy]++; @@ -1831,19 +1819,7 @@ void branch_and_bound_t::single_threaded_solve() continue; } - mutex_original_lp_.lock(); - bool feasible = worker.init_best_first(start_node.value(), original_lp_); - mutex_original_lp_.unlock(); - - if (!feasible) { - // This node was put on the heap earlier but its variables bounds now violates the - // bounds at the root node - start_node.value()->lower_bound = inf; - search_tree_.graphviz_node(settings_.log, start_node.value(), "infeasible", 0.0); - search_tree_.update(start_node.value(), node_status_t::INFEASIBLE); - continue; - } - + worker.init_best_first(start_node.value(), original_lp_); plunge_with(&worker); } } diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index ca41e73653..122aff2e7a 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -98,28 +98,6 @@ class mip_node_t { children[1] = nullptr; } - // Check if no node in the path violates the initial bounds after branching on a given variable. - // The bound violation can happen after the initial bounds is changed via reduced cost - // strengthening and one of the node in the path was created based on the old values. - bool check_variable_bounds(const std::vector& start_lower, - const std::vector& start_upper) - { - if (branch_var_upper < start_lower[branch_var] || branch_var_lower > start_upper[branch_var]) { - return false; - } - - mip_node_t* parent_ptr = parent; - while (parent_ptr != nullptr && parent_ptr->node_id != 0) { - if (parent_ptr->branch_var_upper < start_lower[parent_ptr->branch_var] || - parent_ptr->branch_var_lower > start_upper[parent_ptr->branch_var]) { - return false; - } - parent_ptr = parent_ptr->parent; - } - - return true; - } - // Get the variable bounds starting at the current node and then traversing it back until the // the root node. The bounds are initially set based on the `start_lower` and `start_upper`. // Return true if all bounds are valid (i.e., no node in the path violates the initial bounds @@ -171,12 +149,13 @@ class mip_node_t { // If the bounds have already been updated on another node, // skip this node as it contains looser bounds, since we // are traversing up the tree toward the root - if (bounds_changed[branch_var]) { return true; } + if (!bounds_changed[branch_var]) { + // Apply the bounds at the current node + lower[branch_var] = branch_var_lower; + upper[branch_var] = branch_var_upper; + bounds_changed[branch_var] = true; + } - // Apply the bounds at the current node - lower[branch_var] = branch_var_lower; - upper[branch_var] = branch_var_upper; - bounds_changed[branch_var] = true; return true; } diff --git a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp index 6f85df9285..f1fbe23d91 100644 --- a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp +++ b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp @@ -37,37 +37,34 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, for (i_t j = 0; j < num_cols_to_check; j++) { if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold) { - const f_t lower_j = lower_bounds[j]; - const f_t upper_j = upper_bounds[j]; - f_t reduced_cost_upper_bound = upper_j; - f_t reduced_cost_lower_bound = lower_j; + const f_t lower_j = lower_bounds[j]; + const f_t upper_j = upper_bounds[j]; + const bool is_integer = + var_types[j] == variable_type_t::INTEGER || var_types[j] == variable_type_t::BINARY; + if (lower_j > -inf && reduced_costs[j] > 0) { - const f_t new_upper_bound = lower_j + abs_gap / reduced_costs[j]; - reduced_cost_upper_bound = var_types[j] == variable_type_t::INTEGER - ? std::floor(new_upper_bound + weaken) - : new_upper_bound; - if (reduced_cost_upper_bound < upper_j && var_types[j] == variable_type_t::INTEGER) { + f_t new_upper_bound = lower_j + abs_gap / reduced_costs[j]; + if (is_integer) { new_upper_bound = std::floor(new_upper_bound + weaken); } + + if (new_upper_bound < upper_j) { ++num_improved; - upper_bounds[j] = reduced_cost_upper_bound; + upper_bounds[j] = new_upper_bound; bounds_changed[j] = true; } } + if (upper_j < inf && reduced_costs[j] < 0) { - const f_t new_lower_bound = upper_j + abs_gap / reduced_costs[j]; - reduced_cost_lower_bound = var_types[j] == variable_type_t::INTEGER - ? std::ceil(new_lower_bound - weaken) - : new_lower_bound; - if (reduced_cost_lower_bound > lower_j && var_types[j] == variable_type_t::INTEGER) { + f_t new_lower_bound = upper_j + abs_gap / reduced_costs[j]; + if (is_integer) { new_lower_bound = std::ceil(new_lower_bound - weaken); } + + if (new_lower_bound > lower_j) { ++num_improved; - lower_bounds[j] = reduced_cost_lower_bound; + lower_bounds[j] = new_lower_bound; bounds_changed[j] = true; } } - if (var_types[j] == variable_type_t::INTEGER && - reduced_cost_upper_bound <= reduced_cost_lower_bound + fixed_tol) { - ++num_fixed; - } + if (is_integer && upper_bounds[j] <= lower_bounds[j] + fixed_tol) { ++num_fixed; } } } diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 53ef5ee541..b31fe920cc 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -113,18 +113,14 @@ class branch_and_bound_worker_t { // Initialize the worker for plunging, setting the `start_node`, `start_lower` and // `start_upper`. Returns `true` if no bounds were violated in any of the previous nodes - bool init_best_first(mip_node_t* node, lp_problem_t& original_lp) + void init_best_first(mip_node_t* node, lp_problem_t& original_lp) { - bool feasible = node->check_variable_bounds(original_lp.lower, original_lp.upper); - if (!feasible) { return false; } - start_node = node; start_lower = original_lp.lower; start_upper = original_lp.upper; search_strategy = BEST_FIRST; lower_bound = node->lower_bound; is_active = true; - return true; } // Initialize the worker for diving, setting the `start_node`, `start_lower` and From 0e7e83be8a3a1576bb0fbb1475d70256ded9b98b Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:54:20 +0100 Subject: [PATCH 18/22] removed reduced cost strengthening for continuous variables Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- .../branch_and_bound/reduced_cost_fixing.hpp | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp index f1fbe23d91..e06114ace5 100644 --- a/cpp/src/branch_and_bound/reduced_cost_fixing.hpp +++ b/cpp/src/branch_and_bound/reduced_cost_fixing.hpp @@ -43,24 +43,26 @@ std::pair reduced_cost_fixing(const std::vector& reduced_costs, var_types[j] == variable_type_t::INTEGER || var_types[j] == variable_type_t::BINARY; if (lower_j > -inf && reduced_costs[j] > 0) { - f_t new_upper_bound = lower_j + abs_gap / reduced_costs[j]; - if (is_integer) { new_upper_bound = std::floor(new_upper_bound + weaken); } - - if (new_upper_bound < upper_j) { - ++num_improved; - upper_bounds[j] = new_upper_bound; - bounds_changed[j] = true; + if (is_integer) { + f_t new_upper_bound = lower_j + abs_gap / reduced_costs[j]; + new_upper_bound = std::floor(new_upper_bound + weaken); + if (new_upper_bound < upper_j) { + ++num_improved; + upper_bounds[j] = new_upper_bound; + bounds_changed[j] = true; + } } } if (upper_j < inf && reduced_costs[j] < 0) { - f_t new_lower_bound = upper_j + abs_gap / reduced_costs[j]; - if (is_integer) { new_lower_bound = std::ceil(new_lower_bound - weaken); } - - if (new_lower_bound > lower_j) { - ++num_improved; - lower_bounds[j] = new_lower_bound; - bounds_changed[j] = true; + if (is_integer) { + f_t new_lower_bound = upper_j + abs_gap / reduced_costs[j]; + new_lower_bound = std::ceil(new_lower_bound - weaken); + if (new_lower_bound > lower_j) { + ++num_improved; + lower_bounds[j] = new_lower_bound; + bounds_changed[j] = true; + } } } From e9ac6746a54aa7f17f8f252370a6046b94267fc8 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:08:31 +0200 Subject: [PATCH 19/22] re-added feasibility checks Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/branch_and_bound/branch_and_bound.cpp | 28 +++++++++++++- cpp/src/branch_and_bound/mip_node.hpp | 37 ++++++++++++++----- cpp/src/branch_and_bound/worker.hpp | 6 ++- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 185202b68d..8ec5b6a16f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1725,9 +1725,21 @@ void branch_and_bound_t::run_scheduler() continue; } + mutex_original_lp_.lock(); + bool feasible = worker->init_best_first(start_node.value(), original_lp_); + mutex_original_lp_.unlock(); + + if (!feasible) { + // This node was put on the heap earlier but its variables bounds now violates the + // bounds at the root node + start_node.value()->lower_bound = inf; + search_tree_.graphviz_node(settings_.log, start_node.value(), "infeasible", 0.0); + search_tree_.update(start_node.value(), node_status_t::INFEASIBLE); + continue; + } + // Remove the worker from the idle list. worker_pool_.pop_idle_worker(); - worker->init_best_first(start_node.value(), original_lp_); last_node_depth = start_node.value()->depth; last_int_infeas = start_node.value()->integer_infeasible; active_workers_per_strategy_[strategy]++; @@ -1819,7 +1831,19 @@ void branch_and_bound_t::single_threaded_solve() continue; } - worker.init_best_first(start_node.value(), original_lp_); + mutex_original_lp_.lock(); + bool feasible = worker.init_best_first(start_node.value(), original_lp_); + mutex_original_lp_.unlock(); + + if (!feasible) { + // This node was put on the heap earlier but its variables bounds now violates the + // bounds at the root node + start_node.value()->lower_bound = inf; + search_tree_.graphviz_node(settings_.log, start_node.value(), "infeasible", 0.0); + search_tree_.update(start_node.value(), node_status_t::INFEASIBLE); + continue; + } + plunge_with(&worker); } } diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 122aff2e7a..a2e90c8dce 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -98,10 +98,33 @@ class mip_node_t { children[1] = nullptr; } + // If the start bounds has changed (via reduced cost strengthening), check if the + // node is still feasible. + bool is_infeasible(const std::vector& start_lower, const std::vector& start_upper) + { + return branch_var_upper < start_lower[branch_var] || branch_var_lower > start_upper[branch_var]; + } + + // Check all the nodes from the current one until the root are still feasible according + // to the variable bounds of the branched variable. + bool check_variable_bounds(const std::vector& start_lower, + const std::vector& start_upper) + { + if (is_infeasible(start_lower, start_upper)) { return false; } + + mip_node_t* parent_ptr = parent; + while (parent_ptr != nullptr && parent_ptr->node_id != 0) { + if (parent_ptr->is_infeasible(start_lower, start_upper)) { return false; } + parent_ptr = parent_ptr->parent; + } + + return true; + } + // Get the variable bounds starting at the current node and then traversing it back until the // the root node. The bounds are initially set based on the `start_lower` and `start_upper`. - // Return true if all bounds are valid (i.e., no node in the path violates the initial bounds - // after branching on a given variable). + // Return true if all nodes from the current one until the root are still feasible according + // to the variable bounds bool get_variable_bounds(const std::vector& start_lower, const std::vector& start_upper, std::vector& lower, @@ -127,8 +150,8 @@ class mip_node_t { } // Here we assume that we are traversing from the deepest node to the - // root of the tree. Return true if no bounds were violated in this node - // considering the starting bounds + // root of the tree. Return true if this node is feasible according + // to the variable bounds bool update_branched_variable_bounds(const std::vector& start_lower, const std::vector& start_upper, std::vector& lower, @@ -140,11 +163,7 @@ class mip_node_t { assert(upper.size() > branch_var); assert(bounds_changed.size() > branch_var); - // If the start bounds has changed (via reduced cost strengthening), check if the - // bounds in the node is still valid. - if (branch_var_upper < start_lower[branch_var] || branch_var_lower > start_upper[branch_var]) { - return false; - } + if (is_infeasible(start_lower, start_upper)) { return false; } // If the bounds have already been updated on another node, // skip this node as it contains looser bounds, since we diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index b31fe920cc..53ef5ee541 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -113,14 +113,18 @@ class branch_and_bound_worker_t { // Initialize the worker for plunging, setting the `start_node`, `start_lower` and // `start_upper`. Returns `true` if no bounds were violated in any of the previous nodes - void init_best_first(mip_node_t* node, lp_problem_t& original_lp) + bool init_best_first(mip_node_t* node, lp_problem_t& original_lp) { + bool feasible = node->check_variable_bounds(original_lp.lower, original_lp.upper); + if (!feasible) { return false; } + start_node = node; start_lower = original_lp.lower; start_upper = original_lp.upper; search_strategy = BEST_FIRST; lower_bound = node->lower_bound; is_active = true; + return true; } // Initialize the worker for diving, setting the `start_node`, `start_lower` and From d1ca6304df33457925e5e45e06cf014e436f2195 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:45:55 +0200 Subject: [PATCH 20/22] fix compilation. removed mip_node.cpp (it was just a single, simple function). Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/branch_and_bound/CMakeLists.txt | 1 - cpp/src/branch_and_bound/mip_node.cpp | 18 ------------------ cpp/src/branch_and_bound/mip_node.hpp | 8 ++++++-- 3 files changed, 6 insertions(+), 21 deletions(-) delete mode 100644 cpp/src/branch_and_bound/mip_node.cpp diff --git a/cpp/src/branch_and_bound/CMakeLists.txt b/cpp/src/branch_and_bound/CMakeLists.txt index 5bb1017120..1e40c1bbf1 100644 --- a/cpp/src/branch_and_bound/CMakeLists.txt +++ b/cpp/src/branch_and_bound/CMakeLists.txt @@ -5,7 +5,6 @@ set(BRANCH_AND_BOUND_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/branch_and_bound.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/mip_node.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pseudo_costs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/diving_heuristics.cpp ) diff --git a/cpp/src/branch_and_bound/mip_node.cpp b/cpp/src/branch_and_bound/mip_node.cpp deleted file mode 100644 index 7b0f644f4e..0000000000 --- a/cpp/src/branch_and_bound/mip_node.cpp +++ /dev/null @@ -1,18 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include - -namespace cuopt::linear_programming::dual_simplex { - -bool inactive_status(node_status_t status) -{ - return (status == node_status_t::FATHOMED || status == node_status_t::INTEGER_FEASIBLE || - status == node_status_t::INFEASIBLE || status == node_status_t::NUMERICAL); -} - -} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index a2e90c8dce..4ec4db4b9c 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -31,7 +31,11 @@ enum class node_status_t : int { enum class rounding_direction_t : int8_t { NONE = -1, DOWN = 0, UP = 1 }; -bool inactive_status(node_status_t status); +inline bool inactive_status(node_status_t status) +{ + return (status == node_status_t::FATHOMED || status == node_status_t::INTEGER_FEASIBLE || + status == node_status_t::INFEASIBLE || status == node_status_t::NUMERICAL); +} template class mip_node_t { @@ -100,7 +104,7 @@ class mip_node_t { // If the start bounds has changed (via reduced cost strengthening), check if the // node is still feasible. - bool is_infeasible(const std::vector& start_lower, const std::vector& start_upper) + bool is_infeasible(const std::vector& start_lower, const std::vector& start_upper) const { return branch_var_upper < start_lower[branch_var] || branch_var_lower > start_upper[branch_var]; } From a3453be56818d543687df7caa32a7726efca495f Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:21:47 +0200 Subject: [PATCH 21/22] fixed gap checks in plunging and diving Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/branch_and_bound/branch_and_bound.cpp | 87 +++++++++++-------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 8ec5b6a16f..cc04437c39 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -195,16 +195,6 @@ f_t user_relative_gap(const lp_problem_t& lp, f_t obj_value, f_t lower return user_mip_gap; } -template -f_t user_absolute_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) -{ - f_t user_obj = compute_user_objective(lp, obj_value); - f_t user_lower_bound = compute_user_objective(lp, lower_bound); - f_t user_mip_gap = std::abs(user_obj - user_lower_bound); - if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } - return user_mip_gap; -} - template std::string user_mip_gap(f_t obj_value, f_t lower_bound) { @@ -1458,22 +1448,30 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_trecompute_basis = true; worker->recompute_bounds = true; - while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET) { + f_t lower_bound = get_lower_bound(); + f_t upper_bound = upper_bound_; + f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + f_t abs_gap = upper_bound - lower_bound; + + while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && + rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); - f_t lower_bound = node_ptr->lower_bound; - f_t upper_bound = upper_bound_; - // This is based on three assumptions: // - The stack only contains sibling nodes, i.e., the current node and it sibling (if // applicable) // - The current node and its siblings uses the lower bound of the parent before solving the // LP relaxation // - The lower bound of the parent is lower or equal to its children - worker->lower_bound = lower_bound; + worker->lower_bound = node_ptr->lower_bound; + + lower_bound = get_lower_bound(); + upper_bound = upper_bound_; + rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + abs_gap = lower_bound - lower_bound; - if (lower_bound > upper_bound) { + if (node_ptr->lower_bound > upper_bound) { search_tree_.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); search_tree_.update(node_ptr, node_status_t::FATHOMED); worker->recompute_basis = true; @@ -1540,6 +1538,18 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_t 0 && + (rel_gap <= settings_.relative_mip_gap_tol || abs_gap <= settings_.absolute_mip_gap_tol)) { + // If the solver converged according to the gap rules, but we still have nodes to explore + // in the stack, then we should add all the pending nodes back to the heap so the lower + // bound of the solver is set to the correct value. + while (!stack.empty()) { + auto node = stack.front(); + stack.pop_front(); + node_queue_.push(node); + } + } + if (settings_.num_threads > 1) { worker_pool_.return_worker_to_pool(worker); active_workers_per_strategy_[BEST_FIRST]--; @@ -1570,13 +1580,22 @@ void branch_and_bound_t::dive_with(branch_and_bound_worker_t dive_stats.nodes_explored = 0; dive_stats.nodes_unexplored = 1; - while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET && is_running_) { + f_t lower_bound = get_lower_bound(); + f_t upper_bound = upper_bound_; + f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + f_t abs_gap = upper_bound - lower_bound; + + while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET && is_running_ && + rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); - f_t lower_bound = node_ptr->lower_bound; - f_t upper_bound = upper_bound_; - worker->lower_bound = lower_bound; + worker->lower_bound = node_ptr->lower_bound; + + lower_bound = get_lower_bound(); + upper_bound = upper_bound_; + rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + abs_gap = lower_bound - lower_bound; if (node_ptr->lower_bound > upper_bound) { worker->recompute_basis = true; @@ -1647,19 +1666,15 @@ void branch_and_bound_t::run_scheduler() #endif f_t lower_bound = get_lower_bound(); - f_t abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); + f_t abs_gap = upper_bound_ - lower_bound; f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); i_t last_node_depth = 0; i_t last_int_infeas = 0; - while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && - rel_gap > settings_.relative_mip_gap_tol && + while (solver_status_ == mip_status_t::UNSET && is_running_ && + abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_workers_per_strategy_[0] > 0 || node_queue_.best_first_queue_size() > 0)) { bool launched_any_task = false; - lower_bound = get_lower_bound(); - abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); - rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); - repair_heuristic_solutions(); // If the guided diving was disabled previously due to the lack of an incumbent solution, @@ -1777,6 +1792,9 @@ void branch_and_bound_t::run_scheduler() // execution of the scheduler. As of 8/Jan/2026, GCC does not // implement taskyield, but LLVM does. if (!launched_any_task) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } + lower_bound = get_lower_bound(); + abs_gap = upper_bound_ - lower_bound; + rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); } } @@ -1786,16 +1804,11 @@ void branch_and_bound_t::single_threaded_solve() branch_and_bound_worker_t worker(0, original_lp_, Arow_, var_types_, settings_); f_t lower_bound = get_lower_bound(); - f_t abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); + f_t abs_gap = upper_bound_ - lower_bound; f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && node_queue_.best_first_queue_size() > 0) { - bool launched_any_task = false; - lower_bound = get_lower_bound(); - abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), lower_bound); - rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); - repair_heuristic_solutions(); f_t now = toc(exploration_stats_.start_time); @@ -1845,6 +1858,10 @@ void branch_and_bound_t::single_threaded_solve() } plunge_with(&worker); + + lower_bound = get_lower_bound(); + abs_gap = upper_bound_ - lower_bound; + rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); } } @@ -2470,7 +2487,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut report(' ', obj, root_objective_, 0, num_fractional); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); - f_t abs_gap = user_absolute_gap(original_lp_, upper_bound_.load(), root_objective_); + f_t abs_gap = upper_bound_ - root_relax_objective; if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { set_solution_at_root(solution, cut_info); set_final_solution(solution, root_objective_); @@ -3085,7 +3102,7 @@ void branch_and_bound_t::deterministic_sync_callback() f_t lower_bound = deterministic_compute_lower_bound(); f_t upper_bound = upper_bound_.load(); - f_t abs_gap = user_absolute_gap(original_lp_, upper_bound, lower_bound); + f_t abs_gap = upper_bound - lower_bound; f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); if (abs_gap <= settings_.absolute_mip_gap_tol || rel_gap <= settings_.relative_mip_gap_tol) { From c785f086e978a32a5775731487f8362e95add592 Mon Sep 17 00:00:00 2001 From: Nicolas Guidotti Date: Wed, 1 Apr 2026 18:19:16 +0200 Subject: [PATCH 22/22] fix incorrect bounds Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com> --- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 86e3bd9794..08b4b36be7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1470,7 +1470,7 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_tlower_bound > get_cutoff()) { search_tree_.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); @@ -1596,7 +1596,7 @@ void branch_and_bound_t::dive_with(branch_and_bound_worker_t lower_bound = get_lower_bound(); upper_bound = upper_bound_; rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); - abs_gap = lower_bound - lower_bound; + abs_gap = upper_bound - lower_bound; if (node_ptr->lower_bound > get_cutoff()) { worker->recompute_basis = true; @@ -2488,7 +2488,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut report(' ', obj, root_objective_, 0, num_fractional); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); - f_t abs_gap = upper_bound_ - root_relax_objective; + f_t abs_gap = upper_bound_.load() - root_objective_; if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { set_solution_at_root(solution, cut_info); set_final_solution(solution, root_objective_);