From d1564fa2102fa9474d54a78eaeee9a73fe7320df Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 10 Aug 2026 16:02:23 +0200 Subject: [PATCH 01/29] allow RINS and CPU FJ to run during the cut passes. fixed objective from submip. Signed-off-by: Nicolas L. Guidotti --- .../mip/submip_hyper_params.hpp | 5 + cpp/src/branch_and_bound/branch_and_bound.cpp | 339 ++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 20 +- cpp/src/branch_and_bound/worker.hpp | 2 + cpp/src/dual_simplex/solve.cpp | 8 + cpp/src/dual_simplex/solve.hpp | 3 + .../mip_heuristics/feasibility_jump/fj_cpu.cu | 12 +- .../feasibility_jump/fj_cpu_worker.cuh | 5 +- cpp/src/mip_heuristics/root_heuristics.hpp | 57 +++ 9 files changed, 296 insertions(+), 155 deletions(-) create mode 100644 cpp/src/mip_heuristics/root_heuristics.hpp diff --git a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp index 505a3b61ba..4464a156a7 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp @@ -41,6 +41,11 @@ struct mip_submip_hyper_params_t { // number of simplex iteration from the parent B&B. f_t iteration_limit_ratio = 0.8; + // If there is not enough variables fixed or we already found an improving solution, + // perform a short DFS to quickly find a feasible solution. This setting controls + // the maximum number of nodes allow for backtracking. + i_t dfs_max_backtrack = 5; + // Run CPU FJ over the sub-MIP bool enable_cpufj = true; }; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index fd7987ae1c..3f94284e77 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -654,13 +655,12 @@ void branch_and_bound_t::set_solution_from_cpu_fj(f_t obj, // user space. template void branch_and_bound_t::set_solution_from_submip( - const std::vector& solution, - const third_party_presolve_t& presolver, - f_t fixrate, - f_t obj) + const std::vector& solution, const third_party_presolve_t& presolver, f_t fixrate) { std::vector leaf_sol; presolver.uncrush_primal_solution(solution, leaf_sol); + f_t obj = compute_objective(original_lp_, leaf_sol); + std::vector user_sol; mutex_original_lp_.lock(); uncrush_primal_solution(original_problem_, original_lp_, leaf_sol, user_sol); @@ -2177,19 +2177,35 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so if (!incumbent_.has_incumbent) return false; if (rins_worker_pool_.num_idle() == 0) return false; + bool is_root_heuristic = false; diving_worker_t* worker = rins_worker_pool_.pop_idle_worker(); if (!worker) return false; + std::vector current_incumbent; + mutex_upper_.lock(); + current_incumbent = incumbent_.x; + mutex_upper_.unlock(); + + // Note that this node does not have the vstatus (it was clear at the start of B&B exploration) + worker->start_node = mip_node_t(root_objective_, root_vstatus_); + worker->leaf_vstatus = root_vstatus_; + worker->leaf_problem.lower = original_lp_.lower; + worker->leaf_problem.upper = original_lp_.upper; + worker->leaf_solution.x = sol; + worker->recompute_bounds = false; + worker->recompute_basis = true; + worker->set_active(); worker->search_strategy = search_strategy_t::SUBMIP; if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - rins(worker, sol); + rins(worker, current_incumbent, is_root_heuristic); } else { -#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) firstprivate(worker, sol) - rins(worker, sol); +#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ + firstprivate(worker, current_incumbent) + rins(worker, current_incumbent, is_root_heuristic); } return true; @@ -2201,7 +2217,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix) + std::string_view log_prefix, + bool is_root_heuristic) { double start_time = tic(); @@ -2219,8 +2236,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke return; } - f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); - f_t user_obj = compute_user_objective(original_lp_, upper_bound_.load()); + 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; @@ -2233,10 +2250,15 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.inside_submip = 1; submip_settings.strong_branching_simplex_iteration_limit = 50; submip_settings.submip_settings.level = submip_level; - submip_settings.log.log = false; submip_settings.benchmark_info_ptr = nullptr; #ifdef DEBUG_SUBMIP + submip_settings.log.log = true; +#else + submip_settings.log.log = false; +#endif + +#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), @@ -2257,7 +2279,10 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.submip_settings.rins = settings_.submip_settings.rins != 0 && submip_level <= settings_.submip_settings.max_level; - submip_settings.log.debug_format( +#ifdef DEBUG_SUBMIP + submip_settings.log.print_format( + "Sub-MIP: num variables fixed={}/{} ({:.2f}%)", num_var_fixed, num_integers, fixrate * 100); + submip_settings.log.print_format( "Sub-MIP solve settings: time_limit={:.2f}, node_limit={}, iter_limit={} (current_iter={}), " "tol={:g}", submip_settings.time_limit, @@ -2265,6 +2290,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.branch_and_bound_simplex_iteration_limit, exploration_stats_.total_simplex_iters.load(), submip_settings.relative_mip_gap_tol); +#endif // The `worker->leaf_problem` is directly converted to an `user_problem_t`, meaning that // there is only equality rows (the range row vector is empty) and it contains @@ -2288,13 +2314,14 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke // Also handle optimal if (submip_problem.num_rows == 0 || submip_problem.num_cols == 0) { - submip_settings.log.debug_format( +#ifdef DEBUG_SUBMIP + submip_settings.log.print_format( "Sub-MIP presolved to a trivial {} x {} problem; solving by bound pushing", submip_problem.num_rows, submip_problem.num_cols); +#endif std::vector reduced_sol(submip_problem.num_cols); - f_t obj = 0.0; for (i_t j = 0; j < submip_problem.num_cols; ++j) { const f_t c = submip_problem.objective[j]; @@ -2306,11 +2333,9 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke } else { reduced_sol[j] = std::isfinite(l) ? l : (std::isfinite(u) ? u : 0); } - - obj += reduced_sol[j] * c; } - set_solution_from_submip(reduced_sol, presolver, fixrate, obj); + set_solution_from_submip(reduced_sol, presolver, fixrate); return; } @@ -2319,13 +2344,15 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.set_simplex_solution_callback = nullptr; submip_settings.solution_callback = [this, &presolver, fixrate](const std::vector& solution, f_t obj) { - this->set_solution_from_submip(solution, presolver, fixrate, obj); + this->set_solution_from_submip(solution, presolver, fixrate); }; - submip_settings.log.debug_format("Sub-MIP: {} constraints, {} variables, {} nonzeros\n", +#ifdef DEBUG_SUBMIP + submip_settings.log.print_format("Sub-MIP: {} constraints, {} variables, {} nonzeros\n", submip_problem.num_rows, submip_problem.num_cols, submip_problem.A.nnz()); +#endif probing_implied_bound_t empty_probing(submip_problem.num_cols); branch_and_bound_t submip_bnb(submip_problem, submip_settings, tic(), empty_probing); @@ -2338,24 +2365,24 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke presolver.crush_primal_solution(submip_problem, current_incumbent, presolved_incumbent); submip_bnb.set_initial_guess(presolved_incumbent); - const f_t user_upper = compute_user_objective(original_lp_, upper_bound_.load()); - const f_t submip_cutoff = - user_upper / submip_bnb.original_lp_.obj_scale - submip_bnb.original_lp_.obj_constant; + const f_t user_upper = compute_user_objective(worker->leaf_problem, upper_bound_.load()); + const f_t submip_cutoff = compute_solver_objective(submip_bnb.original_lp_, user_upper); submip_bnb.set_initial_upper_bound(submip_cutoff); - submip_bnb.set_initial_pseudocost(pc_, presolver.get_reduced_to_original_map()); + if (!is_root_heuristic) + submip_bnb.set_initial_pseudocost(pc_, presolver.get_reduced_to_original_map()); if (submip_halt_callback_) { // Copy the halt callback to the deeper level. 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](f_t, f_t submip_lower_bound) { + submip_bnb.set_submip_halt_callback([this, worker](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; + return is_cutoff || !is_solver_running || worker->halt; }); } @@ -2393,13 +2420,15 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke mip_status_t submip_status = submip_bnb.solve(submip_solution); f_t submip_time = toc(start_time); - submip_settings.log.debug_format( +#ifdef DEBUG_SUBMIP + submip_settings.log.print_format( "Sub-MIP: status={}, iterations={} (total={}), presolve_time={:.2f}, total_time={:.2f} \n", mip_status_to_string(submip_status), submip_solution.simplex_iterations, exploration_stats_.total_simplex_iters.load(), presolve_time, submip_time); +#endif if (submip_status == mip_status_t::NUMERICAL) { return; } if (submip_status == mip_status_t::INFEASIBLE || submip_status == mip_status_t::UNBOUNDED) { @@ -2408,7 +2437,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke } if (submip_solution.has_incumbent) { - set_solution_from_submip(submip_solution.x, presolver, fixrate, submip_solution.objective); + set_solution_from_submip(submip_solution.x, presolver, fixrate); } // Accumulate simplex iterations to determine when to stop exploring the sub-MIP @@ -2543,11 +2572,12 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings } template -void branch_and_bound_t::rins(diving_worker_t* rins_worker, - const std::vector& node_solution) +void branch_and_bound_t::rins(diving_worker_t* worker, + const std::vector& current_incumbent, + bool is_root_heuristic) { raft::common::nvtx::range scope("BB::rins_thread"); - if (rins_worker->orbital_fixing) { rins_worker->orbital_fixing->disable(); } + if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } i_t submip_level = settings_.submip_settings.level + 1; std::string log_prefix = std::format("[RINS {}] ", submip_level); @@ -2558,35 +2588,21 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; branch_and_bound_stats_t rins_stats; + mip_node_t& node = worker->start_node; + std::vector& lower = worker->leaf_problem.lower; + std::vector& upper = worker->leaf_problem.upper; + std::vector& bounds_changed = worker->bounds_changed; + std::vector& current_sol = worker->leaf_solution.x; - // Note that this node does not have the vstatus (it was clear at the start of B&B exploration) - mip_node_t node = search_tree_.root.detach_copy(); - rins_worker->leaf_vstatus = root_vstatus_; - rins_worker->leaf_problem.lower = original_lp_.lower; - rins_worker->leaf_problem.upper = original_lp_.upper; - rins_worker->leaf_solution.x = node_solution; - rins_worker->recompute_bounds = false; - rins_worker->recompute_basis = true; - - std::vector& lower = rins_worker->leaf_problem.lower; - std::vector& upper = rins_worker->leaf_problem.upper; - std::vector& bounds_changed = rins_worker->bounds_changed; - std::vector& current_sol = rins_worker->leaf_solution.x; std::vector fractional; i_t num_frac = fractional_variables(settings_, current_sol, var_types_, fractional); - std::vector current_incumbent; - mutex_upper_.lock(); - current_incumbent = incumbent_.x; - mutex_upper_.unlock(); - std::vector integer_list; get_unfixed_integer_variables(lower, upper, var_types_, settings_.fixed_tol, integer_list); i_t num_integers = integer_list.size(); - f_t max_fixrate = - submip_get_max_fixrate(rins_stats_, settings_.submip_settings, rins_worker->rng); + f_t max_fixrate = submip_get_max_fixrate(rins_stats_, settings_.submip_settings, worker->rng); f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); i_t max_var_fixed = max_fixrate * num_integers; i_t min_var_fixed = min_fixrate * num_integers; @@ -2608,11 +2624,13 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, // Enough variables has been fixed if (num_var_fixed >= min_var_fixed) { - settings_.log.debug_format("{}Fixed {} variables (max={}, min={})\n", +#ifdef DEBUG_SUBMIP + settings_.log.print_format("{}Fixed {} variables (max={}, min={})\n", log_prefix, num_var_fixed, max_var_fixed, min_var_fixed); +#endif has_submip = true; break; } @@ -2637,11 +2655,13 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, // Enough variables were fixed if (num_var_fixed >= min_var_fixed) { - settings_.log.debug_format("{}Fixed {} variables (max={}, min={})\n", +#ifdef DEBUG_SUBMIP + settings_.log.print_format("{}Fixed {} variables (max={}, min={})\n", log_prefix, num_var_fixed, max_var_fixed, min_var_fixed); +#endif has_submip = true; break; } @@ -2651,7 +2671,7 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, // an integer solution first in order to reach the fixing threshold. if (prev_num_fixed == num_var_fixed) { extend_variable_fixings(settings_, - rins_worker->leaf_problem.objective, + worker->leaf_problem.objective, fractional, current_sol, root_relax_soln_.x, @@ -2662,21 +2682,25 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, num_var_fixed); if (num_var_fixed >= min_var_fixed) { - settings_.log.debug_format("{}Fixed {} variables (max={}, min={})\n", +#ifdef DEBUG_SUBMIP + settings_.log.print_format("{}Fixed {} variables (max={}, min={})\n", log_prefix, num_var_fixed, max_var_fixed, min_var_fixed); +#endif has_submip = true; break; } if (prev_num_fixed == num_var_fixed) { - settings_.log.debug_format("{}Could not fix more variables ({}, max={}, min={})\n", +#ifdef DEBUG_SUBMIP + settings_.log.print_format("{}Could not fix more variables ({}, max={}, min={})\n", log_prefix, num_var_fixed, max_var_fixed, min_var_fixed); +#endif has_submip = true; break; } @@ -2693,14 +2717,14 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, // We continue to do this until enough variables were fixed or no variable is left to fix. logger_t log; log.log = false; - dual_status_t lp_status = solve_node_lp(&node, rins_worker, rins_stats, log); + dual_status_t lp_status = solve_node_lp(&node, worker, rins_stats, log); if (lp_status != dual_status_t::OPTIMAL) { break; } fractional.clear(); num_frac = fractional_variables(settings_, current_sol, var_types_, fractional); - f_t leaf_obj = compute_objective(rins_worker->leaf_problem, current_sol); + f_t leaf_obj = compute_objective(worker->leaf_problem, current_sol); node.lower_bound = leaf_obj; snap_to_lattice(&node, leaf_obj); @@ -2712,7 +2736,7 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, break; } - rins_worker->recompute_basis = false; + worker->recompute_basis = false; } f_t fixrate = (f_t)num_var_fixed / num_integers; @@ -2723,14 +2747,11 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, // levels up to try to find a feasible solution quickly from the neighbourhood. if (fixrate < settings_.submip_settings.min_fixrate_cap || (settings_.inside_submip && rins_stats_.total_success != 0)) { - // We need to re-populate the vstatus of the node since it was previously cleared. - rins_worker->start_node = std::move(node); - rins_worker->start_node.packed_vstatus = simplex::compress_vstatus(rins_worker->leaf_vstatus); - - rins_worker->start_lower = lower; - rins_worker->start_upper = upper; + worker->start_node.packed_vstatus = simplex::compress_vstatus(worker->leaf_vstatus); + worker->start_lower = lower; + worker->start_upper = upper; - bool is_feasible = rins_worker->presolve_start_bounds(settings_); + bool is_feasible = worker->presolve_start_bounds(settings_); if (is_feasible) { fj_cpu_worker_t submip_fj_cpu_worker; @@ -2743,21 +2764,33 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, 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(rins_worker->leaf_problem, + submip_fj_cpu_worker.create_worker(worker->leaf_problem, var_types_, - rins_worker->leaf_solution.x, + worker->leaf_solution.x, settings_, std::format("{} [CPU FJ]", log_prefix), - rins_worker->rng.next_i64()); + worker->rng.next_i64()); submip_fj_cpu_worker.run_sync(time_limit, work_limit); } - dive_with(rins_worker, 5); + // We need the pseudocost to do the DFS, which we do not have during the cut passes. + if (!is_root_heuristic) { +#ifdef DEBUG_SUBMIP + settings_.log.print_format("{} Running a quick DFS for the submip!", log_prefix); +#endif + + dive_with(worker, settings_.submip_settings.dfs_max_backtrack); + } } } else { - solve_submip( - rins_worker, current_incumbent, num_var_fixed, num_integers, submip_level, log_prefix); + solve_submip(worker, + current_incumbent, + num_var_fixed, + num_integers, + submip_level, + log_prefix, + is_root_heuristic); } } @@ -2766,7 +2799,8 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, exploration_stats_.total_simplex_iters += rins_stats.total_simplex_iters; } - settings_.log.debug_format( +#ifdef DEBUG_SUBMIP + settings_.log.print_format( "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " "min_fixrate={:.4g} ({})\n", log_prefix, @@ -2779,8 +2813,68 @@ void branch_and_bound_t::rins(diving_worker_t* rins_worker, max_var_fixed, min_fixrate, min_var_fixed); +#endif - rins_worker_pool_.return_worker_to_pool(rins_worker); + if (!is_root_heuristic) { + rins_worker_pool_.return_worker_to_pool(worker); + } else { + worker->set_inactive(); + } +} + +template +void branch_and_bound_t::launch_root_heuristics( + const lp_problem_t& lp, + const std::vector& sol, + std::list>& heuristics, + omp_atomic_t* worker_count) +{ + assert(worker_count); + if (settings_.deterministic || *worker_count >= settings_.num_threads - 1) return; + + bool is_root_heuristic = true; + i_t id = heuristics.size(); + root_heuristics_t& heuristic = heuristics.emplace_back(Arow_, var_types_); + + if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent && + (*worker_count < settings_.num_threads - 1)) { + heuristic.create_submip_worker(id, lp, settings_, root_objective_, root_vstatus_, sol); + diving_worker_t* worker = heuristic.submip_worker_.get(); + + std::vector current_incumbent; + mutex_upper_.lock(); + current_incumbent = incumbent_.x; + mutex_upper_.unlock(); + + if (settings_.inside_submip) { + // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy + // function for included tasks. + rins(worker, current_incumbent, is_root_heuristic); + } else { + ++(*worker_count); +#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ + firstprivate(worker, current_incumbent, worker_count) depend(out : *worker) + { + rins(worker, current_incumbent, is_root_heuristic); + --(*worker_count); + } + } + } + + constexpr bool is_cpufj_enabled = true; + if (is_cpufj_enabled && (*worker_count < settings_.num_threads - 1)) { + fj_cpu_worker_t& worker = heuristic.fj_cpu_worker_; + + f_t work_limit = 2.0; + f_t time_limit = settings_.time_limit - toc(exploration_stats_.start_time); + + worker.improvement_callback = + [this](f_t obj, const std::vector& assignment, double work_units) { + set_solution_from_cpu_fj(obj, assignment, work_units); + }; + worker.create_worker(lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); + worker.run_async(time_limit, work_limit, worker_count); + } } template @@ -3460,12 +3554,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut compute_user_objective(original_lp_, root_relax_objective); } - constexpr bool enable_root_cut_cpufj = true; - fj_cpu_worker_t root_fj_cpu_worker; - root_fj_cpu_worker.improvement_callback = - [this](f_t obj, const std::vector& assignment, double work_units) { - set_solution_from_cpu_fj(obj, assignment, work_units); - }; + omp_atomic_t root_worker_count = 0; + std::list> root_heuristics; + launch_root_heuristics(original_lp_, root_relax_soln_.x, root_heuristics, &root_worker_count); f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; @@ -3498,8 +3589,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } cut_pass_result_t cut_pass_result; - root_fj_cpu_worker.run_async(settings_.time_limit - toc(exploration_stats_.start_time)); - cut_pass_result = do_cut_pass(cut_pass, solution, num_fractional, @@ -3518,7 +3607,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_objective, cut_pool_size, saved_solution); - root_fj_cpu_worker.stop(); + + mutex_upper_.lock(); + if (incumbent_.has_incumbent && incumbent_.x.size() != original_lp_.num_cols) { + std::vector uncrushed_incumbent; + uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, uncrushed_incumbent); + crush_primal_solution( + original_problem_, original_lp_, uncrushed_incumbent, new_slacks_, incumbent_.x); + } + mutex_upper_.unlock(); if (cut_pass_result.action == cut_pass_action_t::RETURN) { if (settings_.benchmark_info_ptr != nullptr) { @@ -3530,15 +3627,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (cut_pass_result.action == cut_pass_action_t::BREAK) { break; } - if (enable_root_cut_cpufj && !settings_.deterministic && settings_.num_threads >= 2 && - cut_pass + 1 < settings_.max_cut_passes) { - f_t root_cut_cpufj_build_start_time = tic(); - root_fj_cpu_worker.create_worker( - original_lp_, var_types_, root_relax_soln_.x, settings_, "[RootCut CPUFJ] "); - settings_.log.debug("Root cut CPUFJ problem build time after pass %d: %.6f seconds\n", - cut_pass, - toc(root_cut_cpufj_build_start_time)); - } + launch_root_heuristics(original_lp_, root_relax_soln_.x, root_heuristics, &root_worker_count); } // Publish the post-cuts root LP value. @@ -3554,18 +3643,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.benchmark_info_ptr->cut_generation_time_sec = cut_generation_time; } if (cut_info.has_cuts()) { - // If the incumbent is set before or during the cut passes, it may not have the correct - // dimensions as cuts add additional constraints/variables to `original_lp_`. - mutex_upper_.lock(); - if (incumbent_.has_incumbent && incumbent_.x.size() != original_lp_.num_cols) { - std::vector uncrushed_incumbent; - uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, uncrushed_incumbent); - crush_primal_solution( - original_problem_, original_lp_, uncrushed_incumbent, new_slacks_, incumbent_.x); - } - - mutex_upper_.unlock(); - settings_.log.printf("Cut generation time: %.2f seconds\n", cut_generation_time); settings_.log.printf("Cut pool size : %d\n", cut_pool_size); settings_.log.printf("Size with cuts : %d constraints, %d variables, %d nonzeros\n", @@ -3576,30 +3653,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.printf("\n"); } - if (enable_root_cut_cpufj && cut_info.has_cuts()) { - f_t root_cut_cpufj_build_start_time = tic(); - // In deterministic mode this CPUFJ is built on the B&B task while the LS deterministic - // CPUFJ is being built on the main thread; both would otherwise race on the global - // seed_generator and pick non-reproducible seeds. Pin a stable seed here so this - // climber's behavior depends only on settings_.random_seed. - int64_t root_cut_cpufj_seed = - settings_.deterministic ? static_cast(settings_.random_seed) : -1; - root_fj_cpu_worker.create_worker(original_lp_, - var_types_, - root_relax_soln_.x, - settings_, - "[RootCut CPUFJ] ", - root_cut_cpufj_seed); - settings_.log.debug("Root cut CPUFJ final problem build time: %.6f seconds\n", - toc(root_cut_cpufj_build_start_time)); - f_t remaining_time = f_t(settings_.time_limit - toc(exploration_stats_.start_time)); - // Reserve at least half of the remaining time for B&B exploration; cap absolute spend - // at 1s so generous budgets don't grant CPUFJ more than the historical ceiling. - f_t fj_time_limit = - settings_.deterministic ? remaining_time : std::min(remaining_time * 0.5, 1.0); - root_fj_cpu_worker.run_sync(fj_time_limit, 0.5); - } - set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); @@ -3879,23 +3932,22 @@ Producer Sync: Producing solutions in the past would break determinism, therefore this unidirectional sync ensures no such thing can occur. Instrumentation Aggregator: Collects multiple instrument vectors into a single aggregation point for estimating work from memory operations. Worker Context: Object -representing the "context" (e.g.: the worker) that should register the amount of work recorded There -is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure they -remained synchronized together. Queued Integer Solutions: New integer solutions found within -horizons are queued with a work unit timestamp, in order to be sorted and played in order during the -sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is used -to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently +representing the "context" (e.g.: the worker) that should register the amount of work recorded +There is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure +they remained synchronized together. Queued Integer Solutions: New integer solutions found within +horizons are queued with a work unit timestamp, in order to be sorted and played in order during +the sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is +used to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently nondeterministic. To fix this, in deterministic mode, nodes are addressed by a tuple - where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential ID -local to the worker.\ This sequential ID is similar in principle to the global atomic ID sequence of -the nondeterminsitic mode but since it is local to each worker, it is updated serially and thus is -deterministic. worker IDs are unique, and sequence IDs are unique to their workers, therefor - is a globally unique node identifier. -Pseudocost Update: - Each worker updates its local pseudocosts when branching. These updates are queued within -horizons. During the horizon sync, these updates are all played in order, and the newly updated -global pseudocosts are broadcast to the worker's pseudocost snapshots for the coming horizon. + where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential +ID local to the worker.\ This sequential ID is similar in principle to the global atomic ID +sequence of the nondeterminsitic mode but since it is local to each worker, it is updated serially +and thus is deterministic. worker IDs are unique, and sequence IDs are unique to their workers, +therefor is a globally unique node identifier. Pseudocost Update: Each worker +updates its local pseudocosts when branching. These updates are queued within horizons. During the +horizon sync, these updates are all played in order, and the newly updated global pseudocosts are +broadcast to the worker's pseudocost snapshots for the coming horizon. */ @@ -4005,7 +4057,8 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri "Sync%% | NoWork\n"); settings_.log.printf( " " - "-------+---------+----------+--------+---------+--------+----------+----------+-------+-------" + "-------+---------+----------+--------+---------+--------+----------+----------+-------+-----" + "--" "\n"); for (const auto& worker : *deterministic_workers_) { double sync_time = worker.work_context.total_sync_time; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 96b8a6d8fe..4454f33488 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -34,12 +34,14 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -370,8 +372,7 @@ class branch_and_bound_t { bool launch_rins_worker(const std::vector& sol); void set_solution_from_submip(const std::vector& solution, const third_party_presolve_t& presolver, - f_t fixrate, - f_t obj); + f_t fixrate); // Solve the RINS sub-MIP void solve_submip(diving_worker_t* worker, @@ -379,13 +380,18 @@ class branch_and_bound_t { i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix); + std::string_view log_prefix, + bool is_root_heuristic); // Creates and solves the RINS sub-MIP - void rins(diving_worker_t* rins_worker, const std::vector& node_solution); - - // Get the simplex settings for solving the LP of a single node - simplex::simplex_solver_settings_t get_node_lp_settings(); + void rins(diving_worker_t* worker, + const std::vector& current_incumbent, + bool is_root_heuristic); + + void launch_root_heuristics(const simplex::lp_problem_t& lp, + const std::vector& sol, + std::list>& heuristics, + omp_atomic_t* worker_count); // Solve the LP relaxation of a leaf node simplex::dual_status_t solve_node_lp(mip_node_t* node_ptr, diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index fd5255f86f..f40dcb71b6 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -243,6 +243,8 @@ 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 { diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 7907abd3b9..11ace52bd2 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -104,6 +104,12 @@ f_t compute_user_objective(const lp_problem_t& lp, f_t obj) return user_obj; } +template +f_t compute_solver_objective(const lp_problem_t& lp, f_t user_obj) +{ + return user_obj / lp.obj_scale - lp.obj_constant; +} + template lp_status_t solve_linear_program_advanced(const lp_problem_t& original_lp, const f_t start_time, @@ -813,6 +819,8 @@ template double compute_user_objective(const lp_problem_t& lp, double obj); +template double compute_solver_objective(const lp_problem_t& lp, double user_obj); + template lp_status_t solve_linear_program_advanced( const lp_problem_t& original_lp, const double start_time, diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 90c2dbd690..d80ec8e5d6 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -63,6 +63,9 @@ f_t compute_user_objective(const lp_problem_t& lp, const std::vector f_t compute_user_objective(const lp_problem_t& lp, f_t obj); +template +f_t compute_solver_objective(const lp_problem_t& lp, f_t user_obj); + template lp_status_t solve_linear_program_advanced(const lp_problem_t& original_lp, const f_t start_time, diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index 5d9b0267b1..4e830d7467 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -2112,13 +2112,19 @@ void fj_cpu_worker_t::create_worker( } template -void fj_cpu_worker_t::run_async(f_t time_limit, double work_unit_limit) +void fj_cpu_worker_t::run_async(f_t time_limit, + double work_unit_limit, + omp_atomic_t* worker_count) { if (!fj_cpu) return; -#pragma omp task shared(fj_cpu) firstprivate(time_limit, work_unit_limit) \ + if (worker_count) ++(*worker_count); +#pragma omp task shared(fj_cpu) firstprivate(time_limit, work_unit_limit, worker_count) \ priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : *fj_cpu) - cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); + { + cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); + if (worker_count) --(*worker_count); + } } template diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh index ff6022c4c2..0140347983 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh @@ -47,8 +47,9 @@ struct fj_cpu_worker_t { // Run the worker asynchronously (i.e., launch an openmp task and then continue the // execution). Call `stop()` for stopping the worker - void run_async(f_t time_limit = std::numeric_limits::infinity(), - double work_unit_limit = std::numeric_limits::infinity()); + void run_async(f_t time_limit = std::numeric_limits::infinity(), + double work_unit_limit = std::numeric_limits::infinity(), + omp_atomic_t* worker_count = nullptr); // Run the CPU FJ synchronously (i.e., wait for it to finish before proceeding) void run_sync(f_t time_limit = std::numeric_limits::infinity(), diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp new file mode 100644 index 0000000000..25838872ed --- /dev/null +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -0,0 +1,57 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include "feasibility_jump/fj_cpu_worker.cuh" + +namespace cuopt::mathematical_optimization::mip { + +template +struct root_heuristics_t { + std::unique_ptr> submip_worker_; + fj_cpu_worker_t fj_cpu_worker_; + std::vector var_types_; + csr_matrix_t Arow_; + + root_heuristics_t(const csr_matrix_t& Arow, + const std::vector& var_types) + : submip_worker_(nullptr), var_types_(var_types), Arow_(Arow) {}; + + ~root_heuristics_t() { stop(); } + + void stop() + { + fj_cpu_worker_.stop(); + if (submip_worker_) { + submip_worker_->halt = true; + diving_worker_t* worker = submip_worker_.get(); +#pragma omp taskwait depend(in : *worker) + } + } + + void create_submip_worker(i_t id, + const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + f_t root_obj, + const std::vector& root_vstatus, + const std::vector& sol) + { + submip_worker_ = + std::make_unique>(id, lp, Arow_, var_types_, settings); + submip_worker_->start_node = mip_node_t(root_obj, root_vstatus); + submip_worker_->leaf_vstatus = root_vstatus; + submip_worker_->leaf_solution.x = sol; + submip_worker_->recompute_bounds = false; + submip_worker_->recompute_basis = true; + submip_worker_->search_strategy = search_strategy_t::SUBMIP; + submip_worker_->set_active(); + } +}; +} // namespace cuopt::mathematical_optimization::mip From e0bc53b6a213beabd675beb1423d1c7103dc6a63 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 11 Aug 2026 10:57:56 +0200 Subject: [PATCH 02/29] silent postsolve messages for submip Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 3 ++- cpp/src/mip_heuristics/presolve/third_party_presolve.cpp | 6 +++--- cpp/src/mip_heuristics/presolve/third_party_presolve.hpp | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3f94284e77..0395dcc1c1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -657,8 +657,9 @@ template void branch_and_bound_t::set_solution_from_submip( const std::vector& solution, const third_party_presolve_t& presolver, f_t fixrate) { + bool check_postsolve = false; std::vector leaf_sol; - presolver.uncrush_primal_solution(solution, leaf_sol); + presolver.uncrush_primal_solution(solution, leaf_sol, check_postsolve); f_t obj = compute_objective(original_lp_, leaf_sol); std::vector user_sol; diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 3d324d2007..3d5b165abb 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -1293,9 +1293,9 @@ void third_party_presolve_t::undo(std::vector& primal_solution, template void third_party_presolve_t::uncrush_primal_solution( - const std::vector& reduced_primal, std::vector& full_primal) const + const std::vector& reduced_primal, std::vector& full_primal, bool check_postsolve) const { - if (presolver_ == cuopt::mathematical_optimization::presolver_t::PSLP) { + if (presolver_ == PSLP) { cuopt_expects(false, error_type_t::RuntimeError, "This code path should be never called, as this is meant for callbacks and they " @@ -1311,7 +1311,7 @@ void third_party_presolve_t::uncrush_primal_solution( bool is_optimal = false; auto status = post_solver.undo(reduced_sol, full_sol, *papilo_post_solve_storage_, is_optimal); - check_postsolve_status(status); + if (check_postsolve) check_postsolve_status(status); full_primal = std::move(full_sol.primal); } diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp index 60f4f7fc6d..79eb28d01e 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.hpp @@ -141,7 +141,8 @@ class third_party_presolve_t { bool dual_postsolve); void uncrush_primal_solution(const std::vector& reduced_primal, - std::vector& full_primal) const; + std::vector& full_primal, + bool check_postsolve = true) const; void crush_primal_solution(const optimization_problem_t& reduced_problem, const std::vector& original_primal, From ea154a90f2dbf806f64c3cfb6d1cb13077f30926 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 11 Aug 2026 14:36:17 +0200 Subject: [PATCH 03/29] address coderabbit comments Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 38 ++++++++++--------- cpp/src/branch_and_bound/branch_and_bound.hpp | 4 +- .../feasibility_jump/fj_cpu_worker.cuh | 2 + 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 0395dcc1c1..10c978ec40 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -655,16 +655,19 @@ void branch_and_bound_t::set_solution_from_cpu_fj(f_t obj, // user space. template void branch_and_bound_t::set_solution_from_submip( - const std::vector& solution, const third_party_presolve_t& presolver, f_t fixrate) + const lp_problem_t& lp, + const std::vector& solution, + const third_party_presolve_t& presolver, + f_t fixrate) { bool check_postsolve = false; std::vector leaf_sol; presolver.uncrush_primal_solution(solution, leaf_sol, check_postsolve); - f_t obj = compute_objective(original_lp_, leaf_sol); + f_t obj = compute_objective(lp, leaf_sol); std::vector user_sol; mutex_original_lp_.lock(); - uncrush_primal_solution(original_problem_, original_lp_, leaf_sol, user_sol); + uncrush_primal_solution(original_problem_, lp, leaf_sol, user_sol); mutex_original_lp_.unlock(); settings_.log.debug_format("SubMIP found a feasible solution with obj={:.4g}", obj); bool success = set_solution_from_heuristics(user_sol, heuristics_origin_t::SUBMIP); @@ -2202,11 +2205,11 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - rins(worker, current_incumbent, is_root_heuristic); + rins(worker, current_incumbent, var_types_, is_root_heuristic); } else { #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(worker, current_incumbent) - rins(worker, current_incumbent, is_root_heuristic); + rins(worker, current_incumbent, var_types_, is_root_heuristic); } return true; @@ -2336,16 +2339,16 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke } } - set_solution_from_submip(reduced_sol, presolver, fixrate); + set_solution_from_submip(worker->leaf_problem, reduced_sol, presolver, fixrate); return; } submip_settings.heuristic_preemption_callback = nullptr; submip_settings.dual_simplex_objective_callback = nullptr; submip_settings.set_simplex_solution_callback = nullptr; - submip_settings.solution_callback = [this, &presolver, fixrate](const std::vector& solution, - f_t obj) { - this->set_solution_from_submip(solution, presolver, fixrate); + submip_settings.solution_callback = [this, &presolver, fixrate, worker]( + const std::vector& solution, f_t obj) { + this->set_solution_from_submip(worker->leaf_problem, solution, presolver, fixrate); }; #ifdef DEBUG_SUBMIP @@ -2438,7 +2441,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke } if (submip_solution.has_incumbent) { - set_solution_from_submip(submip_solution.x, presolver, fixrate); + set_solution_from_submip(worker->leaf_problem, submip_solution.x, presolver, fixrate); } // Accumulate simplex iterations to determine when to stop exploring the sub-MIP @@ -2575,6 +2578,7 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings template void branch_and_bound_t::rins(diving_worker_t* worker, const std::vector& current_incumbent, + const std::vector& var_types, bool is_root_heuristic) { raft::common::nvtx::range scope("BB::rins_thread"); @@ -2596,10 +2600,10 @@ void branch_and_bound_t::rins(diving_worker_t* worker, std::vector& current_sol = worker->leaf_solution.x; std::vector fractional; - i_t num_frac = fractional_variables(settings_, current_sol, var_types_, fractional); + i_t num_frac = fractional_variables(settings_, current_sol, var_types, fractional); std::vector integer_list; - get_unfixed_integer_variables(lower, upper, var_types_, settings_.fixed_tol, integer_list); + get_unfixed_integer_variables(lower, upper, var_types, settings_.fixed_tol, integer_list); i_t num_integers = integer_list.size(); @@ -2609,7 +2613,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, i_t min_var_fixed = min_fixrate * num_integers; i_t num_var_fixed = 0; - while (solver_status_ == mip_status_t::UNSET && is_running_) { + while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the // current incumbent, considering only the fractional values in the current node i_t prev_num_fixed = num_var_fixed; @@ -2723,7 +2727,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, if (lp_status != dual_status_t::OPTIMAL) { break; } fractional.clear(); - num_frac = fractional_variables(settings_, current_sol, var_types_, fractional); + num_frac = fractional_variables(settings_, current_sol, var_types, fractional); f_t leaf_obj = compute_objective(worker->leaf_problem, current_sol); node.lower_bound = leaf_obj; @@ -2850,13 +2854,13 @@ 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. - rins(worker, current_incumbent, is_root_heuristic); + rins(worker, current_incumbent, heuristic.var_types_, is_root_heuristic); } else { ++(*worker_count); #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ - firstprivate(worker, current_incumbent, worker_count) depend(out : *worker) + firstprivate(worker, current_incumbent, worker_count) shared(heuristic) depend(out : *worker) { - rins(worker, current_incumbent, is_root_heuristic); + rins(worker, current_incumbent, heuristic.var_types_, is_root_heuristic); --(*worker_count); } } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 4454f33488..5c63991391 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -370,7 +370,8 @@ class branch_and_bound_t { // Launch a new RINS worker bool launch_rins_worker(const std::vector& sol); - void set_solution_from_submip(const std::vector& solution, + void set_solution_from_submip(const simplex::lp_problem_t& lp, + const std::vector& solution, const third_party_presolve_t& presolver, f_t fixrate); @@ -386,6 +387,7 @@ class branch_and_bound_t { // Creates and solves the RINS sub-MIP void rins(diving_worker_t* worker, const std::vector& current_incumbent, + const std::vector& var_types, bool is_root_heuristic); void launch_root_heuristics(const simplex::lp_problem_t& lp, diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh index 0140347983..d594eef8dc 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh @@ -10,6 +10,8 @@ #include #include +#include + #include #include #include From d4a8bd593321a2909771fdb52cb218c8f2e79209 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 11 Aug 2026 17:54:40 +0200 Subject: [PATCH 04/29] address more review comments Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 44 +++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + .../mip_heuristics/feasibility_jump/fj_cpu.cu | 1 + 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 10c978ec40..3270978aa7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2218,6 +2218,7 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so template void branch_and_bound_t::solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, + const std::vector& var_type, i_t num_var_fixed, i_t num_integers, i_t submip_level, @@ -2300,7 +2301,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke // there is only equality rows (the range row vector is empty) and it contains // structural + slacks + cuts constraints/variables. user_problem_t submip_problem(original_problem_.handle_ptr); - simplex::convert_lp_to_user_problem(worker->leaf_problem, var_types_, settings_, submip_problem); + simplex::convert_lp_to_user_problem(worker->leaf_problem, var_type, settings_, submip_problem); third_party_presolve_t presolver; f_t presolve_time_limit = std::min(0.1 * submip_settings.time_limit, 60.0); @@ -2770,7 +2771,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, 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_, + var_types, worker->leaf_solution.x, settings_, std::format("{} [CPU FJ]", log_prefix), @@ -2791,6 +2792,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, } else { solve_submip(worker, current_incumbent, + var_types, num_var_fixed, num_integers, submip_level, @@ -2841,8 +2843,24 @@ void branch_and_bound_t::launch_root_heuristics( i_t id = heuristics.size(); root_heuristics_t& heuristic = heuristics.emplace_back(Arow_, var_types_); - if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent && - (*worker_count < settings_.num_threads - 1)) { + constexpr bool is_cpufj_enabled = true; + if (is_cpufj_enabled) { + fj_cpu_worker_t& worker = heuristic.fj_cpu_worker_; + + f_t work_limit = 2.0; + f_t time_limit = settings_.time_limit - toc(exploration_stats_.start_time); + + worker.improvement_callback = + [this](f_t obj, const std::vector& assignment, double work_units) { + set_solution_from_cpu_fj(obj, assignment, work_units); + }; + worker.create_worker(lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); + worker.run_async(time_limit, work_limit, worker_count); + } + + if (*worker_count >= settings_.num_threads - 1) return; + + if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent) { heuristic.create_submip_worker(id, lp, settings_, root_objective_, root_vstatus_, sol); diving_worker_t* worker = heuristic.submip_worker_.get(); @@ -2862,24 +2880,12 @@ void branch_and_bound_t::launch_root_heuristics( { rins(worker, current_incumbent, heuristic.var_types_, is_root_heuristic); --(*worker_count); + heuristic.Arow_ = csr_matrix_t(1, 1, 1); + heuristic.var_types_ = {}; + heuristic.submip_worker_.reset(); } } } - - constexpr bool is_cpufj_enabled = true; - if (is_cpufj_enabled && (*worker_count < settings_.num_threads - 1)) { - fj_cpu_worker_t& worker = heuristic.fj_cpu_worker_; - - f_t work_limit = 2.0; - f_t time_limit = settings_.time_limit - toc(exploration_stats_.start_time); - - worker.improvement_callback = - [this](f_t obj, const std::vector& assignment, double work_units) { - set_solution_from_cpu_fj(obj, assignment, work_units); - }; - worker.create_worker(lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); - worker.run_async(time_limit, work_limit, worker_count); - } } template diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 5c63991391..743fd02a9c 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -378,6 +378,7 @@ class branch_and_bound_t { // Solve the RINS sub-MIP void solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, + const std::vector& var_type, i_t num_var_fixed, i_t num_integers, i_t submip_level, diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index 4e830d7467..ca96477ab6 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -2123,6 +2123,7 @@ void fj_cpu_worker_t::run_async(f_t time_limit, priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : *fj_cpu) { cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); + fj_cpu.reset(); if (worker_count) --(*worker_count); } } From 9ed4d285d739f3b4a38b9d1ba1c61f159754f966 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 12 Aug 2026 13:34:12 +0200 Subject: [PATCH 05/29] code cleanup Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 63 ++++++++++--------- cpp/src/branch_and_bound/constants.hpp | 28 +++++++-- cpp/src/dual_simplex/solve.cpp | 4 +- cpp/src/dual_simplex/solve.hpp | 2 +- cpp/src/mip_heuristics/root_heuristics.hpp | 2 +- 5 files changed, 61 insertions(+), 38 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3270978aa7..c619390ceb 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -206,6 +206,7 @@ inline char feasible_solution_symbol(heuristics_origin_t origin) inline char feasible_solution_symbol(search_strategy_t strategy, bool show_diving) { if (strategy == search_strategy_t::BEST_FIRST) return 'B'; + if (strategy == search_strategy_t::RINS) return 'S'; if (!show_diving) return 'D'; switch (strategy) { @@ -216,8 +217,10 @@ inline char feasible_solution_symbol(search_strategy_t strategy, bool show_divin case search_strategy_t::GUIDED_DIVING: return 'G'; case search_strategy_t::FARKAS_DIVING: return 'F'; case search_strategy_t::VECTOR_LENGTH_DIVING: return 'V'; - default: return 'U'; + case search_strategy_t::RINS: return 'S'; } + + return 'U'; } template @@ -1022,7 +1025,7 @@ branch_variable_t branch_and_bound_t::variable_selection( case search_strategy_t::VECTOR_LENGTH_DIVING: return vector_length_diving(worker->leaf_problem, fractional, solution, log); - case search_strategy_t::SUBMIP: // This is used for solving the DFS of the sub-MIP. + case search_strategy_t::RINS: // This is used for solving the DFS of the sub-MIP. branch_var = pc_.variable_selection(fractional, solution); round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); return {branch_var, round_dir}; @@ -2117,7 +2120,7 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker, // This is called from the RINS method which already handle the return to the // pool part. Besides, they do not share the same pool. - if (worker->search_strategy != search_strategy_t::SUBMIP) { + if (worker->search_strategy != search_strategy_t::RINS) { diving_worker_pool_.return_worker_to_pool(worker); } } @@ -2198,9 +2201,8 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so worker->leaf_solution.x = sol; worker->recompute_bounds = false; worker->recompute_basis = true; - + worker->search_strategy = search_strategy_t::RINS; worker->set_active(); - worker->search_strategy = search_strategy_t::SUBMIP; if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy @@ -2238,6 +2240,11 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (!feasible) { // This should never happen since we are fixing bounds that are already in the incumbent. rins_stats_.save_infeasible(fixrate); + +#ifdef DEBUG_SUBMIP + settings_.log.print_format("{} The problem is infeasible after running bound strengthening!", + log_prefix); +#endif return; } @@ -2281,8 +2288,8 @@ 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); - submip_settings.submip_settings.rins = - settings_.submip_settings.rins != 0 && submip_level <= settings_.submip_settings.max_level; + bool max_recursion = submip_level > settings_.submip_settings.max_level; + submip_settings.submip_settings.rins = settings_.submip_settings.rins != 0 && !max_recursion; #ifdef DEBUG_SUBMIP submip_settings.log.print_format( @@ -2371,7 +2378,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_bnb.set_initial_guess(presolved_incumbent); const f_t user_upper = compute_user_objective(worker->leaf_problem, upper_bound_.load()); - const f_t submip_cutoff = compute_solver_objective(submip_bnb.original_lp_, user_upper); + const f_t submip_cutoff = compute_internal_objective(submip_bnb.original_lp_, user_upper); submip_bnb.set_initial_upper_bound(submip_cutoff); if (!is_root_heuristic) @@ -2452,9 +2459,9 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke } template -inline f_t submip_get_max_fixrate(const submip_stats_t& stats, - const mip_submip_hyper_params_t& submip_settings, - pcgenerator_t& rng) +f_t submip_get_max_fixrate(const submip_stats_t& stats, + const mip_submip_hyper_params_t& submip_settings, + pcgenerator_t& rng) { // Adaptive fix rate based on previous successes and failures. f_t low = submip_settings.base_target_fixrate; @@ -2738,7 +2745,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, if (num_frac == 0) { // We found a feasible solution when fixing the variables in RINS. - add_feasible_solution(leaf_obj, current_sol, -1, search_strategy_t::SUBMIP); + add_feasible_solution(leaf_obj, current_sol, -1, search_strategy_t::RINS); break; } @@ -3943,22 +3950,23 @@ Producer Sync: Producing solutions in the past would break determinism, therefore this unidirectional sync ensures no such thing can occur. Instrumentation Aggregator: Collects multiple instrument vectors into a single aggregation point for estimating work from memory operations. Worker Context: Object -representing the "context" (e.g.: the worker) that should register the amount of work recorded -There is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure -they remained synchronized together. Queued Integer Solutions: New integer solutions found within -horizons are queued with a work unit timestamp, in order to be sorted and played in order during -the sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is -used to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently +representing the "context" (e.g.: the worker) that should register the amount of work recorded There +is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure they +remained synchronized together. Queued Integer Solutions: New integer solutions found within +horizons are queued with a work unit timestamp, in order to be sorted and played in order during the +sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is used +to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently nondeterministic. To fix this, in deterministic mode, nodes are addressed by a tuple - where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential -ID local to the worker.\ This sequential ID is similar in principle to the global atomic ID -sequence of the nondeterminsitic mode but since it is local to each worker, it is updated serially -and thus is deterministic. worker IDs are unique, and sequence IDs are unique to their workers, -therefor is a globally unique node identifier. Pseudocost Update: Each worker -updates its local pseudocosts when branching. These updates are queued within horizons. During the -horizon sync, these updates are all played in order, and the newly updated global pseudocosts are -broadcast to the worker's pseudocost snapshots for the coming horizon. + where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential ID +local to the worker.\ This sequential ID is similar in principle to the global atomic ID sequence of +the nondeterminsitic mode but since it is local to each worker, it is updated serially and thus is +deterministic. worker IDs are unique, and sequence IDs are unique to their workers, therefor + is a globally unique node identifier. +Pseudocost Update: + Each worker updates its local pseudocosts when branching. These updates are queued within +horizons. During the horizon sync, these updates are all played in order, and the newly updated +global pseudocosts are broadcast to the worker's pseudocost snapshots for the coming horizon. */ @@ -4068,8 +4076,7 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri "Sync%% | NoWork\n"); settings_.log.printf( " " - "-------+---------+----------+--------+---------+--------+----------+----------+-------+-----" - "--" + "-------+---------+----------+--------+---------+--------+----------+----------+-------+-------" "\n"); for (const auto& worker : *deterministic_workers_) { double sync_time = worker.work_context.total_sync_time; diff --git a/cpp/src/branch_and_bound/constants.hpp b/cpp/src/branch_and_bound/constants.hpp index 5629360a86..2bc94c8f1b 100644 --- a/cpp/src/branch_and_bound/constants.hpp +++ b/cpp/src/branch_and_bound/constants.hpp @@ -27,15 +27,31 @@ enum class heuristics_origin_t { // doi: 10.1007/s10107-004-0518-7. enum class search_strategy_t : int { 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). - COEFFICIENT_DIVING = 4, // Coefficient diving (9.2.1) + PSEUDOCOST_DIVING = 1, // Pseudocost diving [1, Section 9.2.5] + LINE_SEARCH_DIVING = 2, // Line search diving [1, Section 9.2.4] + GUIDED_DIVING = 3, // Guided diving. [1, Section 9.2.3] + COEFFICIENT_DIVING = 4, // Coefficient diving [1, Section 9.2.1] FARKAS_DIVING = 5, // Farkas Diving (see [2]) - VECTOR_LENGTH_DIVING = 6, // Vector Length Diving (9.2.6) - SUBMIP = 7 // RINS (see [3]) + VECTOR_LENGTH_DIVING = 6, // Vector Length Diving [1, Section 9.2.6] + RINS = 7, // RINS (see [3]) }; enum class branch_direction_t { NONE = -1, DOWN = 0, UP = 1 }; +inline const char* search_strategy_to_string(search_strategy_t search_strategy) +{ + switch (search_strategy) { + case search_strategy_t::BEST_FIRST: return "BEST_FIRST"; + case search_strategy_t::PSEUDOCOST_DIVING: return "PSEUDOCOST_DIVING"; + case search_strategy_t::LINE_SEARCH_DIVING: return "LINE_SEARCH_DIVING"; + case search_strategy_t::GUIDED_DIVING: return "GUIDED_DIVING"; + case search_strategy_t::COEFFICIENT_DIVING: return "COEFFICIENT_DIVING"; + case search_strategy_t::FARKAS_DIVING: return "FARKAS_DIVING"; + case search_strategy_t::VECTOR_LENGTH_DIVING: return "VECTOR_LENGTH_DIVING"; + case search_strategy_t::RINS: return "RINS"; + } + + return "UNKNOWN"; +} + } // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 11ace52bd2..9792ff72dc 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -105,7 +105,7 @@ f_t compute_user_objective(const lp_problem_t& lp, f_t obj) } template -f_t compute_solver_objective(const lp_problem_t& lp, f_t user_obj) +f_t compute_internal_objective(const lp_problem_t& lp, f_t user_obj) { return user_obj / lp.obj_scale - lp.obj_constant; } @@ -819,7 +819,7 @@ template double compute_user_objective(const lp_problem_t& lp, double obj); -template double compute_solver_objective(const lp_problem_t& lp, double user_obj); +template double compute_internal_objective(const lp_problem_t& lp, double user_obj); template lp_status_t solve_linear_program_advanced( const lp_problem_t& original_lp, diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index d80ec8e5d6..56d032ce99 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -64,7 +64,7 @@ template f_t compute_user_objective(const lp_problem_t& lp, f_t obj); template -f_t compute_solver_objective(const lp_problem_t& lp, f_t user_obj); +f_t compute_internal_objective(const lp_problem_t& lp, f_t user_obj); template lp_status_t solve_linear_program_advanced(const lp_problem_t& original_lp, diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index 25838872ed..bd86652e51 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -50,7 +50,7 @@ struct root_heuristics_t { submip_worker_->leaf_solution.x = sol; submip_worker_->recompute_bounds = false; submip_worker_->recompute_basis = true; - submip_worker_->search_strategy = search_strategy_t::SUBMIP; + submip_worker_->search_strategy = search_strategy_t::RINS; submip_worker_->set_active(); } }; From 75808889f19d4abf2e396f3c25a6971f15255b2e Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 12 Aug 2026 15:12:32 +0200 Subject: [PATCH 06/29] wrap debug submip debug logs in a macro Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 128 ++++++++---------- 1 file changed, 55 insertions(+), 73 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c619390ceb..cd61b1d183 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,6 +45,13 @@ #include #include +#define SUBMIP_VERBOSE false +#if SUBMIP_VERBOSE +#define DEBUG_SUBMIP(fmt, ...) settings_.log.print_format(fmt, __VA_ARGS__); +#else +#define DEBUG_SUBMIP(fmt, ...) +#endif + namespace cuopt::mathematical_optimization::mip { using simplex::basis_update_mpf_t; @@ -2240,11 +2247,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (!feasible) { // This should never happen since we are fixing bounds that are already in the incumbent. rins_stats_.save_infeasible(fixrate); - -#ifdef DEBUG_SUBMIP - settings_.log.print_format("{} The problem is infeasible after running bound strengthening!", - log_prefix); -#endif + DEBUG_SUBMIP("{} The problem is infeasible after running bound strengthening!", log_prefix); return; } @@ -2263,12 +2266,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.strong_branching_simplex_iteration_limit = 50; submip_settings.submip_settings.level = submip_level; submip_settings.benchmark_info_ptr = nullptr; - -#ifdef DEBUG_SUBMIP - submip_settings.log.log = true; -#else - submip_settings.log.log = false; -#endif + submip_settings.log.log = SUBMIP_VERBOSE; #ifdef SAVE_SUBMIP_TO_FILE submip_settings.log.log_prefix = std::format("{}{}", settings_.log.log_prefix, worker->worker_id); @@ -2291,18 +2289,21 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke bool max_recursion = submip_level > settings_.submip_settings.max_level; submip_settings.submip_settings.rins = settings_.submip_settings.rins != 0 && !max_recursion; -#ifdef DEBUG_SUBMIP - submip_settings.log.print_format( - "Sub-MIP: num variables fixed={}/{} ({:.2f}%)", num_var_fixed, num_integers, fixrate * 100); - submip_settings.log.print_format( - "Sub-MIP solve settings: time_limit={:.2f}, node_limit={}, iter_limit={} (current_iter={}), " + DEBUG_SUBMIP("{}Sub-MIP: num variables fixed={}/{} ({:.2f}%)", + log_prefix, + num_var_fixed, + num_integers, + fixrate * 100); + + DEBUG_SUBMIP( + "{}Sub-MIP solve settings: time_limit={:.2f}, node_limit={}, iter_limit={} (current_iter={}), " "tol={:g}", + log_prefix, submip_settings.time_limit, submip_settings.node_limit, submip_settings.branch_and_bound_simplex_iteration_limit, exploration_stats_.total_simplex_iters.load(), submip_settings.relative_mip_gap_tol); -#endif // The `worker->leaf_problem` is directly converted to an `user_problem_t`, meaning that // there is only equality rows (the range row vector is empty) and it contains @@ -2326,12 +2327,10 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke // Also handle optimal if (submip_problem.num_rows == 0 || submip_problem.num_cols == 0) { -#ifdef DEBUG_SUBMIP - submip_settings.log.print_format( - "Sub-MIP presolved to a trivial {} x {} problem; solving by bound pushing", - submip_problem.num_rows, - submip_problem.num_cols); -#endif + DEBUG_SUBMIP("{}Sub-MIP presolved to a trivial {} x {} problem; solving by bound pushing", + log_prefix, + submip_problem.num_rows, + submip_problem.num_cols); std::vector reduced_sol(submip_problem.num_cols); @@ -2359,12 +2358,11 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke this->set_solution_from_submip(worker->leaf_problem, solution, presolver, fixrate); }; -#ifdef DEBUG_SUBMIP - submip_settings.log.print_format("Sub-MIP: {} constraints, {} variables, {} nonzeros\n", - submip_problem.num_rows, - submip_problem.num_cols, - submip_problem.A.nnz()); -#endif + DEBUG_SUBMIP("{}Sub-MIP: {} constraints, {} variables, {} nonzeros\n", + log_prefix, + submip_problem.num_rows, + submip_problem.num_cols, + submip_problem.A.nnz()); probing_implied_bound_t empty_probing(submip_problem.num_cols); branch_and_bound_t submip_bnb(submip_problem, submip_settings, tic(), empty_probing); @@ -2432,15 +2430,14 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke mip_status_t submip_status = submip_bnb.solve(submip_solution); f_t submip_time = toc(start_time); -#ifdef DEBUG_SUBMIP - submip_settings.log.print_format( - "Sub-MIP: status={}, iterations={} (total={}), presolve_time={:.2f}, total_time={:.2f} \n", + DEBUG_SUBMIP( + "{}Sub-MIP: status={}, iterations={} (total={}), presolve_time={:.2f}, total_time={:.2f} \n", + log_prefix, mip_status_to_string(submip_status), submip_solution.simplex_iterations, exploration_stats_.total_simplex_iters.load(), presolve_time, submip_time); -#endif if (submip_status == mip_status_t::NUMERICAL) { return; } if (submip_status == mip_status_t::INFEASIBLE || submip_status == mip_status_t::UNBOUNDED) { @@ -2637,13 +2634,11 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // Enough variables has been fixed if (num_var_fixed >= min_var_fixed) { -#ifdef DEBUG_SUBMIP - settings_.log.print_format("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); -#endif + DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", + log_prefix, + num_var_fixed, + max_var_fixed, + min_var_fixed); has_submip = true; break; } @@ -2668,13 +2663,11 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // Enough variables were fixed if (num_var_fixed >= min_var_fixed) { -#ifdef DEBUG_SUBMIP - settings_.log.print_format("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); -#endif + DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", + log_prefix, + num_var_fixed, + max_var_fixed, + min_var_fixed); has_submip = true; break; } @@ -2695,25 +2688,21 @@ void branch_and_bound_t::rins(diving_worker_t* worker, num_var_fixed); if (num_var_fixed >= min_var_fixed) { -#ifdef DEBUG_SUBMIP - settings_.log.print_format("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); -#endif + DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", + log_prefix, + num_var_fixed, + max_var_fixed, + min_var_fixed); has_submip = true; break; } if (prev_num_fixed == num_var_fixed) { -#ifdef DEBUG_SUBMIP - settings_.log.print_format("{}Could not fix more variables ({}, max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); -#endif + DEBUG_SUBMIP("{}Could not fix more variables ({}, max={}, min={})\n", + log_prefix, + num_var_fixed, + max_var_fixed, + min_var_fixed); has_submip = true; break; } @@ -2788,9 +2777,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // We need the pseudocost to do the DFS, which we do not have during the cut passes. if (!is_root_heuristic) { -#ifdef DEBUG_SUBMIP - settings_.log.print_format("{} Running a quick DFS for the submip!", log_prefix); -#endif + DEBUG_SUBMIP("{} Running a quick DFS for the submip!", log_prefix); dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } @@ -2813,8 +2800,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, exploration_stats_.total_simplex_iters += rins_stats.total_simplex_iters; } -#ifdef DEBUG_SUBMIP - settings_.log.print_format( + DEBUG_SUBMIP( "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " "min_fixrate={:.4g} ({})\n", log_prefix, @@ -2827,7 +2813,6 @@ void branch_and_bound_t::rins(diving_worker_t* worker, max_var_fixed, min_fixrate, min_var_fixed); -#endif if (!is_root_heuristic) { rins_worker_pool_.return_worker_to_pool(worker); @@ -3441,13 +3426,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut nonbasic_list, edge_norms_); } + settings_.log.printf("\n"); solving_root_relaxation_ = false; f_t root_relax_elapsed_time = toc(root_relax_start_time); exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; if (root_status == lp_status_t::INFEASIBLE) { - settings_.log.printf("\nThe root LP relaxation is infeasible\n", + 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) @@ -3455,7 +3441,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::UNBOUNDED) { - settings_.log.printf("\nThe root relaxation is unbounded\n", + settings_.log.printf("The root relaxation is unbounded\n", lp_status_to_string(root_status).c_str()); if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); @@ -3466,7 +3452,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::TIME_LIMIT) { - settings_.log.printf("\n"); solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, -inf); signal_extend_cliques_.store(true, std::memory_order_release); @@ -3475,7 +3460,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::WORK_LIMIT) { - settings_.log.printf("\n"); solver_status_ = mip_status_t::WORK_LIMIT; set_final_solution(solution, -inf); signal_extend_cliques_.store(true, std::memory_order_release); @@ -3484,7 +3468,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::NUMERICAL_ISSUES) { - settings_.log.printf("\n"); solver_status_ = mip_status_t::NUMERICAL; set_final_solution(solution, -inf); signal_extend_cliques_.store(true, std::memory_order_release); @@ -3493,7 +3476,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } assert(root_status == lp_status_t::OPTIMAL); - settings_.log.printf("\n"); settings_.log.print_format("Root relaxation solution found in {} iterations and {:.2f}s by {}\n", root_relax_soln_.iterations, root_relax_elapsed_time, From a8e79127b7e9795b5f22c8ed13cf4eca8de5f057 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 12 Aug 2026 18:08:23 +0200 Subject: [PATCH 07/29] replaced oldest workers when all threads were exhausted. stop all root heuristics before B&B tree exploration. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 71 ++++++++++++------- cpp/src/branch_and_bound/branch_and_bound.hpp | 3 +- cpp/src/mip_heuristics/root_heuristics.hpp | 15 ++-- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index cd61b1d183..8270cdf843 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2825,36 +2825,50 @@ template void branch_and_bound_t::launch_root_heuristics( const lp_problem_t& lp, const std::vector& sol, + i_t cut_pass, std::list>& heuristics, - omp_atomic_t* worker_count) + omp_atomic_t& worker_count) { - assert(worker_count); - if (settings_.deterministic || *worker_count >= settings_.num_threads - 1) return; + if (settings_.deterministic) return; + if (settings_.num_threads < 2) return; + + // If we already exhausted all threads for the root heuristics, stop workers for the + // oldest set of heuristics launched. Leave 2 threads for the cut passes and the clique + // table generation. Add the number of workers that will be launched (1 submip worker + + // 1 CPU FJ worker). + if (worker_count + 4 > settings_.num_threads && !heuristics.empty()) { + heuristics.erase(heuristics.begin()); + } bool is_root_heuristic = true; - i_t id = heuristics.size(); - root_heuristics_t& heuristic = heuristics.emplace_back(Arow_, var_types_); + i_t id = cut_pass; + root_heuristics_t* heuristic = &heuristics.emplace_back(Arow_, var_types_); constexpr bool is_cpufj_enabled = true; if (is_cpufj_enabled) { - fj_cpu_worker_t& worker = heuristic.fj_cpu_worker_; + fj_cpu_worker_t* worker = &heuristic->fj_cpu_worker_; f_t work_limit = 2.0; f_t time_limit = settings_.time_limit - toc(exploration_stats_.start_time); - worker.improvement_callback = + worker->improvement_callback = [this](f_t obj, const std::vector& assignment, double work_units) { set_solution_from_cpu_fj(obj, assignment, work_units); }; - worker.create_worker(lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); - worker.run_async(time_limit, work_limit, worker_count); - } + worker->create_worker(lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); - if (*worker_count >= settings_.num_threads - 1) return; + ++worker_count; +#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ + firstprivate(worker, heuristic) shared(heuristics, worker_count) depend(out : *worker -> fj_cpu) + { + worker->run_sync(time_limit, work_limit); + --worker_count; + } + } if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent) { - heuristic.create_submip_worker(id, lp, settings_, root_objective_, root_vstatus_, sol); - diving_worker_t* worker = heuristic.submip_worker_.get(); + diving_worker_t* worker = + heuristic->create_submip_worker(id, lp, settings_, root_objective_, root_vstatus_, sol); std::vector current_incumbent; mutex_upper_.lock(); @@ -2864,17 +2878,22 @@ 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. - rins(worker, current_incumbent, heuristic.var_types_, is_root_heuristic); + rins(worker, current_incumbent, heuristic->var_types_, is_root_heuristic); + heuristic->Arow_ = csr_matrix_t(1, 1, 1); + heuristic->var_types_ = {}; + heuristic->submip_worker_.reset(); + } else { - ++(*worker_count); -#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ - firstprivate(worker, current_incumbent, worker_count) shared(heuristic) depend(out : *worker) + ++worker_count; +#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ + shared(heuristics, worker_count) firstprivate(worker, current_incumbent, heuristic) \ + depend(out : *worker) { - rins(worker, current_incumbent, heuristic.var_types_, is_root_heuristic); - --(*worker_count); - heuristic.Arow_ = csr_matrix_t(1, 1, 1); - heuristic.var_types_ = {}; - heuristic.submip_worker_.reset(); + rins(worker, current_incumbent, heuristic->var_types_, is_root_heuristic); + --worker_count; + heuristic->Arow_ = csr_matrix_t(1, 1, 1); + heuristic->var_types_ = {}; + heuristic->submip_worker_.reset(); } } } @@ -3556,7 +3575,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut omp_atomic_t root_worker_count = 0; std::list> root_heuristics; - launch_root_heuristics(original_lp_, root_relax_soln_.x, root_heuristics, &root_worker_count); + launch_root_heuristics(original_lp_, root_relax_soln_.x, 0, root_heuristics, root_worker_count); f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; @@ -3627,7 +3646,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (cut_pass_result.action == cut_pass_action_t::BREAK) { break; } - launch_root_heuristics(original_lp_, root_relax_soln_.x, root_heuristics, &root_worker_count); + launch_root_heuristics( + original_lp_, root_relax_soln_.x, cut_pass + 1, root_heuristics, root_worker_count); } // Publish the post-cuts root LP value. @@ -3754,6 +3774,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } + // Stops the root heuristics and clear the associated data + root_heuristics.clear(); + settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); node_concurrent_halt_ = 0; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 743fd02a9c..75d98f7d69 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -393,8 +393,9 @@ class branch_and_bound_t { void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, + i_t cut_pass, std::list>& heuristics, - omp_atomic_t* worker_count); + omp_atomic_t& worker_count); // Solve the LP relaxation of a leaf node simplex::dual_status_t solve_node_lp(mip_node_t* node_ptr, diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index bd86652e51..78a62819a0 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -36,12 +36,13 @@ struct root_heuristics_t { } } - void create_submip_worker(i_t id, - const simplex::lp_problem_t& lp, - const simplex::simplex_solver_settings_t& settings, - f_t root_obj, - const std::vector& root_vstatus, - const std::vector& sol) + diving_worker_t* create_submip_worker( + i_t id, + const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + f_t root_obj, + const std::vector& root_vstatus, + const std::vector& sol) { submip_worker_ = std::make_unique>(id, lp, Arow_, var_types_, settings); @@ -52,6 +53,8 @@ struct root_heuristics_t { submip_worker_->recompute_basis = true; submip_worker_->search_strategy = search_strategy_t::RINS; submip_worker_->set_active(); + + return submip_worker_.get(); } }; } // namespace cuopt::mathematical_optimization::mip From 6f568c1b30ca7350d70e540da830e97e1d6f95d6 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 13 Aug 2026 11:05:27 +0200 Subject: [PATCH 08/29] stop root heuristics before strong branching Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 8270cdf843..24abb3e5f3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3673,6 +3673,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.printf("\n"); } + // Stops the root heuristics and clear the associated data + root_heuristics.clear(); + set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); @@ -3774,9 +3777,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - // Stops the root heuristics and clear the associated data - root_heuristics.clear(); - settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); node_concurrent_halt_ = 0; From f41eca55db5cafb81b0c09e98b1f3af249755c51 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 19 Aug 2026 11:35:18 +0200 Subject: [PATCH 09/29] addressed a few of the reviewer comments Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 19 +++++++++---------- cpp/src/branch_and_bound/branch_and_bound.hpp | 2 +- cpp/src/dual_simplex/solve.cpp | 4 ++-- cpp/src/dual_simplex/solve.hpp | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 24abb3e5f3..d0a6501fc3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2200,14 +2200,12 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so current_incumbent = incumbent_.x; mutex_upper_.unlock(); - // Note that this node does not have the vstatus (it was clear at the start of B&B exploration) + // Note that this node does not have the vstatus (it was cleared at the start of B&B exploration) worker->start_node = mip_node_t(root_objective_, root_vstatus_); worker->leaf_vstatus = root_vstatus_; worker->leaf_problem.lower = original_lp_.lower; worker->leaf_problem.upper = original_lp_.upper; worker->leaf_solution.x = sol; - worker->recompute_bounds = false; - worker->recompute_basis = true; worker->search_strategy = search_strategy_t::RINS; worker->set_active(); @@ -2227,7 +2225,7 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so template void branch_and_bound_t::solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_type, + const std::vector& var_types, i_t num_var_fixed, i_t num_integers, i_t submip_level, @@ -2309,7 +2307,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke // there is only equality rows (the range row vector is empty) and it contains // structural + slacks + cuts constraints/variables. user_problem_t submip_problem(original_problem_.handle_ptr); - simplex::convert_lp_to_user_problem(worker->leaf_problem, var_type, settings_, submip_problem); + simplex::convert_lp_to_user_problem(worker->leaf_problem, var_types, settings_, submip_problem); third_party_presolve_t presolver; f_t presolve_time_limit = std::min(0.1 * submip_settings.time_limit, 60.0); @@ -2376,7 +2374,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_bnb.set_initial_guess(presolved_incumbent); const f_t user_upper = compute_user_objective(worker->leaf_problem, upper_bound_.load()); - const f_t submip_cutoff = compute_internal_objective(submip_bnb.original_lp_, user_upper); + const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); submip_bnb.set_initial_upper_bound(submip_cutoff); if (!is_root_heuristic) @@ -2595,7 +2593,8 @@ void branch_and_bound_t::rins(diving_worker_t* worker, ++rins_stats_.total_calls; bool has_submip = false; - const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; + worker->recompute_bounds = false; + worker->recompute_basis = true; branch_and_bound_stats_t rins_stats; mip_node_t& node = worker->start_node; @@ -2778,7 +2777,6 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // We need the pseudocost to do the DFS, which we do not have during the cut passes. if (!is_root_heuristic) { DEBUG_SUBMIP("{} Running a quick DFS for the submip!", log_prefix); - dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } } @@ -2836,7 +2834,8 @@ void branch_and_bound_t::launch_root_heuristics( // oldest set of heuristics launched. Leave 2 threads for the cut passes and the clique // table generation. Add the number of workers that will be launched (1 submip worker + // 1 CPU FJ worker). - if (worker_count + 4 > settings_.num_threads && !heuristics.empty()) { + i_t clique_table_generation = cut_pass == 0 ? 1 : 0; + if (worker_count + 3 + clique_table_generation > settings_.num_threads && !heuristics.empty()) { heuristics.erase(heuristics.begin()); } @@ -3663,7 +3662,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.benchmark_info_ptr->cut_generation_time_sec = cut_generation_time; } if (cut_info.has_cuts()) { - settings_.log.printf("Cut generation time: %.2f seconds\n", cut_generation_time); + settings_.log.printf("Root cut passes time: %.2f seconds\n", cut_generation_time); settings_.log.printf("Cut pool size : %d\n", cut_pool_size); settings_.log.printf("Size with cuts : %d constraints, %d variables, %d nonzeros\n", original_lp_.num_rows, diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 75d98f7d69..13c5875a26 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -378,7 +378,7 @@ class branch_and_bound_t { // Solve the RINS sub-MIP void solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_type, + const std::vector& var_types, i_t num_var_fixed, i_t num_integers, i_t submip_level, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 9792ff72dc..388bb43b35 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -105,7 +105,7 @@ f_t compute_user_objective(const lp_problem_t& lp, f_t obj) } template -f_t compute_internal_objective(const lp_problem_t& lp, f_t user_obj) +f_t compute_presolved_objective(const lp_problem_t& lp, f_t user_obj) { return user_obj / lp.obj_scale - lp.obj_constant; } @@ -819,7 +819,7 @@ template double compute_user_objective(const lp_problem_t& lp, double obj); -template double compute_internal_objective(const lp_problem_t& lp, double user_obj); +template double compute_presolved_objective(const lp_problem_t& lp, double user_obj); template lp_status_t solve_linear_program_advanced( const lp_problem_t& original_lp, diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 56d032ce99..308c462de5 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -64,7 +64,7 @@ template f_t compute_user_objective(const lp_problem_t& lp, f_t obj); template -f_t compute_internal_objective(const lp_problem_t& lp, f_t user_obj); +f_t compute_presolved_objective(const lp_problem_t& lp, f_t user_obj); template lp_status_t solve_linear_program_advanced(const lp_problem_t& original_lp, From fd3e327584f94d67ae63a3041bf2cba8e5db86c9 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 19 Aug 2026 16:43:50 +0200 Subject: [PATCH 10/29] do not limit the work unit in the CPU FJ during the cut passes. replace is_root_heuristic flag with a global one. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 33 ++++++++----------- cpp/src/branch_and_bound/branch_and_bound.hpp | 7 ++-- cpp/src/branch_and_bound/worker_pool.hpp | 9 +++-- .../mip_heuristics/feasibility_jump/fj_cpu.cu | 1 - 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index d0a6501fc3..bc2e8693c0 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2212,11 +2212,11 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - rins(worker, current_incumbent, var_types_, is_root_heuristic); + rins(worker, current_incumbent, var_types_); } else { #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(worker, current_incumbent) - rins(worker, current_incumbent, var_types_, is_root_heuristic); + rins(worker, current_incumbent, var_types_); } return true; @@ -2229,8 +2229,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix, - bool is_root_heuristic) + std::string_view log_prefix) { double start_time = tic(); @@ -2377,7 +2376,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); submip_bnb.set_initial_upper_bound(submip_cutoff); - if (!is_root_heuristic) + if (during_cut_passes_) submip_bnb.set_initial_pseudocost(pc_, presolver.get_reduced_to_original_map()); if (submip_halt_callback_) { @@ -2581,8 +2580,7 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings template void branch_and_bound_t::rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types, - bool is_root_heuristic) + const std::vector& var_types) { raft::common::nvtx::range scope("BB::rins_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } @@ -2775,7 +2773,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, } // We need the pseudocost to do the DFS, which we do not have during the cut passes. - if (!is_root_heuristic) { + if (during_cut_passes_) { DEBUG_SUBMIP("{} Running a quick DFS for the submip!", log_prefix); dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } @@ -2788,8 +2786,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, num_var_fixed, num_integers, submip_level, - log_prefix, - is_root_heuristic); + log_prefix); } } @@ -2812,11 +2809,8 @@ void branch_and_bound_t::rins(diving_worker_t* worker, min_fixrate, min_var_fixed); - if (!is_root_heuristic) { - rins_worker_pool_.return_worker_to_pool(worker); - } else { - worker->set_inactive(); - } + // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. + rins_worker_pool_.return_worker_to_pool(worker); } template @@ -2839,7 +2833,6 @@ void branch_and_bound_t::launch_root_heuristics( heuristics.erase(heuristics.begin()); } - bool is_root_heuristic = true; i_t id = cut_pass; root_heuristics_t* heuristic = &heuristics.emplace_back(Arow_, var_types_); @@ -2847,7 +2840,7 @@ void branch_and_bound_t::launch_root_heuristics( if (is_cpufj_enabled) { fj_cpu_worker_t* worker = &heuristic->fj_cpu_worker_; - f_t work_limit = 2.0; + f_t work_limit = std::numeric_limits::infinity(); f_t time_limit = settings_.time_limit - toc(exploration_stats_.start_time); worker->improvement_callback = @@ -2877,7 +2870,7 @@ 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. - rins(worker, current_incumbent, heuristic->var_types_, is_root_heuristic); + rins(worker, current_incumbent, heuristic->var_types_); heuristic->Arow_ = csr_matrix_t(1, 1, 1); heuristic->var_types_ = {}; heuristic->submip_worker_.reset(); @@ -2888,7 +2881,7 @@ void branch_and_bound_t::launch_root_heuristics( shared(heuristics, worker_count) firstprivate(worker, current_incumbent, heuristic) \ depend(out : *worker) { - rins(worker, current_incumbent, heuristic->var_types_, is_root_heuristic); + rins(worker, current_incumbent, heuristic->var_types_); --worker_count; heuristic->Arow_ = csr_matrix_t(1, 1, 1); heuristic->var_types_ = {}; @@ -3538,6 +3531,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } is_running_ = true; + during_cut_passes_ = true; lower_bound_numerical_ = inf; if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } @@ -3674,6 +3668,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // Stops the root heuristics and clear the associated data root_heuristics.clear(); + during_cut_passes_ = false; set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 13c5875a26..cdf99acafc 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -281,6 +281,7 @@ class branch_and_bound_t { // Global status of the solver. omp_atomic_t solver_status_; omp_atomic_t is_running_{false}; + omp_atomic_t during_cut_passes_{false}; // Minimum number of node in the queue. When the queue size is less than // this variable, the nodes are added directly to the queue instead of @@ -382,14 +383,12 @@ class branch_and_bound_t { i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix, - bool is_root_heuristic); + std::string_view log_prefix); // Creates and solves the RINS sub-MIP void rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types, - bool is_root_heuristic); + const std::vector& var_types); void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, diff --git a/cpp/src/branch_and_bound/worker_pool.hpp b/cpp/src/branch_and_bound/worker_pool.hpp index e9d55bafe3..87a298f573 100644 --- a/cpp/src/branch_and_bound/worker_pool.hpp +++ b/cpp/src/branch_and_bound/worker_pool.hpp @@ -61,14 +61,17 @@ class worker_pool_t { void return_worker_to_pool(WorkerType* worker) { - std::lock_guard lock(mutex_); assert(worker != nullptr); + worker->set_inactive(); + assert(!worker->is_active.load()); + + if (!is_initialized_) return; + + std::lock_guard lock(mutex_); assert(workers_[worker->worker_id].get() == worker); assert(static_cast(num_idle_workers_.load()) == idle_workers_.size()); assert(idle_workers_.size() <= workers_.size()); - worker->set_inactive(); - assert(!worker->is_active.load()); idle_workers_.push_back(worker->worker_id); num_idle_workers_++; } diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index ca96477ab6..4e830d7467 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -2123,7 +2123,6 @@ void fj_cpu_worker_t::run_async(f_t time_limit, priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : *fj_cpu) { cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); - fj_cpu.reset(); if (worker_count) --(*worker_count); } } From 1c79fdf6b1f1e100608fc322d9e8d92950b284ba Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 19 Aug 2026 18:47:15 +0200 Subject: [PATCH 11/29] replaced halt mechanism for the sub-MIP Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 33 +++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 12 +++++-- cpp/src/branch_and_bound/worker.hpp | 2 -- cpp/src/mip_heuristics/root_heuristics.hpp | 8 +++-- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index bc2e8693c0..048fa1178b 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1689,7 +1689,8 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, bool can_launch_rins = true; - while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && + while (stack.size() > 0 && + (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { if (worker->worker_id == 0) { repair_heuristic_solutions(); } @@ -1938,8 +1939,9 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t worker->calculate_max_diving_workers(bfs_worker_pool_.size(), diving_worker_pool_.size()); worker->update_diving_heuristic_list(diving_settings); - while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && - rel_gap > settings_.relative_mip_gap_tol && node_queue.best_first_queue_size() > 0) { + while ((solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) && + abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && + node_queue.best_first_queue_size() > 0) { if (submip_halt_callback_) { // Stops the solver if the callback returns "true". This happens when the lower bound // in the sub-MIP solve is greater than the upper bound of the main solve (this can @@ -2055,7 +2057,8 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker, f_t rel_gap = user_relative_gap(user_obj, user_lower); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); - while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && + while (stack.size() > 0 && + (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -2229,7 +2232,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix) + std::string_view log_prefix, + std::atomic* halt) { double start_time = tic(); @@ -2264,6 +2268,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; #ifdef SAVE_SUBMIP_TO_FILE submip_settings.log.log_prefix = std::format("{}{}", settings_.log.log_prefix, worker->worker_id); @@ -2376,7 +2381,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); submip_bnb.set_initial_upper_bound(submip_cutoff); - if (during_cut_passes_) + if (!during_cut_passes_) submip_bnb.set_initial_pseudocost(pc_, presolver.get_reduced_to_original_map()); if (submip_halt_callback_) { @@ -2384,12 +2389,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->is_halted(); }); } @@ -2580,7 +2585,8 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings template void branch_and_bound_t::rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types) + const std::vector& var_types, + std::atomic* halt) { raft::common::nvtx::range scope("BB::rins_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } @@ -2615,7 +2621,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, i_t min_var_fixed = min_fixrate * num_integers; i_t num_var_fixed = 0; - while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { + while (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) { // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the // current incumbent, considering only the fractional values in the current node i_t prev_num_fixed = num_var_fixed; @@ -2786,7 +2792,8 @@ void branch_and_bound_t::rins(diving_worker_t* worker, num_var_fixed, num_integers, submip_level, - log_prefix); + log_prefix, + halt); } } @@ -2870,7 +2877,7 @@ 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. - rins(worker, current_incumbent, heuristic->var_types_); + rins(worker, current_incumbent, heuristic->var_types_, &heuristic->halt_); heuristic->Arow_ = csr_matrix_t(1, 1, 1); heuristic->var_types_ = {}; heuristic->submip_worker_.reset(); @@ -2881,7 +2888,7 @@ void branch_and_bound_t::launch_root_heuristics( shared(heuristics, worker_count) firstprivate(worker, current_incumbent, heuristic) \ depend(out : *worker) { - rins(worker, current_incumbent, heuristic->var_types_); + rins(worker, current_incumbent, heuristic->var_types_, &heuristic->halt_); --worker_count; heuristic->Arow_ = csr_matrix_t(1, 1, 1); heuristic->var_types_ = {}; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index cdf99acafc..f022f08512 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -346,6 +346,12 @@ class branch_and_bound_t { // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); + bool is_halted() + { + return settings_.concurrent_halt ? settings_.concurrent_halt->load(std::memory_order_acquire) + : false; + } + // Launch a new diving worker from a given best-first worker. bool launch_diving_worker(bfs_worker_t* bfs_worker); @@ -383,12 +389,14 @@ class branch_and_bound_t { i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix); + std::string_view log_prefix, + std::atomic* halt = nullptr); // Creates and solves the RINS sub-MIP void rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types); + const std::vector& var_types, + 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 f40dcb71b6..fd5255f86f 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -243,8 +243,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 { diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index 78a62819a0..5cac2fff0f 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -15,10 +15,12 @@ namespace cuopt::mathematical_optimization::mip { template struct root_heuristics_t { - std::unique_ptr> submip_worker_; - fj_cpu_worker_t fj_cpu_worker_; std::vector var_types_; csr_matrix_t Arow_; + std::atomic halt_{false}; + + std::unique_ptr> submip_worker_; + fj_cpu_worker_t fj_cpu_worker_; root_heuristics_t(const csr_matrix_t& Arow, const std::vector& var_types) @@ -28,9 +30,9 @@ struct root_heuristics_t { void stop() { + halt_.store(true, std::memory_order_release); fj_cpu_worker_.stop(); if (submip_worker_) { - submip_worker_->halt = true; diving_worker_t* worker = submip_worker_.get(); #pragma omp taskwait depend(in : *worker) } From 6fec9713a4b3de520c3ddea0ed27bcf835c88241 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 20 Aug 2026 11:06:55 +0200 Subject: [PATCH 12/29] fixed inverted logic Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 048fa1178b..84bbda5240 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2779,7 +2779,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, } // We need the pseudocost to do the DFS, which we do not have during the cut passes. - if (during_cut_passes_) { + if (!during_cut_passes_) { DEBUG_SUBMIP("{} Running a quick DFS for the submip!", log_prefix); dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } From f318fba67616ef9ad3e0f8353b552c22efea8e11 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 20 Aug 2026 15:59:26 +0200 Subject: [PATCH 13/29] fixed race conditions. added more halt checks. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 81 +++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 3 +- .../deterministic_workers.hpp | 42 ++++++++-- cpp/src/branch_and_bound/worker.hpp | 14 +++- cpp/src/branch_and_bound/worker_pool.hpp | 6 +- cpp/src/mip_heuristics/root_heuristics.hpp | 16 +++- 6 files changed, 113 insertions(+), 49 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 84bbda5240..6969e4a2aa 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1005,7 +1005,7 @@ branch_variable_t branch_and_bound_t::variable_selection( branch_var = pc_.variable_selection(fractional, solution); } - round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); + round_dir = martin_criteria(solution[branch_var], worker->root_solution[branch_var]); return {branch_var, round_dir}; @@ -1014,10 +1014,10 @@ branch_variable_t branch_and_bound_t::variable_selection( original_lp_, fractional, solution, var_up_locks_, var_down_locks_, log); case search_strategy_t::LINE_SEARCH_DIVING: - return line_search_diving(fractional, solution, root_relax_soln_.x, log); + return line_search_diving(fractional, solution, worker->root_solution, log); case search_strategy_t::PSEUDOCOST_DIVING: - return pseudocost_diving(pc_, fractional, solution, root_relax_soln_.x, log); + return pseudocost_diving(pc_, fractional, solution, worker->root_solution, log); case search_strategy_t::GUIDED_DIVING: assert(incumbent_.has_incumbent); @@ -1034,7 +1034,7 @@ branch_variable_t branch_and_bound_t::variable_selection( case search_strategy_t::RINS: // This is used for solving the DFS of the sub-MIP. branch_var = pc_.variable_selection(fractional, solution); - round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]); + round_dir = martin_criteria(solution[branch_var], worker->root_solution[branch_var]); return {branch_var, round_dir}; } @@ -1187,7 +1187,7 @@ struct deterministic_bfs_policy_t const std::vector& x) override { i_t var = this->worker.pc_snapshot.variable_selection(fractional, x); - auto dir = martin_criteria(x[var], this->bnb.root_relax_soln_.x[var]); + auto dir = martin_criteria(x[var], this->worker.root_solution[var]); return {var, dir}; } @@ -1523,7 +1523,8 @@ dual_status_t branch_and_bound_t::solve_node_lp( branch_and_bound_worker_t* worker, branch_and_bound_stats_t& stats, logger_t& log, - i_t iter_limit) + i_t iter_limit, + std::atomic* halt) { raft::common::nvtx::range scope("BB::solve_node"); #ifdef DEBUG_BRANCHING @@ -1562,7 +1563,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( #endif simplex_solver_settings_t lp_settings = settings_; - lp_settings.concurrent_halt = &node_concurrent_halt_; + lp_settings.concurrent_halt = halt ? halt : &node_concurrent_halt_; lp_settings.set_log(false); f_t cutoff = upper_bound_.load(); if (original_lp_.objective_step.has_step()) { @@ -1609,7 +1610,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( bool feasible = worker->set_lp_variable_bounds(node_ptr, settings_); dual_status_t lp_status = dual_status_t::DUAL_UNBOUNDED; - worker->leaf_edge_norms = edge_norms_; + worker->leaf_edge_norms = worker->root_edge_norm; if (worker->recompute_bounds && worker->orbital_fixing && worker->search_strategy == search_strategy_t::BEST_FIRST) { worker->orbital_fixing->reset(symmetry_, node_ptr); @@ -2352,6 +2353,13 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke return; } + if (toc(exploration_stats_.start_time) > settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + return; + } + + if (halt && 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; @@ -2621,7 +2629,8 @@ void branch_and_bound_t::rins(diving_worker_t* worker, i_t min_var_fixed = min_fixrate * num_integers; i_t num_var_fixed = 0; - while (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) { + while (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted() && + (halt ? !halt->load() : true)) { // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the // current incumbent, considering only the fractional values in the current node i_t prev_num_fixed = num_var_fixed; @@ -2683,7 +2692,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, worker->leaf_problem.objective, fractional, current_sol, - root_relax_soln_.x, + worker->root_solution, max_var_fixed, lower, upper, @@ -2719,10 +2728,11 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // After fixing the variables, re-solve the LP relaxation. We use the optimal solution // in the next iteration to find additional variable fixings. - // We continue to do this until enough variables were fixed or no variable is left to fix. + // We continue to do this until enough variables were fixed or no variable is left to fix + i_t iter_max = std::numeric_limits::max(); logger_t log; log.log = false; - dual_status_t lp_status = solve_node_lp(&node, worker, rins_stats, log); + dual_status_t lp_status = solve_node_lp(&node, worker, rins_stats, log, iter_max, halt); if (lp_status != dual_status_t::OPTIMAL) { break; } @@ -2840,8 +2850,9 @@ void branch_and_bound_t::launch_root_heuristics( heuristics.erase(heuristics.begin()); } - i_t id = cut_pass; - root_heuristics_t* heuristic = &heuristics.emplace_back(Arow_, var_types_); + i_t id = cut_pass; + root_heuristics_t* heuristic = + &heuristics.emplace_back(Arow_, var_types_, sol, edge_norms_); constexpr bool is_cpufj_enabled = true; if (is_cpufj_enabled) { @@ -2855,14 +2866,7 @@ void branch_and_bound_t::launch_root_heuristics( set_solution_from_cpu_fj(obj, assignment, work_units); }; worker->create_worker(lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); - - ++worker_count; -#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ - firstprivate(worker, heuristic) shared(heuristics, worker_count) depend(out : *worker -> fj_cpu) - { - worker->run_sync(time_limit, work_limit); - --worker_count; - } + worker->run_async(time_limit, work_limit, &worker_count); } if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent) { @@ -2878,9 +2882,6 @@ void branch_and_bound_t::launch_root_heuristics( // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. rins(worker, current_incumbent, heuristic->var_types_, &heuristic->halt_); - heuristic->Arow_ = csr_matrix_t(1, 1, 1); - heuristic->var_types_ = {}; - heuristic->submip_worker_.reset(); } else { ++worker_count; @@ -2890,9 +2891,6 @@ void branch_and_bound_t::launch_root_heuristics( { rins(worker, current_incumbent, heuristic->var_types_, &heuristic->halt_); --worker_count; - heuristic->Arow_ = csr_matrix_t(1, 1, 1); - heuristic->var_types_ = {}; - heuristic->submip_worker_.reset(); } } } @@ -3646,6 +3644,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (cut_pass_result.action == cut_pass_action_t::BREAK) { break; } + set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); launch_root_heuristics( original_lp_, root_relax_soln_.x, cut_pass + 1, root_heuristics, root_worker_count); } @@ -3801,9 +3800,23 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut const i_t num_bfs_workers = std::max(num_workers / 2, 1); const i_t num_submip_workers = std::max(num_workers / 8, 1); const i_t num_diving_workers = std::max(num_workers - num_bfs_workers, 1); - bfs_worker_pool_.init(num_bfs_workers, original_lp_, Arow_, var_types_, symmetry_, settings_); - rins_worker_pool_.init( - num_submip_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, num_bfs_workers); + bfs_worker_pool_.init(num_bfs_workers, + original_lp_, + Arow_, + var_types_, + symmetry_, + settings_, + root_relax_soln_.x, + edge_norms_); + rins_worker_pool_.init(num_submip_workers, + original_lp_, + Arow_, + var_types_, + symmetry_, + settings_, + root_relax_soln_.x, + edge_norms_, + num_bfs_workers); if (num_diving_workers > 0) { diving_worker_pool_.init(num_diving_workers, @@ -3812,6 +3825,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut var_types_, symmetry_, settings_, + root_relax_soln_.x, + edge_norms_, num_bfs_workers + num_submip_workers); } @@ -3994,7 +4009,7 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri deterministic_global_termination_status_ = mip_status_t::UNSET; deterministic_workers_ = std::make_unique>( - num_bfs_workers, original_lp_, Arow, var_types_, settings_); + num_bfs_workers, original_lp_, Arow, var_types_, settings_, root_relax_soln_.x, edge_norms_); if (num_diving_workers > 0) { // Extract diving types from search_strategies (skip BEST_FIRST at index 0) @@ -4013,6 +4028,8 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri Arow, var_types_, settings_, + root_relax_soln_.x, + edge_norms_, &root_relax_soln_.x); } } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index f022f08512..c816059ca7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -409,7 +409,8 @@ class branch_and_bound_t { branch_and_bound_worker_t* worker, branch_and_bound_stats_t& stats, simplex::logger_t& log, - i_t iter_limit = std::numeric_limits::max()); + i_t iter_limit = std::numeric_limits::max(), + std::atomic* halt = nullptr); // Apply symmetry-based bound reductions (orbital fixing and, when // settings_.symmetry == 2, lexical reduction) to the current node. diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index 4de3086e61..402df18e1c 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -89,8 +89,10 @@ class deterministic_worker_base_t : public branch_and_bound_worker_t { const csr_matrix_t& Arow, const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, + const std::vector& root_solution, + const std::vector& root_edge_norm, const std::string& context_name) - : base_t(id, original_lp, Arow, var_types, settings), + : base_t(id, original_lp, Arow, var_types, settings, root_solution, root_edge_norm), work_context(context_name), pc_snapshot(1, settings) { @@ -140,8 +142,17 @@ class deterministic_bfs_worker_t const simplex::lp_problem_t& original_lp, const csr_matrix_t& Arow, const std::vector& var_types, - const simplex::simplex_solver_settings_t& settings) - : base_t(id, original_lp, Arow, var_types, settings, "BB_Worker_" + std::to_string(id)) + const simplex::simplex_solver_settings_t& settings, + const std::vector& root_solution, + const std::vector& root_edge_norm) + : base_t(id, + original_lp, + Arow, + var_types, + settings, + root_solution, + root_edge_norm, + "BB_Worker_" + std::to_string(id)) { } @@ -300,8 +311,17 @@ class deterministic_diving_worker_t const csr_matrix_t& Arow, const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, + const std::vector& root_solution, + const std::vector& root_edge_norm, const std::vector* root_sol) - : base_t(id, original_lp, Arow, var_types, settings, "Diving_Worker_" + std::to_string(id)), + : base_t(id, + original_lp, + Arow, + var_types, + settings, + root_solution, + root_edge_norm, + "Diving_Worker_" + std::to_string(id)), diving_type(type), root_solution(root_sol) { @@ -407,11 +427,14 @@ class deterministic_bfs_worker_pool_t const simplex::lp_problem_t& original_lp, const csr_matrix_t& Arow, const std::vector& var_types, - const simplex::simplex_solver_settings_t& settings) + const simplex::simplex_solver_settings_t& settings, + const std::vector& root_solution, + const std::vector& root_edge_norm) { this->workers_.reserve(num_workers); for (int i = 0; i < num_workers; ++i) { - this->workers_.emplace_back(i, original_lp, Arow, var_types, settings); + this->workers_.emplace_back( + i, original_lp, Arow, var_types, settings, root_solution, root_edge_norm); } } @@ -443,12 +466,15 @@ class deterministic_diving_worker_pool_t const csr_matrix_t& Arow, const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, - const std::vector* root_solution) + const std::vector& root_solution, + const std::vector& root_edge_norm, + const std::vector* root_sol) { this->workers_.reserve(num_workers); for (int i = 0; i < num_workers; ++i) { search_strategy_t type = diving_types[i % diving_types.size()]; - this->workers_.emplace_back(i, type, original_lp, Arow, var_types, settings, root_solution); + this->workers_.emplace_back( + i, type, original_lp, Arow, var_types, settings, root_solution, root_edge_norm, root_sol); } } diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index fd5255f86f..28c6edffe6 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -78,6 +78,9 @@ class branch_and_bound_worker_t { bool recompute_basis = true; bool recompute_bounds = true; + const std::vector& root_solution; + const std::vector& root_edge_norm; + void ensure_orbital_fixing() { if (orbital_fixing == nullptr && symmetry_ptr != nullptr) { @@ -94,6 +97,8 @@ class branch_and_bound_worker_t { const csr_matrix_t& Arow, const std::vector& var_type, const simplex::simplex_solver_settings_t& settings, + const std::vector& root_solution, + const std::vector& root_edge_norm, uint64_t rng_offset = 0) : worker_id(worker_id), search_strategy(search_strategy_t::BEST_FIRST), @@ -108,7 +113,9 @@ class branch_and_bound_worker_t { node_presolver(leaf_problem, Arow, {}, var_type), bounds_changed(original_lp.num_cols, false), rng(settings.random_seed + pcgenerator_t::default_seed + rng_offset + worker_id, - pcgenerator_t::default_stream ^ (worker_id + rng_offset)) + pcgenerator_t::default_stream ^ (worker_id + rng_offset)), + root_solution(root_solution), + root_edge_norm(root_edge_norm) { } @@ -146,8 +153,11 @@ class bfs_worker_t : public branch_and_bound_worker_t { const csr_matrix_t& Arow, const std::vector& var_type, const simplex::simplex_solver_settings_t& settings, + const std::vector& root_solution, + const std::vector& root_edge_norm, uint64_t rng_offset = 0) - : Base(worker_id, original_lp, Arow, var_type, settings, rng_offset) + : Base( + worker_id, original_lp, Arow, var_type, settings, root_solution, root_edge_norm, rng_offset) { this->start_lower = original_lp.lower; this->start_upper = original_lp.upper; diff --git a/cpp/src/branch_and_bound/worker_pool.hpp b/cpp/src/branch_and_bound/worker_pool.hpp index 87a298f573..6977c7882b 100644 --- a/cpp/src/branch_and_bound/worker_pool.hpp +++ b/cpp/src/branch_and_bound/worker_pool.hpp @@ -24,6 +24,8 @@ class worker_pool_t { const std::vector& var_type, mip_symmetry_t* symmetry, const simplex::simplex_solver_settings_t& settings, + const std::vector& root_solution, + const std::vector& root_edge_norm, const uint64_t rng_offset = 0) { assert(!is_initialized_); @@ -33,8 +35,8 @@ class worker_pool_t { num_idle_workers_ = num_workers; idle_workers_.clear_resize(num_workers); for (i_t i = 0; i < num_workers; ++i) { - workers_[i] = - std::make_unique(i, original_lp, Arow, var_type, settings, rng_offset); + workers_[i] = std::make_unique( + i, original_lp, Arow, var_type, settings, root_solution, root_edge_norm, rng_offset); idle_workers_.push_back(i); // Propagate the (possibly null) symmetry pointer; workers lazily build // their orbital_fixing/lexical_reduction state via ensure_orbital_fixing(). diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index 5cac2fff0f..50687fc07b 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -17,14 +17,22 @@ template struct root_heuristics_t { std::vector var_types_; csr_matrix_t Arow_; + std::vector root_solution_; + std::vector root_edge_norm_; std::atomic halt_{false}; std::unique_ptr> submip_worker_; fj_cpu_worker_t fj_cpu_worker_; root_heuristics_t(const csr_matrix_t& Arow, - const std::vector& var_types) - : submip_worker_(nullptr), var_types_(var_types), Arow_(Arow) {}; + const std::vector& var_types, + const std::vector& root_solution, + const std::vector& root_edge_norm) + : var_types_(var_types), + Arow_(Arow), + root_solution_(root_solution), + root_edge_norm_(root_edge_norm), + submip_worker_(nullptr) {}; ~root_heuristics_t() { stop(); } @@ -46,8 +54,8 @@ struct root_heuristics_t { const std::vector& root_vstatus, const std::vector& sol) { - submip_worker_ = - std::make_unique>(id, lp, Arow_, var_types_, settings); + submip_worker_ = std::make_unique>( + id, lp, Arow_, var_types_, settings, root_solution_, root_edge_norm_); submip_worker_->start_node = mip_node_t(root_obj, root_vstatus); submip_worker_->leaf_vstatus = root_vstatus; submip_worker_->leaf_solution.x = sol; From d7ba47b36270d7b215af8c0357834f0aabca4a3a Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 21 Aug 2026 10:28:44 +0200 Subject: [PATCH 14/29] code cleanup Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 18 +++++++----------- .../branch_and_bound/deterministic_workers.hpp | 14 ++++---------- cpp/src/mip_heuristics/root_heuristics.hpp | 1 + 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 6969e4a2aa..24fb505f8a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1269,15 +1269,15 @@ struct deterministic_diving_policy_t switch (this->worker.diving_type) { case search_strategy_t::PSEUDOCOST_DIVING: return pseudocost_diving( - this->worker.pc_snapshot, fractional, x, *this->worker.root_solution, log); + this->worker.pc_snapshot, fractional, x, this->worker.root_solution, log); case search_strategy_t::LINE_SEARCH_DIVING: - return line_search_diving(fractional, x, *this->worker.root_solution, log); + return line_search_diving(fractional, x, this->worker.root_solution, log); case search_strategy_t::GUIDED_DIVING: if (this->worker.incumbent_snapshot.empty()) { return pseudocost_diving( - this->worker.pc_snapshot, fractional, x, *this->worker.root_solution, log); + this->worker.pc_snapshot, fractional, x, this->worker.root_solution, log); } else { return guided_diving( this->worker.pc_snapshot, fractional, x, this->worker.incumbent_snapshot, log); @@ -2195,7 +2195,6 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so if (!incumbent_.has_incumbent) return false; if (rins_worker_pool_.num_idle() == 0) return false; - bool is_root_heuristic = false; diving_worker_t* worker = rins_worker_pool_.pop_idle_worker(); if (!worker) return false; @@ -3573,7 +3572,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut omp_atomic_t root_worker_count = 0; std::list> root_heuristics; - launch_root_heuristics(original_lp_, root_relax_soln_.x, 0, root_heuristics, root_worker_count); f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; @@ -3605,6 +3603,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::OPTIMAL; } + launch_root_heuristics( + original_lp_, root_relax_soln_.x, cut_pass, root_heuristics, root_worker_count); + cut_pass_result_t cut_pass_result; cut_pass_result = do_cut_pass(cut_pass, solution, @@ -3643,10 +3644,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return cut_pass_result.status; } if (cut_pass_result.action == cut_pass_action_t::BREAK) { break; } - - set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); - launch_root_heuristics( - original_lp_, root_relax_soln_.x, cut_pass + 1, root_heuristics, root_worker_count); } // Publish the post-cuts root LP value. @@ -4029,8 +4026,7 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri var_types_, settings_, root_relax_soln_.x, - edge_norms_, - &root_relax_soln_.x); + edge_norms_); } } diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index 402df18e1c..7c426452a8 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -293,9 +293,6 @@ class deterministic_diving_worker_t std::vector dive_lower; std::vector dive_upper; - // Root LP relaxation solution (constant, set once at construction) - const std::vector* root_solution{nullptr}; - // Diving state bool recompute_bounds_and_basis{true}; @@ -312,8 +309,7 @@ class deterministic_diving_worker_t const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, const std::vector& root_solution, - const std::vector& root_edge_norm, - const std::vector* root_sol) + const std::vector& root_edge_norm) : base_t(id, original_lp, Arow, @@ -322,8 +318,7 @@ class deterministic_diving_worker_t root_solution, root_edge_norm, "Diving_Worker_" + std::to_string(id)), - diving_type(type), - root_solution(root_sol) + diving_type(type) { dive_lower = original_lp.lower; dive_upper = original_lp.upper; @@ -467,14 +462,13 @@ class deterministic_diving_worker_pool_t const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, const std::vector& root_solution, - const std::vector& root_edge_norm, - const std::vector* root_sol) + const std::vector& root_edge_norm) { this->workers_.reserve(num_workers); for (int i = 0; i < num_workers; ++i) { search_strategy_t type = diving_types[i % diving_types.size()]; this->workers_.emplace_back( - i, type, original_lp, Arow, var_types, settings, root_solution, root_edge_norm, root_sol); + i, type, original_lp, Arow, var_types, settings, root_solution, root_edge_norm); } } diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index 50687fc07b..1e4b25a3d8 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -43,6 +43,7 @@ struct root_heuristics_t { if (submip_worker_) { diving_worker_t* worker = submip_worker_.get(); #pragma omp taskwait depend(in : *worker) + submip_worker_.reset(); } } From e7d2ba04ca14a57ae816a5c8b6b9ae966a64e789 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 21 Aug 2026 10:35:14 +0200 Subject: [PATCH 15/29] revert changes to the halt mechanism for sub-MIP (it was causing crashes) Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 44 +++++++------------ cpp/src/branch_and_bound/branch_and_bound.hpp | 15 ++----- cpp/src/branch_and_bound/worker.hpp | 2 + 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 24fb505f8a..98316266c3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1523,8 +1523,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( branch_and_bound_worker_t* worker, branch_and_bound_stats_t& stats, logger_t& log, - i_t iter_limit, - std::atomic* halt) + i_t iter_limit) { raft::common::nvtx::range scope("BB::solve_node"); #ifdef DEBUG_BRANCHING @@ -1563,7 +1562,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( #endif simplex_solver_settings_t lp_settings = settings_; - lp_settings.concurrent_halt = halt ? halt : &node_concurrent_halt_; + lp_settings.concurrent_halt = &node_concurrent_halt_; lp_settings.set_log(false); f_t cutoff = upper_bound_.load(); if (original_lp_.objective_step.has_step()) { @@ -1690,8 +1689,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, bool can_launch_rins = true; - while (stack.size() > 0 && - (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) && + while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { if (worker->worker_id == 0) { repair_heuristic_solutions(); } @@ -1940,9 +1938,8 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t worker->calculate_max_diving_workers(bfs_worker_pool_.size(), diving_worker_pool_.size()); worker->update_diving_heuristic_list(diving_settings); - while ((solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) && - abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && - node_queue.best_first_queue_size() > 0) { + while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && + rel_gap > settings_.relative_mip_gap_tol && node_queue.best_first_queue_size() > 0) { if (submip_halt_callback_) { // Stops the solver if the callback returns "true". This happens when the lower bound // in the sub-MIP solve is greater than the upper bound of the main solve (this can @@ -2058,8 +2055,7 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker, f_t rel_gap = user_relative_gap(user_obj, user_lower); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); - while (stack.size() > 0 && - (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted()) && + while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -2232,8 +2228,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix, - std::atomic* halt) + std::string_view log_prefix) { double start_time = tic(); @@ -2268,7 +2263,6 @@ 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; #ifdef SAVE_SUBMIP_TO_FILE submip_settings.log.log_prefix = std::format("{}{}", settings_.log.log_prefix, worker->worker_id); @@ -2357,8 +2351,6 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke return; } - if (halt && 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; @@ -2396,12 +2388,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](f_t, f_t submip_lower_bound) { + submip_bnb.set_submip_halt_callback([this, worker](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 || this->is_halted(); + return is_cutoff || !is_solver_running || worker->halt; }); } @@ -2592,8 +2584,7 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings template void branch_and_bound_t::rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types, - std::atomic* halt) + const std::vector& var_types) { raft::common::nvtx::range scope("BB::rins_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } @@ -2628,8 +2619,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, i_t min_var_fixed = min_fixrate * num_integers; i_t num_var_fixed = 0; - while (solver_status_ == mip_status_t::UNSET && is_running_ && !this->is_halted() && - (halt ? !halt->load() : true)) { + while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the // current incumbent, considering only the fractional values in the current node i_t prev_num_fixed = num_var_fixed; @@ -2727,11 +2717,10 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // After fixing the variables, re-solve the LP relaxation. We use the optimal solution // in the next iteration to find additional variable fixings. - // We continue to do this until enough variables were fixed or no variable is left to fix - i_t iter_max = std::numeric_limits::max(); + // We continue to do this until enough variables were fixed or no variable is left to fix. logger_t log; log.log = false; - dual_status_t lp_status = solve_node_lp(&node, worker, rins_stats, log, iter_max, halt); + dual_status_t lp_status = solve_node_lp(&node, worker, rins_stats, log); if (lp_status != dual_status_t::OPTIMAL) { break; } @@ -2801,8 +2790,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, num_var_fixed, num_integers, submip_level, - log_prefix, - halt); + log_prefix); } } @@ -2880,7 +2868,7 @@ 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. - rins(worker, current_incumbent, heuristic->var_types_, &heuristic->halt_); + rins(worker, current_incumbent, heuristic->var_types_); } else { ++worker_count; @@ -2888,7 +2876,7 @@ void branch_and_bound_t::launch_root_heuristics( shared(heuristics, worker_count) firstprivate(worker, current_incumbent, heuristic) \ depend(out : *worker) { - rins(worker, current_incumbent, heuristic->var_types_, &heuristic->halt_); + rins(worker, current_incumbent, heuristic->var_types_); --worker_count; } } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index c816059ca7..cdf99acafc 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -346,12 +346,6 @@ class branch_and_bound_t { // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); - bool is_halted() - { - return settings_.concurrent_halt ? settings_.concurrent_halt->load(std::memory_order_acquire) - : false; - } - // Launch a new diving worker from a given best-first worker. bool launch_diving_worker(bfs_worker_t* bfs_worker); @@ -389,14 +383,12 @@ class branch_and_bound_t { i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix, - std::atomic* halt = nullptr); + std::string_view log_prefix); // Creates and solves the RINS sub-MIP void rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types, - std::atomic* halt = nullptr); + const std::vector& var_types); void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, @@ -409,8 +401,7 @@ class branch_and_bound_t { branch_and_bound_worker_t* worker, branch_and_bound_stats_t& stats, simplex::logger_t& log, - i_t iter_limit = std::numeric_limits::max(), - std::atomic* halt = nullptr); + i_t iter_limit = std::numeric_limits::max()); // Apply symmetry-based bound reductions (orbital fixing and, when // settings_.symmetry == 2, lexical reduction) to the current node. diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 28c6edffe6..50e9a4a206 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -253,6 +253,8 @@ 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 c3a7b29bc11b05836a6f96a4aaeb4d8ac73dec58 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 21 Aug 2026 17:11:16 +0200 Subject: [PATCH 16/29] added missing halt after revert Signed-off-by: Nicolas L. Guidotti --- cpp/src/mip_heuristics/root_heuristics.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index 1e4b25a3d8..d9b1fe7d4a 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -19,7 +19,6 @@ struct root_heuristics_t { csr_matrix_t Arow_; std::vector root_solution_; std::vector root_edge_norm_; - std::atomic halt_{false}; std::unique_ptr> submip_worker_; fj_cpu_worker_t fj_cpu_worker_; @@ -38,10 +37,10 @@ struct root_heuristics_t { void stop() { - halt_.store(true, std::memory_order_release); fj_cpu_worker_.stop(); if (submip_worker_) { diving_worker_t* worker = submip_worker_.get(); + worker->halt = true; #pragma omp taskwait depend(in : *worker) submip_worker_.reset(); } From 2dd6573849fc004d238af76f7feff3c6d48c97b9 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 21 Aug 2026 19:26:11 +0200 Subject: [PATCH 17/29] increase time limit for the node limit test Signed-off-by: Nicolas L. Guidotti --- cpp/tests/mip/miplib_test.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index 924763c437..3bdd6f51ff 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -97,7 +97,7 @@ TEST(mip_solve, node_limit_test) { mip_solver_settings_t settings; settings.node_limit = 1000; - settings.time_limit = 60; + settings.time_limit = 120; settings.num_cpu_threads = 8; double expect_obj = 3.8151140644999992e+02; From 66dc954ee93d1a8ed86dc44471995a066a5dccf2 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Sat, 22 Aug 2026 18:18:58 +0200 Subject: [PATCH 18/29] use shared_ptr to tie the lifetime of the object to the task. this allows the solver to asynchronously stop the tasks. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 68 ++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 3 +- .../mip_heuristics/feasibility_jump/fj_cpu.cu | 37 +++++---- .../feasibility_jump/fj_cpu_worker.cuh | 9 ++- cpp/src/mip_heuristics/root_heuristics.hpp | 78 +++++++++++++++++-- 5 files changed, 135 insertions(+), 60 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 98316266c3..2e0334b4e6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2814,7 +2814,11 @@ void branch_and_bound_t::rins(diving_worker_t* worker, min_var_fixed); // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. - rins_worker_pool_.return_worker_to_pool(worker); + if (!during_cut_passes_) { + rins_worker_pool_.return_worker_to_pool(worker); + } else { + worker->set_inactive(); + } } template @@ -2822,43 +2826,42 @@ void branch_and_bound_t::launch_root_heuristics( const lp_problem_t& lp, const std::vector& sol, i_t cut_pass, - std::list>& heuristics, - omp_atomic_t& worker_count) + root_heuristics_t& root_heuristics) { if (settings_.deterministic) return; if (settings_.num_threads < 2) return; - // If we already exhausted all threads for the root heuristics, stop workers for the - // oldest set of heuristics launched. Leave 2 threads for the cut passes and the clique - // table generation. Add the number of workers that will be launched (1 submip worker + - // 1 CPU FJ worker). - i_t clique_table_generation = cut_pass == 0 ? 1 : 0; - if (worker_count + 3 + clique_table_generation > settings_.num_threads && !heuristics.empty()) { - heuristics.erase(heuristics.begin()); - } - - i_t id = cut_pass; - root_heuristics_t* heuristic = - &heuristics.emplace_back(Arow_, var_types_, sol, edge_norms_); + // Using shared_ptr here, so the lifetime of the object is tied to the related task. This allows + // the solver to send the stop signal and immediately continue the execution. + auto current_heuristic = + root_heuristics.create_new_cut_pass_heuristic(cut_pass, Arow_, var_types_, sol, edge_norms_); + auto worker_count = root_heuristics.worker_count_; constexpr bool is_cpufj_enabled = true; if (is_cpufj_enabled) { - fj_cpu_worker_t* worker = &heuristic->fj_cpu_worker_; - f_t work_limit = std::numeric_limits::infinity(); f_t time_limit = settings_.time_limit - toc(exploration_stats_.start_time); - worker->improvement_callback = + current_heuristic->fj_cpu_worker_.improvement_callback = [this](f_t obj, const std::vector& assignment, double work_units) { set_solution_from_cpu_fj(obj, assignment, work_units); }; - worker->create_worker(lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); - worker->run_async(time_limit, work_limit, &worker_count); + current_heuristic->fj_cpu_worker_.create_worker( + lp, var_types_, sol, settings_, "[RootCut CPUFJ] "); + ++(*worker_count); + +#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) \ + affinity(current_heuristic -> fj_cpu_worker_) firstprivate(current_heuristic, worker_count) \ + depend(out : current_heuristic->fj_cpu_worker_.fj_cpu) + { + current_heuristic->fj_cpu_worker_.run_sync(time_limit, work_limit); + --(*worker_count); + } } if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent) { - diving_worker_t* worker = - heuristic->create_submip_worker(id, lp, settings_, root_objective_, root_vstatus_, sol); + diving_worker_t* worker = current_heuristic->create_submip_worker( + cut_pass, lp, settings_, root_objective_, root_vstatus_, sol); std::vector current_incumbent; mutex_upper_.lock(); @@ -2868,16 +2871,15 @@ 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. - rins(worker, current_incumbent, heuristic->var_types_); + rins(worker, current_incumbent, current_heuristic->var_types_); } else { - ++worker_count; -#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ - shared(heuristics, worker_count) firstprivate(worker, current_incumbent, heuristic) \ - depend(out : *worker) + ++(*worker_count); +#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ + firstprivate(current_incumbent, current_heuristic, worker_count) depend(out : *worker) { - rins(worker, current_incumbent, heuristic->var_types_); - --worker_count; + rins(worker, current_incumbent, current_heuristic->var_types_); + --(*worker_count); } } } @@ -3558,8 +3560,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut compute_user_objective(original_lp_, root_relax_objective); } - omp_atomic_t root_worker_count = 0; - std::list> root_heuristics; + root_heuristics_t root_heuristics(settings_.num_threads - 1); f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; @@ -3591,8 +3592,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::OPTIMAL; } - launch_root_heuristics( - original_lp_, root_relax_soln_.x, cut_pass, root_heuristics, root_worker_count); + 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, @@ -3658,7 +3658,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Stops the root heuristics and clear the associated data - root_heuristics.clear(); + root_heuristics.stop_and_sync(); during_cut_passes_ = false; set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index cdf99acafc..7990e5e467 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -393,8 +393,7 @@ class branch_and_bound_t { void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, i_t cut_pass, - std::list>& heuristics, - omp_atomic_t& worker_count); + root_heuristics_t& root_heuristics); // Solve the LP relaxation of a leaf node simplex::dual_status_t solve_node_lp(mip_node_t* node_ptr, diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index 4e830d7467..b789159953 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -2109,43 +2109,52 @@ void fj_cpu_worker_t::create_worker( fj_cpu.reset(new_climber.release()); fj_cpu->log_prefix = std::move(log_prefix); fj_cpu->improvement_callback = improvement_callback; + fj_cpu->halted = false; + preemption_flag = false; + is_initialized = true; } template -void fj_cpu_worker_t::run_async(f_t time_limit, - double work_unit_limit, - omp_atomic_t* worker_count) +void fj_cpu_worker_t::run_async(f_t time_limit, double work_unit_limit) { - if (!fj_cpu) return; + if (!is_initialized) return; - if (worker_count) ++(*worker_count); -#pragma omp task shared(fj_cpu) firstprivate(time_limit, work_unit_limit, worker_count) \ - priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : *fj_cpu) + auto& fj_ptr = fj_cpu; +#pragma omp task shared(fj_cpu, is_initialized, fj_ptr) firstprivate(time_limit, work_unit_limit) \ + priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : fj_ptr) { - cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); - if (worker_count) --(*worker_count); + if (is_initialized) { cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); } } } template void fj_cpu_worker_t::run_sync(f_t time_limit, double work_unit_limit) { - if (!fj_cpu) return; + if (!is_initialized) return; cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); + is_initialized = false; fj_cpu.reset(); } template void fj_cpu_worker_t::stop() { - if (!fj_cpu) return; + if (!is_initialized) return; - fj_cpu->preemption_flag = true; - fj_cpu->halted = true; -#pragma omp taskwait depend(in : *fj_cpu) + preemption_flag = true; + + auto& fj_ptr = fj_cpu; +#pragma omp taskwait depend(in : fj_ptr) + is_initialized = false; fj_cpu.reset(); } +template +void fj_cpu_worker_t::send_stop_signal() +{ + preemption_flag = true; +} + #if MIP_INSTANTIATE_FLOAT template class fj_t; template struct fj_cpu_worker_t; diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh index d594eef8dc..bb2c69f81c 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh @@ -30,6 +30,8 @@ struct fj_cpu_worker_t { struct fj_cpu_deleter_t { void operator()(fj_cpu_climber_t* ptr) const; }; + + std::atomic is_initialized{false}; std::atomic preemption_flag{false}; std::unique_ptr, fj_cpu_deleter_t> fj_cpu; std::function&, double)> improvement_callback; @@ -49,15 +51,16 @@ struct fj_cpu_worker_t { // Run the worker asynchronously (i.e., launch an openmp task and then continue the // execution). Call `stop()` for stopping the worker - void run_async(f_t time_limit = std::numeric_limits::infinity(), - double work_unit_limit = std::numeric_limits::infinity(), - omp_atomic_t* worker_count = nullptr); + void run_async(f_t time_limit = std::numeric_limits::infinity(), + double work_unit_limit = std::numeric_limits::infinity()); // Run the CPU FJ synchronously (i.e., wait for it to finish before proceeding) void run_sync(f_t time_limit = std::numeric_limits::infinity(), double work_unit_limit = std::numeric_limits::infinity()); void stop(); + + void send_stop_signal(); }; } // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index d9b1fe7d4a..f54aff7c1b 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -14,7 +14,7 @@ namespace cuopt::mathematical_optimization::mip { template -struct root_heuristics_t { +struct cut_pass_heuristics_t { std::vector var_types_; csr_matrix_t Arow_; std::vector root_solution_; @@ -23,19 +23,25 @@ struct root_heuristics_t { std::unique_ptr> submip_worker_; fj_cpu_worker_t fj_cpu_worker_; - root_heuristics_t(const csr_matrix_t& Arow, - const std::vector& var_types, - const std::vector& root_solution, - const std::vector& root_edge_norm) + cut_pass_heuristics_t(const csr_matrix_t& Arow, + const std::vector& var_types, + const std::vector& root_solution, + const std::vector& root_edge_norm) : var_types_(var_types), Arow_(Arow), root_solution_(root_solution), root_edge_norm_(root_edge_norm), submip_worker_(nullptr) {}; - ~root_heuristics_t() { stop(); } + ~cut_pass_heuristics_t() { stop_and_sync(); } + + void send_stop_signal() + { + fj_cpu_worker_.send_stop_signal(); + if (submip_worker_) { submip_worker_->halt = true; } + } - void stop() + void stop_and_sync() { fj_cpu_worker_.stop(); if (submip_worker_) { @@ -67,4 +73,62 @@ struct root_heuristics_t { return submip_worker_.get(); } }; + +/// \brief Object Representing the heuristics run on the root node. +template +struct root_heuristics_t { + // List of the heuristics that run alongside a single cut pass. + // It holds the workers and all the necessary information. + // + // We use the `shared_ptr` here so the object is only destroyed when the task terminates + // (we declare the `shared_ptr` as firstprivate in the task, so they live until the end the + // task). In this way, we can send the stop signal, destroy the entry in the list and the + // object itself will be destroyed when all related tasks ends. + std::list>> cut_passes_heuristics_; + + // Count the number of active workers. Same reason as above. + std::shared_ptr> worker_count_; + i_t max_workers_; + + root_heuristics_t(i_t max_workers) + : worker_count_(std::make_shared>(0)), max_workers_(max_workers) + { + } + + ~root_heuristics_t() { stop_and_sync(); } + + void stop_and_sync() + { + for (auto& heuristic : cut_passes_heuristics_) { + heuristic->send_stop_signal(); + } + + for (auto& heuristic : cut_passes_heuristics_) { + heuristic->stop_and_sync(); + } + } + + std::shared_ptr> create_new_cut_pass_heuristic( + i_t cut_pass, + const csr_matrix_t& Arow, + const std::vector& var_types, + const std::vector& root_solution, + const std::vector& root_edge_norm) + { + // If we already exhausted all threads for the root heuristics, stop workers for the + // oldest set of heuristics launched. Leave 2 threads for the cut passes and the clique + // table generation. Add the number of workers that will be launched (1 submip worker + + // 1 CPU FJ worker). + i_t clique_table_generation = cut_pass == 0 ? 1 : 0; + if (*worker_count_ + 3 + clique_table_generation > max_workers_ && + !cut_passes_heuristics_.empty()) { + cut_passes_heuristics_.begin()->get()->send_stop_signal(); + cut_passes_heuristics_.erase(cut_passes_heuristics_.begin()); + } + + return cut_passes_heuristics_.emplace_back(std::make_shared>( + Arow, var_types, root_solution, root_edge_norm)); + } +}; + } // namespace cuopt::mathematical_optimization::mip From 83c72a7e5968a6b5affbd72b5dc45e88d9fc9b73 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 24 Aug 2026 12:10:40 +0200 Subject: [PATCH 19/29] re-introduced the is_root_heuristic flag to prevent crashes during the asynchronous stop. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 27 ++++++++++--------- cpp/src/branch_and_bound/branch_and_bound.hpp | 7 ++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 2e0334b4e6..1454884a0e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2228,7 +2228,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix) + std::string_view log_prefix, + bool is_root_heuristic) { double start_time = tic(); @@ -2380,7 +2381,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); submip_bnb.set_initial_upper_bound(submip_cutoff); - if (!during_cut_passes_) + if (!is_root_heuristic) submip_bnb.set_initial_pseudocost(pc_, presolver.get_reduced_to_original_map()); if (submip_halt_callback_) { @@ -2584,7 +2585,8 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings template void branch_and_bound_t::rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types) + const std::vector& var_types, + bool is_root_heuristic) { raft::common::nvtx::range scope("BB::rins_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } @@ -2777,7 +2779,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, } // We need the pseudocost to do the DFS, which we do not have during the cut passes. - if (!during_cut_passes_) { + if (!is_root_heuristic) { DEBUG_SUBMIP("{} Running a quick DFS for the submip!", log_prefix); dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } @@ -2790,7 +2792,8 @@ void branch_and_bound_t::rins(diving_worker_t* worker, num_var_fixed, num_integers, submip_level, - log_prefix); + log_prefix, + is_root_heuristic); } } @@ -2814,7 +2817,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, min_var_fixed); // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. - if (!during_cut_passes_) { + if (!is_root_heuristic) { rins_worker_pool_.return_worker_to_pool(worker); } else { worker->set_inactive(); @@ -2835,9 +2838,10 @@ void branch_and_bound_t::launch_root_heuristics( // the solver to send the stop signal and immediately continue the execution. auto current_heuristic = root_heuristics.create_new_cut_pass_heuristic(cut_pass, Arow_, var_types_, sol, edge_norms_); - auto worker_count = root_heuristics.worker_count_; + auto worker_count = root_heuristics.worker_count_; + constexpr bool is_root_heuristic = true; + constexpr bool is_cpufj_enabled = true; - constexpr bool is_cpufj_enabled = true; if (is_cpufj_enabled) { f_t work_limit = std::numeric_limits::infinity(); f_t time_limit = settings_.time_limit - toc(exploration_stats_.start_time); @@ -2871,14 +2875,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. - rins(worker, current_incumbent, current_heuristic->var_types_); + rins(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); } else { ++(*worker_count); #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(current_incumbent, current_heuristic, worker_count) depend(out : *worker) { - rins(worker, current_incumbent, current_heuristic->var_types_); + rins(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); --(*worker_count); } } @@ -3525,7 +3529,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } is_running_ = true; - during_cut_passes_ = true; lower_bound_numerical_ = inf; if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } @@ -3659,8 +3662,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // Stops the root heuristics and clear the associated data root_heuristics.stop_and_sync(); - during_cut_passes_ = false; - set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 7990e5e467..fb3f9fce5d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -281,7 +281,6 @@ class branch_and_bound_t { // Global status of the solver. omp_atomic_t solver_status_; omp_atomic_t is_running_{false}; - omp_atomic_t during_cut_passes_{false}; // Minimum number of node in the queue. When the queue size is less than // this variable, the nodes are added directly to the queue instead of @@ -383,12 +382,14 @@ class branch_and_bound_t { i_t num_var_fixed, i_t num_integers, i_t submip_level, - std::string_view log_prefix); + std::string_view log_prefix, + bool is_root_heuristic = false); // Creates and solves the RINS sub-MIP void rins(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types); + const std::vector& var_types, + bool is_root_heuristic = false); void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, From 31b3b87b7ef4b53997228c24955e91c15c4cd8fc Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 11 Aug 2026 16:40:23 +0200 Subject: [PATCH 20/29] first implementation of the RENS heuristic Signed-off-by: Nicolas L. Guidotti --- .../mathematical_optimization/constants.h | 1 + .../mip/submip_hyper_params.hpp | 3 +- cpp/src/branch_and_bound/branch_and_bound.cpp | 295 ++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 10 +- cpp/src/branch_and_bound/constants.hpp | 4 + cpp/src/math_optimization/solver_settings.cu | 1 + .../diversity/recombiners/sub_mip.cuh | 1 + cpp/src/mip_heuristics/root_heuristics.hpp | 5 +- 8 files changed, 186 insertions(+), 134 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 7128fcf0e9..6cdc5bb2c6 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -78,6 +78,7 @@ #define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts" #define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening" #define CUOPT_MIP_RINS "mip_rins" +#define CUOPT_MIP_RENS "mip_rens" #define CUOPT_MIP_OBJECTIVE_STEP "mip_objective_step" #define CUOPT_MIP_CUT_CHANGE_THRESHOLD "mip_cut_change_threshold" #define CUOPT_MIP_CUT_MIN_ORTHOGONALITY "mip_cut_min_orthogonality" diff --git a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp index 4464a156a7..33c8817866 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp @@ -12,8 +12,9 @@ */ template struct mip_submip_hyper_params_t { - // Enable or disable (recursive) RINS: -1 automatic, 0 disabled, 1 enabled + // Enable or disable (recursive) RINS/RENS: -1 automatic, 0 disabled, 1 enabled i_t rins = -1; + i_t rens = -1; // Base for calculating the target fix rate for the neighbourhood. Actual target value is // determined automatically according to the success and infeasible rate. diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 1454884a0e..68aa1f9ce8 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -213,7 +213,7 @@ inline char feasible_solution_symbol(heuristics_origin_t origin) inline char feasible_solution_symbol(search_strategy_t strategy, bool show_diving) { if (strategy == search_strategy_t::BEST_FIRST) return 'B'; - if (strategy == search_strategy_t::RINS) return 'S'; + if (strategy == search_strategy_t::RINS || strategy == search_strategy_t::RENS) return 'S'; if (!show_diving) return 'D'; switch (strategy) { @@ -225,6 +225,7 @@ inline char feasible_solution_symbol(search_strategy_t strategy, bool show_divin case search_strategy_t::FARKAS_DIVING: return 'F'; case search_strategy_t::VECTOR_LENGTH_DIVING: return 'V'; case search_strategy_t::RINS: return 'S'; + case search_strategy_t::RENS: return 'S'; } return 'U'; @@ -682,7 +683,7 @@ void branch_and_bound_t::set_solution_from_submip( settings_.log.debug_format("SubMIP found a feasible solution with obj={:.4g}", obj); bool success = set_solution_from_heuristics(user_sol, heuristics_origin_t::SUBMIP); if (success) { - rins_stats_.save_success(fixrate); + submip_stats_.save_success(fixrate); if (settings_.solution_callback != nullptr) { settings_.solution_callback(user_sol, obj); } } } @@ -1033,6 +1034,7 @@ branch_variable_t branch_and_bound_t::variable_selection( return vector_length_diving(worker->leaf_problem, fractional, solution, log); case search_strategy_t::RINS: // This is used for solving the DFS of the sub-MIP. + case search_strategy_t::RENS: branch_var = pc_.variable_selection(fractional, solution); round_dir = martin_criteria(solution[branch_var], worker->root_solution[branch_var]); return {branch_var, round_dir}; @@ -1797,7 +1799,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, worker->recompute_bounds = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::HAS_CHILDREN) { - if (can_launch_rins) { can_launch_rins = !launch_rins_worker(worker->leaf_solution.x); } + if (can_launch_rins) { can_launch_rins = !launch_submip_worker(worker->leaf_solution.x); } // The stack should only contain the children of the current parent. // If the stack size is greater than 0, @@ -2127,7 +2129,8 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker, // This is called from the RINS method which already handle the return to the // pool part. Besides, they do not share the same pool. - if (worker->search_strategy != search_strategy_t::RINS) { + if (worker->search_strategy != search_strategy_t::RINS && + worker->search_strategy != search_strategy_t::RENS) { diving_worker_pool_.return_worker_to_pool(worker); } } @@ -2185,18 +2188,19 @@ bool branch_and_bound_t::launch_diving_worker(bfs_worker_t* } template -bool branch_and_bound_t::launch_rins_worker(const std::vector& sol) +bool branch_and_bound_t::launch_submip_worker(const std::vector& sol) { - if (settings_.submip_settings.rins == 0) return false; - if (!incumbent_.has_incumbent) return false; - if (rins_worker_pool_.num_idle() == 0) return false; + if (settings_.submip_settings.rins == 0 && settings_.submip_settings.rens == 0) return false; + if (settings_.submip_settings.rens == 0 && !incumbent_.has_incumbent) return false; + if (submip_worker_pool_.num_idle() == 0) return false; - diving_worker_t* worker = rins_worker_pool_.pop_idle_worker(); + diving_worker_t* worker = submip_worker_pool_.pop_idle_worker(); if (!worker) return false; std::vector current_incumbent; mutex_upper_.lock(); - current_incumbent = incumbent_.x; + bool use_rins = incumbent_.has_incumbent && settings_.submip_settings.rins != 0; + if (use_rins) current_incumbent = incumbent_.x; mutex_upper_.unlock(); // Note that this node does not have the vstatus (it was cleared at the start of B&B exploration) @@ -2205,22 +2209,24 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so worker->leaf_problem.lower = original_lp_.lower; worker->leaf_problem.upper = original_lp_.upper; worker->leaf_solution.x = sol; - worker->search_strategy = search_strategy_t::RINS; + worker->search_strategy = use_rins ? search_strategy_t::RINS : search_strategy_t::RENS; worker->set_active(); if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - rins(worker, current_incumbent, var_types_); + recursive_submip(worker, current_incumbent, var_types_); } else { #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(worker, current_incumbent) - rins(worker, current_incumbent, var_types_); + recursive_submip(worker, current_incumbent, var_types_); } return true; } +#define DEBUG_SUBMIP + template void branch_and_bound_t::solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, @@ -2242,8 +2248,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); if (!feasible) { - // This should never happen since we are fixing bounds that are already in the incumbent. - rins_stats_.save_infeasible(fixrate); + // RINS: This should never happen since we are fixing bounds that are already in the incumbent. + submip_stats_.save_infeasible(fixrate); DEBUG_SUBMIP("{} The problem is infeasible after running bound strengthening!", log_prefix); return; } @@ -2285,6 +2291,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke 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 && submip_level <= settings_.submip_settings.max_level; DEBUG_SUBMIP("{}Sub-MIP: num variables fixed={}/{} ({:.2f}%)", log_prefix, @@ -2318,7 +2326,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (presolver_status == third_party_presolve_status_t::INFEASIBLE || presolver_status == third_party_presolve_status_t::UNBNDORINFEAS || presolver_status == third_party_presolve_status_t::UNBOUNDED) { - rins_stats_.save_infeasible(fixrate); + submip_stats_.save_infeasible(fixrate); return; } @@ -2370,16 +2378,24 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke branch_and_bound_t submip_bnb(submip_problem, submip_settings, tic(), empty_probing); mip_solution_t submip_solution(submip_problem.num_cols); - // Crush the incumbent to presolve space. It may not be valid for the sub-MIP since we - // may fix integer variables that does not match the current incumbent to reach the target - // fix rate. std::vector presolved_incumbent; - presolver.crush_primal_solution(submip_problem, current_incumbent, presolved_incumbent); - submip_bnb.set_initial_guess(presolved_incumbent); - const f_t user_upper = compute_user_objective(worker->leaf_problem, upper_bound_.load()); - const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); - submip_bnb.set_initial_upper_bound(submip_cutoff); + // We do not have an incumbent yet, so skip the initial guess. + if (!current_incumbent.empty()) { + // Crush the incumbent to presolve space. It may not be valid for the sub-MIP since we + // may fix integer variables that does not match the current incumbent to reach the target + // fix rate. + presolver.crush_primal_solution(submip_problem, current_incumbent, presolved_incumbent); + submip_bnb.set_initial_guess(presolved_incumbent); + } + + // Even if we do not have a valid incumbent now, the upper bound can still be set by the early + // heuristics. + if (std::isfinite(upper_bound_.load())) { + const f_t user_upper = compute_user_objective(worker->leaf_problem, upper_bound_.load()); + const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); + submip_bnb.set_initial_upper_bound(submip_cutoff); + } if (!is_root_heuristic) submip_bnb.set_initial_pseudocost(pc_, presolver.get_reduced_to_original_map()); @@ -2401,6 +2417,12 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke fj_cpu_worker_t submip_fj_cpu_worker; if (settings_.submip_settings.enable_cpufj) { + // Since we do not have an incumbent, use the LP solution of the last round of variable fixing + // in RENS. + if (worker->search_strategy == search_strategy_t::RENS) { + presolver.crush_primal_solution(submip_problem, worker->leaf_solution.x, presolved_incumbent); + } + // Launch a CPU FJ worker on the presolved sub-MIP with a fixed budget (in terms of work units) // to run in parallel with the cut-and-branch algorithm with the goal of finding a quick // feasible solution for the sub-MIP problem. The CPU FJ uses the current incumbent (crushed @@ -2443,7 +2465,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (submip_status == mip_status_t::NUMERICAL) { return; } if (submip_status == mip_status_t::INFEASIBLE || submip_status == mip_status_t::UNBOUNDED) { - rins_stats_.save_infeasible(fixrate); + submip_stats_.save_infeasible(fixrate); return; } @@ -2510,11 +2532,40 @@ void fix_variable(i_t j, } template -void apply_rins_fixings(const simplex_solver_settings_t& settings, +bool apply_rens_fixings(const simplex_solver_settings_t& settings, + const std::vector& node_solution, + const std::vector& integer_list, + i_t max_var_fixed, + i_t min_var_fixed, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed, + i_t& num_var_fixed) +{ + for (i_t j : integer_list) { + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { continue; } + f_t old_lower = lower[j]; + f_t old_upper = upper[j]; + lower[j] = std::clamp(std::floor(node_solution[j]), old_lower, old_upper); + upper[j] = std::clamp(std::ceil(node_solution[j]), old_lower, old_upper); + bounds_changed[j] = lower[j] != old_lower || upper[j] != old_upper; + + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { + ++num_var_fixed; + if (num_var_fixed >= max_var_fixed) break; + } + } + + return num_var_fixed >= min_var_fixed; +} + +template +bool apply_rins_fixings(const simplex_solver_settings_t& settings, const std::vector& current_sol, const std::vector& fractional, const std::vector& current_incumbent, i_t max_var_fixed, + i_t min_var_fixed, std::vector& lower, std::vector& upper, std::vector& bounds_changed, @@ -2529,15 +2580,18 @@ void apply_rins_fixings(const simplex_solver_settings_t& settings, if (num_var_fixed >= max_var_fixed) break; } } + + return num_var_fixed >= min_var_fixed; } template -void extend_variable_fixings(const simplex_solver_settings_t& settings, +bool extend_variable_fixings(const simplex_solver_settings_t& settings, const std::vector& obj_coeffs, const std::vector& fractional, const std::vector& current_sol, const std::vector& root_solution, i_t max_var_fixed, + i_t min_var_fixed, std::vector& lower, std::vector& upper, std::vector& bounds_changed, @@ -2580,27 +2634,30 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings change += dist; if (change >= 0.5) { break; } } + + return num_var_fixed >= min_var_fixed; } template -void branch_and_bound_t::rins(diving_worker_t* worker, - const std::vector& current_incumbent, +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) { - raft::common::nvtx::range scope("BB::rins_thread"); + 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 = std::format("[RINS {}] ", submip_level); + i_t submip_level = settings_.submip_settings.level + 1; + std::string log_prefix = + std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); - ++rins_stats_.total_calls; + ++submip_stats_.total_calls; bool has_submip = false; worker->recompute_bounds = false; worker->recompute_basis = true; - branch_and_bound_stats_t rins_stats; + branch_and_bound_stats_t stats; mip_node_t& node = worker->start_node; std::vector& lower = worker->leaf_problem.lower; std::vector& upper = worker->leaf_problem.upper; @@ -2615,7 +2672,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, i_t num_integers = integer_list.size(); - f_t max_fixrate = submip_get_max_fixrate(rins_stats_, settings_.submip_settings, worker->rng); + f_t max_fixrate = submip_get_max_fixrate(submip_stats_, settings_.submip_settings, worker->rng); f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); i_t max_var_fixed = max_fixrate * num_integers; i_t min_var_fixed = min_fixrate * num_integers; @@ -2625,48 +2682,69 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the // current incumbent, considering only the fractional values in the current node i_t prev_num_fixed = num_var_fixed; - apply_rins_fixings(settings_, - current_sol, - fractional, - current_incumbent, - max_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - - // Enough variables has been fixed - if (num_var_fixed >= min_var_fixed) { - DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); - has_submip = true; - break; - } - if (toc(exploration_stats_.start_time) > settings_.time_limit) { - solver_status_ = mip_status_t::TIME_LIMIT; - break; + if (worker->search_strategy == search_strategy_t::RINS) { + has_submip = apply_rins_fixings(settings_, + current_sol, + fractional, + current_incumbent, + max_var_fixed, + min_var_fixed, + lower, + upper, + bounds_changed, + num_var_fixed); + if (has_submip) { break; } + + if (prev_num_fixed == num_var_fixed) { + // RINS neighbourhood 2: Search the entire list of integer variables where the current + // LP solution matches the current incumbent. + has_submip = apply_rins_fixings(settings_, + current_sol, + integer_list, + current_incumbent, + max_var_fixed, + min_var_fixed, + lower, + upper, + bounds_changed, + num_var_fixed); + if (has_submip) { break; } + } + } else if (worker->search_strategy == search_strategy_t::RENS) { + has_submip = apply_rens_fixings(settings_, + current_sol, + integer_list, + max_var_fixed, + min_var_fixed, + lower, + upper, + bounds_changed, + num_var_fixed); + if (has_submip) { break; } + } else { + assert(false && "Incorrect submip type! Must be RINS or RENS!"); } + // Even considering the entire integer list, we were unable to fix a single variable in this + // iteration. Iterate over the fractional variables again and fixing those that closest to + // an integer solution first in order to reach the fixing threshold. if (prev_num_fixed == num_var_fixed) { - // RINS neighbourhood 2: Search the entire list of integer variables where the current - // LP solution matches the current incumbent. - apply_rins_fixings(settings_, - current_sol, - integer_list, - current_incumbent, - max_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - - // Enough variables were fixed - if (num_var_fixed >= min_var_fixed) { - DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", + has_submip = extend_variable_fixings(settings_, + worker->leaf_problem.objective, + fractional, + current_sol, + worker->root_solution, + max_var_fixed, + min_var_fixed, + lower, + upper, + bounds_changed, + num_var_fixed); + if (has_submip) { break; } + + if (prev_num_fixed == num_var_fixed) { + DEBUG_SUBMIP("{}Could not fix more variables ({}, max={}, min={})\n", log_prefix, num_var_fixed, max_var_fixed, @@ -2674,42 +2752,6 @@ void branch_and_bound_t::rins(diving_worker_t* worker, has_submip = true; break; } - - // Even considering the entire integer list, we were unable to fix a single variable in this - // iteration. Iterate over the fractional variables again and fixing those that closest to - // an integer solution first in order to reach the fixing threshold. - if (prev_num_fixed == num_var_fixed) { - extend_variable_fixings(settings_, - worker->leaf_problem.objective, - fractional, - current_sol, - worker->root_solution, - max_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - - if (num_var_fixed >= min_var_fixed) { - DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); - has_submip = true; - break; - } - - if (prev_num_fixed == num_var_fixed) { - DEBUG_SUBMIP("{}Could not fix more variables ({}, max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); - has_submip = true; - break; - } - } } if (toc(exploration_stats_.start_time) > settings_.time_limit) { @@ -2722,7 +2764,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // We continue to do this until enough variables were fixed or no variable is left to fix. logger_t log; log.log = false; - dual_status_t lp_status = solve_node_lp(&node, worker, rins_stats, log); + dual_status_t lp_status = solve_node_lp(&node, worker, stats, log); if (lp_status != dual_status_t::OPTIMAL) { break; } @@ -2736,8 +2778,8 @@ void branch_and_bound_t::rins(diving_worker_t* worker, if (leaf_obj > upper_bound_.load()) { break; } if (num_frac == 0) { - // We found a feasible solution when fixing the variables in RINS. - add_feasible_solution(leaf_obj, current_sol, -1, search_strategy_t::RINS); + // We found a feasible solution when fixing the variables in RINS/RENS. + add_feasible_solution(leaf_obj, current_sol, -1, worker->search_strategy); break; } @@ -2751,7 +2793,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // found a solution that improved the incumbent, then do a DFS with a backtrack_limit of 5 // levels up to try to find a feasible solution quickly from the neighbourhood. if (fixrate < settings_.submip_settings.min_fixrate_cap || - (settings_.inside_submip && rins_stats_.total_success != 0)) { + (settings_.inside_submip && submip_stats_.total_success != 0)) { worker->start_node.packed_vstatus = simplex::compress_vstatus(worker->leaf_vstatus); worker->start_lower = lower; worker->start_upper = upper; @@ -2799,16 +2841,16 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // Accumulate the iterations for sub-MIP so it stops when it reaches the allocated budget. if (settings_.inside_submip) { - exploration_stats_.total_simplex_iters += rins_stats.total_simplex_iters; + exploration_stats_.total_simplex_iters += stats.total_simplex_iters; } DEBUG_SUBMIP( "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " "min_fixrate={:.4g} ({})\n", log_prefix, - rins_stats_.total_success.load(), - rins_stats_.total_infeasible.load(), - rins_stats_.total_calls.load(), + submip_stats_.total_success.load(), + submip_stats_.total_infeasible.load(), + submip_stats_.total_calls.load(), fixrate, num_var_fixed, max_fixrate, @@ -2818,7 +2860,7 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. if (!is_root_heuristic) { - rins_worker_pool_.return_worker_to_pool(worker); + submip_worker_pool_.return_worker_to_pool(worker); } else { worker->set_inactive(); } @@ -2863,26 +2905,27 @@ void branch_and_bound_t::launch_root_heuristics( } } - if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent) { + bool use_rins = settings_.submip_settings.rins != 0 && incumbent_.has_incumbent; + if (use_rins || settings_.submip_settings.rens != 0) { + search_strategy_t strategy = use_rins ? search_strategy_t::RINS : search_strategy_t::RENS; diving_worker_t* worker = current_heuristic->create_submip_worker( - cut_pass, lp, settings_, root_objective_, root_vstatus_, sol); + cut_pass, lp, settings_, root_objective_, root_vstatus_, sol, strategy); std::vector current_incumbent; mutex_upper_.lock(); - current_incumbent = incumbent_.x; + if (use_rins) current_incumbent = incumbent_.x; mutex_upper_.unlock(); if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - rins(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); - + recursive_submip(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); } else { ++(*worker_count); #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(current_incumbent, current_heuristic, worker_count) depend(out : *worker) { - rins(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); + recursive_submip(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); --(*worker_count); } } @@ -3784,7 +3827,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } else { const i_t num_workers = settings_.num_threads; const i_t num_bfs_workers = std::max(num_workers / 2, 1); - const i_t num_submip_workers = std::max(num_workers / 8, 1); + const i_t num_submip_workers = 1; // std::max(num_workers / 8, 1); const i_t num_diving_workers = std::max(num_workers - num_bfs_workers, 1); bfs_worker_pool_.init(num_bfs_workers, original_lp_, @@ -3794,7 +3837,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_, root_relax_soln_.x, edge_norms_); - rins_worker_pool_.init(num_submip_workers, + submip_worker_pool_.init(num_submip_workers, original_lp_, Arow_, var_types_, diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index fb3f9fce5d..3b436f5c63 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -275,8 +275,8 @@ class branch_and_bound_t { diving_worker_pool_t diving_worker_pool_; // Worker pool dedicated to recursive RINS - diving_worker_pool_t rins_worker_pool_; - submip_stats_t rins_stats_; + diving_worker_pool_t submip_worker_pool_; + submip_stats_t submip_stats_; // Global status of the solver. omp_atomic_t solver_status_; @@ -369,7 +369,7 @@ class branch_and_bound_t { void dive_with(diving_worker_t* worker, i_t backtrack_limit); // Launch a new RINS worker - bool launch_rins_worker(const std::vector& sol); + bool launch_submip_worker(const std::vector& sol); void set_solution_from_submip(const simplex::lp_problem_t& lp, const std::vector& solution, const third_party_presolve_t& presolver, @@ -386,8 +386,8 @@ class branch_and_bound_t { bool is_root_heuristic = false); // Creates and solves the RINS sub-MIP - void rins(diving_worker_t* worker, - const std::vector& current_incumbent, + void recursive_submip(diving_worker_t* worker, + const std::vector& current_incumbent, const std::vector& var_types, bool is_root_heuristic = false); diff --git a/cpp/src/branch_and_bound/constants.hpp b/cpp/src/branch_and_bound/constants.hpp index 2bc94c8f1b..59f43549d8 100644 --- a/cpp/src/branch_and_bound/constants.hpp +++ b/cpp/src/branch_and_bound/constants.hpp @@ -25,6 +25,8 @@ enum class heuristics_origin_t { // [3] E. Danna, E. Rothberg, and C. L. Pape, “Exploring relaxation induced neighborhoods to // improve MIP solutions,” Math. Program., vol. 102, no. 1, pp. 71–90, Jan. 2005, // doi: 10.1007/s10107-004-0518-7. +// [4] T. Berthold, “RENS: The optimal rounding,” Math. Prog. Comp., vol. 6, no. 1, +// pp. 33–54, Mar. 2014, doi: 10.1007/s12532-013-0060-9. enum class search_strategy_t : int { BEST_FIRST = 0, // Best-First + Plunging. PSEUDOCOST_DIVING = 1, // Pseudocost diving [1, Section 9.2.5] @@ -34,6 +36,7 @@ enum class search_strategy_t : int { FARKAS_DIVING = 5, // Farkas Diving (see [2]) VECTOR_LENGTH_DIVING = 6, // Vector Length Diving [1, Section 9.2.6] RINS = 7, // RINS (see [3]) + RENS = 8 // RENS (see [1, Section 9.1.1], [4]) }; enum class branch_direction_t { NONE = -1, DOWN = 0, UP = 1 }; @@ -49,6 +52,7 @@ inline const char* search_strategy_to_string(search_strategy_t search_strategy) case search_strategy_t::FARKAS_DIVING: return "FARKAS_DIVING"; case search_strategy_t::VECTOR_LENGTH_DIVING: return "VECTOR_LENGTH_DIVING"; case search_strategy_t::RINS: return "RINS"; + case search_strategy_t::RENS: return "RENS"; } return "UNKNOWN"; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 44f8ac8246..91f5faa656 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -149,6 +149,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, {CUOPT_MIP_RINS, &mip_settings.submip_params.rins, -1, 1, -1}, + {CUOPT_MIP_RENS, &mip_settings.submip_params.rens, -1, 1, -1}, {CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1}, {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, -1, 72, 1}, {CUOPT_NUM_GPUS, &mip_settings.num_gpus, -1, 72, 1}, diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index 474becef25..95e5c45a8c 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -114,6 +114,7 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.zero_half_cuts = 0; branch_and_bound_settings.inside_submip = 1; branch_and_bound_settings.submip_settings.rins = 0; + branch_and_bound_settings.submip_settings.rens = 0; branch_and_bound_settings.strong_branching_simplex_iteration_limit = 200; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index f54aff7c1b..deaf3d8bbf 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -58,7 +58,8 @@ struct cut_pass_heuristics_t { const simplex::simplex_solver_settings_t& settings, f_t root_obj, const std::vector& root_vstatus, - const std::vector& sol) + const std::vector& sol, + search_strategy_t type) { submip_worker_ = std::make_unique>( id, lp, Arow_, var_types_, settings, root_solution_, root_edge_norm_); @@ -67,7 +68,7 @@ struct cut_pass_heuristics_t { submip_worker_->leaf_solution.x = sol; submip_worker_->recompute_bounds = false; submip_worker_->recompute_basis = true; - submip_worker_->search_strategy = search_strategy_t::RINS; + submip_worker_->search_strategy = type; submip_worker_->set_active(); return submip_worker_.get(); From 357295ed6b3e8d25c25e8a8bd60755812c8baa14 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 12 Aug 2026 10:30:29 +0200 Subject: [PATCH 21/29] increase the number of simplex iterations allowed at the first sub-MIP Signed-off-by: Nicolas L. Guidotti --- .../mathematical_optimization/constants.h | 17 +++--- .../mip/submip_hyper_params.hpp | 5 +- cpp/src/branch_and_bound/branch_and_bound.cpp | 57 ++++++++++++------- cpp/src/branch_and_bound/branch_and_bound.hpp | 5 +- cpp/src/math_optimization/solver_settings.cu | 5 +- 5 files changed, 56 insertions(+), 33 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 6cdc5bb2c6..354decbd36 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -138,14 +138,15 @@ #define CUOPT_MIP_HYPER_DIVING_SHOW_TYPE "mip_hyper_diving_show_type" /* @brief Recursive sub-MIP (RINS) hyper-parameters */ -#define CUOPT_MIP_HYPER_SUBMIP_BASE_TARGET_FIXRATE "mip_hyper_submip_base_target_fixrate" -#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE "mip_hyper_submip_min_fixrate" -#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP "mip_hyper_submip_min_fixrate_cap" -#define CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP "mip_hyper_submip_target_mip_gap" -#define CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_BASE "mip_hyper_submip_node_limit_base" -#define CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL "mip_hyper_submip_max_level" -#define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO "mip_hyper_submip_iteration_limit_ratio" -#define CUOPT_MIP_HYPER_SUBMIP_ENABLE_CPUFJ "mip_hyper_submip_enable_cpufj" +#define CUOPT_MIP_HYPER_SUBMIP_BASE_TARGET_FIXRATE "mip_hyper_submip_base_target_fixrate" +#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE "mip_hyper_submip_min_fixrate" +#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP "mip_hyper_submip_min_fixrate_cap" +#define CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP "mip_hyper_submip_target_mip_gap" +#define CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_OFFSET "mip_hyper_submip_node_limit_offset" +#define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET "mip_hyper_submip_iteration_limit_offset" +#define CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL "mip_hyper_submip_max_level" +#define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO "mip_hyper_submip_iteration_limit_ratio" +#define CUOPT_MIP_HYPER_SUBMIP_ENABLE_CPUFJ "mip_hyper_submip_enable_cpufj" /* @brief Block bounded-variable-elimination step of cuOpt's internal MIP presolve */ #define CUOPT_MIP_HYPER_BLOCK_BVE "mip_hyper_block_bve" diff --git a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp index 33c8817866..d2e528c822 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp @@ -30,7 +30,10 @@ struct mip_submip_hyper_params_t { f_t target_mip_gap = 0.01; // The base node limit for the sub-MIP - i_t node_limit_base = 200; + i_t node_limit_offset = 200; + + // The base iteration limit for the sub-MIP + i_t iteration_limit_offset = 10000; // The current level in the recursion. This is an internal parameter and will set automatically. i_t level = 0; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 68aa1f9ce8..3bc1bafe3e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -669,7 +669,8 @@ void branch_and_bound_t::set_solution_from_submip( const lp_problem_t& lp, const std::vector& solution, const third_party_presolve_t& presolver, - f_t fixrate) + f_t fixrate, + std::string_view log_prefix) { bool check_postsolve = false; std::vector leaf_sol; @@ -680,7 +681,11 @@ void branch_and_bound_t::set_solution_from_submip( mutex_original_lp_.lock(); uncrush_primal_solution(original_problem_, lp, leaf_sol, user_sol); mutex_original_lp_.unlock(); - settings_.log.debug_format("SubMIP found a feasible solution with obj={:.4g}", obj); + + DEBUG_SUBMIP("{} Sub-MIP found a feasible solution with obj={:.4g}", + log_prefix, + compute_user_objective(lp, obj)); + bool success = set_solution_from_heuristics(user_sol, heuristics_origin_t::SUBMIP); if (success) { submip_stats_.save_success(fixrate); @@ -1689,7 +1694,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, f_t rel_gap = user_relative_gap(user_obj, user_lower); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); - bool can_launch_rins = true; + bool can_launch_new_submip = true; while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { @@ -1799,7 +1804,9 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, worker->recompute_bounds = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::HAS_CHILDREN) { - if (can_launch_rins) { can_launch_rins = !launch_submip_worker(worker->leaf_solution.x); } + if (can_launch_new_submip) { + can_launch_new_submip = !launch_submip_worker(worker->leaf_solution.x); + } // The stack should only contain the children of the current parent. // If the stack size is greater than 0, @@ -2225,20 +2232,20 @@ bool branch_and_bound_t::launch_submip_worker(const std::vector& return true; } -#define DEBUG_SUBMIP - template void branch_and_bound_t::solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, i_t num_var_fixed, i_t num_integers, - i_t submip_level, - std::string_view log_prefix, bool is_root_heuristic) { 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); + std::vector& lower = worker->leaf_problem.lower; std::vector& upper = worker->leaf_problem.upper; std::vector& bounds_changed = worker->bounds_changed; @@ -2280,9 +2287,17 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.log.log_prefix = log_prefix; #endif - submip_settings.node_limit = settings_.submip_settings.node_limit_base + explored / 20; + submip_settings.node_limit = settings_.submip_settings.node_limit_offset + explored / 20; + + // Add offset only on the top call, we want number of simplex iteration to decay + // as we go down the recursion to avoid spending too much time in the deeper levels. + int64_t iter_offset = + settings_.inside_submip ? 0 : settings_.submip_settings.iteration_limit_offset; + int64_t simplex_iter = exploration_stats_.total_simplex_iters; + f_t iter_ratio = settings_.submip_settings.iteration_limit_ratio; + submip_settings.branch_and_bound_simplex_iteration_limit = - exploration_stats_.total_simplex_iters * settings_.submip_settings.iteration_limit_ratio; + iter_offset + simplex_iter * iter_ratio; submip_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); if (submip_settings.time_limit < 0) { return; } @@ -2291,8 +2306,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke 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 && submip_level <= settings_.submip_settings.max_level; + submip_settings.submip_settings.rens = settings_.submip_settings.rens != 0 && !max_recursion; DEBUG_SUBMIP("{}Sub-MIP: num variables fixed={}/{} ({:.2f}%)", log_prefix, @@ -2363,9 +2377,9 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.heuristic_preemption_callback = nullptr; submip_settings.dual_simplex_objective_callback = nullptr; submip_settings.set_simplex_solution_callback = nullptr; - submip_settings.solution_callback = [this, &presolver, fixrate, worker]( + submip_settings.solution_callback = [this, &presolver, fixrate, log_prefix, worker]( const std::vector& solution, f_t obj) { - this->set_solution_from_submip(worker->leaf_problem, solution, presolver, fixrate); + this->set_solution_from_submip(worker->leaf_problem, solution, presolver, fixrate, log_prefix); }; DEBUG_SUBMIP("{}Sub-MIP: {} constraints, {} variables, {} nonzeros\n", @@ -2470,7 +2484,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke } if (submip_solution.has_incumbent) { - set_solution_from_submip(worker->leaf_problem, submip_solution.x, presolver, fixrate); + set_solution_from_submip( + worker->leaf_problem, submip_solution.x, presolver, fixrate, log_prefix); } // Accumulate simplex iterations to determine when to stop exploring the sub-MIP @@ -2516,6 +2531,8 @@ void get_unfixed_integer_variables(const std::vector& lower, if (std::abs(lower[j] - upper[j]) <= fixed_tol) { continue; } integer_list.push_back(j); } + + assert(!integer_list.empty() && "The integer list cannot be empty!"); } template @@ -2822,7 +2839,11 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w // We need the pseudocost to do the DFS, which we do not have during the cut passes. if (!is_root_heuristic) { - DEBUG_SUBMIP("{} Running a quick DFS for the submip!", log_prefix); + DEBUG_SUBMIP("{}Running a quick DFS. fixrate={:.4g} ({}/{})", + log_prefix, + fixrate, + num_var_fixed, + integer_list.size()); dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } } @@ -2833,8 +2854,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w var_types, num_var_fixed, num_integers, - submip_level, - log_prefix, is_root_heuristic); } } @@ -3827,7 +3846,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } else { const i_t num_workers = settings_.num_threads; const i_t num_bfs_workers = std::max(num_workers / 2, 1); - const i_t num_submip_workers = 1; // std::max(num_workers / 8, 1); + const i_t num_submip_workers = std::max(num_workers / 8, 1); const i_t num_diving_workers = std::max(num_workers - num_bfs_workers, 1); bfs_worker_pool_.init(num_bfs_workers, original_lp_, diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 3b436f5c63..976da68ffd 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -373,7 +373,8 @@ class branch_and_bound_t { void set_solution_from_submip(const simplex::lp_problem_t& lp, const std::vector& solution, const third_party_presolve_t& presolver, - f_t fixrate); + f_t fixrate, + std::string_view log_prefix); // Solve the RINS sub-MIP void solve_submip(diving_worker_t* worker, @@ -381,8 +382,6 @@ class branch_and_bound_t { const std::vector& var_types, i_t num_var_fixed, i_t num_integers, - i_t submip_level, - std::string_view log_prefix, bool is_root_heuristic = false); // Creates and solves the RINS sub-MIP diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 91f5faa656..056e7fee26 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -186,8 +186,9 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_DIVING_NODE_LIMIT, &mip_settings.diving_params.node_limit, 0, std::numeric_limits::max(), 500, "maximum nodes explored per dive"}, {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_BASE, &mip_settings.submip_params.node_limit_base, 0, std::numeric_limits::max(), 200, "base node limit for the sub-MIP"}, - {CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL, &mip_settings.submip_params.max_level, 0, std::numeric_limits::max(), 10, "maximum sub-MIP recursion level"}, + {CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_OFFSET, &mip_settings.submip_params.node_limit_offset, 0, std::numeric_limits::max(), 200, "base node limit for the sub-MIP"}, +{CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET, &mip_settings.submip_params.iteration_limit_offset, 0, std::numeric_limits::max(), 10000, "base sub-MIP simplex-iteration limit for root heuristics"}, +{CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL, &mip_settings.submip_params.max_level, 0, std::numeric_limits::max(), 10, "maximum sub-MIP recursion level"}, {CUOPT_BARRIER_PRESOLVE_BOUND_FREE_VARIABLES, &pdlp_settings.barrier_presolve_bound_free_variables, -1, 1, -1, "Bound free variables during barrier presolve: -1 automatic (current default behavior), 0 disabled, 1 enabled"}, // QCQP (barrier) scaling hyper-parameter {CUOPT_QCQP_HYPER_RUIZ_EQUILIBRATION, &pdlp_settings.qcqp_ruiz_equilibration, -1, 1, -1, "Ruiz equilibration for QCQP barrier scaling: -1 automatic (row/column imbalance heuristic), 0 disabled, 1 enabled"}, From b5d0f97a585524981a4da66f9ba5c0017818bcfd Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 14 Aug 2026 15:23:07 +0200 Subject: [PATCH 22/29] track success/failure separately for RINS and RENS Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 57 ++++++++++++------- cpp/src/branch_and_bound/branch_and_bound.hpp | 5 +- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3bc1bafe3e..5ee4df43d3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -669,8 +669,9 @@ void branch_and_bound_t::set_solution_from_submip( const lp_problem_t& lp, const std::vector& solution, const third_party_presolve_t& presolver, + submip_stats_t& submip_stats, f_t fixrate, - std::string_view log_prefix) + [[maybe_unused]] std::string_view log_prefix) { bool check_postsolve = false; std::vector leaf_sol; @@ -688,7 +689,7 @@ void branch_and_bound_t::set_solution_from_submip( bool success = set_solution_from_heuristics(user_sol, heuristics_origin_t::SUBMIP); if (success) { - submip_stats_.save_success(fixrate); + submip_stats.save_success(fixrate); if (settings_.solution_callback != nullptr) { settings_.solution_callback(user_sol, obj); } } } @@ -2236,6 +2237,7 @@ template void branch_and_bound_t::solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, + submip_stats_t& submip_stats, i_t num_var_fixed, i_t num_integers, bool is_root_heuristic) @@ -2256,7 +2258,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (!feasible) { // RINS: This should never happen since we are fixing bounds that are already in the incumbent. - submip_stats_.save_infeasible(fixrate); + submip_stats.save_infeasible(fixrate); DEBUG_SUBMIP("{} The problem is infeasible after running bound strengthening!", log_prefix); return; } @@ -2340,7 +2342,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (presolver_status == third_party_presolve_status_t::INFEASIBLE || presolver_status == third_party_presolve_status_t::UNBNDORINFEAS || presolver_status == third_party_presolve_status_t::UNBOUNDED) { - submip_stats_.save_infeasible(fixrate); + submip_stats.save_infeasible(fixrate); return; } @@ -2365,7 +2367,8 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke } } - set_solution_from_submip(worker->leaf_problem, reduced_sol, presolver, fixrate); + set_solution_from_submip( + worker->leaf_problem, reduced_sol, presolver, submip_stats, fixrate, log_prefix); return; } @@ -2377,10 +2380,12 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.heuristic_preemption_callback = nullptr; submip_settings.dual_simplex_objective_callback = nullptr; submip_settings.set_simplex_solution_callback = nullptr; - submip_settings.solution_callback = [this, &presolver, fixrate, log_prefix, worker]( - const std::vector& solution, f_t obj) { - this->set_solution_from_submip(worker->leaf_problem, solution, presolver, fixrate, log_prefix); - }; + submip_settings.solution_callback = + [this, &presolver, fixrate, &submip_stats, log_prefix, worker](const std::vector& solution, + f_t obj) { + this->set_solution_from_submip( + worker->leaf_problem, solution, presolver, submip_stats, fixrate, log_prefix); + }; DEBUG_SUBMIP("{}Sub-MIP: {} constraints, {} variables, {} nonzeros\n", log_prefix, @@ -2479,13 +2484,13 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (submip_status == mip_status_t::NUMERICAL) { return; } if (submip_status == mip_status_t::INFEASIBLE || submip_status == mip_status_t::UNBOUNDED) { - submip_stats_.save_infeasible(fixrate); + submip_stats.save_infeasible(fixrate); return; } if (submip_solution.has_incumbent) { set_solution_from_submip( - worker->leaf_problem, submip_solution.x, presolver, fixrate, log_prefix); + worker->leaf_problem, submip_solution.x, presolver, submip_stats, fixrate, log_prefix); } // Accumulate simplex iterations to determine when to stop exploring the sub-MIP @@ -2569,7 +2574,7 @@ bool apply_rens_fixings(const simplex_solver_settings_t& settings, if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) break; + if (num_var_fixed >= max_var_fixed) return true; } } @@ -2594,7 +2599,7 @@ bool apply_rins_fixings(const simplex_solver_settings_t& settings, f_t fixed_val = std::round(current_sol[j]); fix_variable(j, lower, upper, bounds_changed, fixed_val); ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) break; + if (num_var_fixed >= max_var_fixed) return true; } } @@ -2645,7 +2650,7 @@ bool extend_variable_fixings(const simplex_solver_settings_t& settings for (auto [dist, j, fixed_val] : candidates) { fix_variable(j, lower, upper, bounds_changed, fixed_val); ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) break; + if (num_var_fixed >= max_var_fixed) return true; // Limit the amount of fixing to the current LP. change += dist; @@ -2668,7 +2673,14 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w std::string log_prefix = std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); - ++submip_stats_.total_calls; + assert((worker->search_strategy == search_strategy_t::RINS || + worker->search_strategy == search_strategy_t::RENS) && + "Sub-MIP worker must be set to RINS or RENS type"); + + submip_stats_t& submip_stats = + worker->search_strategy == search_strategy_t::RINS ? rins_stats_ : rens_stats_; + + ++submip_stats.total_calls; bool has_submip = false; worker->recompute_bounds = false; @@ -2681,6 +2693,8 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w std::vector& bounds_changed = worker->bounds_changed; std::vector& current_sol = worker->leaf_solution.x; + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + std::vector fractional; i_t num_frac = fractional_variables(settings_, current_sol, var_types, fractional); @@ -2689,7 +2703,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w i_t num_integers = integer_list.size(); - f_t max_fixrate = submip_get_max_fixrate(submip_stats_, settings_.submip_settings, worker->rng); + f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); i_t max_var_fixed = max_fixrate * num_integers; i_t min_var_fixed = min_fixrate * num_integers; @@ -2739,8 +2753,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w bounds_changed, num_var_fixed); if (has_submip) { break; } - } else { - assert(false && "Incorrect submip type! Must be RINS or RENS!"); } // Even considering the entire integer list, we were unable to fix a single variable in this @@ -2810,7 +2822,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w // found a solution that improved the incumbent, then do a DFS with a backtrack_limit of 5 // levels up to try to find a feasible solution quickly from the neighbourhood. if (fixrate < settings_.submip_settings.min_fixrate_cap || - (settings_.inside_submip && submip_stats_.total_success != 0)) { + (settings_.inside_submip && submip_stats.total_success != 0)) { worker->start_node.packed_vstatus = simplex::compress_vstatus(worker->leaf_vstatus); worker->start_lower = lower; worker->start_upper = upper; @@ -2852,6 +2864,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w solve_submip(worker, current_incumbent, var_types, + submip_stats, num_var_fixed, num_integers, is_root_heuristic); @@ -2867,9 +2880,9 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " "min_fixrate={:.4g} ({})\n", log_prefix, - submip_stats_.total_success.load(), - submip_stats_.total_infeasible.load(), - submip_stats_.total_calls.load(), + submip_stats.total_success.load(), + submip_stats.total_infeasible.load(), + submip_stats.total_calls.load(), fixrate, num_var_fixed, max_fixrate, diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 976da68ffd..da8b7bdd5d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -276,7 +276,8 @@ class branch_and_bound_t { // Worker pool dedicated to recursive RINS diving_worker_pool_t submip_worker_pool_; - submip_stats_t submip_stats_; + submip_stats_t rins_stats_; + submip_stats_t rens_stats_; // Global status of the solver. omp_atomic_t solver_status_; @@ -373,6 +374,7 @@ class branch_and_bound_t { void set_solution_from_submip(const simplex::lp_problem_t& lp, const std::vector& solution, const third_party_presolve_t& presolver, + submip_stats_t& submip_stats, f_t fixrate, std::string_view log_prefix); @@ -380,6 +382,7 @@ class branch_and_bound_t { void solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, + submip_stats_t& submip_stats, i_t num_var_fixed, i_t num_integers, bool is_root_heuristic = false); From 339b3c56de7e4a8cf983e5c86f412cdeae6b3c26 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 14 Aug 2026 15:35:25 +0200 Subject: [PATCH 23/29] shuffle the integer and fractional list to give an equal chance for all variables to be picked. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 8 +++++++- cpp/src/mip_heuristics/root_heuristics.hpp | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 5ee4df43d3..7f03e9e8be 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2701,8 +2701,11 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w std::vector integer_list; get_unfixed_integer_variables(lower, upper, var_types, settings_.fixed_tol, integer_list); - i_t num_integers = integer_list.size(); + // Shuffle the integer list, so every variable has the same chance to the picked + // (we iterate the list in order). + worker->rng.shuffle(integer_list); + i_t num_integers = integer_list.size(); f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); i_t max_var_fixed = max_fixrate * num_integers; @@ -2710,6 +2713,9 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w i_t num_var_fixed = 0; while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { + // Shuffle the fractional list, so every variable has the same chance to the picked + // (we iterate the list in order). + worker->rng.shuffle(fractional); // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the // current incumbent, considering only the fractional values in the current node i_t prev_num_fixed = num_var_fixed; diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index deaf3d8bbf..1f29b25eef 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -58,8 +58,8 @@ struct cut_pass_heuristics_t { const simplex::simplex_solver_settings_t& settings, f_t root_obj, const std::vector& root_vstatus, - const std::vector& sol, - search_strategy_t type) + const std::vector& sol, + search_strategy_t type) { submip_worker_ = std::make_unique>( id, lp, Arow_, var_types_, settings, root_solution_, root_edge_norm_); From 7983659cccff06bc6b6b038a8f5882709386bdee Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 17 Aug 2026 16:11:11 +0200 Subject: [PATCH 24/29] modified RENS to construct the neighbourhood over multiple rounds. Signed-off-by: Nicolas L. Guidotti --- .../mathematical_optimization/constants.h | 1 + .../mip/submip_hyper_params.hpp | 4 + cpp/src/branch_and_bound/branch_and_bound.cpp | 197 +++++++++++------- cpp/src/math_optimization/solver_settings.cu | 1 + 4 files changed, 124 insertions(+), 79 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 354decbd36..c4ef61b220 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -146,6 +146,7 @@ #define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET "mip_hyper_submip_iteration_limit_offset" #define CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL "mip_hyper_submip_max_level" #define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO "mip_hyper_submip_iteration_limit_ratio" +#define CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO "mip_hyper_submip_round_close_ratio" #define CUOPT_MIP_HYPER_SUBMIP_ENABLE_CPUFJ "mip_hyper_submip_enable_cpufj" /* @brief Block bounded-variable-elimination step of cuOpt's internal MIP presolve */ diff --git a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp index d2e528c822..d7efc8c5b0 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp @@ -50,6 +50,10 @@ struct mip_submip_hyper_params_t { // the maximum number of nodes allow for backtracking. i_t dfs_max_backtrack = 5; + // How many variables a single round can fix. Set in terms of ratio of + // (1 - current fixrate). + f_t round_close_ratio = 0.9; + // Run CPU FJ over the sub-MIP bool enable_cpufj = true; }; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 7f03e9e8be..47ba5562bc 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,7 +45,7 @@ #include #include -#define SUBMIP_VERBOSE false +#define SUBMIP_VERBOSE true #if SUBMIP_VERBOSE #define DEBUG_SUBMIP(fmt, ...) settings_.log.print_format(fmt, __VA_ARGS__); #else @@ -2554,66 +2554,56 @@ void fix_variable(i_t j, } template -bool apply_rens_fixings(const simplex_solver_settings_t& settings, +void apply_rens_fixings(const simplex_solver_settings_t& settings, const std::vector& node_solution, const std::vector& integer_list, - i_t max_var_fixed, - i_t min_var_fixed, + i_t round_target, std::vector& lower, std::vector& upper, std::vector& bounds_changed, i_t& num_var_fixed) { for (i_t j : integer_list) { + if (num_var_fixed >= round_target) return; if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { continue; } f_t old_lower = lower[j]; f_t old_upper = upper[j]; lower[j] = std::clamp(std::floor(node_solution[j]), old_lower, old_upper); upper[j] = std::clamp(std::ceil(node_solution[j]), old_lower, old_upper); bounds_changed[j] = lower[j] != old_lower || upper[j] != old_upper; - - if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { - ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) return true; - } + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) ++num_var_fixed; } - - return num_var_fixed >= min_var_fixed; } template -bool apply_rins_fixings(const simplex_solver_settings_t& settings, +void apply_rins_fixings(const simplex_solver_settings_t& settings, const std::vector& current_sol, const std::vector& fractional, const std::vector& current_incumbent, - i_t max_var_fixed, - i_t min_var_fixed, + i_t round_target, std::vector& lower, std::vector& upper, std::vector& bounds_changed, i_t& num_var_fixed) { for (i_t j : fractional) { + if (num_var_fixed >= round_target) return; if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { continue; } if (std::abs(current_sol[j] - current_incumbent[j]) <= settings.integer_tol) { f_t fixed_val = std::round(current_sol[j]); fix_variable(j, lower, upper, bounds_changed, fixed_val); ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) return true; } } - - return num_var_fixed >= min_var_fixed; } template -bool extend_variable_fixings(const simplex_solver_settings_t& settings, +void extend_variable_fixings(const simplex_solver_settings_t& settings, const std::vector& obj_coeffs, const std::vector& fractional, const std::vector& current_sol, const std::vector& root_solution, - i_t max_var_fixed, - i_t min_var_fixed, + i_t round_target, std::vector& lower, std::vector& upper, std::vector& bounds_changed, @@ -2648,16 +2638,15 @@ bool extend_variable_fixings(const simplex_solver_settings_t& settings f_t change = 0; for (auto [dist, j, fixed_val] : candidates) { + if (num_var_fixed >= round_target) return; + fix_variable(j, lower, upper, bounds_changed, fixed_val); ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) return true; // Limit the amount of fixing to the current LP. change += dist; - if (change >= 0.5) { break; } + if (change >= 0.5) return; } - - return num_var_fixed >= min_var_fixed; } template @@ -2709,10 +2698,15 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); i_t max_var_fixed = max_fixrate * num_integers; - i_t min_var_fixed = min_fixrate * num_integers; i_t num_var_fixed = 0; + i_t round = 0; + while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { + f_t prev_fixrate = (f_t)num_var_fixed / num_integers; + f_t round_fixrate = std::min( + 1.0 - (1.0 - prev_fixrate) * settings_.submip_settings.round_close_ratio, max_fixrate); + i_t round_target = std::max(round_fixrate * num_integers, num_var_fixed + 1); // Shuffle the fractional list, so every variable has the same chance to the picked // (we iterate the list in order). worker->rng.shuffle(fractional); @@ -2721,86 +2715,131 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w i_t prev_num_fixed = num_var_fixed; if (worker->search_strategy == search_strategy_t::RINS) { - has_submip = apply_rins_fixings(settings_, - current_sol, - fractional, - current_incumbent, - max_var_fixed, - min_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - if (has_submip) { break; } + apply_rins_fixings(settings_, + current_sol, + fractional, + current_incumbent, + round_target, + lower, + upper, + bounds_changed, + num_var_fixed); + if (num_var_fixed >= max_var_fixed) { + has_submip = true; + break; + } if (prev_num_fixed == num_var_fixed) { // RINS neighbourhood 2: Search the entire list of integer variables where the current // LP solution matches the current incumbent. - has_submip = apply_rins_fixings(settings_, - current_sol, - integer_list, - current_incumbent, - max_var_fixed, - min_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - if (has_submip) { break; } + apply_rins_fixings(settings_, + current_sol, + integer_list, + current_incumbent, + round_target, + lower, + upper, + bounds_changed, + num_var_fixed); + if (num_var_fixed >= max_var_fixed) { + has_submip = true; + break; + } + } + + // The RINS neighbourhood ran dry. If it is already tight enough, take it rather than + // diluting it with fixings that do not agree with the incumbent. + if (prev_num_fixed == num_var_fixed && prev_fixrate >= min_fixrate) { + has_submip = true; + break; } + } else if (worker->search_strategy == search_strategy_t::RENS) { - has_submip = apply_rens_fixings(settings_, - current_sol, - integer_list, - max_var_fixed, - min_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - if (has_submip) { break; } + apply_rens_fixings(settings_, + current_sol, + integer_list, + round_target, + lower, + upper, + bounds_changed, + num_var_fixed); + if (num_var_fixed >= max_var_fixed) { + has_submip = true; + break; + } } // Even considering the entire integer list, we were unable to fix a single variable in this // iteration. Iterate over the fractional variables again and fixing those that closest to // an integer solution first in order to reach the fixing threshold. if (prev_num_fixed == num_var_fixed) { - has_submip = extend_variable_fixings(settings_, - worker->leaf_problem.objective, - fractional, - current_sol, - worker->root_solution, - max_var_fixed, - min_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - if (has_submip) { break; } + extend_variable_fixings(settings_, + worker->leaf_problem.objective, + fractional, + current_sol, + worker->root_solution, + round_target, + lower, + upper, + bounds_changed, + num_var_fixed); + if (num_var_fixed >= max_var_fixed) { + has_submip = true; + break; + } + // Even sweep over all integer variables, we exhausted all variables that can be fixed. + // If this is the case, then tries to solve the sub-mip anyway. if (prev_num_fixed == num_var_fixed) { - DEBUG_SUBMIP("{}Could not fix more variables ({}, max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); has_submip = true; break; } } + f_t fixrate = (f_t)num_var_fixed / num_integers; + + DEBUG_SUBMIP( + "{} Round {}: fixed {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} ({:.2f}). max " + "fixrate = {} ({:.2f})", + log_prefix, + round, + prev_num_fixed, + prev_fixrate, + num_var_fixed, + fixrate, + round_target, + round_fixrate, + max_var_fixed, + max_fixrate); + if (toc(exploration_stats_.start_time) > settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; break; } + bool is_feasible = + worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); + if (!is_feasible) { + submip_stats.save_infeasible(fixrate); + break; + } + // After fixing the variables, re-solve the LP relaxation. We use the optimal solution // in the next iteration to find additional variable fixings. // We continue to do this until enough variables were fixed or no variable is left to fix. logger_t log; - log.log = false; - dual_status_t lp_status = solve_node_lp(&node, worker, stats, log); + log.log = false; + + int64_t iter_offset = + settings_.inside_submip ? 0 : settings_.submip_settings.iteration_limit_offset; + int64_t simplex_iter = exploration_stats_.total_simplex_iters; + f_t iter_ratio = settings_.submip_settings.iteration_limit_ratio; + int64_t simplex_iter_limit = iter_offset + simplex_iter * iter_ratio; + i_t max_iter = std::min(simplex_iter_limit - stats.total_simplex_iters, + std::numeric_limits::max()); + if (max_iter <= 0) { break; } + dual_status_t lp_status = solve_node_lp(&node, worker, stats, log, max_iter); if (lp_status != dual_status_t::OPTIMAL) { break; } fractional.clear(); @@ -2819,6 +2858,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w } worker->recompute_basis = false; + ++round; } f_t fixrate = (f_t)num_var_fixed / num_integers; @@ -2884,7 +2924,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w DEBUG_SUBMIP( "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " - "min_fixrate={:.4g} ({})\n", + "min_fixrate={:.4g}\n", log_prefix, submip_stats.total_success.load(), submip_stats.total_infeasible.load(), @@ -2893,8 +2933,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w num_var_fixed, max_fixrate, max_var_fixed, - min_fixrate, - min_var_fixed); + min_fixrate); // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. if (!is_root_heuristic) { diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 056e7fee26..97862db8bf 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -122,6 +122,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP, &mip_settings.submip_params.min_fixrate_cap, f_t(0.0), f_t(1.0), f_t(0.1), "hard cap on the minimum fix rate for solving a sub-MIP"}, {CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP, &mip_settings.submip_params.target_mip_gap, f_t(0.0), f_t(1.0), f_t(0.01), "MIP gap target for the sub-MIP"}, {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO, &mip_settings.submip_params.iteration_limit_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "sub-MIP simplex-iteration limit as a factor of parent B&B iterations"}, + {CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO, &mip_settings.submip_params.round_close_ratio, f_t(0.0), f_t(1.0), f_t(0.9), "share of the still-unfixed integers left for later neighbourhood rounds (0 reaches the target fix rate in a single round)"}, }; // Int parameters From 880ece43f1c6f3eeb97d9d1e2cc44f1126cd202c Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 17 Aug 2026 16:32:36 +0200 Subject: [PATCH 25/29] propagate simplex iteration count to submip solve Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 20 +++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 47ba5562bc..e4eb932768 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2240,6 +2240,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_stats_t& submip_stats, i_t num_var_fixed, i_t num_integers, + i_t simplex_iter_used, bool is_root_heuristic) { double start_time = tic(); @@ -2299,9 +2300,11 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke f_t iter_ratio = settings_.submip_settings.iteration_limit_ratio; submip_settings.branch_and_bound_simplex_iteration_limit = - iter_offset + simplex_iter * iter_ratio; + iter_offset + simplex_iter * iter_ratio - simplex_iter_used; + if (submip_settings.branch_and_bound_simplex_iteration_limit <= 0) { return; } + submip_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); - if (submip_settings.time_limit < 0) { return; } + if (submip_settings.time_limit <= 0) { return; } submip_settings.relative_mip_gap_tol = std::min(settings_.submip_settings.target_mip_gap, rel_gap); @@ -2799,7 +2802,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w f_t fixrate = (f_t)num_var_fixed / num_integers; DEBUG_SUBMIP( - "{} Round {}: fixed {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} ({:.2f}). max " + "{}Round {}: fixed {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} ({:.2f}). max " "fixrate = {} ({:.2f})", log_prefix, round, @@ -2861,6 +2864,11 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w ++round; } + // Accumulate the iterations for sub-MIP so it stops when it reaches the allocated budget. + if (settings_.inside_submip) { + exploration_stats_.total_simplex_iters += stats.total_simplex_iters; + } + f_t fixrate = (f_t)num_var_fixed / num_integers; if (has_submip) { @@ -2913,15 +2921,11 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w submip_stats, num_var_fixed, num_integers, + stats.total_simplex_iters, is_root_heuristic); } } - // Accumulate the iterations for sub-MIP so it stops when it reaches the allocated budget. - if (settings_.inside_submip) { - exploration_stats_.total_simplex_iters += stats.total_simplex_iters; - } - DEBUG_SUBMIP( "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " "min_fixrate={:.4g}\n", diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index da8b7bdd5d..5c570dfff3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -385,6 +385,7 @@ class branch_and_bound_t { submip_stats_t& submip_stats, i_t num_var_fixed, i_t num_integers, + i_t simplex_iter_used, bool is_root_heuristic = false); // Creates and solves the RINS sub-MIP From d790c5cb06eb278802c4d0d315e16dfd99d11340 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 17 Aug 2026 18:13:52 +0200 Subject: [PATCH 26/29] fixed stale fixrate. added more debug info. Signed-off-by: Nicolas L. Guidotti --- .../mip/submip_hyper_params.hpp | 2 +- cpp/src/branch_and_bound/branch_and_bound.cpp | 125 +++++++++++++----- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp index d7efc8c5b0..3ea98f7ab3 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp @@ -52,7 +52,7 @@ struct mip_submip_hyper_params_t { // How many variables a single round can fix. Set in terms of ratio of // (1 - current fixrate). - f_t round_close_ratio = 0.9; + f_t round_close_ratio = 0.8; // Run CPU FJ over the sub-MIP bool enable_cpufj = true; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e4eb932768..96cb22c756 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,7 +45,7 @@ #include #include -#define SUBMIP_VERBOSE true +#define SUBMIP_VERBOSE false #if SUBMIP_VERBOSE #define DEBUG_SUBMIP(fmt, ...) settings_.log.print_format(fmt, __VA_ARGS__); #else @@ -2652,6 +2652,20 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings } } +template +i_t calculate_num_fixed(const std::vector& integer_list, + const std::vector& lower, + const std::vector& upper, + f_t fixed_tol) +{ + i_t num_fixed = 0; + for (i_t j : integer_list) { + if (std::abs(lower[j] - upper[j]) <= fixed_tol) ++num_fixed; + } + + return num_fixed; +} + template void branch_and_bound_t::recursive_submip(diving_worker_t* worker, const std::vector& current_incumbent, @@ -2693,10 +2707,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w std::vector integer_list; get_unfixed_integer_variables(lower, upper, var_types, settings_.fixed_tol, integer_list); - // Shuffle the integer list, so every variable has the same chance to the picked - // (we iterate the list in order). - worker->rng.shuffle(integer_list); - i_t num_integers = integer_list.size(); f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); @@ -2710,8 +2720,9 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w f_t round_fixrate = std::min( 1.0 - (1.0 - prev_fixrate) * settings_.submip_settings.round_close_ratio, max_fixrate); i_t round_target = std::max(round_fixrate * num_integers, num_var_fixed + 1); - // Shuffle the fractional list, so every variable has the same chance to the picked + // Shuffle the fractional and integer list, so every variable has the same chance to the picked // (we iterate the list in order). + worker->rng.shuffle(integer_list); worker->rng.shuffle(fractional); // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the // current incumbent, considering only the fractional values in the current node @@ -2772,10 +2783,45 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w } } + if (toc(exploration_stats_.start_time) > settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + break; + } + + f_t fixrate = (f_t)num_var_fixed / num_integers; + DEBUG_SUBMIP( + "{}Round {}: fixed {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} ({:.2f}). max " + "fixrate = {} ({:.2f})", + log_prefix, + round, + prev_num_fixed, + prev_fixrate, + num_var_fixed, + fixrate, + round_target, + round_fixrate, + max_var_fixed, + max_fixrate); + + if (num_var_fixed > prev_num_fixed) { + bool is_feasible = + worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); + if (!is_feasible) { + DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) + submip_stats.save_infeasible(fixrate); + break; + } + + num_var_fixed = calculate_num_fixed(integer_list, lower, upper, settings_.fixed_tol); + } + // Even considering the entire integer list, we were unable to fix a single variable in this // iteration. Iterate over the fractional variables again and fixing those that closest to // an integer solution first in order to reach the fixing threshold. - if (prev_num_fixed == num_var_fixed) { + if (num_var_fixed <= round_target) { + prev_num_fixed = num_var_fixed; + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + extend_variable_fixings(settings_, worker->leaf_problem.objective, fractional, @@ -2791,6 +2837,17 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w break; } + fixrate = (f_t)num_var_fixed / num_integers; + bool is_feasible = + worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); + if (!is_feasible) { + DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) + submip_stats.save_infeasible(fixrate); + break; + } + + num_var_fixed = calculate_num_fixed(integer_list, lower, upper, settings_.fixed_tol); + // Even sweep over all integer variables, we exhausted all variables that can be fixed. // If this is the case, then tries to solve the sub-mip anyway. if (prev_num_fixed == num_var_fixed) { @@ -2799,34 +2856,11 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w } } - f_t fixrate = (f_t)num_var_fixed / num_integers; - - DEBUG_SUBMIP( - "{}Round {}: fixed {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} ({:.2f}). max " - "fixrate = {} ({:.2f})", - log_prefix, - round, - prev_num_fixed, - prev_fixrate, - num_var_fixed, - fixrate, - round_target, - round_fixrate, - max_var_fixed, - max_fixrate); - if (toc(exploration_stats_.start_time) > settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; break; } - bool is_feasible = - worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); - if (!is_feasible) { - submip_stats.save_infeasible(fixrate); - break; - } - // After fixing the variables, re-solve the LP relaxation. We use the optimal solution // in the next iteration to find additional variable fixings. // We continue to do this until enough variables were fixed or no variable is left to fix. @@ -2840,10 +2874,23 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w int64_t simplex_iter_limit = iter_offset + simplex_iter * iter_ratio; i_t max_iter = std::min(simplex_iter_limit - stats.total_simplex_iters, std::numeric_limits::max()); - if (max_iter <= 0) { break; } + if (max_iter <= 0) { + DEBUG_SUBMIP("{}Round {}: max iteration reached! {}/{}", + log_prefix, + round, + stats.total_simplex_iters.load(), + simplex_iter_limit) + break; + } dual_status_t lp_status = solve_node_lp(&node, worker, stats, log, max_iter); - if (lp_status != dual_status_t::OPTIMAL) { break; } + if (lp_status != dual_status_t::OPTIMAL) { + DEBUG_SUBMIP("{}Round {}: simplex returned {}", + log_prefix, + round, + simplex::dual_status_to_string(lp_status)) + break; + } fractional.clear(); num_frac = fractional_variables(settings_, current_sol, var_types, fractional); @@ -2852,11 +2899,23 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w node.lower_bound = leaf_obj; snap_to_lattice(&node, leaf_obj); - if (leaf_obj > upper_bound_.load()) { break; } + if (leaf_obj > upper_bound_.load()) { + DEBUG_SUBMIP("{}Round {}: reached cutoff point. obj={:.4g}. upper_bound={:.4g}", + log_prefix, + round, + leaf_obj, + upper_bound_.load()) + break; + } if (num_frac == 0) { // 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, + round, + leaf_obj, + upper_bound_.load()) break; } From fff48b232bfb33ca4566482de929cbe2a54b5f17 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 18 Aug 2026 11:47:27 +0200 Subject: [PATCH 27/29] calculate the fixrate after bound propagation. better and more debug messages. infeasible is not counted during construction. fixed incorrect trigger for extension. refine progress tracking during construction. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 113 ++++++++---------- 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 96cb22c756..a33ebdc003 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,7 +45,7 @@ #include #include -#define SUBMIP_VERBOSE false +#define SUBMIP_VERBOSE true #if SUBMIP_VERBOSE #define DEBUG_SUBMIP(fmt, ...) settings_.log.print_format(fmt, __VA_ARGS__); #else @@ -2249,21 +2249,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke std::string log_prefix = std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); - std::vector& lower = worker->leaf_problem.lower; - std::vector& upper = worker->leaf_problem.upper; - std::vector& bounds_changed = worker->bounds_changed; - f_t fixrate = (f_t)num_var_fixed / num_integers; - - bool feasible = - worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); - - if (!feasible) { - // RINS: This should never happen since we are fixing bounds that are already in the incumbent. - submip_stats.save_infeasible(fixrate); - DEBUG_SUBMIP("{} The problem is infeasible after running bound strengthening!", log_prefix); - return; - } - + f_t fixrate = (f_t)num_var_fixed / num_integers; 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); @@ -2313,7 +2299,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke 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: num variables fixed={}/{} ({:.2f}%)", + DEBUG_SUBMIP("{}Sub-MIP: variables fixed={}/{} ({:.2f}%)", log_prefix, num_var_fixed, num_integers, @@ -2345,13 +2331,14 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (presolver_status == third_party_presolve_status_t::INFEASIBLE || presolver_status == third_party_presolve_status_t::UNBNDORINFEAS || presolver_status == third_party_presolve_status_t::UNBOUNDED) { + DEBUG_SUBMIP("{}Presolve detected infeasibility", log_prefix); submip_stats.save_infeasible(fixrate); return; } // Also handle optimal if (submip_problem.num_rows == 0 || submip_problem.num_cols == 0) { - DEBUG_SUBMIP("{}Sub-MIP presolved to a trivial {} x {} problem; solving by bound pushing", + DEBUG_SUBMIP("{}Reduced to a trivial {} x {} problem; solving by bound pushing", log_prefix, submip_problem.num_rows, submip_problem.num_cols); @@ -2653,17 +2640,17 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings } template -i_t calculate_num_fixed(const std::vector& integer_list, - const std::vector& lower, - const std::vector& upper, - f_t fixed_tol) +f_t calculate_fixrate(const std::vector& integer_list, + const std::vector& lower, + const std::vector& upper, + f_t fixed_tol) { i_t num_fixed = 0; for (i_t j : integer_list) { if (std::abs(lower[j] - upper[j]) <= fixed_tol) ++num_fixed; } - return num_fixed; + return (f_t)num_fixed / integer_list.size(); } template @@ -2709,14 +2696,14 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w i_t num_integers = integer_list.size(); f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); - f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); - i_t max_var_fixed = max_fixrate * num_integers; + f_t min_fixrate = settings_.submip_settings.min_fixrate; i_t num_var_fixed = 0; + f_t fixrate = 0; i_t round = 0; while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { - f_t prev_fixrate = (f_t)num_var_fixed / num_integers; + f_t prev_fixrate = fixrate; f_t round_fixrate = std::min( 1.0 - (1.0 - prev_fixrate) * settings_.submip_settings.round_close_ratio, max_fixrate); i_t round_target = std::max(round_fixrate * num_integers, num_var_fixed + 1); @@ -2738,10 +2725,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w upper, bounds_changed, num_var_fixed); - if (num_var_fixed >= max_var_fixed) { - has_submip = true; - break; - } if (prev_num_fixed == num_var_fixed) { // RINS neighbourhood 2: Search the entire list of integer variables where the current @@ -2755,10 +2738,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w upper, bounds_changed, num_var_fixed); - if (num_var_fixed >= max_var_fixed) { - has_submip = true; - break; - } } // The RINS neighbourhood ran dry. If it is already tight enough, take it rather than @@ -2777,10 +2756,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w upper, bounds_changed, num_var_fixed); - if (num_var_fixed >= max_var_fixed) { - has_submip = true; - break; - } } if (toc(exploration_stats_.start_time) > settings_.time_limit) { @@ -2788,10 +2763,14 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w break; } - f_t fixrate = (f_t)num_var_fixed / num_integers; + bool is_feasible = + worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); + fixrate = calculate_fixrate(integer_list, lower, upper, settings_.fixed_tol); + num_var_fixed = fixrate * num_integers; + DEBUG_SUBMIP( "{}Round {}: fixed {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} ({:.2f}). max " - "fixrate = {} ({:.2f})", + "fixrate = {:.4g}", log_prefix, round, prev_num_fixed, @@ -2800,25 +2779,22 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w fixrate, round_target, round_fixrate, - max_var_fixed, max_fixrate); - if (num_var_fixed > prev_num_fixed) { - bool is_feasible = - worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); - if (!is_feasible) { - DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) - submip_stats.save_infeasible(fixrate); - break; - } + if (!is_feasible) { + DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) + break; + } - num_var_fixed = calculate_num_fixed(integer_list, lower, upper, settings_.fixed_tol); + if (fixrate >= max_fixrate) { + has_submip = true; + break; } // Even considering the entire integer list, we were unable to fix a single variable in this // iteration. Iterate over the fractional variables again and fixing those that closest to // an integer solution first in order to reach the fixing threshold. - if (num_var_fixed <= round_target) { + if (num_var_fixed < round_target) { prev_num_fixed = num_var_fixed; std::fill(bounds_changed.begin(), bounds_changed.end(), false); @@ -2832,21 +2808,35 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w upper, bounds_changed, num_var_fixed); - if (num_var_fixed >= max_var_fixed) { - has_submip = true; - break; - } - fixrate = (f_t)num_var_fixed / num_integers; - bool is_feasible = + is_feasible = worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); + fixrate = calculate_fixrate(integer_list, lower, upper, settings_.fixed_tol); + num_var_fixed = fixrate * num_integers; + + DEBUG_SUBMIP( + "{}Round {}: extended fixings {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} " + "({:.2f}). max " + "fixrate = {:.4g}", + log_prefix, + round, + prev_num_fixed, + prev_fixrate, + num_var_fixed, + fixrate, + round_target, + round_fixrate, + max_fixrate); + if (!is_feasible) { DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) - submip_stats.save_infeasible(fixrate); break; } - num_var_fixed = calculate_num_fixed(integer_list, lower, upper, settings_.fixed_tol); + if (fixrate >= max_fixrate) { + has_submip = true; + break; + } // Even sweep over all integer variables, we exhausted all variables that can be fixed. // If this is the case, then tries to solve the sub-mip anyway. @@ -2928,8 +2918,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w exploration_stats_.total_simplex_iters += stats.total_simplex_iters; } - f_t fixrate = (f_t)num_var_fixed / num_integers; - if (has_submip) { // If not enough variables was fixed (the neighbourhood is too loose) or the sub-MIP already // found a solution that improved the incumbent, then do a DFS with a backtrack_limit of 5 @@ -2986,7 +2974,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w } DEBUG_SUBMIP( - "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " + "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g}, " "min_fixrate={:.4g}\n", log_prefix, submip_stats.total_success.load(), @@ -2995,7 +2983,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w fixrate, num_var_fixed, max_fixrate, - max_var_fixed, min_fixrate); // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. From 3c70bb36a813fc53aaf31572d89c473eac09ed3c Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 18 Aug 2026 17:51:56 +0200 Subject: [PATCH 28/29] fixed incorrect trigger for extension. refine progress tracking during construction. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 330 ++++++++---------- cpp/src/branch_and_bound/branch_and_bound.hpp | 3 +- cpp/src/branch_and_bound/worker.hpp | 2 + 3 files changed, 148 insertions(+), 187 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index a33ebdc003..ae8535d847 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,7 +45,7 @@ #include #include -#define SUBMIP_VERBOSE true +#define SUBMIP_VERBOSE false #if SUBMIP_VERBOSE #define DEBUG_SUBMIP(fmt, ...) settings_.log.print_format(fmt, __VA_ARGS__); #else @@ -683,7 +683,7 @@ void branch_and_bound_t::set_solution_from_submip( uncrush_primal_solution(original_problem_, lp, leaf_sol, user_sol); mutex_original_lp_.unlock(); - DEBUG_SUBMIP("{} Sub-MIP found a feasible solution with obj={:.4g}", + DEBUG_SUBMIP("{}Sub-MIP found a feasible solution with obj={:.4g}", log_prefix, compute_user_objective(lp, obj)); @@ -2238,8 +2238,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke const std::vector& current_incumbent, const std::vector& var_types, submip_stats_t& submip_stats, - i_t num_var_fixed, - i_t num_integers, + f_t fixrate, i_t simplex_iter_used, bool is_root_heuristic) { @@ -2249,7 +2248,6 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke std::string log_prefix = std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); - f_t fixrate = (f_t)num_var_fixed / num_integers; 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); @@ -2299,12 +2297,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke 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: variables fixed={}/{} ({:.2f}%)", - log_prefix, - num_var_fixed, - num_integers, - fixrate * 100); - + DEBUG_SUBMIP("{}Sub-MIP: fixrate={:.2f}", log_prefix, fixrate) DEBUG_SUBMIP( "{}Sub-MIP solve settings: time_limit={:.2f}, node_limit={}, iter_limit={} (current_iter={}), " "tol={:g}", @@ -2342,23 +2335,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke log_prefix, submip_problem.num_rows, submip_problem.num_cols); - - std::vector reduced_sol(submip_problem.num_cols); - - for (i_t j = 0; j < submip_problem.num_cols; ++j) { - const f_t c = submip_problem.objective[j]; - const f_t l = submip_problem.lower[j]; - const f_t u = submip_problem.upper[j]; - // Minimize c_j x_j over [l, u]; fall back to any finite bound (0 if both are infinite). - if (c < -settings_.zero_tol) { - reduced_sol[j] = std::isfinite(u) ? u : (std::isfinite(l) ? l : 0); - } else { - reduced_sol[j] = std::isfinite(l) ? l : (std::isfinite(u) ? u : 0); - } - } - - set_solution_from_submip( - worker->leaf_problem, reduced_sol, presolver, submip_stats, fixrate, log_prefix); + submip_stats.save_empty(); return; } @@ -2544,60 +2521,68 @@ void fix_variable(i_t j, } template -void apply_rens_fixings(const simplex_solver_settings_t& settings, - const std::vector& node_solution, - const std::vector& integer_list, - i_t round_target, - std::vector& lower, - std::vector& upper, - std::vector& bounds_changed, - i_t& num_var_fixed) +i_t apply_rens_fixings(const simplex_solver_settings_t& settings, + const std::vector& node_solution, + const std::vector& integer_list, + i_t target_num_fixed, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) { + i_t num_fixed = 0; + i_t num_bound_changed = 0; + for (i_t j : integer_list) { - if (num_var_fixed >= round_target) return; - if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { continue; } + if (num_fixed >= target_num_fixed) break; + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) continue; f_t old_lower = lower[j]; f_t old_upper = upper[j]; lower[j] = std::clamp(std::floor(node_solution[j]), old_lower, old_upper); upper[j] = std::clamp(std::ceil(node_solution[j]), old_lower, old_upper); bounds_changed[j] = lower[j] != old_lower || upper[j] != old_upper; - if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) ++num_var_fixed; + num_bound_changed += bounds_changed[j]; + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) ++num_fixed; } + + return num_bound_changed; } template -void apply_rins_fixings(const simplex_solver_settings_t& settings, - const std::vector& current_sol, - const std::vector& fractional, - const std::vector& current_incumbent, - i_t round_target, - std::vector& lower, - std::vector& upper, - std::vector& bounds_changed, - i_t& num_var_fixed) +i_t apply_rins_fixings(const simplex_solver_settings_t& settings, + const std::vector& current_sol, + const std::vector& integer_list, + const std::vector& current_incumbent, + f_t target_fixrate, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) { - for (i_t j : fractional) { - if (num_var_fixed >= round_target) return; - if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { continue; } + i_t num_fixed = 0; + i_t target_num_fixed = target_fixrate * integer_list.size(); + + for (i_t j : integer_list) { + if (num_fixed >= target_num_fixed) break; + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) continue; if (std::abs(current_sol[j] - current_incumbent[j]) <= settings.integer_tol) { f_t fixed_val = std::round(current_sol[j]); fix_variable(j, lower, upper, bounds_changed, fixed_val); - ++num_var_fixed; + ++num_fixed; } } + + return num_fixed; } template -void extend_variable_fixings(const simplex_solver_settings_t& settings, - const std::vector& obj_coeffs, - const std::vector& fractional, - const std::vector& current_sol, - const std::vector& root_solution, - i_t round_target, - std::vector& lower, - std::vector& upper, - std::vector& bounds_changed, - i_t& num_var_fixed) +i_t extend_variable_fixings(const simplex_solver_settings_t& settings, + const std::vector& obj_coeffs, + const std::vector& fractional, + const std::vector& current_sol, + const std::vector& root_solution, + i_t target_num_fixed, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) { std::vector> candidates; for (i_t j : fractional) { @@ -2626,17 +2611,21 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings return std::get<0>(a) < std::get<0>(b); }); - f_t change = 0; + i_t num_fixed = 0; + f_t change = 0; + for (auto [dist, j, fixed_val] : candidates) { - if (num_var_fixed >= round_target) return; + if (num_fixed >= target_num_fixed) break; fix_variable(j, lower, upper, bounds_changed, fixed_val); - ++num_var_fixed; + ++num_fixed; // Limit the amount of fixing to the current LP. change += dist; - if (change >= 0.5) return; + if (change >= 0.5) break; } + + return num_fixed; } template @@ -2675,7 +2664,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w ++submip_stats.total_calls; - bool has_submip = false; + bool has_submip = false; worker->recompute_bounds = false; worker->recompute_basis = true; @@ -2694,68 +2683,87 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w std::vector integer_list; get_unfixed_integer_variables(lower, upper, var_types, settings_.fixed_tol, integer_list); - i_t num_integers = integer_list.size(); - f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); - f_t min_fixrate = settings_.submip_settings.min_fixrate; - i_t num_var_fixed = 0; - f_t fixrate = 0; + i_t num_integers = integer_list.size(); + f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); + f_t min_fixrate = settings_.submip_settings.min_fixrate; + f_t fixrate = 0; + f_t close_ratio = settings_.submip_settings.round_close_ratio; i_t round = 0; while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { - f_t prev_fixrate = fixrate; - f_t round_fixrate = std::min( - 1.0 - (1.0 - prev_fixrate) * settings_.submip_settings.round_close_ratio, max_fixrate); - i_t round_target = std::max(round_fixrate * num_integers, num_var_fixed + 1); + 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; + i_t round_target = round_target_fixrate * num_integers; + i_t num_bound_changed = 0; // Shuffle the fractional and integer list, so every variable has the same chance to the picked // (we iterate the list in order). worker->rng.shuffle(integer_list); worker->rng.shuffle(fractional); - // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the - // current incumbent, considering only the fractional values in the current node - i_t prev_num_fixed = num_var_fixed; - if (worker->search_strategy == search_strategy_t::RINS) { - apply_rins_fixings(settings_, - current_sol, - fractional, - current_incumbent, - round_target, - lower, - upper, - bounds_changed, - num_var_fixed); - - if (prev_num_fixed == num_var_fixed) { - // RINS neighbourhood 2: Search the entire list of integer variables where the current - // LP solution matches the current incumbent. - apply_rins_fixings(settings_, - current_sol, - integer_list, - current_incumbent, - round_target, - lower, - upper, - bounds_changed, - num_var_fixed); - } + // RINS neighbourhood: Fix all the integer variables where the current solution matches the + // incumbent. We are using the `max_fixrate` here to allow RINS to fix all integer variables + // that it can within our budget. + num_bound_changed = apply_rins_fixings(settings_, + current_sol, + integer_list, + current_incumbent, + max_fixrate - prev_fixrate, + lower, + upper, + bounds_changed); // The RINS neighbourhood ran dry. If it is already tight enough, take it rather than // diluting it with fixings that do not agree with the incumbent. - if (prev_num_fixed == num_var_fixed && prev_fixrate >= min_fixrate) { + if (num_bound_changed == 0 && fixrate >= min_fixrate) { has_submip = true; break; } } else if (worker->search_strategy == search_strategy_t::RENS) { - apply_rens_fixings(settings_, - current_sol, - integer_list, - round_target, - lower, - upper, - bounds_changed, - num_var_fixed); + if (round_target == 0) { + round_target_fixrate = max_fixrate - prev_fixrate; + round_target = round_target_fixrate * num_integers; + if (round_target == 0) { + has_submip = fixrate > 0; + break; + } + } + + num_bound_changed = apply_rens_fixings( + settings_, current_sol, integer_list, round_target, lower, upper, bounds_changed); + } + + // Even considering the entire integer list, we were unable to fix a single variable in this + // iteration. Iterate over the fractional variables again and fixing those that closest to + // an integer solution first in order to reach the fixing threshold. + if (num_bound_changed == 0) { + if (round_target == 0) { + round_target_fixrate = max_fixrate - prev_fixrate; + round_target = round_target_fixrate * num_integers; + if (round_target == 0) { + has_submip = fixrate > 0; + break; + } + } + + num_bound_changed = extend_variable_fixings(settings_, + worker->leaf_problem.objective, + fractional, + current_sol, + worker->root_solution, + round_target, + lower, + upper, + bounds_changed); + + // Even sweep over all integer variables, we exhausted all variables that can be fixed. + // If this is the case, then tries to solve the sub-mip anyway. + if (num_bound_changed == 0) { + has_submip = true; + break; + } } if (toc(exploration_stats_.start_time) > settings_.time_limit) { @@ -2765,20 +2773,20 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w bool is_feasible = worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); - fixrate = calculate_fixrate(integer_list, lower, upper, settings_.fixed_tol); - num_var_fixed = fixrate * num_integers; + fixrate = calculate_fixrate(integer_list, lower, upper, settings_.fixed_tol); DEBUG_SUBMIP( - "{}Round {}: fixed {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} ({:.2f}). max " - "fixrate = {:.4g}", + "{}Round {}: fixed {:.0f} ({:.2f}) -> {:.0f} ({:.2f}) variables. target round fixrate = {} " + "({:.2f}). " + "max fixrate = {:.4g}", log_prefix, round, - prev_num_fixed, + prev_fixrate * num_integers, prev_fixrate, - num_var_fixed, + fixrate * num_integers, fixrate, round_target, - round_fixrate, + round_target_fixrate, max_fixrate); if (!is_feasible) { @@ -2791,66 +2799,6 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w break; } - // Even considering the entire integer list, we were unable to fix a single variable in this - // iteration. Iterate over the fractional variables again and fixing those that closest to - // an integer solution first in order to reach the fixing threshold. - if (num_var_fixed < round_target) { - prev_num_fixed = num_var_fixed; - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - - extend_variable_fixings(settings_, - worker->leaf_problem.objective, - fractional, - current_sol, - worker->root_solution, - round_target, - lower, - upper, - bounds_changed, - num_var_fixed); - - is_feasible = - worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); - fixrate = calculate_fixrate(integer_list, lower, upper, settings_.fixed_tol); - num_var_fixed = fixrate * num_integers; - - DEBUG_SUBMIP( - "{}Round {}: extended fixings {} ({:.2f}) -> {} ({:.2f}) variables. target fixrate = {} " - "({:.2f}). max " - "fixrate = {:.4g}", - log_prefix, - round, - prev_num_fixed, - prev_fixrate, - num_var_fixed, - fixrate, - round_target, - round_fixrate, - max_fixrate); - - if (!is_feasible) { - DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) - break; - } - - if (fixrate >= max_fixrate) { - has_submip = true; - break; - } - - // Even sweep over all integer variables, we exhausted all variables that can be fixed. - // If this is the case, then tries to solve the sub-mip anyway. - if (prev_num_fixed == num_var_fixed) { - has_submip = true; - break; - } - } - - if (toc(exploration_stats_.start_time) > settings_.time_limit) { - solver_status_ = mip_status_t::TIME_LIMIT; - break; - } - // After fixing the variables, re-solve the LP relaxation. We use the optimal solution // in the next iteration to find additional variable fixings. // We continue to do this until enough variables were fixed or no variable is left to fix. @@ -2955,8 +2903,8 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w DEBUG_SUBMIP("{}Running a quick DFS. fixrate={:.4g} ({}/{})", log_prefix, fixrate, - num_var_fixed, - integer_list.size()); + fixrate * num_integers, + num_integers); dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } } @@ -2966,22 +2914,22 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w current_incumbent, var_types, submip_stats, - num_var_fixed, - num_integers, + fixrate, stats.total_simplex_iters, is_root_heuristic); } } DEBUG_SUBMIP( - "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g}, " + "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({:.0f}/{}), max_fixrate={:.4g}, " "min_fixrate={:.4g}\n", log_prefix, submip_stats.total_success.load(), submip_stats.total_infeasible.load(), submip_stats.total_calls.load(), fixrate, - num_var_fixed, + fixrate * num_integers, + num_integers, max_fixrate, min_fixrate); @@ -4027,6 +3975,18 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (!std::isfinite(lower_bound)) { lower_bound = search_tree_.root.lower_bound; } } + DEBUG_SUBMIP("RINS: success={}, infeasible={}, infeasible={}, calls={}", + rins_stats_.total_success.load(), + rins_stats_.total_infeasible.load(), + rins_stats_.total_empty.load(), + rins_stats_.total_calls.load()); + + DEBUG_SUBMIP("RENS: success={}, infeasible={}, infeasible={}, calls={}", + rens_stats_.total_success.load(), + rens_stats_.total_infeasible.load(), + rens_stats_.total_empty.load(), + rens_stats_.total_calls.load()); + set_final_solution(solution, lower_bound); return solver_status_; } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 5c570dfff3..573495cb5f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -383,8 +383,7 @@ class branch_and_bound_t { const std::vector& current_incumbent, const std::vector& var_types, submip_stats_t& submip_stats, - i_t num_var_fixed, - i_t num_integers, + f_t fixrate, i_t simplex_iter_used, bool is_root_heuristic = false); diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 50e9a4a206..017ba848fc 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -263,6 +263,7 @@ struct submip_stats_t { omp_atomic_t total_infeasible = 0; omp_atomic_t infeasible_fixrate_sum = 0; omp_atomic_t total_calls = 0; + omp_atomic_t total_empty = 0; void save_success(double fixrate) { @@ -276,6 +277,7 @@ struct submip_stats_t { infeasible_fixrate_sum += fixrate; } + void save_empty() { ++total_empty; } double average_infeasible_fixrate() const { return infeasible_fixrate_sum / total_infeasible; } double average_success_fixrate() const { return success_fixrate_sum / total_success; } }; From 006395a4e4b08531e7cc15c3a126b3a1e20b9af5 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 19 Aug 2026 16:24:58 +0200 Subject: [PATCH 29/29] fixed default value mismatch. fixed some minor bugs. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 31 ++++++++++--------- cpp/src/branch_and_bound/branch_and_bound.hpp | 4 +-- cpp/src/branch_and_bound/worker.hpp | 2 +- cpp/src/math_optimization/solver_settings.cu | 6 ++-- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index ae8535d847..29174148b7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2645,8 +2645,8 @@ 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) + const std::vector& var_types, + bool is_root_heuristic) { raft::common::nvtx::range scope("BB::submip_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } @@ -2664,7 +2664,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w ++submip_stats.total_calls; - bool has_submip = false; + bool has_submip = false; worker->recompute_bounds = false; worker->recompute_basis = true; @@ -2752,7 +2752,7 @@ void branch_and_bound_t::recursive_submip(diving_worker_t* w worker->leaf_problem.objective, fractional, current_sol, - worker->root_solution, + worker->root_solution, round_target, lower, upper, @@ -3000,7 +3000,8 @@ void branch_and_bound_t::launch_root_heuristics( #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); + recursive_submip( + worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); --(*worker_count); } } @@ -3913,14 +3914,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.x, edge_norms_); submip_worker_pool_.init(num_submip_workers, - original_lp_, - Arow_, - var_types_, - symmetry_, - settings_, - root_relax_soln_.x, - edge_norms_, - num_bfs_workers); + original_lp_, + Arow_, + var_types_, + symmetry_, + settings_, + root_relax_soln_.x, + edge_norms_, + num_bfs_workers); if (num_diving_workers > 0) { diving_worker_pool_.init(num_diving_workers, @@ -3975,13 +3976,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (!std::isfinite(lower_bound)) { lower_bound = search_tree_.root.lower_bound; } } - DEBUG_SUBMIP("RINS: success={}, infeasible={}, infeasible={}, calls={}", + DEBUG_SUBMIP("RINS: success={}, infeasible={}, empty={}, calls={}", rins_stats_.total_success.load(), rins_stats_.total_infeasible.load(), rins_stats_.total_empty.load(), rins_stats_.total_calls.load()); - DEBUG_SUBMIP("RENS: success={}, infeasible={}, infeasible={}, calls={}", + DEBUG_SUBMIP("RENS: success={}, infeasible={}, empty={}, calls={}", rens_stats_.total_success.load(), rens_stats_.total_infeasible.load(), rens_stats_.total_empty.load(), diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 573495cb5f..7cf5ed3680 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -390,8 +390,8 @@ class branch_and_bound_t { // Creates and solves the RINS sub-MIP void recursive_submip(diving_worker_t* worker, const std::vector& current_incumbent, - const std::vector& var_types, - bool is_root_heuristic = false); + const std::vector& var_types, + bool is_root_heuristic = false); 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 017ba848fc..bbc2836d43 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -263,7 +263,7 @@ struct submip_stats_t { omp_atomic_t total_infeasible = 0; omp_atomic_t infeasible_fixrate_sum = 0; omp_atomic_t total_calls = 0; - omp_atomic_t total_empty = 0; + omp_atomic_t total_empty = 0; void save_success(double fixrate) { diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 97862db8bf..820f41ee3e 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -122,7 +122,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP, &mip_settings.submip_params.min_fixrate_cap, f_t(0.0), f_t(1.0), f_t(0.1), "hard cap on the minimum fix rate for solving a sub-MIP"}, {CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP, &mip_settings.submip_params.target_mip_gap, f_t(0.0), f_t(1.0), f_t(0.01), "MIP gap target for the sub-MIP"}, {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO, &mip_settings.submip_params.iteration_limit_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "sub-MIP simplex-iteration limit as a factor of parent B&B iterations"}, - {CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO, &mip_settings.submip_params.round_close_ratio, f_t(0.0), f_t(1.0), f_t(0.9), "share of the still-unfixed integers left for later neighbourhood rounds (0 reaches the target fix rate in a single round)"}, + {CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO, &mip_settings.submip_params.round_close_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "share of the still-unfixed integers left for later neighbourhood rounds (0 reaches the target fix rate in a single round)"}, }; // Int parameters @@ -188,8 +188,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") {CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_OFFSET, &mip_settings.submip_params.node_limit_offset, 0, std::numeric_limits::max(), 200, "base node limit for the sub-MIP"}, -{CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET, &mip_settings.submip_params.iteration_limit_offset, 0, std::numeric_limits::max(), 10000, "base sub-MIP simplex-iteration limit for root heuristics"}, -{CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL, &mip_settings.submip_params.max_level, 0, std::numeric_limits::max(), 10, "maximum sub-MIP recursion level"}, + {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET, &mip_settings.submip_params.iteration_limit_offset, 0, std::numeric_limits::max(), 10000, "base sub-MIP simplex-iteration limit for root heuristics"}, + {CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL, &mip_settings.submip_params.max_level, 0, std::numeric_limits::max(), 10, "maximum sub-MIP recursion level"}, {CUOPT_BARRIER_PRESOLVE_BOUND_FREE_VARIABLES, &pdlp_settings.barrier_presolve_bound_free_variables, -1, 1, -1, "Bound free variables during barrier presolve: -1 automatic (current default behavior), 0 disabled, 1 enabled"}, // QCQP (barrier) scaling hyper-parameter {CUOPT_QCQP_HYPER_RUIZ_EQUILIBRATION, &pdlp_settings.qcqp_ruiz_equilibration, -1, 1, -1, "Ruiz equilibration for QCQP barrier scaling: -1 automatic (row/column imbalance heuristic), 0 disabled, 1 enabled"},