From 93c01f56e2889803dfb7bb32ba0d9a001ed59c6c Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 21 Aug 2026 13:24:28 +0200 Subject: [PATCH 1/3] B&B now can be stopped via the concurrent_halt flag. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 135 ++++++++++++------ cpp/src/branch_and_bound/branch_and_bound.hpp | 20 ++- cpp/src/branch_and_bound/worker.hpp | 2 - 3 files changed, 102 insertions(+), 55 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 29174148b7..5fbb205c96 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #define SUBMIP_VERBOSE false @@ -835,9 +836,7 @@ template void branch_and_bound_t::set_final_solution(mip_solution_t& solution, f_t lower_bound) { - if (solver_status_ == mip_status_t::SUBMIP_HALT) { - settings_.log.debug("Stopping the sub-MIP solve...\n"); - } + if (solver_status_ == mip_status_t::HALT) { settings_.log.debug("Stopping the solver...\n"); } if (solver_status_ == mip_status_t::NUMERICAL) { settings_.log.printf("Numerical issue encountered. Stopping the solver...\n"); @@ -1749,6 +1748,14 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, } } + if (received_halt_signal()) { + solver_status_ = mip_status_t::HALT; + node_concurrent_halt_ = true; + stack.push_front(node_ptr); + --exploration_stats_.nodes_being_solved; + break; + } + if (now > settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; stack.push_front(node_ptr); @@ -1960,7 +1967,7 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t bool stop = submip_halt_callback_(user_obj, user_lower); if (stop) { node_concurrent_halt_ = 1; - solver_status_ = mip_status_t::SUBMIP_HALT; + solver_status_ = mip_status_t::HALT; settings_.log.debug_format( "Received halt signal. Current best obj={:.6e} and best bound={:.6e}\n", user_obj, @@ -1969,6 +1976,12 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t } } + if (received_halt_signal()) { + solver_status_ = mip_status_t::HALT; + node_concurrent_halt_ = true; + break; + } + // If the guided diving was disabled previously due to the lack of an incumbent solution, // re-enable as soon as a new incumbent is found. if (diving_worker_pool_.size() > 0 && settings_.diving_settings.guided_diving != 0 && @@ -2240,7 +2253,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_stats_t& submip_stats, f_t fixrate, i_t simplex_iter_used, - bool is_root_heuristic) + bool is_root_heuristic, + std::atomic* halt) { double start_time = tic(); @@ -2264,6 +2278,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.submip_settings.level = submip_level; submip_settings.benchmark_info_ptr = nullptr; submip_settings.log.log = SUBMIP_VERBOSE; + submip_settings.concurrent_halt = halt ? halt : settings_.concurrent_halt; #ifdef SAVE_SUBMIP_TO_FILE submip_settings.log.log_prefix = std::format("{}{}", settings_.log.log_prefix, worker->worker_id); @@ -2344,6 +2359,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke return; } + if (halt != nullptr && halt->load(std::memory_order_acquire)) { return; } + submip_settings.heuristic_preemption_callback = nullptr; submip_settings.dual_simplex_objective_callback = nullptr; submip_settings.set_simplex_solution_callback = nullptr; @@ -2391,12 +2408,12 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_bnb.set_submip_halt_callback(submip_halt_callback_); } else { // This should only be called by the main solver. - submip_bnb.set_submip_halt_callback([this, worker](f_t, f_t submip_lower_bound) { + submip_bnb.set_submip_halt_callback([this](f_t, f_t submip_lower_bound) { f_t user_upper = compute_user_objective(this->original_lp_, this->upper_bound_.load()); bool is_cutoff = original_lp_.obj_scale > 0 ? submip_lower_bound > user_upper : user_upper > submip_lower_bound; bool is_solver_running = this->solver_status_ == mip_status_t::UNSET && this->is_running_; - return is_cutoff || !is_solver_running || worker->halt; + return is_cutoff || !is_solver_running || this->received_halt_signal(); }); } @@ -2646,7 +2663,7 @@ template void branch_and_bound_t::recursive_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, - bool is_root_heuristic) + bool is_root_heuristic, std::atomic* halt) { raft::common::nvtx::range scope("BB::submip_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } @@ -2691,7 +2708,8 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w i_t round = 0; - while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { + while (solver_status_ == mip_status_t::UNSET && is_running_ && + !(halt && halt->load(std::memory_order::acquire))) { f_t prev_fixrate = fixrate; f_t distance = 1.0 - (1.0 - prev_fixrate) * close_ratio; f_t round_target_fixrate = std::min(distance, max_fixrate) - prev_fixrate; @@ -2916,7 +2934,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w submip_stats, fixrate, stats.total_simplex_iters, - is_root_heuristic); + is_root_heuristi, halt); } } @@ -2994,14 +3012,14 @@ void branch_and_bound_t::launch_root_heuristics( if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - recursive_submip(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); + recursive_submip(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic, ¤t_heuristic->halt_); } else { ++(*worker_count); #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(current_incumbent, current_heuristic, worker_count) depend(out : *worker) { recursive_submip( - worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); + worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic, ¤t_heuristic->halt_); --(*worker_count); } } @@ -3038,6 +3056,11 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( // Wait for the root relaxation solution to be sent by the diversity manager or dual simplex while (!root_crossover_solution_set_.load(std::memory_order_acquire) && *get_root_concurrent_halt() == 0) { + if (received_halt_signal()) { + root_concurrent_halt_.store(true, std::memory_order_release); + return lp_status_t::CONCURRENT_LIMIT; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); #pragma omp taskyield } @@ -3326,7 +3349,7 @@ auto branch_and_bound_t::do_cut_pass( i_t iter = 0; bool initialize_basis = false; - lp_settings.concurrent_halt = NULL; + lp_settings.concurrent_halt = settings_.concurrent_halt; f_t dual_phase2_start_time = tic(); dual_status_t cut_status = dual_phase2_with_advanced_basis(2, 0, @@ -3352,6 +3375,12 @@ auto branch_and_bound_t::do_cut_pass( return {cut_pass_action_t::RETURN, solver_status_}; } + if (cut_status == dual_status_t::CONCURRENT_LIMIT) { + solver_status_ = mip_status_t::HALT; + set_final_solution(solution, root_objective_); + return {cut_pass_action_t::RETURN, solver_status_}; + } + if (cut_status != dual_status_t::OPTIMAL) { settings_.log.printf("Numerical issue at root node. Resolving from scratch\n"); lp_status_t scratch_status = @@ -3386,23 +3415,36 @@ auto branch_and_bound_t::do_cut_pass( f_t remove_cuts_start_time = tic(); mutex_original_lp_.lock(); - remove_cuts(original_lp_, - settings_, - exploration_stats_.start_time, - Arow_, - new_slacks_, - original_rows, - var_types_, - root_vstatus_, - edge_norms_, - root_relax_soln_.x, - root_relax_soln_.y, - root_relax_soln_.z, - basic_list, - nonbasic_list, - basis_update); + i_t remove_cuts_status = remove_cuts(original_lp_, + lp_settings, + exploration_stats_.start_time, + Arow_, + new_slacks_, + original_rows, + var_types_, + root_vstatus_, + edge_norms_, + root_relax_soln_.x, + root_relax_soln_.y, + root_relax_soln_.z, + basic_list, + nonbasic_list, + basis_update); variable_bounds.resize(original_lp_.num_cols); mutex_original_lp_.unlock(); + + if (remove_cuts_status == TIME_LIMIT_RETURN) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return {cut_pass_action_t::RETURN, solver_status_}; + } + + if (remove_cuts_status == CONCURRENT_HALT_RETURN) { + solver_status_ = mip_status_t::HALT; + set_final_solution(solution, root_objective_); + return {cut_pass_action_t::RETURN, solver_status_}; + } + f_t remove_cuts_time = toc(remove_cuts_start_time); if (remove_cuts_time > 1.0) { settings_.log.debug("Remove cuts time %.2f seconds\n", remove_cuts_time); @@ -3531,6 +3573,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // RINS/SUBMIP path settings_.log.printf("\n"); settings_.log.printf("Solving LP root relaxation with dual simplex\n"); + lp_settings.concurrent_halt = settings_.concurrent_halt; root_status = solve_linear_program_with_advanced_basis(original_lp_, exploration_stats_.start_time, lp_settings, @@ -3560,11 +3603,19 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t root_relax_elapsed_time = toc(root_relax_start_time); exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; + scope_guard cliques_scope([&]() { + signal_extend_cliques_.store(true, std::memory_order_release); +#pragma omp taskwait depend(in : *clique_signal) + }); + + if (root_status == lp_status_t::CONCURRENT_LIMIT) { + set_final_solution(solution, -inf); + return mip_status_t::HALT; + } + if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("The root LP relaxation is infeasible\n", lp_status_to_string(root_status).c_str()); - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return mip_status_t::INFEASIBLE; } @@ -3574,32 +3625,24 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return mip_status_t::UNBOUNDED; } if (root_status == lp_status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, -inf); - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return solver_status_; } if (root_status == lp_status_t::WORK_LIMIT) { solver_status_ = mip_status_t::WORK_LIMIT; set_final_solution(solution, -inf); - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return solver_status_; } if (root_status == lp_status_t::NUMERICAL_ISSUES) { solver_status_ = mip_status_t::NUMERICAL; set_final_solution(solution, -inf); - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return solver_status_; } @@ -3637,13 +3680,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (num_fractional == 0) { if (settings_.benchmark_info_ptr != nullptr) { - const double v = static_cast(compute_user_objective(original_lp_, root_objective_)); + f_t v = compute_user_objective(original_lp_, root_objective_); settings_.benchmark_info_ptr->root_lp_no_cuts = v; settings_.benchmark_info_ptr->root_lp_with_cuts = v; } set_solution_at_root(solution, cut_info); - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return mip_status_t::OPTIMAL; } @@ -3693,8 +3734,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time); } - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return solver_status_; } if (num_fractional == 0) { @@ -3709,8 +3748,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time); } - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return mip_status_t::OPTIMAL; } @@ -3749,8 +3786,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time); } - signal_extend_cliques_.store(true, std::memory_order_release); -#pragma omp taskwait depend(in : *clique_signal) return cut_pass_result.status; } if (cut_pass_result.action == cut_pass_action_t::BREAK) { break; } @@ -3806,6 +3841,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut pc_); } + if (received_halt_signal()) { + solver_status_ = mip_status_t::HALT; + set_final_solution(solution, root_objective_); + return solver_status_; + } + if (toc(exploration_stats_.start_time) > settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 7cf5ed3680..216acd1221 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -57,7 +57,7 @@ enum class mip_status_t { NUMERICAL = 6, // The solver encountered a numerical error UNSET = 7, // The status is not set WORK_LIMIT = 8, // The solver reached a deterministic work limit - SUBMIP_HALT = 9 // Halt the solver + HALT = 9 // Halt the solver }; inline std::string mip_status_to_string(mip_status_t status) @@ -72,7 +72,7 @@ inline std::string mip_status_to_string(mip_status_t status) case mip_status_t::NUMERICAL: return "NUMERICAL"; case mip_status_t::UNSET: return "UNSET"; case mip_status_t::WORK_LIMIT: return "WORK_LIMIT"; - case mip_status_t::SUBMIP_HALT: return "SUBMIP_HALT"; + case mip_status_t::HALT: return "HALT"; } return "UNKNOWN"; } @@ -303,6 +303,12 @@ class branch_and_bound_t { i_t node_int_infeas, double work_time = -1); + bool received_halt_signal() + { + return settings_.concurrent_halt ? settings_.concurrent_halt->load(std::memory_order_acquire) + : false; + } + enum class cut_pass_action_t { CONTINUE, BREAK, RETURN }; struct cut_pass_result_t { cut_pass_action_t action{cut_pass_action_t::CONTINUE}; @@ -378,20 +384,22 @@ class branch_and_bound_t { f_t fixrate, std::string_view log_prefix); - // Solve the RINS sub-MIP + // Solve the RINS sub-MIP. void solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, submip_stats_t& submip_stats, f_t fixrate, i_t simplex_iter_used, - bool is_root_heuristic = false); + bool is_root_heuristic = false, + std::atomic* halt = nullptr); - // Creates and solves the RINS sub-MIP + // Creates and solves the RINS/RENS sub-MIP. void recursive_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, - bool is_root_heuristic = false); + bool is_root_heuristic = false, + std::atomic* halt = nullptr); void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index bbc2836d43..aa0ef6bb88 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -253,8 +253,6 @@ class diving_worker_t : public branch_and_bound_worker_t { // The best-first worker that is associated with this diving worker. Used for controlling the // number of active diving workers. bfs_worker_t* bfs_worker{nullptr}; - - std::atomic halt = false; }; struct submip_stats_t { From 4f8d4d54bbe55314d8607d4af0a404b6678ce1c6 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 21 Aug 2026 14:52:55 +0200 Subject: [PATCH 2/3] simplified return from `do_cut_pass`. covered missing cases. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 90 +++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 6 +- cpp/src/mip_heuristics/root_heuristics.hpp | 6 +- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 5fbb205c96..3599f98cc7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3191,7 +3191,7 @@ auto branch_and_bound_t::do_cut_pass( f_t& last_objective, f_t root_relax_objective, i_t& cut_pool_size, - [[maybe_unused]] const std::vector& saved_solution) -> cut_pass_result_t + [[maybe_unused]] const std::vector& saved_solution) -> cut_pass_action_t { #ifdef PRINT_FRACTIONAL_INFO settings_.log.printf("Found %d fractional variables on cut pass %d\n", num_fractional, cut_pass); @@ -3222,12 +3222,14 @@ auto branch_and_bound_t::do_cut_pass( if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } - return {cut_pass_action_t::RETURN, mip_status_t::INFEASIBLE}; + + solver_status_ = mip_status_t::INFEASIBLE; + return cut_pass_action_t::RETURN; } if (toc(exploration_stats_.start_time) >= settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); - return {cut_pass_action_t::RETURN, solver_status_}; + return cut_pass_action_t::RETURN; } f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { @@ -3243,7 +3245,7 @@ auto branch_and_bound_t::do_cut_pass( std::vector cut_rhs; std::vector cut_types; i_t num_cuts = cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types); - if (num_cuts == 0) { return {cut_pass_action_t::BREAK, mip_status_t::UNSET}; } + if (num_cuts == 0) { return cut_pass_action_t::BREAK; } cut_info.record_cut_types(cut_types); #ifdef PRINT_CUT_POOL_TYPES cut_pool.print_cutpool_types(); @@ -3257,7 +3259,8 @@ auto branch_and_bound_t::do_cut_pass( for (i_t i = 0; i < static_cast(cut_types.size()); ++i) { settings_.log.printf("row %d cut type %d\n", i, cut_types[i]); } - return {cut_pass_action_t::RETURN, mip_status_t::NUMERICAL}; + solver_status_ = mip_status_t::NUMERICAL; + return cut_pass_action_t::RETURN; } #endif #ifdef CHECK_CUTS_AGAINST_SAVED_SOLUTION @@ -3294,7 +3297,8 @@ auto branch_and_bound_t::do_cut_pass( if (add_cuts_time > 1.0) { settings_.log.debug("Add cuts time %.2f seconds\n", add_cuts_time); } if (add_cuts_status != 0) { settings_.log.printf("Failed to add cuts\n"); - return {cut_pass_action_t::RETURN, mip_status_t::NUMERICAL}; + solver_status_ = mip_status_t::NUMERICAL; + return cut_pass_action_t::RETURN; } if (settings_.reduced_cost_strengthening >= 1 && upper_bound_.load() < last_upper_bound) { @@ -3338,13 +3342,14 @@ auto branch_and_bound_t::do_cut_pass( #ifdef WRITE_BOUND_STRENGTHENING_INFEASIBLE_MPS original_lp_.write_mps("bound_strengthening_infeasible.mps"); #endif - return {cut_pass_action_t::RETURN, mip_status_t::INFEASIBLE}; + solver_status_ = mip_status_t::INFEASIBLE; + return cut_pass_action_t::RETURN; } if (toc(exploration_stats_.start_time) >= settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); - return {cut_pass_action_t::RETURN, solver_status_}; + return cut_pass_action_t::RETURN; } i_t iter = 0; @@ -3372,13 +3377,13 @@ auto branch_and_bound_t::do_cut_pass( if (cut_status == dual_status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); - return {cut_pass_action_t::RETURN, solver_status_}; + return cut_pass_action_t::RETURN; } if (cut_status == dual_status_t::CONCURRENT_LIMIT) { solver_status_ = mip_status_t::HALT; set_final_solution(solution, root_objective_); - return {cut_pass_action_t::RETURN, solver_status_}; + return cut_pass_action_t::RETURN; } if (cut_status != dual_status_t::OPTIMAL) { @@ -3398,12 +3403,19 @@ auto branch_and_bound_t::do_cut_pass( cut_status = convert_lp_status_to_dual_status(scratch_status); exploration_stats_.total_simplex_iters += root_relax_soln_.iterations; root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + + } else if (scratch_status == lp_status_t::CONCURRENT_LIMIT) { + cut_status = convert_lp_status_to_dual_status(scratch_status); + solver_status_ = mip_status_t::HALT; + set_final_solution(solution, root_objective_); + return cut_pass_action_t::RETURN; } else { settings_.log.printf("Cut status %s\n", simplex::dual_status_to_string(cut_status).c_str()); #ifdef WRITE_CUT_INFEASIBLE_MPS original_lp_.write_mps("cut_infeasible.mps"); #endif - return {cut_pass_action_t::RETURN, mip_status_t::NUMERICAL}; + solver_status_ = mip_status_t::NUMERICAL; + return cut_pass_action_t::RETURN; } } root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); @@ -3436,13 +3448,13 @@ auto branch_and_bound_t::do_cut_pass( if (remove_cuts_status == TIME_LIMIT_RETURN) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); - return {cut_pass_action_t::RETURN, solver_status_}; + return cut_pass_action_t::RETURN; } if (remove_cuts_status == CONCURRENT_HALT_RETURN) { solver_status_ = mip_status_t::HALT; set_final_solution(solution, root_objective_); - return {cut_pass_action_t::RETURN, solver_status_}; + return cut_pass_action_t::RETURN; } f_t remove_cuts_time = toc(remove_cuts_start_time); @@ -3468,7 +3480,8 @@ auto branch_and_bound_t::do_cut_pass( if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { if (num_fractional == 0) { set_solution_at_root(solution, cut_info); } set_final_solution(solution, root_objective_); - return {cut_pass_action_t::RETURN, mip_status_t::OPTIMAL}; + solver_status_ = mip_status_t::OPTIMAL; + return cut_pass_action_t::RETURN; } f_t change_in_objective = root_objective_ - last_objective; @@ -3480,10 +3493,10 @@ auto branch_and_bound_t::do_cut_pass( "Change in objective %.16e is less than 1e-3 of root relax objective %.16e\n", change_in_objective, root_relax_objective); - return {cut_pass_action_t::BREAK, mip_status_t::UNSET}; + return cut_pass_action_t::BREAK; } last_objective = root_objective_; - return {cut_pass_action_t::CONTINUE, mip_status_t::UNSET}; + return cut_pass_action_t::CONTINUE; } template @@ -3609,6 +3622,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut }); if (root_status == lp_status_t::CONCURRENT_LIMIT) { + solver_status_ = mip_status_t::HALT; set_final_solution(solution, -inf); return mip_status_t::HALT; } @@ -3753,25 +3767,24 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut launch_root_heuristics(original_lp_, root_relax_soln_.x, cut_pass, root_heuristics); - cut_pass_result_t cut_pass_result; - cut_pass_result = do_cut_pass(cut_pass, - solution, - num_fractional, - fractional, - cut_generation, - basis_update, - basic_list, - nonbasic_list, - variable_bounds, - cut_pool, - cut_info, - lp_settings, - original_rows, - last_upper_bound, - last_objective, - root_relax_objective, - cut_pool_size, - saved_solution); + cut_pass_action_t cut_pass_action = do_cut_pass(cut_pass, + solution, + num_fractional, + fractional, + cut_generation, + basis_update, + basic_list, + nonbasic_list, + variable_bounds, + cut_pool, + cut_info, + lp_settings, + original_rows, + last_upper_bound, + last_objective, + root_relax_objective, + cut_pool_size, + saved_solution); mutex_upper_.lock(); if (incumbent_.has_incumbent && incumbent_.x.size() != original_lp_.num_cols) { @@ -3782,13 +3795,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } mutex_upper_.unlock(); - if (cut_pass_result.action == cut_pass_action_t::RETURN) { + if (cut_pass_action == cut_pass_action_t::RETURN) { if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time); } - return cut_pass_result.status; + assert(solver_status_ != mip_status_t::UNSET); + return solver_status_; } - if (cut_pass_result.action == cut_pass_action_t::BREAK) { break; } + if (cut_pass_action == cut_pass_action_t::BREAK) { break; } } // Publish the post-cuts root LP value. diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 216acd1221..5c6bba3481 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -310,12 +310,8 @@ class branch_and_bound_t { } enum class cut_pass_action_t { CONTINUE, BREAK, RETURN }; - struct cut_pass_result_t { - cut_pass_action_t action{cut_pass_action_t::CONTINUE}; - mip_status_t status{mip_status_t::UNSET}; - }; - cut_pass_result_t do_cut_pass(i_t cut_pass, + cut_pass_action_t do_cut_pass(i_t cut_pass, simplex::mip_solution_t& solution, i_t& num_fractional, std::vector& fractional, diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index 1f29b25eef..097fbd7107 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -19,6 +19,7 @@ struct cut_pass_heuristics_t { csr_matrix_t Arow_; std::vector root_solution_; std::vector root_edge_norm_; + std::atomic halt_; std::unique_ptr> submip_worker_; fj_cpu_worker_t fj_cpu_worker_; @@ -38,15 +39,16 @@ struct cut_pass_heuristics_t { void send_stop_signal() { fj_cpu_worker_.send_stop_signal(); - if (submip_worker_) { submip_worker_->halt = true; } + halt_ = true; } void stop_and_sync() { fj_cpu_worker_.stop(); + halt_ = true; + if (submip_worker_) { diving_worker_t* worker = submip_worker_.get(); - worker->halt = true; #pragma omp taskwait depend(in : *worker) submip_worker_.reset(); } From b961233568dc56d74ff6d2ca880f0ed2eeb66c27 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 24 Aug 2026 11:20:00 +0200 Subject: [PATCH 3/3] use the settings instead of bare halt. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 126 +++++++++--------- cpp/src/branch_and_bound/branch_and_bound.hpp | 6 +- .../dual_simplex/simplex_solver_settings.hpp | 4 +- cpp/src/mip_heuristics/root_heuristics.hpp | 3 + 4 files changed, 74 insertions(+), 65 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3599f98cc7..e2ea33bb6f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2236,11 +2236,11 @@ bool branch_and_bound_t::launch_submip_worker(const std::vector& if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - recursive_submip(worker, current_incumbent, var_types_); + recursive_submip(worker, current_incumbent, var_types_, settings_); } else { #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(worker, current_incumbent) - recursive_submip(worker, current_incumbent, var_types_); + recursive_submip(worker, current_incumbent, var_types_, settings_); } return true; @@ -2253,21 +2253,17 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_stats_t& submip_stats, f_t fixrate, i_t simplex_iter_used, - bool is_root_heuristic, - std::atomic* halt) + simplex_solver_settings_t submip_settings) { double start_time = tic(); - i_t submip_level = settings_.submip_settings.level + 1; - std::string log_prefix = - std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); + bool is_root_heuristic = submip_settings.inside_root_node; + i_t submip_level = settings_.submip_settings.level + 1; + f_t user_lower = compute_user_objective(worker->leaf_problem, get_lower_bound()); + f_t user_obj = compute_user_objective(worker->leaf_problem, upper_bound_.load()); + f_t rel_gap = user_relative_gap(user_obj, user_lower); + i_t explored = exploration_stats_.nodes_explored; - f_t user_lower = compute_user_objective(worker->leaf_problem, get_lower_bound()); - f_t user_obj = compute_user_objective(worker->leaf_problem, upper_bound_.load()); - f_t rel_gap = user_relative_gap(user_obj, user_lower); - i_t explored = exploration_stats_.nodes_explored; - - simplex_solver_settings_t submip_settings = settings_; submip_settings.print_presolve_stats = false; submip_settings.num_threads = 1; submip_settings.reliability_branching = 0; @@ -2275,19 +2271,15 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.zero_half_cuts = 0; submip_settings.inside_submip = 1; submip_settings.strong_branching_simplex_iteration_limit = 50; + submip_settings.inside_root_node = 0; submip_settings.submip_settings.level = submip_level; submip_settings.benchmark_info_ptr = nullptr; submip_settings.log.log = SUBMIP_VERBOSE; - submip_settings.concurrent_halt = halt ? halt : settings_.concurrent_halt; + std::string_view log_prefix = submip_settings.log.log_prefix; -#ifdef SAVE_SUBMIP_TO_FILE - submip_settings.log.log_prefix = std::format("{}{}", settings_.log.log_prefix, worker->worker_id); - CUOPT_LOG_INFO("Writting submip %s to MPS file", submip_settings.log.log_prefix); - worker->leaf_problem.write_mps(std::format("submip-{}.mps", submip_settings.log.log_prefix), - var_types_); -#else - submip_settings.log.log_prefix = log_prefix; -#endif + bool max_recursion = submip_level > settings_.submip_settings.max_level; + submip_settings.submip_settings.rins = settings_.submip_settings.rins != 0 && !max_recursion; + submip_settings.submip_settings.rens = settings_.submip_settings.rens != 0 && !max_recursion; submip_settings.node_limit = settings_.submip_settings.node_limit_offset + explored / 20; @@ -2308,10 +2300,6 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.relative_mip_gap_tol = std::min(settings_.submip_settings.target_mip_gap, rel_gap); - bool max_recursion = submip_level > settings_.submip_settings.max_level; - submip_settings.submip_settings.rins = settings_.submip_settings.rins != 0 && !max_recursion; - submip_settings.submip_settings.rens = settings_.submip_settings.rens != 0 && !max_recursion; - DEBUG_SUBMIP("{}Sub-MIP: fixrate={:.2f}", log_prefix, fixrate) DEBUG_SUBMIP( "{}Sub-MIP solve settings: time_limit={:.2f}, node_limit={}, iter_limit={} (current_iter={}), " @@ -2359,7 +2347,10 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke return; } - if (halt != nullptr && halt->load(std::memory_order_acquire)) { return; } + if (submip_settings.concurrent_halt && + submip_settings.concurrent_halt->load(std::memory_order_acquire)) { + return; + } submip_settings.heuristic_preemption_callback = nullptr; submip_settings.dual_simplex_objective_callback = nullptr; @@ -2660,16 +2651,17 @@ f_t calculate_fixrate(const std::vector& integer_list, } template -void branch_and_bound_t::recursive_submip(diving_worker_t* worker, - const std::vector& current_incumbent, - const std::vector& var_types, - bool is_root_heuristic, std::atomic* halt) +void branch_and_bound_t::recursive_submip( + diving_worker_t* worker, + const std::vector& current_incumbent, + const std::vector& var_types, + simplex_solver_settings_t submip_settings) { raft::common::nvtx::range scope("BB::submip_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } i_t submip_level = settings_.submip_settings.level + 1; - std::string log_prefix = + submip_settings.log.log_prefix = std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); assert((worker->search_strategy == search_strategy_t::RINS || @@ -2709,7 +2701,8 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w i_t round = 0; while (solver_status_ == mip_status_t::UNSET && is_running_ && - !(halt && halt->load(std::memory_order::acquire))) { + !(submip_settings.concurrent_halt && + submip_settings.concurrent_halt->load(std::memory_order::acquire))) { f_t prev_fixrate = fixrate; f_t distance = 1.0 - (1.0 - prev_fixrate) * close_ratio; f_t round_target_fixrate = std::min(distance, max_fixrate) - prev_fixrate; @@ -2797,7 +2790,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w "{}Round {}: fixed {:.0f} ({:.2f}) -> {:.0f} ({:.2f}) variables. target round fixrate = {} " "({:.2f}). " "max fixrate = {:.4g}", - log_prefix, + submip_settings.log.log_prefix, round, prev_fixrate * num_integers, prev_fixrate, @@ -2808,7 +2801,9 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w max_fixrate); if (!is_feasible) { - DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) + DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", + submip_settings.log.log_prefix, + round) break; } @@ -2832,7 +2827,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w std::numeric_limits::max()); if (max_iter <= 0) { DEBUG_SUBMIP("{}Round {}: max iteration reached! {}/{}", - log_prefix, + submip_settings.log.log_prefix, round, stats.total_simplex_iters.load(), simplex_iter_limit) @@ -2842,7 +2837,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w dual_status_t lp_status = solve_node_lp(&node, worker, stats, log, max_iter); if (lp_status != dual_status_t::OPTIMAL) { DEBUG_SUBMIP("{}Round {}: simplex returned {}", - log_prefix, + submip_settings.log.log_prefix, round, simplex::dual_status_to_string(lp_status)) break; @@ -2857,7 +2852,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w snap_to_lattice(&node, leaf_obj); if (leaf_obj > upper_bound_.load()) { DEBUG_SUBMIP("{}Round {}: reached cutoff point. obj={:.4g}. upper_bound={:.4g}", - log_prefix, + submip_settings.log.log_prefix, round, leaf_obj, upper_bound_.load()) @@ -2868,7 +2863,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w // We found a feasible solution when fixing the variables in RINS/RENS. add_feasible_solution(leaf_obj, current_sol, -1, worker->search_strategy); DEBUG_SUBMIP("{}Round {}: found a solution with obj={:.4g}. upper_bound={:.4g}", - log_prefix, + submip_settings.log.log_prefix, round, leaf_obj, upper_bound_.load()) @@ -2907,19 +2902,20 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w f_t time_limit = std::max(settings_.time_limit - toc(exploration_stats_.start_time), 0); f_t work_limit = 1.0; - submip_fj_cpu_worker.create_worker(worker->leaf_problem, - var_types, - worker->leaf_solution.x, - settings_, - std::format("{} [CPU FJ]", log_prefix), - worker->rng.next_i64()); + submip_fj_cpu_worker.create_worker( + worker->leaf_problem, + var_types, + worker->leaf_solution.x, + settings_, + std::format("{} [CPU FJ]", submip_settings.log.log_prefix), + worker->rng.next_i64()); submip_fj_cpu_worker.run_sync(time_limit, work_limit); } // We need the pseudocost to do the DFS, which we do not have during the cut passes. - if (!is_root_heuristic) { + if (!submip_settings.inside_root_node) { DEBUG_SUBMIP("{}Running a quick DFS. fixrate={:.4g} ({}/{})", - log_prefix, + submip_settings.log.log_prefix, fixrate, fixrate * num_integers, num_integers); @@ -2934,14 +2930,14 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w submip_stats, fixrate, stats.total_simplex_iters, - is_root_heuristi, halt); + submip_settings); } } DEBUG_SUBMIP( "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({:.0f}/{}), max_fixrate={:.4g}, " "min_fixrate={:.4g}\n", - log_prefix, + submip_settings.log.log_prefix, submip_stats.total_success.load(), submip_stats.total_infeasible.load(), submip_stats.total_calls.load(), @@ -2952,7 +2948,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w min_fixrate); // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. - if (!is_root_heuristic) { + if (!submip_settings.inside_root_node) { submip_worker_pool_.return_worker_to_pool(worker); } else { worker->set_inactive(); @@ -3009,17 +3005,20 @@ void branch_and_bound_t::launch_root_heuristics( if (use_rins) current_incumbent = incumbent_.x; mutex_upper_.unlock(); + simplex_solver_settings_t submip_settings = settings_; + submip_settings.concurrent_halt = ¤t_heuristic->halt_; + submip_settings.inside_root_node = true; + if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - recursive_submip(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic, ¤t_heuristic->halt_); + recursive_submip(worker, current_incumbent, current_heuristic->var_types_, submip_settings); } else { ++(*worker_count); -#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ - firstprivate(current_incumbent, current_heuristic, worker_count) depend(out : *worker) +#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) firstprivate( \ + current_incumbent, current_heuristic, worker_count, submip_settings) depend(out : *worker) { - recursive_submip( - worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic, ¤t_heuristic->halt_); + recursive_submip(worker, current_incumbent, current_heuristic->var_types_, submip_settings); --(*worker_count); } } @@ -3058,6 +3057,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( *get_root_concurrent_halt() == 0) { if (received_halt_signal()) { root_concurrent_halt_.store(true, std::memory_order_release); +#pragma omp taskwait depend(in : root_status) return lp_status_t::CONCURRENT_LIMIT; } @@ -3352,11 +3352,10 @@ auto branch_and_bound_t::do_cut_pass( return cut_pass_action_t::RETURN; } - i_t iter = 0; - bool initialize_basis = false; - lp_settings.concurrent_halt = settings_.concurrent_halt; - f_t dual_phase2_start_time = tic(); - dual_status_t cut_status = dual_phase2_with_advanced_basis(2, + i_t iter = 0; + bool initialize_basis = false; + f_t dual_phase2_start_time = tic(); + dual_status_t cut_status = dual_phase2_with_advanced_basis(2, 0, initialize_basis, exploration_stats_.start_time, @@ -3570,6 +3569,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut simplex_solver_settings_t lp_settings = settings_; lp_settings.inside_mip = 1; lp_settings.scale_columns = false; + lp_settings.inside_root_node = true; lp_settings.concurrent_halt = get_root_concurrent_halt(); lp_settings.dual_simplex_objective_callback = [this](f_t user_obj) { root_lp_current_lower_bound_.store(user_obj); @@ -3587,6 +3587,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.printf("\n"); settings_.log.printf("Solving LP root relaxation with dual simplex\n"); lp_settings.concurrent_halt = settings_.concurrent_halt; + lp_settings.inside_mip = 2; root_status = solve_linear_program_with_advanced_basis(original_lp_, exploration_stats_.start_time, lp_settings, @@ -3616,6 +3617,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t root_relax_elapsed_time = toc(root_relax_start_time); exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; + // This stops the clique table generation when the solve exit early. Note that we cannot + // do this in the destructor since we store the clique_table as a pointer. scope_guard cliques_scope([&]() { signal_extend_cliques_.store(true, std::memory_order_release); #pragma omp taskwait depend(in : *clique_signal) @@ -3741,6 +3744,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; + lp_settings.concurrent_halt = settings_.concurrent_halt; + lp_settings.inside_mip = 2; + for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { if (toc(exploration_stats_.start_time) >= settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 5c6bba3481..17ebdafa33 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -387,15 +387,13 @@ class branch_and_bound_t { submip_stats_t& submip_stats, f_t fixrate, i_t simplex_iter_used, - bool is_root_heuristic = false, - std::atomic* halt = nullptr); + simplex::simplex_solver_settings_t submip_settings); // Creates and solves the RINS/RENS sub-MIP. void recursive_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, - bool is_root_heuristic = false, - std::atomic* halt = nullptr); + simplex::simplex_solver_settings_t submip_settings); void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index c7fe06ed4a..b72761f222 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -110,6 +110,7 @@ struct simplex_solver_settings_t { reliability_branching(-1), inside_mip(0), inside_submip(0), + inside_root_node(0), solution_callback(nullptr), heuristic_preemption_callback(nullptr), dual_simplex_objective_callback(nullptr), @@ -228,7 +229,8 @@ struct simplex_solver_settings_t { i_t reliability_branching; i_t inside_mip; // 0 if outside MIP, 1 if inside MIP at root node, 2 if inside MIP at leaf node - i_t inside_submip; // 0 if in regular MIP solve, 1 if in sub-MIP solve + i_t inside_submip; // 0 if in regular MIP solve, 1 if in sub-MIP solve + i_t inside_root_node; // 1 if this is running during the root node, 0 otherwise // Settings for the recursive sub-MIP mip_submip_hyper_params_t submip_settings; diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index 097fbd7107..eac8cb954f 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -32,6 +32,7 @@ struct cut_pass_heuristics_t { Arow_(Arow), root_solution_(root_solution), root_edge_norm_(root_edge_norm), + halt_(false), submip_worker_(nullptr) {}; ~cut_pass_heuristics_t() { stop_and_sync(); } @@ -109,6 +110,8 @@ struct root_heuristics_t { for (auto& heuristic : cut_passes_heuristics_) { heuristic->stop_and_sync(); } + + cut_passes_heuristics_.clear(); } std::shared_ptr> create_new_cut_pass_heuristic(