From c9081a6e71ea5e8e1ccee4cc79b85b35ce146c46 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 3 Dec 2025 16:27:16 +0100 Subject: [PATCH 01/70] implemented diving heuristics. sorted starting nodes for diving based on the objective pseudcost estimate. --- cpp/src/dual_simplex/branch_and_bound.cpp | 256 +++++++++++------- cpp/src/dual_simplex/branch_and_bound.hpp | 24 +- cpp/src/dual_simplex/diving_queue.hpp | 2 +- cpp/src/dual_simplex/logger.hpp | 8 +- cpp/src/dual_simplex/mip_node.hpp | 16 +- cpp/src/dual_simplex/pseudo_costs.cpp | 309 +++++++++++++++++++++- cpp/src/dual_simplex/pseudo_costs.hpp | 45 +++- 7 files changed, 531 insertions(+), 129 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 77acca8f7..fce774c86 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -183,21 +183,24 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) { const f_t user_mip_gap = relative_gap(obj_value, lower_bound); if (user_mip_gap == std::numeric_limits::infinity()) { - return " - "; + return " - "; } else { constexpr int BUFFER_LEN = 32; char buffer[BUFFER_LEN]; - snprintf(buffer, BUFFER_LEN - 1, "%4.1f%%", user_mip_gap * 100); + snprintf(buffer, BUFFER_LEN - 1, "%5.1f%%", user_mip_gap * 100); return std::string(buffer); } } -inline const char* feasible_solution_symbol(thread_type_t type) +inline const char* feasible_solution_symbol(bnb_thread_type_t type) { switch (type) { - case thread_type_t::EXPLORATION: return "B"; - case thread_type_t::DIVING: return "D"; - default: return "U"; + case bnb_thread_type_t::EXPLORATION: return "B "; + case bnb_thread_type_t::COEFFICIENT_DIVING: return "CD"; + case bnb_thread_type_t::LINE_SEARCH_DIVING: return "LD"; + case bnb_thread_type_t::PSEUDOCOST_DIVING: return "PD"; + case bnb_thread_type_t::GUIDED_DIVING: return "GD"; + default: return "U "; } } @@ -310,7 +313,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu std::string gap = user_mip_gap(user_obj, user_lower); settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + "H %+13.6e %+10.6e %s %9.2f\n", user_obj, user_lower, gap.c_str(), @@ -423,7 +426,7 @@ void branch_and_bound_t::repair_heuristic_solutions() std::string user_gap = user_mip_gap(obj, lower); settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + "H %+13.6e %+10.6e %s %9.2f\n", obj, lower, user_gap.c_str(), @@ -521,12 +524,16 @@ template void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - thread_type_t thread_type) + bnb_thread_type_t thread_type) { bool send_solution = false; i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + settings_.log.debug("%s found a feasible solution with obj=%.10e.\n", + feasible_solution_symbol(thread_type), + compute_user_objective(original_lp_, leaf_objective)); + mutex_upper_.lock(); if (leaf_objective < upper_bound_) { incumbent_.set_incumbent_solution(leaf_objective, leaf_solution); @@ -534,17 +541,17 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - settings_.log.printf( - "%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - feasible_solution_symbol(thread_type), - nodes_explored, - nodes_unexplored, - obj, - lower, - leaf_depth, - nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(exploration_stats_.start_time)); + f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + feasible_solution_symbol(thread_type), + nodes_explored, + nodes_unexplored, + obj, + lower, + leaf_depth, + iter_node, + user_mip_gap(obj, lower).c_str(), + toc(exploration_stats_.start_time)); send_solution = true; } @@ -558,21 +565,44 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, } template -rounding_direction_t branch_and_bound_t::child_selection(mip_node_t* node_ptr) +branch_variable_t branch_and_bound_t::variable_selection( + mip_node_t* node_ptr, + const std::vector& fractional, + const std::vector& solution, + bnb_thread_type_t type, + logger_t& log) { - const i_t branch_var = node_ptr->get_down_child()->branch_var; - const f_t branch_var_val = node_ptr->get_down_child()->fractional_val; - const f_t down_val = std::floor(root_relax_soln_.x[branch_var]); - const f_t up_val = std::ceil(root_relax_soln_.x[branch_var]); - const f_t down_dist = branch_var_val - down_val; - const f_t up_dist = up_val - branch_var_val; - constexpr f_t eps = 1e-6; + i_t branch_var = -1; + f_t obj_estimate = 0; + rounding_direction_t round_dir = rounding_direction_t::NONE; - if (down_dist < up_dist + eps) { - return rounding_direction_t::DOWN; + switch (type) { + case bnb_thread_type_t::EXPLORATION: + std::tie(branch_var, obj_estimate) = + pc_.variable_selection_and_obj_estimate(fractional, solution, node_ptr->lower_bound, log); + round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); - } else { - return rounding_direction_t::UP; + // Note that the exploration thread is the only one that can insert new nodes into the heap, + // and thus, we only need to calculate the objective estimate here (it is used for + // sorting the nodes for diving). + node_ptr->objective_estimate = obj_estimate; + return {branch_var, round_dir}; + + case bnb_thread_type_t::COEFFICIENT_DIVING: + return coefficient_diving(original_lp_, fractional, solution, log); + + case bnb_thread_type_t::LINE_SEARCH_DIVING: + return line_search_diving(fractional, solution, root_relax_soln_.x, log); + + case bnb_thread_type_t::PSEUDOCOST_DIVING: + return pseudocost_diving(pc_, fractional, solution, root_relax_soln_.x, log); + + case bnb_thread_type_t::GUIDED_DIVING: + return guided_diving(pc_, fractional, solution, incumbent_.x, log); + + default: + log.debug("Unknown variable selection method: %d\n", type); + return {-1, rounding_direction_t::NONE}; } } @@ -585,15 +615,21 @@ node_solve_info_t branch_and_bound_t::solve_node( std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, - thread_type_t thread_type, + bnb_thread_type_t thread_type, bool recompute_bounds_and_basis, const std::vector& root_lower, const std::vector& root_upper, + stats_t& stats, logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; const f_t upper_bound = get_upper_bound(); + // If there is no incumbent, use pseudocost diving instead of guided diving + if (upper_bound == inf && thread_type == bnb_thread_type_t::GUIDED_DIVING) { + thread_type = bnb_thread_type_t::PSEUDOCOST_DIVING; + } + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); std::vector& leaf_vstatus = node_ptr->vstatus; assert(leaf_vstatus.size() == leaf_problem.num_cols); @@ -605,25 +641,11 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); lp_settings.scale_columns = false; -#ifdef LOG_NODE_SIMPLEX - lp_settings.set_log(true); - std::stringstream ss; - ss << "simplex-" << std::this_thread::get_id() << ".log"; - std::string logname; - ss >> logname; - lp_settings.set_log_filename(logname); - lp_settings.log.enable_log_to_file("a+"); - lp_settings.log.log_to_console = false; - lp_settings.log.printf( - "%s node id = %d, branch var = %d, fractional val = %f, variable lower bound = %f, variable " - "upper bound = %f\n", - settings_.log.log_prefix.c_str(), - node_ptr->node_id, - node_ptr->branch_var, - node_ptr->fractional_val, - node_ptr->branch_var_lower, - node_ptr->branch_var_upper); -#endif + if (thread_type != bnb_thread_type_t::EXPLORATION) { + f_t max_iter = 0.05 * exploration_stats_.total_lp_iters; + lp_settings.iteration_limit = max_iter - stats.total_lp_iters; + if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } + } // Reset the bound_changed markers std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); @@ -650,6 +672,29 @@ node_solve_info_t branch_and_bound_t::solve_node( f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; +#ifdef LOG_NODE_SIMPLEX + lp_settings.set_log(true); + std::stringstream ss; + ss << "simplex-" << std::this_thread::get_id() << ".log"; + std::string logname; + ss >> logname; + lp_settings.log.set_log_file(logname, "a"); + lp_settings.log.log_to_console = false; + lp_settings.log.printf( + "%s\ncurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " + "%f, variable lower " + "bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", + settings_.log.log_prefix.c_str(), + node_ptr->node_id, + node_ptr->depth, + node_ptr->branch_var, + node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP", + node_ptr->fractional_val, + node_ptr->branch_var_lower, + node_ptr->branch_var_upper, + node_ptr->vstatus[node_ptr->branch_var]); +#endif + lp_status = dual_phase2_with_advanced_basis(2, 0, recompute_bounds_and_basis, @@ -679,10 +724,12 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_status = convert_lp_status_to_dual_status(second_status); } - if (thread_type == thread_type_t::EXPLORATION) { - exploration_stats_.total_lp_solve_time += toc(lp_start_time); - exploration_stats_.total_lp_iters += node_iter; - } +#ifdef LOG_NODE_SIMPLEX + lp_settings.log.printf("\nLP status: %d\n\n", lp_status); +#endif + + stats.total_lp_solve_time += toc(lp_start_time); + stats.total_lp_iters += node_iter; } if (lp_status == dual::status_t::DUAL_UNBOUNDED) { @@ -726,16 +773,17 @@ node_solve_info_t branch_and_bound_t::solve_node( } else if (leaf_objective <= upper_bound + abs_fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = - pc_.variable_selection(leaf_fractional, leaf_solution.x, lp_settings.log); + auto [branch_var, round_dir] = variable_selection( + node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); + assert(round_dir != rounding_direction_t::NONE); + assert(branch_var >= 0); assert(leaf_vstatus.size() == leaf_problem.num_cols); + search_tree.branch( node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, leaf_problem, log); search_tree.update(node_ptr, node_status_t::HAS_CHILDREN); - rounding_direction_t round_dir = child_selection(node_ptr); - if (round_dir == rounding_direction_t::UP) { return node_solve_info_t::UP_CHILD_FIRST; } else { @@ -751,8 +799,11 @@ node_solve_info_t branch_and_bound_t::solve_node( search_tree.graphviz_node(log, node_ptr, "timeout", 0.0); return node_solve_info_t::TIME_LIMIT; + } else if (lp_status == dual::status_t::ITERATION_LIMIT) { + return node_solve_info_t::ITERATION_LIMIT; + } else { - if (thread_type == thread_type_t::EXPLORATION) { + if (thread_type == bnb_thread_type_t::EXPLORATION) { fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); log.printf( "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " @@ -810,17 +861,17 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod std::string gap_user = user_mip_gap(obj, user_lower); i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - - settings_.log.printf( - " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node->depth, - nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, - gap_user.c_str(), - now); + f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + + settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + obj, + user_lower, + node->depth, + iter_node, + gap_user.c_str(), + now); exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); @@ -850,10 +901,11 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod basic_list, nonbasic_list, node_presolver, - thread_type_t::EXPLORATION, + bnb_thread_type_t::EXPLORATION, true, original_lp_.lower, original_lp_.upper, + exploration_stats_, settings_.log); ++exploration_stats_.nodes_since_last_log; @@ -922,6 +974,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); search_tree.update(node_ptr, node_status_t::FATHOMED); + recompute_bounds_and_basis = true; --exploration_stats_.nodes_unexplored; continue; } @@ -943,7 +996,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, i_t nodes_unexplored = exploration_stats_.nodes_unexplored; settings_.log.printf( - " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", nodes_explored, nodes_unexplored, obj, @@ -973,10 +1026,11 @@ void branch_and_bound_t::explore_subtree(i_t task_id, basic_list, nonbasic_list, node_presolver, - thread_type_t::EXPLORATION, + bnb_thread_type_t::EXPLORATION, recompute_bounds_and_basis, original_lp_.lower, original_lp_.upper, + exploration_stats_, settings_.log); recompute_bounds_and_basis = !has_children(status); @@ -1109,8 +1163,10 @@ void branch_and_bound_t::best_first_thread(i_t task_id, } template -void branch_and_bound_t::diving_thread(const csr_matrix_t& Arow) +void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type, + const csr_matrix_t& Arow) { + constexpr i_t backtrack = 5; logger_t log; log.log = false; // Make a copy of the original LP. We will modify its bounds at each leaf @@ -1139,6 +1195,12 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A std::deque*> stack; stack.push_front(&subtree.root); + stats_t dive_stats; + dive_stats.total_lp_iters = 0; + dive_stats.total_lp_solve_time = 0; + dive_stats.nodes_explored = 0; + dive_stats.nodes_unexplored = 0; + while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -1159,10 +1221,11 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A basic_list, nonbasic_list, node_presolver, - thread_type_t::DIVING, + diving_type, recompute_bounds_and_basis, start_node->lower, start_node->upper, + dive_stats, log); recompute_bounds_and_basis = !has_children(status); @@ -1171,6 +1234,9 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; + } else if (status == node_solve_info_t::ITERATION_LIMIT) { + break; + } else if (has_children(status)) { if (status == node_solve_info_t::UP_CHILD_FIRST) { stack.push_front(node_ptr->get_down_child()); @@ -1181,24 +1247,8 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A } } - if (stack.size() > 1) { - // If the diving thread is consuming the nodes faster than the - // best first search, then we split the current subtree at the - // lowest possible point and move to the queue, so it can - // be picked by another thread. - if (std::lock_guard lock(mutex_dive_queue_); - diving_queue_.size() < min_diving_queue_size_) { - mip_node_t* new_node = stack.back(); - stack.pop_back(); - - std::vector lower = start_node->lower; - std::vector upper = start_node->upper; - std::fill( - node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); - new_node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); - - diving_queue_.emplace(new_node->detach_copy(), std::move(lower), std::move(upper)); - } + if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > backtrack) { + stack.pop_back(); } } } @@ -1421,7 +1471,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Choose variable to branch on - i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x, log); + auto [branch_var, obj_estimate] = + pc_.variable_selection_and_obj_estimate(fractional, root_relax_soln_.x, root_objective_, log); search_tree_.root = std::move(mip_node_t(root_objective_, root_vstatus_)); search_tree_.num_nodes = 0; @@ -1455,6 +1506,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lower_bound_ceiling_ = inf; should_report_ = true; + std::vector diving_strategies = { + bnb_thread_type_t::PSEUDOCOST_DIVING, + bnb_thread_type_t::LINE_SEARCH_DIVING, + bnb_thread_type_t::GUIDED_DIVING, + bnb_thread_type_t::COEFFICIENT_DIVING, + }; + #pragma omp parallel num_threads(settings_.num_threads) { #pragma omp master @@ -1479,9 +1537,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut best_first_thread(i, search_tree_, Arow); } - for (i_t i = 0; i < settings_.num_diving_threads; i++) { + for (i_t k = 0; k < settings_.num_diving_threads; k++) { + const i_t m = diving_strategies.size(); + const bnb_thread_type_t diving_type = diving_strategies[k % m]; #pragma omp task - diving_thread(Arow); + diving_thread(diving_type, Arow); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 7891711f7..9026336f3 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -57,9 +57,12 @@ enum class node_solve_info_t { // // [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, // Berlin, 2007. doi: 10.14279/depositonce-1634. -enum class thread_type_t { - EXPLORATION = 0, // Best-First + Plunging. Pseudocost branching + Martin's criteria. - DIVING = 1, +enum class bnb_thread_type_t { + EXPLORATION = 0, // Best-First + Plunging. + PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) + LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) + GUIDED_DIVING = 3, // Guided diving (9.2.3). If no incumbent is found yet, use pseudocost diving. + COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1) }; template @@ -207,7 +210,7 @@ class branch_and_bound_t { void add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - thread_type_t thread_type); + bnb_thread_type_t thread_type); // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); @@ -237,7 +240,7 @@ class branch_and_bound_t { // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(const csr_matrix_t& Arow); + void diving_thread(bnb_thread_type_t diving_type, const csr_matrix_t& Arow); // Solve the LP relaxation of a leaf node and update the tree. node_solve_info_t solve_node(mip_node_t* node_ptr, @@ -247,14 +250,19 @@ class branch_and_bound_t { std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, - thread_type_t thread_type, + bnb_thread_type_t thread_type, bool recompute_basis_and_bounds, const std::vector& root_lower, const std::vector& root_upper, + stats_t& stats, logger_t& log); - // Sort the children based on the Martin's criteria. - rounding_direction_t child_selection(mip_node_t* node_ptr); + // Selects the variable to branch on. + branch_variable_t variable_selection(mip_node_t* node_ptr, + const std::vector& fractional, + const std::vector& solution, + bnb_thread_type_t type, + logger_t& log); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp index f7035109e..0c13c2ed5 100644 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -26,7 +26,7 @@ struct diving_root_t { friend bool operator>(const diving_root_t& a, const diving_root_t& b) { - return a.node.lower_bound > b.node.lower_bound; + return a.node.objective_estimate > b.node.objective_estimate; } }; diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index ac5e394f9..f6030d521 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -30,17 +30,17 @@ class logger_t { { } - void enable_log_to_file(std::string mode = "w") + void enable_log_to_file(const char* mode = "w") { if (log_file != nullptr) { std::fclose(log_file); } - log_file = std::fopen(log_filename.c_str(), mode.c_str()); + log_file = std::fopen(log_filename.c_str(), mode); log_to_file = true; } - void set_log_file(const std::string& filename) + void set_log_file(const std::string& filename, const char* mode = "w") { log_filename = filename; - enable_log_to_file(); + enable_log_to_file(mode); } void close_log_file() diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 1d66a21f7..61f63f17a 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -59,6 +59,7 @@ class mip_node_t { node_id(0), branch_var(-1), branch_dir(rounding_direction_t::NONE), + objective_estimate(inf), vstatus(basis) { children[0] = nullptr; @@ -80,6 +81,7 @@ class mip_node_t { branch_var(branch_variable), branch_dir(branch_direction), fractional_val(branch_var_value), + objective_estimate(parent_node->objective_estimate), vstatus(basis) { @@ -227,17 +229,19 @@ class mip_node_t { mip_node_t detach_copy() const { mip_node_t copy(lower_bound, vstatus); - copy.branch_var = branch_var; - copy.branch_dir = branch_dir; - copy.branch_var_lower = branch_var_lower; - copy.branch_var_upper = branch_var_upper; - copy.fractional_val = fractional_val; - copy.node_id = node_id; + copy.branch_var = branch_var; + copy.branch_dir = branch_dir; + copy.branch_var_lower = branch_var_lower; + copy.branch_var_upper = branch_var_upper; + copy.fractional_val = fractional_val; + copy.objective_estimate = objective_estimate; + copy.node_id = node_id; return copy; } node_status_t status; f_t lower_bound; + f_t objective_estimate; i_t depth; i_t node_id; i_t branch_var; diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 9f84e108d..3fdbbbfbf 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -195,11 +195,276 @@ void strong_branching(const lp_problem_t& original_lp, pc.update_pseudo_costs_from_strong_branching(fractional, root_soln); } +template +rounding_direction_t martin_criteria(f_t val, f_t root_val) +{ + const f_t down_val = std::floor(root_val); + const f_t up_val = std::ceil(root_val); + const f_t down_dist = val - down_val; + const f_t up_dist = up_val - val; + constexpr f_t eps = 1e-6; + + if (down_dist < up_dist + eps) { + return rounding_direction_t::DOWN; + } else { + return rounding_direction_t::UP; + } +} + +template +branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log) +{ + constexpr f_t eps = 1e-6; + i_t branch_var = -1; + f_t min_score = INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + + for (auto j : fractional) { + f_t score = inf; + rounding_direction_t dir = rounding_direction_t::NONE; + + if (solution[j] < root_solution[j] - eps) { + f_t f = solution[j] - std::floor(solution[j]); + f_t d = root_solution[j] - solution[j]; + score = f / d; + dir = rounding_direction_t::DOWN; + + } else if (solution[j] > root_solution[j] + eps) { + f_t f = std::ceil(solution[j]) - solution[j]; + f_t d = solution[j] - root_solution[j]; + score = f / d; + dir = rounding_direction_t::UP; + } + + if (min_score > score) { + min_score = score; + branch_var = j; + round_dir = dir; + } + } + + log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_score); + + // If the current solution is equal to the root solution, arbitrarily + // set the branch variable to the first fractional variable and round it down + if (round_dir == rounding_direction_t::NONE) { + branch_var = fractional[0]; + round_dir = rounding_direction_t::DOWN; + } + + return {branch_var, round_dir}; +} + +template +branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log) +{ + std::lock_guard lock(pc.mutex); + i_t branch_var = -1; + f_t max_score = -INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + pc.initialized( + num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (auto j : fractional) { + rounding_direction_t dir = rounding_direction_t::NONE; + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + + f_t pc_down = pc.pseudo_cost_num_down[j] != 0 + ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] + : pseudo_cost_down_avg; + + f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] + : pseudo_cost_up_avg; + + f_t score_down = std::sqrt(f_up) * (1 + pc_up) / (1 + pc_down); + f_t score_up = std::sqrt(f_down) * (1 + pc_down) / (1 + pc_up); + f_t score = 0; + + if (solution[j] < root_solution[j] - 0.4) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else if (solution[j] > root_solution[j] + 0.4) { + score = score_up; + dir = rounding_direction_t::UP; + } else if (f_down < 0.3) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else if (f_down > 0.7) { + score = score_up; + dir = rounding_direction_t::UP; + } else if (pc_down < pc_up + eps) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else { + score = score_up; + dir = rounding_direction_t::UP; + } + + if (score > max_score) { + max_score = score; + branch_var = j; + round_dir = dir; + } + } + log.debug("Pseudocost diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + + return {branch_var, round_dir}; +} + +template +branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log) +{ + std::lock_guard lock(pc.mutex); + i_t branch_var = -1; + f_t max_score = -INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + pc.initialized( + num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (auto j : fractional) { + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j])); + f_t up_dist = std::abs(std::ceil(solution[j]) - incumbent[j]); + rounding_direction_t dir = + down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP; + + f_t pc_down = pc.pseudo_cost_num_down[j] != 0 + ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] + : pseudo_cost_down_avg; + + f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] + : pseudo_cost_up_avg; + + f_t score1 = dir == rounding_direction_t::DOWN ? 5 * pc_down * f_down : 5 * pc_up * f_up; + f_t score2 = dir == rounding_direction_t::DOWN ? pc_up * f_up : pc_down * f_down; + f_t score = (score1 + score2) / 6; + + if (score > max_score) { + max_score = score; + branch_var = j; + round_dir = dir; + } + } + + log.debug("Guided diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + return {branch_var, round_dir}; +} + +template +std::tuple calculate_variable_locks(const lp_problem_t& lp_problem, i_t var_idx) +{ + i_t up_lock = 0; + i_t down_lock = 0; + i_t start = lp_problem.A.col_start[var_idx]; + i_t end = lp_problem.A.col_start[var_idx + 1]; + + for (i_t k = start; k < end; ++k) { + f_t nz_val = lp_problem.A.x[k]; + i_t nz_row = lp_problem.A.i[k]; + + if (std::isfinite(lp_problem.upper[nz_row]) && std::isfinite(lp_problem.lower[nz_row])) { + down_lock += 1; + up_lock += 1; + continue; + } + + f_t sign = std::isfinite(lp_problem.upper[nz_row]) ? 1 : -1; + + if (nz_val * sign > 0) { + up_lock += 1; + } else { + down_lock += 1; + } + } + + return {up_lock, down_lock}; +} + +template +branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log) +{ + i_t branch_var = -1; + f_t min_locks = INT_MAX; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + for (auto j : fractional) { + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + auto [up_lock, down_lock] = calculate_variable_locks(lp_problem, j); + f_t locks = std::min(up_lock, down_lock); + + if (min_locks > locks) { + min_locks = locks; + branch_var = j; + + if (up_lock < down_lock) { + round_dir = rounding_direction_t::UP; + } else if (up_lock > down_lock) { + round_dir = rounding_direction_t::DOWN; + } else if (f_down < f_up + eps) { + round_dir = rounding_direction_t::DOWN; + } else { + round_dir = rounding_direction_t::UP; + } + } + } + + log.debug( + "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_locks); + + return {branch_var, round_dir}; +} + template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) { - mutex.lock(); + std::lock_guard lock(mutex); const f_t change_in_obj = leaf_objective - node_ptr->lower_bound; const f_t frac = node_ptr->branch_dir == rounding_direction_t::DOWN ? node_ptr->fractional_val - std::floor(node_ptr->fractional_val) @@ -211,7 +476,6 @@ void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_pt pseudo_cost_sum_up[node_ptr->branch_var] += change_in_obj / frac; pseudo_cost_num_up[node_ptr->branch_var]++; } - mutex.unlock(); } template @@ -254,16 +518,19 @@ void pseudo_costs_t::initialized(i_t& num_initialized_down, } template -i_t pseudo_costs_t::variable_selection(const std::vector& fractional, - const std::vector& solution, - logger_t& log) +std::pair pseudo_costs_t::variable_selection_and_obj_estimate( + const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log) { - mutex.lock(); + std::lock_guard lock(mutex); const i_t num_fractional = fractional.size(); std::vector pseudo_cost_up(num_fractional); std::vector pseudo_cost_down(num_fractional); std::vector score(num_fractional); + f_t estimate = lower_bound; i_t num_initialized_down; i_t num_initialized_up; @@ -296,6 +563,9 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio const f_t f_up = std::ceil(solution[j]) - solution[j]; score[k] = std::max(f_down * pseudo_cost_down[k], eps) * std::max(f_up * pseudo_cost_up[k], eps); + + estimate += std::min(std::max(pseudo_cost_down[k] * f_down, eps), + std::max(pseudo_cost_up[k] * f_up, eps)); } i_t branch_var = fractional[0]; @@ -312,9 +582,7 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio log.printf( "pc branching on %d. Value %e. Score %e\n", branch_var, solution[branch_var], score[select]); - mutex.unlock(); - - return branch_var; + return {branch_var, estimate}; } template @@ -356,6 +624,29 @@ template void strong_branching(const lp_problem_t& ori const std::vector& edge_norms, pseudo_costs_t& pc); +template rounding_direction_t martin_criteria(double val, double root_val); + +template branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log); + +template branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 799cdc3ff..f2c8abef1 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -17,6 +17,12 @@ namespace cuopt::linear_programming::dual_simplex { +template +struct branch_variable_t { + i_t variable; + rounding_direction_t direction; +}; + template class pseudo_costs_t { public: @@ -43,9 +49,10 @@ class pseudo_costs_t { f_t& pseudo_cost_down_avg, f_t& pseudo_cost_up_avg) const; - i_t variable_selection(const std::vector& fractional, - const std::vector& solution, - logger_t& log); + std::pair variable_selection_and_obj_estimate(const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log); void update_pseudo_costs_from_strong_branching(const std::vector& fractional, const std::vector& root_soln); @@ -72,4 +79,36 @@ void strong_branching(const lp_problem_t& original_lp, const std::vector& edge_norms, pseudo_costs_t& pc); +// Martin's criteria for the preferred rounding direction (see [1]) +// [1] A. Martin, “Integer Programs with Block Structure,” +// Technische Universit¨at Berlin, Berlin, 1999. Accessed: Aug. 08, 2025. +// [Online]. Available: https://opus4.kobv.de/opus4-zib/frontdoor/index/index/docId/391 +template +rounding_direction_t martin_criteria(f_t val, f_t root_val); + +template +branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template +branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template +branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log); + +template +branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); } // namespace cuopt::linear_programming::dual_simplex From 046a501e9da7408111d7be5e0fa77fd9ccaa54df Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 3 Dec 2025 18:58:00 +0100 Subject: [PATCH 02/70] moved diving heuristics to a separated file --- cpp/src/dual_simplex/CMakeLists.txt | 1 + cpp/src/dual_simplex/branch_and_bound.cpp | 7 +- cpp/src/dual_simplex/branch_and_bound.hpp | 31 ++- cpp/src/dual_simplex/diving_heuristics.cpp | 285 +++++++++++++++++++++ cpp/src/dual_simplex/diving_heuristics.hpp | 57 +++++ cpp/src/dual_simplex/pseudo_costs.cpp | 270 ------------------- cpp/src/dual_simplex/pseudo_costs.hpp | 31 --- 7 files changed, 365 insertions(+), 317 deletions(-) create mode 100644 cpp/src/dual_simplex/diving_heuristics.cpp create mode 100644 cpp/src/dual_simplex/diving_heuristics.hpp diff --git a/cpp/src/dual_simplex/CMakeLists.txt b/cpp/src/dual_simplex/CMakeLists.txt index e8a9b5dce..ebaf9cbb7 100644 --- a/cpp/src/dual_simplex/CMakeLists.txt +++ b/cpp/src/dual_simplex/CMakeLists.txt @@ -31,6 +31,7 @@ set(DUAL_SIMPLEX_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/triangle_solve.cpp ${CMAKE_CURRENT_SOURCE_DIR}/vector_math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pinned_host_allocator.cu + ${CMAKE_CURRENT_SOURCE_DIR}/diving_heuristics.cpp ) # Uncomment to enable debug info diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index fce774c86..3a7475c64 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -619,7 +619,7 @@ node_solve_info_t branch_and_bound_t::solve_node( bool recompute_bounds_and_basis, const std::vector& root_lower, const std::vector& root_upper, - stats_t& stats, + bnb_stats_t& stats, logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; @@ -642,7 +642,8 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_settings.scale_columns = false; if (thread_type != bnb_thread_type_t::EXPLORATION) { - f_t max_iter = 0.05 * exploration_stats_.total_lp_iters; + i_t bnb_lp_iters = exploration_stats_.total_lp_iters; + f_t max_iter = 0.05 * bnb_lp_iters; lp_settings.iteration_limit = max_iter - stats.total_lp_iters; if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } } @@ -1195,7 +1196,7 @@ void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type, std::deque*> stack; stack.push_front(&subtree.root); - stats_t dive_stats; + bnb_stats_t dive_stats; dive_stats.total_lp_iters = 0; dive_stats.total_lp_solve_time = 0; dive_stats.nodes_explored = 0; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 9026336f3..b0ed7e800 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -53,7 +54,8 @@ enum class node_solve_info_t { NUMERICAL = 5 // The solver encounter a numerical error when solving the node }; -// Indicate the search and variable selection algorithms used by the thread (See [1]). +// Indicate the search and variable selection algorithms used by each thread +// in B&B (See [1]). // // [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, // Berlin, 2007. doi: 10.14279/depositonce-1634. @@ -71,6 +73,19 @@ class bounds_strengthening_t; template void upper_bound_callback(f_t upper_bound); +template +struct bnb_stats_t { + f_t start_time = 0.0; + omp_atomic_t total_lp_solve_time = 0.0; + omp_atomic_t nodes_explored = 0; + omp_atomic_t nodes_unexplored = 0; + omp_atomic_t total_lp_iters = 0; + + // This should only be used by the main thread + omp_atomic_t last_log = 0.0; + omp_atomic_t nodes_since_last_log = 0; +}; + template class branch_and_bound_t { public: @@ -148,17 +163,7 @@ class branch_and_bound_t { mip_solution_t incumbent_; // Structure with the general info of the solver. - struct stats_t { - f_t start_time = 0.0; - omp_atomic_t total_lp_solve_time = 0.0; - omp_atomic_t nodes_explored = 0; - omp_atomic_t nodes_unexplored = 0; - omp_atomic_t total_lp_iters = 0; - - // This should only be used by the main thread - omp_atomic_t last_log = 0.0; - omp_atomic_t nodes_since_last_log = 0; - } exploration_stats_; + bnb_stats_t exploration_stats_; // Mutex for repair omp_mutex_t mutex_repair_; @@ -254,7 +259,7 @@ class branch_and_bound_t { bool recompute_basis_and_bounds, const std::vector& root_lower, const std::vector& root_upper, - stats_t& stats, + bnb_stats_t& stats, logger_t& log); // Selects the variable to branch on. diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp new file mode 100644 index 000000000..c59a0e850 --- /dev/null +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -0,0 +1,285 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log) +{ + constexpr f_t eps = 1e-6; + i_t branch_var = -1; + f_t min_score = INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + + for (auto j : fractional) { + f_t score = inf; + rounding_direction_t dir = rounding_direction_t::NONE; + + if (solution[j] < root_solution[j] - eps) { + f_t f = solution[j] - std::floor(solution[j]); + f_t d = root_solution[j] - solution[j]; + score = f / d; + dir = rounding_direction_t::DOWN; + + } else if (solution[j] > root_solution[j] + eps) { + f_t f = std::ceil(solution[j]) - solution[j]; + f_t d = solution[j] - root_solution[j]; + score = f / d; + dir = rounding_direction_t::UP; + } + + if (min_score > score) { + min_score = score; + branch_var = j; + round_dir = dir; + } + } + + log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_score); + + // If the current solution is equal to the root solution, arbitrarily + // set the branch variable to the first fractional variable and round it down + if (round_dir == rounding_direction_t::NONE) { + branch_var = fractional[0]; + round_dir = rounding_direction_t::DOWN; + } + + return {branch_var, round_dir}; +} + +template +branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log) +{ + std::lock_guard lock(pc.mutex); + i_t branch_var = -1; + f_t max_score = -INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + pc.initialized( + num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (auto j : fractional) { + rounding_direction_t dir = rounding_direction_t::NONE; + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + + f_t pc_down = pc.pseudo_cost_num_down[j] != 0 + ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] + : pseudo_cost_down_avg; + + f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] + : pseudo_cost_up_avg; + + f_t score_down = std::sqrt(f_up) * (1 + pc_up) / (1 + pc_down); + f_t score_up = std::sqrt(f_down) * (1 + pc_down) / (1 + pc_up); + f_t score = 0; + + if (solution[j] < root_solution[j] - 0.4) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else if (solution[j] > root_solution[j] + 0.4) { + score = score_up; + dir = rounding_direction_t::UP; + } else if (f_down < 0.3) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else if (f_down > 0.7) { + score = score_up; + dir = rounding_direction_t::UP; + } else if (pc_down < pc_up + eps) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else { + score = score_up; + dir = rounding_direction_t::UP; + } + + if (score > max_score) { + max_score = score; + branch_var = j; + round_dir = dir; + } + } + log.debug("Pseudocost diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + + return {branch_var, round_dir}; +} + +template +branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log) +{ + std::lock_guard lock(pc.mutex); + i_t branch_var = -1; + f_t max_score = -INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + pc.initialized( + num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (auto j : fractional) { + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j])); + f_t up_dist = std::abs(std::ceil(solution[j]) - incumbent[j]); + rounding_direction_t dir = + down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP; + + f_t pc_down = pc.pseudo_cost_num_down[j] != 0 + ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] + : pseudo_cost_down_avg; + + f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] + : pseudo_cost_up_avg; + + f_t score1 = dir == rounding_direction_t::DOWN ? 5 * pc_down * f_down : 5 * pc_up * f_up; + f_t score2 = dir == rounding_direction_t::DOWN ? pc_up * f_up : pc_down * f_down; + f_t score = (score1 + score2) / 6; + + if (score > max_score) { + max_score = score; + branch_var = j; + round_dir = dir; + } + } + + log.debug("Guided diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + return {branch_var, round_dir}; +} + +template +std::tuple calculate_variable_locks(const lp_problem_t& lp_problem, i_t var_idx) +{ + i_t up_lock = 0; + i_t down_lock = 0; + i_t start = lp_problem.A.col_start[var_idx]; + i_t end = lp_problem.A.col_start[var_idx + 1]; + + for (i_t k = start; k < end; ++k) { + f_t nz_val = lp_problem.A.x[k]; + i_t nz_row = lp_problem.A.i[k]; + + if (std::isfinite(lp_problem.upper[nz_row]) && std::isfinite(lp_problem.lower[nz_row])) { + down_lock += 1; + up_lock += 1; + continue; + } + + f_t sign = std::isfinite(lp_problem.upper[nz_row]) ? 1 : -1; + + if (nz_val * sign > 0) { + up_lock += 1; + } else { + down_lock += 1; + } + } + + return {up_lock, down_lock}; +} + +template +branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log) +{ + i_t branch_var = -1; + f_t min_locks = INT_MAX; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + for (auto j : fractional) { + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + auto [up_lock, down_lock] = calculate_variable_locks(lp_problem, j); + f_t locks = std::min(up_lock, down_lock); + + if (min_locks > locks) { + min_locks = locks; + branch_var = j; + + if (up_lock < down_lock) { + round_dir = rounding_direction_t::UP; + } else if (up_lock > down_lock) { + round_dir = rounding_direction_t::DOWN; + } else if (f_down < f_up + eps) { + round_dir = rounding_direction_t::DOWN; + } else { + round_dir = rounding_direction_t::UP; + } + } + } + + log.debug( + "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_locks); + + return {branch_var, round_dir}; +} + +#ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE +template branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log); + +template branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); +#endif + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/diving_heuristics.hpp b/cpp/src/dual_simplex/diving_heuristics.hpp new file mode 100644 index 000000000..5b259dd33 --- /dev/null +++ b/cpp/src/dual_simplex/diving_heuristics.hpp @@ -0,0 +1,57 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex { + +struct diving_general_settings_t { + int num_diving_threads; + bool disable_line_search_diving = false; + bool disable_pseudocost_diving = false; + bool disable_guided_diving = false; + bool disable_coefficient_diving = false; +}; + +template +struct branch_variable_t { + i_t variable; + rounding_direction_t direction; +}; + +template +branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template +branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template +branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log); + +template +branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 3fdbbbfbf..091639539 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -211,255 +211,6 @@ rounding_direction_t martin_criteria(f_t val, f_t root_val) } } -template -branch_variable_t line_search_diving(const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log) -{ - constexpr f_t eps = 1e-6; - i_t branch_var = -1; - f_t min_score = INFINITY; - rounding_direction_t round_dir = rounding_direction_t::NONE; - - for (auto j : fractional) { - f_t score = inf; - rounding_direction_t dir = rounding_direction_t::NONE; - - if (solution[j] < root_solution[j] - eps) { - f_t f = solution[j] - std::floor(solution[j]); - f_t d = root_solution[j] - solution[j]; - score = f / d; - dir = rounding_direction_t::DOWN; - - } else if (solution[j] > root_solution[j] + eps) { - f_t f = std::ceil(solution[j]) - solution[j]; - f_t d = solution[j] - root_solution[j]; - score = f / d; - dir = rounding_direction_t::UP; - } - - if (min_score > score) { - min_score = score; - branch_var = j; - round_dir = dir; - } - } - - log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", - branch_var, - solution[branch_var], - round_dir, - min_score); - - // If the current solution is equal to the root solution, arbitrarily - // set the branch variable to the first fractional variable and round it down - if (round_dir == rounding_direction_t::NONE) { - branch_var = fractional[0]; - round_dir = rounding_direction_t::DOWN; - } - - return {branch_var, round_dir}; -} - -template -branch_variable_t pseudocost_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log) -{ - std::lock_guard lock(pc.mutex); - i_t branch_var = -1; - f_t max_score = -INFINITY; - rounding_direction_t round_dir = rounding_direction_t::NONE; - constexpr f_t eps = 1e-6; - - i_t num_initialized_down; - i_t num_initialized_up; - f_t pseudo_cost_down_avg; - f_t pseudo_cost_up_avg; - pc.initialized( - num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - - for (auto j : fractional) { - rounding_direction_t dir = rounding_direction_t::NONE; - f_t f_down = solution[j] - std::floor(solution[j]); - f_t f_up = std::ceil(solution[j]) - solution[j]; - - f_t pc_down = pc.pseudo_cost_num_down[j] != 0 - ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] - : pseudo_cost_down_avg; - - f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] - : pseudo_cost_up_avg; - - f_t score_down = std::sqrt(f_up) * (1 + pc_up) / (1 + pc_down); - f_t score_up = std::sqrt(f_down) * (1 + pc_down) / (1 + pc_up); - f_t score = 0; - - if (solution[j] < root_solution[j] - 0.4) { - score = score_down; - dir = rounding_direction_t::DOWN; - } else if (solution[j] > root_solution[j] + 0.4) { - score = score_up; - dir = rounding_direction_t::UP; - } else if (f_down < 0.3) { - score = score_down; - dir = rounding_direction_t::DOWN; - } else if (f_down > 0.7) { - score = score_up; - dir = rounding_direction_t::UP; - } else if (pc_down < pc_up + eps) { - score = score_down; - dir = rounding_direction_t::DOWN; - } else { - score = score_up; - dir = rounding_direction_t::UP; - } - - if (score > max_score) { - max_score = score; - branch_var = j; - round_dir = dir; - } - } - log.debug("Pseudocost diving: selected var %d with val = %e, round dir = %d and score = %e\n", - branch_var, - solution[branch_var], - round_dir, - max_score); - - return {branch_var, round_dir}; -} - -template -branch_variable_t guided_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& incumbent, - logger_t& log) -{ - std::lock_guard lock(pc.mutex); - i_t branch_var = -1; - f_t max_score = -INFINITY; - rounding_direction_t round_dir = rounding_direction_t::NONE; - constexpr f_t eps = 1e-6; - - i_t num_initialized_down; - i_t num_initialized_up; - f_t pseudo_cost_down_avg; - f_t pseudo_cost_up_avg; - pc.initialized( - num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - - for (auto j : fractional) { - f_t f_down = solution[j] - std::floor(solution[j]); - f_t f_up = std::ceil(solution[j]) - solution[j]; - f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j])); - f_t up_dist = std::abs(std::ceil(solution[j]) - incumbent[j]); - rounding_direction_t dir = - down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP; - - f_t pc_down = pc.pseudo_cost_num_down[j] != 0 - ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] - : pseudo_cost_down_avg; - - f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] - : pseudo_cost_up_avg; - - f_t score1 = dir == rounding_direction_t::DOWN ? 5 * pc_down * f_down : 5 * pc_up * f_up; - f_t score2 = dir == rounding_direction_t::DOWN ? pc_up * f_up : pc_down * f_down; - f_t score = (score1 + score2) / 6; - - if (score > max_score) { - max_score = score; - branch_var = j; - round_dir = dir; - } - } - - log.debug("Guided diving: selected var %d with val = %e, round dir = %d and score = %e\n", - branch_var, - solution[branch_var], - round_dir, - max_score); - return {branch_var, round_dir}; -} - -template -std::tuple calculate_variable_locks(const lp_problem_t& lp_problem, i_t var_idx) -{ - i_t up_lock = 0; - i_t down_lock = 0; - i_t start = lp_problem.A.col_start[var_idx]; - i_t end = lp_problem.A.col_start[var_idx + 1]; - - for (i_t k = start; k < end; ++k) { - f_t nz_val = lp_problem.A.x[k]; - i_t nz_row = lp_problem.A.i[k]; - - if (std::isfinite(lp_problem.upper[nz_row]) && std::isfinite(lp_problem.lower[nz_row])) { - down_lock += 1; - up_lock += 1; - continue; - } - - f_t sign = std::isfinite(lp_problem.upper[nz_row]) ? 1 : -1; - - if (nz_val * sign > 0) { - up_lock += 1; - } else { - down_lock += 1; - } - } - - return {up_lock, down_lock}; -} - -template -branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, - const std::vector& fractional, - const std::vector& solution, - logger_t& log) -{ - i_t branch_var = -1; - f_t min_locks = INT_MAX; - rounding_direction_t round_dir = rounding_direction_t::NONE; - constexpr f_t eps = 1e-6; - - for (auto j : fractional) { - f_t f_down = solution[j] - std::floor(solution[j]); - f_t f_up = std::ceil(solution[j]) - solution[j]; - auto [up_lock, down_lock] = calculate_variable_locks(lp_problem, j); - f_t locks = std::min(up_lock, down_lock); - - if (min_locks > locks) { - min_locks = locks; - branch_var = j; - - if (up_lock < down_lock) { - round_dir = rounding_direction_t::UP; - } else if (up_lock > down_lock) { - round_dir = rounding_direction_t::DOWN; - } else if (f_down < f_up + eps) { - round_dir = rounding_direction_t::DOWN; - } else { - round_dir = rounding_direction_t::UP; - } - } - } - - log.debug( - "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %e\n", - branch_var, - solution[branch_var], - round_dir, - min_locks); - - return {branch_var, round_dir}; -} - template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) @@ -626,27 +377,6 @@ template void strong_branching(const lp_problem_t& ori template rounding_direction_t martin_criteria(double val, double root_val); -template branch_variable_t line_search_diving(const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template branch_variable_t pseudocost_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template branch_variable_t guided_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& incumbent, - logger_t& log); - -template branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, - const std::vector& fractional, - const std::vector& solution, - logger_t& log); #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index f2c8abef1..e1df3ad8e 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -17,12 +17,6 @@ namespace cuopt::linear_programming::dual_simplex { -template -struct branch_variable_t { - i_t variable; - rounding_direction_t direction; -}; - template class pseudo_costs_t { public: @@ -86,29 +80,4 @@ void strong_branching(const lp_problem_t& original_lp, template rounding_direction_t martin_criteria(f_t val, f_t root_val); -template -branch_variable_t line_search_diving(const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template -branch_variable_t pseudocost_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template -branch_variable_t guided_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& incumbent, - logger_t& log); - -template -branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, - const std::vector& fractional, - const std::vector& solution, - logger_t& log); } // namespace cuopt::linear_programming::dual_simplex From 6ea6d72a3e22db2833d2041ada3489063094dea6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 3 Dec 2025 19:09:12 +0100 Subject: [PATCH 03/70] organized code. added toggle to disable each type of diving. --- cpp/src/dual_simplex/branch_and_bound.cpp | 98 ++++++++++++------- cpp/src/dual_simplex/diving_heuristics.hpp | 8 -- cpp/src/dual_simplex/mip_node.hpp | 1 + cpp/src/dual_simplex/pseudo_costs.cpp | 18 ---- cpp/src/dual_simplex/pseudo_costs.hpp | 7 -- .../dual_simplex/simplex_solver_settings.hpp | 10 ++ 6 files changed, 76 insertions(+), 66 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 3a7475c64..8a6ba9545 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -564,6 +564,27 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, mutex_upper_.unlock(); } +// Martin's criteria for the preferred rounding direction (see [1]) +// [1] A. Martin, “Integer Programs with Block Structure,” +// Technische Universit¨at Berlin, Berlin, 1999. Accessed: Aug. 08, 2025. +// [Online]. Available: https://opus4.kobv.de/opus4-zib/frontdoor/index/index/docId/391 +template +rounding_direction_t martin_criteria(f_t val, f_t root_val) +{ + const f_t down_val = std::floor(root_val); + const f_t up_val = std::ceil(root_val); + const f_t down_dist = val - down_val; + const f_t up_dist = up_val - val; + constexpr f_t eps = 1e-6; + + if (down_dist < up_dist + eps) { + return rounding_direction_t::DOWN; + + } else { + return rounding_direction_t::UP; + } +} + template branch_variable_t branch_and_bound_t::variable_selection( mip_node_t* node_ptr, @@ -648,6 +669,28 @@ node_solve_info_t branch_and_bound_t::solve_node( if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } } +#ifdef LOG_NODE_SIMPLEX + lp_settings.set_log(true); + std::stringstream ss; + ss << "simplex-" << std::this_thread::get_id() << ".log"; + std::string logname; + ss >> logname; + lp_settings.log.set_log_file(logname, "a"); + lp_settings.log.log_to_console = false; + lp_settings.log.printf( + "%scurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " + "%f, variable lower bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", + settings_.log.log_prefix.c_str(), + node_ptr->node_id, + node_ptr->depth, + node_ptr->branch_var, + node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP", + node_ptr->fractional_val, + node_ptr->branch_var_lower, + node_ptr->branch_var_upper, + node_ptr->vstatus[node_ptr->branch_var]); +#endif + // Reset the bound_changed markers std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); @@ -673,29 +716,6 @@ node_solve_info_t branch_and_bound_t::solve_node( f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; -#ifdef LOG_NODE_SIMPLEX - lp_settings.set_log(true); - std::stringstream ss; - ss << "simplex-" << std::this_thread::get_id() << ".log"; - std::string logname; - ss >> logname; - lp_settings.log.set_log_file(logname, "a"); - lp_settings.log.log_to_console = false; - lp_settings.log.printf( - "%s\ncurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " - "%f, variable lower " - "bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", - settings_.log.log_prefix.c_str(), - node_ptr->node_id, - node_ptr->depth, - node_ptr->branch_var, - node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP", - node_ptr->fractional_val, - node_ptr->branch_var_lower, - node_ptr->branch_var_upper, - node_ptr->vstatus[node_ptr->branch_var]); -#endif - lp_status = dual_phase2_with_advanced_basis(2, 0, recompute_bounds_and_basis, @@ -725,14 +745,14 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_status = convert_lp_status_to_dual_status(second_status); } -#ifdef LOG_NODE_SIMPLEX - lp_settings.log.printf("\nLP status: %d\n\n", lp_status); -#endif - stats.total_lp_solve_time += toc(lp_start_time); stats.total_lp_iters += node_iter; } +#ifdef LOG_NODE_SIMPLEX + lp_settings.log.printf("\nLP status: %d\n\n", lp_status); +#endif + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { // Node was infeasible. Do not branch node_ptr->lower_bound = inf; @@ -1507,12 +1527,24 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lower_bound_ceiling_ = inf; should_report_ = true; - std::vector diving_strategies = { - bnb_thread_type_t::PSEUDOCOST_DIVING, - bnb_thread_type_t::LINE_SEARCH_DIVING, - bnb_thread_type_t::GUIDED_DIVING, - bnb_thread_type_t::COEFFICIENT_DIVING, - }; + std::vector diving_strategies; + diving_strategies.reserve(4); + + if (!settings_.disable_pseudocost_diving) { + diving_strategies.push_back(bnb_thread_type_t::PSEUDOCOST_DIVING); + } + + if (!settings_.disable_line_search_diving) { + diving_strategies.push_back(bnb_thread_type_t::LINE_SEARCH_DIVING); + } + + if (!settings_.disable_guided_diving) { + diving_strategies.push_back(bnb_thread_type_t::GUIDED_DIVING); + } + + if (!settings_.disable_coefficient_diving) { + diving_strategies.push_back(bnb_thread_type_t::COEFFICIENT_DIVING); + } #pragma omp parallel num_threads(settings_.num_threads) { diff --git a/cpp/src/dual_simplex/diving_heuristics.hpp b/cpp/src/dual_simplex/diving_heuristics.hpp index 5b259dd33..c7b1e2050 100644 --- a/cpp/src/dual_simplex/diving_heuristics.hpp +++ b/cpp/src/dual_simplex/diving_heuristics.hpp @@ -14,14 +14,6 @@ namespace cuopt::linear_programming::dual_simplex { -struct diving_general_settings_t { - int num_diving_threads; - bool disable_line_search_diving = false; - bool disable_pseudocost_diving = false; - bool disable_guided_diving = false; - bool disable_coefficient_diving = false; -}; - template struct branch_variable_t { i_t variable; diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 61f63f17a..e2e9c6868 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -45,6 +45,7 @@ class mip_node_t { branch_var_lower(-std::numeric_limits::infinity()), branch_var_upper(std::numeric_limits::infinity()), fractional_val(std::numeric_limits::infinity()), + objective_estimate(std::numeric_limits::infinity()), vstatus(0) { children[0] = nullptr; diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 091639539..a2defd3b3 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -195,22 +195,6 @@ void strong_branching(const lp_problem_t& original_lp, pc.update_pseudo_costs_from_strong_branching(fractional, root_soln); } -template -rounding_direction_t martin_criteria(f_t val, f_t root_val) -{ - const f_t down_val = std::floor(root_val); - const f_t up_val = std::ceil(root_val); - const f_t down_dist = val - down_val; - const f_t up_dist = up_val - val; - constexpr f_t eps = 1e-6; - - if (down_dist < up_dist + eps) { - return rounding_direction_t::DOWN; - } else { - return rounding_direction_t::UP; - } -} - template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) @@ -375,8 +359,6 @@ template void strong_branching(const lp_problem_t& ori const std::vector& edge_norms, pseudo_costs_t& pc); -template rounding_direction_t martin_criteria(double val, double root_val); - #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index e1df3ad8e..ab01b2a85 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -73,11 +73,4 @@ void strong_branching(const lp_problem_t& original_lp, const std::vector& edge_norms, pseudo_costs_t& pc); -// Martin's criteria for the preferred rounding direction (see [1]) -// [1] A. Martin, “Integer Programs with Block Structure,” -// Technische Universit¨at Berlin, Berlin, 1999. Accessed: Aug. 08, 2025. -// [Online]. Available: https://opus4.kobv.de/opus4-zib/frontdoor/index/index/docId/391 -template -rounding_direction_t martin_criteria(f_t val, f_t root_val); - } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 98be9d4cb..47e4ca49b 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -72,6 +72,10 @@ struct simplex_solver_settings_t { num_threads(omp_get_max_threads() - 1), num_bfs_threads(std::min(num_threads / 4, 1)), num_diving_threads(std::min(num_threads - num_bfs_threads, 1)), + disable_line_search_diving(false), + disable_pseudocost_diving(false), + disable_guided_diving(false), + disable_coefficient_diving(false), random_seed(0), inside_mip(0), solution_callback(nullptr), @@ -139,6 +143,12 @@ struct simplex_solver_settings_t { i_t random_seed; // random seed i_t num_bfs_threads; // number of threads dedicated to the best-first search i_t num_diving_threads; // number of threads dedicated to diving + + bool disable_line_search_diving; // true to disable line search diving + bool disable_pseudocost_diving; // true to disable pseudocost diving + bool disable_guided_diving; // true to disable guided diving + bool disable_coefficient_diving; // true to disable coefficient diving + i_t inside_mip; // 0 if outside MIP, 1 if inside MIP at root node, 2 if inside MIP at leaf node std::function&, f_t)> solution_callback; std::function&, f_t)> node_processed_callback; From 5422b97864fc483203cae58f25d85d831467b130 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 4 Dec 2025 11:16:40 +0100 Subject: [PATCH 04/70] restrict calling RINS to the best-first threads --- cpp/src/dual_simplex/branch_and_bound.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 8a6ba9545..d583ed214 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -779,10 +779,12 @@ node_solve_info_t branch_and_bound_t::solve_node( search_tree.graphviz_node(log, node_ptr, "lower bound", leaf_objective); pc_.update_pseudo_costs(node_ptr, leaf_objective); - if (settings_.node_processed_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem_, original_lp_, leaf_solution.x, original_x); - settings_.node_processed_callback(original_x, leaf_objective); + if (thread_type == bnb_thread_type_t::EXPLORATION) { + if (settings_.node_processed_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem_, original_lp_, leaf_solution.x, original_x); + settings_.node_processed_callback(original_x, leaf_objective); + } } if (leaf_num_fractional == 0) { From 73c1a63086231fc72d200379ec4d1ddc9822e752 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 4 Dec 2025 16:27:02 +0100 Subject: [PATCH 05/70] fix invalid branch var in line search diving --- cpp/src/dual_simplex/diving_heuristics.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index c59a0e850..f6096f40f 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -44,12 +44,6 @@ branch_variable_t line_search_diving(const std::vector& fractional, } } - log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", - branch_var, - solution[branch_var], - round_dir, - min_score); - // If the current solution is equal to the root solution, arbitrarily // set the branch variable to the first fractional variable and round it down if (round_dir == rounding_direction_t::NONE) { @@ -57,6 +51,12 @@ branch_variable_t line_search_diving(const std::vector& fractional, round_dir = rounding_direction_t::DOWN; } + log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_score); + return {branch_var, round_dir}; } @@ -182,6 +182,7 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, solution[branch_var], round_dir, max_score); + return {branch_var, round_dir}; } From 3a77ccaaf439e9eb1d654408d000995cb22de4fc Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 9 Dec 2025 13:45:18 +0100 Subject: [PATCH 06/70] moved asserts --- cpp/src/dual_simplex/branch_and_bound.cpp | 3 --- cpp/src/dual_simplex/branch_and_bound.hpp | 1 - cpp/src/dual_simplex/diving_heuristics.cpp | 13 +++++++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d583ed214..6167033be 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -799,8 +799,6 @@ node_solve_info_t branch_and_bound_t::solve_node( auto [branch_var, round_dir] = variable_selection( node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); - assert(round_dir != rounding_direction_t::NONE); - assert(branch_var >= 0); assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( @@ -1524,7 +1522,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); active_subtrees_ = 0; - min_diving_queue_size_ = 4 * settings_.num_diving_threads; solver_status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; should_report_ = true; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index b0ed7e800..c774315d8 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -196,7 +196,6 @@ class branch_and_bound_t { // Queue for storing the promising node for performing dives. omp_mutex_t mutex_dive_queue_; diving_queue_t diving_queue_; - i_t min_diving_queue_size_; // Global status of the solver. omp_atomic_t solver_status_; diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index f6096f40f..09f15a70f 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -51,6 +51,9 @@ branch_variable_t line_search_diving(const std::vector& fractional, round_dir = rounding_direction_t::DOWN; } + assert(round_dir != rounding_direction_t::NONE); + assert(branch_var >= 0); + log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", branch_var, solution[branch_var], @@ -122,6 +125,10 @@ branch_variable_t pseudocost_diving(pseudo_costs_t& pc, round_dir = dir; } } + + assert(round_dir != rounding_direction_t::NONE); + assert(branch_var >= 0); + log.debug("Pseudocost diving: selected var %d with val = %e, round dir = %d and score = %e\n", branch_var, solution[branch_var], @@ -177,6 +184,9 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, } } + assert(round_dir != rounding_direction_t::NONE); + assert(branch_var >= 0); + log.debug("Guided diving: selected var %d with val = %e, round dir = %d and score = %e\n", branch_var, solution[branch_var], @@ -249,6 +259,9 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl } } + assert(round_dir != rounding_direction_t::NONE); + assert(branch_var >= 0); + log.debug( "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %e\n", branch_var, From 0f7af4e2cebdd67df125b4592ed76e3613d4a3e2 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 9 Dec 2025 15:06:37 +0100 Subject: [PATCH 07/70] replace inf and max with STL calls --- cpp/src/dual_simplex/diving_heuristics.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 09f15a70f..9709123f9 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -17,7 +17,7 @@ branch_variable_t line_search_diving(const std::vector& fractional, { constexpr f_t eps = 1e-6; i_t branch_var = -1; - f_t min_score = INFINITY; + f_t min_score = std::numeric_limits::max(); rounding_direction_t round_dir = rounding_direction_t::NONE; for (auto j : fractional) { @@ -72,7 +72,7 @@ branch_variable_t pseudocost_diving(pseudo_costs_t& pc, { std::lock_guard lock(pc.mutex); i_t branch_var = -1; - f_t max_score = -INFINITY; + f_t max_score = std::numeric_limits::lowest(); rounding_direction_t round_dir = rounding_direction_t::NONE; constexpr f_t eps = 1e-6; @@ -147,7 +147,7 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, { std::lock_guard lock(pc.mutex); i_t branch_var = -1; - f_t max_score = -INFINITY; + f_t max_score = std::numeric_limits::lowest(); rounding_direction_t round_dir = rounding_direction_t::NONE; constexpr f_t eps = 1e-6; @@ -233,7 +233,7 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl logger_t& log) { i_t branch_var = -1; - f_t min_locks = INT_MAX; + i_t min_locks = std::numeric_limits::max(); rounding_direction_t round_dir = rounding_direction_t::NONE; constexpr f_t eps = 1e-6; From 79368c3be743fd943137e0e214de5bdcda79211b Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 11 Dec 2025 18:52:50 -0800 Subject: [PATCH 08/70] Fix incorrect infeasible list --- cpp/src/dual_simplex/basis_solves.cpp | 14 +++++++- cpp/src/dual_simplex/basis_solves.hpp | 2 ++ cpp/src/dual_simplex/basis_updates.cpp | 4 ++- cpp/src/dual_simplex/basis_updates.hpp | 2 ++ cpp/src/dual_simplex/crossover.cpp | 6 ++-- cpp/src/dual_simplex/phase2.cpp | 45 ++++++++++++++------------ cpp/src/dual_simplex/primal.cpp | 2 +- 7 files changed, 49 insertions(+), 26 deletions(-) diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index db24f55a2..3080f269d 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -613,6 +613,8 @@ i_t factorize_basis(const csc_matrix_t& A, template i_t basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, @@ -658,7 +660,15 @@ i_t basis_repair(const csc_matrix_t& A, nonbasic_list[nonbasic_map[replace_j]] = bad_j; vstatus[replace_j] = variable_status_t::BASIC; // This is the main issue. What value should bad_j take on. - vstatus[bad_j] = variable_status_t::NONBASIC_FREE; + if (lower[bad_j] == -inf && upper[bad_j] == inf) { + vstatus[bad_j] = variable_status_t::NONBASIC_FREE; + } else if (lower[bad_j] > -inf) { + vstatus[bad_j] = variable_status_t::NONBASIC_LOWER; + } else if (upper[bad_j] < inf) { + vstatus[bad_j] = variable_status_t::NONBASIC_UPPER; + } else { + assert(1 == 0); + } } return 0; @@ -849,6 +859,8 @@ template int factorize_basis(const csc_matrix_t& A, template int basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, diff --git a/cpp/src/dual_simplex/basis_solves.hpp b/cpp/src/dual_simplex/basis_solves.hpp index b668c0f46..0745806a6 100644 --- a/cpp/src/dual_simplex/basis_solves.hpp +++ b/cpp/src/dual_simplex/basis_solves.hpp @@ -42,6 +42,8 @@ i_t factorize_basis(const csc_matrix_t& A, template i_t basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 6b79f3c86..11056a65e 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2046,6 +2046,8 @@ template int basis_update_mpf_t::refactor_basis( const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus) @@ -2066,7 +2068,7 @@ int basis_update_mpf_t::refactor_basis( deficient, slacks_needed) == -1) { settings.log.debug("Initial factorization failed\n"); - basis_repair(A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(A, settings, lower, upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); #ifdef CHECK_BASIS_REPAIR const i_t m = A.m; diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index cea907074..9b5d3e614 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -373,6 +373,8 @@ class basis_update_mpf_t { // Compute L*U = A(p, basic_list) int refactor_basis(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus); diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 23d9a0e8e..3dd61b152 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -786,7 +786,7 @@ i_t primal_push(const lp_problem_t& lp, if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair( - lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1132,7 +1132,7 @@ crossover_status_t crossover(const lp_problem_t& lp, rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1323,7 +1323,7 @@ crossover_status_t crossover(const lp_problem_t& lp, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 56298ef4d..e0ac7239e 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -623,14 +623,17 @@ f_t compute_initial_primal_infeasibilities(const lp_problem_t& lp, const std::vector& basic_list, const std::vector& x, std::vector& squared_infeasibilities, - std::vector& infeasibility_indices) + std::vector& infeasibility_indices, + f_t& primal_inf) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; - squared_infeasibilities.resize(n, 0.0); + squared_infeasibilities.resize(n); + std::fill(squared_infeasibilities.begin(), squared_infeasibilities.end(), 0.0); infeasibility_indices.reserve(n); infeasibility_indices.clear(); - f_t primal_inf = 0.0; + f_t primal_inf_squared = 0.0; + primal_inf = 0.0; for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; const f_t lower_infeas = lp.lower[j] - x[j]; @@ -640,10 +643,11 @@ f_t compute_initial_primal_infeasibilities(const lp_problem_t& lp, const f_t square_infeas = infeas * infeas; squared_infeasibilities[j] = square_infeas; infeasibility_indices.push_back(j); - primal_inf += square_infeas; + primal_inf_squared += square_infeas; + primal_inf += infeas; } } - return primal_inf; + return primal_inf_squared; } template @@ -2241,7 +2245,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); - if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { return dual::status_t::NUMERICAL; } @@ -2268,7 +2272,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #ifdef COMPUTE_DUAL_RESIDUAL std::vector dual_res1; - compute_dual_residual(lp.A, objective, y, z, dual_res1); + phase2::compute_dual_residual(lp.A, objective, y, z, dual_res1); f_t dual_res_norm = vector_norm_inf(dual_res1); if (dual_res_norm > settings.tight_tol) { settings.log.printf("|| A'*y + z - c || %e\n", dual_res_norm); @@ -2357,8 +2361,9 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector bounded_variables(n, 0); phase2::compute_bounded_info(lp.lower, lp.upper, bounded_variables); - f_t primal_infeasibility = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + f_t primal_infeasibility; + f_t primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 0); @@ -2557,8 +2562,8 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); x = unperturbed_x; - primal_infeasibility = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); settings.log.printf("Updated primal infeasibility: %e\n", primal_infeasibility); objective = lp.objective; @@ -2594,8 +2599,8 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); x = unperturbed_x; - primal_infeasibility = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); const f_t orig_dual_infeas = phase2::dual_infeasibility( lp, settings, vstatus, z, settings.tight_tol, settings.dual_tol); @@ -2810,7 +2815,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, delta_xB_0_sparse.i, squared_infeasibilities, infeasibility_indices, - primal_infeasibility); + primal_infeasibility_squared); // Update primal infeasibilities due to changes in basic variables // from the leaving and entering variables phase2::update_primal_infeasibilities(lp, @@ -2822,7 +2827,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, scaled_delta_xB_sparse.i, squared_infeasibilities, infeasibility_indices, - primal_infeasibility); + primal_infeasibility_squared); // Update the entering variable phase2::update_single_primal_infeasibility(lp.lower, lp.upper, @@ -2883,14 +2888,14 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #endif if (should_refactor) { bool should_recompute_x = false; - if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } i_t count = 0; i_t deficient_size; while ((deficient_size = - ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus)) > 0) { + ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus)) > 0) { settings.log.printf("Failed to repair basis. Iteration %d. %d deficient columns.\n", iter, static_cast(deficient_size)); @@ -2912,8 +2917,8 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); x = unperturbed_x; } - phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); } #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 7); @@ -2951,7 +2956,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, iter, compute_user_objective(lp, obj), infeasibility_indices.size(), - primal_infeasibility, + primal_infeasibility_squared, sum_perturb, now); } diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 80406dcf0..445177fac 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -298,7 +298,7 @@ primal::status_t primal_phase2(i_t phase, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); From 6334ad71e8e3d5b03a7cfef156ff473e92a32642 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 3 Dec 2025 16:27:16 +0100 Subject: [PATCH 09/70] implemented diving heuristics. sorted starting nodes for diving based on the objective pseudcost estimate. --- cpp/src/dual_simplex/branch_and_bound.cpp | 25 ++ cpp/src/dual_simplex/pseudo_costs.cpp | 288 ++++++++++++++++++++++ cpp/src/dual_simplex/pseudo_costs.hpp | 38 +++ 3 files changed, 351 insertions(+) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 6167033be..e2af8550f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -716,6 +716,29 @@ node_solve_info_t branch_and_bound_t::solve_node( f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; +#ifdef LOG_NODE_SIMPLEX + lp_settings.set_log(true); + std::stringstream ss; + ss << "simplex-" << std::this_thread::get_id() << ".log"; + std::string logname; + ss >> logname; + lp_settings.log.set_log_file(logname, "a"); + lp_settings.log.log_to_console = false; + lp_settings.log.printf( + "%s\ncurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " + "%f, variable lower " + "bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", + settings_.log.log_prefix.c_str(), + node_ptr->node_id, + node_ptr->depth, + node_ptr->branch_var, + node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP", + node_ptr->fractional_val, + node_ptr->branch_var_lower, + node_ptr->branch_var_upper, + node_ptr->vstatus[node_ptr->branch_var]); +#endif + lp_status = dual_phase2_with_advanced_basis(2, 0, recompute_bounds_and_basis, @@ -799,6 +822,8 @@ node_solve_info_t branch_and_bound_t::solve_node( auto [branch_var, round_dir] = variable_selection( node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); + assert(round_dir != rounding_direction_t::NONE); + assert(branch_var >= 0); assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index a2defd3b3..3fdbbbfbf 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -195,6 +195,271 @@ void strong_branching(const lp_problem_t& original_lp, pc.update_pseudo_costs_from_strong_branching(fractional, root_soln); } +template +rounding_direction_t martin_criteria(f_t val, f_t root_val) +{ + const f_t down_val = std::floor(root_val); + const f_t up_val = std::ceil(root_val); + const f_t down_dist = val - down_val; + const f_t up_dist = up_val - val; + constexpr f_t eps = 1e-6; + + if (down_dist < up_dist + eps) { + return rounding_direction_t::DOWN; + } else { + return rounding_direction_t::UP; + } +} + +template +branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log) +{ + constexpr f_t eps = 1e-6; + i_t branch_var = -1; + f_t min_score = INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + + for (auto j : fractional) { + f_t score = inf; + rounding_direction_t dir = rounding_direction_t::NONE; + + if (solution[j] < root_solution[j] - eps) { + f_t f = solution[j] - std::floor(solution[j]); + f_t d = root_solution[j] - solution[j]; + score = f / d; + dir = rounding_direction_t::DOWN; + + } else if (solution[j] > root_solution[j] + eps) { + f_t f = std::ceil(solution[j]) - solution[j]; + f_t d = solution[j] - root_solution[j]; + score = f / d; + dir = rounding_direction_t::UP; + } + + if (min_score > score) { + min_score = score; + branch_var = j; + round_dir = dir; + } + } + + log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_score); + + // If the current solution is equal to the root solution, arbitrarily + // set the branch variable to the first fractional variable and round it down + if (round_dir == rounding_direction_t::NONE) { + branch_var = fractional[0]; + round_dir = rounding_direction_t::DOWN; + } + + return {branch_var, round_dir}; +} + +template +branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log) +{ + std::lock_guard lock(pc.mutex); + i_t branch_var = -1; + f_t max_score = -INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + pc.initialized( + num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (auto j : fractional) { + rounding_direction_t dir = rounding_direction_t::NONE; + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + + f_t pc_down = pc.pseudo_cost_num_down[j] != 0 + ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] + : pseudo_cost_down_avg; + + f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] + : pseudo_cost_up_avg; + + f_t score_down = std::sqrt(f_up) * (1 + pc_up) / (1 + pc_down); + f_t score_up = std::sqrt(f_down) * (1 + pc_down) / (1 + pc_up); + f_t score = 0; + + if (solution[j] < root_solution[j] - 0.4) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else if (solution[j] > root_solution[j] + 0.4) { + score = score_up; + dir = rounding_direction_t::UP; + } else if (f_down < 0.3) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else if (f_down > 0.7) { + score = score_up; + dir = rounding_direction_t::UP; + } else if (pc_down < pc_up + eps) { + score = score_down; + dir = rounding_direction_t::DOWN; + } else { + score = score_up; + dir = rounding_direction_t::UP; + } + + if (score > max_score) { + max_score = score; + branch_var = j; + round_dir = dir; + } + } + log.debug("Pseudocost diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + + return {branch_var, round_dir}; +} + +template +branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log) +{ + std::lock_guard lock(pc.mutex); + i_t branch_var = -1; + f_t max_score = -INFINITY; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + pc.initialized( + num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (auto j : fractional) { + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j])); + f_t up_dist = std::abs(std::ceil(solution[j]) - incumbent[j]); + rounding_direction_t dir = + down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP; + + f_t pc_down = pc.pseudo_cost_num_down[j] != 0 + ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] + : pseudo_cost_down_avg; + + f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] + : pseudo_cost_up_avg; + + f_t score1 = dir == rounding_direction_t::DOWN ? 5 * pc_down * f_down : 5 * pc_up * f_up; + f_t score2 = dir == rounding_direction_t::DOWN ? pc_up * f_up : pc_down * f_down; + f_t score = (score1 + score2) / 6; + + if (score > max_score) { + max_score = score; + branch_var = j; + round_dir = dir; + } + } + + log.debug("Guided diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + return {branch_var, round_dir}; +} + +template +std::tuple calculate_variable_locks(const lp_problem_t& lp_problem, i_t var_idx) +{ + i_t up_lock = 0; + i_t down_lock = 0; + i_t start = lp_problem.A.col_start[var_idx]; + i_t end = lp_problem.A.col_start[var_idx + 1]; + + for (i_t k = start; k < end; ++k) { + f_t nz_val = lp_problem.A.x[k]; + i_t nz_row = lp_problem.A.i[k]; + + if (std::isfinite(lp_problem.upper[nz_row]) && std::isfinite(lp_problem.lower[nz_row])) { + down_lock += 1; + up_lock += 1; + continue; + } + + f_t sign = std::isfinite(lp_problem.upper[nz_row]) ? 1 : -1; + + if (nz_val * sign > 0) { + up_lock += 1; + } else { + down_lock += 1; + } + } + + return {up_lock, down_lock}; +} + +template +branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log) +{ + i_t branch_var = -1; + f_t min_locks = INT_MAX; + rounding_direction_t round_dir = rounding_direction_t::NONE; + constexpr f_t eps = 1e-6; + + for (auto j : fractional) { + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + auto [up_lock, down_lock] = calculate_variable_locks(lp_problem, j); + f_t locks = std::min(up_lock, down_lock); + + if (min_locks > locks) { + min_locks = locks; + branch_var = j; + + if (up_lock < down_lock) { + round_dir = rounding_direction_t::UP; + } else if (up_lock > down_lock) { + round_dir = rounding_direction_t::DOWN; + } else if (f_down < f_up + eps) { + round_dir = rounding_direction_t::DOWN; + } else { + round_dir = rounding_direction_t::UP; + } + } + } + + log.debug( + "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_locks); + + return {branch_var, round_dir}; +} + template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) @@ -359,6 +624,29 @@ template void strong_branching(const lp_problem_t& ori const std::vector& edge_norms, pseudo_costs_t& pc); +template rounding_direction_t martin_criteria(double val, double root_val); + +template branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log); + +template branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index ab01b2a85..f2c8abef1 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -17,6 +17,12 @@ namespace cuopt::linear_programming::dual_simplex { +template +struct branch_variable_t { + i_t variable; + rounding_direction_t direction; +}; + template class pseudo_costs_t { public: @@ -73,4 +79,36 @@ void strong_branching(const lp_problem_t& original_lp, const std::vector& edge_norms, pseudo_costs_t& pc); +// Martin's criteria for the preferred rounding direction (see [1]) +// [1] A. Martin, “Integer Programs with Block Structure,” +// Technische Universit¨at Berlin, Berlin, 1999. Accessed: Aug. 08, 2025. +// [Online]. Available: https://opus4.kobv.de/opus4-zib/frontdoor/index/index/docId/391 +template +rounding_direction_t martin_criteria(f_t val, f_t root_val); + +template +branch_variable_t line_search_diving(const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template +branch_variable_t pseudocost_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& root_solution, + logger_t& log); + +template +branch_variable_t guided_diving(pseudo_costs_t& pc, + const std::vector& fractional, + const std::vector& solution, + const std::vector& incumbent, + logger_t& log); + +template +branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); } // namespace cuopt::linear_programming::dual_simplex From 2c94a7c389844231d0fde933b93650d0840adec3 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 3 Dec 2025 18:58:00 +0100 Subject: [PATCH 10/70] moved diving heuristics to a separated file --- cpp/src/dual_simplex/pseudo_costs.cpp | 270 -------------------------- cpp/src/dual_simplex/pseudo_costs.hpp | 31 --- 2 files changed, 301 deletions(-) diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 3fdbbbfbf..091639539 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -211,255 +211,6 @@ rounding_direction_t martin_criteria(f_t val, f_t root_val) } } -template -branch_variable_t line_search_diving(const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log) -{ - constexpr f_t eps = 1e-6; - i_t branch_var = -1; - f_t min_score = INFINITY; - rounding_direction_t round_dir = rounding_direction_t::NONE; - - for (auto j : fractional) { - f_t score = inf; - rounding_direction_t dir = rounding_direction_t::NONE; - - if (solution[j] < root_solution[j] - eps) { - f_t f = solution[j] - std::floor(solution[j]); - f_t d = root_solution[j] - solution[j]; - score = f / d; - dir = rounding_direction_t::DOWN; - - } else if (solution[j] > root_solution[j] + eps) { - f_t f = std::ceil(solution[j]) - solution[j]; - f_t d = solution[j] - root_solution[j]; - score = f / d; - dir = rounding_direction_t::UP; - } - - if (min_score > score) { - min_score = score; - branch_var = j; - round_dir = dir; - } - } - - log.debug("Line search diving: selected var %d with val = %e, round dir = %d and score = %e\n", - branch_var, - solution[branch_var], - round_dir, - min_score); - - // If the current solution is equal to the root solution, arbitrarily - // set the branch variable to the first fractional variable and round it down - if (round_dir == rounding_direction_t::NONE) { - branch_var = fractional[0]; - round_dir = rounding_direction_t::DOWN; - } - - return {branch_var, round_dir}; -} - -template -branch_variable_t pseudocost_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log) -{ - std::lock_guard lock(pc.mutex); - i_t branch_var = -1; - f_t max_score = -INFINITY; - rounding_direction_t round_dir = rounding_direction_t::NONE; - constexpr f_t eps = 1e-6; - - i_t num_initialized_down; - i_t num_initialized_up; - f_t pseudo_cost_down_avg; - f_t pseudo_cost_up_avg; - pc.initialized( - num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - - for (auto j : fractional) { - rounding_direction_t dir = rounding_direction_t::NONE; - f_t f_down = solution[j] - std::floor(solution[j]); - f_t f_up = std::ceil(solution[j]) - solution[j]; - - f_t pc_down = pc.pseudo_cost_num_down[j] != 0 - ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] - : pseudo_cost_down_avg; - - f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] - : pseudo_cost_up_avg; - - f_t score_down = std::sqrt(f_up) * (1 + pc_up) / (1 + pc_down); - f_t score_up = std::sqrt(f_down) * (1 + pc_down) / (1 + pc_up); - f_t score = 0; - - if (solution[j] < root_solution[j] - 0.4) { - score = score_down; - dir = rounding_direction_t::DOWN; - } else if (solution[j] > root_solution[j] + 0.4) { - score = score_up; - dir = rounding_direction_t::UP; - } else if (f_down < 0.3) { - score = score_down; - dir = rounding_direction_t::DOWN; - } else if (f_down > 0.7) { - score = score_up; - dir = rounding_direction_t::UP; - } else if (pc_down < pc_up + eps) { - score = score_down; - dir = rounding_direction_t::DOWN; - } else { - score = score_up; - dir = rounding_direction_t::UP; - } - - if (score > max_score) { - max_score = score; - branch_var = j; - round_dir = dir; - } - } - log.debug("Pseudocost diving: selected var %d with val = %e, round dir = %d and score = %e\n", - branch_var, - solution[branch_var], - round_dir, - max_score); - - return {branch_var, round_dir}; -} - -template -branch_variable_t guided_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& incumbent, - logger_t& log) -{ - std::lock_guard lock(pc.mutex); - i_t branch_var = -1; - f_t max_score = -INFINITY; - rounding_direction_t round_dir = rounding_direction_t::NONE; - constexpr f_t eps = 1e-6; - - i_t num_initialized_down; - i_t num_initialized_up; - f_t pseudo_cost_down_avg; - f_t pseudo_cost_up_avg; - pc.initialized( - num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - - for (auto j : fractional) { - f_t f_down = solution[j] - std::floor(solution[j]); - f_t f_up = std::ceil(solution[j]) - solution[j]; - f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j])); - f_t up_dist = std::abs(std::ceil(solution[j]) - incumbent[j]); - rounding_direction_t dir = - down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP; - - f_t pc_down = pc.pseudo_cost_num_down[j] != 0 - ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] - : pseudo_cost_down_avg; - - f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] - : pseudo_cost_up_avg; - - f_t score1 = dir == rounding_direction_t::DOWN ? 5 * pc_down * f_down : 5 * pc_up * f_up; - f_t score2 = dir == rounding_direction_t::DOWN ? pc_up * f_up : pc_down * f_down; - f_t score = (score1 + score2) / 6; - - if (score > max_score) { - max_score = score; - branch_var = j; - round_dir = dir; - } - } - - log.debug("Guided diving: selected var %d with val = %e, round dir = %d and score = %e\n", - branch_var, - solution[branch_var], - round_dir, - max_score); - return {branch_var, round_dir}; -} - -template -std::tuple calculate_variable_locks(const lp_problem_t& lp_problem, i_t var_idx) -{ - i_t up_lock = 0; - i_t down_lock = 0; - i_t start = lp_problem.A.col_start[var_idx]; - i_t end = lp_problem.A.col_start[var_idx + 1]; - - for (i_t k = start; k < end; ++k) { - f_t nz_val = lp_problem.A.x[k]; - i_t nz_row = lp_problem.A.i[k]; - - if (std::isfinite(lp_problem.upper[nz_row]) && std::isfinite(lp_problem.lower[nz_row])) { - down_lock += 1; - up_lock += 1; - continue; - } - - f_t sign = std::isfinite(lp_problem.upper[nz_row]) ? 1 : -1; - - if (nz_val * sign > 0) { - up_lock += 1; - } else { - down_lock += 1; - } - } - - return {up_lock, down_lock}; -} - -template -branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, - const std::vector& fractional, - const std::vector& solution, - logger_t& log) -{ - i_t branch_var = -1; - f_t min_locks = INT_MAX; - rounding_direction_t round_dir = rounding_direction_t::NONE; - constexpr f_t eps = 1e-6; - - for (auto j : fractional) { - f_t f_down = solution[j] - std::floor(solution[j]); - f_t f_up = std::ceil(solution[j]) - solution[j]; - auto [up_lock, down_lock] = calculate_variable_locks(lp_problem, j); - f_t locks = std::min(up_lock, down_lock); - - if (min_locks > locks) { - min_locks = locks; - branch_var = j; - - if (up_lock < down_lock) { - round_dir = rounding_direction_t::UP; - } else if (up_lock > down_lock) { - round_dir = rounding_direction_t::DOWN; - } else if (f_down < f_up + eps) { - round_dir = rounding_direction_t::DOWN; - } else { - round_dir = rounding_direction_t::UP; - } - } - } - - log.debug( - "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %e\n", - branch_var, - solution[branch_var], - round_dir, - min_locks); - - return {branch_var, round_dir}; -} - template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) @@ -626,27 +377,6 @@ template void strong_branching(const lp_problem_t& ori template rounding_direction_t martin_criteria(double val, double root_val); -template branch_variable_t line_search_diving(const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template branch_variable_t pseudocost_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template branch_variable_t guided_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& incumbent, - logger_t& log); - -template branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, - const std::vector& fractional, - const std::vector& solution, - logger_t& log); #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index f2c8abef1..e1df3ad8e 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -17,12 +17,6 @@ namespace cuopt::linear_programming::dual_simplex { -template -struct branch_variable_t { - i_t variable; - rounding_direction_t direction; -}; - template class pseudo_costs_t { public: @@ -86,29 +80,4 @@ void strong_branching(const lp_problem_t& original_lp, template rounding_direction_t martin_criteria(f_t val, f_t root_val); -template -branch_variable_t line_search_diving(const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template -branch_variable_t pseudocost_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& root_solution, - logger_t& log); - -template -branch_variable_t guided_diving(pseudo_costs_t& pc, - const std::vector& fractional, - const std::vector& solution, - const std::vector& incumbent, - logger_t& log); - -template -branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, - const std::vector& fractional, - const std::vector& solution, - logger_t& log); } // namespace cuopt::linear_programming::dual_simplex From 0e815e1cda4a13b6b97273dba3b1df10534663ec Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 3 Dec 2025 19:09:12 +0100 Subject: [PATCH 11/70] organized code. added toggle to disable each type of diving. --- cpp/src/dual_simplex/branch_and_bound.cpp | 23 ----------------------- cpp/src/dual_simplex/pseudo_costs.cpp | 18 ------------------ cpp/src/dual_simplex/pseudo_costs.hpp | 7 ------- 3 files changed, 48 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index e2af8550f..54a335bbd 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -716,29 +716,6 @@ node_solve_info_t branch_and_bound_t::solve_node( f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; -#ifdef LOG_NODE_SIMPLEX - lp_settings.set_log(true); - std::stringstream ss; - ss << "simplex-" << std::this_thread::get_id() << ".log"; - std::string logname; - ss >> logname; - lp_settings.log.set_log_file(logname, "a"); - lp_settings.log.log_to_console = false; - lp_settings.log.printf( - "%s\ncurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " - "%f, variable lower " - "bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", - settings_.log.log_prefix.c_str(), - node_ptr->node_id, - node_ptr->depth, - node_ptr->branch_var, - node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP", - node_ptr->fractional_val, - node_ptr->branch_var_lower, - node_ptr->branch_var_upper, - node_ptr->vstatus[node_ptr->branch_var]); -#endif - lp_status = dual_phase2_with_advanced_basis(2, 0, recompute_bounds_and_basis, diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 091639539..a2defd3b3 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -195,22 +195,6 @@ void strong_branching(const lp_problem_t& original_lp, pc.update_pseudo_costs_from_strong_branching(fractional, root_soln); } -template -rounding_direction_t martin_criteria(f_t val, f_t root_val) -{ - const f_t down_val = std::floor(root_val); - const f_t up_val = std::ceil(root_val); - const f_t down_dist = val - down_val; - const f_t up_dist = up_val - val; - constexpr f_t eps = 1e-6; - - if (down_dist < up_dist + eps) { - return rounding_direction_t::DOWN; - } else { - return rounding_direction_t::UP; - } -} - template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) @@ -375,8 +359,6 @@ template void strong_branching(const lp_problem_t& ori const std::vector& edge_norms, pseudo_costs_t& pc); -template rounding_direction_t martin_criteria(double val, double root_val); - #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index e1df3ad8e..ab01b2a85 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -73,11 +73,4 @@ void strong_branching(const lp_problem_t& original_lp, const std::vector& edge_norms, pseudo_costs_t& pc); -// Martin's criteria for the preferred rounding direction (see [1]) -// [1] A. Martin, “Integer Programs with Block Structure,” -// Technische Universit¨at Berlin, Berlin, 1999. Accessed: Aug. 08, 2025. -// [Online]. Available: https://opus4.kobv.de/opus4-zib/frontdoor/index/index/docId/391 -template -rounding_direction_t martin_criteria(f_t val, f_t root_val); - } // namespace cuopt::linear_programming::dual_simplex From 29a2a330b0b8eefcc5d72cbc828432177ac2451d Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 9 Dec 2025 13:45:18 +0100 Subject: [PATCH 12/70] moved asserts --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 54a335bbd..6167033be 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -799,8 +799,6 @@ node_solve_info_t branch_and_bound_t::solve_node( auto [branch_var, round_dir] = variable_selection( node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); - assert(round_dir != rounding_direction_t::NONE); - assert(branch_var >= 0); assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( From 5a3ef60d09c6bcdb324f8ae8c54da29e7c0d5c0a Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 4 Dec 2025 15:40:25 +0100 Subject: [PATCH 13/70] unified global node heap. fathom node in the diving node that was already explored. --- cpp/src/dual_simplex/bounds_strengthening.cpp | 4 +- cpp/src/dual_simplex/branch_and_bound.cpp | 102 +++++------- cpp/src/dual_simplex/branch_and_bound.hpp | 16 +- cpp/src/dual_simplex/diving_queue.hpp | 73 --------- cpp/src/dual_simplex/mip_node.hpp | 16 -- cpp/src/dual_simplex/node_queue.hpp | 155 ++++++++++++++++++ 6 files changed, 197 insertions(+), 169 deletions(-) delete mode 100644 cpp/src/dual_simplex/diving_queue.hpp create mode 100644 cpp/src/dual_simplex/node_queue.hpp diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index f1bf52c1e..c56c9db98 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -154,7 +154,7 @@ bool bounds_strengthening_t::bounds_strengthening( bool is_infeasible = check_infeasibility(min_a, max_a, cnst_lb, cnst_ub, settings.primal_tol); if (is_infeasible) { - settings.log.printf( + settings.log.debug( "Iter:: %d, Infeasible constraint %d, cnst_lb %e, cnst_ub %e, min_a %e, max_a %e\n", iter, i, @@ -211,7 +211,7 @@ bool bounds_strengthening_t::bounds_strengthening( new_ub = std::min(new_ub, upper_bounds[k]); if (new_lb > new_ub + 1e-6) { - settings.log.printf( + settings.log.debug( "Iter:: %d, Infeasible variable after update %d, %e > %e\n", iter, k, new_lb, new_ub); return false; } diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 6167033be..87e89a15c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -247,25 +247,15 @@ f_t branch_and_bound_t::get_upper_bound() template f_t branch_and_bound_t::get_lower_bound() { - f_t lower_bound = lower_bound_ceiling_.load(); - mutex_heap_.lock(); - if (heap_.size() > 0) { lower_bound = std::min(heap_.top()->lower_bound, lower_bound); } - mutex_heap_.unlock(); + f_t lower_bound = lower_bound_ceiling_.load(); + f_t heap_lower_bound = node_queue.get_lower_bound(); + lower_bound = std::min(heap_lower_bound, lower_bound); for (i_t i = 0; i < local_lower_bounds_.size(); ++i) { lower_bound = std::min(local_lower_bounds_[i].load(), lower_bound); } - return lower_bound; -} - -template -i_t branch_and_bound_t::get_heap_size() -{ - mutex_heap_.lock(); - i_t size = heap_.size(); - mutex_heap_.unlock(); - return size; + return std::isfinite(lower_bound) ? lower_bound : -inf; } template @@ -950,10 +940,8 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod } else { // We've generated enough nodes, push further nodes onto the heap - mutex_heap_.lock(); - heap_.push(node->get_down_child()); - heap_.push(node->get_up_child()); - mutex_heap_.unlock(); + node_queue.push(node->get_down_child()); + node_queue.push(node->get_up_child()); } } } @@ -1072,28 +1060,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (stack.size() > 0) { mip_node_t* node = stack.back(); stack.pop_back(); - - // The order here matters. We want to create a copy of the node - // before adding to the global heap. Otherwise, - // some thread may consume the node (possibly fathoming it) - // before we had the chance to add to the diving queue. - // This lead to a SIGSEGV. Although, in this case, it - // would be better if we discard the node instead. - if (get_heap_size() > settings_.num_bfs_threads) { - std::vector lower = original_lp_.lower; - std::vector upper = original_lp_.upper; - std::fill( - node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); - node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); - - mutex_dive_queue_.lock(); - diving_queue_.emplace(node->detach_copy(), std::move(lower), std::move(upper)); - mutex_dive_queue_.unlock(); - } - - mutex_heap_.lock(); - heap_.push(node); - mutex_heap_.unlock(); + node_queue.push(node); } exploration_stats_.nodes_unexplored += 2; @@ -1131,31 +1098,24 @@ void branch_and_bound_t::best_first_thread(i_t task_id, while (solver_status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && - (active_subtrees_ > 0 || get_heap_size() > 0)) { - mip_node_t* start_node = nullptr; - + (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { // If there any node left in the heap, we pop the top node and explore it. - mutex_heap_.lock(); - if (heap_.size() > 0) { - start_node = heap_.top(); - heap_.pop(); - active_subtrees_++; - } - mutex_heap_.unlock(); + std::optional*> start_node = node_queue.pop_best_first(active_subtrees_); - if (start_node != nullptr) { - if (get_upper_bound() < start_node->lower_bound) { + if (start_node.has_value()) { + if (get_upper_bound() < start_node.value()->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound - search_tree.graphviz_node(settings_.log, start_node, "cutoff", start_node->lower_bound); - search_tree.update(start_node, node_status_t::FATHOMED); + 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); active_subtrees_--; continue; } // Best-first search with plunging explore_subtree(task_id, - start_node, + start_node.value(), search_tree, leaf_problem, node_presolver, @@ -1200,19 +1160,30 @@ void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type, std::vector basic_list(m); std::vector nonbasic_list; + std::vector start_lower; + std::vector start_upper; + bool reset_starting_bounds = true; + while (solver_status_ == mip_exploration_status_t::RUNNING && - (active_subtrees_ > 0 || get_heap_size() > 0)) { - std::optional> start_node; + (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { + if (reset_starting_bounds) { + start_lower = original_lp_.lower; + start_upper = original_lp_.upper; + std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); + reset_starting_bounds = false; + } - mutex_dive_queue_.lock(); - if (diving_queue_.size() > 0) { start_node = diving_queue_.pop(); } - mutex_dive_queue_.unlock(); + std::optional> start_node = + node_queue.pop_diving(start_lower, start_upper, node_presolver.bounds_changed); if (start_node.has_value()) { - if (get_upper_bound() < start_node->node.lower_bound) { continue; } + reset_starting_bounds = true; + + bool is_feasible = node_presolver.bounds_strengthening(start_lower, start_upper, settings_); + if (get_upper_bound() < start_node->lower_bound || !is_feasible) { continue; } bool recompute_bounds_and_basis = true; - search_tree_t subtree(std::move(start_node->node)); + search_tree_t subtree(std::move(start_node.value())); std::deque*> stack; stack.push_front(&subtree.root); @@ -1244,8 +1215,8 @@ void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type, node_presolver, diving_type, recompute_bounds_and_basis, - start_node->lower, - start_node->upper, + start_lower, + start_upper, dive_stats, log); @@ -1578,7 +1549,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - f_t lower_bound = heap_.size() > 0 ? heap_.top()->lower_bound : search_tree_.root.lower_bound; + f_t lower_bound = node_queue.best_first_queue_size() > 0 ? node_queue.get_lower_bound() + : search_tree_.root.lower_bound; return set_final_solution(solution, lower_bound); } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index c774315d8..76d3b61f9 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -8,9 +8,9 @@ #pragma once #include -#include #include #include +#include #include #include #include @@ -21,7 +21,6 @@ #include #include -#include #include namespace cuopt::linear_programming::dual_simplex { @@ -89,9 +88,6 @@ struct bnb_stats_t { template class branch_and_bound_t { public: - template - using mip_node_heap_t = std::priority_queue, node_compare_t>; - branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings); @@ -129,7 +125,6 @@ class branch_and_bound_t { f_t get_upper_bound(); f_t get_lower_bound(); - i_t get_heap_size(); bool enable_concurrent_lp_root_solve() const { return enable_concurrent_lp_root_solve_; } volatile int* get_root_concurrent_halt() { return &root_concurrent_halt_; } void set_root_concurrent_halt(int value) { root_concurrent_halt_ = value; } @@ -183,9 +178,8 @@ class branch_and_bound_t { // Pseudocosts pseudo_costs_t pc_; - // Heap storing the nodes to be explored. - omp_mutex_t mutex_heap_; - mip_node_heap_t*> heap_; + // Heap storing the nodes waiting to be explored. + node_queue_t node_queue; // Search tree search_tree_t search_tree_; @@ -193,10 +187,6 @@ class branch_and_bound_t { // Count the number of subtrees that are currently being explored. omp_atomic_t active_subtrees_; - // Queue for storing the promising node for performing dives. - omp_mutex_t mutex_dive_queue_; - diving_queue_t diving_queue_; - // Global status of the solver. omp_atomic_t solver_status_; diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp deleted file mode 100644 index 0c13c2ed5..000000000 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include -#include -#include - -#include - -namespace cuopt::linear_programming::dual_simplex { - -template -struct diving_root_t { - mip_node_t node; - std::vector lower; - std::vector upper; - - diving_root_t(mip_node_t&& node, std::vector&& lower, std::vector&& upper) - : node(std::move(node)), lower(std::move(lower)), upper(std::move(upper)) - { - } - - friend bool operator>(const diving_root_t& a, const diving_root_t& b) - { - return a.node.objective_estimate > b.node.objective_estimate; - } -}; - -// A min-heap for storing the starting nodes for the dives. -// This has a maximum size of 1024, such that the container -// will discard the least promising node if the queue is full. -template -class diving_queue_t { - private: - std::vector> buffer; - static constexpr i_t max_size_ = 1024; - - public: - diving_queue_t() { buffer.reserve(max_size_); } - - void push(diving_root_t&& node) - { - buffer.push_back(std::move(node)); - std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size() - 1) { buffer.pop_back(); } - } - - void emplace(mip_node_t&& node, std::vector&& lower, std::vector&& upper) - { - buffer.emplace_back(std::move(node), std::move(lower), std::move(upper)); - std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size() - 1) { buffer.pop_back(); } - } - - diving_root_t pop() - { - std::pop_heap(buffer.begin(), buffer.end(), std::greater<>()); - diving_root_t node = std::move(buffer.back()); - buffer.pop_back(); - return node; - } - - i_t size() const { return buffer.size(); } - constexpr i_t max_size() const { return max_size_; } - const diving_root_t& top() const { return buffer.front(); } - void clear() { buffer.clear(); } -}; - -} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index e2e9c6868..9cd858173 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -267,22 +267,6 @@ void remove_fathomed_nodes(std::vector*>& stack) } } -template -class node_compare_t { - public: - bool operator()(const mip_node_t& a, const mip_node_t& b) const - { - return a.lower_bound > - b.lower_bound; // True if a comes before b, elements that come before are output last - } - - bool operator()(const mip_node_t* a, const mip_node_t* b) const - { - return a->lower_bound > - b->lower_bound; // True if a comes before b, elements that come before are output last - } -}; - template class search_tree_t { public: diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp new file mode 100644 index 000000000..8b5eb5fdf --- /dev/null +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +class heap_t { + public: + heap_t() = default; + virtual ~heap_t() = default; + + void push(const T& node) + { + buffer.push_back(node); + std::push_heap(buffer.begin(), buffer.end(), comp); + } + + void push(T&& node) + { + buffer.push_back(std::move(node)); + std::push_heap(buffer.begin(), buffer.end(), comp); + } + + template + void emplace(Args&&... args) + { + buffer.emplace_back(std::forward(args)...); + std::push_heap(buffer.begin(), buffer.end(), comp); + } + + std::optional pop() + { + if (buffer.empty()) return std::nullopt; + + std::pop_heap(buffer.begin(), buffer.end(), comp); + T node = std::move(buffer.back()); + buffer.pop_back(); + return node; + } + + size_t size() const { return buffer.size(); } + T& top() { return buffer.front(); } + void clear() { buffer.clear(); } + bool empty() const { return buffer.empty(); } + + private: + std::vector buffer; + Comp comp; +}; + +template +class node_queue_t { + private: + struct heap_entry_t { + mip_node_t* node = nullptr; + f_t lower_bound = -inf; + f_t score = inf; + + heap_entry_t(mip_node_t* new_node) + : node(new_node), lower_bound(new_node->lower_bound), score(new_node->objective_estimate) + { + } + }; + + struct lower_bound_comp { + bool operator()(const std::shared_ptr& a, const std::shared_ptr& b) + { + // `a` will be placed after `b` + return a->lower_bound > b->lower_bound; + } + }; + + struct score_comp { + bool operator()(const std::shared_ptr& a, const std::shared_ptr& b) + { + // `a` will be placed after `b` + return a->score > b->score; + } + }; + + heap_t, lower_bound_comp> best_first_heap; + heap_t, score_comp> diving_heap; + omp_mutex_t mutex; + + public: + void push(mip_node_t* new_node) + { + std::lock_guard lock(mutex); + auto entry = std::make_shared(new_node); + best_first_heap.push(entry); + diving_heap.push(entry); + } + + std::optional*> pop_best_first(omp_atomic_t& active_subtree) + { + std::lock_guard lock(mutex); + auto entry = best_first_heap.pop(); + + if (entry.has_value()) { + active_subtree++; + return std::exchange(entry.value()->node, nullptr); + } + + return std::nullopt; + } + + std::optional> pop_diving(std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) + { + std::lock_guard lock(mutex); + + while (!diving_heap.empty()) { + auto entry = diving_heap.pop(); + + if (entry.has_value()) { + if (auto node_ptr = entry.value()->node; node_ptr != nullptr) { + node_ptr->get_variable_bounds(lower, upper, bounds_changed); + return node_ptr->detach_copy(); + } + } + } + + return std::nullopt; + } + + i_t diving_queue_size() + { + std::lock_guard lock(mutex); + return diving_heap.size(); + } + + i_t best_first_queue_size() + { + std::lock_guard lock(mutex); + return best_first_heap.size(); + } + + f_t get_lower_bound() + { + std::lock_guard lock(mutex); + return best_first_heap.empty() ? inf : best_first_heap.top()->lower_bound; + } +}; + +} // namespace cuopt::linear_programming::dual_simplex From c181ccf172f335eb4dfbb3dc9b145ce6caeb9f85 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 12 Dec 2025 14:34:45 +0100 Subject: [PATCH 14/70] refactoring code --- cpp/src/dual_simplex/branch_and_bound.cpp | 254 ++++++++++-------- cpp/src/dual_simplex/branch_and_bound.hpp | 43 +-- .../dual_simplex/simplex_solver_settings.hpp | 26 +- cpp/src/mip/diversity/lns/rins.cu | 20 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 11 +- cpp/src/mip/solver.cu | 11 +- 6 files changed, 203 insertions(+), 162 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 87e89a15c..ed6c58de3 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -219,6 +219,7 @@ branch_and_bound_t::branch_and_bound_t( : original_problem_(user_problem), settings_(solver_settings), original_lp_(user_problem.handle_ptr, 1, 1, 1), + Arow_(1, 1, 0), incumbent_(1), root_relax_soln_(1, 1), root_crossover_soln_(1, 1), @@ -654,7 +655,7 @@ node_solve_info_t branch_and_bound_t::solve_node( if (thread_type != bnb_thread_type_t::EXPLORATION) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; - f_t max_iter = 0.05 * bnb_lp_iters; + f_t max_iter = settings_.diving_settings.iteration_limit_factor * bnb_lp_iters; lp_settings.iteration_limit = max_iter - stats.total_lp_iters; if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } } @@ -833,8 +834,6 @@ node_solve_info_t branch_and_bound_t::solve_node( template void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, - search_tree_t* search_tree, - const csr_matrix_t& Arow, i_t initial_heap_size) { if (solver_status_ != mip_exploration_status_t::RUNNING) { return; } @@ -850,8 +849,8 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod f_t abs_gap = upper_bound - lower_bound; if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { - search_tree->graphviz_node(settings_.log, node, "cutoff", node->lower_bound); - search_tree->update(node, node_status_t::FATHOMED); + search_tree_.graphviz_node(settings_.log, node, "cutoff", node->lower_bound); + search_tree_.update(node, node_status_t::FATHOMED); --exploration_stats_.nodes_unexplored; return; } @@ -898,7 +897,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); + bounds_strengthening_t node_presolver(leaf_problem, Arow_, row_sense, var_types_); const i_t m = leaf_problem.num_rows; basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); @@ -906,7 +905,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod std::vector nonbasic_list; node_solve_info_t status = solve_node(node, - *search_tree, + search_tree_, leaf_problem, basis_factors, basic_list, @@ -933,10 +932,10 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase if (exploration_stats_.nodes_unexplored < initial_heap_size) { #pragma omp task - exploration_ramp_up(node->get_down_child(), search_tree, Arow, initial_heap_size); + exploration_ramp_up(node->get_down_child(), initial_heap_size); #pragma omp task - exploration_ramp_up(node->get_up_child(), search_tree, Arow, initial_heap_size); + exploration_ramp_up(node->get_up_child(), initial_heap_size); } else { // We've generated enough nodes, push further nodes onto the heap @@ -947,14 +946,14 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod } template -void branch_and_bound_t::explore_subtree(i_t task_id, - mip_node_t* start_node, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_factors, - std::vector& basic_list, - std::vector& nonbasic_list) +void branch_and_bound_t::plunge_from(i_t task_id, + mip_node_t* start_node, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + basis_update_mpf_t& basis_factors, + std::vector& basic_list, + std::vector& nonbasic_list) { bool recompute_bounds_and_basis = true; std::deque*> stack; @@ -1077,9 +1076,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } template -void branch_and_bound_t::best_first_thread(i_t task_id, - search_tree_t& search_tree, - const csr_matrix_t& Arow) +void branch_and_bound_t::best_first_thread(i_t task_id) { f_t lower_bound = -inf; f_t upper_bound = inf; @@ -1089,7 +1086,7 @@ void branch_and_bound_t::best_first_thread(i_t task_id, // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); + bounds_strengthening_t node_presolver(leaf_problem, Arow_, row_sense, var_types_); const i_t m = leaf_problem.num_rows; basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); @@ -1106,22 +1103,22 @@ void branch_and_bound_t::best_first_thread(i_t task_id, if (get_upper_bound() < start_node.value()->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound - search_tree.graphviz_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); + search_tree_.update(start_node.value(), node_status_t::FATHOMED); active_subtrees_--; continue; } // Best-first search with plunging - explore_subtree(task_id, - start_node.value(), - search_tree, - leaf_problem, - node_presolver, - basis_factors, - basic_list, - nonbasic_list); + plunge_from(task_id, + start_node.value(), + search_tree_, + leaf_problem, + node_presolver, + basis_factors, + basic_list, + nonbasic_list); active_subtrees_--; } @@ -1144,16 +1141,91 @@ void branch_and_bound_t::best_first_thread(i_t task_id, } template -void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type, - const csr_matrix_t& Arow) +void branch_and_bound_t::dive_from(mip_node_t& start_node, + const std::vector& start_lower, + const std::vector& start_upper, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + basis_update_mpf_t& basis_factors, + std::vector& basic_list, + std::vector& nonbasic_list, + bnb_thread_type_t diving_type) { - constexpr i_t backtrack = 5; logger_t log; log.log = false; + + bool recompute_bounds_and_basis = true; + search_tree_t subtree(std::move(start_node)); + std::deque*> stack; + stack.push_front(&subtree.root); + + bnb_stats_t dive_stats; + dive_stats.total_lp_iters = 0; + dive_stats.total_lp_solve_time = 0; + dive_stats.nodes_explored = 0; + dive_stats.nodes_unexplored = 0; + + while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { + mip_node_t* node_ptr = stack.front(); + stack.pop_front(); + f_t upper_bound = get_upper_bound(); + f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node_ptr->lower_bound); + + if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { + recompute_bounds_and_basis = true; + continue; + } + + if (toc(exploration_stats_.start_time) > settings_.time_limit) { break; } + if (dive_stats.nodes_explored > settings_.diving_settings.node_limit) { break; } + + node_solve_info_t status = solve_node(node_ptr, + subtree, + leaf_problem, + basis_factors, + basic_list, + nonbasic_list, + node_presolver, + diving_type, + recompute_bounds_and_basis, + start_lower, + start_upper, + dive_stats, + log); + dive_stats.nodes_explored++; + recompute_bounds_and_basis = !has_children(status); + + if (status == node_solve_info_t::TIME_LIMIT) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; + break; + + } else if (status == node_solve_info_t::ITERATION_LIMIT) { + break; + + } else if (has_children(status)) { + if (status == node_solve_info_t::UP_CHILD_FIRST) { + stack.push_front(node_ptr->get_down_child()); + stack.push_front(node_ptr->get_up_child()); + } else { + stack.push_front(node_ptr->get_up_child()); + stack.push_front(node_ptr->get_down_child()); + } + } + + if (stack.size() > 1 && + stack.front()->depth - stack.back()->depth > settings_.diving_settings.backtrack) { + stack.pop_back(); + } + } +} + +template +void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type) +{ // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); + bounds_strengthening_t node_presolver(leaf_problem, Arow_, row_sense, var_types_); const i_t m = leaf_problem.num_rows; basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); @@ -1182,67 +1254,15 @@ void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type, bool is_feasible = node_presolver.bounds_strengthening(start_lower, start_upper, settings_); if (get_upper_bound() < start_node->lower_bound || !is_feasible) { continue; } - bool recompute_bounds_and_basis = true; - search_tree_t subtree(std::move(start_node.value())); - std::deque*> stack; - stack.push_front(&subtree.root); - - bnb_stats_t dive_stats; - dive_stats.total_lp_iters = 0; - dive_stats.total_lp_solve_time = 0; - dive_stats.nodes_explored = 0; - dive_stats.nodes_unexplored = 0; - - while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { - mip_node_t* node_ptr = stack.front(); - stack.pop_front(); - f_t upper_bound = get_upper_bound(); - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node_ptr->lower_bound); - - if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { - recompute_bounds_and_basis = true; - continue; - } - - if (toc(exploration_stats_.start_time) > settings_.time_limit) { return; } - - node_solve_info_t status = solve_node(node_ptr, - subtree, - leaf_problem, - basis_factors, - basic_list, - nonbasic_list, - node_presolver, - diving_type, - recompute_bounds_and_basis, - start_lower, - start_upper, - dive_stats, - log); - - recompute_bounds_and_basis = !has_children(status); - - if (status == node_solve_info_t::TIME_LIMIT) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return; - - } else if (status == node_solve_info_t::ITERATION_LIMIT) { - break; - - } else if (has_children(status)) { - if (status == node_solve_info_t::UP_CHILD_FIRST) { - stack.push_front(node_ptr->get_down_child()); - stack.push_front(node_ptr->get_up_child()); - } else { - stack.push_front(node_ptr->get_up_child()); - stack.push_front(node_ptr->get_down_child()); - } - } - - if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > backtrack) { - stack.pop_back(); - } - } + dive_from(start_node.value(), + start_lower, + start_upper, + leaf_problem, + node_presolver, + basis_factors, + basic_list, + nonbasic_list, + diving_type); } } } @@ -1331,6 +1351,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solver_status_ = mip_exploration_status_t::UNSET; exploration_stats_.nodes_unexplored = 0; exploration_stats_.nodes_explored = 0; + original_lp_.A.to_compressed_row(Arow_); if (guess_.size() != 0) { std::vector crushed_guess; @@ -1476,13 +1497,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - csr_matrix_t Arow(1, 1, 0); - original_lp_.A.to_compressed_row(Arow); - settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, settings_.num_bfs_threads, - settings_.num_diving_threads); + settings_.diving_settings.num_diving_tasks); settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " @@ -1500,19 +1518,19 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::vector diving_strategies; diving_strategies.reserve(4); - if (!settings_.disable_pseudocost_diving) { + if (!settings_.diving_settings.disable_pseudocost_diving) { diving_strategies.push_back(bnb_thread_type_t::PSEUDOCOST_DIVING); } - if (!settings_.disable_line_search_diving) { + if (!settings_.diving_settings.disable_line_search_diving) { diving_strategies.push_back(bnb_thread_type_t::LINE_SEARCH_DIVING); } - if (!settings_.disable_guided_diving) { + if (!settings_.diving_settings.disable_guided_diving) { diving_strategies.push_back(bnb_thread_type_t::GUIDED_DIVING); } - if (!settings_.disable_coefficient_diving) { + if (!settings_.diving_settings.disable_coefficient_diving) { diving_strategies.push_back(bnb_thread_type_t::COEFFICIENT_DIVING); } @@ -1520,31 +1538,29 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut { #pragma omp master { - auto down_child = search_tree_.root.get_down_child(); - auto up_child = search_tree_.root.get_up_child(); - i_t initial_size = 2 * settings_.num_threads; + auto down_child = search_tree_.root.get_down_child(); + auto up_child = search_tree_.root.get_up_child(); + i_t initial_size = 2 * settings_.num_threads; + const i_t num_strategies = diving_strategies.size(); +#pragma omp taskgroup + { #pragma omp task - exploration_ramp_up(down_child, &search_tree_, Arow, initial_size); + exploration_ramp_up(down_child, initial_size); #pragma omp task - exploration_ramp_up(up_child, &search_tree_, Arow, initial_size); - } - -#pragma omp barrier + exploration_ramp_up(up_child, initial_size); + } -#pragma omp master - { for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree_, Arow); + best_first_thread(i); } - for (i_t k = 0; k < settings_.num_diving_threads; k++) { - const i_t m = diving_strategies.size(); - const bnb_thread_type_t diving_type = diving_strategies[k % m]; + for (i_t k = 0; k < settings_.diving_settings.num_diving_tasks; k++) { + const bnb_thread_type_t diving_type = diving_strategies[k % num_strategies]; #pragma omp task - diving_thread(diving_type, Arow); + diving_thread(diving_type); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 76d3b61f9..4ab9c3d06 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -141,6 +141,7 @@ class branch_and_bound_t { std::vector guess_; // LP relaxation + csr_matrix_t Arow_; lp_problem_t original_lp_; std::vector new_slacks_; std::vector var_types_; @@ -211,30 +212,36 @@ class branch_and_bound_t { // Ramp-up phase of the solver, where we greedily expand the tree until // there is enough unexplored nodes. This is done recursively using OpenMP tasks. - void exploration_ramp_up(mip_node_t* node, - search_tree_t* search_tree, - const csr_matrix_t& Arow, - i_t initial_heap_size); - - // Explore the search tree using the best-first search with plunging strategy. - void explore_subtree(i_t task_id, - mip_node_t* start_node, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_update, - std::vector& basic_list, - std::vector& nonbasic_list); + void exploration_ramp_up(mip_node_t* node, i_t initial_heap_size); + + // Perform a plunge in the subtree determined by the `start_node`. + void plunge_from(i_t task_id, + mip_node_t* start_node, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. - void best_first_thread(i_t task_id, - search_tree_t& search_tree, - const csr_matrix_t& Arow); + void best_first_thread(i_t task_id); + + // Perform a deep dive in the subtree determined by the `start_node`. + void dive_from(mip_node_t& start_node, + const std::vector& start_lower, + const std::vector& start_upper, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list, + bnb_thread_type_t diving_type); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(bnb_thread_type_t diving_type, const csr_matrix_t& Arow); + void diving_thread(bnb_thread_type_t diving_type); // Solve the LP relaxation of a leaf node and update the tree. node_solve_info_t solve_node(mip_node_t* node_ptr, diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 47e4ca49b..9dd67ac62 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -19,6 +19,20 @@ namespace cuopt::linear_programming::dual_simplex { +template +struct diving_heuristics_settings_t { + i_t num_diving_tasks = -1; + + bool disable_line_search_diving = false; + bool disable_pseudocost_diving = false; + bool disable_guided_diving = false; + bool disable_coefficient_diving = false; + + i_t node_limit = 500; + f_t iteration_limit_factor = 0.05; + i_t backtrack = 5; +}; + template struct simplex_solver_settings_t { public: @@ -71,17 +85,13 @@ struct simplex_solver_settings_t { first_iteration_log(2), num_threads(omp_get_max_threads() - 1), num_bfs_threads(std::min(num_threads / 4, 1)), - num_diving_threads(std::min(num_threads - num_bfs_threads, 1)), - disable_line_search_diving(false), - disable_pseudocost_diving(false), - disable_guided_diving(false), - disable_coefficient_diving(false), random_seed(0), inside_mip(0), solution_callback(nullptr), heuristic_preemption_callback(nullptr), concurrent_halt(nullptr) { + diving_settings.num_diving_tasks = std::min(num_threads - num_bfs_threads, 1); } void set_log(bool logging) const { log.log = logging; } @@ -142,12 +152,8 @@ struct simplex_solver_settings_t { i_t num_threads; // number of threads to use i_t random_seed; // random seed i_t num_bfs_threads; // number of threads dedicated to the best-first search - i_t num_diving_threads; // number of threads dedicated to diving - bool disable_line_search_diving; // true to disable line search diving - bool disable_pseudocost_diving; // true to disable pseudocost diving - bool disable_guided_diving; // true to disable guided diving - bool disable_coefficient_diving; // true to disable coefficient diving + diving_heuristics_settings_t diving_settings; // Settings for the diving heuristics i_t inside_mip; // 0 if outside MIP, 1 if inside MIP at root node, 2 if inside MIP at leaf node std::function&, f_t)> solution_callback; diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 0bb1d85c5..0be16bb1e 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -255,13 +255,19 @@ void rins_t::run_rins() branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = 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 = 2; - branch_and_bound_settings.num_bfs_threads = 1; - branch_and_bound_settings.num_diving_threads = 1; - branch_and_bound_settings.log.log = false; - branch_and_bound_settings.log.log_prefix = "[RINS] "; - branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( + branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; + branch_and_bound_settings.num_threads = 2; + branch_and_bound_settings.num_bfs_threads = 1; + + // In the future, let RINS use all the diving heuristics. For now, + // restricting to line search diving. + branch_and_bound_settings.diving_settings.num_diving_tasks = 1; + branch_and_bound_settings.diving_settings.disable_guided_diving = true; + branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + branch_and_bound_settings.log.log = false; + branch_and_bound_settings.log.log_prefix = "[RINS] "; + branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( std::vector& solution, f_t objective) { rins_solution_queue.push_back(solution); }; diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 5be807372..dcfda86a5 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -104,8 +104,15 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; branch_and_bound_settings.num_threads = 2; branch_and_bound_settings.num_bfs_threads = 1; - branch_and_bound_settings.num_diving_threads = 1; - branch_and_bound_settings.solution_callback = [this](std::vector& solution, + + // In the future, let SubMIP use all the diving heuristics. For now, + // restricting to line search diving. + branch_and_bound_settings.diving_settings.num_diving_tasks = 1; + branch_and_bound_settings.diving_settings.disable_guided_diving = true; + branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + + branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); }; diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 0da4c6398..7eb2b226d 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -172,13 +172,12 @@ solution_t mip_solver_t::run_solver() } else { branch_and_bound_settings.num_threads = std::max(1, context.settings.num_cpu_threads); } - CUOPT_LOG_INFO("Using %d CPU threads for B&B", branch_and_bound_settings.num_threads); - i_t num_threads = branch_and_bound_settings.num_threads; - i_t num_bfs_threads = std::max(1, num_threads / 4); - i_t num_diving_threads = std::max(1, num_threads - num_bfs_threads); - branch_and_bound_settings.num_bfs_threads = num_bfs_threads; - branch_and_bound_settings.num_diving_threads = num_diving_threads; + i_t num_threads = branch_and_bound_settings.num_threads; + i_t num_bfs_threads = std::max(1, num_threads / 4); + i_t num_diving_threads = std::max(1, num_threads - num_bfs_threads); + branch_and_bound_settings.num_bfs_threads = num_bfs_threads; + branch_and_bound_settings.diving_settings.num_diving_tasks = num_diving_threads; // Set the branch and bound -> primal heuristics callback branch_and_bound_settings.solution_callback = From 501c20d14949eaa916a95e535478fa2f9cbdf273 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 12 Dec 2025 16:04:24 +0100 Subject: [PATCH 15/70] fix style --- cpp/src/dual_simplex/basis_updates.cpp | 3 +- cpp/src/dual_simplex/crossover.cpp | 31 ++++++++++++-- cpp/src/dual_simplex/phase2.cpp | 56 +++++++++++++++++++------- cpp/src/dual_simplex/primal.cpp | 10 ++++- 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 11056a65e..e44e3b21c 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2068,7 +2068,8 @@ int basis_update_mpf_t::refactor_basis( deficient, slacks_needed) == -1) { settings.log.debug("Initial factorization failed\n"); - basis_repair(A, settings, lower, upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair( + A, settings, lower, upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); #ifdef CHECK_BASIS_REPAIR const i_t m = A.m; diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 3dd61b152..41844729e 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -785,8 +785,15 @@ i_t primal_push(const lp_problem_t& lp, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair( - lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1132,7 +1139,15 @@ crossover_status_t crossover(const lp_problem_t& lp, rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1323,7 +1338,15 @@ crossover_status_t crossover(const lp_problem_t& lp, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index e0ac7239e..3aeef35e1 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -633,7 +633,7 @@ f_t compute_initial_primal_infeasibilities(const lp_problem_t& lp, infeasibility_indices.reserve(n); infeasibility_indices.clear(); f_t primal_inf_squared = 0.0; - primal_inf = 0.0; + primal_inf = 0.0; for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; const f_t lower_infeas = lp.lower[j] - x[j]; @@ -2245,7 +2245,8 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); - if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > + 0) { return dual::status_t::NUMERICAL; } @@ -2362,8 +2363,14 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, phase2::compute_bounded_info(lp.lower, lp.upper, bounded_variables); f_t primal_infeasibility; - f_t primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); + f_t primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 0); @@ -2561,9 +2568,15 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector unperturbed_x(n); phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); - x = unperturbed_x; - primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); + x = unperturbed_x; + primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); settings.log.printf("Updated primal infeasibility: %e\n", primal_infeasibility); objective = lp.objective; @@ -2598,9 +2611,15 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector unperturbed_x(n); phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); - x = unperturbed_x; - primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); + x = unperturbed_x; + primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); const f_t orig_dual_infeas = phase2::dual_infeasibility( lp, settings, vstatus, z, settings.tight_tol, settings.dual_tol); @@ -2888,14 +2907,15 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #endif if (should_refactor) { bool should_recompute_x = false; - if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } i_t count = 0; i_t deficient_size; - while ((deficient_size = - ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus)) > 0) { + while ((deficient_size = ft.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus)) > 0) { settings.log.printf("Failed to repair basis. Iteration %d. %d deficient columns.\n", iter, static_cast(deficient_size)); @@ -2917,8 +2937,14 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); x = unperturbed_x; } - primal_infeasibility_squared = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices, primal_infeasibility); + primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); } #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 7); diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 445177fac..3d9849fbe 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -298,7 +298,15 @@ primal::status_t primal_phase2(i_t phase, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, lp.lower, lp.upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); From 0ee757a02a7fcf7d171376262458177c30013498 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 12 Dec 2025 17:10:21 +0100 Subject: [PATCH 16/70] small fixes --- cpp/src/dual_simplex/branch_and_bound.cpp | 50 +++++++++++-------- cpp/src/dual_simplex/diving_heuristics.cpp | 2 +- cpp/src/dual_simplex/mip_node.hpp | 2 +- .../dual_simplex/simplex_solver_settings.hpp | 2 +- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index ed6c58de3..d294e5409 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1353,6 +1353,29 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); + std::vector diving_strategies; + diving_strategies.reserve(4); + + if (!settings_.diving_settings.disable_pseudocost_diving) { + diving_strategies.push_back(bnb_thread_type_t::PSEUDOCOST_DIVING); + } + + if (!settings_.diving_settings.disable_line_search_diving) { + diving_strategies.push_back(bnb_thread_type_t::LINE_SEARCH_DIVING); + } + + if (!settings_.diving_settings.disable_guided_diving) { + diving_strategies.push_back(bnb_thread_type_t::GUIDED_DIVING); + } + + if (!settings_.diving_settings.disable_coefficient_diving) { + diving_strategies.push_back(bnb_thread_type_t::COEFFICIENT_DIVING); + } + + if (diving_strategies.empty()) { + settings_.log.printf("Warning: All diving heuristics are disabled!"); + } + if (guess_.size() != 0) { std::vector crushed_guess; crush_primal_solution(original_problem_, original_lp_, guess_, new_slacks_, crushed_guess); @@ -1515,25 +1538,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lower_bound_ceiling_ = inf; should_report_ = true; - std::vector diving_strategies; - diving_strategies.reserve(4); - - if (!settings_.diving_settings.disable_pseudocost_diving) { - diving_strategies.push_back(bnb_thread_type_t::PSEUDOCOST_DIVING); - } - - if (!settings_.diving_settings.disable_line_search_diving) { - diving_strategies.push_back(bnb_thread_type_t::LINE_SEARCH_DIVING); - } - - if (!settings_.diving_settings.disable_guided_diving) { - diving_strategies.push_back(bnb_thread_type_t::GUIDED_DIVING); - } - - if (!settings_.diving_settings.disable_coefficient_diving) { - diving_strategies.push_back(bnb_thread_type_t::COEFFICIENT_DIVING); - } - #pragma omp parallel num_threads(settings_.num_threads) { #pragma omp master @@ -1557,10 +1561,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut best_first_thread(i); } - for (i_t k = 0; k < settings_.diving_settings.num_diving_tasks; k++) { - const bnb_thread_type_t diving_type = diving_strategies[k % num_strategies]; + if (!diving_strategies.empty()) { + for (i_t k = 0; k < settings_.diving_settings.num_diving_tasks; k++) { + const bnb_thread_type_t diving_type = diving_strategies[k % num_strategies]; #pragma omp task - diving_thread(diving_type); + diving_thread(diving_type); + } } } } diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 9709123f9..e7ebb9ebe 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -197,7 +197,7 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, } template -std::tuple calculate_variable_locks(const lp_problem_t& lp_problem, i_t var_idx) +std::tuple calculate_variable_locks(const lp_problem_t& lp_problem, i_t var_idx) { i_t up_lock = 0; i_t down_lock = 0; diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 9cd858173..a082932ac 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -60,7 +60,7 @@ class mip_node_t { node_id(0), branch_var(-1), branch_dir(rounding_direction_t::NONE), - objective_estimate(inf), + objective_estimate(std::numeric_limits::infinity()), vstatus(basis) { children[0] = nullptr; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 9dd67ac62..ad4915f5c 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -91,7 +91,7 @@ struct simplex_solver_settings_t { heuristic_preemption_callback(nullptr), concurrent_halt(nullptr) { - diving_settings.num_diving_tasks = std::min(num_threads - num_bfs_threads, 1); + diving_settings.num_diving_tasks = std::max(num_threads - num_bfs_threads, 1); } void set_log(bool logging) const { log.log = logging; } From 546b116894a5aa762bdb343804bf8ad39b4e6fa1 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 12:20:06 +0100 Subject: [PATCH 17/70] unified the best-first and diving heap into a single object. imposed iteration and node limit to the diving threads. --- cpp/src/dual_simplex/branch_and_bound.cpp | 199 ++++++++++------------ cpp/src/dual_simplex/branch_and_bound.hpp | 71 ++++---- cpp/src/dual_simplex/diving_queue.hpp | 73 -------- cpp/src/dual_simplex/mip_node.hpp | 33 ++-- cpp/src/dual_simplex/node_queue.hpp | 173 +++++++++++++++++++ cpp/src/dual_simplex/pseudo_costs.cpp | 38 +++-- cpp/src/dual_simplex/pseudo_costs.hpp | 7 +- 7 files changed, 337 insertions(+), 257 deletions(-) delete mode 100644 cpp/src/dual_simplex/diving_queue.hpp create mode 100644 cpp/src/dual_simplex/node_queue.hpp diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 77acca8f7..ff55d06f7 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -244,25 +244,15 @@ f_t branch_and_bound_t::get_upper_bound() template f_t branch_and_bound_t::get_lower_bound() { - f_t lower_bound = lower_bound_ceiling_.load(); - mutex_heap_.lock(); - if (heap_.size() > 0) { lower_bound = std::min(heap_.top()->lower_bound, lower_bound); } - mutex_heap_.unlock(); + f_t lower_bound = lower_bound_ceiling_.load(); + f_t heap_lower_bound = node_queue.get_lower_bound(); + lower_bound = std::min(heap_lower_bound, lower_bound); for (i_t i = 0; i < local_lower_bounds_.size(); ++i) { lower_bound = std::min(local_lower_bounds_[i].load(), lower_bound); } - return lower_bound; -} - -template -i_t branch_and_bound_t::get_heap_size() -{ - mutex_heap_.lock(); - i_t size = heap_.size(); - mutex_heap_.unlock(); - return size; + return std::isfinite(lower_bound) ? lower_bound : -inf; } template @@ -589,6 +579,7 @@ node_solve_info_t branch_and_bound_t::solve_node( bool recompute_bounds_and_basis, const std::vector& root_lower, const std::vector& root_upper, + bnb_stats_t& stats, logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; @@ -605,6 +596,13 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); lp_settings.scale_columns = false; + if (thread_type != thread_type_t::EXPLORATION) { + i_t bnb_lp_iters = exploration_stats_.total_lp_iters; + f_t max_iter = 0.05 * bnb_lp_iters; + lp_settings.iteration_limit = max_iter - stats.total_lp_iters; + if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } + } + #ifdef LOG_NODE_SIMPLEX lp_settings.set_log(true); std::stringstream ss; @@ -679,10 +677,8 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_status = convert_lp_status_to_dual_status(second_status); } - if (thread_type == thread_type_t::EXPLORATION) { - exploration_stats_.total_lp_solve_time += toc(lp_start_time); - exploration_stats_.total_lp_iters += node_iter; - } + stats.total_lp_solve_time += toc(lp_start_time); + stats.total_lp_iters += node_iter; } if (lp_status == dual::status_t::DUAL_UNBOUNDED) { @@ -726,8 +722,9 @@ node_solve_info_t branch_and_bound_t::solve_node( } else if (leaf_objective <= upper_bound + abs_fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = - pc_.variable_selection(leaf_fractional, leaf_solution.x, lp_settings.log); + auto [branch_var, obj_estimate] = pc_.variable_selection_and_obj_estimate( + leaf_fractional, leaf_solution.x, node_ptr->lower_bound, log); + node_ptr->objective_estimate = obj_estimate; assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( @@ -751,6 +748,9 @@ node_solve_info_t branch_and_bound_t::solve_node( search_tree.graphviz_node(log, node_ptr, "timeout", 0.0); return node_solve_info_t::TIME_LIMIT; + } else if (lp_status == dual::status_t::ITERATION_LIMIT) { + return node_solve_info_t::ITERATION_LIMIT; + } else { if (thread_type == thread_type_t::EXPLORATION) { fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); @@ -854,6 +854,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod true, original_lp_.lower, original_lp_.upper, + exploration_stats_, settings_.log); ++exploration_stats_.nodes_since_last_log; @@ -877,23 +878,21 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod } else { // We've generated enough nodes, push further nodes onto the heap - mutex_heap_.lock(); - heap_.push(node->get_down_child()); - heap_.push(node->get_up_child()); - mutex_heap_.unlock(); + node_queue.push(node->get_down_child()); + node_queue.push(node->get_up_child()); } } } template -void branch_and_bound_t::explore_subtree(i_t task_id, - mip_node_t* start_node, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_factors, - std::vector& basic_list, - std::vector& nonbasic_list) +void branch_and_bound_t::plunge_from(i_t task_id, + mip_node_t* start_node, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + basis_update_mpf_t& basis_factors, + std::vector& basic_list, + std::vector& nonbasic_list) { bool recompute_bounds_and_basis = true; std::deque*> stack; @@ -922,6 +921,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); search_tree.update(node_ptr, node_status_t::FATHOMED); + recompute_bounds_and_basis = true; --exploration_stats_.nodes_unexplored; continue; } @@ -977,6 +977,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, recompute_bounds_and_basis, original_lp_.lower, original_lp_.upper, + exploration_stats_, settings_.log); recompute_bounds_and_basis = !has_children(status); @@ -997,28 +998,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (stack.size() > 0) { mip_node_t* node = stack.back(); stack.pop_back(); - - // The order here matters. We want to create a copy of the node - // before adding to the global heap. Otherwise, - // some thread may consume the node (possibly fathoming it) - // before we had the chance to add to the diving queue. - // This lead to a SIGSEGV. Although, in this case, it - // would be better if we discard the node instead. - if (get_heap_size() > settings_.num_bfs_threads) { - std::vector lower = original_lp_.lower; - std::vector upper = original_lp_.upper; - std::fill( - node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); - node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); - - mutex_dive_queue_.lock(); - diving_queue_.emplace(node->detach_copy(), std::move(lower), std::move(upper)); - mutex_dive_queue_.unlock(); - } - - mutex_heap_.lock(); - heap_.push(node); - mutex_heap_.unlock(); + node_queue.push(node); } exploration_stats_.nodes_unexplored += 2; @@ -1056,37 +1036,31 @@ void branch_and_bound_t::best_first_thread(i_t task_id, while (solver_status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && - (active_subtrees_ > 0 || get_heap_size() > 0)) { - mip_node_t* start_node = nullptr; - + (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { // If there any node left in the heap, we pop the top node and explore it. - mutex_heap_.lock(); - if (heap_.size() > 0) { - start_node = heap_.top(); - heap_.pop(); - active_subtrees_++; - } - mutex_heap_.unlock(); + std::optional*> start_node = node_queue.pop_best_first(active_subtrees_); + + if (start_node.has_value()) { + mip_node_t* node = start_node.value(); - if (start_node != nullptr) { - if (get_upper_bound() < start_node->lower_bound) { + if (get_upper_bound() < node->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound - search_tree.graphviz_node(settings_.log, start_node, "cutoff", start_node->lower_bound); - search_tree.update(start_node, node_status_t::FATHOMED); + search_tree.graphviz_node(settings_.log, node, "cutoff", node->lower_bound); + search_tree.update(node, node_status_t::FATHOMED); active_subtrees_--; continue; } // Best-first search with plunging - explore_subtree(task_id, - start_node, - search_tree, - leaf_problem, - node_presolver, - basis_factors, - basic_list, - nonbasic_list); + plunge_from(task_id, + node, + search_tree, + leaf_problem, + node_presolver, + basis_factors, + basic_list, + nonbasic_list); active_subtrees_--; } @@ -1123,22 +1097,39 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A std::vector basic_list(m); std::vector nonbasic_list; + std::vector start_lower; + std::vector start_upper; + bool reset_starting_bounds = true; + while (solver_status_ == mip_exploration_status_t::RUNNING && - (active_subtrees_ > 0 || get_heap_size() > 0)) { - std::optional> start_node; + (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { + if (reset_starting_bounds) { + start_lower = original_lp_.lower; + start_upper = original_lp_.upper; + std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); + reset_starting_bounds = false; + } - mutex_dive_queue_.lock(); - if (diving_queue_.size() > 0) { start_node = diving_queue_.pop(); } - mutex_dive_queue_.unlock(); + std::optional> start_node = + node_queue.pop_diving(start_lower, start_upper, node_presolver.bounds_changed); if (start_node.has_value()) { - if (get_upper_bound() < start_node->node.lower_bound) { continue; } + reset_starting_bounds = true; + + bool is_feasible = node_presolver.bounds_strengthening(start_lower, start_upper, settings_); + if (get_upper_bound() < start_node->lower_bound || !is_feasible) { continue; } bool recompute_bounds_and_basis = true; - search_tree_t subtree(std::move(start_node->node)); + search_tree_t subtree(std::move(start_node.value())); std::deque*> stack; stack.push_front(&subtree.root); + bnb_stats_t dive_stats; + dive_stats.total_lp_iters = 0; + dive_stats.total_lp_solve_time = 0; + dive_stats.nodes_explored = 0; + dive_stats.nodes_unexplored = 0; + while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -1150,7 +1141,8 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A continue; } - if (toc(exploration_stats_.start_time) > settings_.time_limit) { return; } + if (toc(exploration_stats_.start_time) > settings_.time_limit) { break; } + if (dive_stats.nodes_explored > 500) { break; } node_solve_info_t status = solve_node(node_ptr, subtree, @@ -1161,15 +1153,19 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A node_presolver, thread_type_t::DIVING, recompute_bounds_and_basis, - start_node->lower, - start_node->upper, + start_lower, + start_upper, + dive_stats, log); - + dive_stats.nodes_explored++; recompute_bounds_and_basis = !has_children(status); if (status == node_solve_info_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return; + break; + + } else if (status == node_solve_info_t::ITERATION_LIMIT) { + break; } else if (has_children(status)) { if (status == node_solve_info_t::UP_CHILD_FIRST) { @@ -1181,24 +1177,8 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A } } - if (stack.size() > 1) { - // If the diving thread is consuming the nodes faster than the - // best first search, then we split the current subtree at the - // lowest possible point and move to the queue, so it can - // be picked by another thread. - if (std::lock_guard lock(mutex_dive_queue_); - diving_queue_.size() < min_diving_queue_size_) { - mip_node_t* new_node = stack.back(); - stack.pop_back(); - - std::vector lower = start_node->lower; - std::vector upper = start_node->upper; - std::fill( - node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); - new_node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); - - diving_queue_.emplace(new_node->detach_copy(), std::move(lower), std::move(upper)); - } + if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > 5) { + stack.pop_back(); } } } @@ -1421,7 +1401,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Choose variable to branch on - i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x, log); + auto [branch_var, obj_estimate] = + pc_.variable_selection_and_obj_estimate(fractional, root_relax_soln_.x, root_objective_, log); search_tree_.root = std::move(mip_node_t(root_objective_, root_vstatus_)); search_tree_.num_nodes = 0; @@ -1450,7 +1431,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); active_subtrees_ = 0; - min_diving_queue_size_ = 4 * settings_.num_diving_threads; solver_status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; should_report_ = true; @@ -1486,7 +1466,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - f_t lower_bound = heap_.size() > 0 ? heap_.top()->lower_bound : search_tree_.root.lower_bound; + f_t lower_bound = node_queue.best_first_queue_size() > 0 ? node_queue.get_lower_bound() + : search_tree_.root.lower_bound; return set_final_solution(solution, lower_bound); } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 7891711f7..5b8f0b21e 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -7,9 +7,9 @@ #pragma once -#include #include #include +#include #include #include #include @@ -20,7 +20,6 @@ #include #include -#include #include namespace cuopt::linear_programming::dual_simplex { @@ -68,12 +67,22 @@ class bounds_strengthening_t; template void upper_bound_callback(f_t upper_bound); +template +struct bnb_stats_t { + f_t start_time = 0.0; + omp_atomic_t total_lp_solve_time = 0.0; + omp_atomic_t nodes_explored = 0; + omp_atomic_t nodes_unexplored = 0; + omp_atomic_t total_lp_iters = 0; + + // This should only be used by the main thread + omp_atomic_t last_log = 0.0; + omp_atomic_t nodes_since_last_log = 0; +}; + template class branch_and_bound_t { public: - template - using mip_node_heap_t = std::priority_queue, node_compare_t>; - branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings); @@ -111,7 +120,6 @@ class branch_and_bound_t { f_t get_upper_bound(); f_t get_lower_bound(); - i_t get_heap_size(); bool enable_concurrent_lp_root_solve() const { return enable_concurrent_lp_root_solve_; } volatile int* get_root_concurrent_halt() { return &root_concurrent_halt_; } void set_root_concurrent_halt(int value) { root_concurrent_halt_ = value; } @@ -145,17 +153,7 @@ class branch_and_bound_t { mip_solution_t incumbent_; // Structure with the general info of the solver. - struct stats_t { - f_t start_time = 0.0; - omp_atomic_t total_lp_solve_time = 0.0; - omp_atomic_t nodes_explored = 0; - omp_atomic_t nodes_unexplored = 0; - omp_atomic_t total_lp_iters = 0; - - // This should only be used by the main thread - omp_atomic_t last_log = 0.0; - omp_atomic_t nodes_since_last_log = 0; - } exploration_stats_; + bnb_stats_t exploration_stats_; // Mutex for repair omp_mutex_t mutex_repair_; @@ -175,9 +173,8 @@ class branch_and_bound_t { // Pseudocosts pseudo_costs_t pc_; - // Heap storing the nodes to be explored. - omp_mutex_t mutex_heap_; - mip_node_heap_t*> heap_; + // Heap storing the nodes waiting to be explored. + node_queue_t node_queue; // Search tree search_tree_t search_tree_; @@ -185,11 +182,6 @@ class branch_and_bound_t { // Count the number of subtrees that are currently being explored. omp_atomic_t active_subtrees_; - // Queue for storing the promising node for performing dives. - omp_mutex_t mutex_dive_queue_; - diving_queue_t diving_queue_; - i_t min_diving_queue_size_; - // Global status of the solver. omp_atomic_t solver_status_; @@ -219,15 +211,15 @@ class branch_and_bound_t { const csr_matrix_t& Arow, i_t initial_heap_size); - // Explore the search tree using the best-first search with plunging strategy. - void explore_subtree(i_t task_id, - mip_node_t* start_node, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_update, - std::vector& basic_list, - std::vector& nonbasic_list); + // Perform a plunge in the subtree determined by the `start_node`. + void plunge_from(i_t task_id, + mip_node_t* start_node, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. @@ -235,6 +227,16 @@ class branch_and_bound_t { search_tree_t& search_tree, const csr_matrix_t& Arow); + // Perform a deep dive in the subtree determined by the `start_node`. + void dive_from(mip_node_t& start_node, + const std::vector& start_lower, + const std::vector& start_upper, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list, + thread_type_t diving_type); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. void diving_thread(const csr_matrix_t& Arow); @@ -251,6 +253,7 @@ class branch_and_bound_t { bool recompute_basis_and_bounds, const std::vector& root_lower, const std::vector& root_upper, + bnb_stats_t& stats, logger_t& log); // Sort the children based on the Martin's criteria. diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp deleted file mode 100644 index f7035109e..000000000 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include -#include -#include - -#include - -namespace cuopt::linear_programming::dual_simplex { - -template -struct diving_root_t { - mip_node_t node; - std::vector lower; - std::vector upper; - - diving_root_t(mip_node_t&& node, std::vector&& lower, std::vector&& upper) - : node(std::move(node)), lower(std::move(lower)), upper(std::move(upper)) - { - } - - friend bool operator>(const diving_root_t& a, const diving_root_t& b) - { - return a.node.lower_bound > b.node.lower_bound; - } -}; - -// A min-heap for storing the starting nodes for the dives. -// This has a maximum size of 1024, such that the container -// will discard the least promising node if the queue is full. -template -class diving_queue_t { - private: - std::vector> buffer; - static constexpr i_t max_size_ = 1024; - - public: - diving_queue_t() { buffer.reserve(max_size_); } - - void push(diving_root_t&& node) - { - buffer.push_back(std::move(node)); - std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size() - 1) { buffer.pop_back(); } - } - - void emplace(mip_node_t&& node, std::vector&& lower, std::vector&& upper) - { - buffer.emplace_back(std::move(node), std::move(lower), std::move(upper)); - std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size() - 1) { buffer.pop_back(); } - } - - diving_root_t pop() - { - std::pop_heap(buffer.begin(), buffer.end(), std::greater<>()); - diving_root_t node = std::move(buffer.back()); - buffer.pop_back(); - return node; - } - - i_t size() const { return buffer.size(); } - constexpr i_t max_size() const { return max_size_; } - const diving_root_t& top() const { return buffer.front(); } - void clear() { buffer.clear(); } -}; - -} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 1d66a21f7..a082932ac 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -45,6 +45,7 @@ class mip_node_t { branch_var_lower(-std::numeric_limits::infinity()), branch_var_upper(std::numeric_limits::infinity()), fractional_val(std::numeric_limits::infinity()), + objective_estimate(std::numeric_limits::infinity()), vstatus(0) { children[0] = nullptr; @@ -59,6 +60,7 @@ class mip_node_t { node_id(0), branch_var(-1), branch_dir(rounding_direction_t::NONE), + objective_estimate(std::numeric_limits::infinity()), vstatus(basis) { children[0] = nullptr; @@ -80,6 +82,7 @@ class mip_node_t { branch_var(branch_variable), branch_dir(branch_direction), fractional_val(branch_var_value), + objective_estimate(parent_node->objective_estimate), vstatus(basis) { @@ -227,17 +230,19 @@ class mip_node_t { mip_node_t detach_copy() const { mip_node_t copy(lower_bound, vstatus); - copy.branch_var = branch_var; - copy.branch_dir = branch_dir; - copy.branch_var_lower = branch_var_lower; - copy.branch_var_upper = branch_var_upper; - copy.fractional_val = fractional_val; - copy.node_id = node_id; + copy.branch_var = branch_var; + copy.branch_dir = branch_dir; + copy.branch_var_lower = branch_var_lower; + copy.branch_var_upper = branch_var_upper; + copy.fractional_val = fractional_val; + copy.objective_estimate = objective_estimate; + copy.node_id = node_id; return copy; } node_status_t status; f_t lower_bound; + f_t objective_estimate; i_t depth; i_t node_id; i_t branch_var; @@ -262,22 +267,6 @@ void remove_fathomed_nodes(std::vector*>& stack) } } -template -class node_compare_t { - public: - bool operator()(const mip_node_t& a, const mip_node_t& b) const - { - return a.lower_bound > - b.lower_bound; // True if a comes before b, elements that come before are output last - } - - bool operator()(const mip_node_t* a, const mip_node_t* b) const - { - return a->lower_bound > - b->lower_bound; // True if a comes before b, elements that come before are output last - } -}; - template class search_tree_t { public: diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp new file mode 100644 index 000000000..0234fa038 --- /dev/null +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -0,0 +1,173 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include + +namespace cuopt::linear_programming::dual_simplex { + +// This is a generic heap implementation based +// on the STL functions. The main benefit here is +// that we access the underlying container. +template +class heap_t { + public: + heap_t() = default; + virtual ~heap_t() = default; + + void push(const T& node) + { + buffer.push_back(node); + std::push_heap(buffer.begin(), buffer.end(), comp); + } + + void push(T&& node) + { + buffer.push_back(std::move(node)); + std::push_heap(buffer.begin(), buffer.end(), comp); + } + + template + void emplace(Args&&... args) + { + buffer.emplace_back(std::forward(args)...); + std::push_heap(buffer.begin(), buffer.end(), comp); + } + + std::optional pop() + { + if (buffer.empty()) return std::nullopt; + + std::pop_heap(buffer.begin(), buffer.end(), comp); + T node = std::move(buffer.back()); + buffer.pop_back(); + return node; + } + + size_t size() const { return buffer.size(); } + T& top() { return buffer.front(); } + void clear() { buffer.clear(); } + bool empty() const { return buffer.empty(); } + + private: + std::vector buffer; + Comp comp; +}; + +// A queue storing the nodes waiting to be explored/dived from. +template +class node_queue_t { + private: + struct heap_entry_t { + mip_node_t* node = nullptr; + f_t lower_bound = -inf; + f_t score = inf; + + heap_entry_t(mip_node_t* new_node) + : node(new_node), lower_bound(new_node->lower_bound), score(new_node->objective_estimate) + { + } + }; + + // Comparision function for ordering the nodes based on their lower bound with + // lowest one being explored first. + struct lower_bound_comp { + bool operator()(const std::shared_ptr& a, const std::shared_ptr& b) + { + // `a` will be placed after `b` + return a->lower_bound > b->lower_bound; + } + }; + + // Comparision function for ordering the nodes based on some score (currently the pseudocost + // estimate) with the lowest being explored first. + struct score_comp { + bool operator()(const std::shared_ptr& a, const std::shared_ptr& b) + { + // `a` will be placed after `b` + return a->score > b->score; + } + }; + + heap_t, lower_bound_comp> best_first_heap; + heap_t, score_comp> diving_heap; + omp_mutex_t mutex; + + public: + void push(mip_node_t* new_node) + { + std::lock_guard lock(mutex); + auto entry = std::make_shared(new_node); + best_first_heap.push(entry); + diving_heap.push(entry); + } + + // In the current implementation, we are use the active number of subtree to decide + // when to stop the execution. We need to increment the counter at the same + // time as we pop a node from the queue to avoid some threads exiting + // the main loop thinking that the solver has already finished. + // This will be not needed in the master-worker model. + std::optional*> pop_best_first(omp_atomic_t& active_subtree) + { + std::lock_guard lock(mutex); + auto entry = best_first_heap.pop(); + + if (entry.has_value()) { + active_subtree++; + return std::exchange(entry.value()->node, nullptr); + } + + return std::nullopt; + } + + // In the current implementation, multiple threads can pop the nodes + // from the queue, so we need to pass the lower and upper bound here + // to avoid other thread fathoming the node (i.e., deleting) before we can read + // the variable bounds from the tree. + // This will be not needed in the master-worker model. + std::optional> pop_diving(std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) + { + std::lock_guard lock(mutex); + + while (!diving_heap.empty()) { + auto entry = diving_heap.pop(); + + if (entry.has_value()) { + if (auto node_ptr = entry.value()->node; node_ptr != nullptr) { + node_ptr->get_variable_bounds(lower, upper, bounds_changed); + return node_ptr->detach_copy(); + } + } + } + + return std::nullopt; + } + + i_t diving_queue_size() + { + std::lock_guard lock(mutex); + return diving_heap.size(); + } + + i_t best_first_queue_size() + { + std::lock_guard lock(mutex); + return best_first_heap.size(); + } + + f_t get_lower_bound() + { + std::lock_guard lock(mutex); + return best_first_heap.empty() ? inf : best_first_heap.top()->lower_bound; + } +}; + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 9f84e108d..1c0a33042 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -199,7 +199,7 @@ template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) { - mutex.lock(); + std::lock_guard lock(mutex); const f_t change_in_obj = leaf_objective - node_ptr->lower_bound; const f_t frac = node_ptr->branch_dir == rounding_direction_t::DOWN ? node_ptr->fractional_val - std::floor(node_ptr->fractional_val) @@ -211,7 +211,6 @@ void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_pt pseudo_cost_sum_up[node_ptr->branch_var] += change_in_obj / frac; pseudo_cost_num_up[node_ptr->branch_var]++; } - mutex.unlock(); } template @@ -254,16 +253,19 @@ void pseudo_costs_t::initialized(i_t& num_initialized_down, } template -i_t pseudo_costs_t::variable_selection(const std::vector& fractional, - const std::vector& solution, - logger_t& log) +std::pair pseudo_costs_t::variable_selection_and_obj_estimate( + const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log) { - mutex.lock(); + std::lock_guard lock(mutex); const i_t num_fractional = fractional.size(); std::vector pseudo_cost_up(num_fractional); std::vector pseudo_cost_down(num_fractional); std::vector score(num_fractional); + f_t estimate = lower_bound; i_t num_initialized_down; i_t num_initialized_up; @@ -272,11 +274,11 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio initialized(num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - log.printf("PC: num initialized down %d up %d avg down %e up %e\n", - num_initialized_down, - num_initialized_up, - pseudo_cost_down_avg, - pseudo_cost_up_avg); + log.debug("PC: num initialized down %d up %d avg down %e up %e\n", + num_initialized_down, + num_initialized_up, + pseudo_cost_down_avg, + pseudo_cost_up_avg); for (i_t k = 0; k < num_fractional; k++) { const i_t j = fractional[k]; @@ -296,6 +298,9 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio const f_t f_up = std::ceil(solution[j]) - solution[j]; score[k] = std::max(f_down * pseudo_cost_down[k], eps) * std::max(f_up * pseudo_cost_up[k], eps); + + estimate += std::min(std::max(pseudo_cost_down[k] * f_down, eps), + std::max(pseudo_cost_up[k] * f_up, eps)); } i_t branch_var = fractional[0]; @@ -309,12 +314,13 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio } } - log.printf( - "pc branching on %d. Value %e. Score %e\n", branch_var, solution[branch_var], score[select]); - - mutex.unlock(); + log.debug("Pseudocost branching on %d. Value %e. Score %e. Obj Estimate %e\n", + branch_var, + solution[branch_var], + score[select], + estimate); - return branch_var; + return {branch_var, estimate}; } template diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 799cdc3ff..ab01b2a85 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -43,9 +43,10 @@ class pseudo_costs_t { f_t& pseudo_cost_down_avg, f_t& pseudo_cost_up_avg) const; - i_t variable_selection(const std::vector& fractional, - const std::vector& solution, - logger_t& log); + std::pair variable_selection_and_obj_estimate(const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log); void update_pseudo_costs_from_strong_branching(const std::vector& fractional, const std::vector& root_soln); From 06d531add93eb06e881479e660fb69e5466b9400 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 12:26:39 +0100 Subject: [PATCH 18/70] adjusted column spacing in bnb logs. added opening mode for logger. --- cpp/src/dual_simplex/branch_and_bound.cpp | 92 ++++++++++++----------- cpp/src/dual_simplex/logger.hpp | 8 +- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 77acca8f7..226cbb0a8 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -195,9 +195,9 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) inline const char* feasible_solution_symbol(thread_type_t type) { switch (type) { - case thread_type_t::EXPLORATION: return "B"; - case thread_type_t::DIVING: return "D"; - default: return "U"; + case thread_type_t::EXPLORATION: return "B "; + case thread_type_t::DIVING: return "D "; + default: return "U "; } } @@ -310,7 +310,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu std::string gap = user_mip_gap(user_obj, user_lower); settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + "H %+13.6e %+10.6e %s %9.2f\n", user_obj, user_lower, gap.c_str(), @@ -423,7 +423,7 @@ void branch_and_bound_t::repair_heuristic_solutions() std::string user_gap = user_mip_gap(obj, lower); settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + "H %+13.6e %+10.6e %s %9.2f\n", obj, lower, user_gap.c_str(), @@ -534,17 +534,17 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - settings_.log.printf( - "%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - feasible_solution_symbol(thread_type), - nodes_explored, - nodes_unexplored, - obj, - lower, - leaf_depth, - nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(exploration_stats_.start_time)); + f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + feasible_solution_symbol(thread_type), + nodes_explored, + nodes_unexplored, + obj, + lower, + leaf_depth, + iter_node, + user_mip_gap(obj, lower).c_str(), + toc(exploration_stats_.start_time)); send_solution = true; } @@ -611,18 +611,20 @@ node_solve_info_t branch_and_bound_t::solve_node( ss << "simplex-" << std::this_thread::get_id() << ".log"; std::string logname; ss >> logname; - lp_settings.set_log_filename(logname); - lp_settings.log.enable_log_to_file("a+"); + lp_settings.log.set_log_file(logname, "a"); lp_settings.log.log_to_console = false; lp_settings.log.printf( - "%s node id = %d, branch var = %d, fractional val = %f, variable lower bound = %f, variable " - "upper bound = %f\n", + "%scurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " + "%f, variable lower bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", settings_.log.log_prefix.c_str(), node_ptr->node_id, + node_ptr->depth, node_ptr->branch_var, + node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP", node_ptr->fractional_val, node_ptr->branch_var_lower, - node_ptr->branch_var_upper); + node_ptr->branch_var_upper, + node_ptr->vstatus[node_ptr->branch_var]); #endif // Reset the bound_changed markers @@ -685,6 +687,10 @@ node_solve_info_t branch_and_bound_t::solve_node( } } +#ifdef LOG_NODE_SIMPLEX + lp_settings.log.printf("\nLP status: %d\n\n", lp_status); +#endif + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { // Node was infeasible. Do not branch node_ptr->lower_bound = inf; @@ -810,17 +816,17 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod std::string gap_user = user_mip_gap(obj, user_lower); i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - - settings_.log.printf( - " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node->depth, - nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, - gap_user.c_str(), - now); + f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + + settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + obj, + user_lower, + node->depth, + iter_node, + gap_user.c_str(), + now); exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); @@ -941,17 +947,17 @@ void branch_and_bound_t::explore_subtree(i_t task_id, std::string gap_user = user_mip_gap(obj, user_lower); i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - - settings_.log.printf( - " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node_ptr->depth, - nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, - gap_user.c_str(), - now); + f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + + settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + obj, + user_lower, + node_ptr->depth, + iter_node, + gap_user.c_str(), + now); exploration_stats_.last_log = tic(); exploration_stats_.nodes_since_last_log = 0; } diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index ac5e394f9..f6030d521 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -30,17 +30,17 @@ class logger_t { { } - void enable_log_to_file(std::string mode = "w") + void enable_log_to_file(const char* mode = "w") { if (log_file != nullptr) { std::fclose(log_file); } - log_file = std::fopen(log_filename.c_str(), mode.c_str()); + log_file = std::fopen(log_filename.c_str(), mode); log_to_file = true; } - void set_log_file(const std::string& filename) + void set_log_file(const std::string& filename, const char* mode = "w") { log_filename = filename; - enable_log_to_file(); + enable_log_to_file(mode); } void close_log_file() From ae742c2231442e5ee213132f87ed25e63e6863cc Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 13:21:17 +0100 Subject: [PATCH 19/70] revert fixes for dual simplex. changed RINS and SubMIP to use guided diving --- cpp/src/dual_simplex/basis_solves.cpp | 14 +--- cpp/src/dual_simplex/basis_solves.hpp | 2 - cpp/src/dual_simplex/basis_updates.hpp | 2 - cpp/src/dual_simplex/crossover.cpp | 31 +------- cpp/src/dual_simplex/phase2.cpp | 77 ++++++------------- cpp/src/dual_simplex/primal.cpp | 10 +-- cpp/src/mip/diversity/lns/rins.cu | 4 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 4 +- 8 files changed, 33 insertions(+), 111 deletions(-) diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index 3080f269d..db24f55a2 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -613,8 +613,6 @@ i_t factorize_basis(const csc_matrix_t& A, template i_t basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, - const std::vector& lower, - const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, @@ -660,15 +658,7 @@ i_t basis_repair(const csc_matrix_t& A, nonbasic_list[nonbasic_map[replace_j]] = bad_j; vstatus[replace_j] = variable_status_t::BASIC; // This is the main issue. What value should bad_j take on. - if (lower[bad_j] == -inf && upper[bad_j] == inf) { - vstatus[bad_j] = variable_status_t::NONBASIC_FREE; - } else if (lower[bad_j] > -inf) { - vstatus[bad_j] = variable_status_t::NONBASIC_LOWER; - } else if (upper[bad_j] < inf) { - vstatus[bad_j] = variable_status_t::NONBASIC_UPPER; - } else { - assert(1 == 0); - } + vstatus[bad_j] = variable_status_t::NONBASIC_FREE; } return 0; @@ -859,8 +849,6 @@ template int factorize_basis(const csc_matrix_t& A, template int basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, - const std::vector& lower, - const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, diff --git a/cpp/src/dual_simplex/basis_solves.hpp b/cpp/src/dual_simplex/basis_solves.hpp index 0745806a6..b668c0f46 100644 --- a/cpp/src/dual_simplex/basis_solves.hpp +++ b/cpp/src/dual_simplex/basis_solves.hpp @@ -42,8 +42,6 @@ i_t factorize_basis(const csc_matrix_t& A, template i_t basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, - const std::vector& lower, - const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index 9b5d3e614..cea907074 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -373,8 +373,6 @@ class basis_update_mpf_t { // Compute L*U = A(p, basic_list) int refactor_basis(const csc_matrix_t& A, const simplex_solver_settings_t& settings, - const std::vector& lower, - const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus); diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 41844729e..23d9a0e8e 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -785,15 +785,8 @@ i_t primal_push(const lp_problem_t& lp, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, - settings, - lp.lower, - lp.upper, - deficient, - slacks_needed, - basic_list, - nonbasic_list, - vstatus); + basis_repair( + lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1139,15 +1132,7 @@ crossover_status_t crossover(const lp_problem_t& lp, rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, - settings, - lp.lower, - lp.upper, - deficient, - slacks_needed, - basic_list, - nonbasic_list, - vstatus); + basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1338,15 +1323,7 @@ crossover_status_t crossover(const lp_problem_t& lp, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, - settings, - lp.lower, - lp.upper, - deficient, - slacks_needed, - basic_list, - nonbasic_list, - vstatus); + basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 3aeef35e1..56298ef4d 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -623,17 +623,14 @@ f_t compute_initial_primal_infeasibilities(const lp_problem_t& lp, const std::vector& basic_list, const std::vector& x, std::vector& squared_infeasibilities, - std::vector& infeasibility_indices, - f_t& primal_inf) + std::vector& infeasibility_indices) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; - squared_infeasibilities.resize(n); - std::fill(squared_infeasibilities.begin(), squared_infeasibilities.end(), 0.0); + squared_infeasibilities.resize(n, 0.0); infeasibility_indices.reserve(n); infeasibility_indices.clear(); - f_t primal_inf_squared = 0.0; - primal_inf = 0.0; + f_t primal_inf = 0.0; for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; const f_t lower_infeas = lp.lower[j] - x[j]; @@ -643,11 +640,10 @@ f_t compute_initial_primal_infeasibilities(const lp_problem_t& lp, const f_t square_infeas = infeas * infeas; squared_infeasibilities[j] = square_infeas; infeasibility_indices.push_back(j); - primal_inf_squared += square_infeas; - primal_inf += infeas; + primal_inf += square_infeas; } } - return primal_inf_squared; + return primal_inf; } template @@ -2245,8 +2241,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); - if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > - 0) { + if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { return dual::status_t::NUMERICAL; } @@ -2273,7 +2268,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #ifdef COMPUTE_DUAL_RESIDUAL std::vector dual_res1; - phase2::compute_dual_residual(lp.A, objective, y, z, dual_res1); + compute_dual_residual(lp.A, objective, y, z, dual_res1); f_t dual_res_norm = vector_norm_inf(dual_res1); if (dual_res_norm > settings.tight_tol) { settings.log.printf("|| A'*y + z - c || %e\n", dual_res_norm); @@ -2362,15 +2357,8 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector bounded_variables(n, 0); phase2::compute_bounded_info(lp.lower, lp.upper, bounded_variables); - f_t primal_infeasibility; - f_t primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); + f_t primal_infeasibility = phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 0); @@ -2568,15 +2556,9 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector unperturbed_x(n); phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); - x = unperturbed_x; - primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); + x = unperturbed_x; + primal_infeasibility = phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); settings.log.printf("Updated primal infeasibility: %e\n", primal_infeasibility); objective = lp.objective; @@ -2611,15 +2593,9 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector unperturbed_x(n); phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); - x = unperturbed_x; - primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); + x = unperturbed_x; + primal_infeasibility = phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); const f_t orig_dual_infeas = phase2::dual_infeasibility( lp, settings, vstatus, z, settings.tight_tol, settings.dual_tol); @@ -2834,7 +2810,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, delta_xB_0_sparse.i, squared_infeasibilities, infeasibility_indices, - primal_infeasibility_squared); + primal_infeasibility); // Update primal infeasibilities due to changes in basic variables // from the leaving and entering variables phase2::update_primal_infeasibilities(lp, @@ -2846,7 +2822,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, scaled_delta_xB_sparse.i, squared_infeasibilities, infeasibility_indices, - primal_infeasibility_squared); + primal_infeasibility); // Update the entering variable phase2::update_single_primal_infeasibility(lp.lower, lp.upper, @@ -2907,15 +2883,14 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #endif if (should_refactor) { bool should_recompute_x = false; - if (ft.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } i_t count = 0; i_t deficient_size; - while ((deficient_size = ft.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus)) > 0) { + while ((deficient_size = + ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus)) > 0) { settings.log.printf("Failed to repair basis. Iteration %d. %d deficient columns.\n", iter, static_cast(deficient_size)); @@ -2937,14 +2912,8 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); x = unperturbed_x; } - primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); + phase2::compute_initial_primal_infeasibilities( + lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); } #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 7); @@ -2982,7 +2951,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, iter, compute_user_objective(lp, obj), infeasibility_indices.size(), - primal_infeasibility_squared, + primal_infeasibility, sum_perturb, now); } diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 3d9849fbe..80406dcf0 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -298,15 +298,7 @@ primal::status_t primal_phase2(i_t phase, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, - settings, - lp.lower, - lp.upper, - deficient, - slacks_needed, - basic_list, - nonbasic_list, - vstatus); + basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 0be16bb1e..41dd6d39e 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -260,9 +260,9 @@ void rins_t::run_rins() branch_and_bound_settings.num_bfs_threads = 1; // In the future, let RINS use all the diving heuristics. For now, - // restricting to line search diving. + // restricting to guided diving. branch_and_bound_settings.diving_settings.num_diving_tasks = 1; - branch_and_bound_settings.diving_settings.disable_guided_diving = true; + branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; branch_and_bound_settings.log.log = false; diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index dcfda86a5..fa21fb7d2 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -106,9 +106,9 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.num_bfs_threads = 1; // In the future, let SubMIP use all the diving heuristics. For now, - // restricting to line search diving. + // restricting to guided diving. branch_and_bound_settings.diving_settings.num_diving_tasks = 1; - branch_and_bound_settings.diving_settings.disable_guided_diving = true; + branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; From b5f2c7e93be3ccf560bfa5758539c28a29e36a07 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 13:23:00 +0100 Subject: [PATCH 20/70] moved bound propagation logs to debug mode --- cpp/src/dual_simplex/bounds_strengthening.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index f1bf52c1e..c56c9db98 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -154,7 +154,7 @@ bool bounds_strengthening_t::bounds_strengthening( bool is_infeasible = check_infeasibility(min_a, max_a, cnst_lb, cnst_ub, settings.primal_tol); if (is_infeasible) { - settings.log.printf( + settings.log.debug( "Iter:: %d, Infeasible constraint %d, cnst_lb %e, cnst_ub %e, min_a %e, max_a %e\n", iter, i, @@ -211,7 +211,7 @@ bool bounds_strengthening_t::bounds_strengthening( new_ub = std::min(new_ub, upper_bounds[k]); if (new_lb > new_ub + 1e-6) { - settings.log.printf( + settings.log.debug( "Iter:: %d, Infeasible variable after update %d, %e > %e\n", iter, k, new_lb, new_ub); return false; } From eb5c6959702d1d42672b4718780f5d0d6b8bf82b Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 13:41:38 +0100 Subject: [PATCH 21/70] addressing code rabbit suggestions --- cpp/src/dual_simplex/branch_and_bound.cpp | 8 ++++++-- cpp/src/dual_simplex/logger.hpp | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 226cbb0a8..eef5decaf 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -816,7 +816,9 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod std::string gap_user = user_mip_gap(obj, user_lower); i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / + static_cast(nodes_explored) + : 0; settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", nodes_explored, @@ -947,7 +949,9 @@ void branch_and_bound_t::explore_subtree(i_t task_id, std::string gap_user = user_mip_gap(obj, user_lower); i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / + static_cast(nodes_explored) + : 0; settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", nodes_explored, diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index f6030d521..c45e3ede3 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -46,6 +46,8 @@ class logger_t { void close_log_file() { if (log_file != nullptr) { std::fclose(log_file); } + log_file = nullptr; + log_to_file = false; } void printf(const char* fmt, ...) From 5cf5ac0fbaddd5d242843d109ffa13f6ead9f1c5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 13:53:16 +0100 Subject: [PATCH 22/70] added explicit conversion to float --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index eef5decaf..854594169 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -534,7 +534,9 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / + static_cast(nodes_explored) + : 0; settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", feasible_solution_symbol(thread_type), nodes_explored, From d7dfb0d6e035c59f086403efbc0591207873bc23 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 15:01:36 +0100 Subject: [PATCH 23/70] missing code revert in basis update --- cpp/src/dual_simplex/basis_updates.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index e44e3b21c..6b79f3c86 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2046,8 +2046,6 @@ template int basis_update_mpf_t::refactor_basis( const csc_matrix_t& A, const simplex_solver_settings_t& settings, - const std::vector& lower, - const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus) @@ -2068,8 +2066,7 @@ int basis_update_mpf_t::refactor_basis( deficient, slacks_needed) == -1) { settings.log.debug("Initial factorization failed\n"); - basis_repair( - A, settings, lower, upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); #ifdef CHECK_BASIS_REPAIR const i_t m = A.m; From 55e64ccac66ca6bb0f2ae53f7d9263132c6dfefb Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 15:04:01 +0100 Subject: [PATCH 24/70] fixed variable type --- cpp/src/dual_simplex/diving_heuristics.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index e7ebb9ebe..978a97e42 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -241,7 +241,7 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl f_t f_down = solution[j] - std::floor(solution[j]); f_t f_up = std::ceil(solution[j]) - solution[j]; auto [up_lock, down_lock] = calculate_variable_locks(lp_problem, j); - f_t locks = std::min(up_lock, down_lock); + i_t locks = std::min(up_lock, down_lock); if (min_locks > locks) { min_locks = locks; @@ -263,7 +263,7 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl assert(branch_var >= 0); log.debug( - "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %e\n", + "Coefficient diving: selected var %d with val = %e, round dir = %d and min locks = %d\n", branch_var, solution[branch_var], round_dir, From 0b1e9948b7dac6fd5ac922a14d863bb0990d9568 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 16 Dec 2025 14:51:23 +0100 Subject: [PATCH 25/70] added comments --- cpp/src/dual_simplex/node_queue.hpp | 18 ++++++++++++++++++ cpp/src/dual_simplex/pseudo_costs.cpp | 17 ++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 8b5eb5fdf..0234fa038 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -12,6 +12,9 @@ namespace cuopt::linear_programming::dual_simplex { +// This is a generic heap implementation based +// on the STL functions. The main benefit here is +// that we access the underlying container. template class heap_t { public: @@ -57,6 +60,7 @@ class heap_t { Comp comp; }; +// A queue storing the nodes waiting to be explored/dived from. template class node_queue_t { private: @@ -71,6 +75,8 @@ class node_queue_t { } }; + // Comparision function for ordering the nodes based on their lower bound with + // lowest one being explored first. struct lower_bound_comp { bool operator()(const std::shared_ptr& a, const std::shared_ptr& b) { @@ -79,6 +85,8 @@ class node_queue_t { } }; + // Comparision function for ordering the nodes based on some score (currently the pseudocost + // estimate) with the lowest being explored first. struct score_comp { bool operator()(const std::shared_ptr& a, const std::shared_ptr& b) { @@ -100,6 +108,11 @@ class node_queue_t { diving_heap.push(entry); } + // In the current implementation, we are use the active number of subtree to decide + // when to stop the execution. We need to increment the counter at the same + // time as we pop a node from the queue to avoid some threads exiting + // the main loop thinking that the solver has already finished. + // This will be not needed in the master-worker model. std::optional*> pop_best_first(omp_atomic_t& active_subtree) { std::lock_guard lock(mutex); @@ -113,6 +126,11 @@ class node_queue_t { return std::nullopt; } + // In the current implementation, multiple threads can pop the nodes + // from the queue, so we need to pass the lower and upper bound here + // to avoid other thread fathoming the node (i.e., deleting) before we can read + // the variable bounds from the tree. + // This will be not needed in the master-worker model. std::optional> pop_diving(std::vector& lower, std::vector& upper, std::vector& bounds_changed) diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index a2defd3b3..1c0a33042 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -274,11 +274,11 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat initialized(num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - log.printf("PC: num initialized down %d up %d avg down %e up %e\n", - num_initialized_down, - num_initialized_up, - pseudo_cost_down_avg, - pseudo_cost_up_avg); + log.debug("PC: num initialized down %d up %d avg down %e up %e\n", + num_initialized_down, + num_initialized_up, + pseudo_cost_down_avg, + pseudo_cost_up_avg); for (i_t k = 0; k < num_fractional; k++) { const i_t j = fractional[k]; @@ -314,8 +314,11 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat } } - log.printf( - "pc branching on %d. Value %e. Score %e\n", branch_var, solution[branch_var], score[select]); + log.debug("Pseudocost branching on %d. Value %e. Score %e. Obj Estimate %e\n", + branch_var, + solution[branch_var], + score[select], + estimate); return {branch_var, estimate}; } From 9effdc8ca4d9490756c91771cfcc6b92ded8c678 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 16 Dec 2025 14:54:44 +0100 Subject: [PATCH 26/70] added missing spacing --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 854594169..fc3786fcc 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -183,11 +183,11 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) { const f_t user_mip_gap = relative_gap(obj_value, lower_bound); if (user_mip_gap == std::numeric_limits::infinity()) { - return " - "; + return " - "; } else { constexpr int BUFFER_LEN = 32; char buffer[BUFFER_LEN]; - snprintf(buffer, BUFFER_LEN - 1, "%4.1f%%", user_mip_gap * 100); + snprintf(buffer, BUFFER_LEN - 1, "%5.1f%%", user_mip_gap * 100); return std::string(buffer); } } From 6d43e037d036cd1ec7add445b7c5a602669fdba9 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 16 Dec 2025 14:57:22 +0100 Subject: [PATCH 27/70] updated logs --- cpp/src/dual_simplex/branch_and_bound.cpp | 32 ++++++++++++++--------- cpp/src/dual_simplex/logger.hpp | 2 ++ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d294e5409..6524c2ac1 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -532,7 +532,9 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / + static_cast(nodes_explored) + : 0; settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", feasible_solution_symbol(thread_type), nodes_explored, @@ -871,7 +873,9 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod std::string gap_user = user_mip_gap(obj, user_lower); i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - f_t iter_node = nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0; + f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / + static_cast(nodes_explored) + : 0; settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", nodes_explored, @@ -1002,17 +1006,19 @@ void branch_and_bound_t::plunge_from(i_t task_id, std::string gap_user = user_mip_gap(obj, user_lower); i_t nodes_explored = exploration_stats_.nodes_explored; i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - - settings_.log.printf( - " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node_ptr->depth, - nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, - gap_user.c_str(), - now); + f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / + static_cast(nodes_explored) + : 0; + + settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + obj, + user_lower, + node_ptr->depth, + iter_node, + gap_user.c_str(), + now); exploration_stats_.last_log = tic(); exploration_stats_.nodes_since_last_log = 0; } diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index f6030d521..c45e3ede3 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -46,6 +46,8 @@ class logger_t { void close_log_file() { if (log_file != nullptr) { std::fclose(log_file); } + log_file = nullptr; + log_to_file = false; } void printf(const char* fmt, ...) From fbb99664fcdaa5c85c76ea76eae248bde6587110 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 18 Dec 2025 19:35:41 +0100 Subject: [PATCH 28/70] refactoring --- cpp/src/dual_simplex/branch_and_bound.cpp | 142 ++++++++-------------- cpp/src/dual_simplex/branch_and_bound.hpp | 3 + 2 files changed, 56 insertions(+), 89 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index fa9d2e6c7..79765898d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -265,6 +265,51 @@ i_t branch_and_bound_t::get_heap_size() return size; } +template +void branch_and_bound_t::report_heuristic(f_t obj) +{ + if (solver_status_ == mip_exploration_status_t::RUNNING) { + f_t user_obj = compute_user_objective(original_lp_, obj); + f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); + std::string user_gap = user_mip_gap(user_obj, user_lower); + + settings_.log.printf( + "H %+13.6e %+10.6e %s %9.2f\n", + user_obj, + user_lower, + user_gap.c_str(), + toc(exploration_stats_.start_time)); + } else { + settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", + compute_user_objective(original_lp_, obj), + toc(exploration_stats_.start_time)); + } +} + +template +void branch_and_bound_t::report(std::string symbol, + f_t obj, + f_t lower_bound, + i_t node_depth) +{ + i_t nodes_explored = exploration_stats_.nodes_explored; + i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + f_t user_obj = compute_user_objective(original_lp_, obj); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + f_t iter_node = exploration_stats_.total_lp_iters / nodes_explored; + std::string user_gap = user_mip_gap(user_obj, user_lower); + settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + symbol.c_str(), + nodes_explored, + nodes_unexplored, + user_obj, + user_lower, + node_depth, + iter_node, + user_gap.c_str(), + toc(exploration_stats_.start_time)); +} + template void branch_and_bound_t::set_new_solution(const std::vector& solution) { @@ -303,25 +348,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu } mutex_upper_.unlock(); - if (is_feasible) { - if (solver_status_ == mip_exploration_status_t::RUNNING) { - f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); - std::string gap = user_mip_gap(user_obj, user_lower); - - settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", - user_obj, - user_lower, - gap.c_str(), - toc(exploration_stats_.start_time)); - } else { - settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", - compute_user_objective(original_lp_, obj), - toc(exploration_stats_.start_time)); - } - } - + if (is_feasible) { report_heuristic(obj); } if (attempt_repair) { mutex_repair_.lock(); repair_queue_.push_back(crushed_solution); @@ -417,17 +444,7 @@ void branch_and_bound_t::repair_heuristic_solutions() if (repaired_obj < upper_bound_) { upper_bound_ = repaired_obj; incumbent_.set_incumbent_solution(repaired_obj, repaired_solution); - - f_t obj = compute_user_objective(original_lp_, repaired_obj); - f_t lower = compute_user_objective(original_lp_, get_lower_bound()); - std::string user_gap = user_mip_gap(obj, lower); - - settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", - obj, - lower, - user_gap.c_str(), - toc(exploration_stats_.start_time)); + report_heuristic(repaired_obj); if (settings_.solution_callback != nullptr) { std::vector original_x; @@ -523,31 +540,13 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, i_t leaf_depth, thread_type_t thread_type) { - bool send_solution = false; - i_t nodes_explored = exploration_stats_.nodes_explored; - i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + bool send_solution = false; mutex_upper_.lock(); if (leaf_objective < upper_bound_) { incumbent_.set_incumbent_solution(leaf_objective, leaf_solution); - upper_bound_ = leaf_objective; - f_t lower_bound = get_lower_bound(); - f_t obj = compute_user_objective(original_lp_, upper_bound_); - f_t lower = compute_user_objective(original_lp_, lower_bound); - f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / - static_cast(nodes_explored) - : 0; - settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - feasible_solution_symbol(thread_type), - nodes_explored, - nodes_unexplored, - obj, - lower, - leaf_depth, - iter_node, - user_mip_gap(obj, lower).c_str(), - toc(exploration_stats_.start_time)); - + upper_bound_ = leaf_objective; + report(feasible_solution_symbol(thread_type), leaf_objective, get_lower_bound(), leaf_depth); send_solution = true; } @@ -813,25 +812,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod bool should_report = should_report_.exchange(false); if (should_report) { - f_t obj = compute_user_objective(original_lp_, upper_bound); - f_t user_lower = compute_user_objective(original_lp_, root_objective_); - std::string gap_user = user_mip_gap(obj, user_lower); - i_t nodes_explored = exploration_stats_.nodes_explored; - i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / - static_cast(nodes_explored) - : 0; - - settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node->depth, - iter_node, - gap_user.c_str(), - now); - + report(" ", upper_bound, root_objective_, node->depth); exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); should_report_ = true; @@ -946,24 +927,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, abs_gap < 10 * settings_.absolute_mip_gap_tol) && time_since_last_log >= 1) || (time_since_last_log > 30) || now > settings_.time_limit) { - f_t obj = compute_user_objective(original_lp_, upper_bound); - f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); - std::string gap_user = user_mip_gap(obj, user_lower); - i_t nodes_explored = exploration_stats_.nodes_explored; - i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - f_t iter_node = nodes_explored > 0 ? static_cast(exploration_stats_.total_lp_iters) / - static_cast(nodes_explored) - : 0; - - settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node_ptr->depth, - iter_node, - gap_user.c_str(), - now); + report(" ", upper_bound, get_lower_bound(), node_ptr->depth); exploration_stats_.last_log = tic(); exploration_stats_.nodes_since_last_log = 0; } @@ -1462,7 +1426,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " "| Time |\n"); - exploration_stats_.nodes_explored = 0; + exploration_stats_.nodes_explored = 1; exploration_stats_.nodes_unexplored = 2; exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 38438cc9e..a55964c36 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -199,6 +199,9 @@ class branch_and_bound_t { // its blocks the progression of the lower bound. omp_atomic_t lower_bound_ceiling_; + void report_heuristic(f_t obj); + void report(std::string symbol, f_t obj, f_t lower_bound, i_t node_depth); + // Set the final solution. mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); From 78f38a406eb1e02285a985dda470b0f7c1ef674d Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 19 Dec 2025 17:17:16 +0100 Subject: [PATCH 29/70] adjust header spacing --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 79765898d..46785d19d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1423,7 +1423,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.num_diving_threads); settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " + " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " "| Time |\n"); exploration_stats_.nodes_explored = 1; From dd8955fde78a582a4094761a5508b2e9b3366906 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 19 Dec 2025 17:39:24 +0100 Subject: [PATCH 30/70] fix compilation error --- cpp/src/dual_simplex/branch_and_bound.cpp | 45 ----------------------- 1 file changed, 45 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 5f238e46a..fb433fed4 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -304,51 +304,6 @@ void branch_and_bound_t::report(std::string symbol, toc(exploration_stats_.start_time)); } -template -void branch_and_bound_t::report_heuristic(f_t obj) -{ - if (solver_status_ == mip_exploration_status_t::RUNNING) { - f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); - std::string user_gap = user_mip_gap(user_obj, user_lower); - - settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", - user_obj, - user_lower, - user_gap.c_str(), - toc(exploration_stats_.start_time)); - } else { - settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", - compute_user_objective(original_lp_, obj), - toc(exploration_stats_.start_time)); - } -} - -template -void branch_and_bound_t::report(std::string symbol, - f_t obj, - f_t lower_bound, - i_t node_depth) -{ - i_t nodes_explored = exploration_stats_.nodes_explored; - i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_lower = compute_user_objective(original_lp_, lower_bound); - f_t iter_node = exploration_stats_.total_lp_iters / nodes_explored; - std::string user_gap = user_mip_gap(user_obj, user_lower); - settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - symbol.c_str(), - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_depth, - iter_node, - user_gap.c_str(), - toc(exploration_stats_.start_time)); -} - template void branch_and_bound_t::set_new_solution(const std::vector& solution) { From 426b445e279bd47988032738e4b3e0cd31ee43f0 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 6 Jan 2026 14:45:31 +0100 Subject: [PATCH 31/70] added cli option for disabling each diving heuristic --- .../linear_programming/cuopt/run_mip.cpp | 51 ++++++++++++++++++- .../mip/solver_settings.hpp | 7 ++- cpp/src/dual_simplex/branch_and_bound.cpp | 8 ++- cpp/src/mip/diversity/lns/rins.cu | 14 +++-- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 9 +++- cpp/src/mip/solver.cu | 11 +++- 6 files changed, 89 insertions(+), 11 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 6013dcaf5..6d8fbdd81 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -147,6 +147,10 @@ int run_single_file(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, + bool disable_line_search_diving, + bool disable_pseudocost_diving, + bool disable_guided_diving, + bool disable_coefficient_diving, double time_limit) { const raft::handle_t handle_{}; @@ -204,6 +208,11 @@ int run_single_file(std::string file_path, settings.tolerances.relative_tolerance = 1e-12; settings.tolerances.absolute_tolerance = 1e-6; settings.presolve = true; + settings.disable_line_search_diving = disable_line_search_diving; + settings.disable_pseudocost_diving = disable_pseudocost_diving; + settings.disable_guided_diving = disable_guided_diving; + settings.disable_coefficient_diving = disable_coefficient_diving; + cuopt::linear_programming::benchmark_info_t benchmark_info; settings.benchmark_info_ptr = &benchmark_info; auto start_run_solver = std::chrono::high_resolution_clock::now(); @@ -250,6 +259,10 @@ void run_single_file_mp(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, + bool disable_line_search_diving, + bool disable_pseudocost_diving, + bool disable_guided_diving, + bool disable_coefficient_diving, double time_limit) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; @@ -265,6 +278,10 @@ void run_single_file_mp(std::string file_path, num_cpu_threads, write_log_file, log_to_console, + disable_line_search_diving, + disable_pseudocost_diving, + disable_guided_diving, + disable_coefficient_diving, time_limit); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes @@ -348,6 +365,22 @@ int main(int argc, char* argv[]) .help("track allocations (t/f)") .default_value(std::string("f")); + program.add_argument("--disable-line-search-diving") + .help("disable line search diving (t/f)") + .default_value(std::string("f")); + + program.add_argument("--disable-pseudocost-diving") + .help("disable pseudocost diving (t/f)") + .default_value(std::string("f")); + + program.add_argument("--disable-guided-diving") + .help("disable guided diving (t/f)") + .default_value(std::string("f")); + + program.add_argument("--disable-coefficient-diving") + .help("disable coefficient diving (t/f)") + .default_value(std::string("f")); + // Parse arguments try { program.parse_args(argc, argv); @@ -377,6 +410,14 @@ int main(int argc, char* argv[]) double memory_limit = program.get("--memory-limit"); bool track_allocations = program.get("--track-allocations")[0] == 't'; + bool disable_line_search_diving = + program.get("--disable-line-search-diving")[0] == 't'; + bool disable_pseudocost_diving = + program.get("--disable-pseudocost-diving")[0] == 't'; + bool disable_guided_diving = program.get("--disable-guided-diving")[0] == 't'; + bool disable_coefficient_diving = + program.get("--disable-coefficient-diving")[0] == 't'; + if (num_cpu_threads < 0) { num_cpu_threads = omp_get_max_threads() / n_gpus; } if (program.is_used("--out-dir")) { @@ -463,6 +504,10 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, + disable_line_search_diving, + disable_pseudocost_diving, + disable_guided_diving, + disable_coefficient_diving, time_limit); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; @@ -503,6 +548,10 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, + disable_line_search_diving, + disable_pseudocost_diving, + disable_guided_diving, + disable_coefficient_diving, time_limit); } diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 4f6320752..680cceaba 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -87,6 +87,11 @@ class mip_solver_settings_t { std::string sol_file; std::string user_problem_file; + bool disable_line_search_diving = false; + bool disable_pseudocost_diving = false; + bool disable_guided_diving = false; + bool disable_coefficient_diving = false; + /** Initial primal solutions */ std::vector>> initial_solutions; bool mip_scaling = true; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index fb433fed4..1993630e1 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -640,7 +640,11 @@ node_solve_info_t branch_and_bound_t::solve_node( // If there is no incumbent, use pseudocost diving instead of guided diving if (upper_bound == inf && thread_type == bnb_thread_type_t::GUIDED_DIVING) { - thread_type = bnb_thread_type_t::PSEUDOCOST_DIVING; + if (settings_.diving_settings.disable_pseudocost_diving) { + thread_type = bnb_thread_type_t::COEFFICIENT_DIVING; + } else { + thread_type = bnb_thread_type_t::PSEUDOCOST_DIVING; + } } lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 59ca45de5..1893e84fe 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights * reserved. SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -265,9 +265,15 @@ void rins_t::run_rins() branch_and_bound_settings.diving_settings.num_diving_tasks = 1; branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; - branch_and_bound_settings.log.log = false; - branch_and_bound_settings.log.log_prefix = "[RINS] "; + + if (context.settings.disable_guided_diving) { + branch_and_bound_settings.diving_settings.disable_guided_diving = true; + } else { + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + } + + branch_and_bound_settings.log.log = false; + branch_and_bound_settings.log.log_prefix = "[RINS] "; branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( std::vector& solution, f_t objective) { rins_solution_queue.push_back(solution); diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index fa21fb7d2..2335003b6 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -110,7 +110,12 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.diving_settings.num_diving_tasks = 1; branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + + if (context.settings.disable_guided_diving) { + branch_and_bound_settings.diving_settings.disable_guided_diving = true; + } else { + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + } branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 7eb2b226d..790831129 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -167,6 +167,15 @@ solution_t mip_solver_t::run_solver() 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.diving_settings.disable_coefficient_diving = + context.settings.disable_coefficient_diving; + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = + context.settings.disable_pseudocost_diving; + branch_and_bound_settings.diving_settings.disable_guided_diving = + context.settings.disable_guided_diving; + branch_and_bound_settings.diving_settings.disable_line_search_diving = + context.settings.disable_line_search_diving; + if (context.settings.num_cpu_threads < 0) { branch_and_bound_settings.num_threads = omp_get_max_threads() - 1; } else { From 319bd22acfa3dd6c15be01253d256914b6ff1096 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 6 Jan 2026 15:00:41 +0100 Subject: [PATCH 32/70] fix style --- cpp/src/dual_simplex/bounds_strengthening.cpp | 2 +- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- cpp/src/dual_simplex/branch_and_bound.hpp | 2 +- cpp/src/dual_simplex/mip_node.hpp | 2 +- cpp/src/dual_simplex/node_queue.hpp | 4 ++-- cpp/src/dual_simplex/pseudo_costs.cpp | 2 +- cpp/src/dual_simplex/pseudo_costs.hpp | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index c56c9db98..4114e7e09 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index ff55d06f7..15db6f975 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 032eed11f..b719a220a 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index a082932ac..de147132a 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 0234fa038..804273697 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -1,6 +1,6 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 */ #pragma once diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 1c0a33042..aabbe5a17 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index ab01b2a85..5c34e0296 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ From 3e52ffeac6ef0dacd1a8ff12788cf951b8ca73fc Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 6 Jan 2026 15:35:49 +0100 Subject: [PATCH 33/70] fix infeasible list (#694) --- cpp/src/dual_simplex/basis_solves.cpp | 16 +++++- cpp/src/dual_simplex/basis_solves.hpp | 4 +- cpp/src/dual_simplex/basis_updates.cpp | 7 ++- cpp/src/dual_simplex/basis_updates.hpp | 4 +- cpp/src/dual_simplex/crossover.cpp | 33 +++++++++-- cpp/src/dual_simplex/phase2.cpp | 79 ++++++++++++++++++-------- cpp/src/dual_simplex/primal.cpp | 12 +++- 7 files changed, 118 insertions(+), 37 deletions(-) diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index db24f55a2..f5cd54053 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -613,6 +613,8 @@ i_t factorize_basis(const csc_matrix_t& A, template i_t basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, @@ -658,7 +660,15 @@ i_t basis_repair(const csc_matrix_t& A, nonbasic_list[nonbasic_map[replace_j]] = bad_j; vstatus[replace_j] = variable_status_t::BASIC; // This is the main issue. What value should bad_j take on. - vstatus[bad_j] = variable_status_t::NONBASIC_FREE; + if (lower[bad_j] == -inf && upper[bad_j] == inf) { + vstatus[bad_j] = variable_status_t::NONBASIC_FREE; + } else if (lower[bad_j] > -inf) { + vstatus[bad_j] = variable_status_t::NONBASIC_LOWER; + } else if (upper[bad_j] < inf) { + vstatus[bad_j] = variable_status_t::NONBASIC_UPPER; + } else { + assert(1 == 0); + } } return 0; @@ -849,6 +859,8 @@ template int factorize_basis(const csc_matrix_t& A, template int basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, diff --git a/cpp/src/dual_simplex/basis_solves.hpp b/cpp/src/dual_simplex/basis_solves.hpp index b668c0f46..295bedccd 100644 --- a/cpp/src/dual_simplex/basis_solves.hpp +++ b/cpp/src/dual_simplex/basis_solves.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -42,6 +42,8 @@ i_t factorize_basis(const csc_matrix_t& A, template i_t basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, const std::vector& deficient, const std::vector& slacks_needed, std::vector& basis_list, diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 6b79f3c86..2c781a515 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -2046,6 +2046,8 @@ template int basis_update_mpf_t::refactor_basis( const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus) @@ -2066,7 +2068,8 @@ int basis_update_mpf_t::refactor_basis( deficient, slacks_needed) == -1) { settings.log.debug("Initial factorization failed\n"); - basis_repair(A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair( + A, settings, lower, upper, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); #ifdef CHECK_BASIS_REPAIR const i_t m = A.m; diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index cea907074..afd4f4c9a 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -373,6 +373,8 @@ class basis_update_mpf_t { // Compute L*U = A(p, basic_list) int refactor_basis(const csc_matrix_t& A, const simplex_solver_settings_t& settings, + const std::vector& lower, + const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus); diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 23d9a0e8e..8ee3fb0ce 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -785,8 +785,15 @@ i_t primal_push(const lp_problem_t& lp, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair( - lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1132,7 +1139,15 @@ crossover_status_t crossover(const lp_problem_t& lp, rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); @@ -1323,7 +1338,15 @@ crossover_status_t crossover(const lp_problem_t& lp, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 56298ef4d..2bc00f636 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -623,14 +623,17 @@ f_t compute_initial_primal_infeasibilities(const lp_problem_t& lp, const std::vector& basic_list, const std::vector& x, std::vector& squared_infeasibilities, - std::vector& infeasibility_indices) + std::vector& infeasibility_indices, + f_t& primal_inf) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; - squared_infeasibilities.resize(n, 0.0); + squared_infeasibilities.resize(n); + std::fill(squared_infeasibilities.begin(), squared_infeasibilities.end(), 0.0); infeasibility_indices.reserve(n); infeasibility_indices.clear(); - f_t primal_inf = 0.0; + f_t primal_inf_squared = 0.0; + primal_inf = 0.0; for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; const f_t lower_infeas = lp.lower[j] - x[j]; @@ -640,10 +643,11 @@ f_t compute_initial_primal_infeasibilities(const lp_problem_t& lp, const f_t square_infeas = infeas * infeas; squared_infeasibilities[j] = square_infeas; infeasibility_indices.push_back(j); - primal_inf += square_infeas; + primal_inf_squared += square_infeas; + primal_inf += infeas; } } - return primal_inf; + return primal_inf_squared; } template @@ -2241,7 +2245,8 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); - if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > + 0) { return dual::status_t::NUMERICAL; } @@ -2268,7 +2273,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #ifdef COMPUTE_DUAL_RESIDUAL std::vector dual_res1; - compute_dual_residual(lp.A, objective, y, z, dual_res1); + phase2::compute_dual_residual(lp.A, objective, y, z, dual_res1); f_t dual_res_norm = vector_norm_inf(dual_res1); if (dual_res_norm > settings.tight_tol) { settings.log.printf("|| A'*y + z - c || %e\n", dual_res_norm); @@ -2357,8 +2362,15 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector bounded_variables(n, 0); phase2::compute_bounded_info(lp.lower, lp.upper, bounded_variables); - f_t primal_infeasibility = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + f_t primal_infeasibility; + f_t primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 0); @@ -2556,9 +2568,15 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector unperturbed_x(n); phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); - x = unperturbed_x; - primal_infeasibility = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + x = unperturbed_x; + primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); settings.log.printf("Updated primal infeasibility: %e\n", primal_infeasibility); objective = lp.objective; @@ -2593,9 +2611,15 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, std::vector unperturbed_x(n); phase2::compute_primal_solution_from_basis( lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); - x = unperturbed_x; - primal_infeasibility = phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + x = unperturbed_x; + primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); const f_t orig_dual_infeas = phase2::dual_infeasibility( lp, settings, vstatus, z, settings.tight_tol, settings.dual_tol); @@ -2810,7 +2834,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, delta_xB_0_sparse.i, squared_infeasibilities, infeasibility_indices, - primal_infeasibility); + primal_infeasibility_squared); // Update primal infeasibilities due to changes in basic variables // from the leaving and entering variables phase2::update_primal_infeasibilities(lp, @@ -2822,7 +2846,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, scaled_delta_xB_sparse.i, squared_infeasibilities, infeasibility_indices, - primal_infeasibility); + primal_infeasibility_squared); // Update the entering variable phase2::update_single_primal_infeasibility(lp.lower, lp.upper, @@ -2883,14 +2907,15 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #endif if (should_refactor) { bool should_recompute_x = false; - if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } i_t count = 0; i_t deficient_size; - while ((deficient_size = - ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus)) > 0) { + while ((deficient_size = ft.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus)) > 0) { settings.log.printf("Failed to repair basis. Iteration %d. %d deficient columns.\n", iter, static_cast(deficient_size)); @@ -2912,8 +2937,14 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, lp, ft, basic_list, nonbasic_list, vstatus, unperturbed_x); x = unperturbed_x; } - phase2::compute_initial_primal_infeasibilities( - lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); + primal_infeasibility_squared = + phase2::compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); } #ifdef CHECK_BASIC_INFEASIBILITIES phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 7); @@ -2951,7 +2982,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, iter, compute_user_objective(lp, obj), infeasibility_indices.size(), - primal_infeasibility, + primal_infeasibility_squared, sum_perturb, now); } diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 80406dcf0..69f15ba18 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -298,7 +298,15 @@ primal::status_t primal_phase2(i_t phase, factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + basis_repair(lp.A, + settings, + lp.lower, + lp.upper, + deficient, + slacks_needed, + basic_list, + nonbasic_list, + vstatus); if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); From 972b18766ffaba47dfb3f9a1fa64e4456f440d81 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Dec 2025 17:08:39 +0100 Subject: [PATCH 34/70] created a struct with all persistent data used by each worker --- cpp/src/dual_simplex/bounds_strengthening.cpp | 4 +- cpp/src/dual_simplex/bounds_strengthening.hpp | 3 +- cpp/src/dual_simplex/branch_and_bound.cpp | 256 ++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 60 ++-- cpp/src/dual_simplex/presolve.cpp | 6 +- 5 files changed, 162 insertions(+), 167 deletions(-) diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index c56c9db98..cc8f8241d 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -59,8 +59,7 @@ bounds_strengthening_t::bounds_strengthening_t( const csr_matrix_t& Arow, const std::vector& row_sense, const std::vector& var_types) - : bounds_changed(problem.num_cols, false), - A(problem.A), + : A(problem.A), Arow(Arow), var_types(var_types), delta_min_activity(problem.num_rows), @@ -93,6 +92,7 @@ template bool bounds_strengthening_t::bounds_strengthening( std::vector& lower_bounds, std::vector& upper_bounds, + const std::vector& bounds_changed, const simplex_solver_settings_t& settings) { const i_t m = A.m; diff --git a/cpp/src/dual_simplex/bounds_strengthening.hpp b/cpp/src/dual_simplex/bounds_strengthening.hpp index e7e218b82..dfad27005 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.hpp +++ b/cpp/src/dual_simplex/bounds_strengthening.hpp @@ -22,10 +22,9 @@ class bounds_strengthening_t { bool bounds_strengthening(std::vector& lower_bounds, std::vector& upper_bounds, + const std::vector& bounds_changed, const simplex_solver_settings_t& settings); - std::vector bounds_changed; - private: const csc_matrix_t& A; const csr_matrix_t& Arow; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 1993630e1..a23f5c172 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -304,6 +304,18 @@ void branch_and_bound_t::report(std::string symbol, toc(exploration_stats_.start_time)); } +template +bnb_worker_data_t* branch_and_bound_t::get_worker_data(i_t tid) +{ + std::lock_guard lock(mutex_worker_data_); + if (persistent_worker_data_.find(tid) == persistent_worker_data_.end()) { + persistent_worker_data_[tid] = + std::make_unique>(original_lp_, Arow_, var_types_, settings_); + } + + return persistent_worker_data_[tid].get(); +} + template void branch_and_bound_t::set_new_solution(const std::vector& solution) { @@ -311,6 +323,7 @@ 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; crush_primal_solution( original_problem_, original_lp_, solution, new_slacks_, crushed_solution); @@ -620,20 +633,46 @@ branch_variable_t branch_and_bound_t::variable_selection( } template -node_solve_info_t branch_and_bound_t::solve_node( - mip_node_t* node_ptr, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - basis_update_mpf_t& basis_factors, - std::vector& basic_list, - std::vector& nonbasic_list, - bounds_strengthening_t& node_presolver, - bnb_thread_type_t thread_type, - bool recompute_bounds_and_basis, - const std::vector& root_lower, - const std::vector& root_upper, - bnb_stats_t& stats, - logger_t& log) +bool branch_and_bound_t::set_node_bounds(mip_node_t* node_ptr, + const std::vector& start_lower, + const std::vector& start_upper, + bnb_worker_data_t* worker_data) +{ + bounds_strengthening_t& node_presolver = worker_data->node_presolver; + std::vector& upper = worker_data->leaf_problem.upper; + std::vector& lower = worker_data->leaf_problem.lower; + std::vector& bounds_changed = worker_data->bounds_changed; + + assert(bounds_changed.size() == original_lp_.num_cols); + assert(lower.size() == original_lp_.num_cols); + assert(upper.size() == original_lp_.num_cols); + + // Reset the bound_changed markers + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + + // Set the correct bounds for the leaf problem + if (worker_data->recompute_bounds) { + lower = start_lower; + upper = start_upper; + node_ptr->get_variable_bounds(lower, upper, bounds_changed); + + } else { + node_ptr->update_branched_variable_bounds(lower, upper, bounds_changed); + } + + bool feasible = node_presolver.bounds_strengthening(lower, upper, bounds_changed, settings_); + return feasible; +} + +template +node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* node_ptr, + search_tree_t& search_tree, + bnb_thread_type_t thread_type, + bnb_worker_data_t* worker_data, + const std::vector& root_lower, + const std::vector& root_upper, + bnb_stats_t& stats, + logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; const f_t upper_bound = get_upper_bound(); @@ -647,6 +686,7 @@ node_solve_info_t branch_and_bound_t::solve_node( } } + lp_problem_t& leaf_problem = worker_data->leaf_problem; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); std::vector& leaf_vstatus = node_ptr->vstatus; assert(leaf_vstatus.size() == leaf_problem.num_cols); @@ -687,56 +727,41 @@ node_solve_info_t branch_and_bound_t::solve_node( node_ptr->vstatus[node_ptr->branch_var]); #endif - // Reset the bound_changed markers - std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); - - // Set the correct bounds for the leaf problem - if (recompute_bounds_and_basis) { - leaf_problem.lower = root_lower; - leaf_problem.upper = root_upper; - node_ptr->get_variable_bounds( - leaf_problem.lower, leaf_problem.upper, node_presolver.bounds_changed); - - } else { - node_ptr->update_branched_variable_bounds( - leaf_problem.lower, leaf_problem.upper, node_presolver.bounds_changed); - } - - bool feasible = - node_presolver.bounds_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); + bool is_feasible = set_node_bounds(node_ptr, root_lower, root_upper, worker_data); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; - if (feasible) { + if (is_feasible) { i_t node_iter = 0; f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; lp_status = dual_phase2_with_advanced_basis(2, 0, - recompute_bounds_and_basis, + worker_data->recompute_basis, lp_start_time, leaf_problem, lp_settings, leaf_vstatus, - basis_factors, - basic_list, - nonbasic_list, + worker_data->basis_factors, + worker_data->basic_list, + worker_data->nonbasic_list, leaf_solution, node_iter, leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); - lp_status_t second_status = solve_linear_program_with_advanced_basis(leaf_problem, - lp_start_time, - lp_settings, - leaf_solution, - basis_factors, - basic_list, - nonbasic_list, - leaf_vstatus, - leaf_edge_norms); + lp_status_t second_status = + solve_linear_program_with_advanced_basis(leaf_problem, + lp_start_time, + lp_settings, + leaf_solution, + worker_data->basis_factors, + worker_data->basic_list, + worker_data->nonbasic_list, + leaf_vstatus, + leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } @@ -848,6 +873,10 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // to repair the heuristic solution. repair_heuristic_solutions(); + i_t tid = omp_get_thread_num(); + bnb_worker_data_t* worker_data = get_worker_data(tid); + assert(worker_data); + f_t lower_bound = node->lower_bound; f_t upper_bound = get_upper_bound(); f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); @@ -883,25 +912,13 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod return; } - // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp_; - std::vector row_sense; - bounds_strengthening_t node_presolver(leaf_problem, Arow_, row_sense, var_types_); - - const i_t m = leaf_problem.num_rows; - basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); - std::vector basic_list(m); - std::vector nonbasic_list; + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; node_solve_info_t status = solve_node(node, search_tree_, - leaf_problem, - basis_factors, - basic_list, - nonbasic_list, - node_presolver, bnb_thread_type_t::EXPLORATION, - true, + worker_data, original_lp_.lower, original_lp_.upper, exploration_stats_, @@ -935,19 +952,18 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod } template -void branch_and_bound_t::plunge_from(i_t task_id, - mip_node_t* start_node, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_factors, - std::vector& basic_list, - std::vector& nonbasic_list) +void branch_and_bound_t::plunge_from(i_t task_id, mip_node_t* start_node) { - bool recompute_bounds_and_basis = true; + i_t tid = omp_get_thread_num(); + bnb_worker_data_t* worker_data = get_worker_data(tid); + assert(worker_data); + std::deque*> stack; stack.push_front(start_node); + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; + while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { if (task_id == 0) { repair_heuristic_solutions(); } @@ -969,9 +985,10 @@ void branch_and_bound_t::plunge_from(i_t task_id, local_lower_bounds_[task_id] = lower_bound; if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { - search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); - search_tree.update(node_ptr, node_status_t::FATHOMED); - recompute_bounds_and_basis = true; + search_tree_.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); + search_tree_.update(node_ptr, node_status_t::FATHOMED); + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; --exploration_stats_.nodes_unexplored; continue; } @@ -1002,20 +1019,16 @@ void branch_and_bound_t::plunge_from(i_t task_id, } node_solve_info_t status = solve_node(node_ptr, - search_tree, - leaf_problem, - basis_factors, - basic_list, - nonbasic_list, - node_presolver, + search_tree_, bnb_thread_type_t::EXPLORATION, - recompute_bounds_and_basis, + worker_data, original_lp_.lower, original_lp_.upper, exploration_stats_, settings_.log); - recompute_bounds_and_basis = !has_children(status); + worker_data->recompute_basis = !has_children(status); + worker_data->recompute_bounds = !has_children(status); ++exploration_stats_.nodes_since_last_log; ++exploration_stats_.nodes_explored; @@ -1057,16 +1070,6 @@ void branch_and_bound_t::best_first_thread(i_t task_id) f_t abs_gap = inf; f_t rel_gap = inf; - // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp_; - std::vector row_sense; - bounds_strengthening_t node_presolver(leaf_problem, Arow_, row_sense, var_types_); - - const i_t m = leaf_problem.num_rows; - basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); - std::vector basic_list(m); - std::vector nonbasic_list; - while (solver_status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { @@ -1085,14 +1088,7 @@ void branch_and_bound_t::best_first_thread(i_t task_id) } // Best-first search with plunging - plunge_from(task_id, - start_node.value(), - search_tree_, - leaf_problem, - node_presolver, - basis_factors, - basic_list, - nonbasic_list); + plunge_from(task_id, start_node.value()); active_subtrees_--; } @@ -1118,17 +1114,18 @@ template void branch_and_bound_t::dive_from(mip_node_t& start_node, const std::vector& start_lower, const std::vector& start_upper, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_factors, - std::vector& basic_list, - std::vector& nonbasic_list, bnb_thread_type_t diving_type) { logger_t log; log.log = false; - bool recompute_bounds_and_basis = true; + i_t tid = omp_get_thread_num(); + bnb_worker_data_t* worker_data = get_worker_data(tid); + assert(worker_data); + + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; + search_tree_t subtree(std::move(start_node)); std::deque*> stack; stack.push_front(&subtree.root); @@ -1146,28 +1143,19 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node_ptr->lower_bound); if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { - recompute_bounds_and_basis = true; + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; continue; } if (toc(exploration_stats_.start_time) > settings_.time_limit) { break; } if (dive_stats.nodes_explored > settings_.diving_settings.node_limit) { break; } - node_solve_info_t status = solve_node(node_ptr, - subtree, - leaf_problem, - basis_factors, - basic_list, - nonbasic_list, - node_presolver, - diving_type, - recompute_bounds_and_basis, - start_lower, - start_upper, - dive_stats, - log); + node_solve_info_t status = solve_node( + node_ptr, subtree, diving_type, worker_data, start_lower, start_upper, dive_stats, log); dive_stats.nodes_explored++; - recompute_bounds_and_basis = !has_children(status); + worker_data->recompute_basis = !has_children(status); + worker_data->recompute_bounds = !has_children(status); if (status == node_solve_info_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; @@ -1196,18 +1184,13 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, template void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type) { - // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp_; - std::vector row_sense; - bounds_strengthening_t node_presolver(leaf_problem, Arow_, row_sense, var_types_); - - const i_t m = leaf_problem.num_rows; - basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); - std::vector basic_list(m); - std::vector nonbasic_list; + i_t tid = omp_get_thread_num(); + bnb_worker_data_t* worker_data = get_worker_data(tid); + bounds_strengthening_t& node_presolver = worker_data->node_presolver; std::vector start_lower; std::vector start_upper; + std::vector bounds_changed(original_lp_.num_cols, false); bool reset_starting_bounds = true; while (solver_status_ == mip_exploration_status_t::RUNNING && @@ -1215,28 +1198,21 @@ void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type) if (reset_starting_bounds) { start_lower = original_lp_.lower; start_upper = original_lp_.upper; - std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); + std::fill(bounds_changed.begin(), bounds_changed.end(), false); reset_starting_bounds = false; } std::optional> start_node = - node_queue.pop_diving(start_lower, start_upper, node_presolver.bounds_changed); + node_queue.pop_diving(start_lower, start_upper, bounds_changed); if (start_node.has_value()) { reset_starting_bounds = true; - bool is_feasible = node_presolver.bounds_strengthening(start_lower, start_upper, settings_); + bool is_feasible = + node_presolver.bounds_strengthening(start_lower, start_upper, bounds_changed, settings_); if (get_upper_bound() < start_node->lower_bound || !is_feasible) { continue; } - dive_from(start_node.value(), - start_lower, - start_upper, - leaf_problem, - node_presolver, - basis_factors, - basic_list, - nonbasic_list, - diving_type); + dive_from(start_node.value(), start_lower, start_upper, diving_type); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index b30682bdb..19fa2ee77 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -85,6 +85,32 @@ struct bnb_stats_t { omp_atomic_t nodes_since_last_log = 0; }; +template +struct bnb_worker_data_t { + lp_problem_t leaf_problem; + basis_update_mpf_t basis_factors; + std::vector basic_list; + std::vector nonbasic_list; + bounds_strengthening_t node_presolver; + std::vector bounds_changed; + + bool recompute_basis = true; + bool recompute_bounds = true; + + bnb_worker_data_t(const lp_problem_t& original_lp, + const csr_matrix_t& Arow, + const std::vector& var_type, + const simplex_solver_settings_t& settings) + : leaf_problem(original_lp), + basis_factors(original_lp.num_rows, settings.refactor_frequency), + basic_list(original_lp.num_rows), + nonbasic_list(), + node_presolver(leaf_problem, Arow, {}, var_type), + bounds_changed(original_lp.num_cols, false) + { + } +}; + template class branch_and_bound_t { public: @@ -200,6 +226,11 @@ class branch_and_bound_t { void report_heuristic(f_t obj); void report(std::string symbol, f_t obj, f_t lower_bound, i_t node_depth); + // Persistent data private for each individual worker. + std::unordered_map>> persistent_worker_data_; + omp_mutex_t mutex_worker_data_; + bnb_worker_data_t* get_worker_data(i_t tid); + // Set the final solution. mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); @@ -217,15 +248,7 @@ class branch_and_bound_t { // there is enough unexplored nodes. This is done recursively using OpenMP tasks. void exploration_ramp_up(mip_node_t* node, i_t initial_heap_size); - // Perform a plunge in the subtree determined by the `start_node`. - void plunge_from(i_t task_id, - mip_node_t* start_node, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_update, - std::vector& basic_list, - std::vector& nonbasic_list); + void plunge_from(i_t task_id, mip_node_t* start_node); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. @@ -235,27 +258,24 @@ class branch_and_bound_t { void dive_from(mip_node_t& start_node, const std::vector& start_lower, const std::vector& start_upper, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_update, - std::vector& basic_list, - std::vector& nonbasic_list, bnb_thread_type_t diving_type); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. void diving_thread(bnb_thread_type_t diving_type); + // Set the bounds of the leaf node and then apply bounds propagation. + // Return true if the problem is feasible, false otherwise. + bool set_node_bounds(mip_node_t* node_ptr, + const std::vector& start_lower, + const std::vector& start_upper, + bnb_worker_data_t* worker_data); + // Solve the LP relaxation of a leaf node and update the tree. node_solve_info_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, - lp_problem_t& leaf_problem, - basis_update_mpf_t& basis_factors, - std::vector& basic_list, - std::vector& nonbasic_list, - bounds_strengthening_t& node_presolver, bnb_thread_type_t thread_type, - bool recompute_basis_and_bounds, + bnb_worker_data_t* worker_data, const std::vector& root_lower, const std::vector& root_upper, bnb_stats_t& stats, diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index d247fbf67..56b89f884 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -621,7 +621,7 @@ void convert_user_problem(const user_problem_t& user_problem, } constexpr bool run_bounds_strengthening = false; - if (run_bounds_strengthening) { + if constexpr (run_bounds_strengthening) { csr_matrix_t Arow(1, 1, 1); problem.A.to_compressed_row(Arow); @@ -629,8 +629,8 @@ void convert_user_problem(const user_problem_t& user_problem, // Empty var_types means that all variables are continuous bounds_strengthening_t strengthening(problem, Arow, row_sense, {}); - std::fill(strengthening.bounds_changed.begin(), strengthening.bounds_changed.end(), true); - strengthening.bounds_strengthening(problem.lower, problem.upper, settings); + std::vector bounds_changed(problem.num_cols, true); + strengthening.bounds_strengthening(problem.lower, problem.upper, bounds_changed, settings); } settings.log.debug( From d076da2e3d6765e06632d9a73cb831e490d568d5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 16 Dec 2025 15:35:36 +0100 Subject: [PATCH 35/70] replace hashmap with a vector --- cpp/src/dual_simplex/branch_and_bound.cpp | 7 +++---- cpp/src/dual_simplex/branch_and_bound.hpp | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index a23f5c172..9204f6633 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -307,13 +307,12 @@ void branch_and_bound_t::report(std::string symbol, template bnb_worker_data_t* branch_and_bound_t::get_worker_data(i_t tid) { - std::lock_guard lock(mutex_worker_data_); - if (persistent_worker_data_.find(tid) == persistent_worker_data_.end()) { - persistent_worker_data_[tid] = + if (!worker_data_pool_[tid].get()) { + worker_data_pool_[tid] = std::make_unique>(original_lp_, Arow_, var_types_, settings_); } - return persistent_worker_data_[tid].get(); + return worker_data_pool_[tid].get(); } template diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 19fa2ee77..baa7107be 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -226,9 +226,10 @@ class branch_and_bound_t { void report_heuristic(f_t obj); void report(std::string symbol, f_t obj, f_t lower_bound, i_t node_depth); - // Persistent data private for each individual worker. - std::unordered_map>> persistent_worker_data_; - omp_mutex_t mutex_worker_data_; + + // A pool containing the data needed for a worker to perform a plunge or dive. + // This is lazily initialized via `get_worker_data()`. + std::vector>> worker_data_pool_; bnb_worker_data_t* get_worker_data(i_t tid); // Set the final solution. From d8e3541c9e8dfb0e804653e2b9b6ce1417163dc2 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 16 Dec 2025 16:15:22 +0100 Subject: [PATCH 36/70] fix missing initialization --- cpp/src/dual_simplex/branch_and_bound.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9204f6633..9f293a72b 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1468,6 +1468,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_vstatus_, original_lp_, log); + worker_data_pool_.resize(settings_.num_threads); settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, From 4796733c6b84cfd992a61d099fb22a35010b3703 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 17 Dec 2025 17:08:23 +0100 Subject: [PATCH 37/70] added a setting for each type of task. changed how to set the number of tasks per type --- cpp/src/dual_simplex/branch_and_bound.cpp | 106 ++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 21 +--- cpp/src/dual_simplex/diving_heuristics.cpp | 15 +++ cpp/src/dual_simplex/diving_heuristics.hpp | 3 + .../dual_simplex/simplex_solver_settings.hpp | 105 ++++++++++++++--- cpp/src/mip/diversity/lns/rins.cu | 17 +-- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 20 ++-- cpp/src/mip/solver.cu | 15 +-- 8 files changed, 174 insertions(+), 128 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9f293a72b..ce29a86cb 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -192,14 +192,14 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) } } -inline const char* feasible_solution_symbol(bnb_thread_type_t type) +inline const char* feasible_solution_symbol(bnb_task_type_t type) { switch (type) { - case bnb_thread_type_t::EXPLORATION: return "B "; - case bnb_thread_type_t::COEFFICIENT_DIVING: return "CD"; - case bnb_thread_type_t::LINE_SEARCH_DIVING: return "LD"; - case bnb_thread_type_t::PSEUDOCOST_DIVING: return "PD"; - case bnb_thread_type_t::GUIDED_DIVING: return "GD"; + case bnb_task_type_t::EXPLORATION: return "B "; + case bnb_task_type_t::COEFFICIENT_DIVING: return "CD"; + case bnb_task_type_t::LINE_SEARCH_DIVING: return "LD"; + case bnb_task_type_t::PSEUDOCOST_DIVING: return "PD"; + case bnb_task_type_t::GUIDED_DIVING: return "GD"; default: return "U "; } } @@ -544,7 +544,7 @@ template void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - bnb_thread_type_t thread_type) + bnb_task_type_t thread_type) { bool send_solution = false; @@ -594,7 +594,7 @@ branch_variable_t branch_and_bound_t::variable_selection( mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_thread_type_t type, + bnb_task_type_t type, logger_t& log) { i_t branch_var = -1; @@ -602,7 +602,7 @@ branch_variable_t branch_and_bound_t::variable_selection( rounding_direction_t round_dir = rounding_direction_t::NONE; switch (type) { - case bnb_thread_type_t::EXPLORATION: + case bnb_task_type_t::EXPLORATION: std::tie(branch_var, obj_estimate) = pc_.variable_selection_and_obj_estimate(fractional, solution, node_ptr->lower_bound, log); round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); @@ -613,16 +613,16 @@ branch_variable_t branch_and_bound_t::variable_selection( node_ptr->objective_estimate = obj_estimate; return {branch_var, round_dir}; - case bnb_thread_type_t::COEFFICIENT_DIVING: + case bnb_task_type_t::COEFFICIENT_DIVING: return coefficient_diving(original_lp_, fractional, solution, log); - case bnb_thread_type_t::LINE_SEARCH_DIVING: + case bnb_task_type_t::LINE_SEARCH_DIVING: return line_search_diving(fractional, solution, root_relax_soln_.x, log); - case bnb_thread_type_t::PSEUDOCOST_DIVING: + case bnb_task_type_t::PSEUDOCOST_DIVING: return pseudocost_diving(pc_, fractional, solution, root_relax_soln_.x, log); - case bnb_thread_type_t::GUIDED_DIVING: + case bnb_task_type_t::GUIDED_DIVING: return guided_diving(pc_, fractional, solution, incumbent_.x, log); default: @@ -666,7 +666,7 @@ bool branch_and_bound_t::set_node_bounds(mip_node_t* node_pt template node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, - bnb_thread_type_t thread_type, + bnb_task_type_t thread_type, bnb_worker_data_t* worker_data, const std::vector& root_lower, const std::vector& root_upper, @@ -677,11 +677,11 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* const f_t upper_bound = get_upper_bound(); // If there is no incumbent, use pseudocost diving instead of guided diving - if (upper_bound == inf && thread_type == bnb_thread_type_t::GUIDED_DIVING) { + if (upper_bound == inf && thread_type == bnb_task_type_t::GUIDED_DIVING) { if (settings_.diving_settings.disable_pseudocost_diving) { thread_type = bnb_thread_type_t::COEFFICIENT_DIVING; } else { - thread_type = bnb_thread_type_t::PSEUDOCOST_DIVING; + thread_type = bnb_task_type_t::PSEUDOCOST_DIVING; } } @@ -697,9 +697,10 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); lp_settings.scale_columns = false; - if (thread_type != bnb_thread_type_t::EXPLORATION) { + if (thread_type != bnb_task_type_t::EXPLORATION) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; - f_t max_iter = settings_.diving_settings.iteration_limit_factor * bnb_lp_iters; + f_t factor = settings_.bnb_task_settings[thread_type].iteration_limit_factor; + f_t max_iter = factor * bnb_lp_iters; lp_settings.iteration_limit = max_iter - stats.total_lp_iters; if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } } @@ -799,7 +800,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* search_tree.graphviz_node(log, node_ptr, "lower bound", leaf_objective); pc_.update_pseudo_costs(node_ptr, leaf_objective); - if (thread_type == bnb_thread_type_t::EXPLORATION) { + if (thread_type == bnb_task_type_t::EXPLORATION) { if (settings_.node_processed_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, leaf_solution.x, original_x); @@ -844,7 +845,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* return node_solve_info_t::ITERATION_LIMIT; } else { - if (thread_type == bnb_thread_type_t::EXPLORATION) { + if (thread_type == bnb_task_type_t::EXPLORATION) { fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); log.printf( "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " @@ -916,7 +917,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod node_solve_info_t status = solve_node(node, search_tree_, - bnb_thread_type_t::EXPLORATION, + bnb_task_type_t::EXPLORATION, worker_data, original_lp_.lower, original_lp_.upper, @@ -1019,7 +1020,7 @@ void branch_and_bound_t::plunge_from(i_t task_id, mip_node_t node_solve_info_t status = solve_node(node_ptr, search_tree_, - bnb_thread_type_t::EXPLORATION, + bnb_task_type_t::EXPLORATION, worker_data, original_lp_.lower, original_lp_.upper, @@ -1113,11 +1114,14 @@ template void branch_and_bound_t::dive_from(mip_node_t& start_node, const std::vector& start_lower, const std::vector& start_upper, - bnb_thread_type_t diving_type) + bnb_task_type_t diving_type) { logger_t log; log.log = false; + const i_t node_limit = settings_.bnb_task_settings[diving_type].node_limit; + const i_t backtrack = settings_.bnb_task_settings[diving_type].backtrack; + i_t tid = omp_get_thread_num(); bnb_worker_data_t* worker_data = get_worker_data(tid); assert(worker_data); @@ -1148,7 +1152,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, } if (toc(exploration_stats_.start_time) > settings_.time_limit) { break; } - if (dive_stats.nodes_explored > settings_.diving_settings.node_limit) { break; } + if (dive_stats.nodes_explored > node_limit) { break; } node_solve_info_t status = solve_node( node_ptr, subtree, diving_type, worker_data, start_lower, start_upper, dive_stats, log); @@ -1173,15 +1177,14 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, } } - if (stack.size() > 1 && - stack.front()->depth - stack.back()->depth > settings_.diving_settings.backtrack) { + if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > backtrack) { stack.pop_back(); } } } template -void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type) +void branch_and_bound_t::diving_thread(bnb_task_type_t diving_type) { i_t tid = omp_get_thread_num(); bnb_worker_data_t* worker_data = get_worker_data(tid); @@ -1302,29 +1305,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); - std::vector diving_strategies; - diving_strategies.reserve(4); - - if (!settings_.diving_settings.disable_pseudocost_diving) { - diving_strategies.push_back(bnb_thread_type_t::PSEUDOCOST_DIVING); - } - - if (!settings_.diving_settings.disable_line_search_diving) { - diving_strategies.push_back(bnb_thread_type_t::LINE_SEARCH_DIVING); - } - - if (!settings_.diving_settings.disable_guided_diving) { - diving_strategies.push_back(bnb_thread_type_t::GUIDED_DIVING); - } - - if (!settings_.diving_settings.disable_coefficient_diving) { - diving_strategies.push_back(bnb_thread_type_t::COEFFICIENT_DIVING); - } - - if (diving_strategies.empty()) { - settings_.log.printf("Warning: All diving heuristics are disabled!"); - } - if (guess_.size() != 0) { std::vector crushed_guess; crush_primal_solution(original_problem_, original_lp_, guess_, new_slacks_, crushed_guess); @@ -1393,7 +1373,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut set_uninitialized_steepest_edge_norms(edge_norms_); root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); - local_lower_bounds_.assign(settings_.num_bfs_threads, root_objective_); + + i_t num_bfs_threads = settings_.bnb_task_settings[EXPLORATION].num_tasks; + local_lower_bounds_.assign(num_bfs_threads, root_objective_); if (settings_.set_simplex_solution_callback != nullptr) { std::vector original_x; @@ -1472,8 +1454,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, - settings_.num_bfs_threads, - settings_.diving_settings.num_diving_tasks); + num_bfs_threads, + settings_.num_threads - num_bfs_threads); settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " @@ -1492,10 +1474,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut { #pragma omp master { - auto down_child = search_tree_.root.get_down_child(); - auto up_child = search_tree_.root.get_up_child(); - i_t initial_size = 2 * settings_.num_threads; - const i_t num_strategies = diving_strategies.size(); + auto down_child = search_tree_.root.get_down_child(); + auto up_child = search_tree_.root.get_up_child(); + i_t initial_size = 2 * settings_.num_threads; #pragma omp taskgroup { @@ -1506,16 +1487,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_ramp_up(up_child, initial_size); } - for (i_t i = 0; i < settings_.num_bfs_threads; i++) { + for (i_t i = 0; i < num_bfs_threads; i++) { #pragma omp task best_first_thread(i); } - if (!diving_strategies.empty()) { - for (i_t k = 0; k < settings_.diving_settings.num_diving_tasks; k++) { - const bnb_thread_type_t diving_type = diving_strategies[k % num_strategies]; + for (auto& settings : settings_.bnb_task_settings) { + if (settings.type != EXPLORATION && settings.is_enabled) { + for (i_t k = 0; k < settings.num_tasks; k++) { #pragma omp task - diving_thread(diving_type); + diving_thread(settings.type); + } } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index baa7107be..e6e225b73 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -53,19 +53,6 @@ enum class node_solve_info_t { NUMERICAL = 5 // The solver encounter a numerical error when solving the node }; -// Indicate the search and variable selection algorithms used by each thread -// in B&B (See [1]). -// -// [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, -// Berlin, 2007. doi: 10.14279/depositonce-1634. -enum class bnb_thread_type_t { - EXPLORATION = 0, // Best-First + Plunging. - PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) - LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) - GUIDED_DIVING = 3, // Guided diving (9.2.3). If no incumbent is found yet, use pseudocost diving. - COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1) -}; - template class bounds_strengthening_t; @@ -240,7 +227,7 @@ class branch_and_bound_t { void add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - bnb_thread_type_t thread_type); + bnb_task_type_t thread_type); // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); @@ -259,7 +246,7 @@ class branch_and_bound_t { void dive_from(mip_node_t& start_node, const std::vector& start_lower, const std::vector& start_upper, - bnb_thread_type_t diving_type); + bnb_task_type_t diving_type); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. @@ -275,7 +262,7 @@ class branch_and_bound_t { // Solve the LP relaxation of a leaf node and update the tree. node_solve_info_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, - bnb_thread_type_t thread_type, + bnb_task_type_t thread_type, bnb_worker_data_t* worker_data, const std::vector& root_lower, const std::vector& root_upper, @@ -286,7 +273,7 @@ class branch_and_bound_t { branch_variable_t variable_selection(mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_thread_type_t type, + bnb_task_type_t type, logger_t& log); }; diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 978a97e42..94464d3cf 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -9,6 +9,18 @@ namespace cuopt::linear_programming::dual_simplex { +template +bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type) +{ + return bnb_task_settings_t{.type = type, + .is_enabled = true, + .num_tasks = -1, + .min_node_depth = 0, + .node_limit = 500, + .iteration_limit_factor = 0.05, + .backtrack = 5}; +} + template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, @@ -273,6 +285,9 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE + +template bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type); + template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, const std::vector& root_solution, diff --git a/cpp/src/dual_simplex/diving_heuristics.hpp b/cpp/src/dual_simplex/diving_heuristics.hpp index c7b1e2050..422db1a68 100644 --- a/cpp/src/dual_simplex/diving_heuristics.hpp +++ b/cpp/src/dual_simplex/diving_heuristics.hpp @@ -20,6 +20,9 @@ struct branch_variable_t { rounding_direction_t direction; }; +template +bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type); + template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 60b92ee33..e791c5da1 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -13,26 +13,58 @@ #include #include #include +#include #include #include #include namespace cuopt::linear_programming::dual_simplex { +// Indicate the search and variable selection algorithms used by each task +// in B&B (See [1]). +// +// [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, +// Berlin, 2007. doi: 10.14279/depositonce-1634. +enum bnb_task_type_t { + EXPLORATION = 0, // Best-First + Plunging. + PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) + LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) + GUIDED_DIVING = 3, // Guided diving (9.2.3). If no incumbent is found yet, use pseudocost diving. + COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1) +}; + +// Settings for each task type in B&B. template -struct diving_heuristics_settings_t { - i_t num_diving_tasks = -1; +struct bnb_task_settings_t { + // Type of the task. + bnb_task_type_t type; + + // Is this type of task enabled? + // This will be ignored if `type == EXPLORATION`. + bool is_enabled; + + // Number of tasks of this type. + i_t num_tasks; + + // Minimum node depth to start this task + // This will be ignored if `type == EXPLORATION`. + i_t min_node_depth; + + // Maximum number of nodes explored in this task. + i_t node_limit; - bool disable_line_search_diving = false; - bool disable_pseudocost_diving = false; - bool disable_guided_diving = false; - bool disable_coefficient_diving = false; + // Maximum fraction of the number of simplex iterations for this task + // compared to the number of simplex iterations for normal exploration. + f_t iteration_limit_factor; - i_t node_limit = 500; - f_t iteration_limit_factor = 0.05; - i_t backtrack = 5; + // Number of nodes that it allows to backtrack when + // reaching the bottom of a given branch of the tree. + i_t backtrack; }; +template +bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type); + template struct simplex_solver_settings_t { public: @@ -83,21 +115,65 @@ struct simplex_solver_settings_t { refactor_frequency(100), iteration_log_frequency(1000), first_iteration_log(2), - num_threads(omp_get_max_threads() - 1), - num_bfs_threads(std::min(num_threads / 4, 1)), random_seed(0), inside_mip(0), solution_callback(nullptr), heuristic_preemption_callback(nullptr), concurrent_halt(nullptr) { - diving_settings.num_diving_tasks = std::max(num_threads - num_bfs_threads, 1); + bnb_task_settings[EXPLORATION] = + bnb_task_settings_t{.type = EXPLORATION, + .is_enabled = true, + .num_tasks = -1, + .min_node_depth = 0, + .node_limit = std::numeric_limits::max(), + .iteration_limit_factor = std::numeric_limits::max(), + .backtrack = 1}; + + bnb_task_settings[PSEUDOCOST_DIVING] = get_default_diving_settings(PSEUDOCOST_DIVING); + + bnb_task_settings[LINE_SEARCH_DIVING] = + get_default_diving_settings(LINE_SEARCH_DIVING); + + bnb_task_settings[GUIDED_DIVING] = get_default_diving_settings(GUIDED_DIVING); + + bnb_task_settings[COEFFICIENT_DIVING] = + get_default_diving_settings(COEFFICIENT_DIVING); + + set_bnb_tasks(omp_get_max_threads() - 1); + } + + void set_bnb_tasks(i_t num_threads) + { + this->num_threads = num_threads; + bnb_task_settings[EXPLORATION].num_tasks = std::max(1, num_threads / 4); + + i_t diving_tasks = num_threads - bnb_task_settings[EXPLORATION].num_tasks; + i_t num_enabled = 0; + + for (size_t i = 1; i < bnb_task_settings.size(); ++i) { + num_enabled += static_cast(bnb_task_settings[i].is_enabled); + } + + for (size_t i = 1, k = 0; i < bnb_task_settings.size(); ++i) { + i_t start = (double)k * diving_tasks / num_enabled; + i_t end = (double)(k + 1) * diving_tasks / num_enabled; + + if (bnb_task_settings[i].is_enabled) { + bnb_task_settings[i].num_tasks = end - start; + ++k; + + } else { + bnb_task_settings[i].num_tasks = 0; + } + } } void set_log(bool logging) const { log.log = logging; } void enable_log_to_file() { log.enable_log_to_file(); } void set_log_filename(const std::string& log_filename) { log.set_log_file(log_filename); } void close_log_file() { log.close_log_file(); } + i_t iteration_limit; i_t node_limit; f_t time_limit; @@ -151,9 +227,10 @@ struct simplex_solver_settings_t { i_t first_iteration_log; // number of iterations to log at beginning of solve i_t num_threads; // number of threads to use i_t random_seed; // random seed - i_t num_bfs_threads; // number of threads dedicated to the best-first search - diving_heuristics_settings_t diving_settings; // Settings for the diving heuristics + // Indicate the settings used by each task + // The position in the array is indicated by the `bnb_task_type_t`. + std::array, 5> bnb_task_settings; i_t inside_mip; // 0 if outside MIP, 1 if inside MIP at root node, 2 if inside MIP at leaf node std::function&, f_t)> solution_callback; diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 1893e84fe..4442029e8 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -256,21 +256,14 @@ void rins_t::run_rins() branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = 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 = 2; - branch_and_bound_settings.num_bfs_threads = 1; + branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; // In the future, let RINS use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.diving_settings.num_diving_tasks = 1; - branch_and_bound_settings.diving_settings.disable_line_search_diving = true; - branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; - - if (context.settings.disable_guided_diving) { - branch_and_bound_settings.diving_settings.disable_guided_diving = true; - } else { - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; - } + branch_and_bound_settings.bnb_task_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = false; + branch_and_bound_settings.bnb_task_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = false; + branch_and_bound_settings.bnb_task_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = false; + branch_and_bound_settings.set_bnb_tasks(2); branch_and_bound_settings.log.log = false; branch_and_bound_settings.log.log_prefix = "[RINS] "; diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 2335003b6..8501b70cc 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -101,21 +101,17 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.print_presolve_stats = false; branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; 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 = 2; - branch_and_bound_settings.num_bfs_threads = 1; + branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; // In the future, let SubMIP use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.diving_settings.num_diving_tasks = 1; - branch_and_bound_settings.diving_settings.disable_line_search_diving = true; - branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; - - if (context.settings.disable_guided_diving) { - branch_and_bound_settings.diving_settings.disable_guided_diving = true; - } else { - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; - } + branch_and_bound_settings.bnb_task_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = + false; + branch_and_bound_settings.bnb_task_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = + false; + branch_and_bound_settings.bnb_task_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = + false; + branch_and_bound_settings.set_bnb_tasks(2); branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 790831129..a77ede1a2 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -176,17 +176,10 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.diving_settings.disable_line_search_diving = context.settings.disable_line_search_diving; - if (context.settings.num_cpu_threads < 0) { - branch_and_bound_settings.num_threads = omp_get_max_threads() - 1; - } else { - branch_and_bound_settings.num_threads = std::max(1, context.settings.num_cpu_threads); - } - - i_t num_threads = branch_and_bound_settings.num_threads; - i_t num_bfs_threads = std::max(1, num_threads / 4); - i_t num_diving_threads = std::max(1, num_threads - num_bfs_threads); - branch_and_bound_settings.num_bfs_threads = num_bfs_threads; - branch_and_bound_settings.diving_settings.num_diving_tasks = num_diving_threads; + i_t num_threads = context.settings.num_cpu_threads < 0 + ? omp_get_max_threads() - 1 + : std::max(1, context.settings.num_cpu_threads); + branch_and_bound_settings.set_bnb_tasks(num_threads); // Set the branch and bound -> primal heuristics callback branch_and_bound_settings.solution_callback = From 308237e35d30b0ab1e6336852c4f60799698e0e0 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 19 Dec 2025 17:04:37 +0100 Subject: [PATCH 38/70] first version of the new parallel b&b --- cpp/src/dual_simplex/CMakeLists.txt | 1 + cpp/src/dual_simplex/bnb_worker.cpp | 81 +++ cpp/src/dual_simplex/bnb_worker.hpp | 90 +++ cpp/src/dual_simplex/branch_and_bound.cpp | 661 +++++++++--------- cpp/src/dual_simplex/branch_and_bound.hpp | 95 +-- cpp/src/dual_simplex/diving_heuristics.cpp | 18 +- cpp/src/dual_simplex/diving_heuristics.hpp | 2 +- cpp/src/dual_simplex/node_queue.hpp | 32 +- .../dual_simplex/simplex_solver_settings.hpp | 77 +- cpp/src/mip/diversity/lns/rins.cu | 8 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 6 +- 11 files changed, 574 insertions(+), 497 deletions(-) create mode 100644 cpp/src/dual_simplex/bnb_worker.cpp create mode 100644 cpp/src/dual_simplex/bnb_worker.hpp diff --git a/cpp/src/dual_simplex/CMakeLists.txt b/cpp/src/dual_simplex/CMakeLists.txt index ebaf9cbb7..db1fa94dc 100644 --- a/cpp/src/dual_simplex/CMakeLists.txt +++ b/cpp/src/dual_simplex/CMakeLists.txt @@ -10,6 +10,7 @@ set(DUAL_SIMPLEX_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/basis_updates.cpp ${CMAKE_CURRENT_SOURCE_DIR}/bound_flipping_ratio_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/branch_and_bound.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/bnb_worker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/crossover.cpp ${CMAKE_CURRENT_SOURCE_DIR}/folding.cpp ${CMAKE_CURRENT_SOURCE_DIR}/initial_basis.cpp diff --git a/cpp/src/dual_simplex/bnb_worker.cpp b/cpp/src/dual_simplex/bnb_worker.cpp new file mode 100644 index 000000000..b4a1d8583 --- /dev/null +++ b/cpp/src/dual_simplex/bnb_worker.cpp @@ -0,0 +1,81 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +bnb_worker_t::bnb_worker_t(i_t worker_id, + const lp_problem_t& original_lp, + const csr_matrix_t& Arow, + const std::vector& var_type, + const simplex_solver_settings_t& settings) + : worker_id(worker_id), + worker_type(EXPLORATION), + is_active(false), + lower_bound(-std::numeric_limits::infinity()), + leaf_problem(original_lp), + basis_factors(original_lp.num_rows, settings.refactor_frequency), + basic_list(original_lp.num_rows), + nonbasic_list(), + node_presolver(leaf_problem, Arow, {}, var_type), + bounds_changed(original_lp.num_cols, false) +{ +} + +template +bool bnb_worker_t::init_diving(mip_node_t* node, + bnb_worker_type_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; + worker_type = type; + lower_bound = node->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(start_lower, start_upper, bounds_changed, settings); +} + +template +bool bnb_worker_t::set_lp_variable_bounds_for( + mip_node_t* node_ptr, const simplex_solver_settings_t& settings) +{ + // 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); + + } else { + node_ptr->update_branched_variable_bounds( + leaf_problem.lower, leaf_problem.upper, bounds_changed); + } + + return node_presolver.bounds_strengthening( + leaf_problem.lower, leaf_problem.upper, bounds_changed, settings); +} + +#ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE +template class bnb_worker_t; +#endif + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp new file mode 100644 index 000000000..34eeb38c8 --- /dev/null +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -0,0 +1,90 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +struct bnb_stats_t { + f_t start_time = 0.0; + omp_atomic_t total_lp_solve_time = 0.0; + omp_atomic_t nodes_explored = 0; + omp_atomic_t nodes_unexplored = 0; + omp_atomic_t total_lp_iters = 0; +}; + +template +class bnb_worker_t { + public: + const i_t worker_id; + omp_atomic_t worker_type; + omp_atomic_t is_active; + omp_atomic_t lower_bound; + + lp_problem_t leaf_problem; + + basis_update_mpf_t basis_factors; + std::vector basic_list; + std::vector nonbasic_list; + + bounds_strengthening_t node_presolver; + std::vector bounds_changed; + + std::vector start_lower; + std::vector start_upper; + mip_node_t* start_node; + + bool recompute_basis = true; + bool recompute_bounds = true; + + bnb_worker_t(i_t worker_id, + const lp_problem_t& original_lp, + const csr_matrix_t& Arow, + const std::vector& var_type, + const simplex_solver_settings_t& settings); + + // Set the `start_node` for best-first search. + void init_best_first(mip_node_t* node, const lp_problem_t& original_lp) + { + start_node = node; + start_lower = original_lp.lower; + start_upper = original_lp.upper; + worker_type = EXPLORATION; + lower_bound = node->lower_bound; + is_active = true; + } + + // 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, + bnb_worker_type_t type, + const lp_problem_t& original_lp, + const simplex_solver_settings_t& settings); + + // Set the variables bounds for the LP relaxation of the current node. + bool set_lp_variable_bounds_for(mip_node_t* node_ptr, + const simplex_solver_settings_t& settings); + + private: + // For diving, we need to store the full node instead of + // of just a pointer, since it is detached from the + // tree. To keep the same interface for any type of worker, + // the start node will point to this node when diving. + // For best-first search, this will not be used. + mip_node_t internal_node; +}; + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index ce29a86cb..a2966fb26 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -192,14 +192,14 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) } } -inline const char* feasible_solution_symbol(bnb_task_type_t type) +inline const char* feasible_solution_symbol(bnb_worker_type_t type) { switch (type) { - case bnb_task_type_t::EXPLORATION: return "B "; - case bnb_task_type_t::COEFFICIENT_DIVING: return "CD"; - case bnb_task_type_t::LINE_SEARCH_DIVING: return "LD"; - case bnb_task_type_t::PSEUDOCOST_DIVING: return "PD"; - case bnb_task_type_t::GUIDED_DIVING: return "GD"; + case bnb_worker_type_t::EXPLORATION: return "B "; + case bnb_worker_type_t::COEFFICIENT_DIVING: return "CD"; + case bnb_worker_type_t::LINE_SEARCH_DIVING: return "LD"; + case bnb_worker_type_t::PSEUDOCOST_DIVING: return "PD"; + case bnb_worker_type_t::GUIDED_DIVING: return "GD"; default: return "U "; } } @@ -252,8 +252,10 @@ f_t branch_and_bound_t::get_lower_bound() f_t heap_lower_bound = node_queue.get_lower_bound(); lower_bound = std::min(heap_lower_bound, lower_bound); - for (i_t i = 0; i < local_lower_bounds_.size(); ++i) { - lower_bound = std::min(local_lower_bounds_[i].load(), lower_bound); + for (i_t i = 0; i < workers_.size(); ++i) { + if (workers_[i]->worker_type == EXPLORATION && workers_[i]->is_active) { + lower_bound = std::min(workers_[i]->lower_bound.load(), lower_bound); + } } return std::isfinite(lower_bound) ? lower_bound : -inf; @@ -304,17 +306,6 @@ void branch_and_bound_t::report(std::string symbol, toc(exploration_stats_.start_time)); } -template -bnb_worker_data_t* branch_and_bound_t::get_worker_data(i_t tid) -{ - if (!worker_data_pool_[tid].get()) { - worker_data_pool_[tid] = - std::make_unique>(original_lp_, Arow_, var_types_, settings_); - } - - return worker_data_pool_[tid].get(); -} - template void branch_and_bound_t::set_new_solution(const std::vector& solution) { @@ -544,7 +535,7 @@ template void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - bnb_task_type_t thread_type) + bnb_worker_type_t thread_type) { bool send_solution = false; @@ -594,7 +585,7 @@ branch_variable_t branch_and_bound_t::variable_selection( mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_task_type_t type, + bnb_worker_type_t type, logger_t& log) { i_t branch_var = -1; @@ -602,7 +593,7 @@ branch_variable_t branch_and_bound_t::variable_selection( rounding_direction_t round_dir = rounding_direction_t::NONE; switch (type) { - case bnb_task_type_t::EXPLORATION: + case bnb_worker_type_t::EXPLORATION: std::tie(branch_var, obj_estimate) = pc_.variable_selection_and_obj_estimate(fractional, solution, node_ptr->lower_bound, log); round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); @@ -613,16 +604,16 @@ branch_variable_t branch_and_bound_t::variable_selection( node_ptr->objective_estimate = obj_estimate; return {branch_var, round_dir}; - case bnb_task_type_t::COEFFICIENT_DIVING: + case bnb_worker_type_t::COEFFICIENT_DIVING: return coefficient_diving(original_lp_, fractional, solution, log); - case bnb_task_type_t::LINE_SEARCH_DIVING: + case bnb_worker_type_t::LINE_SEARCH_DIVING: return line_search_diving(fractional, solution, root_relax_soln_.x, log); - case bnb_task_type_t::PSEUDOCOST_DIVING: + case bnb_worker_type_t::PSEUDOCOST_DIVING: return pseudocost_diving(pc_, fractional, solution, root_relax_soln_.x, log); - case bnb_task_type_t::GUIDED_DIVING: + case bnb_worker_type_t::GUIDED_DIVING: return guided_diving(pc_, fractional, solution, incumbent_.x, log); default: @@ -631,45 +622,11 @@ branch_variable_t branch_and_bound_t::variable_selection( } } -template -bool branch_and_bound_t::set_node_bounds(mip_node_t* node_ptr, - const std::vector& start_lower, - const std::vector& start_upper, - bnb_worker_data_t* worker_data) -{ - bounds_strengthening_t& node_presolver = worker_data->node_presolver; - std::vector& upper = worker_data->leaf_problem.upper; - std::vector& lower = worker_data->leaf_problem.lower; - std::vector& bounds_changed = worker_data->bounds_changed; - - assert(bounds_changed.size() == original_lp_.num_cols); - assert(lower.size() == original_lp_.num_cols); - assert(upper.size() == original_lp_.num_cols); - - // Reset the bound_changed markers - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - - // Set the correct bounds for the leaf problem - if (worker_data->recompute_bounds) { - lower = start_lower; - upper = start_upper; - node_ptr->get_variable_bounds(lower, upper, bounds_changed); - - } else { - node_ptr->update_branched_variable_bounds(lower, upper, bounds_changed); - } - - bool feasible = node_presolver.bounds_strengthening(lower, upper, bounds_changed, settings_); - return feasible; -} - template node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, - bnb_task_type_t thread_type, - bnb_worker_data_t* worker_data, - const std::vector& root_lower, - const std::vector& root_upper, + bnb_worker_type_t thread_type, + bnb_worker_t* worker, bnb_stats_t& stats, logger_t& log) { @@ -677,15 +634,15 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* const f_t upper_bound = get_upper_bound(); // If there is no incumbent, use pseudocost diving instead of guided diving - if (upper_bound == inf && thread_type == bnb_task_type_t::GUIDED_DIVING) { + if (upper_bound == inf && thread_type == bnb_worker_type_t::GUIDED_DIVING) { if (settings_.diving_settings.disable_pseudocost_diving) { thread_type = bnb_thread_type_t::COEFFICIENT_DIVING; } else { - thread_type = bnb_task_type_t::PSEUDOCOST_DIVING; + thread_type = bnb_worker_type_t::PSEUDOCOST_DIVING; } } - lp_problem_t& leaf_problem = worker_data->leaf_problem; + lp_problem_t& leaf_problem = worker->leaf_problem; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); std::vector& leaf_vstatus = node_ptr->vstatus; assert(leaf_vstatus.size() == leaf_problem.num_cols); @@ -697,9 +654,9 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); lp_settings.scale_columns = false; - if (thread_type != bnb_task_type_t::EXPLORATION) { + if (thread_type != bnb_worker_type_t::EXPLORATION) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; - f_t factor = settings_.bnb_task_settings[thread_type].iteration_limit_factor; + f_t factor = settings_.bnb_worker_settings[thread_type].iteration_limit_factor; f_t max_iter = factor * bnb_lp_iters; lp_settings.iteration_limit = max_iter - stats.total_lp_iters; if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } @@ -727,8 +684,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* node_ptr->vstatus[node_ptr->branch_var]); #endif - bool is_feasible = set_node_bounds(node_ptr, root_lower, root_upper, worker_data); - + bool is_feasible = worker->set_lp_variable_bounds_for(node_ptr, settings_); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; if (is_feasible) { @@ -738,30 +694,29 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* lp_status = dual_phase2_with_advanced_basis(2, 0, - worker_data->recompute_basis, + worker->recompute_basis, lp_start_time, leaf_problem, lp_settings, leaf_vstatus, - worker_data->basis_factors, - worker_data->basic_list, - worker_data->nonbasic_list, + worker->basis_factors, + worker->basic_list, + worker->nonbasic_list, leaf_solution, node_iter, leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); - lp_status_t second_status = - solve_linear_program_with_advanced_basis(leaf_problem, - lp_start_time, - lp_settings, - leaf_solution, - worker_data->basis_factors, - worker_data->basic_list, - worker_data->nonbasic_list, - leaf_vstatus, - leaf_edge_norms); + lp_status_t second_status = solve_linear_program_with_advanced_basis(leaf_problem, + lp_start_time, + lp_settings, + leaf_solution, + worker->basis_factors, + worker->basic_list, + worker->nonbasic_list, + leaf_vstatus, + leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } @@ -800,7 +755,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* search_tree.graphviz_node(log, node_ptr, "lower bound", leaf_objective); pc_.update_pseudo_costs(node_ptr, leaf_objective); - if (thread_type == bnb_task_type_t::EXPLORATION) { + if (thread_type == bnb_worker_type_t::EXPLORATION) { if (settings_.node_processed_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, leaf_solution.x, original_x); @@ -845,7 +800,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* return node_solve_info_t::ITERATION_LIMIT; } else { - if (thread_type == bnb_task_type_t::EXPLORATION) { + if (thread_type == bnb_worker_type_t::EXPLORATION) { fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); log.printf( "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " @@ -862,117 +817,110 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* } } -template -void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, - i_t initial_heap_size) -{ - if (solver_status_ != mip_exploration_status_t::RUNNING) { return; } - - // Note that we do not know which thread will execute the - // `exploration_ramp_up` task, so we allow to any thread - // to repair the heuristic solution. - repair_heuristic_solutions(); - - i_t tid = omp_get_thread_num(); - bnb_worker_data_t* worker_data = get_worker_data(tid); - assert(worker_data); - - f_t lower_bound = node->lower_bound; - f_t upper_bound = get_upper_bound(); - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); - f_t abs_gap = upper_bound - lower_bound; - - if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { - search_tree_.graphviz_node(settings_.log, node, "cutoff", node->lower_bound); - search_tree_.update(node, node_status_t::FATHOMED); - --exploration_stats_.nodes_unexplored; - return; - } - - f_t now = toc(exploration_stats_.start_time); - f_t time_since_last_log = - exploration_stats_.last_log == 0 ? 1.0 : toc(exploration_stats_.last_log); - - if (((exploration_stats_.nodes_since_last_log >= 10 || - abs_gap < 10 * settings_.absolute_mip_gap_tol) && - (time_since_last_log >= 1)) || - (time_since_last_log > 30) || now > settings_.time_limit) { - bool should_report = should_report_.exchange(false); - - if (should_report) { - report(" ", upper_bound, root_objective_, node->depth); - exploration_stats_.nodes_since_last_log = 0; - exploration_stats_.last_log = tic(); - should_report_ = true; - } - } - - if (now > settings_.time_limit) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return; - } - - worker_data->recompute_basis = true; - worker_data->recompute_bounds = true; - - node_solve_info_t status = solve_node(node, - search_tree_, - bnb_task_type_t::EXPLORATION, - worker_data, - original_lp_.lower, - original_lp_.upper, - exploration_stats_, - settings_.log); - - ++exploration_stats_.nodes_since_last_log; - ++exploration_stats_.nodes_explored; - --exploration_stats_.nodes_unexplored; - - if (status == node_solve_info_t::TIME_LIMIT) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return; - - } else if (has_children(status)) { - exploration_stats_.nodes_unexplored += 2; - - // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase - if (exploration_stats_.nodes_unexplored < initial_heap_size) { -#pragma omp task - exploration_ramp_up(node->get_down_child(), initial_heap_size); - -#pragma omp task - exploration_ramp_up(node->get_up_child(), initial_heap_size); - - } else { - // We've generated enough nodes, push further nodes onto the heap - node_queue.push(node->get_down_child()); - node_queue.push(node->get_up_child()); - } - } -} +// template +// void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, +// i_t initial_heap_size) +// { +// if (solver_status_ != mip_exploration_status_t::RUNNING) { return; } + +// // Note that we do not know which thread will execute the +// // `exploration_ramp_up` task, so we allow to any thread +// // to repair the heuristic solution. +// repair_heuristic_solutions(); + +// i_t tid = omp_get_thread_num(); +// bnb_worker_data_t* worker_data = get_worker_data(tid); +// assert(worker_data); + +// f_t lower_bound = node->lower_bound; +// f_t upper_bound = get_upper_bound(); +// f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); +// f_t abs_gap = upper_bound - lower_bound; + +// if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { +// search_tree_.graphviz_node(settings_.log, node, "cutoff", node->lower_bound); +// search_tree_.update(node, node_status_t::FATHOMED); +// --exploration_stats_.nodes_unexplored; +// return; +// } + +// f_t now = toc(exploration_stats_.start_time); +// f_t time_since_last_log = +// exploration_stats_.last_log == 0 ? 1.0 : toc(exploration_stats_.last_log); + +// if (((exploration_stats_.nodes_since_last_log >= 10 || +// abs_gap < 10 * settings_.absolute_mip_gap_tol) && +// (time_since_last_log >= 1)) || +// (time_since_last_log > 30) || now > settings_.time_limit) { +// bool should_report = should_report_.exchange(false); + + // if (should_report) { + // report(" ", upper_bound, root_objective_, node->depth); + // exploration_stats_.nodes_since_last_log = 0; + // exploration_stats_.last_log = tic(); + // should_report_ = true; + // } + // } + +// if (now > settings_.time_limit) { +// solver_status_ = mip_exploration_status_t::TIME_LIMIT; +// return; +// } + +// worker_data->recompute_basis = true; +// worker_data->recompute_bounds = true; + +// node_solve_info_t status = solve_node(node, +// search_tree_, +// bnb_worker_type_t::EXPLORATION, +// worker_data, +// original_lp_.lower, +// original_lp_.upper, +// exploration_stats_, +// settings_.log); + +// ++exploration_stats_.nodes_since_last_log; +// ++exploration_stats_.nodes_explored; +// --exploration_stats_.nodes_unexplored; + +// if (status == node_solve_info_t::TIME_LIMIT) { +// solver_status_ = mip_exploration_status_t::TIME_LIMIT; +// return; + +// } else if (has_children(status)) { +// exploration_stats_.nodes_unexplored += 2; + +// // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase +// if (exploration_stats_.nodes_unexplored < initial_heap_size) { +// #pragma omp task +// exploration_ramp_up(node->get_down_child(), initial_heap_size); + +// #pragma omp task +// exploration_ramp_up(node->get_up_child(), initial_heap_size); + +// } else { +// // We've generated enough nodes, push further nodes onto the heap +// node_queue.push(node->get_down_child()); +// node_queue.push(node->get_up_child()); +// } +// } +// } template -void branch_and_bound_t::plunge_from(i_t task_id, mip_node_t* start_node) +void branch_and_bound_t::plunge_with(bnb_worker_t* worker) { - i_t tid = omp_get_thread_num(); - bnb_worker_data_t* worker_data = get_worker_data(tid); - assert(worker_data); - std::deque*> stack; - stack.push_front(start_node); + stack.push_front(worker->start_node); - worker_data->recompute_basis = true; - worker_data->recompute_bounds = true; + worker->recompute_basis = true; + worker->recompute_bounds = true; while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { - if (task_id == 0) { repair_heuristic_solutions(); } - mip_node_t* node_ptr = stack.front(); stack.pop_front(); f_t lower_bound = node_ptr->lower_bound; f_t upper_bound = get_upper_bound(); - f_t abs_gap = upper_bound - lower_bound; f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); // This is based on three assumptions: @@ -981,62 +929,41 @@ void branch_and_bound_t::plunge_from(i_t task_id, mip_node_t // - 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 - assert(task_id < local_lower_bounds_.size()); - local_lower_bounds_[task_id] = lower_bound; + worker->lower_bound = lower_bound; if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree_.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); search_tree_.update(node_ptr, node_status_t::FATHOMED); - worker_data->recompute_basis = true; - worker_data->recompute_bounds = true; + worker->recompute_basis = true; + worker->recompute_bounds = true; --exploration_stats_.nodes_unexplored; continue; } - f_t now = toc(exploration_stats_.start_time); - - if (task_id == 0) { - f_t time_since_last_log = - exploration_stats_.last_log == 0 ? 1.0 : toc(exploration_stats_.last_log); - - if (((exploration_stats_.nodes_since_last_log >= 1000 || - abs_gap < 10 * settings_.absolute_mip_gap_tol) && - time_since_last_log >= 1) || - (time_since_last_log > 30) || now > settings_.time_limit) { - report(" ", upper_bound, get_lower_bound(), node_ptr->depth); - exploration_stats_.last_log = tic(); - exploration_stats_.nodes_since_last_log = 0; - } - } - - if (now > settings_.time_limit) { + if (toc(exploration_stats_.start_time) > settings_.time_limit) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return; + break; } if (exploration_stats_.nodes_explored >= settings_.node_limit) { solver_status_ = mip_exploration_status_t::NODE_LIMIT; - return; + break; } node_solve_info_t status = solve_node(node_ptr, search_tree_, - bnb_task_type_t::EXPLORATION, - worker_data, - original_lp_.lower, - original_lp_.upper, + bnb_worker_type_t::EXPLORATION, + worker, exploration_stats_, settings_.log); - worker_data->recompute_basis = !has_children(status); - worker_data->recompute_bounds = !has_children(status); + worker->recompute_basis = !has_children(status); + worker->recompute_bounds = !has_children(status); - ++exploration_stats_.nodes_since_last_log; ++exploration_stats_.nodes_explored; --exploration_stats_.nodes_unexplored; if (status == node_solve_info_t::TIME_LIMIT) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return; + break; } else if (has_children(status)) { // The stack should only contain the children of the current parent. @@ -1060,76 +987,28 @@ void branch_and_bound_t::plunge_from(i_t task_id, mip_node_t } } } -} -template -void branch_and_bound_t::best_first_thread(i_t task_id) -{ - f_t lower_bound = -inf; - f_t upper_bound = inf; - f_t abs_gap = inf; - f_t rel_gap = inf; - - while (solver_status_ == mip_exploration_status_t::RUNNING && - abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && - (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { - // If there any node left in the heap, we pop the top node and explore it. - std::optional*> start_node = node_queue.pop_best_first(active_subtrees_); - - if (start_node.has_value()) { - if (get_upper_bound() < start_node.value()->lower_bound) { - // This node was put on the heap earlier but its lower bound is now greater than the - // current upper bound - 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); - active_subtrees_--; - continue; - } - - // Best-first search with plunging - plunge_from(task_id, start_node.value()); - - active_subtrees_--; - } - - lower_bound = get_lower_bound(); - upper_bound = get_upper_bound(); - abs_gap = upper_bound - lower_bound; - rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); - } - - // Check if it is the last thread that exited the loop and no - // timeout or numerical error has happen. - if (solver_status_ == mip_exploration_status_t::RUNNING) { - if (active_subtrees_ == 0) { - solver_status_ = mip_exploration_status_t::COMPLETED; - } else { - local_lower_bounds_[task_id] = inf; - } - } + worker->is_active = false; + mutex_available_workers_.lock(); + available_workers_.push_back(worker->worker_id); + mutex_available_workers_.unlock(); + active_workers_per_type[EXPLORATION]--; } template -void branch_and_bound_t::dive_from(mip_node_t& start_node, - const std::vector& start_lower, - const std::vector& start_upper, - bnb_task_type_t diving_type) +void branch_and_bound_t::dive_with(bnb_worker_t* worker) { logger_t log; log.log = false; - const i_t node_limit = settings_.bnb_task_settings[diving_type].node_limit; - const i_t backtrack = settings_.bnb_task_settings[diving_type].backtrack; + bnb_worker_type_t diving_type = worker->worker_type; + const i_t node_limit = settings_.bnb_worker_settings[diving_type].node_limit; + const i_t backtrack = settings_.bnb_worker_settings[diving_type].backtrack; - i_t tid = omp_get_thread_num(); - bnb_worker_data_t* worker_data = get_worker_data(tid); - assert(worker_data); + worker->recompute_basis = true; + worker->recompute_bounds = true; - worker_data->recompute_basis = true; - worker_data->recompute_bounds = true; - - search_tree_t subtree(std::move(start_node)); + search_tree_t subtree(std::move(*worker->start_node)); std::deque*> stack; stack.push_front(&subtree.root); @@ -1142,23 +1021,25 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); - f_t upper_bound = get_upper_bound(); - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node_ptr->lower_bound); + + f_t lower_bound = node_ptr->lower_bound; + f_t upper_bound = get_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 || rel_gap < settings_.relative_mip_gap_tol) { - worker_data->recompute_basis = true; - worker_data->recompute_bounds = true; + worker->recompute_basis = true; + worker->recompute_bounds = true; continue; } if (toc(exploration_stats_.start_time) > settings_.time_limit) { break; } if (dive_stats.nodes_explored > node_limit) { break; } - node_solve_info_t status = solve_node( - node_ptr, subtree, diving_type, worker_data, start_lower, start_upper, dive_stats, log); + node_solve_info_t status = solve_node(node_ptr, subtree, diving_type, worker, dive_stats, log); dive_stats.nodes_explored++; - worker_data->recompute_basis = !has_children(status); - worker_data->recompute_bounds = !has_children(status); + worker->recompute_basis = !has_children(status); + worker->recompute_bounds = !has_children(status); if (status == node_solve_info_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; @@ -1181,40 +1062,147 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, stack.pop_back(); } } + + worker->is_active = false; + mutex_available_workers_.lock(); + available_workers_.push_back(worker->worker_id); + mutex_available_workers_.unlock(); + active_workers_per_type[diving_type]--; } template -void branch_and_bound_t::diving_thread(bnb_task_type_t diving_type) +void branch_and_bound_t::master_loop() { - i_t tid = omp_get_thread_num(); - bnb_worker_data_t* worker_data = get_worker_data(tid); - bounds_strengthening_t& node_presolver = worker_data->node_presolver; + f_t lower_bound = get_lower_bound(); + f_t upper_bound = get_upper_bound(); + f_t abs_gap = upper_bound - lower_bound; + f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + i_t last_node_depth = 0; - std::vector start_lower; - std::vector start_upper; - std::vector bounds_changed(original_lp_.num_cols, false); - bool reset_starting_bounds = true; + f_t last_log = 0.0; + i_t nodes_since_last_log = 0; + + constexpr bnb_worker_type_t task_types[] = { + EXPLORATION, PSEUDOCOST_DIVING, LINE_SEARCH_DIVING, GUIDED_DIVING, COEFFICIENT_DIVING}; while (solver_status_ == mip_exploration_status_t::RUNNING && - (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { - if (reset_starting_bounds) { - start_lower = original_lp_.lower; - start_upper = original_lp_.upper; - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - reset_starting_bounds = false; + abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && + (active_workers_per_type[0] > 0 || node_queue.best_first_queue_size() > 0)) { + lower_bound = get_lower_bound(); + upper_bound = get_upper_bound(); + abs_gap = upper_bound - lower_bound; + rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + + repair_heuristic_solutions(); + + f_t now = toc(exploration_stats_.start_time); + f_t time_since_last_log = last_log == 0 ? 1.0 : toc(last_log); + + if (((nodes_since_last_log >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && + time_since_last_log >= 1) || + (time_since_last_log > 30) || now > settings_.time_limit) { + f_t obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); + std::string gap_user = user_mip_gap(obj, user_lower); + i_t nodes_explored = exploration_stats_.nodes_explored; + i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + f_t iter_node = exploration_stats_.total_lp_iters / nodes_explored; + i_t depth = + node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; + + settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + obj, + user_lower, + depth, + iter_node, + gap_user.c_str(), + now); + + last_log = tic(); + nodes_since_last_log = 0; + } + + // There is no node in the queue, so we suspend temporarily the execution of the master + // so it can execute a worker task instead. + if (node_queue.best_first_queue_size() == 0 && node_queue.diving_queue_size() == 0) { +#pragma omp taskyield + continue; } - std::optional> start_node = - node_queue.pop_diving(start_lower, start_upper, bounds_changed); + for (auto type : task_types) { + if (active_workers_per_type[type] < settings_.bnb_worker_settings[type].num_workers) { + if (type == EXPLORATION) { + // If there any node left in the heap, we pop the top node and explore it. + std::optional*> start_node = node_queue.pop_best_first(); + + if (start_node.has_value()) { + if (get_upper_bound() < start_node.value()->lower_bound) { + // This node was put on the heap earlier but its lower bound is now greater than the + // current upper bound + 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; + } + + i_t worker_id = -1; + + mutex_available_workers_.lock(); + if (available_workers_.size() > 0) { + worker_id = available_workers_.front(); + available_workers_.pop_front(); + } + mutex_available_workers_.unlock(); + + // There is no available workers + if (worker_id < 0) { break; } + + workers_[worker_id]->init_best_first(start_node.value(), original_lp_); + last_node_depth = start_node.value()->depth; + active_workers_per_type[type]++; + nodes_since_last_log++; - if (start_node.has_value()) { - reset_starting_bounds = true; +#pragma omp task + plunge_with(workers_[worker_id].get()); + } - bool is_feasible = - node_presolver.bounds_strengthening(start_lower, start_upper, bounds_changed, settings_); - if (get_upper_bound() < start_node->lower_bound || !is_feasible) { continue; } + } else { + std::optional*> start_node = node_queue.pop_diving(); + + if (start_node.has_value()) { + if (get_upper_bound() < start_node.value()->lower_bound) { continue; } + + i_t worker_id = -1; + + mutex_available_workers_.lock(); + if (available_workers_.size() > 0) { + worker_id = available_workers_.front(); + available_workers_.pop_front(); + } + mutex_available_workers_.unlock(); + + // There is no available workers + if (worker_id < 0) { break; } - dive_from(start_node.value(), start_lower, start_upper, diving_type); + bool is_feasible = + workers_[worker_id]->init_diving(start_node.value(), type, original_lp_, settings_); + + if (!is_feasible) { + mutex_available_workers_.lock(); + available_workers_.push_back(worker_id); + mutex_available_workers_.unlock(); + continue; + } + + active_workers_per_type[type]++; + +#pragma omp task + dive_with(workers_[worker_id].get()); + } + } + } } } } @@ -1374,9 +1362,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); - i_t num_bfs_threads = settings_.bnb_task_settings[EXPLORATION].num_tasks; - local_lower_bounds_.assign(num_bfs_threads, root_objective_); - if (settings_.set_simplex_solution_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, root_relax_soln_.x, original_x); @@ -1450,56 +1435,42 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_vstatus_, original_lp_, log); - worker_data_pool_.resize(settings_.num_threads); + workers_.resize(settings_.num_threads); + active_workers_per_type.fill(0); + for (i_t i = 0; i < settings_.num_threads; ++i) { + workers_[i] = + std::make_unique>(i, original_lp_, Arow_, var_types_, settings_); + available_workers_.push_front(i); + } + + i_t num_bfs_workers = settings_.bnb_worker_settings[EXPLORATION].num_workers; settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, - num_bfs_threads, - settings_.num_threads - num_bfs_threads); + num_bfs_workers, + settings_.num_threads - num_bfs_workers); settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " "| Time |\n"); - exploration_stats_.nodes_explored = 1; - exploration_stats_.nodes_unexplored = 2; - exploration_stats_.nodes_since_last_log = 0; - exploration_stats_.last_log = tic(); - active_subtrees_ = 0; - solver_status_ = mip_exploration_status_t::RUNNING; - lower_bound_ceiling_ = inf; - should_report_ = true; + exploration_stats_.nodes_explored = 1; + exploration_stats_.nodes_unexplored = 2; + solver_status_ = mip_exploration_status_t::RUNNING; + lower_bound_ceiling_ = inf; + should_report_ = true; + + auto down_child = search_tree_.root.get_down_child(); + auto up_child = search_tree_.root.get_up_child(); + node_queue.push(down_child); + node_queue.push(up_child); #pragma omp parallel num_threads(settings_.num_threads) { #pragma omp master { - auto down_child = search_tree_.root.get_down_child(); - auto up_child = search_tree_.root.get_up_child(); - i_t initial_size = 2 * settings_.num_threads; - -#pragma omp taskgroup - { #pragma omp task - exploration_ramp_up(down_child, initial_size); - -#pragma omp task - exploration_ramp_up(up_child, initial_size); - } - - for (i_t i = 0; i < num_bfs_threads; i++) { -#pragma omp task - best_first_thread(i); - } - - for (auto& settings : settings_.bnb_task_settings) { - if (settings.type != EXPLORATION && settings.is_enabled) { - for (i_t k = 0; k < settings.num_tasks; k++) { -#pragma omp task - diving_thread(settings.type); - } - } - } + master_loop(); } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index e6e225b73..8a002f946 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -21,6 +22,7 @@ #include #include +#include #include namespace cuopt::linear_programming::dual_simplex { @@ -53,51 +55,9 @@ enum class node_solve_info_t { NUMERICAL = 5 // The solver encounter a numerical error when solving the node }; -template -class bounds_strengthening_t; - template void upper_bound_callback(f_t upper_bound); -template -struct bnb_stats_t { - f_t start_time = 0.0; - omp_atomic_t total_lp_solve_time = 0.0; - omp_atomic_t nodes_explored = 0; - omp_atomic_t nodes_unexplored = 0; - omp_atomic_t total_lp_iters = 0; - - // This should only be used by the main thread - omp_atomic_t last_log = 0.0; - omp_atomic_t nodes_since_last_log = 0; -}; - -template -struct bnb_worker_data_t { - lp_problem_t leaf_problem; - basis_update_mpf_t basis_factors; - std::vector basic_list; - std::vector nonbasic_list; - bounds_strengthening_t node_presolver; - std::vector bounds_changed; - - bool recompute_basis = true; - bool recompute_bounds = true; - - bnb_worker_data_t(const lp_problem_t& original_lp, - const csr_matrix_t& Arow, - const std::vector& var_type, - const simplex_solver_settings_t& settings) - : leaf_problem(original_lp), - basis_factors(original_lp.num_rows, settings.refactor_frequency), - basic_list(original_lp.num_rows), - nonbasic_list(), - node_presolver(leaf_problem, Arow, {}, var_type), - bounds_changed(original_lp.num_cols, false) - { - } -}; - template class branch_and_bound_t { public: @@ -159,9 +119,6 @@ class branch_and_bound_t { std::vector new_slacks_; std::vector var_types_; - // Local lower bounds for each thread - std::vector> local_lower_bounds_; - // Mutex for upper bound omp_mutex_t mutex_upper_; @@ -198,8 +155,9 @@ class branch_and_bound_t { // Search tree search_tree_t search_tree_; - // Count the number of subtrees that are currently being explored. - omp_atomic_t active_subtrees_; + // Count the number of tasks per type that either being executed or + // waiting to be executed. + std::array, 5> active_workers_per_type; // Global status of the solver. omp_atomic_t solver_status_; @@ -214,10 +172,12 @@ class branch_and_bound_t { void report(std::string symbol, f_t obj, f_t lower_bound, i_t node_depth); - // A pool containing the data needed for a worker to perform a plunge or dive. - // This is lazily initialized via `get_worker_data()`. - std::vector>> worker_data_pool_; - bnb_worker_data_t* get_worker_data(i_t tid); + // Worker pool + std::vector>> workers_; + + // FIXME: Implement a lock-free queue + omp_mutex_t mutex_available_workers_; + std::deque available_workers_; // Set the final solution. mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); @@ -227,7 +187,7 @@ class branch_and_bound_t { void add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - bnb_task_type_t thread_type); + bnb_worker_type_t thread_type); // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); @@ -236,36 +196,17 @@ class branch_and_bound_t { // there is enough unexplored nodes. This is done recursively using OpenMP tasks. void exploration_ramp_up(mip_node_t* node, i_t initial_heap_size); - void plunge_from(i_t task_id, mip_node_t* start_node); - - // Each "main" thread pops a node from the global heap and then performs a plunge - // (i.e., a shallow dive) into the subtree determined by the node. - void best_first_thread(i_t task_id); - - // Perform a deep dive in the subtree determined by the `start_node`. - void dive_from(mip_node_t& start_node, - const std::vector& start_lower, - const std::vector& start_upper, - bnb_task_type_t diving_type); + void plunge_with(bnb_worker_t* worker); - // Each diving thread pops the first node from the dive queue and then performs - // a deep dive into the subtree determined by the node. - void diving_thread(bnb_thread_type_t diving_type); + void dive_with(bnb_worker_t* worker); - // Set the bounds of the leaf node and then apply bounds propagation. - // Return true if the problem is feasible, false otherwise. - bool set_node_bounds(mip_node_t* node_ptr, - const std::vector& start_lower, - const std::vector& start_upper, - bnb_worker_data_t* worker_data); + void master_loop(); // Solve the LP relaxation of a leaf node and update the tree. node_solve_info_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, - bnb_task_type_t thread_type, - bnb_worker_data_t* worker_data, - const std::vector& root_lower, - const std::vector& root_upper, + bnb_worker_type_t thread_type, + bnb_worker_t* worker, bnb_stats_t& stats, logger_t& log); @@ -273,7 +214,7 @@ class branch_and_bound_t { branch_variable_t variable_selection(mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_task_type_t type, + bnb_worker_type_t type, logger_t& log); }; diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 94464d3cf..6574d4bfd 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -10,15 +10,15 @@ namespace cuopt::linear_programming::dual_simplex { template -bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type) +bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type) { - return bnb_task_settings_t{.type = type, - .is_enabled = true, - .num_tasks = -1, - .min_node_depth = 0, - .node_limit = 500, - .iteration_limit_factor = 0.05, - .backtrack = 5}; + return bnb_worker_settings_t{.type = type, + .is_enabled = true, + .num_workers = -1, + .min_node_depth = 0, + .node_limit = 500, + .iteration_limit_factor = 0.05, + .backtrack = 5}; } template @@ -286,7 +286,7 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE -template bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type); +template bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type); template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, diff --git a/cpp/src/dual_simplex/diving_heuristics.hpp b/cpp/src/dual_simplex/diving_heuristics.hpp index 422db1a68..02390ae8e 100644 --- a/cpp/src/dual_simplex/diving_heuristics.hpp +++ b/cpp/src/dual_simplex/diving_heuristics.hpp @@ -21,7 +21,7 @@ struct branch_variable_t { }; template -bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type); +bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type); template branch_variable_t line_search_diving(const std::vector& fractional, diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 0234fa038..faa959f9f 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -108,32 +108,17 @@ class node_queue_t { diving_heap.push(entry); } - // In the current implementation, we are use the active number of subtree to decide - // when to stop the execution. We need to increment the counter at the same - // time as we pop a node from the queue to avoid some threads exiting - // the main loop thinking that the solver has already finished. - // This will be not needed in the master-worker model. - std::optional*> pop_best_first(omp_atomic_t& active_subtree) + std::optional*> pop_best_first() { std::lock_guard lock(mutex); auto entry = best_first_heap.pop(); - if (entry.has_value()) { - active_subtree++; - return std::exchange(entry.value()->node, nullptr); - } + if (entry.has_value()) { return std::exchange(entry.value()->node, nullptr); } return std::nullopt; } - // In the current implementation, multiple threads can pop the nodes - // from the queue, so we need to pass the lower and upper bound here - // to avoid other thread fathoming the node (i.e., deleting) before we can read - // the variable bounds from the tree. - // This will be not needed in the master-worker model. - std::optional> pop_diving(std::vector& lower, - std::vector& upper, - std::vector& bounds_changed) + std::optional*> pop_diving() { std::lock_guard lock(mutex); @@ -141,10 +126,7 @@ class node_queue_t { auto entry = diving_heap.pop(); if (entry.has_value()) { - if (auto node_ptr = entry.value()->node; node_ptr != nullptr) { - node_ptr->get_variable_bounds(lower, upper, bounds_changed); - return node_ptr->detach_copy(); - } + if (auto node_ptr = entry.value()->node; node_ptr != nullptr) { return node_ptr; } } } @@ -168,6 +150,12 @@ class node_queue_t { std::lock_guard lock(mutex); return best_first_heap.empty() ? inf : best_first_heap.top()->lower_bound; } + + mip_node_t* bfs_top() + { + std::lock_guard lock(mutex); + return best_first_heap.empty() ? nullptr : best_first_heap.top()->node; + } }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index e791c5da1..d7512e3be 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -20,12 +20,12 @@ namespace cuopt::linear_programming::dual_simplex { -// Indicate the search and variable selection algorithms used by each task +// Indicate the search and variable selection algorithms used by each worker // in B&B (See [1]). // // [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, // Berlin, 2007. doi: 10.14279/depositonce-1634. -enum bnb_task_type_t { +enum bnb_worker_type_t { EXPLORATION = 0, // Best-First + Plunging. PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) @@ -33,37 +33,39 @@ enum bnb_task_type_t { COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1) }; -// Settings for each task type in B&B. +// Settings for each worker in B&B. template -struct bnb_task_settings_t { - // Type of the task. - bnb_task_type_t type; +struct bnb_worker_settings_t { + // Type of the worker. + bnb_worker_type_t type; - // Is this type of task enabled? + // Is this worker enabled? // This will be ignored if `type == EXPLORATION`. bool is_enabled; - // Number of tasks of this type. - i_t num_tasks; + // Number of workers of this type. + i_t num_workers; - // Minimum node depth to start this task + // Minimum node depth to start this worker // This will be ignored if `type == EXPLORATION`. i_t min_node_depth; - // Maximum number of nodes explored in this task. + // Maximum number of nodes explored in this worker. i_t node_limit; - // Maximum fraction of the number of simplex iterations for this task + // Maximum fraction of the number of simplex iterations for this worker // compared to the number of simplex iterations for normal exploration. + // This will be ignored if `type == EXPLORATION`. f_t iteration_limit_factor; // Number of nodes that it allows to backtrack when // reaching the bottom of a given branch of the tree. + // This will be ignored if `type == EXPLORATION`. i_t backtrack; }; template -bnb_task_settings_t get_default_diving_settings(bnb_task_type_t type); +bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type); template struct simplex_solver_settings_t { @@ -121,23 +123,24 @@ struct simplex_solver_settings_t { heuristic_preemption_callback(nullptr), concurrent_halt(nullptr) { - bnb_task_settings[EXPLORATION] = - bnb_task_settings_t{.type = EXPLORATION, - .is_enabled = true, - .num_tasks = -1, - .min_node_depth = 0, - .node_limit = std::numeric_limits::max(), - .iteration_limit_factor = std::numeric_limits::max(), - .backtrack = 1}; - - bnb_task_settings[PSEUDOCOST_DIVING] = get_default_diving_settings(PSEUDOCOST_DIVING); - - bnb_task_settings[LINE_SEARCH_DIVING] = + bnb_worker_settings[EXPLORATION] = + bnb_worker_settings_t{.type = EXPLORATION, + .is_enabled = true, + .num_workers = -1, + .min_node_depth = 0, + .node_limit = std::numeric_limits::max(), + .iteration_limit_factor = std::numeric_limits::max(), + .backtrack = 1}; + + bnb_worker_settings[PSEUDOCOST_DIVING] = + get_default_diving_settings(PSEUDOCOST_DIVING); + + bnb_worker_settings[LINE_SEARCH_DIVING] = get_default_diving_settings(LINE_SEARCH_DIVING); - bnb_task_settings[GUIDED_DIVING] = get_default_diving_settings(GUIDED_DIVING); + bnb_worker_settings[GUIDED_DIVING] = get_default_diving_settings(GUIDED_DIVING); - bnb_task_settings[COEFFICIENT_DIVING] = + bnb_worker_settings[COEFFICIENT_DIVING] = get_default_diving_settings(COEFFICIENT_DIVING); set_bnb_tasks(omp_get_max_threads() - 1); @@ -145,26 +148,26 @@ struct simplex_solver_settings_t { void set_bnb_tasks(i_t num_threads) { - this->num_threads = num_threads; - bnb_task_settings[EXPLORATION].num_tasks = std::max(1, num_threads / 4); + this->num_threads = num_threads; + bnb_worker_settings[EXPLORATION].num_workers = std::max(1, num_threads / 2); - i_t diving_tasks = num_threads - bnb_task_settings[EXPLORATION].num_tasks; + i_t diving_tasks = num_threads - bnb_worker_settings[EXPLORATION].num_workers; i_t num_enabled = 0; - for (size_t i = 1; i < bnb_task_settings.size(); ++i) { - num_enabled += static_cast(bnb_task_settings[i].is_enabled); + for (size_t i = 1; i < bnb_worker_settings.size(); ++i) { + num_enabled += static_cast(bnb_worker_settings[i].is_enabled); } - for (size_t i = 1, k = 0; i < bnb_task_settings.size(); ++i) { + for (size_t i = 1, k = 0; i < bnb_worker_settings.size(); ++i) { i_t start = (double)k * diving_tasks / num_enabled; i_t end = (double)(k + 1) * diving_tasks / num_enabled; - if (bnb_task_settings[i].is_enabled) { - bnb_task_settings[i].num_tasks = end - start; + if (bnb_worker_settings[i].is_enabled) { + bnb_worker_settings[i].num_workers = 2 * (end - start); ++k; } else { - bnb_task_settings[i].num_tasks = 0; + bnb_worker_settings[i].num_workers = 0; } } } @@ -230,7 +233,7 @@ struct simplex_solver_settings_t { // Indicate the settings used by each task // The position in the array is indicated by the `bnb_task_type_t`. - std::array, 5> bnb_task_settings; + std::array, 5> bnb_worker_settings; i_t inside_mip; // 0 if outside MIP, 1 if inside MIP at root node, 2 if inside MIP at leaf node std::function&, f_t)> solution_callback; diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 4442029e8..0461167a8 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -260,9 +260,11 @@ void rins_t::run_rins() // In the future, let RINS use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.bnb_task_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = false; - branch_and_bound_settings.bnb_task_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = false; - branch_and_bound_settings.bnb_task_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = false; + branch_and_bound_settings.bnb_worker_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = false; + branch_and_bound_settings.bnb_worker_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = + false; + branch_and_bound_settings.bnb_worker_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = + false; branch_and_bound_settings.set_bnb_tasks(2); branch_and_bound_settings.log.log = false; diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 8501b70cc..fc28af783 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -105,11 +105,11 @@ class sub_mip_recombiner_t : public recombiner_t { // In the future, let SubMIP use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.bnb_task_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = + branch_and_bound_settings.bnb_worker_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = false; - branch_and_bound_settings.bnb_task_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = + branch_and_bound_settings.bnb_worker_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = false; - branch_and_bound_settings.bnb_task_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = + branch_and_bound_settings.bnb_worker_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = false; branch_and_bound_settings.set_bnb_tasks(2); From a0fa4e18502a5f8dca6032b66d2498ed77bec090 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Jan 2026 15:24:34 +0100 Subject: [PATCH 39/70] refactoring and fixing bugs --- cpp/src/dual_simplex/bnb_worker.hpp | 120 ++++++++++- cpp/src/dual_simplex/branch_and_bound.cpp | 201 +++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 20 +- cpp/src/dual_simplex/diving_heuristics.cpp | 17 +- cpp/src/dual_simplex/diving_heuristics.hpp | 5 +- cpp/src/dual_simplex/node_queue.hpp | 24 +-- .../dual_simplex/simplex_solver_settings.hpp | 114 ++-------- cpp/src/mip/diversity/lns/rins.cu | 19 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 20 +- cpp/src/mip/solver.cu | 15 +- cpp/src/utilities/omp_helpers.hpp | 4 +- 11 files changed, 268 insertions(+), 291 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index 34eeb38c8..f2ec37ca1 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -12,10 +12,26 @@ #include #include +#include #include namespace cuopt::linear_programming::dual_simplex { +constexpr int bnb_num_worker_types = 5; + +// Indicate the search and variable selection algorithms used by each thread +// in B&B (See [1]). +// +// [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, +// Berlin, 2007. doi: 10.14279/depositonce-1634. +enum bnb_worker_type_t : int { + EXPLORATION = 0, // Best-First + Plunging. + PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) + LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) + GUIDED_DIVING = 3, // Guided diving (9.2.3). + COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1) +}; + template struct bnb_stats_t { f_t start_time = 0.0; @@ -80,11 +96,105 @@ class bnb_worker_t { private: // For diving, we need to store the full node instead of - // of just a pointer, since it is detached from the - // tree. To keep the same interface for any type of worker, - // the start node will point to this node when diving. - // For best-first search, this will not be used. + // of just a pointer, since it is not store 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; }; +template +class bnb_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) + { + for (i_t i = 0; i < num_workers; ++i) { + workers_[i] = + std::make_unique>(i, original_lp, Arow, var_type, settings); + available_workers_.push_front(i); + } + } + + bnb_worker_t* get_worker() + { + std::lock_guard lock(mutex_); + + if (available_workers_.empty()) { + return nullptr; + } else { + i_t idx = available_workers_.front(); + available_workers_.pop_front(); + return workers_[idx].get(); + } + } + + void return_worker_to_pool(bnb_worker_t* worker) + { + worker->is_active = false; + std::lock_guard lock(mutex_); + available_workers_.push_back(worker->worker_id); + } + + f_t get_lower_bounds() + { + f_t lower_bound = std::numeric_limits::infinity(); + + for (i_t i = 0; i < workers_.size(); ++i) { + if (workers_[i]->worker_type == EXPLORATION && workers_[i]->is_active) { + lower_bound = std::min(workers_[i]->lower_bound.load(), lower_bound); + } + } + + return lower_bound; + } + + private: + // Worker pool + std::vector>> workers_; + + // FIXME: Implement a lock-free queue + omp_mutex_t mutex_; + std::deque available_workers_; +}; + +template +std::vector bnb_get_worker_types(diving_heuristics_settings_t settings) +{ + std::vector types; + types.reserve(bnb_num_worker_types); + types.push_back(EXPLORATION); + if (!settings.disable_pseudocost_diving) { types.push_back(PSEUDOCOST_DIVING); } + if (!settings.disable_line_search_diving) { types.push_back(LINE_SEARCH_DIVING); } + if (!settings.disable_guided_diving) { types.push_back(GUIDED_DIVING); } + if (!settings.disable_coefficient_diving) { types.push_back(COEFFICIENT_DIVING); } + return types; +} + +template +std::array bnb_get_num_workers_round_robin( + i_t num_threads, diving_heuristics_settings_t settings) +{ + std::array max_num_workers; + auto worker_types = bnb_get_worker_types(settings); + + max_num_workers.fill(0); + max_num_workers[EXPLORATION] = std::max(1, num_threads / 4); + + i_t diving_workers = settings.num_diving_workers; + i_t m = worker_types.size() - 1; + for (size_t i = 1, k = 0; i < bnb_num_worker_types; ++i) { + i_t start = (double)k * diving_workers / m; + i_t end = (double)(k + 1) * diving_workers / m; + max_num_workers[i] = 2 * (end - start); + ++k; + } + + return max_num_workers; +} + } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index a2966fb26..dcf0be8a2 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -251,13 +251,7 @@ f_t branch_and_bound_t::get_lower_bound() f_t lower_bound = lower_bound_ceiling_.load(); f_t heap_lower_bound = node_queue.get_lower_bound(); lower_bound = std::min(heap_lower_bound, lower_bound); - - for (i_t i = 0; i < workers_.size(); ++i) { - if (workers_[i]->worker_type == EXPLORATION && workers_[i]->is_active) { - lower_bound = std::min(workers_[i]->lower_bound.load(), lower_bound); - } - } - + lower_bound = std::min(worker_pool_.get_lower_bounds(), lower_bound); return std::isfinite(lower_bound) ? lower_bound : -inf; } @@ -633,15 +627,6 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; const f_t upper_bound = get_upper_bound(); - // If there is no incumbent, use pseudocost diving instead of guided diving - if (upper_bound == inf && thread_type == bnb_worker_type_t::GUIDED_DIVING) { - if (settings_.diving_settings.disable_pseudocost_diving) { - thread_type = bnb_thread_type_t::COEFFICIENT_DIVING; - } else { - thread_type = bnb_worker_type_t::PSEUDOCOST_DIVING; - } - } - lp_problem_t& leaf_problem = worker->leaf_problem; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); std::vector& leaf_vstatus = node_ptr->vstatus; @@ -656,7 +641,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* if (thread_type != bnb_worker_type_t::EXPLORATION) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; - f_t factor = settings_.bnb_worker_settings[thread_type].iteration_limit_factor; + f_t factor = settings_.diving_settings.iteration_limit_factor; f_t max_iter = factor * bnb_lp_iters; lp_settings.iteration_limit = max_iter - stats.total_lp_iters; if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } @@ -854,13 +839,13 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* // (time_since_last_log > 30) || now > settings_.time_limit) { // bool should_report = should_report_.exchange(false); - // if (should_report) { - // report(" ", upper_bound, root_objective_, node->depth); - // exploration_stats_.nodes_since_last_log = 0; - // exploration_stats_.last_log = tic(); - // should_report_ = true; - // } - // } +// if (should_report) { +// report(" ", upper_bound, root_objective_, node->depth); +// exploration_stats_.nodes_since_last_log = 0; +// exploration_stats_.last_log = tic(); +// should_report_ = true; +// } +// } // if (now > settings_.time_limit) { // solver_status_ = mip_exploration_status_t::TIME_LIMIT; @@ -988,10 +973,7 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) } } - worker->is_active = false; - mutex_available_workers_.lock(); - available_workers_.push_back(worker->worker_id); - mutex_available_workers_.unlock(); + worker_pool_.return_worker_to_pool(worker); active_workers_per_type[EXPLORATION]--; } @@ -1002,8 +984,8 @@ void branch_and_bound_t::dive_with(bnb_worker_t* worker) log.log = false; bnb_worker_type_t diving_type = worker->worker_type; - const i_t node_limit = settings_.bnb_worker_settings[diving_type].node_limit; - const i_t backtrack = settings_.bnb_worker_settings[diving_type].backtrack; + const i_t node_limit = settings_.diving_settings.node_limit; + const i_t backtrack = settings_.diving_settings.backtrack; worker->recompute_basis = true; worker->recompute_bounds = true; @@ -1063,10 +1045,7 @@ void branch_and_bound_t::dive_with(bnb_worker_t* worker) } } - worker->is_active = false; - mutex_available_workers_.lock(); - available_workers_.push_back(worker->worker_id); - mutex_available_workers_.unlock(); + worker_pool_.return_worker_to_pool(worker); active_workers_per_type[diving_type]--; } @@ -1082,8 +1061,12 @@ void branch_and_bound_t::master_loop() f_t last_log = 0.0; i_t nodes_since_last_log = 0; - constexpr bnb_worker_type_t task_types[] = { - EXPLORATION, PSEUDOCOST_DIVING, LINE_SEARCH_DIVING, GUIDED_DIVING, COEFFICIENT_DIVING}; + diving_heuristics_settings_t diving_settings = settings_.diving_settings; + if (!std::isfinite(upper_bound)) { diving_settings.disable_guided_diving = true; } + + auto worker_types = bnb_get_worker_types(diving_settings); + auto max_num_workers_per_type = + bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); while (solver_status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && @@ -1095,31 +1078,25 @@ void branch_and_bound_t::master_loop() repair_heuristic_solutions(); + // If the guided diving was disabled previously due to the lack of an incumbent solution, + // re-enable as soon as a new incumbent is found. + if (settings_.diving_settings.disable_guided_diving != diving_settings.disable_guided_diving) { + if (std::isfinite(upper_bound)) { + diving_settings.disable_guided_diving = settings_.diving_settings.disable_guided_diving; + max_num_workers_per_type = + bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); + } + } + f_t now = toc(exploration_stats_.start_time); f_t time_since_last_log = last_log == 0 ? 1.0 : toc(last_log); if (((nodes_since_last_log >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && time_since_last_log >= 1) || (time_since_last_log > 30) || now > settings_.time_limit) { - f_t obj = compute_user_objective(original_lp_, upper_bound); - f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); - std::string gap_user = user_mip_gap(obj, user_lower); - i_t nodes_explored = exploration_stats_.nodes_explored; - i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - f_t iter_node = exploration_stats_.total_lp_iters / nodes_explored; i_t depth = node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; - - settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - depth, - iter_node, - gap_user.c_str(), - now); - + report(" ", upper_bound, lower_bound, depth); last_log = tic(); nodes_since_last_log = 0; } @@ -1131,77 +1108,63 @@ void branch_and_bound_t::master_loop() continue; } - for (auto type : task_types) { - if (active_workers_per_type[type] < settings_.bnb_worker_settings[type].num_workers) { - if (type == EXPLORATION) { - // If there any node left in the heap, we pop the top node and explore it. - std::optional*> start_node = node_queue.pop_best_first(); - - if (start_node.has_value()) { - if (get_upper_bound() < start_node.value()->lower_bound) { - // This node was put on the heap earlier but its lower bound is now greater than the - // current upper bound - 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; - } - - i_t worker_id = -1; - - mutex_available_workers_.lock(); - if (available_workers_.size() > 0) { - worker_id = available_workers_.front(); - available_workers_.pop_front(); - } - mutex_available_workers_.unlock(); - - // There is no available workers - if (worker_id < 0) { break; } - - workers_[worker_id]->init_best_first(start_node.value(), original_lp_); - last_node_depth = start_node.value()->depth; - active_workers_per_type[type]++; - nodes_since_last_log++; + for (auto type : worker_types) { + if (active_workers_per_type[type] >= max_num_workers_per_type[type]) { continue; } -#pragma omp task - plunge_with(workers_[worker_id].get()); - } + bnb_worker_t* worker = worker_pool_.get_worker(); + if (worker == nullptr) { continue; } - } else { - std::optional*> start_node = node_queue.pop_diving(); + if (type == EXPLORATION) { + // If there any node left in the heap, we pop the top node and explore it. + std::optional*> start_node = node_queue.pop_best_first(); - if (start_node.has_value()) { - if (get_upper_bound() < start_node.value()->lower_bound) { continue; } + if (start_node.has_value()) { + worker_pool_.return_worker_to_pool(worker); + continue; + } - i_t worker_id = -1; + if (get_upper_bound() < start_node.value()->lower_bound) { + // This node was put on the heap earlier but its lower bound is now greater than the + // current upper bound + 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); + worker_pool_.return_worker_to_pool(worker); + continue; + } - mutex_available_workers_.lock(); - if (available_workers_.size() > 0) { - worker_id = available_workers_.front(); - available_workers_.pop_front(); - } - mutex_available_workers_.unlock(); + worker->init_best_first(start_node.value(), original_lp_); + last_node_depth = start_node.value()->depth; + active_workers_per_type[type]++; + nodes_since_last_log++; - // There is no available workers - if (worker_id < 0) { break; } +#pragma omp task + plunge_with(worker); - bool is_feasible = - workers_[worker_id]->init_diving(start_node.value(), type, original_lp_, settings_); + } else { + std::optional*> start_node = node_queue.pop_diving(); - if (!is_feasible) { - mutex_available_workers_.lock(); - available_workers_.push_back(worker_id); - mutex_available_workers_.unlock(); - continue; - } + if (start_node.has_value()) { + worker_pool_.return_worker_to_pool(worker); + continue; + } - active_workers_per_type[type]++; + if (get_upper_bound() < start_node.value()->lower_bound) { + worker_pool_.return_worker_to_pool(worker); + continue; + } -#pragma omp task - dive_with(workers_[worker_id].get()); - } + bool is_feasible = worker->init_diving(start_node.value(), type, original_lp_, settings_); + + if (!is_feasible) { + worker_pool_.return_worker_to_pool(worker); + continue; } + + active_workers_per_type[type]++; + +#pragma omp task + dive_with(worker); } } } @@ -1436,19 +1399,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - workers_.resize(settings_.num_threads); + worker_pool_.init(settings_.num_threads, original_lp_, Arow_, var_types_, settings_); active_workers_per_type.fill(0); - for (i_t i = 0; i < settings_.num_threads; ++i) { - workers_[i] = - std::make_unique>(i, original_lp_, Arow_, var_types_, settings_); - available_workers_.push_front(i); - } - i_t num_bfs_workers = settings_.bnb_worker_settings[EXPLORATION].num_workers; settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, - num_bfs_workers, - settings_.num_threads - num_bfs_workers); + settings_.num_bfs_workers, + settings_.num_threads - settings_.num_bfs_workers); settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 8a002f946..cf8e57db5 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -22,7 +22,6 @@ #include #include -#include #include namespace cuopt::linear_programming::dual_simplex { @@ -155,9 +154,12 @@ class branch_and_bound_t { // Search tree search_tree_t search_tree_; - // Count the number of tasks per type that either being executed or - // waiting to be executed. - std::array, 5> active_workers_per_type; + // Count the number of workers per type that either are being executed or + // are waiting to be executed. + std::array, bnb_num_worker_types> active_workers_per_type; + + // Worker pool + bnb_worker_pool_t worker_pool_; // Global status of the solver. omp_atomic_t solver_status_; @@ -171,14 +173,6 @@ class branch_and_bound_t { void report_heuristic(f_t obj); void report(std::string symbol, f_t obj, f_t lower_bound, i_t node_depth); - - // Worker pool - std::vector>> workers_; - - // FIXME: Implement a lock-free queue - omp_mutex_t mutex_available_workers_; - std::deque available_workers_; - // Set the final solution. mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 6574d4bfd..ce9460fa9 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -9,18 +9,6 @@ namespace cuopt::linear_programming::dual_simplex { -template -bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type) -{ - return bnb_worker_settings_t{.type = type, - .is_enabled = true, - .num_workers = -1, - .min_node_depth = 0, - .node_limit = 500, - .iteration_limit_factor = 0.05, - .backtrack = 5}; -} - template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, @@ -285,9 +273,6 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE - -template bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type); - template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, const std::vector& root_solution, diff --git a/cpp/src/dual_simplex/diving_heuristics.hpp b/cpp/src/dual_simplex/diving_heuristics.hpp index 02390ae8e..1f44fee31 100644 --- a/cpp/src/dual_simplex/diving_heuristics.hpp +++ b/cpp/src/dual_simplex/diving_heuristics.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -20,9 +20,6 @@ struct branch_variable_t { rounding_direction_t direction; }; -template -bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type); - template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index faa959f9f..8efe6087c 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -1,6 +1,6 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 */ #pragma once @@ -25,12 +25,14 @@ class heap_t { { buffer.push_back(node); std::push_heap(buffer.begin(), buffer.end(), comp); + buffer_size++; } void push(T&& node) { buffer.push_back(std::move(node)); std::push_heap(buffer.begin(), buffer.end(), comp); + buffer_size++; } template @@ -38,6 +40,7 @@ class heap_t { { buffer.emplace_back(std::forward(args)...); std::push_heap(buffer.begin(), buffer.end(), comp); + buffer_size++; } std::optional pop() @@ -47,16 +50,18 @@ class heap_t { std::pop_heap(buffer.begin(), buffer.end(), comp); T node = std::move(buffer.back()); buffer.pop_back(); + buffer_size--; return node; } - size_t size() const { return buffer.size(); } + size_t size() const { return buffer_size.load(); } T& top() { return buffer.front(); } void clear() { buffer.clear(); } bool empty() const { return buffer.empty(); } private: std::vector buffer; + omp_atomic_t buffer_size; Comp comp; }; @@ -133,17 +138,8 @@ class node_queue_t { return std::nullopt; } - i_t diving_queue_size() - { - std::lock_guard lock(mutex); - return diving_heap.size(); - } - - i_t best_first_queue_size() - { - std::lock_guard lock(mutex); - return best_first_heap.size(); - } + size_t diving_queue_size() const { return diving_heap.size(); } + size_t best_first_queue_size() const { return best_first_heap.size(); } f_t get_lower_bound() { diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index d7512e3be..51d59c5ba 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -20,53 +20,21 @@ namespace cuopt::linear_programming::dual_simplex { -// Indicate the search and variable selection algorithms used by each worker -// in B&B (See [1]). -// -// [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, -// Berlin, 2007. doi: 10.14279/depositonce-1634. -enum bnb_worker_type_t { - EXPLORATION = 0, // Best-First + Plunging. - PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) - LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) - GUIDED_DIVING = 3, // Guided diving (9.2.3). If no incumbent is found yet, use pseudocost diving. - COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1) -}; - -// Settings for each worker in B&B. template -struct bnb_worker_settings_t { - // Type of the worker. - bnb_worker_type_t type; - - // Is this worker enabled? - // This will be ignored if `type == EXPLORATION`. - bool is_enabled; - - // Number of workers of this type. - i_t num_workers; - - // Minimum node depth to start this worker - // This will be ignored if `type == EXPLORATION`. - i_t min_node_depth; - - // Maximum number of nodes explored in this worker. - i_t node_limit; - - // Maximum fraction of the number of simplex iterations for this worker - // compared to the number of simplex iterations for normal exploration. - // This will be ignored if `type == EXPLORATION`. - f_t iteration_limit_factor; - - // Number of nodes that it allows to backtrack when - // reaching the bottom of a given branch of the tree. - // This will be ignored if `type == EXPLORATION`. - i_t backtrack; +struct diving_heuristics_settings_t { + i_t num_diving_workers = -1; + + bool disable_line_search_diving = false; + bool disable_pseudocost_diving = false; + bool disable_guided_diving = false; + bool disable_coefficient_diving = false; + + i_t min_node_depth = 0; + i_t node_limit = 500; + f_t iteration_limit_factor = 0.05; + i_t backtrack = 5; }; -template -bnb_worker_settings_t get_default_diving_settings(bnb_worker_type_t type); - template struct simplex_solver_settings_t { public: @@ -117,66 +85,21 @@ struct simplex_solver_settings_t { refactor_frequency(100), iteration_log_frequency(1000), first_iteration_log(2), + num_threads(omp_get_max_threads() - 1), + num_bfs_workers(std::min(num_threads / 4, 1)), random_seed(0), inside_mip(0), solution_callback(nullptr), heuristic_preemption_callback(nullptr), concurrent_halt(nullptr) { - bnb_worker_settings[EXPLORATION] = - bnb_worker_settings_t{.type = EXPLORATION, - .is_enabled = true, - .num_workers = -1, - .min_node_depth = 0, - .node_limit = std::numeric_limits::max(), - .iteration_limit_factor = std::numeric_limits::max(), - .backtrack = 1}; - - bnb_worker_settings[PSEUDOCOST_DIVING] = - get_default_diving_settings(PSEUDOCOST_DIVING); - - bnb_worker_settings[LINE_SEARCH_DIVING] = - get_default_diving_settings(LINE_SEARCH_DIVING); - - bnb_worker_settings[GUIDED_DIVING] = get_default_diving_settings(GUIDED_DIVING); - - bnb_worker_settings[COEFFICIENT_DIVING] = - get_default_diving_settings(COEFFICIENT_DIVING); - - set_bnb_tasks(omp_get_max_threads() - 1); - } - - void set_bnb_tasks(i_t num_threads) - { - this->num_threads = num_threads; - bnb_worker_settings[EXPLORATION].num_workers = std::max(1, num_threads / 2); - - i_t diving_tasks = num_threads - bnb_worker_settings[EXPLORATION].num_workers; - i_t num_enabled = 0; - - for (size_t i = 1; i < bnb_worker_settings.size(); ++i) { - num_enabled += static_cast(bnb_worker_settings[i].is_enabled); - } - - for (size_t i = 1, k = 0; i < bnb_worker_settings.size(); ++i) { - i_t start = (double)k * diving_tasks / num_enabled; - i_t end = (double)(k + 1) * diving_tasks / num_enabled; - - if (bnb_worker_settings[i].is_enabled) { - bnb_worker_settings[i].num_workers = 2 * (end - start); - ++k; - - } else { - bnb_worker_settings[i].num_workers = 0; - } - } + diving_settings.num_diving_workers = std::max(num_threads - num_bfs_workers, 1); } void set_log(bool logging) const { log.log = logging; } void enable_log_to_file() { log.enable_log_to_file(); } void set_log_filename(const std::string& log_filename) { log.set_log_file(log_filename); } void close_log_file() { log.close_log_file(); } - i_t iteration_limit; i_t node_limit; f_t time_limit; @@ -230,10 +153,9 @@ struct simplex_solver_settings_t { i_t first_iteration_log; // number of iterations to log at beginning of solve i_t num_threads; // number of threads to use i_t random_seed; // random seed + i_t num_bfs_workers; // number of threads dedicated to the best-first search - // Indicate the settings used by each task - // The position in the array is indicated by the `bnb_task_type_t`. - std::array, 5> bnb_worker_settings; + diving_heuristics_settings_t diving_settings; // Settings for the diving heuristics i_t inside_mip; // 0 if outside MIP, 1 if inside MIP at root node, 2 if inside MIP at leaf node std::function&, f_t)> solution_callback; diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 0461167a8..8822efb47 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -256,16 +256,21 @@ void rins_t::run_rins() branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = 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.integer_tol = context.settings.tolerances.integrality_tolerance; + branch_and_bound_settings.num_threads = 2; + branch_and_bound_settings.num_bfs_workers = 1; // In the future, let RINS use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.bnb_worker_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = false; - branch_and_bound_settings.bnb_worker_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = - false; - branch_and_bound_settings.bnb_worker_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = - false; - branch_and_bound_settings.set_bnb_tasks(2); + branch_and_bound_settings.diving_settings.num_diving_workers = 1; + branch_and_bound_settings.diving_settings.disable_line_search_diving = true; + branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; + + if (context.settings.disable_guided_diving) { + branch_and_bound_settings.diving_settings.disable_guided_diving = true; + } else { + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + } branch_and_bound_settings.log.log = false; branch_and_bound_settings.log.log_prefix = "[RINS] "; diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index fc28af783..1efed74ce 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -101,17 +101,21 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.print_presolve_stats = false; branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; 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.integer_tol = context.settings.tolerances.integrality_tolerance; + branch_and_bound_settings.num_threads = 2; + branch_and_bound_settings.num_bfs_workers = 1; // In the future, let SubMIP use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.bnb_worker_settings[dual_simplex::PSEUDOCOST_DIVING].is_enabled = - false; - branch_and_bound_settings.bnb_worker_settings[dual_simplex::LINE_SEARCH_DIVING].is_enabled = - false; - branch_and_bound_settings.bnb_worker_settings[dual_simplex::COEFFICIENT_DIVING].is_enabled = - false; - branch_and_bound_settings.set_bnb_tasks(2); + branch_and_bound_settings.diving_settings.num_diving_workers = 1; + branch_and_bound_settings.diving_settings.disable_line_search_diving = true; + branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; + + if (context.settings.disable_guided_diving) { + branch_and_bound_settings.diving_settings.disable_guided_diving = true; + } else { + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + } branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index a77ede1a2..00292c428 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -176,10 +176,17 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.diving_settings.disable_line_search_diving = context.settings.disable_line_search_diving; - i_t num_threads = context.settings.num_cpu_threads < 0 - ? omp_get_max_threads() - 1 - : std::max(1, context.settings.num_cpu_threads); - branch_and_bound_settings.set_bnb_tasks(num_threads); + if (context.settings.num_cpu_threads < 0) { + branch_and_bound_settings.num_threads = omp_get_max_threads() - 1; + } else { + branch_and_bound_settings.num_threads = std::max(1, context.settings.num_cpu_threads); + } + + i_t num_threads = branch_and_bound_settings.num_threads; + i_t num_bfs_threads = std::max(1, num_threads / 4); + i_t num_diving_threads = std::max(1, num_threads - num_bfs_threads); + branch_and_bound_settings.num_bfs_workers = num_bfs_threads; + branch_and_bound_settings.diving_settings.num_diving_workers = num_diving_threads; // Set the branch and bound -> primal heuristics callback branch_and_bound_settings.solution_callback = diff --git a/cpp/src/utilities/omp_helpers.hpp b/cpp/src/utilities/omp_helpers.hpp index 33eda66cb..e1b68bf88 100644 --- a/cpp/src/utilities/omp_helpers.hpp +++ b/cpp/src/utilities/omp_helpers.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -53,7 +53,7 @@ class omp_atomic_t { T operator--() { return fetch_sub(T(1)) - 1; } T operator--(int) { return fetch_sub(T(1)); } - T load() + T load() const { T res; #pragma omp atomic read From 25b0bed9c16f9cd249fce75d8b54d692b320146a Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Jan 2026 16:01:08 +0100 Subject: [PATCH 40/70] several bug fixes --- cpp/src/dual_simplex/bnb_worker.hpp | 1 + cpp/src/dual_simplex/branch_and_bound.cpp | 12 +++++++----- cpp/src/dual_simplex/branch_and_bound.hpp | 1 + cpp/src/dual_simplex/node_queue.hpp | 20 ++++++++++++-------- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index f2ec37ca1..b30f6d485 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -113,6 +113,7 @@ class bnb_worker_pool_t { const std::vector& var_type, const simplex_solver_settings_t& settings) { + workers_.resize(num_workers); for (i_t i = 0; i < num_workers; ++i) { workers_[i] = std::make_unique>(i, original_lp, Arow, var_type, settings); diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index dcf0be8a2..9bd423ff7 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -946,6 +946,7 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) ++exploration_stats_.nodes_explored; --exploration_stats_.nodes_unexplored; + ++nodes_since_last_log; if (status == node_solve_info_t::TIME_LIMIT) { break; @@ -1058,8 +1059,7 @@ void branch_and_bound_t::master_loop() f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); i_t last_node_depth = 0; - f_t last_log = 0.0; - i_t nodes_since_last_log = 0; + f_t last_log = 0.0; diving_heuristics_settings_t diving_settings = settings_.diving_settings; if (!std::isfinite(upper_bound)) { diving_settings.disable_guided_diving = true; } @@ -1096,11 +1096,13 @@ void branch_and_bound_t::master_loop() (time_since_last_log > 30) || now > settings_.time_limit) { i_t depth = node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; - report(" ", upper_bound, lower_bound, depth); + report(" ", upper_bound, lower_bound, depth); last_log = tic(); nodes_since_last_log = 0; } + if (now > settings_.time_limit) { break; } + // There is no node in the queue, so we suspend temporarily the execution of the master // so it can execute a worker task instead. if (node_queue.best_first_queue_size() == 0 && node_queue.diving_queue_size() == 0) { @@ -1118,7 +1120,7 @@ void branch_and_bound_t::master_loop() // If there any node left in the heap, we pop the top node and explore it. std::optional*> start_node = node_queue.pop_best_first(); - if (start_node.has_value()) { + if (!start_node.has_value()) { worker_pool_.return_worker_to_pool(worker); continue; } @@ -1144,7 +1146,7 @@ void branch_and_bound_t::master_loop() } else { std::optional*> start_node = node_queue.pop_diving(); - if (start_node.has_value()) { + if (!start_node.has_value()) { worker_pool_.return_worker_to_pool(worker); continue; } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index cf8e57db5..017fd9efc 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -165,6 +165,7 @@ class branch_and_bound_t { omp_atomic_t solver_status_; omp_atomic_t should_report_; + omp_atomic_t nodes_since_last_log; // In case, a best-first thread encounters a numerical issue when solving a node, // its blocks the progression of the lower bound. diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 8efe6087c..47b31f590 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -25,14 +25,12 @@ class heap_t { { buffer.push_back(node); std::push_heap(buffer.begin(), buffer.end(), comp); - buffer_size++; } void push(T&& node) { buffer.push_back(std::move(node)); std::push_heap(buffer.begin(), buffer.end(), comp); - buffer_size++; } template @@ -40,7 +38,6 @@ class heap_t { { buffer.emplace_back(std::forward(args)...); std::push_heap(buffer.begin(), buffer.end(), comp); - buffer_size++; } std::optional pop() @@ -50,18 +47,16 @@ class heap_t { std::pop_heap(buffer.begin(), buffer.end(), comp); T node = std::move(buffer.back()); buffer.pop_back(); - buffer_size--; return node; } - size_t size() const { return buffer_size.load(); } + size_t size() const { return buffer.size(); } T& top() { return buffer.front(); } void clear() { buffer.clear(); } bool empty() const { return buffer.empty(); } private: std::vector buffer; - omp_atomic_t buffer_size; Comp comp; }; @@ -138,8 +133,17 @@ class node_queue_t { return std::nullopt; } - size_t diving_queue_size() const { return diving_heap.size(); } - size_t best_first_queue_size() const { return best_first_heap.size(); } + i_t diving_queue_size() + { + std::lock_guard lock(mutex); + return diving_heap.size(); + } + + i_t best_first_queue_size() + { + std::lock_guard lock(mutex); + return best_first_heap.size(); + } f_t get_lower_bound() { From 25dac0283cdac7eb1e6e3ba4437ea775f88d8aab Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Jan 2026 16:14:01 +0100 Subject: [PATCH 41/70] handle master suspension. added overdecomposition. --- cpp/src/dual_simplex/bnb_worker.hpp | 6 +++--- cpp/src/dual_simplex/branch_and_bound.cpp | 24 ++++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index b30f6d485..2cc48a271 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -184,14 +184,14 @@ std::array bnb_get_num_workers_round_robin( auto worker_types = bnb_get_worker_types(settings); max_num_workers.fill(0); - max_num_workers[EXPLORATION] = std::max(1, num_threads / 4); + max_num_workers[EXPLORATION] = std::max(1, num_threads / 2); - i_t diving_workers = settings.num_diving_workers; + i_t diving_workers = 2 * settings.num_diving_workers; i_t m = worker_types.size() - 1; for (size_t i = 1, k = 0; i < bnb_num_worker_types; ++i) { i_t start = (double)k * diving_workers / m; i_t end = (double)(k + 1) * diving_workers / m; - max_num_workers[i] = 2 * (end - start); + max_num_workers[i] = end - start; ++k; } diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9bd423ff7..d0b376ef6 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1071,10 +1071,11 @@ void branch_and_bound_t::master_loop() while (solver_status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_workers_per_type[0] > 0 || node_queue.best_first_queue_size() > 0)) { - lower_bound = get_lower_bound(); - upper_bound = get_upper_bound(); - abs_gap = upper_bound - lower_bound; - rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + bool launched_any_task = false; + lower_bound = get_lower_bound(); + upper_bound = get_upper_bound(); + abs_gap = upper_bound - lower_bound; + rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); repair_heuristic_solutions(); @@ -1103,13 +1104,6 @@ void branch_and_bound_t::master_loop() if (now > settings_.time_limit) { break; } - // There is no node in the queue, so we suspend temporarily the execution of the master - // so it can execute a worker task instead. - if (node_queue.best_first_queue_size() == 0 && node_queue.diving_queue_size() == 0) { -#pragma omp taskyield - continue; - } - for (auto type : worker_types) { if (active_workers_per_type[type] >= max_num_workers_per_type[type]) { continue; } @@ -1139,6 +1133,7 @@ void branch_and_bound_t::master_loop() last_node_depth = start_node.value()->depth; active_workers_per_type[type]++; nodes_since_last_log++; + launched_any_task = true; #pragma omp task plunge_with(worker); @@ -1164,11 +1159,18 @@ void branch_and_bound_t::master_loop() } active_workers_per_type[type]++; + launched_any_task = true; #pragma omp task dive_with(worker); } } + + // If no new task was launched in this iteration, suspend temporarily the execution of the + // master + if (!launched_any_task) { +#pragma omp taskyield + } } } From 85eab6467d05440851a8510bc67d02aeefadb88d Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Jan 2026 17:11:25 +0100 Subject: [PATCH 42/70] fix incorrect number of workers --- cpp/src/dual_simplex/branch_and_bound.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d0b376ef6..22d67d7b3 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1086,6 +1086,7 @@ void branch_and_bound_t::master_loop() diving_settings.disable_guided_diving = settings_.diving_settings.disable_guided_diving; max_num_workers_per_type = bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); + worker_types = bnb_get_worker_types(diving_settings); } } @@ -1403,7 +1404,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - worker_pool_.init(settings_.num_threads, original_lp_, Arow_, var_types_, settings_); + worker_pool_.init(2 * settings_.num_threads, original_lp_, Arow_, var_types_, settings_); active_workers_per_type.fill(0); settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", From 227d7f83643da487ab7169117df6c7c7ae0f18e5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Jan 2026 17:18:43 +0100 Subject: [PATCH 43/70] removed ramp-up phase --- cpp/src/dual_simplex/branch_and_bound.cpp | 90 ----------------------- 1 file changed, 90 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 22d67d7b3..c14b17dc3 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -307,7 +307,6 @@ 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; crush_primal_solution( original_problem_, original_lp_, solution, new_slacks_, crushed_solution); @@ -802,95 +801,6 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* } } -// template -// void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, -// i_t initial_heap_size) -// { -// if (solver_status_ != mip_exploration_status_t::RUNNING) { return; } - -// // Note that we do not know which thread will execute the -// // `exploration_ramp_up` task, so we allow to any thread -// // to repair the heuristic solution. -// repair_heuristic_solutions(); - -// i_t tid = omp_get_thread_num(); -// bnb_worker_data_t* worker_data = get_worker_data(tid); -// assert(worker_data); - -// f_t lower_bound = node->lower_bound; -// f_t upper_bound = get_upper_bound(); -// f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); -// f_t abs_gap = upper_bound - lower_bound; - -// if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { -// search_tree_.graphviz_node(settings_.log, node, "cutoff", node->lower_bound); -// search_tree_.update(node, node_status_t::FATHOMED); -// --exploration_stats_.nodes_unexplored; -// return; -// } - -// f_t now = toc(exploration_stats_.start_time); -// f_t time_since_last_log = -// exploration_stats_.last_log == 0 ? 1.0 : toc(exploration_stats_.last_log); - -// if (((exploration_stats_.nodes_since_last_log >= 10 || -// abs_gap < 10 * settings_.absolute_mip_gap_tol) && -// (time_since_last_log >= 1)) || -// (time_since_last_log > 30) || now > settings_.time_limit) { -// bool should_report = should_report_.exchange(false); - -// if (should_report) { -// report(" ", upper_bound, root_objective_, node->depth); -// exploration_stats_.nodes_since_last_log = 0; -// exploration_stats_.last_log = tic(); -// should_report_ = true; -// } -// } - -// if (now > settings_.time_limit) { -// solver_status_ = mip_exploration_status_t::TIME_LIMIT; -// return; -// } - -// worker_data->recompute_basis = true; -// worker_data->recompute_bounds = true; - -// node_solve_info_t status = solve_node(node, -// search_tree_, -// bnb_worker_type_t::EXPLORATION, -// worker_data, -// original_lp_.lower, -// original_lp_.upper, -// exploration_stats_, -// settings_.log); - -// ++exploration_stats_.nodes_since_last_log; -// ++exploration_stats_.nodes_explored; -// --exploration_stats_.nodes_unexplored; - -// if (status == node_solve_info_t::TIME_LIMIT) { -// solver_status_ = mip_exploration_status_t::TIME_LIMIT; -// return; - -// } else if (has_children(status)) { -// exploration_stats_.nodes_unexplored += 2; - -// // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase -// if (exploration_stats_.nodes_unexplored < initial_heap_size) { -// #pragma omp task -// exploration_ramp_up(node->get_down_child(), initial_heap_size); - -// #pragma omp task -// exploration_ramp_up(node->get_up_child(), initial_heap_size); - -// } else { -// // We've generated enough nodes, push further nodes onto the heap -// node_queue.push(node->get_down_child()); -// node_queue.push(node->get_up_child()); -// } -// } -// } - template void branch_and_bound_t::plunge_with(bnb_worker_t* worker) { From 5aecd63f29c8b45997a8ec93ac21159ae1ff7551 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Jan 2026 17:35:11 +0100 Subject: [PATCH 44/70] added some comments --- cpp/src/dual_simplex/bnb_worker.hpp | 3 ++- cpp/src/dual_simplex/branch_and_bound.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index 2cc48a271..a7a94761d 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -158,7 +158,8 @@ class bnb_worker_pool_t { // Worker pool std::vector>> workers_; - // FIXME: Implement a lock-free queue + // FIXME: Implement a lock-free queue (it can also be used for + // passing feasible solutions between bnb and heuristics) omp_mutex_t mutex_; std::deque available_workers_; }; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index c14b17dc3..71d912e21 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -839,6 +839,7 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) solver_status_ = mip_exploration_status_t::TIME_LIMIT; break; } + if (exploration_stats_.nodes_explored >= settings_.node_limit) { solver_status_ = mip_exploration_status_t::NODE_LIMIT; break; From 8dd2d081192854bfab7f71d0c92b42dc4361ec84 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 11:16:53 +0100 Subject: [PATCH 45/70] fix incorrect termination status --- cpp/src/dual_simplex/bnb_worker.hpp | 9 +++-- cpp/src/dual_simplex/branch_and_bound.cpp | 42 ++++++++++------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index a7a94761d..2f1a363b0 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -121,7 +121,7 @@ class bnb_worker_pool_t { } } - bnb_worker_t* get_worker() + bnb_worker_t* get_idle_worker() { std::lock_guard lock(mutex_); @@ -129,11 +129,16 @@ class bnb_worker_pool_t { return nullptr; } else { i_t idx = available_workers_.front(); - available_workers_.pop_front(); return workers_[idx].get(); } } + void pop_idle_worker() + { + std::lock_guard lock(mutex_); + if (!available_workers_.empty()) { available_workers_.pop_front(); } + } + void return_worker_to_pool(bnb_worker_t* worker) { worker->is_active = false; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 71d912e21..21ceb3ea2 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1014,33 +1014,34 @@ void branch_and_bound_t::master_loop() nodes_since_last_log = 0; } - if (now > settings_.time_limit) { break; } + if (now > settings_.time_limit) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; + break; + } for (auto type : worker_types) { if (active_workers_per_type[type] >= max_num_workers_per_type[type]) { continue; } - bnb_worker_t* worker = worker_pool_.get_worker(); - if (worker == nullptr) { continue; } + // Get an idle worker. + bnb_worker_t* worker = worker_pool_.get_idle_worker(); + if (worker == nullptr) { break; } if (type == EXPLORATION) { // If there any node left in the heap, we pop the top node and explore it. std::optional*> start_node = node_queue.pop_best_first(); - if (!start_node.has_value()) { - worker_pool_.return_worker_to_pool(worker); - continue; - } - + if (!start_node.has_value()) { continue; } if (get_upper_bound() < start_node.value()->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound 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); - worker_pool_.return_worker_to_pool(worker); 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; active_workers_per_type[type]++; @@ -1053,23 +1054,14 @@ void branch_and_bound_t::master_loop() } else { std::optional*> start_node = node_queue.pop_diving(); - if (!start_node.has_value()) { - worker_pool_.return_worker_to_pool(worker); - continue; - } - - if (get_upper_bound() < start_node.value()->lower_bound) { - worker_pool_.return_worker_to_pool(worker); - continue; - } + if (!start_node.has_value()) { continue; } + if (get_upper_bound() < start_node.value()->lower_bound) { continue; } bool is_feasible = worker->init_diving(start_node.value(), type, original_lp_, settings_); + if (!is_feasible) { continue; } - if (!is_feasible) { - worker_pool_.return_worker_to_pool(worker); - continue; - } - + // Remove the worker from the idle list. + worker_pool_.pop_idle_worker(); active_workers_per_type[type]++; launched_any_task = true; @@ -1084,6 +1076,10 @@ void branch_and_bound_t::master_loop() #pragma omp taskyield } } + + if (solver_status_ == mip_exploration_status_t::RUNNING) { + solver_status_ = mip_exploration_status_t::COMPLETED; + } } template From 1dcee0349b12f52b6e7520dbfd5cd881ba7c6dd6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 12:33:32 +0100 Subject: [PATCH 46/70] replace upper bound lock with atomic --- cpp/src/dual_simplex/branch_and_bound.cpp | 51 +++++++++-------------- cpp/src/dual_simplex/branch_and_bound.hpp | 3 +- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 21ceb3ea2..538555225 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -236,15 +236,6 @@ branch_and_bound_t::branch_and_bound_t( mutex_upper_.unlock(); } -template -f_t branch_and_bound_t::get_upper_bound() -{ - mutex_upper_.lock(); - const f_t upper_bound = upper_bound_; - mutex_upper_.unlock(); - return upper_bound; -} - template f_t branch_and_bound_t::get_lower_bound() { @@ -469,11 +460,10 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t::set_final_solution(mip_solution_t 0 && exploration_stats_.nodes_unexplored == 0 && - upper_bound == inf) { + upper_bound_ == inf) { settings_.log.printf("Integer infeasible.\n"); mip_status = mip_status_t::INFEASIBLE; if (settings_.heuristic_preemption_callback != nullptr) { @@ -512,7 +502,7 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t::solve_node(mip_node_t* logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; - const f_t upper_bound = get_upper_bound(); lp_problem_t& leaf_problem = worker->leaf_problem; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); @@ -633,7 +622,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* simplex_solver_settings_t lp_settings = settings_; lp_settings.set_log(false); - lp_settings.cut_off = upper_bound + settings_.dual_tol; + lp_settings.cut_off = upper_bound_ + settings_.dual_tol; lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); lp_settings.scale_columns = false; @@ -722,7 +711,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* } else if (lp_status == dual::status_t::CUTOFF) { // Node was cut off. Do not branch - node_ptr->lower_bound = upper_bound; + node_ptr->lower_bound = upper_bound_; f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); search_tree.graphviz_node(log, node_ptr, "cut off", leaf_objective); search_tree.update(node_ptr, node_status_t::FATHOMED); @@ -754,7 +743,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* search_tree.update(node_ptr, node_status_t::INTEGER_FEASIBLE); return node_solve_info_t::NO_CHILDREN; - } else if (leaf_objective <= upper_bound + abs_fathom_tol) { + } else if (leaf_objective <= upper_bound_ + abs_fathom_tol) { // Choose fractional variable to branch on auto [branch_var, round_dir] = variable_selection( node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); @@ -815,7 +804,7 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) stack.pop_front(); f_t lower_bound = node_ptr->lower_bound; - f_t upper_bound = get_upper_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: @@ -917,7 +906,7 @@ void branch_and_bound_t::dive_with(bnb_worker_t* worker) stack.pop_front(); f_t lower_bound = node_ptr->lower_bound; - f_t upper_bound = get_upper_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; @@ -965,15 +954,14 @@ template void branch_and_bound_t::master_loop() { f_t lower_bound = get_lower_bound(); - f_t upper_bound = get_upper_bound(); - f_t abs_gap = upper_bound - lower_bound; - f_t rel_gap = user_relative_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_.load(), lower_bound); i_t last_node_depth = 0; f_t last_log = 0.0; diving_heuristics_settings_t diving_settings = settings_.diving_settings; - if (!std::isfinite(upper_bound)) { diving_settings.disable_guided_diving = true; } + if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } auto worker_types = bnb_get_worker_types(diving_settings); auto max_num_workers_per_type = @@ -984,16 +972,15 @@ void branch_and_bound_t::master_loop() (active_workers_per_type[0] > 0 || node_queue.best_first_queue_size() > 0)) { bool launched_any_task = false; lower_bound = get_lower_bound(); - upper_bound = get_upper_bound(); - abs_gap = upper_bound - lower_bound; - rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + abs_gap = upper_bound_ - 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, // re-enable as soon as a new incumbent is found. if (settings_.diving_settings.disable_guided_diving != diving_settings.disable_guided_diving) { - if (std::isfinite(upper_bound)) { + if (std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = settings_.diving_settings.disable_guided_diving; max_num_workers_per_type = bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); @@ -1009,7 +996,7 @@ void branch_and_bound_t::master_loop() (time_since_last_log > 30) || now > settings_.time_limit) { i_t depth = node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; - report(" ", upper_bound, lower_bound, depth); + report(" ", upper_bound_, lower_bound, depth); last_log = tic(); nodes_since_last_log = 0; } @@ -1031,7 +1018,7 @@ void branch_and_bound_t::master_loop() std::optional*> start_node = node_queue.pop_best_first(); if (!start_node.has_value()) { continue; } - if (get_upper_bound() < start_node.value()->lower_bound) { + if (upper_bound_ < start_node.value()->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound search_tree_.graphviz_node( @@ -1055,7 +1042,7 @@ void branch_and_bound_t::master_loop() std::optional*> start_node = node_queue.pop_diving(); if (!start_node.has_value()) { continue; } - if (get_upper_bound() < start_node.value()->lower_bound) { continue; } + if (upper_bound_ < start_node.value()->lower_bound) { continue; } bool is_feasible = worker->init_diving(start_node.value(), type, original_lp_, settings_); if (!is_feasible) { continue; } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 017fd9efc..0b64bf06f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -95,7 +95,6 @@ class branch_and_bound_t { f_t& repaired_obj, std::vector& repaired_solution) const; - f_t get_upper_bound(); f_t get_lower_bound(); bool enable_concurrent_lp_root_solve() const { return enable_concurrent_lp_root_solve_; } std::atomic* get_root_concurrent_halt() { return &root_concurrent_halt_; } @@ -122,7 +121,7 @@ class branch_and_bound_t { omp_mutex_t mutex_upper_; // Global variable for upper bound - f_t upper_bound_; + omp_atomic_t upper_bound_; // Global variable for incumbent. The incumbent should be updated with the upper bound mip_solution_t incumbent_; From 9e2e5c7cae2a74d9c5db48aabb8f2f9b19e6ad41 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 15:06:38 +0100 Subject: [PATCH 47/70] improve idling master thread --- cpp/src/dual_simplex/bnb_worker.hpp | 15 +++++++-------- cpp/src/dual_simplex/branch_and_bound.cpp | 17 ++++++++++------- cpp/src/dual_simplex/branch_and_bound.hpp | 2 -- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index 2f1a363b0..f7f1a6783 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -117,7 +117,7 @@ class bnb_worker_pool_t { for (i_t i = 0; i < num_workers; ++i) { workers_[i] = std::make_unique>(i, original_lp, Arow, var_type, settings); - available_workers_.push_front(i); + idle_workers_.push_front(i); } } @@ -125,10 +125,10 @@ class bnb_worker_pool_t { { std::lock_guard lock(mutex_); - if (available_workers_.empty()) { + if (idle_workers_.empty()) { return nullptr; } else { - i_t idx = available_workers_.front(); + i_t idx = idle_workers_.front(); return workers_[idx].get(); } } @@ -136,14 +136,14 @@ class bnb_worker_pool_t { void pop_idle_worker() { std::lock_guard lock(mutex_); - if (!available_workers_.empty()) { available_workers_.pop_front(); } + if (!idle_workers_.empty()) { idle_workers_.pop_front(); } } void return_worker_to_pool(bnb_worker_t* worker) { worker->is_active = false; std::lock_guard lock(mutex_); - available_workers_.push_back(worker->worker_id); + idle_workers_.push_back(worker->worker_id); } f_t get_lower_bounds() @@ -163,10 +163,9 @@ class bnb_worker_pool_t { // Worker pool std::vector>> workers_; - // FIXME: Implement a lock-free queue (it can also be used for - // passing feasible solutions between bnb and heuristics) omp_mutex_t mutex_; - std::deque available_workers_; + std::deque idle_workers_; + omp_atomic_t num_idle_workers_; }; template diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 538555225..573fbc657 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -957,8 +957,7 @@ void branch_and_bound_t::master_loop() 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; - - f_t last_log = 0.0; + f_t last_log = 0.0; diving_heuristics_settings_t diving_settings = settings_.diving_settings; if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } @@ -1035,7 +1034,7 @@ void branch_and_bound_t::master_loop() nodes_since_last_log++; launched_any_task = true; -#pragma omp task +#pragma omp task affinity(worker) plunge_with(worker); } else { @@ -1052,15 +1051,20 @@ void branch_and_bound_t::master_loop() active_workers_per_type[type]++; launched_any_task = true; -#pragma omp task +#pragma omp task affinity(worker) dive_with(worker); } } - // If no new task was launched in this iteration, suspend temporarily the execution of the - // master + // If no new task was launched in this iteration, suspend temporarily the + // execution of the master. As of 8/Jan/2026, GCC does not + // implement taskyield, but LLVM does. if (!launched_any_task) { +#ifndef __GNUC__ #pragma omp taskyield +#else + std::this_thread::sleep_for(std::chrono::milliseconds(1)); +#endif } } @@ -1314,7 +1318,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_unexplored = 2; solver_status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; - should_report_ = true; auto down_child = search_tree_.root.get_down_child(); auto up_child = search_tree_.root.get_up_child(); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 0b64bf06f..f721951c8 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -162,8 +162,6 @@ class branch_and_bound_t { // Global status of the solver. omp_atomic_t solver_status_; - - omp_atomic_t should_report_; omp_atomic_t nodes_since_last_log; // In case, a best-first thread encounters a numerical issue when solving a node, From 34f7eaae313ddb63739f3f0ac2cda526810b8f66 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 16:28:17 +0100 Subject: [PATCH 48/70] added ramp-up-phase --- cpp/src/dual_simplex/bnb_worker.hpp | 23 ++++++- cpp/src/dual_simplex/branch_and_bound.cpp | 65 +++++++++++++++---- cpp/src/dual_simplex/branch_and_bound.hpp | 8 +-- .../dual_simplex/simplex_solver_settings.hpp | 2 +- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index f7f1a6783..00352d50a 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -114,6 +114,7 @@ class bnb_worker_pool_t { 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); @@ -136,7 +137,24 @@ class bnb_worker_pool_t { void pop_idle_worker() { std::lock_guard lock(mutex_); - if (!idle_workers_.empty()) { idle_workers_.pop_front(); } + if (!idle_workers_.empty()) { + idle_workers_.pop_front(); + num_idle_workers_--; + } + } + + bnb_worker_t* get_and_pop_idle_worker() + { + std::lock_guard lock(mutex_); + + if (idle_workers_.empty()) { + return nullptr; + } else { + i_t idx = idle_workers_.front(); + idle_workers_.pop_front(); + num_idle_workers_--; + return workers_[idx].get(); + } } void return_worker_to_pool(bnb_worker_t* worker) @@ -144,6 +162,7 @@ class bnb_worker_pool_t { worker->is_active = false; std::lock_guard lock(mutex_); idle_workers_.push_back(worker->worker_id); + num_idle_workers_++; } f_t get_lower_bounds() @@ -159,6 +178,8 @@ class bnb_worker_pool_t { return lower_bound; } + i_t num_idle_workers() { return num_idle_workers_; } + private: // Worker pool std::vector>> workers_; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 573fbc657..172dc963d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -846,7 +846,7 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) ++exploration_stats_.nodes_explored; --exploration_stats_.nodes_unexplored; - ++nodes_since_last_log; + ++nodes_since_last_log_; if (status == node_solve_info_t::TIME_LIMIT) { break; @@ -865,10 +865,20 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) exploration_stats_.nodes_unexplored += 2; if (status == node_solve_info_t::UP_CHILD_FIRST) { - stack.push_front(node_ptr->get_down_child()); + if (node_queue.best_first_queue_size() < min_node_queue_size_) { + node_queue.push(node_ptr->get_down_child()); + } else { + stack.push_front(node_ptr->get_down_child()); + } + stack.push_front(node_ptr->get_up_child()); } else { - stack.push_front(node_ptr->get_up_child()); + if (node_queue.best_first_queue_size() < min_node_queue_size_) { + node_queue.push(node_ptr->get_up_child()); + } else { + stack.push_front(node_ptr->get_up_child()); + } + stack.push_front(node_ptr->get_down_child()); } } @@ -960,11 +970,12 @@ void branch_and_bound_t::master_loop() f_t last_log = 0.0; diving_heuristics_settings_t diving_settings = settings_.diving_settings; - if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } + bool is_ramp_up_finished = false; - auto worker_types = bnb_get_worker_types(diving_settings); - auto max_num_workers_per_type = - bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); + std::vector worker_types = {EXPLORATION}; + std::array max_num_workers_per_type; + max_num_workers_per_type.fill(0); + max_num_workers_per_type[EXPLORATION] = settings_.num_threads; while (solver_status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && @@ -976,6 +987,29 @@ void branch_and_bound_t::master_loop() repair_heuristic_solutions(); + if (!is_ramp_up_finished) { + if (node_queue.best_first_queue_size() >= min_node_queue_size_ && + node_queue.bfs_top()->depth >= diving_settings.min_node_depth) { + if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } + max_num_workers_per_type = + bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); + worker_types = bnb_get_worker_types(diving_settings); + is_ramp_up_finished = true; + +#ifdef CUOPT_LOG_DEBUG + settings_.log.debug("Ramp-up phase is finished. num active workers = %d, heap size = %d\n", + active_workers_per_type[EXPLORATION], + node_queue.best_first_queue_size()); + + for (auto type : worker_types) { + settings_.log.debug("%s: max num of workers = %d", + feasible_solution_symbol(type), + max_num_workers_per_type[type]); + } +#endif + } + } + // If the guided diving was disabled previously due to the lack of an incumbent solution, // re-enable as soon as a new incumbent is found. if (settings_.diving_settings.disable_guided_diving != diving_settings.disable_guided_diving) { @@ -984,20 +1018,28 @@ void branch_and_bound_t::master_loop() max_num_workers_per_type = bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); worker_types = bnb_get_worker_types(diving_settings); + +#ifdef CUOPT_LOG_DEBUG + for (auto type : worker_types) { + settings_.log.debug("%s: max num of workers = %d", + feasible_solution_symbol(type), + max_num_workers_per_type[type]); + } +#endif } } f_t now = toc(exploration_stats_.start_time); f_t time_since_last_log = last_log == 0 ? 1.0 : toc(last_log); - if (((nodes_since_last_log >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && + if (((nodes_since_last_log_ >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && time_since_last_log >= 1) || (time_since_last_log > 30) || now > settings_.time_limit) { i_t depth = node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; report(" ", upper_bound_, lower_bound, depth); - last_log = tic(); - nodes_since_last_log = 0; + last_log = tic(); + nodes_since_last_log_ = 0; } if (now > settings_.time_limit) { @@ -1031,7 +1073,7 @@ void branch_and_bound_t::master_loop() worker->init_best_first(start_node.value(), original_lp_); last_node_depth = start_node.value()->depth; active_workers_per_type[type]++; - nodes_since_last_log++; + nodes_since_last_log_++; launched_any_task = true; #pragma omp task affinity(worker) @@ -1318,6 +1360,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_unexplored = 2; solver_status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; + min_node_queue_size_ = 2 * settings_.num_threads; auto down_child = search_tree_.root.get_down_child(); auto up_child = search_tree_.root.get_up_child(); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index f721951c8..1930b2cb6 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -162,7 +162,9 @@ class branch_and_bound_t { // Global status of the solver. omp_atomic_t solver_status_; - omp_atomic_t nodes_since_last_log; + + omp_atomic_t nodes_since_last_log_; + i_t min_node_queue_size_; // In case, a best-first thread encounters a numerical issue when solving a node, // its blocks the progression of the lower bound. @@ -184,10 +186,6 @@ class branch_and_bound_t { // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); - // Ramp-up phase of the solver, where we greedily expand the tree until - // there is enough unexplored nodes. This is done recursively using OpenMP tasks. - void exploration_ramp_up(mip_node_t* node, i_t initial_heap_size); - void plunge_with(bnb_worker_t* worker); void dive_with(bnb_worker_t* worker); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 51d59c5ba..d0f9dd408 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -29,7 +29,7 @@ struct diving_heuristics_settings_t { bool disable_guided_diving = false; bool disable_coefficient_diving = false; - i_t min_node_depth = 0; + i_t min_node_depth = 5; i_t node_limit = 500; f_t iteration_limit_factor = 0.05; i_t backtrack = 5; From 769a3d83a9423782a0c0a70f64319a845cc46ecb Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 17:48:38 +0100 Subject: [PATCH 49/70] refactoring --- cpp/src/dual_simplex/branch_and_bound.cpp | 329 +++++++++++----------- 1 file changed, 158 insertions(+), 171 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 172dc963d..572027a5d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -231,9 +231,7 @@ branch_and_bound_t::branch_and_bound_t( convert_user_problem(original_problem_, settings_, original_lp_, new_slacks_, dualize_info); full_variable_types(original_problem_, original_lp_, var_types_); - mutex_upper_.lock(); upper_bound_ = inf; - mutex_upper_.unlock(); } template @@ -960,161 +958,6 @@ void branch_and_bound_t::dive_with(bnb_worker_t* worker) active_workers_per_type[diving_type]--; } -template -void branch_and_bound_t::master_loop() -{ - f_t lower_bound = get_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; - f_t last_log = 0.0; - - diving_heuristics_settings_t diving_settings = settings_.diving_settings; - bool is_ramp_up_finished = false; - - std::vector worker_types = {EXPLORATION}; - std::array max_num_workers_per_type; - max_num_workers_per_type.fill(0); - max_num_workers_per_type[EXPLORATION] = settings_.num_threads; - - while (solver_status_ == mip_exploration_status_t::RUNNING && - abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && - (active_workers_per_type[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; - rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); - - repair_heuristic_solutions(); - - if (!is_ramp_up_finished) { - if (node_queue.best_first_queue_size() >= min_node_queue_size_ && - node_queue.bfs_top()->depth >= diving_settings.min_node_depth) { - if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } - max_num_workers_per_type = - bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); - worker_types = bnb_get_worker_types(diving_settings); - is_ramp_up_finished = true; - -#ifdef CUOPT_LOG_DEBUG - settings_.log.debug("Ramp-up phase is finished. num active workers = %d, heap size = %d\n", - active_workers_per_type[EXPLORATION], - node_queue.best_first_queue_size()); - - for (auto type : worker_types) { - settings_.log.debug("%s: max num of workers = %d", - feasible_solution_symbol(type), - max_num_workers_per_type[type]); - } -#endif - } - } - - // If the guided diving was disabled previously due to the lack of an incumbent solution, - // re-enable as soon as a new incumbent is found. - if (settings_.diving_settings.disable_guided_diving != diving_settings.disable_guided_diving) { - if (std::isfinite(upper_bound_)) { - diving_settings.disable_guided_diving = settings_.diving_settings.disable_guided_diving; - max_num_workers_per_type = - bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); - worker_types = bnb_get_worker_types(diving_settings); - -#ifdef CUOPT_LOG_DEBUG - for (auto type : worker_types) { - settings_.log.debug("%s: max num of workers = %d", - feasible_solution_symbol(type), - max_num_workers_per_type[type]); - } -#endif - } - } - - f_t now = toc(exploration_stats_.start_time); - f_t time_since_last_log = last_log == 0 ? 1.0 : toc(last_log); - - if (((nodes_since_last_log_ >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && - time_since_last_log >= 1) || - (time_since_last_log > 30) || now > settings_.time_limit) { - i_t depth = - node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; - report(" ", upper_bound_, lower_bound, depth); - last_log = tic(); - nodes_since_last_log_ = 0; - } - - if (now > settings_.time_limit) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - break; - } - - for (auto type : worker_types) { - if (active_workers_per_type[type] >= max_num_workers_per_type[type]) { continue; } - - // Get an idle worker. - bnb_worker_t* worker = worker_pool_.get_idle_worker(); - if (worker == nullptr) { break; } - - if (type == EXPLORATION) { - // If there any node left in the heap, we pop the top node and explore it. - std::optional*> start_node = node_queue.pop_best_first(); - - if (!start_node.has_value()) { continue; } - if (upper_bound_ < start_node.value()->lower_bound) { - // This node was put on the heap earlier but its lower bound is now greater than the - // current upper bound - 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_); - last_node_depth = start_node.value()->depth; - active_workers_per_type[type]++; - nodes_since_last_log_++; - launched_any_task = true; - -#pragma omp task affinity(worker) - plunge_with(worker); - - } else { - std::optional*> start_node = node_queue.pop_diving(); - - if (!start_node.has_value()) { continue; } - if (upper_bound_ < start_node.value()->lower_bound) { continue; } - - bool is_feasible = worker->init_diving(start_node.value(), type, original_lp_, settings_); - if (!is_feasible) { continue; } - - // Remove the worker from the idle list. - worker_pool_.pop_idle_worker(); - active_workers_per_type[type]++; - launched_any_task = true; - -#pragma omp task affinity(worker) - dive_with(worker); - } - } - - // If no new task was launched in this iteration, suspend temporarily the - // execution of the master. As of 8/Jan/2026, GCC does not - // implement taskyield, but LLVM does. - if (!launched_any_task) { -#ifndef __GNUC__ -#pragma omp taskyield -#else - std::this_thread::sleep_for(std::chrono::milliseconds(1)); -#endif - } - } - - if (solver_status_ == mip_exploration_status_t::RUNNING) { - solver_status_ = mip_exploration_status_t::COMPLETED; - } -} - template lp_status_t branch_and_bound_t::solve_root_relaxation( simplex_solver_settings_t const& lp_settings) @@ -1344,17 +1187,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - worker_pool_.init(2 * settings_.num_threads, original_lp_, Arow_, var_types_, settings_); - active_workers_per_type.fill(0); - settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, settings_.num_bfs_workers, settings_.num_threads - settings_.num_bfs_workers); - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " - "| Time |\n"); + auto down_child = search_tree_.root.get_down_child(); + auto up_child = search_tree_.root.get_up_child(); + node_queue.push(down_child); + node_queue.push(up_child); + + f_t lower_bound = get_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; + f_t last_log = 0.0; exploration_stats_.nodes_explored = 1; exploration_stats_.nodes_unexplored = 2; @@ -1362,22 +1209,162 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lower_bound_ceiling_ = inf; min_node_queue_size_ = 2 * settings_.num_threads; - auto down_child = search_tree_.root.get_down_child(); - auto up_child = search_tree_.root.get_up_child(); - node_queue.push(down_child); - node_queue.push(up_child); + diving_heuristics_settings_t diving_settings = settings_.diving_settings; + bool is_ramp_up_finished = false; + + std::vector worker_types = {EXPLORATION}; + std::array max_num_workers_per_type; + max_num_workers_per_type.fill(0); + max_num_workers_per_type[EXPLORATION] = settings_.num_threads; + worker_pool_.init(2 * settings_.num_threads, original_lp_, Arow_, var_types_, settings_); + active_workers_per_type.fill(0); + + settings_.log.printf( + " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " + "| Time |\n"); #pragma omp parallel num_threads(settings_.num_threads) { #pragma omp master { -#pragma omp task - master_loop(); + while (solver_status_ == mip_exploration_status_t::RUNNING && + abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && + (active_workers_per_type[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; + rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); + + repair_heuristic_solutions(); + + if (!is_ramp_up_finished) { + if (node_queue.best_first_queue_size() >= min_node_queue_size_ && + node_queue.bfs_top()->depth >= diving_settings.min_node_depth) { + if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } + max_num_workers_per_type = + bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); + worker_types = bnb_get_worker_types(diving_settings); + is_ramp_up_finished = true; + +#ifdef CUOPT_LOG_DEBUG + settings_.log.debug( + "Ramp-up phase is finished. num active workers = %d, heap size = %d\n", + active_workers_per_type[EXPLORATION], + node_queue.best_first_queue_size()); + + for (auto type : worker_types) { + settings_.log.debug("%s: max num of workers = %d", + feasible_solution_symbol(type), + max_num_workers_per_type[type]); + } +#endif + } + } + + // If the guided diving was disabled previously due to the lack of an incumbent solution, + // re-enable as soon as a new incumbent is found. + if (settings_.diving_settings.disable_guided_diving != + diving_settings.disable_guided_diving) { + if (std::isfinite(upper_bound_)) { + diving_settings.disable_guided_diving = settings_.diving_settings.disable_guided_diving; + max_num_workers_per_type = + bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); + worker_types = bnb_get_worker_types(diving_settings); + +#ifdef CUOPT_LOG_DEBUG + for (auto type : worker_types) { + settings_.log.debug("%s: max num of workers = %d", + feasible_solution_symbol(type), + max_num_workers_per_type[type]); + } +#endif + } + } + + f_t now = toc(exploration_stats_.start_time); + f_t time_since_last_log = last_log == 0 ? 1.0 : toc(last_log); + + if (((nodes_since_last_log_ >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && + time_since_last_log >= 1) || + (time_since_last_log > 30) || now > settings_.time_limit) { + i_t depth = + node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; + report(" ", upper_bound_, lower_bound, depth); + last_log = tic(); + nodes_since_last_log_ = 0; + } + + if (now > settings_.time_limit) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; + break; + } + + for (auto type : worker_types) { + if (active_workers_per_type[type] >= max_num_workers_per_type[type]) { continue; } + + // Get an idle worker. + bnb_worker_t* worker = worker_pool_.get_idle_worker(); + if (worker == nullptr) { break; } + + if (type == EXPLORATION) { + // If there any node left in the heap, we pop the top node and explore it. + std::optional*> start_node = node_queue.pop_best_first(); + + if (!start_node.has_value()) { continue; } + if (upper_bound_ < start_node.value()->lower_bound) { + // This node was put on the heap earlier but its lower bound is now greater than the + // current upper bound + 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_); + last_node_depth = start_node.value()->depth; + active_workers_per_type[type]++; + nodes_since_last_log_++; + launched_any_task = true; + +#pragma omp task affinity(worker) + plunge_with(worker); + + } else { + std::optional*> start_node = node_queue.pop_diving(); + + if (!start_node.has_value()) { continue; } + if (upper_bound_ < start_node.value()->lower_bound) { continue; } + + bool is_feasible = + worker->init_diving(start_node.value(), type, original_lp_, settings_); + if (!is_feasible) { continue; } + + // Remove the worker from the idle list. + worker_pool_.pop_idle_worker(); + active_workers_per_type[type]++; + launched_any_task = true; + +#pragma omp task affinity(worker) + dive_with(worker); + } + } + + // If no new task was launched in this iteration, suspend temporarily the + // execution of the master. 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)); } + } } } - f_t lower_bound = node_queue.best_first_queue_size() > 0 ? node_queue.get_lower_bound() - : search_tree_.root.lower_bound; + if (solver_status_ == mip_exploration_status_t::RUNNING) { + solver_status_ = mip_exploration_status_t::COMPLETED; + } + + lower_bound = node_queue.best_first_queue_size() > 0 ? node_queue.get_lower_bound() + : search_tree_.root.lower_bound; return set_final_solution(solution, lower_bound); } From c54033c1ea8c64acea029384a80c7f05114bf19f Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 18:15:34 +0100 Subject: [PATCH 50/70] updating code to match the new parallel bnb --- cpp/src/dual_simplex/branch_and_bound.cpp | 82 +++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 14 ++-- cpp/src/dual_simplex/diving_heuristics.cpp | 2 +- cpp/src/dual_simplex/diving_heuristics.hpp | 2 +- cpp/src/dual_simplex/node_queue.hpp | 10 ++- .../dual_simplex/simplex_solver_settings.hpp | 12 +-- cpp/src/mip/diversity/lns/rins.cu | 4 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 4 +- cpp/src/mip/solver.cu | 8 +- 9 files changed, 73 insertions(+), 65 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 1993630e1..29120bda2 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -192,14 +192,14 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) } } -inline const char* feasible_solution_symbol(bnb_thread_type_t type) +inline const char* feasible_solution_symbol(bnb_worker_type_t type) { switch (type) { - case bnb_thread_type_t::EXPLORATION: return "B "; - case bnb_thread_type_t::COEFFICIENT_DIVING: return "CD"; - case bnb_thread_type_t::LINE_SEARCH_DIVING: return "LD"; - case bnb_thread_type_t::PSEUDOCOST_DIVING: return "PD"; - case bnb_thread_type_t::GUIDED_DIVING: return "GD"; + case bnb_worker_type_t::EXPLORATION: return "B "; + case bnb_worker_type_t::COEFFICIENT_DIVING: return "D "; + case bnb_worker_type_t::LINE_SEARCH_DIVING: return "D "; + case bnb_worker_type_t::PSEUDOCOST_DIVING: return "D "; + case bnb_worker_type_t::GUIDED_DIVING: return "D "; default: return "U "; } } @@ -532,7 +532,7 @@ template void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - bnb_thread_type_t thread_type) + bnb_worker_type_t thread_type) { bool send_solution = false; @@ -582,7 +582,7 @@ branch_variable_t branch_and_bound_t::variable_selection( mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_thread_type_t type, + bnb_worker_type_t type, logger_t& log) { i_t branch_var = -1; @@ -590,7 +590,7 @@ branch_variable_t branch_and_bound_t::variable_selection( rounding_direction_t round_dir = rounding_direction_t::NONE; switch (type) { - case bnb_thread_type_t::EXPLORATION: + case bnb_worker_type_t::EXPLORATION: std::tie(branch_var, obj_estimate) = pc_.variable_selection_and_obj_estimate(fractional, solution, node_ptr->lower_bound, log); round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); @@ -601,16 +601,16 @@ branch_variable_t branch_and_bound_t::variable_selection( node_ptr->objective_estimate = obj_estimate; return {branch_var, round_dir}; - case bnb_thread_type_t::COEFFICIENT_DIVING: + case bnb_worker_type_t::COEFFICIENT_DIVING: return coefficient_diving(original_lp_, fractional, solution, log); - case bnb_thread_type_t::LINE_SEARCH_DIVING: + case bnb_worker_type_t::LINE_SEARCH_DIVING: return line_search_diving(fractional, solution, root_relax_soln_.x, log); - case bnb_thread_type_t::PSEUDOCOST_DIVING: + case bnb_worker_type_t::PSEUDOCOST_DIVING: return pseudocost_diving(pc_, fractional, solution, root_relax_soln_.x, log); - case bnb_thread_type_t::GUIDED_DIVING: + case bnb_worker_type_t::GUIDED_DIVING: return guided_diving(pc_, fractional, solution, incumbent_.x, log); default: @@ -628,7 +628,7 @@ node_solve_info_t branch_and_bound_t::solve_node( std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, - bnb_thread_type_t thread_type, + bnb_worker_type_t thread_type, bool recompute_bounds_and_basis, const std::vector& root_lower, const std::vector& root_upper, @@ -639,11 +639,11 @@ node_solve_info_t branch_and_bound_t::solve_node( const f_t upper_bound = get_upper_bound(); // If there is no incumbent, use pseudocost diving instead of guided diving - if (upper_bound == inf && thread_type == bnb_thread_type_t::GUIDED_DIVING) { + if (upper_bound == inf && thread_type == bnb_worker_type_t::GUIDED_DIVING) { if (settings_.diving_settings.disable_pseudocost_diving) { - thread_type = bnb_thread_type_t::COEFFICIENT_DIVING; + thread_type = bnb_worker_type_t::COEFFICIENT_DIVING; } else { - thread_type = bnb_thread_type_t::PSEUDOCOST_DIVING; + thread_type = bnb_worker_type_t::PSEUDOCOST_DIVING; } } @@ -658,7 +658,7 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); lp_settings.scale_columns = false; - if (thread_type != bnb_thread_type_t::EXPLORATION) { + if (thread_type != bnb_worker_type_t::EXPLORATION) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; f_t max_iter = settings_.diving_settings.iteration_limit_factor * bnb_lp_iters; lp_settings.iteration_limit = max_iter - stats.total_lp_iters; @@ -702,12 +702,12 @@ node_solve_info_t branch_and_bound_t::solve_node( leaf_problem.lower, leaf_problem.upper, node_presolver.bounds_changed); } - bool feasible = + bool is_feasible = node_presolver.bounds_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; - if (feasible) { + if (is_feasible) { i_t node_iter = 0; f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; @@ -775,7 +775,7 @@ node_solve_info_t branch_and_bound_t::solve_node( search_tree.graphviz_node(log, node_ptr, "lower bound", leaf_objective); pc_.update_pseudo_costs(node_ptr, leaf_objective); - if (thread_type == bnb_thread_type_t::EXPLORATION) { + if (thread_type == bnb_worker_type_t::EXPLORATION) { if (settings_.node_processed_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, leaf_solution.x, original_x); @@ -820,7 +820,7 @@ node_solve_info_t branch_and_bound_t::solve_node( return node_solve_info_t::ITERATION_LIMIT; } else { - if (thread_type == bnb_thread_type_t::EXPLORATION) { + if (thread_type == bnb_worker_type_t::EXPLORATION) { fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); log.printf( "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " @@ -900,7 +900,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod basic_list, nonbasic_list, node_presolver, - bnb_thread_type_t::EXPLORATION, + bnb_worker_type_t::EXPLORATION, true, original_lp_.lower, original_lp_.upper, @@ -1008,7 +1008,7 @@ void branch_and_bound_t::plunge_from(i_t task_id, basic_list, nonbasic_list, node_presolver, - bnb_thread_type_t::EXPLORATION, + bnb_worker_type_t::EXPLORATION, recompute_bounds_and_basis, original_lp_.lower, original_lp_.upper, @@ -1123,7 +1123,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, basis_update_mpf_t& basis_factors, std::vector& basic_list, std::vector& nonbasic_list, - bnb_thread_type_t diving_type) + bnb_worker_type_t diving_type) { logger_t log; log.log = false; @@ -1194,7 +1194,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, } template -void branch_and_bound_t::diving_thread(bnb_thread_type_t diving_type) +void branch_and_bound_t::diving_thread(bnb_worker_type_t diving_type) { // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; @@ -1327,23 +1327,23 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); - std::vector diving_strategies; + std::vector diving_strategies; diving_strategies.reserve(4); if (!settings_.diving_settings.disable_pseudocost_diving) { - diving_strategies.push_back(bnb_thread_type_t::PSEUDOCOST_DIVING); + diving_strategies.push_back(bnb_worker_type_t::PSEUDOCOST_DIVING); } if (!settings_.diving_settings.disable_line_search_diving) { - diving_strategies.push_back(bnb_thread_type_t::LINE_SEARCH_DIVING); + diving_strategies.push_back(bnb_worker_type_t::LINE_SEARCH_DIVING); } if (!settings_.diving_settings.disable_guided_diving) { - diving_strategies.push_back(bnb_thread_type_t::GUIDED_DIVING); + diving_strategies.push_back(bnb_worker_type_t::GUIDED_DIVING); } if (!settings_.diving_settings.disable_coefficient_diving) { - diving_strategies.push_back(bnb_thread_type_t::COEFFICIENT_DIVING); + diving_strategies.push_back(bnb_worker_type_t::COEFFICIENT_DIVING); } if (diving_strategies.empty()) { @@ -1418,7 +1418,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut set_uninitialized_steepest_edge_norms(edge_norms_); root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); - local_lower_bounds_.assign(settings_.num_bfs_threads, root_objective_); + local_lower_bounds_.assign(settings_.num_bfs_workers, root_objective_); if (settings_.set_simplex_solution_callback != nullptr) { std::vector original_x; @@ -1496,12 +1496,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, - settings_.num_bfs_threads, - settings_.diving_settings.num_diving_tasks); - - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " - "| Time |\n"); + settings_.num_bfs_workers, + settings_.num_threads - settings_.num_bfs_workers); exploration_stats_.nodes_explored = 1; exploration_stats_.nodes_unexplored = 2; @@ -1512,6 +1508,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lower_bound_ceiling_ = inf; should_report_ = true; + settings_.log.printf( + " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " + "| Time |\n"); + #pragma omp parallel num_threads(settings_.num_threads) { #pragma omp master @@ -1530,14 +1530,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_ramp_up(up_child, initial_size); } - for (i_t i = 0; i < settings_.num_bfs_threads; i++) { + for (i_t i = 0; i < settings_.num_bfs_workers; i++) { #pragma omp task best_first_thread(i); } if (!diving_strategies.empty()) { - for (i_t k = 0; k < settings_.diving_settings.num_diving_tasks; k++) { - const bnb_thread_type_t diving_type = diving_strategies[k % num_strategies]; + for (i_t k = 0; k < settings_.diving_settings.num_diving_workers; k++) { + const bnb_worker_type_t diving_type = diving_strategies[k % num_strategies]; #pragma omp task diving_thread(diving_type); } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index b30682bdb..dc838bb80 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -58,7 +58,7 @@ enum class node_solve_info_t { // // [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, // Berlin, 2007. doi: 10.14279/depositonce-1634. -enum class bnb_thread_type_t { +enum class bnb_worker_type_t { EXPLORATION = 0, // Best-First + Plunging. PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) @@ -208,7 +208,7 @@ class branch_and_bound_t { void add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - bnb_thread_type_t thread_type); + bnb_worker_type_t thread_type); // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); @@ -240,11 +240,11 @@ class branch_and_bound_t { basis_update_mpf_t& basis_update, std::vector& basic_list, std::vector& nonbasic_list, - bnb_thread_type_t diving_type); + bnb_worker_type_t diving_type); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(bnb_thread_type_t diving_type); + void diving_thread(bnb_worker_type_t diving_type); // Solve the LP relaxation of a leaf node and update the tree. node_solve_info_t solve_node(mip_node_t* node_ptr, @@ -254,7 +254,7 @@ class branch_and_bound_t { std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, - bnb_thread_type_t thread_type, + bnb_worker_type_t thread_type, bool recompute_basis_and_bounds, const std::vector& root_lower, const std::vector& root_upper, @@ -265,7 +265,7 @@ class branch_and_bound_t { branch_variable_t variable_selection(mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_thread_type_t type, + bnb_worker_type_t type, logger_t& log); }; diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 978a97e42..ce9460fa9 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/diving_heuristics.hpp b/cpp/src/dual_simplex/diving_heuristics.hpp index c7b1e2050..1f44fee31 100644 --- a/cpp/src/dual_simplex/diving_heuristics.hpp +++ b/cpp/src/dual_simplex/diving_heuristics.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 0234fa038..c3b0e9336 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -1,6 +1,6 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 */ #pragma once @@ -168,6 +168,12 @@ class node_queue_t { std::lock_guard lock(mutex); return best_first_heap.empty() ? inf : best_first_heap.top()->lower_bound; } + + mip_node_t* bfs_top() + { + std::lock_guard lock(mutex); + return best_first_heap.empty() ? nullptr : best_first_heap.top()->node; + } }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 60b92ee33..d0f9dd408 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -21,13 +22,14 @@ namespace cuopt::linear_programming::dual_simplex { template struct diving_heuristics_settings_t { - i_t num_diving_tasks = -1; + i_t num_diving_workers = -1; bool disable_line_search_diving = false; bool disable_pseudocost_diving = false; bool disable_guided_diving = false; bool disable_coefficient_diving = false; + i_t min_node_depth = 5; i_t node_limit = 500; f_t iteration_limit_factor = 0.05; i_t backtrack = 5; @@ -84,14 +86,14 @@ struct simplex_solver_settings_t { iteration_log_frequency(1000), first_iteration_log(2), num_threads(omp_get_max_threads() - 1), - num_bfs_threads(std::min(num_threads / 4, 1)), + num_bfs_workers(std::min(num_threads / 4, 1)), random_seed(0), inside_mip(0), solution_callback(nullptr), heuristic_preemption_callback(nullptr), concurrent_halt(nullptr) { - diving_settings.num_diving_tasks = std::max(num_threads - num_bfs_threads, 1); + diving_settings.num_diving_workers = std::max(num_threads - num_bfs_workers, 1); } void set_log(bool logging) const { log.log = logging; } @@ -151,7 +153,7 @@ struct simplex_solver_settings_t { i_t first_iteration_log; // number of iterations to log at beginning of solve i_t num_threads; // number of threads to use i_t random_seed; // random seed - i_t num_bfs_threads; // number of threads dedicated to the best-first search + i_t num_bfs_workers; // number of threads dedicated to the best-first search diving_heuristics_settings_t diving_settings; // Settings for the diving heuristics diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 0121f76ef..035b03144 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -258,11 +258,11 @@ 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 = 2; - branch_and_bound_settings.num_bfs_threads = 1; + branch_and_bound_settings.num_bfs_workers = 1; // In the future, let RINS use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.diving_settings.num_diving_tasks = 1; + branch_and_bound_settings.diving_settings.num_diving_workers = 1; branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 2335003b6..1efed74ce 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -103,11 +103,11 @@ 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 = 2; - branch_and_bound_settings.num_bfs_threads = 1; + branch_and_bound_settings.num_bfs_workers = 1; // In the future, let SubMIP use all the diving heuristics. For now, // restricting to guided diving. - branch_and_bound_settings.diving_settings.num_diving_tasks = 1; + branch_and_bound_settings.diving_settings.num_diving_workers = 1; branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 35a94f36c..f9f885a81 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -184,10 +184,10 @@ solution_t mip_solver_t::run_solver() } i_t num_threads = branch_and_bound_settings.num_threads; - i_t num_bfs_threads = std::max(1, num_threads / 4); - i_t num_diving_threads = std::max(1, num_threads - num_bfs_threads); - branch_and_bound_settings.num_bfs_threads = num_bfs_threads; - branch_and_bound_settings.diving_settings.num_diving_tasks = num_diving_threads; + i_t num_bfs_workers = std::max(1, num_threads / 4); + i_t num_diving_workers = std::max(1, num_threads - num_bfs_workers); + branch_and_bound_settings.num_bfs_workers = num_bfs_workers; + branch_and_bound_settings.diving_settings.num_diving_workers = num_diving_workers; // Set the branch and bound -> primal heuristics callback branch_and_bound_settings.solution_callback = From 4bcf801d5e74fbf978ce5b0cd025570d6b6cfdc7 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 18:27:43 +0100 Subject: [PATCH 51/70] removed command line options --- .../linear_programming/cuopt/run_mip.cpp | 51 +------------------ .../mip/solver_settings.hpp | 7 +-- cpp/src/mip/solver.cu | 9 ---- 3 files changed, 2 insertions(+), 65 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 6d8fbdd81..6013dcaf5 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -147,10 +147,6 @@ int run_single_file(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, - bool disable_line_search_diving, - bool disable_pseudocost_diving, - bool disable_guided_diving, - bool disable_coefficient_diving, double time_limit) { const raft::handle_t handle_{}; @@ -208,11 +204,6 @@ int run_single_file(std::string file_path, settings.tolerances.relative_tolerance = 1e-12; settings.tolerances.absolute_tolerance = 1e-6; settings.presolve = true; - settings.disable_line_search_diving = disable_line_search_diving; - settings.disable_pseudocost_diving = disable_pseudocost_diving; - settings.disable_guided_diving = disable_guided_diving; - settings.disable_coefficient_diving = disable_coefficient_diving; - cuopt::linear_programming::benchmark_info_t benchmark_info; settings.benchmark_info_ptr = &benchmark_info; auto start_run_solver = std::chrono::high_resolution_clock::now(); @@ -259,10 +250,6 @@ void run_single_file_mp(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, - bool disable_line_search_diving, - bool disable_pseudocost_diving, - bool disable_guided_diving, - bool disable_coefficient_diving, double time_limit) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; @@ -278,10 +265,6 @@ void run_single_file_mp(std::string file_path, num_cpu_threads, write_log_file, log_to_console, - disable_line_search_diving, - disable_pseudocost_diving, - disable_guided_diving, - disable_coefficient_diving, time_limit); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes @@ -365,22 +348,6 @@ int main(int argc, char* argv[]) .help("track allocations (t/f)") .default_value(std::string("f")); - program.add_argument("--disable-line-search-diving") - .help("disable line search diving (t/f)") - .default_value(std::string("f")); - - program.add_argument("--disable-pseudocost-diving") - .help("disable pseudocost diving (t/f)") - .default_value(std::string("f")); - - program.add_argument("--disable-guided-diving") - .help("disable guided diving (t/f)") - .default_value(std::string("f")); - - program.add_argument("--disable-coefficient-diving") - .help("disable coefficient diving (t/f)") - .default_value(std::string("f")); - // Parse arguments try { program.parse_args(argc, argv); @@ -410,14 +377,6 @@ int main(int argc, char* argv[]) double memory_limit = program.get("--memory-limit"); bool track_allocations = program.get("--track-allocations")[0] == 't'; - bool disable_line_search_diving = - program.get("--disable-line-search-diving")[0] == 't'; - bool disable_pseudocost_diving = - program.get("--disable-pseudocost-diving")[0] == 't'; - bool disable_guided_diving = program.get("--disable-guided-diving")[0] == 't'; - bool disable_coefficient_diving = - program.get("--disable-coefficient-diving")[0] == 't'; - if (num_cpu_threads < 0) { num_cpu_threads = omp_get_max_threads() / n_gpus; } if (program.is_used("--out-dir")) { @@ -504,10 +463,6 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, - disable_line_search_diving, - disable_pseudocost_diving, - disable_guided_diving, - disable_coefficient_diving, time_limit); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; @@ -548,10 +503,6 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, - disable_line_search_diving, - disable_pseudocost_diving, - disable_guided_diving, - disable_coefficient_diving, time_limit); } diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 680cceaba..4f6320752 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -87,11 +87,6 @@ class mip_solver_settings_t { std::string sol_file; std::string user_problem_file; - bool disable_line_search_diving = false; - bool disable_pseudocost_diving = false; - bool disable_guided_diving = false; - bool disable_coefficient_diving = false; - /** Initial primal solutions */ std::vector>> initial_solutions; bool mip_scaling = true; diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index f9f885a81..08e1806b9 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -168,15 +168,6 @@ solution_t mip_solver_t::run_solver() 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.diving_settings.disable_coefficient_diving = - context.settings.disable_coefficient_diving; - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = - context.settings.disable_pseudocost_diving; - branch_and_bound_settings.diving_settings.disable_guided_diving = - context.settings.disable_guided_diving; - branch_and_bound_settings.diving_settings.disable_line_search_diving = - context.settings.disable_line_search_diving; - if (context.settings.num_cpu_threads < 0) { branch_and_bound_settings.num_threads = omp_get_max_threads() - 1; } else { From d91369d5f9b59969c3e60bd6bbcf860edf1ce7ae Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 18:32:44 +0100 Subject: [PATCH 52/70] fix style --- cpp/src/dual_simplex/CMakeLists.txt | 2 +- cpp/src/dual_simplex/bounds_strengthening.cpp | 2 +- cpp/src/dual_simplex/logger.hpp | 2 +- cpp/src/dual_simplex/mip_node.hpp | 2 +- cpp/src/dual_simplex/pseudo_costs.cpp | 2 +- cpp/src/dual_simplex/pseudo_costs.hpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cpp/src/dual_simplex/CMakeLists.txt b/cpp/src/dual_simplex/CMakeLists.txt index ebaf9cbb7..af1415fa9 100644 --- a/cpp/src/dual_simplex/CMakeLists.txt +++ b/cpp/src/dual_simplex/CMakeLists.txt @@ -1,5 +1,5 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index c56c9db98..4114e7e09 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index c45e3ede3..f81308670 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index a082932ac..de147132a 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 1c0a33042..aabbe5a17 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index ab01b2a85..5c34e0296 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ From 4ee57f93448c62593caba26558b0b64fff9264a6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 10:44:25 +0100 Subject: [PATCH 53/70] fix compilation failure --- cpp/src/dual_simplex/branch_and_bound.cpp | 8 +++++--- cpp/src/mip/diversity/lns/rins.cu | 12 +++--------- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 8 +------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 29120bda2..f63c13b2f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -5,10 +5,9 @@ */ /* clang-format on */ -#include -#include -#include #include + +#include #include #include #include @@ -20,6 +19,9 @@ #include #include +#include + +#include #include #include #include diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 035b03144..7394e2db6 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -265,15 +265,9 @@ void rins_t::run_rins() branch_and_bound_settings.diving_settings.num_diving_workers = 1; branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; - - if (context.settings.disable_guided_diving) { - branch_and_bound_settings.diving_settings.disable_guided_diving = true; - } else { - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; - } - - branch_and_bound_settings.log.log = false; - branch_and_bound_settings.log.log_prefix = "[RINS] "; + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + branch_and_bound_settings.log.log = false; + branch_and_bound_settings.log.log_prefix = "[RINS] "; branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( std::vector& solution, f_t objective) { rins_solution_queue.push_back(solution); diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 1efed74ce..d46c6b31a 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -110,13 +110,7 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.diving_settings.num_diving_workers = 1; branch_and_bound_settings.diving_settings.disable_line_search_diving = true; branch_and_bound_settings.diving_settings.disable_coefficient_diving = true; - - if (context.settings.disable_guided_diving) { - branch_and_bound_settings.diving_settings.disable_guided_diving = true; - } else { - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; - } - + branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); From b99a9c791e21925348df34216ce0b9baab309ec4 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 11:13:50 +0100 Subject: [PATCH 54/70] separated objective estimate and variable selection --- cpp/src/dual_simplex/branch_and_bound.cpp | 16 ++--- cpp/src/dual_simplex/pseudo_costs.cpp | 63 +++++++++++++++---- cpp/src/dual_simplex/pseudo_costs.hpp | 12 ++-- .../dual_simplex/simplex_solver_settings.hpp | 2 +- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index f63c13b2f..28220e622 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -588,19 +588,18 @@ branch_variable_t branch_and_bound_t::variable_selection( logger_t& log) { i_t branch_var = -1; - f_t obj_estimate = 0; rounding_direction_t round_dir = rounding_direction_t::NONE; switch (type) { case bnb_worker_type_t::EXPLORATION: - std::tie(branch_var, obj_estimate) = - pc_.variable_selection_and_obj_estimate(fractional, solution, node_ptr->lower_bound, log); - round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); + branch_var = pc_.variable_selection(fractional, solution, log); + round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); // Note that the exploration thread is the only one that can insert new nodes into the heap, // and thus, we only need to calculate the objective estimate here (it is used for // sorting the nodes for diving). - node_ptr->objective_estimate = obj_estimate; + node_ptr->objective_estimate = + pc_.obj_estimate(fractional, solution, node_ptr->lower_bound, log); return {branch_var, round_dir}; case bnb_worker_type_t::COEFFICIENT_DIVING: @@ -798,6 +797,8 @@ node_solve_info_t branch_and_bound_t::solve_node( node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); assert(leaf_vstatus.size() == leaf_problem.num_cols); + assert(branch_var >= 0); + assert(round_dir != rounding_direction_t::NONE); search_tree.branch( node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, leaf_problem, log); @@ -1349,7 +1350,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (diving_strategies.empty()) { - settings_.log.printf("Warning: All diving heuristics are disabled!"); + settings_.log.printf("Warning: All diving heuristics are disabled!\n"); } if (guess_.size() != 0) { @@ -1483,8 +1484,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Choose variable to branch on - auto [branch_var, obj_estimate] = - pc_.variable_selection_and_obj_estimate(fractional, root_relax_soln_.x, root_objective_, log); + auto branch_var = pc_.variable_selection(fractional, root_relax_soln_.x, log); search_tree_.root = std::move(mip_node_t(root_objective_, root_vstatus_)); search_tree_.num_nodes = 0; diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index aabbe5a17..143b25d24 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -253,11 +253,9 @@ void pseudo_costs_t::initialized(i_t& num_initialized_down, } template -std::pair pseudo_costs_t::variable_selection_and_obj_estimate( - const std::vector& fractional, - const std::vector& solution, - f_t lower_bound, - logger_t& log) +i_t pseudo_costs_t::variable_selection(const std::vector& fractional, + const std::vector& solution, + logger_t& log) { std::lock_guard lock(mutex); @@ -265,7 +263,6 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat std::vector pseudo_cost_up(num_fractional); std::vector pseudo_cost_down(num_fractional); std::vector score(num_fractional); - f_t estimate = lower_bound; i_t num_initialized_down; i_t num_initialized_up; @@ -298,9 +295,6 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat const f_t f_up = std::ceil(solution[j]) - solution[j]; score[k] = std::max(f_down * pseudo_cost_down[k], eps) * std::max(f_up * pseudo_cost_up[k], eps); - - estimate += std::min(std::max(pseudo_cost_down[k] * f_down, eps), - std::max(pseudo_cost_up[k] * f_up, eps)); } i_t branch_var = fractional[0]; @@ -314,13 +308,56 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat } } - log.debug("Pseudocost branching on %d. Value %e. Score %e. Obj Estimate %e\n", + log.debug("Pseudocost branching on %d. Value %e. Score %e.\n", branch_var, solution[branch_var], - score[select], - estimate); + score[select]); + + return branch_var; +} + +template +f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log) +{ + std::lock_guard lock(mutex); + + const i_t num_fractional = fractional.size(); + f_t estimate = lower_bound; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + + initialized(num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (i_t k = 0; k < num_fractional; k++) { + const i_t j = fractional[k]; + f_t pseudo_cost_down = 0; + f_t pseudo_cost_up = 0; + + if (pseudo_cost_num_down[j] != 0) { + pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; + } else { + pseudo_cost_down = pseudo_cost_down_avg; + } + + if (pseudo_cost_num_up[j] != 0) { + pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; + } else { + pseudo_cost_up = pseudo_cost_up_avg; + } + constexpr f_t eps = 1e-6; + const f_t f_down = solution[j] - std::floor(solution[j]); + const f_t f_up = std::ceil(solution[j]) - solution[j]; + estimate += + std::min(std::max(pseudo_cost_down * f_down, eps), std::max(pseudo_cost_up * f_up, eps)); + } - return {branch_var, estimate}; + return estimate; } template diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 5c34e0296..49a810506 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -43,10 +43,14 @@ class pseudo_costs_t { f_t& pseudo_cost_down_avg, f_t& pseudo_cost_up_avg) const; - std::pair variable_selection_and_obj_estimate(const std::vector& fractional, - const std::vector& solution, - f_t lower_bound, - logger_t& log); + i_t variable_selection(const std::vector& fractional, + const std::vector& solution, + logger_t& log); + + f_t obj_estimate(const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log); void update_pseudo_costs_from_strong_branching(const std::vector& fractional, const std::vector& root_soln); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index d0f9dd408..77e0628ce 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -86,7 +86,7 @@ struct simplex_solver_settings_t { iteration_log_frequency(1000), first_iteration_log(2), num_threads(omp_get_max_threads() - 1), - num_bfs_workers(std::min(num_threads / 4, 1)), + num_bfs_workers(std::max(num_threads / 4, 1)), random_seed(0), inside_mip(0), solution_callback(nullptr), From 43f8b31de63250efc62e1b2a84e72750a68ddd8e Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 11:43:41 +0100 Subject: [PATCH 55/70] separating objective estimate from variable selection --- cpp/src/dual_simplex/branch_and_bound.cpp | 18 ++++--- cpp/src/dual_simplex/branch_and_bound.hpp | 1 + cpp/src/dual_simplex/pseudo_costs.cpp | 63 ++++++++++++++++++----- cpp/src/dual_simplex/pseudo_costs.hpp | 12 +++-- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 15db6f975..d3dabbacb 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -722,9 +722,14 @@ node_solve_info_t branch_and_bound_t::solve_node( } else if (leaf_objective <= upper_bound + abs_fathom_tol) { // Choose fractional variable to branch on - auto [branch_var, obj_estimate] = pc_.variable_selection_and_obj_estimate( - leaf_fractional, leaf_solution.x, node_ptr->lower_bound, log); - node_ptr->objective_estimate = obj_estimate; + const i_t branch_var = + pc_.variable_selection(leaf_fractional, leaf_solution.x, lp_settings.log); + + // Note that the exploration thread is the only one that can insert new nodes into the heap, + // and thus, we only need to calculate the objective estimate here (it is used for + // sorting the nodes for diving). + node_ptr->objective_estimate = + pc_.obj_estimate(leaf_fractional, leaf_solution.x, node_ptr->lower_bound, lp_settings.log); assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( @@ -1101,6 +1106,8 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A std::vector start_upper; bool reset_starting_bounds = true; + constexpr i_t node_limit = 500; + while (solver_status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { if (reset_starting_bounds) { @@ -1142,7 +1149,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A } if (toc(exploration_stats_.start_time) > settings_.time_limit) { break; } - if (dive_stats.nodes_explored > 500) { break; } + if (dive_stats.nodes_explored > node_limit) { break; } node_solve_info_t status = solve_node(node_ptr, subtree, @@ -1401,8 +1408,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Choose variable to branch on - auto [branch_var, obj_estimate] = - pc_.variable_selection_and_obj_estimate(fractional, root_relax_soln_.x, root_objective_, log); + i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x, log); search_tree_.root = std::move(mip_node_t(root_objective_, root_vstatus_)); search_tree_.num_nodes = 0; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index b719a220a..36b3b5f69 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -237,6 +237,7 @@ class branch_and_bound_t { std::vector& basic_list, std::vector& nonbasic_list, thread_type_t diving_type); + // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. void diving_thread(const csr_matrix_t& Arow); diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index aabbe5a17..143b25d24 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -253,11 +253,9 @@ void pseudo_costs_t::initialized(i_t& num_initialized_down, } template -std::pair pseudo_costs_t::variable_selection_and_obj_estimate( - const std::vector& fractional, - const std::vector& solution, - f_t lower_bound, - logger_t& log) +i_t pseudo_costs_t::variable_selection(const std::vector& fractional, + const std::vector& solution, + logger_t& log) { std::lock_guard lock(mutex); @@ -265,7 +263,6 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat std::vector pseudo_cost_up(num_fractional); std::vector pseudo_cost_down(num_fractional); std::vector score(num_fractional); - f_t estimate = lower_bound; i_t num_initialized_down; i_t num_initialized_up; @@ -298,9 +295,6 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat const f_t f_up = std::ceil(solution[j]) - solution[j]; score[k] = std::max(f_down * pseudo_cost_down[k], eps) * std::max(f_up * pseudo_cost_up[k], eps); - - estimate += std::min(std::max(pseudo_cost_down[k] * f_down, eps), - std::max(pseudo_cost_up[k] * f_up, eps)); } i_t branch_var = fractional[0]; @@ -314,13 +308,56 @@ std::pair pseudo_costs_t::variable_selection_and_obj_estimat } } - log.debug("Pseudocost branching on %d. Value %e. Score %e. Obj Estimate %e\n", + log.debug("Pseudocost branching on %d. Value %e. Score %e.\n", branch_var, solution[branch_var], - score[select], - estimate); + score[select]); + + return branch_var; +} + +template +f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log) +{ + std::lock_guard lock(mutex); + + const i_t num_fractional = fractional.size(); + f_t estimate = lower_bound; + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + + initialized(num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + for (i_t k = 0; k < num_fractional; k++) { + const i_t j = fractional[k]; + f_t pseudo_cost_down = 0; + f_t pseudo_cost_up = 0; + + if (pseudo_cost_num_down[j] != 0) { + pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; + } else { + pseudo_cost_down = pseudo_cost_down_avg; + } + + if (pseudo_cost_num_up[j] != 0) { + pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; + } else { + pseudo_cost_up = pseudo_cost_up_avg; + } + constexpr f_t eps = 1e-6; + const f_t f_down = solution[j] - std::floor(solution[j]); + const f_t f_up = std::ceil(solution[j]) - solution[j]; + estimate += + std::min(std::max(pseudo_cost_down * f_down, eps), std::max(pseudo_cost_up * f_up, eps)); + } - return {branch_var, estimate}; + return estimate; } template diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 5c34e0296..49a810506 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -43,10 +43,14 @@ class pseudo_costs_t { f_t& pseudo_cost_down_avg, f_t& pseudo_cost_up_avg) const; - std::pair variable_selection_and_obj_estimate(const std::vector& fractional, - const std::vector& solution, - f_t lower_bound, - logger_t& log); + i_t variable_selection(const std::vector& fractional, + const std::vector& solution, + logger_t& log); + + f_t obj_estimate(const std::vector& fractional, + const std::vector& solution, + f_t lower_bound, + logger_t& log); void update_pseudo_costs_from_strong_branching(const std::vector& fractional, const std::vector& root_soln); From a36bf03e64854ee038be29d94ff9f6de19230b0c Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 11:56:51 +0100 Subject: [PATCH 56/70] added log --- cpp/src/dual_simplex/pseudo_costs.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 143b25d24..f3cbb4447 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -357,6 +357,7 @@ f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, std::min(std::max(pseudo_cost_down * f_down, eps), std::max(pseudo_cost_up * f_up, eps)); } + log.debug("pseudocost estimate = %e\n", estimate); return estimate; } From 7c5c9968b1abf2ab50d7251e4db34fe084aae6e2 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 14:29:24 +0100 Subject: [PATCH 57/70] small refactor --- cpp/src/dual_simplex/branch_and_bound.cpp | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index bab8f8685..cbd72e92c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -845,11 +845,12 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) worker->recompute_basis = !has_children(status); worker->recompute_bounds = !has_children(status); + ++nodes_since_last_log_; ++exploration_stats_.nodes_explored; --exploration_stats_.nodes_unexplored; - ++nodes_since_last_log_; if (status == node_solve_info_t::TIME_LIMIT) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; break; } else if (has_children(status)) { @@ -1189,15 +1190,25 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); + auto down_child = search_tree_.root.get_down_child(); + auto up_child = search_tree_.root.get_up_child(); + node_queue.push(down_child); + node_queue.push(up_child); + settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", settings_.num_threads, settings_.num_bfs_workers, settings_.num_threads - settings_.num_bfs_workers); - auto down_child = search_tree_.root.get_down_child(); - auto up_child = search_tree_.root.get_up_child(); - node_queue.push(down_child); - node_queue.push(up_child); + diving_heuristics_settings_t diving_settings = settings_.diving_settings; + bool is_ramp_up_finished = false; + + std::vector worker_types = {EXPLORATION}; + std::array max_num_workers_per_type; + max_num_workers_per_type.fill(0); + max_num_workers_per_type[EXPLORATION] = settings_.num_threads; + worker_pool_.init(2 * settings_.num_threads, original_lp_, Arow_, var_types_, settings_); + active_workers_per_type.fill(0); f_t lower_bound = get_lower_bound(); f_t abs_gap = upper_bound_ - lower_bound; @@ -1211,16 +1222,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lower_bound_ceiling_ = inf; min_node_queue_size_ = 2 * settings_.num_threads; - diving_heuristics_settings_t diving_settings = settings_.diving_settings; - bool is_ramp_up_finished = false; - - std::vector worker_types = {EXPLORATION}; - std::array max_num_workers_per_type; - max_num_workers_per_type.fill(0); - max_num_workers_per_type[EXPLORATION] = settings_.num_threads; - worker_pool_.init(2 * settings_.num_threads, original_lp_, Arow_, var_types_, settings_); - active_workers_per_type.fill(0); - settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " "| Time |\n"); From 5753de86aff3441ef080aca80c3f7b0b942071ae Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 15:14:18 +0100 Subject: [PATCH 58/70] code cleanup --- cpp/src/dual_simplex/branch_and_bound.cpp | 39 +++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 9 ++++- .../dual_simplex/simplex_solver_settings.hpp | 2 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index cbd72e92c..fa65d7928 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1189,16 +1189,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_vstatus_, original_lp_, log); - - auto down_child = search_tree_.root.get_down_child(); - auto up_child = search_tree_.root.get_up_child(); - node_queue.push(down_child); - node_queue.push(up_child); - - settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", - settings_.num_threads, - settings_.num_bfs_workers, - settings_.num_threads - settings_.num_bfs_workers); + node_queue.push(search_tree_.root.get_down_child()); + node_queue.push(search_tree_.root.get_up_child()); diving_heuristics_settings_t diving_settings = settings_.diving_settings; bool is_ramp_up_finished = false; @@ -1210,11 +1202,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut worker_pool_.init(2 * settings_.num_threads, original_lp_, Arow_, var_types_, settings_); active_workers_per_type.fill(0); - f_t lower_bound = get_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; - f_t last_log = 0.0; + settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", + settings_.num_threads, + settings_.num_bfs_workers, + settings_.num_threads - settings_.num_bfs_workers); exploration_stats_.nodes_explored = 1; exploration_stats_.nodes_unexplored = 2; @@ -1230,6 +1221,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut { #pragma omp master { + f_t lower_bound = get_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; + f_t last_log = 0.0; + while (solver_status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_workers_per_type[0] > 0 || node_queue.best_first_queue_size() > 0)) { @@ -1241,8 +1238,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut repair_heuristic_solutions(); if (!is_ramp_up_finished) { - if (node_queue.best_first_queue_size() >= min_node_queue_size_ && - node_queue.bfs_top()->depth >= diving_settings.min_node_depth) { + if (node_queue.best_first_queue_size() >= min_node_queue_size_) { if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } max_num_workers_per_type = bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); @@ -1338,7 +1334,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::optional*> start_node = node_queue.pop_diving(); if (!start_node.has_value()) { continue; } - if (upper_bound_ < start_node.value()->lower_bound) { continue; } + if (upper_bound_ < start_node.value()->lower_bound || + start_node.value()->depth < diving_settings.min_node_depth) { + continue; + } bool is_feasible = worker->init_diving(start_node.value(), type, original_lp_, settings_); @@ -1366,8 +1365,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solver_status_ = mip_exploration_status_t::COMPLETED; } - lower_bound = node_queue.best_first_queue_size() > 0 ? node_queue.get_lower_bound() - : search_tree_.root.lower_bound; + f_t lower_bound = node_queue.best_first_queue_size() > 0 ? node_queue.get_lower_bound() + : search_tree_.root.lower_bound; return set_final_solution(solution, lower_bound); } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 1930b2cb6..e15f7df96 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -163,7 +163,12 @@ class branch_and_bound_t { // Global status of the solver. omp_atomic_t solver_status_; + // Count the number of nodes since the last report. omp_atomic_t nodes_since_last_log_; + + // Minimum number of node in the queue. When the queue size is less than + // than this variable, the nodes are added directly to the queue instead of + // the local stack. This also determines the end of the ramp-up phase. i_t min_node_queue_size_; // In case, a best-first thread encounters a numerical issue when solving a node, @@ -186,12 +191,12 @@ class branch_and_bound_t { // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); + // Perform a plunge over a subtree using a given worker. void plunge_with(bnb_worker_t* worker); + // Perform a deep dive over a subtree using a given worker. void dive_with(bnb_worker_t* worker); - void master_loop(); - // Solve the LP relaxation of a leaf node and update the tree. node_solve_info_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 77e0628ce..c46eda085 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -29,7 +29,7 @@ struct diving_heuristics_settings_t { bool disable_guided_diving = false; bool disable_coefficient_diving = false; - i_t min_node_depth = 5; + i_t min_node_depth = 10; i_t node_limit = 500; f_t iteration_limit_factor = 0.05; i_t backtrack = 5; From 421cbfd84d9e39618a665db3378fbe1442979a53 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 15:31:16 +0100 Subject: [PATCH 59/70] fix reporting frequency --- cpp/src/dual_simplex/branch_and_bound.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index fa65d7928..d42b09e02 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1212,6 +1212,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solver_status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; min_node_queue_size_ = 2 * settings_.num_threads; + nodes_since_last_log_ = 0; settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " From 6faeed0f58b749b1d0ad3d246eb1a1e6ef6872eb Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 15:43:49 +0100 Subject: [PATCH 60/70] fix style --- cpp/src/dual_simplex/bnb_worker.cpp | 2 +- cpp/src/dual_simplex/bounds_strengthening.hpp | 2 +- cpp/src/dual_simplex/presolve.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.cpp b/cpp/src/dual_simplex/bnb_worker.cpp index b4a1d8583..0d8157541 100644 --- a/cpp/src/dual_simplex/bnb_worker.cpp +++ b/cpp/src/dual_simplex/bnb_worker.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/bounds_strengthening.hpp b/cpp/src/dual_simplex/bounds_strengthening.hpp index dfad27005..6a43ef44c 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.hpp +++ b/cpp/src/dual_simplex/bounds_strengthening.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 56b89f884..6c8c3ae4a 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ From d7046e31fa25e4fd78f89067141c593af0ccd4f9 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 16:25:53 +0100 Subject: [PATCH 61/70] added missing stl headers. fix incorrect round-robin. --- cpp/src/dual_simplex/bnb_worker.hpp | 11 +++++++---- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- cpp/src/dual_simplex/branch_and_bound.hpp | 2 +- cpp/src/dual_simplex/diving_heuristics.cpp | 1 + cpp/src/dual_simplex/node_queue.hpp | 6 +++++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index 00352d50a..121f6a6a3 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -12,7 +12,9 @@ #include #include +#include #include +#include #include namespace cuopt::linear_programming::dual_simplex { @@ -214,10 +216,11 @@ std::array bnb_get_num_workers_round_robin( i_t diving_workers = 2 * settings.num_diving_workers; i_t m = worker_types.size() - 1; - for (size_t i = 1, k = 0; i < bnb_num_worker_types; ++i) { - i_t start = (double)k * diving_workers / m; - i_t end = (double)(k + 1) * diving_workers / m; - max_num_workers[i] = end - start; + + for (size_t i = 1, k = 0; i < worker_types.size(); ++i) { + i_t start = (double)k * diving_workers / m; + i_t end = (double)(k + 1) * diving_workers / m; + max_num_workers[worker_types[i]] = end - start; ++k; } diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d42b09e02..250b7421f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1178,7 +1178,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Choose variable to branch on - auto branch_var = pc_.variable_selection(fractional, root_relax_soln_.x, log); + i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x, log); search_tree_.root = std::move(mip_node_t(root_objective_, root_vstatus_)); search_tree_.num_nodes = 0; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index e15f7df96..0ee5a82f5 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -167,7 +167,7 @@ class branch_and_bound_t { omp_atomic_t nodes_since_last_log_; // Minimum number of node in the queue. When the queue size is less than - // than this variable, the nodes are added directly to the queue instead of + // this variable, the nodes are added directly to the queue instead of // the local stack. This also determines the end of the ramp-up phase. i_t min_node_queue_size_; diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index ce9460fa9..2d564a815 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -6,6 +6,7 @@ /* clang-format on */ #include +#include namespace cuopt::linear_programming::dual_simplex { diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 47b31f590..b5c9a9ea7 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -6,6 +6,10 @@ #pragma once #include +#include +#include +#include +#include #include #include @@ -36,7 +40,7 @@ class heap_t { template void emplace(Args&&... args) { - buffer.emplace_back(std::forward(args)...); + buffer.emplace_back(std::forward(args)...); std::push_heap(buffer.begin(), buffer.end(), comp); } From 14441d15f8898bb819c83df26ca844fb84d65656 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 12 Jan 2026 10:58:32 +0100 Subject: [PATCH 62/70] refactor to eliminate enum --- cpp/src/dual_simplex/CMakeLists.txt | 1 - cpp/src/dual_simplex/bnb_worker.cpp | 81 ------ cpp/src/dual_simplex/bnb_worker.hpp | 85 ++++-- cpp/src/dual_simplex/branch_and_bound.cpp | 325 +++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 50 ++-- 5 files changed, 247 insertions(+), 295 deletions(-) delete mode 100644 cpp/src/dual_simplex/bnb_worker.cpp diff --git a/cpp/src/dual_simplex/CMakeLists.txt b/cpp/src/dual_simplex/CMakeLists.txt index d59120878..af1415fa9 100644 --- a/cpp/src/dual_simplex/CMakeLists.txt +++ b/cpp/src/dual_simplex/CMakeLists.txt @@ -10,7 +10,6 @@ set(DUAL_SIMPLEX_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/basis_updates.cpp ${CMAKE_CURRENT_SOURCE_DIR}/bound_flipping_ratio_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/branch_and_bound.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/bnb_worker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/crossover.cpp ${CMAKE_CURRENT_SOURCE_DIR}/folding.cpp ${CMAKE_CURRENT_SOURCE_DIR}/initial_basis.cpp diff --git a/cpp/src/dual_simplex/bnb_worker.cpp b/cpp/src/dual_simplex/bnb_worker.cpp deleted file mode 100644 index 0d8157541..000000000 --- a/cpp/src/dual_simplex/bnb_worker.cpp +++ /dev/null @@ -1,81 +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 -#include -#include -#include - -namespace cuopt::linear_programming::dual_simplex { - -template -bnb_worker_t::bnb_worker_t(i_t worker_id, - const lp_problem_t& original_lp, - const csr_matrix_t& Arow, - const std::vector& var_type, - const simplex_solver_settings_t& settings) - : worker_id(worker_id), - worker_type(EXPLORATION), - is_active(false), - lower_bound(-std::numeric_limits::infinity()), - leaf_problem(original_lp), - basis_factors(original_lp.num_rows, settings.refactor_frequency), - basic_list(original_lp.num_rows), - nonbasic_list(), - node_presolver(leaf_problem, Arow, {}, var_type), - bounds_changed(original_lp.num_cols, false) -{ -} - -template -bool bnb_worker_t::init_diving(mip_node_t* node, - bnb_worker_type_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; - worker_type = type; - lower_bound = node->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(start_lower, start_upper, bounds_changed, settings); -} - -template -bool bnb_worker_t::set_lp_variable_bounds_for( - mip_node_t* node_ptr, const simplex_solver_settings_t& settings) -{ - // 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); - - } else { - node_ptr->update_branched_variable_bounds( - leaf_problem.lower, leaf_problem.upper, bounds_changed); - } - - return node_presolver.bounds_strengthening( - leaf_problem.lower, leaf_problem.upper, bounds_changed, settings); -} - -#ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE -template class bnb_worker_t; -#endif - -} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index 121f6a6a3..e4b12380d 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -36,15 +36,17 @@ enum bnb_worker_type_t : int { template struct bnb_stats_t { - f_t start_time = 0.0; - omp_atomic_t total_lp_solve_time = 0.0; - omp_atomic_t nodes_explored = 0; - omp_atomic_t nodes_unexplored = 0; - omp_atomic_t total_lp_iters = 0; + f_t start_time = 0.0; + omp_atomic_t total_lp_solve_time = 0.0; + omp_atomic_t nodes_explored = 0; + omp_atomic_t nodes_unexplored = 0; + omp_atomic_t total_lp_iters = 0; + omp_atomic_t nodes_since_last_log = 0; + omp_atomic_t last_log = 0.0; }; template -class bnb_worker_t { +class bnb_worker_data_t { public: const i_t worker_id; omp_atomic_t worker_type; @@ -52,6 +54,7 @@ class bnb_worker_t { omp_atomic_t lower_bound; lp_problem_t leaf_problem; + lp_solution_t leaf_solution; basis_update_mpf_t basis_factors; std::vector basic_list; @@ -67,11 +70,24 @@ class bnb_worker_t { bool recompute_basis = true; bool recompute_bounds = true; - bnb_worker_t(i_t worker_id, - const lp_problem_t& original_lp, - const csr_matrix_t& Arow, - const std::vector& var_type, - const simplex_solver_settings_t& settings); + bnb_worker_data_t(i_t worker_id, + const lp_problem_t& original_lp, + const csr_matrix_t& Arow, + const std::vector& var_type, + const simplex_solver_settings_t& settings) + : worker_id(worker_id), + worker_type(EXPLORATION), + is_active(false), + lower_bound(-std::numeric_limits::infinity()), + leaf_problem(original_lp), + leaf_solution(original_lp.num_rows, original_lp.num_cols), + basis_factors(original_lp.num_rows, settings.refactor_frequency), + basic_list(original_lp.num_rows), + nonbasic_list(), + node_presolver(leaf_problem, Arow, {}, var_type), + bounds_changed(original_lp.num_cols, false) + { + } // Set the `start_node` for best-first search. void init_best_first(mip_node_t* node, const lp_problem_t& original_lp) @@ -90,11 +106,44 @@ class bnb_worker_t { bool init_diving(mip_node_t* node, bnb_worker_type_t type, const lp_problem_t& original_lp, - const simplex_solver_settings_t& settings); + 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; + worker_type = type; + lower_bound = node->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(start_lower, start_upper, bounds_changed, settings); + } // Set the variables bounds for the LP relaxation of the current node. bool set_lp_variable_bounds_for(mip_node_t* node_ptr, - const simplex_solver_settings_t& settings); + const simplex_solver_settings_t& settings) + { + // 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); + + } else { + node_ptr->update_branched_variable_bounds( + leaf_problem.lower, leaf_problem.upper, bounds_changed); + } + + return node_presolver.bounds_strengthening( + leaf_problem.lower, leaf_problem.upper, bounds_changed, settings); + } private: // For diving, we need to store the full node instead of @@ -119,12 +168,12 @@ class bnb_worker_pool_t { 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); + std::make_unique>(i, original_lp, Arow, var_type, settings); idle_workers_.push_front(i); } } - bnb_worker_t* get_idle_worker() + bnb_worker_data_t* get_idle_worker() { std::lock_guard lock(mutex_); @@ -145,7 +194,7 @@ class bnb_worker_pool_t { } } - bnb_worker_t* get_and_pop_idle_worker() + bnb_worker_data_t* get_and_pop_idle_worker() { std::lock_guard lock(mutex_); @@ -159,7 +208,7 @@ class bnb_worker_pool_t { } } - void return_worker_to_pool(bnb_worker_t* worker) + void return_worker_to_pool(bnb_worker_data_t* worker) { worker->is_active = false; std::lock_guard lock(mutex_); @@ -184,7 +233,7 @@ class bnb_worker_pool_t { private: // Worker pool - std::vector>> workers_; + std::vector>> workers_; omp_mutex_t mutex_; std::deque idle_workers_; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 250b7421f..a3cd256ef 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -206,12 +206,6 @@ inline const char* feasible_solution_symbol(bnb_worker_type_t type) } } -inline bool has_children(node_solve_info_t status) -{ - return status == node_solve_info_t::UP_CHILD_FIRST || - status == node_solve_info_t::DOWN_CHILD_FIRST; -} - } // namespace template @@ -226,7 +220,7 @@ branch_and_bound_t::branch_and_bound_t( root_relax_soln_(1, 1), root_crossover_soln_(1, 1), pc_(1), - solver_status_(mip_exploration_status_t::UNSET) + solver_status_(mip_status_t::UNSET) { exploration_stats_.start_time = tic(); dualize_info_t dualize_info; @@ -240,7 +234,7 @@ template f_t branch_and_bound_t::get_lower_bound() { f_t lower_bound = lower_bound_ceiling_.load(); - f_t heap_lower_bound = node_queue.get_lower_bound(); + f_t heap_lower_bound = node_queue_.get_lower_bound(); lower_bound = std::min(heap_lower_bound, lower_bound); lower_bound = std::min(worker_pool_.get_lower_bounds(), lower_bound); return std::isfinite(lower_bound) ? lower_bound : -inf; @@ -249,7 +243,7 @@ f_t branch_and_bound_t::get_lower_bound() template void branch_and_bound_t::report_heuristic(f_t obj) { - if (solver_status_ == mip_exploration_status_t::RUNNING) { + if (is_running) { f_t user_obj = compute_user_objective(original_lp_, obj); f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); std::string user_gap = user_mip_gap(user_obj, user_lower); @@ -441,23 +435,18 @@ void branch_and_bound_t::repair_heuristic_solutions() } template -mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t& solution, - f_t lower_bound) +void branch_and_bound_t::set_final_solution(mip_solution_t& solution, + f_t lower_bound) { - mip_status_t mip_status = mip_status_t::UNSET; - - if (solver_status_ == mip_exploration_status_t::NUMERICAL) { + if (solver_status_ == mip_status_t::NUMERICAL) { settings_.log.printf("Numerical issue encountered. Stopping the solver...\n"); - mip_status = mip_status_t::NUMERICAL; } - if (solver_status_ == mip_exploration_status_t::TIME_LIMIT) { + if (solver_status_ == mip_status_t::TIME_LIMIT) { settings_.log.printf("Time limit reached. Stopping the solver...\n"); - mip_status = mip_status_t::TIME_LIMIT; } - if (solver_status_ == mip_exploration_status_t::NODE_LIMIT) { + if (solver_status_ == mip_status_t::NODE_LIMIT) { settings_.log.printf("Node limit reached. Stopping the solver...\n"); - mip_status = mip_status_t::NODE_LIMIT; } f_t gap = upper_bound_ - lower_bound; @@ -476,7 +465,7 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t 0 && gap <= settings_.absolute_mip_gap_tol) { settings_.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", settings_.absolute_mip_gap_tol); @@ -491,11 +480,11 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t 0 && exploration_stats_.nodes_unexplored == 0 && upper_bound_ == inf) { settings_.log.printf("Integer infeasible.\n"); - mip_status = mip_status_t::INFEASIBLE; + solver_status_ = mip_status_t::INFEASIBLE; if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } @@ -510,8 +499,6 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t @@ -605,17 +592,12 @@ branch_variable_t branch_and_bound_t::variable_selection( } template -node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* node_ptr, - search_tree_t& search_tree, - bnb_worker_type_t thread_type, - bnb_worker_t* worker, +dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* node_ptr, + bnb_worker_data_t* worker_data, bnb_stats_t& stats, logger_t& log) { - 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(leaf_problem.num_rows, leaf_problem.num_cols); + lp_problem_t& leaf_problem = worker_data->leaf_problem; std::vector& leaf_vstatus = node_ptr->vstatus; assert(leaf_vstatus.size() == leaf_problem.num_cols); @@ -626,12 +608,12 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); lp_settings.scale_columns = false; - if (thread_type != bnb_worker_type_t::EXPLORATION) { + if (worker_data->worker_type != bnb_worker_type_t::EXPLORATION) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; f_t factor = settings_.diving_settings.iteration_limit_factor; f_t max_iter = factor * bnb_lp_iters; lp_settings.iteration_limit = max_iter - stats.total_lp_iters; - if (lp_settings.iteration_limit <= 0) { return node_solve_info_t::ITERATION_LIMIT; } + if (lp_settings.iteration_limit <= 0) { return dual::status_t::ITERATION_LIMIT; } } #ifdef LOG_NODE_SIMPLEX @@ -656,7 +638,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* node_ptr->vstatus[node_ptr->branch_var]); #endif - bool is_feasible = worker->set_lp_variable_bounds_for(node_ptr, settings_); + bool is_feasible = worker_data->set_lp_variable_bounds_for(node_ptr, settings_); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; if (is_feasible) { @@ -666,29 +648,30 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* lp_status = dual_phase2_with_advanced_basis(2, 0, - worker->recompute_basis, + worker_data->recompute_basis, lp_start_time, leaf_problem, lp_settings, leaf_vstatus, - worker->basis_factors, - worker->basic_list, - worker->nonbasic_list, - leaf_solution, + worker_data->basis_factors, + worker_data->basic_list, + worker_data->nonbasic_list, + worker_data->leaf_solution, node_iter, leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); - lp_status_t second_status = solve_linear_program_with_advanced_basis(leaf_problem, - lp_start_time, - lp_settings, - leaf_solution, - worker->basis_factors, - worker->basic_list, - worker->nonbasic_list, - leaf_vstatus, - leaf_edge_norms); + lp_status_t second_status = + solve_linear_program_with_advanced_basis(leaf_problem, + lp_start_time, + lp_settings, + worker_data->leaf_solution, + worker_data->basis_factors, + worker_data->basic_list, + worker_data->nonbasic_list, + leaf_vstatus, + leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } @@ -701,12 +684,27 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* lp_settings.log.printf("\nLP status: %d\n\n", lp_status); #endif + return lp_status; +} +template +std::pair branch_and_bound_t::update_tree( + mip_node_t* node_ptr, + search_tree_t& search_tree, + bnb_worker_data_t* worker_data, + dual::status_t lp_status, + logger_t& log) +{ + const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; + std::vector& leaf_vstatus = node_ptr->vstatus; + lp_problem_t& leaf_problem = worker_data->leaf_problem; + lp_solution_t& leaf_solution = worker_data->leaf_solution; + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { // Node was infeasible. Do not branch node_ptr->lower_bound = inf; search_tree.graphviz_node(log, node_ptr, "infeasible", 0.0); search_tree.update(node_ptr, node_status_t::INFEASIBLE); - return node_solve_info_t::NO_CHILDREN; + return {node_status_t::INFEASIBLE, rounding_direction_t::NONE}; } else if (lp_status == dual::status_t::CUTOFF) { // Node was cut off. Do not branch @@ -714,7 +712,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); search_tree.graphviz_node(log, node_ptr, "cut off", leaf_objective); search_tree.update(node_ptr, node_status_t::FATHOMED); - return node_solve_info_t::NO_CHILDREN; + return {node_status_t::FATHOMED, rounding_direction_t::NONE}; } else if (lp_status == dual::status_t::OPTIMAL) { // LP was feasible @@ -727,7 +725,7 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* search_tree.graphviz_node(log, node_ptr, "lower bound", leaf_objective); pc_.update_pseudo_costs(node_ptr, leaf_objective); - if (thread_type == bnb_worker_type_t::EXPLORATION) { + if (worker_data->worker_type == bnb_worker_type_t::EXPLORATION) { if (settings_.node_processed_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, leaf_solution.x, original_x); @@ -737,15 +735,19 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* if (leaf_num_fractional == 0) { // Found a integer feasible solution - add_feasible_solution(leaf_objective, leaf_solution.x, node_ptr->depth, thread_type); + add_feasible_solution( + leaf_objective, leaf_solution.x, node_ptr->depth, worker_data->worker_type); search_tree.graphviz_node(log, node_ptr, "integer feasible", leaf_objective); search_tree.update(node_ptr, node_status_t::INTEGER_FEASIBLE); - return node_solve_info_t::NO_CHILDREN; + return {node_status_t::INTEGER_FEASIBLE, rounding_direction_t::NONE}; } else if (leaf_objective <= upper_bound_ + abs_fathom_tol) { + logger_t select_log; + select_log.log = false; + // Choose fractional variable to branch on auto [branch_var, round_dir] = variable_selection( - node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); + node_ptr, leaf_fractional, leaf_solution.x, worker_data->worker_type, select_log); assert(leaf_vstatus.size() == leaf_problem.num_cols); assert(branch_var >= 0); @@ -754,27 +756,15 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* search_tree.branch( node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, leaf_problem, log); search_tree.update(node_ptr, node_status_t::HAS_CHILDREN); - - if (round_dir == rounding_direction_t::UP) { - return node_solve_info_t::UP_CHILD_FIRST; - } else { - return node_solve_info_t::DOWN_CHILD_FIRST; - } + return {node_status_t::HAS_CHILDREN, round_dir}; } else { search_tree.graphviz_node(log, node_ptr, "fathomed", leaf_objective); search_tree.update(node_ptr, node_status_t::FATHOMED); - return node_solve_info_t::NO_CHILDREN; + return {node_status_t::FATHOMED, rounding_direction_t::NONE}; } - } else if (lp_status == dual::status_t::TIME_LIMIT) { - search_tree.graphviz_node(log, node_ptr, "timeout", 0.0); - return node_solve_info_t::TIME_LIMIT; - - } else if (lp_status == dual::status_t::ITERATION_LIMIT) { - return node_solve_info_t::ITERATION_LIMIT; - } else { - if (thread_type == bnb_worker_type_t::EXPLORATION) { + if (worker_data->worker_type == bnb_worker_type_t::EXPLORATION) { fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); log.printf( "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " @@ -787,20 +777,19 @@ node_solve_info_t branch_and_bound_t::solve_node(mip_node_t* search_tree.graphviz_node(log, node_ptr, "numerical", 0.0); search_tree.update(node_ptr, node_status_t::NUMERICAL); - return node_solve_info_t::NUMERICAL; + return {node_status_t::NUMERICAL, rounding_direction_t::NONE}; } } template -void branch_and_bound_t::plunge_with(bnb_worker_t* worker) +void branch_and_bound_t::plunge_with(bnb_worker_data_t* worker_data) { std::deque*> stack; - stack.push_front(worker->start_node); - - worker->recompute_basis = true; - worker->recompute_bounds = true; + stack.push_front(worker_data->start_node); + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; - while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { + while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -814,46 +803,48 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) // - 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_data->lower_bound = lower_bound; if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { 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; - worker->recompute_bounds = true; + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; --exploration_stats_.nodes_unexplored; continue; } if (toc(exploration_stats_.start_time) > settings_.time_limit) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; + solver_status_ = mip_status_t::TIME_LIMIT; break; } if (exploration_stats_.nodes_explored >= settings_.node_limit) { - solver_status_ = mip_exploration_status_t::NODE_LIMIT; + solver_status_ = mip_status_t::NODE_LIMIT; break; } - node_solve_info_t status = solve_node(node_ptr, - search_tree_, - bnb_worker_type_t::EXPLORATION, - worker, - exploration_stats_, - settings_.log); + dual::status_t lp_status = + solve_node_lp(node_ptr, worker_data, exploration_stats_, settings_.log); - worker->recompute_basis = !has_children(status); - worker->recompute_bounds = !has_children(status); + if (lp_status == dual::status_t::TIME_LIMIT) { + solver_status_ = mip_status_t::TIME_LIMIT; + break; + } else if (lp_status == dual::status_t::ITERATION_LIMIT) { + break; + } - ++nodes_since_last_log_; + ++exploration_stats_.nodes_since_last_log; ++exploration_stats_.nodes_explored; --exploration_stats_.nodes_unexplored; - if (status == node_solve_info_t::TIME_LIMIT) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - break; + auto [node_status, round_dir] = + update_tree(node_ptr, search_tree_, worker_data, lp_status, settings_.log); - } else if (has_children(status)) { + worker_data->recompute_basis = node_status != node_status_t::HAS_CHILDREN; + worker_data->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, // we pop the current node from the stack and place it in the global heap, @@ -861,22 +852,22 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) if (stack.size() > 0) { mip_node_t* node = stack.back(); stack.pop_back(); - node_queue.push(node); + node_queue_.push(node); } exploration_stats_.nodes_unexplored += 2; - if (status == node_solve_info_t::UP_CHILD_FIRST) { - if (node_queue.best_first_queue_size() < min_node_queue_size_) { - node_queue.push(node_ptr->get_down_child()); + if (round_dir == rounding_direction_t::UP) { + if (node_queue_.best_first_queue_size() < min_node_queue_size_) { + node_queue_.push(node_ptr->get_down_child()); } else { stack.push_front(node_ptr->get_down_child()); } stack.push_front(node_ptr->get_up_child()); } else { - if (node_queue.best_first_queue_size() < min_node_queue_size_) { - node_queue.push(node_ptr->get_up_child()); + if (node_queue_.best_first_queue_size() < min_node_queue_size_) { + node_queue_.push(node_ptr->get_up_child()); } else { stack.push_front(node_ptr->get_up_child()); } @@ -886,26 +877,26 @@ void branch_and_bound_t::plunge_with(bnb_worker_t* worker) } } - worker_pool_.return_worker_to_pool(worker); + worker_pool_.return_worker_to_pool(worker_data); active_workers_per_type[EXPLORATION]--; } template -void branch_and_bound_t::dive_with(bnb_worker_t* worker) +void branch_and_bound_t::dive_with(bnb_worker_data_t* worker_data) { logger_t log; log.log = false; - bnb_worker_type_t diving_type = worker->worker_type; + bnb_worker_type_t diving_type = worker_data->worker_type; const i_t node_limit = settings_.diving_settings.node_limit; const i_t backtrack = settings_.diving_settings.backtrack; - worker->recompute_basis = true; - worker->recompute_bounds = true; + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; - search_tree_t subtree(std::move(*worker->start_node)); + search_tree_t dive_tree(std::move(*worker_data->start_node)); std::deque*> stack; - stack.push_front(&subtree.root); + stack.push_front(&dive_tree.root); bnb_stats_t dive_stats; dive_stats.total_lp_iters = 0; @@ -913,38 +904,42 @@ void branch_and_bound_t::dive_with(bnb_worker_t* worker) dive_stats.nodes_explored = 0; dive_stats.nodes_unexplored = 0; - while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { + while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); - 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; + 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_data->lower_bound = lower_bound; if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { - worker->recompute_basis = true; - worker->recompute_bounds = true; + worker_data->recompute_basis = true; + worker_data->recompute_bounds = true; continue; } if (toc(exploration_stats_.start_time) > settings_.time_limit) { break; } if (dive_stats.nodes_explored > node_limit) { break; } - node_solve_info_t status = solve_node(node_ptr, subtree, diving_type, worker, dive_stats, log); - dive_stats.nodes_explored++; - worker->recompute_basis = !has_children(status); - worker->recompute_bounds = !has_children(status); + dual::status_t lp_status = solve_node_lp(node_ptr, worker_data, dive_stats, log); - if (status == node_solve_info_t::TIME_LIMIT) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; + if (lp_status == dual::status_t::TIME_LIMIT) { + solver_status_ = mip_status_t::TIME_LIMIT; break; - - } else if (status == node_solve_info_t::ITERATION_LIMIT) { + } else if (lp_status == dual::status_t::ITERATION_LIMIT) { break; + } + + ++dive_stats.nodes_explored; + + auto [node_status, round_dir] = update_tree(node_ptr, dive_tree, worker_data, lp_status, log); - } else if (has_children(status)) { - if (status == node_solve_info_t::UP_CHILD_FIRST) { + worker_data->recompute_basis = node_status != node_status_t::HAS_CHILDREN; + worker_data->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()); stack.push_front(node_ptr->get_up_child()); } else { @@ -958,7 +953,7 @@ void branch_and_bound_t::dive_with(bnb_worker_t* worker) } } - worker_pool_.return_worker_to_pool(worker); + worker_pool_.return_worker_to_pool(worker_data); active_workers_per_type[diving_type]--; } @@ -1043,7 +1038,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut logger_t log; log.log = false; log.log_prefix = settings_.log.log_prefix; - solver_status_ = mip_exploration_status_t::UNSET; + solver_status_ = mip_status_t::UNSET; + is_running = false; exploration_stats_.nodes_unexplored = 0; exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); @@ -1108,8 +1104,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::TIME_LIMIT) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return set_final_solution(solution, -inf); + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, -inf); + return solver_status_; } assert(root_vstatus_.size() == original_lp_.num_cols); @@ -1173,8 +1170,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut pc_); if (toc(exploration_stats_.start_time) > settings_.time_limit) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; - return set_final_solution(solution, root_objective_); + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; } // Choose variable to branch on @@ -1189,8 +1187,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_vstatus_, original_lp_, log); - node_queue.push(search_tree_.root.get_down_child()); - node_queue.push(search_tree_.root.get_up_child()); + node_queue_.push(search_tree_.root.get_down_child()); + node_queue_.push(search_tree_.root.get_up_child()); diving_heuristics_settings_t diving_settings = settings_.diving_settings; bool is_ramp_up_finished = false; @@ -1207,12 +1205,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.num_bfs_workers, settings_.num_threads - settings_.num_bfs_workers); - exploration_stats_.nodes_explored = 1; - exploration_stats_.nodes_unexplored = 2; - solver_status_ = mip_exploration_status_t::RUNNING; - lower_bound_ceiling_ = inf; - min_node_queue_size_ = 2 * settings_.num_threads; - nodes_since_last_log_ = 0; + exploration_stats_.nodes_explored = 1; + exploration_stats_.nodes_unexplored = 2; + exploration_stats_.nodes_since_last_log = 0; + exploration_stats_.last_log = 0.0; + is_running = true; + lower_bound_ceiling_ = inf; + min_node_queue_size_ = 2 * settings_.num_threads; settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " @@ -1226,11 +1225,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut 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; - f_t last_log = 0.0; - while (solver_status_ == mip_exploration_status_t::RUNNING && - abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && - (active_workers_per_type[0] > 0 || node_queue.best_first_queue_size() > 0)) { + while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && + rel_gap > settings_.relative_mip_gap_tol && + (active_workers_per_type[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; @@ -1239,7 +1237,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut repair_heuristic_solutions(); if (!is_ramp_up_finished) { - if (node_queue.best_first_queue_size() >= min_node_queue_size_) { + if (node_queue_.best_first_queue_size() >= min_node_queue_size_) { if (!std::isfinite(upper_bound_)) { diving_settings.disable_guided_diving = true; } max_num_workers_per_type = bnb_get_num_workers_round_robin(settings_.num_threads, diving_settings); @@ -1250,7 +1248,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.debug( "Ramp-up phase is finished. num active workers = %d, heap size = %d\n", active_workers_per_type[EXPLORATION], - node_queue.best_first_queue_size()); + node_queue_.best_first_queue_size()); for (auto type : worker_types) { settings_.log.debug("%s: max num of workers = %d", @@ -1281,21 +1279,23 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - f_t now = toc(exploration_stats_.start_time); - f_t time_since_last_log = last_log == 0 ? 1.0 : toc(last_log); + f_t now = toc(exploration_stats_.start_time); + f_t time_since_last_log = + exploration_stats_.last_log == 0 ? 1.0 : toc(exploration_stats_.last_log); + i_t nodes_since_last_log = exploration_stats_.nodes_since_last_log; - if (((nodes_since_last_log_ >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && + if (((nodes_since_last_log >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && time_since_last_log >= 1) || (time_since_last_log > 30) || now > settings_.time_limit) { - i_t depth = - node_queue.best_first_queue_size() > 0 ? node_queue.bfs_top()->depth : last_node_depth; + i_t depth = node_queue_.best_first_queue_size() > 0 ? node_queue_.bfs_top()->depth + : last_node_depth; report(" ", upper_bound_, lower_bound, depth); - last_log = tic(); - nodes_since_last_log_ = 0; + exploration_stats_.last_log = tic(); + exploration_stats_.nodes_since_last_log = 0; } if (now > settings_.time_limit) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; + solver_status_ = mip_status_t::TIME_LIMIT; break; } @@ -1303,12 +1303,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (active_workers_per_type[type] >= max_num_workers_per_type[type]) { continue; } // Get an idle worker. - bnb_worker_t* worker = worker_pool_.get_idle_worker(); + bnb_worker_data_t* worker = worker_pool_.get_idle_worker(); if (worker == nullptr) { break; } if (type == EXPLORATION) { // If there any node left in the heap, we pop the top node and explore it. - std::optional*> start_node = node_queue.pop_best_first(); + std::optional*> start_node = node_queue_.pop_best_first(); if (!start_node.has_value()) { continue; } if (upper_bound_ < start_node.value()->lower_bound) { @@ -1325,14 +1325,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut worker->init_best_first(start_node.value(), original_lp_); last_node_depth = start_node.value()->depth; active_workers_per_type[type]++; - nodes_since_last_log_++; launched_any_task = true; #pragma omp task affinity(worker) plunge_with(worker); } else { - std::optional*> start_node = node_queue.pop_diving(); + std::optional*> start_node = node_queue_.pop_diving(); if (!start_node.has_value()) { continue; } if (upper_bound_ < start_node.value()->lower_bound || @@ -1362,13 +1361,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - if (solver_status_ == mip_exploration_status_t::RUNNING) { - solver_status_ = mip_exploration_status_t::COMPLETED; - } - - f_t lower_bound = node_queue.best_first_queue_size() > 0 ? node_queue.get_lower_bound() - : search_tree_.root.lower_bound; - return set_final_solution(solution, lower_bound); + is_running = false; + f_t lower_bound = node_queue_.best_first_queue_size() > 0 ? node_queue_.get_lower_bound() + : search_tree_.root.lower_bound; + set_final_solution(solution, lower_bound); + return solver_status_; } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 0ee5a82f5..18b48311e 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -36,24 +36,6 @@ enum class mip_status_t { UNSET = 6, // The status is not set }; -enum class mip_exploration_status_t { - UNSET = 0, // The status is not set - TIME_LIMIT = 1, // The solver reached a time limit - NODE_LIMIT = 2, // The maximum number of nodes was reached (not implemented) - NUMERICAL = 3, // The solver encountered a numerical error - RUNNING = 4, // The solver is currently exploring the tree - COMPLETED = 5, // The solver finished exploring the tree -}; - -enum class node_solve_info_t { - NO_CHILDREN = 0, // The node does not produced children - UP_CHILD_FIRST = 1, // The up child should be explored first - DOWN_CHILD_FIRST = 2, // The down child should be explored first - TIME_LIMIT = 3, // The solver reached a time limit - ITERATION_LIMIT = 4, // The solver reached a iteration limit - NUMERICAL = 5 // The solver encounter a numerical error when solving the node -}; - template void upper_bound_callback(f_t upper_bound); @@ -148,7 +130,7 @@ class branch_and_bound_t { pseudo_costs_t pc_; // Heap storing the nodes waiting to be explored. - node_queue_t node_queue; + node_queue_t node_queue_; // Search tree search_tree_t search_tree_; @@ -161,10 +143,8 @@ class branch_and_bound_t { bnb_worker_pool_t worker_pool_; // Global status of the solver. - omp_atomic_t solver_status_; - - // Count the number of nodes since the last report. - omp_atomic_t nodes_since_last_log_; + omp_atomic_t solver_status_; + omp_atomic_t is_running{false}; // Minimum number of node in the queue. When the queue size is less than // this variable, the nodes are added directly to the queue instead of @@ -179,7 +159,7 @@ class branch_and_bound_t { void report(std::string symbol, f_t obj, f_t lower_bound, i_t node_depth); // Set the final solution. - mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); + void set_final_solution(mip_solution_t& solution, f_t lower_bound); // Update the incumbent solution with the new feasible solution // found during branch and bound. @@ -192,19 +172,27 @@ class branch_and_bound_t { void repair_heuristic_solutions(); // Perform a plunge over a subtree using a given worker. - void plunge_with(bnb_worker_t* worker); + void plunge_with(bnb_worker_data_t* worker_data); // Perform a deep dive over a subtree using a given worker. - void dive_with(bnb_worker_t* worker); + void dive_with(bnb_worker_data_t* worker_data); - // Solve the LP relaxation of a leaf node and update the tree. - node_solve_info_t solve_node(mip_node_t* node_ptr, - search_tree_t& search_tree, - bnb_worker_type_t thread_type, - bnb_worker_t* worker, + // Solve the LP relaxation of a leaf node + dual::status_t solve_node_lp(mip_node_t* node_ptr, + bnb_worker_data_t* worker_data, bnb_stats_t& stats, logger_t& log); + // Update the tree based on the LP relaxation. Returns the status + // of the node and, if appropriated, the preferred rounding direction + // when visiting the children. + std::pair update_tree( + mip_node_t* node_ptr, + search_tree_t& search_tree, + bnb_worker_data_t* worker_data, + dual::status_t lp_status, + logger_t& log); + // Selects the variable to branch on. branch_variable_t variable_selection(mip_node_t* node_ptr, const std::vector& fractional, From 89cc6dec98ede67ef8a80f2bb79a4cab9ef37b94 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 12 Jan 2026 16:29:42 +0100 Subject: [PATCH 63/70] fix race condition in guided diving --- cpp/src/dual_simplex/branch_and_bound.cpp | 18 ++++++++++-------- cpp/src/dual_simplex/branch_and_bound.hpp | 3 +-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index a3cd256ef..aaed6eb60 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -555,11 +555,13 @@ branch_variable_t branch_and_bound_t::variable_selection( mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_worker_type_t type, - logger_t& log) + bnb_worker_type_t type) { + logger_t log; + log.log = false; i_t branch_var = -1; rounding_direction_t round_dir = rounding_direction_t::NONE; + std::vector current_incumbent; switch (type) { case bnb_worker_type_t::EXPLORATION: @@ -583,7 +585,10 @@ branch_variable_t branch_and_bound_t::variable_selection( return pseudocost_diving(pc_, fractional, solution, root_relax_soln_.x, log); case bnb_worker_type_t::GUIDED_DIVING: - return guided_diving(pc_, fractional, solution, incumbent_.x, log); + mutex_upper_.lock(); + current_incumbent = incumbent_.x; + mutex_upper_.unlock(); + return guided_diving(pc_, fractional, solution, current_incumbent, log); default: log.debug("Unknown variable selection method: %d\n", type); @@ -742,12 +747,9 @@ std::pair branch_and_bound_t::upd return {node_status_t::INTEGER_FEASIBLE, rounding_direction_t::NONE}; } else if (leaf_objective <= upper_bound_ + abs_fathom_tol) { - logger_t select_log; - select_log.log = false; - // Choose fractional variable to branch on - auto [branch_var, round_dir] = variable_selection( - node_ptr, leaf_fractional, leaf_solution.x, worker_data->worker_type, select_log); + auto [branch_var, round_dir] = + variable_selection(node_ptr, leaf_fractional, leaf_solution.x, worker_data->worker_type); assert(leaf_vstatus.size() == leaf_problem.num_cols); assert(branch_var >= 0); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 18b48311e..564858419 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -197,8 +197,7 @@ class branch_and_bound_t { branch_variable_t variable_selection(mip_node_t* node_ptr, const std::vector& fractional, const std::vector& solution, - bnb_worker_type_t type, - logger_t& log); + bnb_worker_type_t type); }; } // namespace cuopt::linear_programming::dual_simplex From 525f01337bad4493e05e5ca1350b2ea2dbd6b391 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 16:45:53 +0100 Subject: [PATCH 64/70] reliability branching from #599 --- cpp/src/dual_simplex/branch_and_bound.cpp | 41 ++++-- cpp/src/dual_simplex/branch_and_bound.hpp | 5 + cpp/src/dual_simplex/pseudo_costs.cpp | 145 ++++++++++++++++++++++ cpp/src/dual_simplex/pseudo_costs.hpp | 10 ++ 4 files changed, 192 insertions(+), 9 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index aaed6eb60..d72993405 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -553,6 +553,11 @@ rounding_direction_t martin_criteria(f_t val, f_t root_val) template branch_variable_t branch_and_bound_t::variable_selection( mip_node_t* node_ptr, + const lp_problem_t& lp, + const simplex_solver_settings_t& lp_settings, + const std::vector& var_types, + const std::vector& vstatus, + const std::vector& edge_norms, const std::vector& fractional, const std::vector& solution, bnb_worker_type_t type) @@ -565,8 +570,18 @@ branch_variable_t branch_and_bound_t::variable_selection( switch (type) { case bnb_worker_type_t::EXPLORATION: - branch_var = pc_.variable_selection(fractional, solution, log); - round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); + // branch_var = pc_.variable_selection(fractional, solution, log); + branch_var = pc_.reliable_variable_selection(lp, + lp_settings, + var_types_, + vstatus, + edge_norms, + fractional, + solution, + node_ptr->lower_bound, + log); + + round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); // Note that the exploration thread is the only one that can insert new nodes into the heap, // and thus, we only need to calculate the objective estimate here (it is used for @@ -643,13 +658,13 @@ dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* node_ptr->vstatus[node_ptr->branch_var]); #endif - bool is_feasible = worker_data->set_lp_variable_bounds_for(node_ptr, settings_); - dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; + bool is_feasible = worker_data->set_lp_variable_bounds_for(node_ptr, settings_); + dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; + std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; if (is_feasible) { - i_t node_iter = 0; - f_t lp_start_time = tic(); - std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; + i_t node_iter = 0; + f_t lp_start_time = tic(); lp_status = dual_phase2_with_advanced_basis(2, 0, @@ -748,8 +763,16 @@ std::pair branch_and_bound_t::upd } else if (leaf_objective <= upper_bound_ + abs_fathom_tol) { // Choose fractional variable to branch on - auto [branch_var, round_dir] = - variable_selection(node_ptr, leaf_fractional, leaf_solution.x, worker_data->worker_type); + auto [branch_var, round_dir] = variable_selection(node_ptr, + leaf_problem, + lp_settings, + var_types_, + leaf_vstatus, + leaf_edge_norms, + leaf_fractional, + leaf_solution.x, + thread_type, + lp_settings.log); assert(leaf_vstatus.size() == leaf_problem.num_cols); assert(branch_var >= 0); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 564858419..cface427f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -195,6 +195,11 @@ class branch_and_bound_t { // Selects the variable to branch on. branch_variable_t variable_selection(mip_node_t* node_ptr, + const lp_problem_t& lp, + const simplex_solver_settings_t& lp_settings, + const std::vector& var_types, + const std::vector& vstatus, + const std::vector& edge_norms, const std::vector& fractional, const std::vector& solution, bnb_worker_type_t type); diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index f3cbb4447..2d6a82fbc 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -133,6 +133,46 @@ void strong_branch_helper(i_t start, } } +template +f_t trial_branching(const lp_problem_t& original_lp, + const simplex_solver_settings_t& settings, + const std::vector& var_types, + const std::vector& root_vstatus, + const std::vector& edge_norms, + i_t branch_var, + f_t branch_var_lower, + f_t branch_var_upper) +{ + lp_problem_t child_problem = original_lp; + child_problem.lower[branch_var] = branch_var_lower; + child_problem.upper[branch_var] = branch_var_upper; + + simplex_solver_settings_t child_settings = settings; + child_settings.set_log(false); + f_t lp_start_time = tic(); + child_settings.iteration_limit = 200; + lp_solution_t solution(original_lp.num_rows, original_lp.num_cols); + i_t iter = 0; + std::vector vstatus = root_vstatus; + std::vector child_edge_norms = edge_norms; + dual::status_t status = dual_phase2( + 2, 0, lp_start_time, child_problem, child_settings, vstatus, solution, iter, child_edge_norms); + printf("Trial branching on variable %d. Lo: %e Up: %e. Iter %d. Status %d. Obj %e\n", + branch_var, + child_problem.lower[branch_var], + child_problem.upper[branch_var], + iter, + status, + compute_objective(child_problem, solution.x)); + + if (status == dual::status_t::OPTIMAL || status == dual::status_t::ITERATION_LIMIT || + status == dual::status_t::CUTOFF) { + return compute_objective(child_problem, solution.x); + } else { + return std::numeric_limits::quiet_NaN(); + } +} + } // namespace template @@ -316,6 +356,111 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio return branch_var; } +template +i_t pseudo_costs_t::reliable_variable_selection( + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& var_types, + const std::vector& vstatus, + const std::vector& edge_norms, + const std::vector& fractional, + const std::vector& solution, + f_t current_obj, + logger_t& log) +{ + mutex.lock(); + + const i_t num_fractional = fractional.size(); + std::vector pseudo_cost_up(num_fractional); + std::vector pseudo_cost_down(num_fractional); + std::vector score(num_fractional); + + i_t num_initialized_down; + i_t num_initialized_up; + f_t pseudo_cost_down_avg; + f_t pseudo_cost_up_avg; + + initialized(num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); + + mutex.unlock(); + + log.printf("PC: num initialized down %d up %d avg down %e up %e\n", + num_initialized_down, + num_initialized_up, + pseudo_cost_down_avg, + pseudo_cost_up_avg); + + const i_t reliable_threshold = 1; + + for (i_t k = 0; k < num_fractional; k++) { + const i_t j = fractional[k]; + mutex.lock(); + bool down_reliable = pseudo_cost_num_down[j] >= reliable_threshold; + mutex.unlock(); + if (down_reliable) { + mutex.lock(); + pseudo_cost_down[k] = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; + mutex.unlock(); + } else { + // Do trial branching on the down branch + f_t obj = trial_branching( + lp, settings, var_types, vstatus, edge_norms, j, lp.lower[j], std::floor(solution[j])); + if (!std::isnan(obj)) { + f_t change_in_obj = obj - current_obj; + f_t change_in_x = solution[j] - std::floor(solution[j]); + mutex.lock(); + pseudo_cost_sum_down[j] += change_in_obj / change_in_x; + pseudo_cost_num_down[j]++; + mutex.unlock(); + pseudo_cost_down[k] = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; + } + } + + mutex.lock(); + bool up_reliable = pseudo_cost_num_up[j] >= reliable_threshold; + mutex.unlock(); + if (up_reliable) { + mutex.lock(); + pseudo_cost_up[k] = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; + mutex.unlock(); + } else { + // Do trial branching on the up branch + f_t obj = trial_branching( + lp, settings, var_types, vstatus, edge_norms, j, std::ceil(solution[j]), lp.upper[j]); + if (!std::isnan(obj)) { + f_t change_in_obj = obj - current_obj; + f_t change_in_x = std::ceil(solution[j]) - solution[j]; + mutex.lock(); + pseudo_cost_sum_up[j] += change_in_obj / change_in_x; + pseudo_cost_num_up[j]++; + pseudo_cost_up[k] = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; + mutex.unlock(); + } + } + constexpr f_t eps = 1e-6; + const f_t f_down = solution[j] - std::floor(solution[j]); + const f_t f_up = std::ceil(solution[j]) - solution[j]; + score[k] = + std::max(f_down * pseudo_cost_down[k], eps) * std::max(f_up * pseudo_cost_up[k], eps); + } + + i_t branch_var = fractional[0]; + f_t max_score = -1; + i_t select = -1; + for (i_t k = 0; k < num_fractional; k++) { + if (score[k] > max_score) { + max_score = score[k]; + branch_var = fractional[k]; + select = k; + } + } + + log.printf( + "pc branching on %d. Value %e. Score %e\n", branch_var, solution[branch_var], score[select]); + + return branch_var; +} + template f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, const std::vector& solution, diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 49a810506..1569cb1c1 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -47,6 +47,16 @@ class pseudo_costs_t { const std::vector& solution, logger_t& log); + i_t reliable_variable_selection(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& var_types, + const std::vector& vstatus, + const std::vector& edge_norms, + const std::vector& fractional, + const std::vector& solution, + f_t current_obj, + logger_t& log); + f_t obj_estimate(const std::vector& fractional, const std::vector& solution, f_t lower_bound, From 0fa76f1deae7b384039548d544cc1f488996c29d Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 12 Jan 2026 12:06:17 +0100 Subject: [PATCH 65/70] modfiied reliability branching to reuse basis from node --- cpp/src/dual_simplex/basis_updates.hpp | 3 + cpp/src/dual_simplex/bnb_worker.hpp | 1 + cpp/src/dual_simplex/branch_and_bound.cpp | 53 ++++------ cpp/src/dual_simplex/branch_and_bound.hpp | 8 +- cpp/src/dual_simplex/pseudo_costs.cpp | 123 ++++++++++++++-------- cpp/src/dual_simplex/pseudo_costs.hpp | 5 + 6 files changed, 113 insertions(+), 80 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index afd4f4c9a..038db59b9 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -223,6 +223,9 @@ class basis_update_mpf_t { reset_stats(); } + basis_update_mpf_t(const basis_update_mpf_t& other) = default; + basis_update_mpf_t& operator=(const basis_update_mpf_t& other) = default; + void print_stats() const { i_t total_L_transpose_calls = total_sparse_L_transpose_ + total_dense_L_transpose_; diff --git a/cpp/src/dual_simplex/bnb_worker.hpp b/cpp/src/dual_simplex/bnb_worker.hpp index e4b12380d..e77b64639 100644 --- a/cpp/src/dual_simplex/bnb_worker.hpp +++ b/cpp/src/dual_simplex/bnb_worker.hpp @@ -55,6 +55,7 @@ class bnb_worker_data_t { lp_problem_t leaf_problem; lp_solution_t leaf_solution; + std::vector leaf_edge_norms; basis_update_mpf_t basis_factors; std::vector basic_list; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d72993405..c3fecd35f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -553,32 +553,31 @@ rounding_direction_t martin_criteria(f_t val, f_t root_val) template branch_variable_t branch_and_bound_t::variable_selection( mip_node_t* node_ptr, - const lp_problem_t& lp, - const simplex_solver_settings_t& lp_settings, - const std::vector& var_types, - const std::vector& vstatus, - const std::vector& edge_norms, const std::vector& fractional, - const std::vector& solution, - bnb_worker_type_t type) + bnb_worker_data_t* worker_data) { logger_t log; log.log = false; i_t branch_var = -1; rounding_direction_t round_dir = rounding_direction_t::NONE; std::vector current_incumbent; + std::vector& solution = worker_data->leaf_solution.x; - switch (type) { + switch (worker_data->worker_type) { case bnb_worker_type_t::EXPLORATION: // branch_var = pc_.variable_selection(fractional, solution, log); - branch_var = pc_.reliable_variable_selection(lp, - lp_settings, + branch_var = pc_.reliable_variable_selection(worker_data->leaf_problem, + settings_, var_types_, - vstatus, - edge_norms, + node_ptr->vstatus, + worker_data->leaf_edge_norms, fractional, solution, + worker_data->basis_factors, + worker_data->basic_list, + worker_data->nonbasic_list, node_ptr->lower_bound, + upper_bound_, log); round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); @@ -606,7 +605,7 @@ branch_variable_t branch_and_bound_t::variable_selection( return guided_diving(pc_, fractional, solution, current_incumbent, log); default: - log.debug("Unknown variable selection method: %d\n", type); + log.debug("Unknown variable selection method: %d\n", worker_data->worker_type); return {-1, rounding_direction_t::NONE}; } } @@ -617,9 +616,8 @@ dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* bnb_stats_t& stats, logger_t& log) { - lp_problem_t& leaf_problem = worker_data->leaf_problem; std::vector& leaf_vstatus = node_ptr->vstatus; - assert(leaf_vstatus.size() == leaf_problem.num_cols); + assert(leaf_vstatus.size() == worker_data->leaf_problem.num_cols); simplex_solver_settings_t lp_settings = settings_; lp_settings.set_log(false); @@ -658,9 +656,9 @@ dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* node_ptr->vstatus[node_ptr->branch_var]); #endif - bool is_feasible = worker_data->set_lp_variable_bounds_for(node_ptr, settings_); - dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; - std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; + bool is_feasible = worker_data->set_lp_variable_bounds_for(node_ptr, settings_); + dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; + worker_data->leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; if (is_feasible) { i_t node_iter = 0; @@ -670,7 +668,7 @@ dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* 0, worker_data->recompute_basis, lp_start_time, - leaf_problem, + worker_data->leaf_problem, lp_settings, leaf_vstatus, worker_data->basis_factors, @@ -678,12 +676,12 @@ dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* worker_data->nonbasic_list, worker_data->leaf_solution, node_iter, - leaf_edge_norms); + worker_data->leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = - solve_linear_program_with_advanced_basis(leaf_problem, + solve_linear_program_with_advanced_basis(worker_data->leaf_problem, lp_start_time, lp_settings, worker_data->leaf_solution, @@ -691,7 +689,7 @@ dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* worker_data->basic_list, worker_data->nonbasic_list, leaf_vstatus, - leaf_edge_norms); + worker_data->leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } @@ -763,16 +761,7 @@ std::pair branch_and_bound_t::upd } else if (leaf_objective <= upper_bound_ + abs_fathom_tol) { // Choose fractional variable to branch on - auto [branch_var, round_dir] = variable_selection(node_ptr, - leaf_problem, - lp_settings, - var_types_, - leaf_vstatus, - leaf_edge_norms, - leaf_fractional, - leaf_solution.x, - thread_type, - lp_settings.log); + auto [branch_var, round_dir] = variable_selection(node_ptr, leaf_fractional, worker_data); assert(leaf_vstatus.size() == leaf_problem.num_cols); assert(branch_var >= 0); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index cface427f..c4836b356 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -195,14 +195,8 @@ class branch_and_bound_t { // Selects the variable to branch on. branch_variable_t variable_selection(mip_node_t* node_ptr, - const lp_problem_t& lp, - const simplex_solver_settings_t& lp_settings, - const std::vector& var_types, - const std::vector& vstatus, - const std::vector& edge_norms, const std::vector& fractional, - const std::vector& solution, - bnb_worker_type_t type); + bnb_worker_data_t* worker_data); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 2d6a82fbc..89a539485 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -137,11 +137,15 @@ template f_t trial_branching(const lp_problem_t& original_lp, const simplex_solver_settings_t& settings, const std::vector& var_types, - const std::vector& root_vstatus, + const std::vector& vstatus, const std::vector& edge_norms, + const basis_update_mpf_t& basis_factors, + const std::vector& basic_list, + const std::vector& nonbasic_list, i_t branch_var, f_t branch_var_lower, - f_t branch_var_upper) + f_t branch_var_upper, + f_t upper_bound) { lp_problem_t child_problem = original_lp; child_problem.lower[branch_var] = branch_var_lower; @@ -151,19 +155,38 @@ f_t trial_branching(const lp_problem_t& original_lp, child_settings.set_log(false); f_t lp_start_time = tic(); child_settings.iteration_limit = 200; + child_settings.cut_off = upper_bound + settings.dual_tol; + child_settings.inside_mip = 2; + child_settings.scale_columns = false; + lp_solution_t solution(original_lp.num_rows, original_lp.num_cols); - i_t iter = 0; - std::vector vstatus = root_vstatus; - std::vector child_edge_norms = edge_norms; - dual::status_t status = dual_phase2( - 2, 0, lp_start_time, child_problem, child_settings, vstatus, solution, iter, child_edge_norms); - printf("Trial branching on variable %d. Lo: %e Up: %e. Iter %d. Status %d. Obj %e\n", - branch_var, - child_problem.lower[branch_var], - child_problem.upper[branch_var], - iter, - status, - compute_objective(child_problem, solution.x)); + i_t iter = 0; + std::vector child_vstatus = vstatus; + std::vector child_edge_norms = edge_norms; + std::vector child_basic_list = basic_list; + std::vector child_nonbasic_list = nonbasic_list; + basis_update_mpf_t child_basis_factors = basis_factors; + + dual::status_t status = dual_phase2_with_advanced_basis(2, + 0, + false, + lp_start_time, + child_problem, + child_settings, + child_vstatus, + child_basis_factors, + child_basic_list, + child_nonbasic_list, + solution, + iter, + child_edge_norms); + settings.log.debug("Trial branching on variable %d. Lo: %e Up: %e. Iter %d. Status %d. Obj %e\n", + branch_var, + child_problem.lower[branch_var], + child_problem.upper[branch_var], + iter, + status, + compute_objective(child_problem, solution.x)); if (status == dual::status_t::OPTIMAL || status == dual::status_t::ITERATION_LIMIT || status == dual::status_t::CUTOFF) { @@ -365,16 +388,17 @@ i_t pseudo_costs_t::reliable_variable_selection( const std::vector& edge_norms, const std::vector& fractional, const std::vector& solution, + const basis_update_mpf_t& basis_factors, + const std::vector& basic_list, + const std::vector& nonbasic_list, f_t current_obj, + f_t upper_bound, logger_t& log) { mutex.lock(); - const i_t num_fractional = fractional.size(); - std::vector pseudo_cost_up(num_fractional); - std::vector pseudo_cost_down(num_fractional); - std::vector score(num_fractional); - + i_t branch_var = fractional[0]; + f_t max_score = -1; i_t num_initialized_down; i_t num_initialized_up; f_t pseudo_cost_down_avg; @@ -392,19 +416,32 @@ i_t pseudo_costs_t::reliable_variable_selection( const i_t reliable_threshold = 1; - for (i_t k = 0; k < num_fractional; k++) { - const i_t j = fractional[k]; + for (auto j : fractional) { mutex.lock(); bool down_reliable = pseudo_cost_num_down[j] >= reliable_threshold; mutex.unlock(); + + f_t pseudo_cost_down = 0; + f_t pseudo_cost_up = 0; + if (down_reliable) { mutex.lock(); - pseudo_cost_down[k] = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; + pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; mutex.unlock(); } else { // Do trial branching on the down branch - f_t obj = trial_branching( - lp, settings, var_types, vstatus, edge_norms, j, lp.lower[j], std::floor(solution[j])); + f_t obj = trial_branching(lp, + settings, + var_types, + vstatus, + edge_norms, + basis_factors, + basic_list, + nonbasic_list, + j, + lp.lower[j], + std::floor(solution[j]), + upper_bound); if (!std::isnan(obj)) { f_t change_in_obj = obj - current_obj; f_t change_in_x = solution[j] - std::floor(solution[j]); @@ -412,7 +449,7 @@ i_t pseudo_costs_t::reliable_variable_selection( pseudo_cost_sum_down[j] += change_in_obj / change_in_x; pseudo_cost_num_down[j]++; mutex.unlock(); - pseudo_cost_down[k] = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; + pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; } } @@ -421,42 +458,46 @@ i_t pseudo_costs_t::reliable_variable_selection( mutex.unlock(); if (up_reliable) { mutex.lock(); - pseudo_cost_up[k] = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; + pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; mutex.unlock(); } else { // Do trial branching on the up branch - f_t obj = trial_branching( - lp, settings, var_types, vstatus, edge_norms, j, std::ceil(solution[j]), lp.upper[j]); + f_t obj = trial_branching(lp, + settings, + var_types, + vstatus, + edge_norms, + basis_factors, + basic_list, + nonbasic_list, + j, + std::ceil(solution[j]), + lp.upper[j], + upper_bound); + if (!std::isnan(obj)) { f_t change_in_obj = obj - current_obj; f_t change_in_x = std::ceil(solution[j]) - solution[j]; mutex.lock(); pseudo_cost_sum_up[j] += change_in_obj / change_in_x; pseudo_cost_num_up[j]++; - pseudo_cost_up[k] = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; + pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; mutex.unlock(); } } constexpr f_t eps = 1e-6; const f_t f_down = solution[j] - std::floor(solution[j]); const f_t f_up = std::ceil(solution[j]) - solution[j]; - score[k] = - std::max(f_down * pseudo_cost_down[k], eps) * std::max(f_up * pseudo_cost_up[k], eps); - } + f_t score = std::max(f_down * pseudo_cost_down, eps) * std::max(f_up * pseudo_cost_up, eps); - i_t branch_var = fractional[0]; - f_t max_score = -1; - i_t select = -1; - for (i_t k = 0; k < num_fractional; k++) { - if (score[k] > max_score) { - max_score = score[k]; - branch_var = fractional[k]; - select = k; + if (score > max_score) { + max_score = score; + branch_var = j; } } log.printf( - "pc branching on %d. Value %e. Score %e\n", branch_var, solution[branch_var], score[select]); + "pc branching on %d. Value %e. Score %e\n", branch_var, solution[branch_var], max_score); return branch_var; } diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 1569cb1c1..8367637b0 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -54,7 +55,11 @@ class pseudo_costs_t { const std::vector& edge_norms, const std::vector& fractional, const std::vector& solution, + const basis_update_mpf_t& basis_factors, + const std::vector& basic_list, + const std::vector& nonbasic_list, f_t current_obj, + f_t upper_bound, logger_t& log); f_t obj_estimate(const std::vector& fractional, From 0926349378876595cd4dac6aa3a407cf36286705 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 12 Jan 2026 12:20:53 +0100 Subject: [PATCH 66/70] replaced mutex with a vector of mutexes --- cpp/src/dual_simplex/diving_heuristics.cpp | 6 ++- cpp/src/dual_simplex/pseudo_costs.cpp | 48 +++++++++++----------- cpp/src/dual_simplex/pseudo_costs.hpp | 9 ++-- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 2d564a815..b5e58a097 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -71,7 +71,6 @@ branch_variable_t pseudocost_diving(pseudo_costs_t& pc, const std::vector& root_solution, logger_t& log) { - std::lock_guard lock(pc.mutex); i_t branch_var = -1; f_t max_score = std::numeric_limits::lowest(); rounding_direction_t round_dir = rounding_direction_t::NONE; @@ -89,12 +88,14 @@ branch_variable_t pseudocost_diving(pseudo_costs_t& pc, f_t f_down = solution[j] - std::floor(solution[j]); f_t f_up = std::ceil(solution[j]) - solution[j]; + pc.pseudo_cost_mutex[j].lock(); f_t pc_down = pc.pseudo_cost_num_down[j] != 0 ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] : pseudo_cost_down_avg; f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] : pseudo_cost_up_avg; + pc.pseudo_cost_mutex[j].unlock(); f_t score_down = std::sqrt(f_up) * (1 + pc_up) / (1 + pc_down); f_t score_up = std::sqrt(f_down) * (1 + pc_down) / (1 + pc_up); @@ -146,7 +147,6 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, const std::vector& incumbent, logger_t& log) { - std::lock_guard lock(pc.mutex); i_t branch_var = -1; f_t max_score = std::numeric_limits::lowest(); rounding_direction_t round_dir = rounding_direction_t::NONE; @@ -167,12 +167,14 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, rounding_direction_t dir = down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP; + pc.pseudo_cost_mutex[j].lock(); f_t pc_down = pc.pseudo_cost_num_down[j] != 0 ? pc.pseudo_cost_sum_down[j] / pc.pseudo_cost_num_down[j] : pseudo_cost_down_avg; f_t pc_up = pc.pseudo_cost_num_up[j] != 0 ? pc.pseudo_cost_sum_up[j] / pc.pseudo_cost_num_up[j] : pseudo_cost_up_avg; + pc.pseudo_cost_mutex[j].unlock(); f_t score1 = dir == rounding_direction_t::DOWN ? 5 * pc_down * f_down : 5 * pc_up * f_up; f_t score2 = dir == rounding_direction_t::DOWN ? pc_up * f_up : pc_down * f_down; diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 89a539485..e79284211 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -262,7 +262,7 @@ template void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective) { - std::lock_guard lock(mutex); + std::lock_guard lock(pseudo_cost_mutex[node_ptr->branch_var]); const f_t change_in_obj = leaf_objective - node_ptr->lower_bound; const f_t frac = node_ptr->branch_dir == rounding_direction_t::DOWN ? node_ptr->fractional_val - std::floor(node_ptr->fractional_val) @@ -280,7 +280,7 @@ template void pseudo_costs_t::initialized(i_t& num_initialized_down, i_t& num_initialized_up, f_t& pseudo_cost_down_avg, - f_t& pseudo_cost_up_avg) const + f_t& pseudo_cost_up_avg) { num_initialized_down = 0; num_initialized_up = 0; @@ -288,6 +288,8 @@ void pseudo_costs_t::initialized(i_t& num_initialized_down, pseudo_cost_up_avg = 0; const i_t n = pseudo_cost_sum_down.size(); for (i_t j = 0; j < n; j++) { + std::lock_guard lock(pseudo_cost_mutex[j]); + if (pseudo_cost_num_down[j] > 0) { num_initialized_down++; if (std::isfinite(pseudo_cost_sum_down[j])) { @@ -320,8 +322,6 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio const std::vector& solution, logger_t& log) { - std::lock_guard lock(mutex); - const i_t num_fractional = fractional.size(); std::vector pseudo_cost_up(num_fractional); std::vector pseudo_cost_down(num_fractional); @@ -342,6 +342,8 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio for (i_t k = 0; k < num_fractional; k++) { const i_t j = fractional[k]; + + pseudo_cost_mutex[j].lock(); if (pseudo_cost_num_down[j] != 0) { pseudo_cost_down[k] = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; } else { @@ -353,6 +355,8 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio } else { pseudo_cost_up[k] = pseudo_cost_up_avg; } + pseudo_cost_mutex[j].unlock(); + constexpr f_t eps = 1e-6; const f_t f_down = solution[j] - std::floor(solution[j]); const f_t f_up = std::ceil(solution[j]) - solution[j]; @@ -395,8 +399,6 @@ i_t pseudo_costs_t::reliable_variable_selection( f_t upper_bound, logger_t& log) { - mutex.lock(); - i_t branch_var = fractional[0]; f_t max_score = -1; i_t num_initialized_down; @@ -406,8 +408,6 @@ i_t pseudo_costs_t::reliable_variable_selection( initialized(num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - mutex.unlock(); - log.printf("PC: num initialized down %d up %d avg down %e up %e\n", num_initialized_down, num_initialized_up, @@ -417,17 +417,17 @@ i_t pseudo_costs_t::reliable_variable_selection( const i_t reliable_threshold = 1; for (auto j : fractional) { - mutex.lock(); + pseudo_cost_mutex[j].lock(); bool down_reliable = pseudo_cost_num_down[j] >= reliable_threshold; - mutex.unlock(); + pseudo_cost_mutex[j].unlock(); f_t pseudo_cost_down = 0; f_t pseudo_cost_up = 0; if (down_reliable) { - mutex.lock(); + pseudo_cost_mutex[j].lock(); pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; - mutex.unlock(); + pseudo_cost_mutex[j].unlock(); } else { // Do trial branching on the down branch f_t obj = trial_branching(lp, @@ -445,21 +445,21 @@ i_t pseudo_costs_t::reliable_variable_selection( if (!std::isnan(obj)) { f_t change_in_obj = obj - current_obj; f_t change_in_x = solution[j] - std::floor(solution[j]); - mutex.lock(); + pseudo_cost_mutex[j].lock(); pseudo_cost_sum_down[j] += change_in_obj / change_in_x; pseudo_cost_num_down[j]++; - mutex.unlock(); + pseudo_cost_mutex[j].unlock(); pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; } } - mutex.lock(); + pseudo_cost_mutex[j].lock(); bool up_reliable = pseudo_cost_num_up[j] >= reliable_threshold; - mutex.unlock(); + pseudo_cost_mutex[j].unlock(); if (up_reliable) { - mutex.lock(); + pseudo_cost_mutex[j].lock(); pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; - mutex.unlock(); + pseudo_cost_mutex[j].unlock(); } else { // Do trial branching on the up branch f_t obj = trial_branching(lp, @@ -478,11 +478,11 @@ i_t pseudo_costs_t::reliable_variable_selection( if (!std::isnan(obj)) { f_t change_in_obj = obj - current_obj; f_t change_in_x = std::ceil(solution[j]) - solution[j]; - mutex.lock(); + pseudo_cost_mutex[j].lock(); pseudo_cost_sum_up[j] += change_in_obj / change_in_x; pseudo_cost_num_up[j]++; pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; - mutex.unlock(); + pseudo_cost_mutex[j].unlock(); } } constexpr f_t eps = 1e-6; @@ -508,8 +508,6 @@ f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, f_t lower_bound, logger_t& log) { - std::lock_guard lock(mutex); - const i_t num_fractional = fractional.size(); f_t estimate = lower_bound; @@ -521,10 +519,12 @@ f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, initialized(num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); for (i_t k = 0; k < num_fractional; k++) { - const i_t j = fractional[k]; + const i_t j = fractional[k]; + f_t pseudo_cost_down = 0; f_t pseudo_cost_up = 0; + pseudo_cost_mutex[j].lock(); if (pseudo_cost_num_down[j] != 0) { pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; } else { @@ -536,6 +536,8 @@ f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, } else { pseudo_cost_up = pseudo_cost_up_avg; } + pseudo_cost_mutex[j].unlock(); + constexpr f_t eps = 1e-6; const f_t f_down = solution[j] - std::floor(solution[j]); const f_t f_up = std::ceil(solution[j]) - solution[j]; diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 8367637b0..2f2d60f79 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -25,7 +25,8 @@ class pseudo_costs_t { : pseudo_cost_sum_down(num_variables), pseudo_cost_sum_up(num_variables), pseudo_cost_num_down(num_variables), - pseudo_cost_num_up(num_variables) + pseudo_cost_num_up(num_variables), + pseudo_cost_mutex(num_variables) { } @@ -37,12 +38,13 @@ class pseudo_costs_t { pseudo_cost_sum_up.resize(num_variables); pseudo_cost_num_down.resize(num_variables); pseudo_cost_num_up.resize(num_variables); + pseudo_cost_mutex.resize(num_variables); } void initialized(i_t& num_initialized_down, i_t& num_initialized_up, f_t& pseudo_cost_down_avg, - f_t& pseudo_cost_up_avg) const; + f_t& pseudo_cost_up_avg); i_t variable_selection(const std::vector& fractional, const std::vector& solution, @@ -75,8 +77,7 @@ class pseudo_costs_t { std::vector pseudo_cost_num_down; std::vector strong_branch_down; std::vector strong_branch_up; - - omp_mutex_t mutex; + std::vector pseudo_cost_mutex; omp_atomic_t num_strong_branches_completed = 0; }; From 2c069c0de7ce450bf18597932a15e50dc93817d8 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 12 Jan 2026 16:11:01 +0100 Subject: [PATCH 67/70] restrict reliability branching to main thread --- cpp/src/dual_simplex/branch_and_bound.cpp | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index c3fecd35f..b72e0bf53 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -565,20 +565,25 @@ branch_variable_t branch_and_bound_t::variable_selection( switch (worker_data->worker_type) { case bnb_worker_type_t::EXPLORATION: - // branch_var = pc_.variable_selection(fractional, solution, log); - branch_var = pc_.reliable_variable_selection(worker_data->leaf_problem, - settings_, - var_types_, - node_ptr->vstatus, - worker_data->leaf_edge_norms, - fractional, - solution, - worker_data->basis_factors, - worker_data->basic_list, - worker_data->nonbasic_list, - node_ptr->lower_bound, - upper_bound_, - log); + + // RINS/SubMIP path + if (!enable_concurrent_lp_root_solve()) { + branch_var = pc_.variable_selection(fractional, solution, log); + } else { + branch_var = pc_.reliable_variable_selection(worker_data->leaf_problem, + settings_, + var_types_, + node_ptr->vstatus, + worker_data->leaf_edge_norms, + fractional, + solution, + worker_data->basis_factors, + worker_data->basic_list, + worker_data->nonbasic_list, + node_ptr->lower_bound, + upper_bound_, + log); + } round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); From f043ac90240fd3a7a21ed02f564b3df8f80531a1 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 13 Jan 2026 16:17:16 +0100 Subject: [PATCH 68/70] fixed unintialized pseudocost. added adaptive rule for the number of LP iterations and reliable threshold for reliability branching. --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 +- cpp/src/dual_simplex/pseudo_costs.cpp | 181 ++++++++++++---------- cpp/src/dual_simplex/pseudo_costs.hpp | 11 +- 3 files changed, 111 insertions(+), 85 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b72e0bf53..d6a95e82e 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -561,7 +561,7 @@ branch_variable_t branch_and_bound_t::variable_selection( i_t branch_var = -1; rounding_direction_t round_dir = rounding_direction_t::NONE; std::vector current_incumbent; - std::vector& solution = worker_data->leaf_solution.x; + std::vector& solution = worker_data->leaf_solution.x; switch (worker_data->worker_type) { case bnb_worker_type_t::EXPLORATION: @@ -582,6 +582,8 @@ branch_variable_t branch_and_bound_t::variable_selection( worker_data->nonbasic_list, node_ptr->lower_bound, upper_bound_, + exploration_stats_.total_lp_iters, + exploration_stats_.nodes_explored, log); } diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index e79284211..de6c3a009 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -145,7 +145,9 @@ f_t trial_branching(const lp_problem_t& original_lp, i_t branch_var, f_t branch_var_lower, f_t branch_var_upper, - f_t upper_bound) + f_t upper_bound, + i_t bnb_lp_iter_per_node, + omp_atomic_t& total_lp_iter) { lp_problem_t child_problem = original_lp; child_problem.lower[branch_var] = branch_var_lower; @@ -154,7 +156,7 @@ f_t trial_branching(const lp_problem_t& original_lp, simplex_solver_settings_t child_settings = settings; child_settings.set_log(false); f_t lp_start_time = tic(); - child_settings.iteration_limit = 200; + child_settings.iteration_limit = std::clamp(bnb_lp_iter_per_node, 10, 100); child_settings.cut_off = upper_bound + settings.dual_tol; child_settings.inside_mip = 2; child_settings.scale_columns = false; @@ -180,16 +182,20 @@ f_t trial_branching(const lp_problem_t& original_lp, solution, iter, child_edge_norms); - settings.log.debug("Trial branching on variable %d. Lo: %e Up: %e. Iter %d. Status %d. Obj %e\n", + total_lp_iter += iter; + settings.log.debug("Trial branching on variable %d. Lo: %e Up: %e. Iter %d. Status %s. Obj %e\n", branch_var, child_problem.lower[branch_var], child_problem.upper[branch_var], iter, - status, + dual::status_to_string(status).c_str(), compute_objective(child_problem, solution.x)); - if (status == dual::status_t::OPTIMAL || status == dual::status_t::ITERATION_LIMIT || - status == dual::status_t::CUTOFF) { + if (status == dual::status_t::DUAL_UNBOUNDED) { + // LP was infeasible + return std::numeric_limits::infinity(); + } else if (status == dual::status_t::OPTIMAL || status == dual::status_t::ITERATION_LIMIT || + status == dual::status_t::CUTOFF) { return compute_objective(child_problem, solution.x); } else { return std::numeric_limits::quiet_NaN(); @@ -211,8 +217,8 @@ void strong_branching(const lp_problem_t& original_lp, pseudo_costs_t& pc) { pc.resize(original_lp.num_cols); - pc.strong_branch_down.resize(fractional.size()); - pc.strong_branch_up.resize(fractional.size()); + pc.strong_branch_down.assign(fractional.size(), 0); + pc.strong_branch_up.assign(fractional.size(), 0); pc.num_strong_branches_completed = 0; settings.log.printf("Strong branching using %d threads and %ld fractional variables\n", @@ -397,6 +403,8 @@ i_t pseudo_costs_t::reliable_variable_selection( const std::vector& nonbasic_list, f_t current_obj, f_t upper_bound, + i_t bnb_lp_iter, + i_t bnb_explored_nodes, logger_t& log) { i_t branch_var = fractional[0]; @@ -414,86 +422,99 @@ i_t pseudo_costs_t::reliable_variable_selection( pseudo_cost_down_avg, pseudo_cost_up_avg); - const i_t reliable_threshold = 1; - - for (auto j : fractional) { - pseudo_cost_mutex[j].lock(); - bool down_reliable = pseudo_cost_num_down[j] >= reliable_threshold; - pseudo_cost_mutex[j].unlock(); + const i_t max_iter = 0.5 * bnb_lp_iter; + const i_t gamma = (max_iter - total_lp_iter) / (total_lp_iter + 1); + const i_t max_v = 5; + const i_t min_v = 1; + i_t reliable_threshold = 0; // std::clamp((1 - gamma) * min_v + gamma * max_v, min_v, max_v); + // reliable_threshold = total_lp_iter < max_iter ? reliable_threshold : 0; + + settings.log.debug("RB LP iterations = %d, B&B LP iterations = %d reliable_threshold = %d\n", + total_lp_iter.load(), + bnb_lp_iter, + reliable_threshold); + + std::vector pending = fractional; + std::vector conflict; + conflict.reserve(fractional.size()); + + while (!pending.empty()) { + for (auto j : pending) { + bool is_locked = pseudo_cost_mutex[j].try_lock(); + + if (!is_locked) { + conflict.push_back(j); + continue; + } - f_t pseudo_cost_down = 0; - f_t pseudo_cost_up = 0; + if (pseudo_cost_num_down[j] < reliable_threshold) { + // Do trial branching on the down branch + f_t obj = trial_branching(lp, + settings, + var_types, + vstatus, + edge_norms, + basis_factors, + basic_list, + nonbasic_list, + j, + lp.lower[j], + std::floor(solution[j]), + upper_bound, + bnb_lp_iter / bnb_explored_nodes, + total_lp_iter); + if (!std::isnan(obj)) { + f_t change_in_obj = obj - current_obj; + f_t change_in_x = solution[j] - std::floor(solution[j]); + pseudo_cost_sum_down[j] += change_in_obj / change_in_x; + pseudo_cost_num_down[j]++; + } + } - if (down_reliable) { - pseudo_cost_mutex[j].lock(); - pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; - pseudo_cost_mutex[j].unlock(); - } else { - // Do trial branching on the down branch - f_t obj = trial_branching(lp, - settings, - var_types, - vstatus, - edge_norms, - basis_factors, - basic_list, - nonbasic_list, - j, - lp.lower[j], - std::floor(solution[j]), - upper_bound); - if (!std::isnan(obj)) { - f_t change_in_obj = obj - current_obj; - f_t change_in_x = solution[j] - std::floor(solution[j]); - pseudo_cost_mutex[j].lock(); - pseudo_cost_sum_down[j] += change_in_obj / change_in_x; - pseudo_cost_num_down[j]++; - pseudo_cost_mutex[j].unlock(); - pseudo_cost_down = pseudo_cost_sum_down[j] / pseudo_cost_num_down[j]; + if (pseudo_cost_num_up[j] < reliable_threshold) { + f_t obj = trial_branching(lp, + settings, + var_types, + vstatus, + edge_norms, + basis_factors, + basic_list, + nonbasic_list, + j, + std::ceil(solution[j]), + lp.upper[j], + upper_bound, + bnb_lp_iter / bnb_explored_nodes, + total_lp_iter); + + if (!std::isnan(obj)) { + f_t change_in_obj = obj - current_obj; + f_t change_in_x = std::ceil(solution[j]) - solution[j]; + pseudo_cost_sum_up[j] += change_in_obj / change_in_x; + pseudo_cost_num_up[j]++; + } } - } - pseudo_cost_mutex[j].lock(); - bool up_reliable = pseudo_cost_num_up[j] >= reliable_threshold; - pseudo_cost_mutex[j].unlock(); - if (up_reliable) { - pseudo_cost_mutex[j].lock(); - pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; + f_t pc_up = pseudo_cost_num_up[j] > 0 ? pseudo_cost_sum_up[j] / pseudo_cost_num_up[j] + : pseudo_cost_up_avg; + f_t pc_down = pseudo_cost_sum_down[j] > 0 ? pseudo_cost_sum_down[j] / pseudo_cost_num_down[j] + : pseudo_cost_down_avg; + pseudo_cost_mutex[j].unlock(); - } else { - // Do trial branching on the up branch - f_t obj = trial_branching(lp, - settings, - var_types, - vstatus, - edge_norms, - basis_factors, - basic_list, - nonbasic_list, - j, - std::ceil(solution[j]), - lp.upper[j], - upper_bound); - - if (!std::isnan(obj)) { - f_t change_in_obj = obj - current_obj; - f_t change_in_x = std::ceil(solution[j]) - solution[j]; - pseudo_cost_mutex[j].lock(); - pseudo_cost_sum_up[j] += change_in_obj / change_in_x; - pseudo_cost_num_up[j]++; - pseudo_cost_up = pseudo_cost_sum_up[j] / pseudo_cost_num_up[j]; - pseudo_cost_mutex[j].unlock(); + + constexpr f_t eps = 1e-6; + const f_t f_down = solution[j] - std::floor(solution[j]); + const f_t f_up = std::ceil(solution[j]) - solution[j]; + f_t score = std::max(f_down * pc_down, eps) * std::max(f_up * pc_up, eps); + + if (score > max_score) { + max_score = score; + branch_var = j; } } - constexpr f_t eps = 1e-6; - const f_t f_down = solution[j] - std::floor(solution[j]); - const f_t f_up = std::ceil(solution[j]) - solution[j]; - f_t score = std::max(f_down * pseudo_cost_down, eps) * std::max(f_up * pseudo_cost_up, eps); - if (score > max_score) { - max_score = score; - branch_var = j; - } + std::swap(pending, conflict); + conflict.clear(); } log.printf( diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 2f2d60f79..8e079d0da 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -34,10 +34,10 @@ class pseudo_costs_t { void resize(i_t num_variables) { - pseudo_cost_sum_down.resize(num_variables); - pseudo_cost_sum_up.resize(num_variables); - pseudo_cost_num_down.resize(num_variables); - pseudo_cost_num_up.resize(num_variables); + pseudo_cost_sum_down.assign(num_variables, 0); + pseudo_cost_sum_up.assign(num_variables, 0); + pseudo_cost_num_down.assign(num_variables, 0); + pseudo_cost_num_up.assign(num_variables, 0); pseudo_cost_mutex.resize(num_variables); } @@ -62,6 +62,8 @@ class pseudo_costs_t { const std::vector& nonbasic_list, f_t current_obj, f_t upper_bound, + i_t bnb_lp_iter, + i_t bnb_explored_nodes, logger_t& log); f_t obj_estimate(const std::vector& fractional, @@ -79,6 +81,7 @@ class pseudo_costs_t { std::vector strong_branch_up; std::vector pseudo_cost_mutex; omp_atomic_t num_strong_branches_completed = 0; + omp_atomic_t total_lp_iter = 0; }; template From 24a3838cdcdf929cb757101573763a1141549f0b Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 13 Jan 2026 16:42:13 +0100 Subject: [PATCH 69/70] re-enable reliability branching --- cpp/src/dual_simplex/pseudo_costs.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index de6c3a009..a2c1ef029 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -426,8 +426,8 @@ i_t pseudo_costs_t::reliable_variable_selection( const i_t gamma = (max_iter - total_lp_iter) / (total_lp_iter + 1); const i_t max_v = 5; const i_t min_v = 1; - i_t reliable_threshold = 0; // std::clamp((1 - gamma) * min_v + gamma * max_v, min_v, max_v); - // reliable_threshold = total_lp_iter < max_iter ? reliable_threshold : 0; + i_t reliable_threshold = std::clamp((1 - gamma) * min_v + gamma * max_v, min_v, max_v); + reliable_threshold = total_lp_iter < max_iter ? reliable_threshold : 0; settings.log.debug("RB LP iterations = %d, B&B LP iterations = %d reliable_threshold = %d\n", total_lp_iter.load(), From ccca7febc9f298f17ad122d96756d11b8ea352f6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 13 Jan 2026 20:00:30 +0100 Subject: [PATCH 70/70] setting reliable_threshold to 1 --- cpp/src/dual_simplex/pseudo_costs.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index a2c1ef029..e30471f9a 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -422,12 +422,13 @@ i_t pseudo_costs_t::reliable_variable_selection( pseudo_cost_down_avg, pseudo_cost_up_avg); - const i_t max_iter = 0.5 * bnb_lp_iter; - const i_t gamma = (max_iter - total_lp_iter) / (total_lp_iter + 1); - const i_t max_v = 5; - const i_t min_v = 1; - i_t reliable_threshold = std::clamp((1 - gamma) * min_v + gamma * max_v, min_v, max_v); - reliable_threshold = total_lp_iter < max_iter ? reliable_threshold : 0; + // const i_t max_iter = 0.5 * bnb_lp_iter; + // const i_t gamma = (max_iter - total_lp_iter) / (total_lp_iter + 1); + // const i_t max_v = 5; + // const i_t min_v = 1; + // i_t reliable_threshold = std::clamp((1 - gamma) * min_v + gamma * max_v, min_v, max_v); + // reliable_threshold = total_lp_iter < max_iter ? reliable_threshold : 0; + i_t reliable_threshold = 1; settings.log.debug("RB LP iterations = %d, B&B LP iterations = %d reliable_threshold = %d\n", total_lp_iter.load(),