From c9081a6e71ea5e8e1ccee4cc79b85b35ce146c46 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 3 Dec 2025 16:27:16 +0100 Subject: [PATCH 01/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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 c54033c1ea8c64acea029384a80c7f05114bf19f Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 8 Jan 2026 18:15:34 +0100 Subject: [PATCH 34/53] 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 35/53] 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 36/53] 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 37/53] 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 38/53] 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 39/53] 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 40/53] 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 0f6285ceccc6ef52bb76c9553217a8780b7ab3eb Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 9 Jan 2026 16:51:11 +0100 Subject: [PATCH 41/53] revert type change --- 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 28220e622..35ed51cf5 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1484,7 +1484,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; From 2d10acefd2d5e87e7d2fe2f7842b168a9040d4c2 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 12 Jan 2026 16:26:27 +0100 Subject: [PATCH 42/53] fix race condition on guided diving --- cpp/src/dual_simplex/branch_and_bound.cpp | 60 ++++++++++--------- cpp/src/dual_simplex/branch_and_bound.hpp | 6 +- cpp/src/dual_simplex/diving_heuristics.cpp | 1 + cpp/src/dual_simplex/node_queue.hpp | 6 +- .../dual_simplex/simplex_solver_settings.hpp | 2 +- 5 files changed, 41 insertions(+), 34 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 35ed51cf5..faab769cf 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -251,7 +251,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); for (i_t i = 0; i < local_lower_bounds_.size(); ++i) { @@ -584,11 +584,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: @@ -612,7 +614,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); @@ -793,8 +798,8 @@ 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, round_dir] = variable_selection( - node_ptr, leaf_fractional, leaf_solution.x, thread_type, lp_settings.log); + auto [branch_var, round_dir] = + variable_selection(node_ptr, leaf_fractional, leaf_solution.x, thread_type); assert(leaf_vstatus.size() == leaf_problem.num_cols); assert(branch_var >= 0); @@ -931,8 +936,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 - node_queue.push(node->get_down_child()); - node_queue.push(node->get_up_child()); + node_queue_.push(node->get_down_child()); + node_queue_.push(node->get_up_child()); } } } @@ -940,7 +945,6 @@ 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, @@ -972,8 +976,8 @@ 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); + 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; @@ -997,15 +1001,15 @@ void branch_and_bound_t::plunge_from(i_t task_id, if (now > 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, + search_tree_, leaf_problem, basis_factors, basic_list, @@ -1036,7 +1040,7 @@ void branch_and_bound_t::plunge_from(i_t task_id, 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; @@ -1072,9 +1076,9 @@ 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 || node_queue.best_first_queue_size() > 0)) { + (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_); + 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) { @@ -1090,7 +1094,6 @@ 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, @@ -1131,10 +1134,12 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, logger_t log; log.log = false; + const i_t node_limit = settings_.diving_settings.node_limit; + const i_t backtrack = settings_.diving_settings.backtrack; bool recompute_bounds_and_basis = true; - search_tree_t subtree(std::move(start_node)); + search_tree_t dive_tree(std::move(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; @@ -1154,10 +1159,10 @@ 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, + dive_tree, leaf_problem, basis_factors, basic_list, @@ -1189,8 +1194,7 @@ 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(); } } @@ -1214,7 +1218,7 @@ void branch_and_bound_t::diving_thread(bnb_worker_type_t diving_type) bool reset_starting_bounds = true; while (solver_status_ == mip_exploration_status_t::RUNNING && - (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { + (active_subtrees_ > 0 || node_queue_.best_first_queue_size() > 0)) { if (reset_starting_bounds) { start_lower = original_lp_.lower; start_upper = original_lp_.upper; @@ -1223,7 +1227,7 @@ void branch_and_bound_t::diving_thread(bnb_worker_type_t diving_type) } std::optional> start_node = - node_queue.pop_diving(start_lower, start_upper, node_presolver.bounds_changed); + node_queue_.pop_diving(start_lower, start_upper, node_presolver.bounds_changed); if (start_node.has_value()) { reset_starting_bounds = true; @@ -1547,8 +1551,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - f_t 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 dc838bb80..84d9ea266 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -180,7 +180,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_; @@ -220,7 +220,6 @@ class branch_and_bound_t { // 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, @@ -265,8 +264,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 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 c3b0e9336..2a4933c4c 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); } 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 01b791d407b66d015951f6aeb431b3d3a41ed8e2 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 14 Jan 2026 14:39:38 +0100 Subject: [PATCH 43/53] addressing reviewer's feedback --- cpp/src/dual_simplex/branch_and_bound.cpp | 29 +++++++++++++++++------ cpp/src/dual_simplex/branch_and_bound.hpp | 7 ++++-- cpp/src/dual_simplex/node_queue.hpp | 27 +++++++-------------- cpp/src/dual_simplex/pseudo_costs.cpp | 18 +++++++------- cpp/src/dual_simplex/pseudo_costs.hpp | 8 +++---- 5 files changed, 47 insertions(+), 42 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d3dabbacb..22c2f617e 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1042,8 +1042,11 @@ 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 || node_queue.best_first_queue_size() > 0)) { + node_queue.lock(); // 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_); + std::optional*> start_node = node_queue.pop_best_first(); + if (start_node.has_value()) { active_subtrees_++; }; + node_queue.unlock(); if (start_node.has_value()) { mip_node_t* node = start_node.value(); @@ -1106,7 +1109,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; + constexpr i_t diving_node_limit = 500; + constexpr i_t diving_backtrack = 5; while (solver_status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { @@ -1117,14 +1121,23 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A reset_starting_bounds = false; } - std::optional> start_node = - node_queue.pop_diving(start_lower, start_upper, node_presolver.bounds_changed); + node_queue.lock(); + std::optional*> node_ptr = node_queue.pop_diving(); + std::optional> start_node = std::nullopt; + + if (node_ptr.has_value()) { + node_ptr.value()->get_variable_bounds( + start_lower, start_upper, node_presolver.bounds_changed); + start_node = node_ptr.value()->detach_copy(); + } + node_queue.unlock(); if (start_node.has_value()) { reset_starting_bounds = true; + if (get_upper_bound() < start_node->lower_bound) { continue; } bool is_feasible = node_presolver.bounds_strengthening(start_lower, start_upper, settings_); - if (get_upper_bound() < start_node->lower_bound || !is_feasible) { continue; } + if (!is_feasible) { continue; } bool recompute_bounds_and_basis = true; search_tree_t subtree(std::move(start_node.value())); @@ -1149,7 +1162,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 > node_limit) { break; } + if (dive_stats.nodes_explored > diving_node_limit) { break; } node_solve_info_t status = solve_node(node_ptr, subtree, @@ -1184,7 +1197,9 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A } } - if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > 5) { + // Remove nodes that we no longer can backtrack to (i.e., from the current node, we can only + // backtrack to a node that is has a depth of at most 5 levels lower than the current node). + if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > diving_backtrack) { stack.pop_back(); } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 36b3b5f69..fb6d52627 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -211,7 +211,9 @@ class branch_and_bound_t { const csr_matrix_t& Arow, i_t initial_heap_size); - // Perform a plunge in the subtree determined by the `start_node`. + // We use best-first to pick the `start_node` and then perform a depth-first search + // from this node (i.e., a plunge). It can only backtrack to a sibling node. + // Unexplored nodes in the subtree are inserted back into the global heap. void plunge_from(i_t task_id, mip_node_t* start_node, search_tree_t& search_tree, @@ -227,7 +229,8 @@ 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`. + // Perform a deep dive in the subtree determined by the `start_node` in order + // to find integer feasible solutions. void dive_from(mip_node_t& start_node, const std::vector& start_lower, const std::vector& start_upper, diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 804273697..3f7ddb325 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -113,16 +113,10 @@ class node_queue_t { // 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; } @@ -131,26 +125,21 @@ class node_queue_t { // 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); - 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(); - } + if (auto node_ptr = entry.value()->node; node_ptr != nullptr) { return node_ptr; } } } - return std::nullopt; } + void lock() { mutex.lock(); } + + void unlock() { mutex.unlock(); } + i_t diving_queue_size() { std::lock_guard lock(mutex); diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index f3cbb4447..3187cc896 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -271,11 +271,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.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); + 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); for (i_t k = 0; k < num_fractional; k++) { const i_t j = fractional[k]; @@ -308,10 +308,8 @@ i_t pseudo_costs_t::variable_selection(const std::vector& fractio } } - log.debug("Pseudocost branching on %d. Value %e. Score %e.\n", - branch_var, - solution[branch_var], - score[select]); + log.printf( + "pc branching on %d. Value %e. Score %e\n", branch_var, solution[branch_var], score[select]); return branch_var; } @@ -357,7 +355,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); + log.printf("pseudocost estimate = %e\n", estimate); return estimate; } diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 49a810506..b9b467d04 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -43,15 +43,15 @@ 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); - f_t 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); + void update_pseudo_costs_from_strong_branching(const std::vector& fractional, const std::vector& root_soln); std::vector pseudo_cost_sum_up; From f88f3714bbc274fd2fe6668d89321dc132c65137 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 14 Jan 2026 14:41:47 +0100 Subject: [PATCH 44/53] fix node queue naming --- cpp/src/dual_simplex/branch_and_bound.cpp | 28 +++++++++++------------ cpp/src/dual_simplex/branch_and_bound.hpp | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 22c2f617e..1e767c429 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -245,7 +245,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); for (i_t i = 0; i < local_lower_bounds_.size(); ++i) { @@ -883,8 +883,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 - node_queue.push(node->get_down_child()); - node_queue.push(node->get_up_child()); + node_queue_.push(node->get_down_child()); + node_queue_.push(node->get_up_child()); } } } @@ -1003,7 +1003,7 @@ void branch_and_bound_t::plunge_from(i_t task_id, 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; @@ -1041,12 +1041,12 @@ 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 || node_queue.best_first_queue_size() > 0)) { - node_queue.lock(); + (active_subtrees_ > 0 || node_queue_.best_first_queue_size() > 0)) { + node_queue_.lock(); // 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()) { active_subtrees_++; }; - node_queue.unlock(); + node_queue_.unlock(); if (start_node.has_value()) { mip_node_t* node = start_node.value(); @@ -1113,7 +1113,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A constexpr i_t diving_backtrack = 5; while (solver_status_ == mip_exploration_status_t::RUNNING && - (active_subtrees_ > 0 || node_queue.best_first_queue_size() > 0)) { + (active_subtrees_ > 0 || node_queue_.best_first_queue_size() > 0)) { if (reset_starting_bounds) { start_lower = original_lp_.lower; start_upper = original_lp_.upper; @@ -1121,8 +1121,8 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A reset_starting_bounds = false; } - node_queue.lock(); - std::optional*> node_ptr = node_queue.pop_diving(); + node_queue_.lock(); + std::optional*> node_ptr = node_queue_.pop_diving(); std::optional> start_node = std::nullopt; if (node_ptr.has_value()) { @@ -1130,7 +1130,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A start_lower, start_upper, node_presolver.bounds_changed); start_node = node_ptr.value()->detach_copy(); } - node_queue.unlock(); + node_queue_.unlock(); if (start_node.has_value()) { reset_starting_bounds = true; @@ -1487,8 +1487,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - f_t 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 fb6d52627..f20c19181 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -174,7 +174,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_; From 2df9f732afda5ea2764b39ee1f332b2d8e812326 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 14 Jan 2026 16:56:40 +0100 Subject: [PATCH 45/53] fixed incorrect calculation of variable locks. addressing reviewer's feedback. --- cpp/src/dual_simplex/branch_and_bound.cpp | 108 +++++++++--------- cpp/src/dual_simplex/branch_and_bound.hpp | 10 +- cpp/src/dual_simplex/diving_heuristics.cpp | 81 +++++++------ cpp/src/dual_simplex/diving_heuristics.hpp | 9 ++ .../dual_simplex/simplex_solver_settings.hpp | 8 +- cpp/src/mip/diversity/lns/rins.cu | 12 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 8 +- .../rounding/simple_rounding_kernels.cuh | 3 +- 8 files changed, 135 insertions(+), 104 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index a403be0da..8c00ab057 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -194,15 +194,15 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) } } -inline const char* feasible_solution_symbol(bnb_worker_type_t type) +inline char feasible_solution_symbol(bnb_worker_type_t type) { switch (type) { - 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 "; + case bnb_worker_type_t::BEST_FIRST: 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'; } } @@ -283,10 +283,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) } template -void branch_and_bound_t::report(std::string symbol, - f_t obj, - f_t lower_bound, - i_t node_depth) +void branch_and_bound_t::report(char 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; @@ -294,8 +291,8 @@ void branch_and_bound_t::report(std::string symbol, 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(), + settings_.log.printf("%c %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + symbol, nodes_explored, nodes_unexplored, user_obj, @@ -538,7 +535,7 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, { bool send_solution = false; - settings_.log.debug("%s found a feasible solution with obj=%.10e.\n", + settings_.log.debug("%c found a feasible solution with obj=%.10e.\n", feasible_solution_symbol(thread_type), compute_user_objective(original_lp_, leaf_objective)); @@ -592,20 +589,24 @@ branch_variable_t branch_and_bound_t::variable_selection( rounding_direction_t round_dir = rounding_direction_t::NONE; std::vector current_incumbent; + // If there is no incumbent, use pseudocost diving instead of guided diving + if (get_upper_bound() == inf && type == bnb_worker_type_t::GUIDED_DIVING) { + if (!settings_.diving_settings.with_pseudocost_diving) { + type = bnb_worker_type_t::PSEUDOCOST_DIVING; + } else { + type = bnb_worker_type_t::COEFFICIENT_DIVING; + } + } + switch (type) { - case bnb_worker_type_t::EXPLORATION: + case bnb_worker_type_t::BEST_FIRST: 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 = - pc_.obj_estimate(fractional, solution, node_ptr->lower_bound, log); return {branch_var, round_dir}; case bnb_worker_type_t::COEFFICIENT_DIVING: - return coefficient_diving(original_lp_, fractional, solution, log); + return coefficient_diving( + original_lp_, fractional, solution, var_up_locks_, var_down_locks_, log); case bnb_worker_type_t::LINE_SEARCH_DIVING: return line_search_diving(fractional, solution, root_relax_soln_.x, log); @@ -644,15 +645,6 @@ node_solve_info_t branch_and_bound_t::solve_node( 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_worker_type_t::COEFFICIENT_DIVING; - } else { - thread_type = bnb_worker_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); @@ -664,7 +656,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_worker_type_t::EXPLORATION) { + if (thread_type != bnb_worker_type_t::BEST_FIRST) { 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; @@ -755,6 +747,10 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_settings.log.printf("\nLP status: %d\n\n", lp_status); #endif + ++stats.nodes_since_last_log; + ++stats.nodes_explored; + --stats.nodes_unexplored; + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { // Node was infeasible. Do not branch node_ptr->lower_bound = inf; @@ -781,7 +777,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_worker_type_t::EXPLORATION) { + if (thread_type == bnb_worker_type_t::BEST_FIRST) { if (settings_.node_processed_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, leaf_solution.x, original_x); @@ -805,6 +801,16 @@ node_solve_info_t branch_and_bound_t::solve_node( assert(branch_var >= 0); assert(round_dir != rounding_direction_t::NONE); + // 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). + if (thread_type == bnb_worker_type_t::BEST_FIRST) { + logger_t pc_log; + pc_log.log = false; + node_ptr->objective_estimate = + pc_.obj_estimate(leaf_fractional, leaf_solution.x, node_ptr->lower_bound, pc_log); + } + 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); @@ -828,7 +834,7 @@ node_solve_info_t branch_and_bound_t::solve_node( return node_solve_info_t::ITERATION_LIMIT; } else { - if (thread_type == bnb_worker_type_t::EXPLORATION) { + if (thread_type == bnb_worker_type_t::BEST_FIRST) { 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 " @@ -879,7 +885,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod bool should_report = should_report_.exchange(false); if (should_report) { - report(" ", upper_bound, root_objective_, node->depth); + report(' ', upper_bound, root_objective_, node->depth); exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); should_report_ = true; @@ -908,17 +914,13 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod basic_list, nonbasic_list, node_presolver, - bnb_worker_type_t::EXPLORATION, + bnb_worker_type_t::BEST_FIRST, true, 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; @@ -993,7 +995,7 @@ void branch_and_bound_t::plunge_from(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) { - report(" ", upper_bound, get_lower_bound(), node_ptr->depth); + report(' ', upper_bound, get_lower_bound(), node_ptr->depth); exploration_stats_.last_log = tic(); exploration_stats_.nodes_since_last_log = 0; } @@ -1015,7 +1017,7 @@ void branch_and_bound_t::plunge_from(i_t task_id, basic_list, nonbasic_list, node_presolver, - bnb_worker_type_t::EXPLORATION, + bnb_worker_type_t::BEST_FIRST, recompute_bounds_and_basis, original_lp_.lower, original_lp_.upper, @@ -1024,10 +1026,6 @@ void branch_and_bound_t::plunge_from(i_t task_id, recompute_bounds_and_basis = !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; @@ -1148,7 +1146,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, dive_stats.total_lp_iters = 0; dive_stats.total_lp_solve_time = 0; dive_stats.nodes_explored = 0; - dive_stats.nodes_unexplored = 0; + dive_stats.nodes_unexplored = 1; while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { mip_node_t* node_ptr = stack.front(); @@ -1177,7 +1175,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, start_upper, dive_stats, log); - dive_stats.nodes_explored++; + recompute_bounds_and_basis = !has_children(status); if (status == node_solve_info_t::TIME_LIMIT) { @@ -1188,6 +1186,8 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, break; } else if (has_children(status)) { + dive_stats.nodes_unexplored += 2; + 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()); @@ -1196,6 +1196,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, stack.push_front(node_ptr->get_down_child()); } } + // Remove nodes that we no longer can backtrack to (i.e., from the current node, we can only // backtrack to a node that is has a depth of at most 5 levels lower than the current node). if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > diving_backtrack) { @@ -1349,20 +1350,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::vector diving_strategies; diving_strategies.reserve(4); - if (!settings_.diving_settings.disable_pseudocost_diving) { + if (settings_.diving_settings.with_pseudocost_diving) { diving_strategies.push_back(bnb_worker_type_t::PSEUDOCOST_DIVING); } - if (!settings_.diving_settings.disable_line_search_diving) { + if (settings_.diving_settings.with_line_search_diving) { diving_strategies.push_back(bnb_worker_type_t::LINE_SEARCH_DIVING); } - if (!settings_.diving_settings.disable_guided_diving) { + if (settings_.diving_settings.with_guided_diving) { diving_strategies.push_back(bnb_worker_type_t::GUIDED_DIVING); } - if (!settings_.diving_settings.disable_coefficient_diving) { + if (settings_.diving_settings.with_coefficient_diving) { diving_strategies.push_back(bnb_worker_type_t::COEFFICIENT_DIVING); + calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); } if (diving_strategies.empty()) { @@ -1517,7 +1519,7 @@ 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_explored = 0; 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 4efc62fd4..4b163a7eb 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -59,7 +59,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_worker_type_t { - EXPLORATION = 0, // Best-First + Plunging. + BEST_FIRST = 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. @@ -146,6 +146,12 @@ class branch_and_bound_t { std::vector new_slacks_; std::vector var_types_; + // Variable locks (see definition 3.3 from T. Achterberg, “Constraint Integer Programming,” + // PhD, Technischen Universität Berlin, Berlin, 2007. doi: 10.14279/depositonce-1634). + // Here we assume that the constraints are in the form `Ax = b, l <= x <= u`. + std::vector var_up_locks_; + std::vector var_down_locks_; + // Local lower bounds for each thread std::vector> local_lower_bounds_; @@ -198,7 +204,7 @@ class branch_and_bound_t { 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); + void report(char 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); diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index 2d564a815..c0f7b30d2 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -84,7 +84,7 @@ branch_variable_t pseudocost_diving(pseudo_costs_t& pc, pc.initialized( num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - for (auto j : fractional) { + for (i_t 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]; @@ -159,7 +159,7 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, pc.initialized( num_initialized_down, num_initialized_up, pseudo_cost_down_avg, pseudo_cost_up_avg); - for (auto j : fractional) { + for (i_t 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])); @@ -198,39 +198,41 @@ 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) +void calculate_variable_locks(const lp_problem_t& lp_problem, + std::vector& up_locks, + std::vector& down_locks) { - 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; + up_locks.resize(lp_problem.num_cols); + down_locks.resize(lp_problem.num_cols); + + for (i_t j = 0; j < lp_problem.num_cols; ++j) { + i_t up_lock = 0; + i_t down_lock = 0; + i_t start = lp_problem.A.col_start[j]; + i_t end = lp_problem.A.col_start[j + 1]; + constexpr f_t eps = 1E-6; + + up_locks[j] = 0; + down_locks[j] = 0; + + for (i_t p = start; p < end; ++p) { + f_t val = lp_problem.A.x[p]; + i_t i = lp_problem.A.i[p]; + + if (std::abs(val) > eps) { + up_locks[j]++; + down_locks[j]++; + } } } - - 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, + const std::vector& up_locks, + const std::vector& down_locks, logger_t& log) { i_t branch_var = -1; @@ -238,14 +240,19 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl 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); - i_t locks = std::min(up_lock, down_lock); - - if (min_locks > locks) { - min_locks = locks; + for (i_t j : fractional) { + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + i_t up_lock = up_locks[j]; + i_t down_lock = down_locks[j]; + f_t upper = lp_problem.upper[j]; + f_t lower = lp_problem.lower[j]; + if (std::isfinite(upper)) { up_lock++; } + if (std::isfinite(lower)) { down_lock++; } + i_t alpha = std::min(up_lock, down_lock); + + if (min_locks > alpha) { + min_locks = alpha; branch_var = j; if (up_lock < down_lock) { @@ -291,9 +298,15 @@ template branch_variable_t guided_diving(pseudo_costs_t& pc, const std::vector& incumbent, logger_t& log); +template void calculate_variable_locks(const lp_problem_t& lp_problem, + std::vector& up_locks, + std::vector& down_locks); + template branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, const std::vector& fractional, const std::vector& solution, + const std::vector& up_locks, + const std::vector& down_locks, logger_t& log); #endif diff --git a/cpp/src/dual_simplex/diving_heuristics.hpp b/cpp/src/dual_simplex/diving_heuristics.hpp index 1f44fee31..3c6d77c04 100644 --- a/cpp/src/dual_simplex/diving_heuristics.hpp +++ b/cpp/src/dual_simplex/diving_heuristics.hpp @@ -40,10 +40,19 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, const std::vector& incumbent, logger_t& log); +// Calculate the variable locks assuming that the constraints +// has the following format: `Ax = b`. +template +void calculate_variable_locks(const lp_problem_t& lp_problem, + std::vector& up_locks, + std::vector& down_locks); + template branch_variable_t coefficient_diving(const lp_problem_t& lp_problem, const std::vector& fractional, const std::vector& solution, + const std::vector& up_locks, + const std::vector& down_locks, logger_t& log); } // 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 c46eda085..0c616909c 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -24,10 +24,10 @@ template 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; + bool with_line_search_diving = true; + bool with_pseudocost_diving = true; + bool with_guided_diving = true; + bool with_coefficient_diving = true; i_t min_node_depth = 10; i_t node_limit = 500; diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 7c2df3bee..77a25684d 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -262,12 +262,12 @@ 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.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; - 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.num_diving_workers = 1; + branch_and_bound_settings.diving_settings.with_line_search_diving = false; + branch_and_bound_settings.diving_settings.with_coefficient_diving = false; + branch_and_bound_settings.diving_settings.with_pseudocost_diving = false; + branch_and_bound_settings.log.log = false; + branch_and_bound_settings.log.log_prefix = "[RINS] "; branch_and_bound_settings.solution_callback = [&rins_solution_queue](std::vector& solution, f_t objective) { rins_solution_queue.push_back(solution); diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index d46c6b31a..7d96c9bcb 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -107,10 +107,10 @@ 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.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; - branch_and_bound_settings.diving_settings.disable_pseudocost_diving = true; + branch_and_bound_settings.diving_settings.num_diving_workers = 1; + branch_and_bound_settings.diving_settings.with_line_search_diving = false; + branch_and_bound_settings.diving_settings.with_coefficient_diving = false; + branch_and_bound_settings.diving_settings.with_pseudocost_diving = false; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); diff --git a/cpp/src/mip/local_search/rounding/simple_rounding_kernels.cuh b/cpp/src/mip/local_search/rounding/simple_rounding_kernels.cuh index 2906e648f..5cd219ec3 100644 --- a/cpp/src/mip/local_search/rounding/simple_rounding_kernels.cuh +++ b/cpp/src/mip/local_search/rounding/simple_rounding_kernels.cuh @@ -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 */ @@ -32,6 +32,7 @@ __global__ void simple_rounding_kernel(typename solution_t::view_t sol auto cstr_idx = solution.problem.reverse_constraints[i]; auto cstr_coeff = solution.problem.reverse_coefficients[i]; + // Here, we are storing the constraints in the following format: u <= Ax <= l // boxed constraint. can't be rounded safely if (std::isfinite(solution.problem.constraint_lower_bounds[cstr_idx]) && std::isfinite(solution.problem.constraint_upper_bounds[cstr_idx])) { From f01fc5f736f2e0bfa8c2654459f126c553edab2e Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 14 Jan 2026 17:37:40 +0100 Subject: [PATCH 46/53] fixed logic inversion --- 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 8c00ab057..e060f5f0c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -591,7 +591,7 @@ branch_variable_t branch_and_bound_t::variable_selection( // If there is no incumbent, use pseudocost diving instead of guided diving if (get_upper_bound() == inf && type == bnb_worker_type_t::GUIDED_DIVING) { - if (!settings_.diving_settings.with_pseudocost_diving) { + if (settings_.diving_settings.with_pseudocost_diving) { type = bnb_worker_type_t::PSEUDOCOST_DIVING; } else { type = bnb_worker_type_t::COEFFICIENT_DIVING; From 110ebdaa5c1cc37bba2132875b0b6dc689aa31b6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 15 Jan 2026 10:03:28 +0100 Subject: [PATCH 47/53] fix incorrect fallback for guided diving --- cpp/src/dual_simplex/branch_and_bound.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index e060f5f0c..4c4089601 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -591,11 +591,7 @@ branch_variable_t branch_and_bound_t::variable_selection( // If there is no incumbent, use pseudocost diving instead of guided diving if (get_upper_bound() == inf && type == bnb_worker_type_t::GUIDED_DIVING) { - if (settings_.diving_settings.with_pseudocost_diving) { - type = bnb_worker_type_t::PSEUDOCOST_DIVING; - } else { - type = bnb_worker_type_t::COEFFICIENT_DIVING; - } + type = bnb_worker_type_t::PSEUDOCOST_DIVING; } switch (type) { From e1ee8efe93279684b1b07e9a29c110396378c75f Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 16 Jan 2026 11:32:04 +0100 Subject: [PATCH 48/53] fixing minor bugs. renaming variables --- cpp/src/dual_simplex/branch_and_bound.cpp | 28 +++++++++++++------ cpp/src/dual_simplex/diving_heuristics.cpp | 17 ++++------- cpp/src/dual_simplex/node_queue.hpp | 10 ------- cpp/src/dual_simplex/pseudo_costs.cpp | 3 +- .../dual_simplex/simplex_solver_settings.hpp | 11 ++++---- cpp/src/mip/diversity/lns/rins.cu | 12 ++++---- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 8 +++--- 7 files changed, 42 insertions(+), 47 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 4c4089601..8cd4fb326 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -654,7 +654,7 @@ node_solve_info_t branch_and_bound_t::solve_node( if (thread_type != bnb_worker_type_t::BEST_FIRST) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; - f_t max_iter = settings_.diving_settings.iteration_limit_factor * bnb_lp_iters; + i_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; } } @@ -1071,6 +1071,11 @@ 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 || node_queue_.best_first_queue_size() > 0)) { + // 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. node_queue_.lock(); // 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(); @@ -1131,9 +1136,9 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, logger_t log; log.log = false; - const i_t diving_node_limit = settings_.diving_settings.node_limit; - const i_t diving_backtrack = settings_.diving_settings.backtrack; - bool recompute_bounds_and_basis = true; + const i_t diving_node_limit = settings_.diving_settings.node_limit; + const i_t diving_backtrack_limit = settings_.diving_settings.backtrack_limit; + bool recompute_bounds_and_basis = true; search_tree_t dive_tree(std::move(start_node)); std::deque*> stack; stack.push_front(&dive_tree.root); @@ -1195,7 +1200,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, // Remove nodes that we no longer can backtrack to (i.e., from the current node, we can only // backtrack to a node that is has a depth of at most 5 levels lower than the current node). - if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > diving_backtrack) { + if (stack.size() > 1 && stack.front()->depth - stack.back()->depth > diving_backtrack_limit) { stack.pop_back(); } } @@ -1227,6 +1232,11 @@ void branch_and_bound_t::diving_thread(bnb_worker_type_t diving_type) reset_starting_bounds = false; } + // In the current implementation, multiple threads can pop the nodes + // from the queue, so we need to initialize 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. node_queue_.lock(); std::optional*> node_ptr = node_queue_.pop_diving(); std::optional> start_node = std::nullopt; @@ -1346,19 +1356,19 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::vector diving_strategies; diving_strategies.reserve(4); - if (settings_.diving_settings.with_pseudocost_diving) { + if (settings_.diving_settings.pseudocost_diving != 0) { diving_strategies.push_back(bnb_worker_type_t::PSEUDOCOST_DIVING); } - if (settings_.diving_settings.with_line_search_diving) { + if (settings_.diving_settings.line_search_diving != 0) { diving_strategies.push_back(bnb_worker_type_t::LINE_SEARCH_DIVING); } - if (settings_.diving_settings.with_guided_diving) { + if (settings_.diving_settings.guided_diving != 0) { diving_strategies.push_back(bnb_worker_type_t::GUIDED_DIVING); } - if (settings_.diving_settings.with_coefficient_diving) { + if (settings_.diving_settings.coefficient_diving != 0) { diving_strategies.push_back(bnb_worker_type_t::COEFFICIENT_DIVING); calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); } diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index c0f7b30d2..a1c9ad086 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -21,7 +21,7 @@ branch_variable_t line_search_diving(const std::vector& fractional, f_t min_score = std::numeric_limits::max(); rounding_direction_t round_dir = rounding_direction_t::NONE; - for (auto j : fractional) { + for (i_t j : fractional) { f_t score = inf; rounding_direction_t dir = rounding_direction_t::NONE; @@ -202,18 +202,13 @@ void calculate_variable_locks(const lp_problem_t& lp_problem, std::vector& up_locks, std::vector& down_locks) { - up_locks.resize(lp_problem.num_cols); - down_locks.resize(lp_problem.num_cols); + constexpr f_t eps = 1E-6; + up_locks.assign(lp_problem.num_cols, 0); + down_locks.assign(lp_problem.num_cols, 0); for (i_t j = 0; j < lp_problem.num_cols; ++j) { - i_t up_lock = 0; - i_t down_lock = 0; - i_t start = lp_problem.A.col_start[j]; - i_t end = lp_problem.A.col_start[j + 1]; - constexpr f_t eps = 1E-6; - - up_locks[j] = 0; - down_locks[j] = 0; + i_t start = lp_problem.A.col_start[j]; + i_t end = lp_problem.A.col_start[j + 1]; for (i_t p = start; p < end; ++p) { f_t val = lp_problem.A.x[p]; diff --git a/cpp/src/dual_simplex/node_queue.hpp b/cpp/src/dual_simplex/node_queue.hpp index 80715285e..28072795a 100644 --- a/cpp/src/dual_simplex/node_queue.hpp +++ b/cpp/src/dual_simplex/node_queue.hpp @@ -112,11 +112,6 @@ 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() { auto entry = best_first_heap.pop(); @@ -124,11 +119,6 @@ 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() { while (!diving_heap.empty()) { diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 3187cc896..f6ff72bcc 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -351,8 +351,7 @@ f_t pseudo_costs_t::obj_estimate(const std::vector& fractional, 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)); + estimate += std::min(pseudo_cost_down * f_down, pseudo_cost_up * f_up); } log.printf("pseudocost estimate = %e\n", estimate); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 0c616909c..d86f84c39 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -24,15 +24,16 @@ template struct diving_heuristics_settings_t { i_t num_diving_workers = -1; - bool with_line_search_diving = true; - bool with_pseudocost_diving = true; - bool with_guided_diving = true; - bool with_coefficient_diving = true; + // -1 automatic, 0 disabled, 1 enabled + i_t line_search_diving = -1; + i_t pseudocost_diving = -1; + i_t guided_diving = -1; + i_t coefficient_diving = -1; i_t min_node_depth = 10; i_t node_limit = 500; f_t iteration_limit_factor = 0.05; - i_t backtrack = 5; + i_t backtrack_limit = 5; }; template diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 77a25684d..7456b59ed 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -262,12 +262,12 @@ 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.diving_settings.num_diving_workers = 1; - branch_and_bound_settings.diving_settings.with_line_search_diving = false; - branch_and_bound_settings.diving_settings.with_coefficient_diving = false; - branch_and_bound_settings.diving_settings.with_pseudocost_diving = false; - branch_and_bound_settings.log.log = false; - branch_and_bound_settings.log.log_prefix = "[RINS] "; + branch_and_bound_settings.diving_settings.num_diving_workers = 1; + branch_and_bound_settings.diving_settings.line_search_diving = 0; + branch_and_bound_settings.diving_settings.coefficient_diving = 0; + branch_and_bound_settings.diving_settings.pseudocost_diving = 0; + branch_and_bound_settings.log.log = false; + branch_and_bound_settings.log.log_prefix = "[RINS] "; branch_and_bound_settings.solution_callback = [&rins_solution_queue](std::vector& solution, f_t objective) { rins_solution_queue.push_back(solution); diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 7d96c9bcb..82670437a 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -107,10 +107,10 @@ 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.diving_settings.num_diving_workers = 1; - branch_and_bound_settings.diving_settings.with_line_search_diving = false; - branch_and_bound_settings.diving_settings.with_coefficient_diving = false; - branch_and_bound_settings.diving_settings.with_pseudocost_diving = false; + branch_and_bound_settings.diving_settings.num_diving_workers = 1; + branch_and_bound_settings.diving_settings.line_search_diving = 0; + branch_and_bound_settings.diving_settings.coefficient_diving = 0; + branch_and_bound_settings.diving_settings.pseudocost_diving = 0; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); From 977b841bf0dc4a2195b9623edd8d1386f362ed8b Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 16 Jan 2026 11:37:50 +0100 Subject: [PATCH 49/53] fixed incorrect initialized of the pseudocosts --- cpp/src/dual_simplex/pseudo_costs.cpp | 4 ++-- cpp/src/dual_simplex/pseudo_costs.hpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index f6ff72bcc..0a8f660e9 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -148,8 +148,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", diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index b9b467d04..4bab438fa 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -32,10 +32,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); } void initialized(i_t& num_initialized_down, From ab45fbf96281155442bf480e87b4e905113386bf Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 16 Jan 2026 13:14:24 +0100 Subject: [PATCH 50/53] refactor to eliminate confusing enums --- cpp/src/dual_simplex/branch_and_bound.cpp | 336 +++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 45 ++- cpp/src/utilities/omp_helpers.hpp | 4 +- 3 files changed, 190 insertions(+), 195 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 8cd4fb326..b8536aef7 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -206,12 +206,6 @@ inline 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,25 +220,14 @@ 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; 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 -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 @@ -264,7 +247,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); @@ -453,30 +436,24 @@ 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 upper_bound = get_upper_bound(); - f_t gap = upper_bound - lower_bound; - f_t obj = compute_user_objective(original_lp_, upper_bound); + f_t gap = upper_bound_ - lower_bound; + f_t obj = compute_user_objective(original_lp_, upper_bound_.load()); f_t user_bound = compute_user_objective(original_lp_, lower_bound); - f_t gap_rel = user_relative_gap(original_lp_, upper_bound, lower_bound); + f_t gap_rel = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); bool is_maximization = original_lp_.obj_scale < 0.0; settings_.log.printf("Explored %d nodes in %.2fs.\n", @@ -489,7 +466,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); @@ -504,18 +481,18 @@ mip_status_t branch_and_bound_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; + solver_status_ = mip_status_t::INFEASIBLE; if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } } } - if (upper_bound != inf) { + if (upper_bound_ != inf) { assert(incumbent_.has_incumbent); uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, solution.x); } @@ -523,8 +500,6 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t @@ -590,7 +565,7 @@ branch_variable_t branch_and_bound_t::variable_selection( std::vector current_incumbent; // If there is no incumbent, use pseudocost diving instead of guided diving - if (get_upper_bound() == inf && type == bnb_worker_type_t::GUIDED_DIVING) { + if (upper_bound_ == inf && type == bnb_worker_type_t::GUIDED_DIVING) { type = bnb_worker_type_t::PSEUDOCOST_DIVING; } @@ -623,10 +598,10 @@ branch_variable_t branch_and_bound_t::variable_selection( } template -node_solve_info_t branch_and_bound_t::solve_node( +dual::status_t branch_and_bound_t::solve_node_lp( mip_node_t* node_ptr, - search_tree_t& search_tree, lp_problem_t& leaf_problem, + lp_solution_t& leaf_solution, basis_update_mpf_t& basis_factors, std::vector& basic_list, std::vector& nonbasic_list, @@ -638,25 +613,22 @@ node_solve_info_t branch_and_bound_t::solve_node( 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(); - - 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); 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; if (thread_type != bnb_worker_type_t::BEST_FIRST) { i_t bnb_lp_iters = exploration_stats_.total_lp_iters; - i_t max_iter = settings_.diving_settings.iteration_limit_factor * bnb_lp_iters; + f_t factor = settings_.diving_settings.iteration_limit_factor; + i_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 @@ -743,24 +715,36 @@ node_solve_info_t branch_and_bound_t::solve_node( lp_settings.log.printf("\nLP status: %d\n\n", lp_status); #endif - ++stats.nodes_since_last_log; - ++stats.nodes_explored; - --stats.nodes_unexplored; + return lp_status; +} + +template +std::pair branch_and_bound_t::update_tree( + mip_node_t* node_ptr, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + lp_solution_t& leaf_solution, + bnb_worker_type_t thread_type, + 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; 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 - 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); - 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 @@ -786,9 +770,9 @@ node_solve_info_t branch_and_bound_t::solve_node( add_feasible_solution(leaf_objective, leaf_solution.x, node_ptr->depth, thread_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) { + } 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); @@ -810,25 +794,13 @@ node_solve_info_t branch_and_bound_t::solve_node( 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::BEST_FIRST) { fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); @@ -843,7 +815,7 @@ node_solve_info_t branch_and_bound_t::solve_node( 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}; } } @@ -851,7 +823,7 @@ 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; } + if (solver_status_ != mip_status_t::UNSET) { return; } // Note that we do not know which thread will execute the // `exploration_ramp_up` task, so we allow to any thread @@ -859,7 +831,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod repair_heuristic_solutions(); f_t lower_bound = node->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); f_t abs_gap = upper_bound - lower_bound; @@ -889,7 +861,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod } if (now > settings_.time_limit) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; + solver_status_ = mip_status_t::TIME_LIMIT; return; } @@ -903,25 +875,34 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod std::vector basic_list(m); std::vector nonbasic_list; - node_solve_info_t status = solve_node(node, - search_tree_, - leaf_problem, - basis_factors, - basic_list, - nonbasic_list, - node_presolver, - bnb_worker_type_t::BEST_FIRST, - true, - original_lp_.lower, - original_lp_.upper, - exploration_stats_, - settings_.log); - - if (status == node_solve_info_t::TIME_LIMIT) { - solver_status_ = mip_exploration_status_t::TIME_LIMIT; + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + dual::status_t lp_status = solve_node_lp(node, + leaf_problem, + leaf_solution, + basis_factors, + basic_list, + nonbasic_list, + node_presolver, + bnb_worker_type_t::BEST_FIRST, + true, + original_lp_.lower, + original_lp_.upper, + exploration_stats_, + settings_.log); + if (lp_status == dual::status_t::TIME_LIMIT) { + solver_status_ = mip_status_t::TIME_LIMIT; return; + } + + auto [node_status, round_dir] = update_tree(node, + search_tree_, + leaf_problem, + leaf_solution, + bnb_worker_type_t::BEST_FIRST, + lp_status, + settings_.log); - } else if (has_children(status)) { + if (node_status == node_status_t::HAS_CHILDREN) { exploration_stats_.nodes_unexplored += 2; // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase @@ -953,14 +934,14 @@ void branch_and_bound_t::plunge_from(i_t task_id, std::deque*> stack; stack.push_front(start_node); - while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { + while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET) { 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 upper_bound = upper_bound_; f_t abs_gap = upper_bound - lower_bound; f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); @@ -998,35 +979,51 @@ void branch_and_bound_t::plunge_from(i_t task_id, } if (now > 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_, - leaf_problem, - basis_factors, - basic_list, - nonbasic_list, - node_presolver, - bnb_worker_type_t::BEST_FIRST, - recompute_bounds_and_basis, - original_lp_.lower, - original_lp_.upper, - exploration_stats_, - settings_.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 (has_children(status)) { + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + dual::status_t lp_status = solve_node_lp(node_ptr, + leaf_problem, + leaf_solution, + basis_factors, + basic_list, + nonbasic_list, + node_presolver, + bnb_worker_type_t::BEST_FIRST, + recompute_bounds_and_basis, + original_lp_.lower, + original_lp_.upper, + exploration_stats_, + settings_.log); + + 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; + } + + ++exploration_stats_.nodes_since_last_log; + ++exploration_stats_.nodes_explored; + --exploration_stats_.nodes_unexplored; + + auto [node_status, round_dir] = update_tree(node_ptr, + search_tree_, + leaf_problem, + leaf_solution, + bnb_worker_type_t::BEST_FIRST, + lp_status, + settings_.log); + + recompute_bounds_and_basis = 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, @@ -1039,7 +1036,7 @@ void branch_and_bound_t::plunge_from(i_t task_id, exploration_stats_.nodes_unexplored += 2; - if (status == node_solve_info_t::UP_CHILD_FIRST) { + if (round_dir == rounding_direction_t::UP) { stack.push_front(node_ptr->get_down_child()); stack.push_front(node_ptr->get_up_child()); } else { @@ -1054,7 +1051,6 @@ 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; @@ -1068,8 +1064,8 @@ void branch_and_bound_t::best_first_thread(i_t task_id) 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 && + while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && + rel_gap > settings_.relative_mip_gap_tol && (active_subtrees_ > 0 || node_queue_.best_first_queue_size() > 0)) { // 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 @@ -1083,7 +1079,7 @@ void branch_and_bound_t::best_first_thread(i_t task_id) node_queue_.unlock(); if (start_node.has_value()) { - 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( @@ -1106,19 +1102,14 @@ void branch_and_bound_t::best_first_thread(i_t task_id) } 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); } // 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; - } + if (solver_status_ == mip_status_t::UNSET) { + if (active_subtrees_ > 0) { local_lower_bounds_[task_id] = inf; } } } @@ -1149,11 +1140,13 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, dive_stats.nodes_explored = 0; dive_stats.nodes_unexplored = 1; - 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 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 = upper_bound_; + f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { recompute_bounds_and_basis = true; @@ -1163,33 +1156,36 @@ 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 > diving_node_limit) { break; } - node_solve_info_t status = solve_node(node_ptr, - dive_tree, - 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; + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + dual::status_t lp_status = solve_node_lp(node_ptr, + leaf_problem, + leaf_solution, + basis_factors, + basic_list, + nonbasic_list, + node_presolver, + diving_type, + recompute_bounds_and_basis, + start_lower, + start_upper, + dive_stats, + log); + + 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; - } else if (has_children(status)) { - dive_stats.nodes_unexplored += 2; + auto [node_status, round_dir] = + update_tree(node_ptr, dive_tree, leaf_problem, leaf_solution, diving_type, lp_status, log); + recompute_bounds_and_basis = node_status != node_status_t::HAS_CHILDREN; - if (status == node_solve_info_t::UP_CHILD_FIRST) { + 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 { @@ -1223,7 +1219,7 @@ void branch_and_bound_t::diving_thread(bnb_worker_type_t diving_type) std::vector start_upper; bool reset_starting_bounds = true; - while (solver_status_ == mip_exploration_status_t::RUNNING && + while (solver_status_ == mip_status_t::UNSET && (active_subtrees_ > 0 || node_queue_.best_first_queue_size() > 0)) { if (reset_starting_bounds) { start_lower = original_lp_.lower; @@ -1251,8 +1247,9 @@ void branch_and_bound_t::diving_thread(bnb_worker_type_t diving_type) if (start_node.has_value()) { reset_starting_bounds = true; + if (upper_bound_ < start_node->lower_bound) { continue; } bool is_feasible = node_presolver.bounds_strengthening(start_lower, start_upper, settings_); - if (get_upper_bound() < start_node->lower_bound || !is_feasible) { continue; } + if (!is_feasible) { continue; } dive_from(start_node.value(), start_lower, @@ -1348,7 +1345,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_); @@ -1437,8 +1435,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); @@ -1503,8 +1502,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 @@ -1530,7 +1530,7 @@ 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; - solver_status_ = mip_exploration_status_t::RUNNING; + is_running = true; lower_bound_ceiling_ = inf; should_report_ = true; @@ -1571,9 +1571,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } + is_running = false; 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); + 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 4b163a7eb..dac1ab393 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -35,24 +35,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 -}; - // Indicate the search and variable selection algorithms used by each thread // in B&B (See [1]). // @@ -123,7 +105,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_; } @@ -159,7 +140,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_; @@ -195,7 +176,8 @@ class branch_and_bound_t { omp_atomic_t active_subtrees_; // Global status of the solver. - omp_atomic_t solver_status_; + omp_atomic_t solver_status_; + omp_atomic_t is_running{false}; omp_atomic_t should_report_; @@ -207,7 +189,7 @@ class branch_and_bound_t { void report(char 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. @@ -254,21 +236,32 @@ class branch_and_bound_t { // a deep dive into the subtree determined by the node. 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, - search_tree_t& search_tree, + // Solve the LP relaxation of a leaf node + dual::status_t solve_node_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, + lp_solution_t& leaf_solution, basis_update_mpf_t& basis_factors, std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, bnb_worker_type_t thread_type, - bool recompute_basis_and_bounds, + bool recompute_bounds_and_basis, const std::vector& root_lower, const std::vector& root_upper, 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, + lp_problem_t& leaf_problem, + lp_solution_t& leaf_solution, + bnb_worker_type_t thread_type, + 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, 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 77c237b91d875f1001486f52c225a10d26e75cec Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 16 Jan 2026 13:22:51 +0100 Subject: [PATCH 51/53] removed unused variable --- cpp/src/dual_simplex/diving_heuristics.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/src/dual_simplex/diving_heuristics.cpp b/cpp/src/dual_simplex/diving_heuristics.cpp index a1c9ad086..a56b4cce3 100644 --- a/cpp/src/dual_simplex/diving_heuristics.cpp +++ b/cpp/src/dual_simplex/diving_heuristics.cpp @@ -212,8 +212,6 @@ void calculate_variable_locks(const lp_problem_t& lp_problem, for (i_t p = start; p < end; ++p) { f_t val = lp_problem.A.x[p]; - i_t i = lp_problem.A.i[p]; - if (std::abs(val) > eps) { up_locks[j]++; down_locks[j]++; From d4f568e34c49ea456956db509edfe9348b1091d0 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 16 Jan 2026 14:05:32 +0100 Subject: [PATCH 52/53] fixed incorrect termination --- cpp/src/dual_simplex/branch_and_bound.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b8536aef7..8f71eec57 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -194,6 +194,19 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) } } +#ifdef SHOW_DIVING_TYPE +inline char feasible_solution_symbol(bnb_worker_type_t type) +{ + switch (type) { + case bnb_worker_type_t::BEST_FIRST: return 'B'; + case bnb_worker_type_t::COEFFICIENT_DIVING: return 'C'; + case bnb_worker_type_t::LINE_SEARCH_DIVING: return 'L'; + case bnb_worker_type_t::PSEUDOCOST_DIVING: return 'P'; + case bnb_worker_type_t::GUIDED_DIVING: return 'G'; + default: return 'U'; + } +} +#else inline char feasible_solution_symbol(bnb_worker_type_t type) { switch (type) { @@ -205,6 +218,7 @@ inline char feasible_solution_symbol(bnb_worker_type_t type) default: return 'U'; } } +#endif } // namespace @@ -934,7 +948,7 @@ void branch_and_bound_t::plunge_from(i_t task_id, std::deque*> stack; stack.push_front(start_node); - while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET) { + while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET && is_running) { if (task_id == 0) { repair_heuristic_solutions(); } mip_node_t* node_ptr = stack.front(); @@ -1106,6 +1120,8 @@ void branch_and_bound_t::best_first_thread(i_t task_id) rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); } + is_running = false; + // Check if it is the last thread that exited the loop and no // timeout or numerical error has happen. if (solver_status_ == mip_status_t::UNSET) { @@ -1140,7 +1156,7 @@ void branch_and_bound_t::dive_from(mip_node_t& start_node, dive_stats.nodes_explored = 0; dive_stats.nodes_unexplored = 1; - while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET) { + while (stack.size() > 0 && solver_status_ == mip_status_t::UNSET && is_running) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -1219,7 +1235,7 @@ void branch_and_bound_t::diving_thread(bnb_worker_type_t diving_type) std::vector start_upper; bool reset_starting_bounds = true; - while (solver_status_ == mip_status_t::UNSET && + while (solver_status_ == mip_status_t::UNSET && is_running && (active_subtrees_ > 0 || node_queue_.best_first_queue_size() > 0)) { if (reset_starting_bounds) { start_lower = original_lp_.lower; From 161fd781f506b0f8cc835f0edfacc6ad623a52f8 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 16 Jan 2026 14:37:02 +0100 Subject: [PATCH 53/53] fixed missing counters in ramp-up phase --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 8f71eec57..b2c9f85d2 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -908,6 +908,10 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod return; } + ++exploration_stats_.nodes_since_last_log; + ++exploration_stats_.nodes_explored; + --exploration_stats_.nodes_unexplored; + auto [node_status, round_dir] = update_tree(node, search_tree_, leaf_problem,