From e0fd56f2746217764e99064cc592a08304cc0e1d Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 10 Aug 2026 16:32:25 -0700 Subject: [PATCH 1/9] Generate cuts during concurrent root solve Signed-off-by: Hugo Linsenmaier --- cpp/src/branch_and_bound/branch_and_bound.cpp | 68 ++++++++++++++++++- cpp/src/branch_and_bound/branch_and_bound.hpp | 4 +- cpp/src/cuts/cuts.cpp | 17 +++++ cpp/src/cuts/cuts.hpp | 11 +++ 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 29174148b7..7438a0f828 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3016,9 +3016,18 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( basis_update_mpf_t& basis_update, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& edge_norms) + std::vector& edge_norms, + variable_bounds_t& variable_bounds, + cut_pool_t& cut_pool) { lp_status_t root_status; + i_t relaxation_cut_task_status = 0; + bool relaxation_cut_task_started{false}; + f_t relaxation_cut_task_elapsed{0.0}; + method_t relaxation_cut_method{Unset}; + std::vector relaxation_root_x; + std::vector relaxation_root_y; + std::vector relaxation_root_z; // Launch a task for solving the root LP relaxation via dual simplex. #pragma omp task default(shared) depend(out : root_status) priority(CUOPT_CRITICAL_TASK_PRIORITY) @@ -3062,6 +3071,47 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( root_crossover_soln_.y = crushed_root_y; root_crossover_soln_.z = crushed_root_z; + if ((root_relax_solved_by == PDLP || root_relax_solved_by == Barrier) && + settings_.max_cut_passes > 0 && omp_get_num_threads() >= 3) { + relaxation_root_x = root_crossover_soln_.x; + relaxation_root_y = root_crossover_soln_.y; + relaxation_root_z = root_crossover_soln_.z; + relaxation_cut_method = root_relax_solved_by; + relaxation_cut_task_started = true; + +#pragma omp task default(shared) depend(out : relaxation_cut_task_status) \ + priority(CUOPT_DEFAULT_TASK_PRIORITY) + { + const f_t cut_start_time = tic(); + auto cut_settings = settings_; + // The clique table is still being built asynchronously. Keep this pass independent of it; + // the normal root pass runs clique and zero-half separation after the join. + cut_settings.clique_cuts = 0; + cut_settings.zero_half_cuts = 0; + cut_generation_t relaxation_cut_generation(cut_pool, + original_lp_, + cut_settings, + Arow_, + new_slacks_, + var_types_, + original_problem_, + probing_implied_bound_); + const bool feasible = + relaxation_cut_generation.generate_basis_independent_cuts(original_lp_, + cut_settings, + Arow_, + new_slacks_, + var_types_, + relaxation_root_x, + relaxation_root_y, + relaxation_root_z, + variable_bounds, + exploration_stats_.start_time); + relaxation_cut_task_elapsed = toc(cut_start_time); + relaxation_cut_task_status = feasible ? 1 : -1; + } + } + // Call crossover on the crushed solution auto root_crossover_settings = settings_; root_crossover_settings.log.log = false; @@ -3144,6 +3194,16 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( exploration_stats_.total_simplex_iters = root_relax_soln_.iterations; } + if (relaxation_cut_task_started) { +#pragma omp taskwait depend(in : relaxation_cut_task_status) + settings_.log.printf( + "%s root cut pass generated %d candidates in %.2f seconds%s\n", + method_to_string(relaxation_cut_method).c_str(), + cut_pool.pool_size(), + relaxation_cut_task_elapsed, + relaxation_cut_task_status < 0 ? " (separator reported infeasibility)" : ""); + } + is_root_solution_set = true; return root_status; @@ -3464,6 +3524,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut variable_bounds_t variable_bounds( original_lp_, settings_, var_types_, Arow_, new_slacks_); + cut_pool_t cut_pool(original_lp_.num_cols, settings_); if (guess_.size() != 0) { raft::common::nvtx::range scope_guess("BB::check_initial_guess"); @@ -3552,7 +3613,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, basic_list, nonbasic_list, - edge_norms_); + edge_norms_, + variable_bounds, + cut_pool); } settings_.log.printf("\n"); @@ -3652,7 +3715,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } - cut_pool_t cut_pool(original_lp_.num_cols, settings_); cut_generation_t cut_generation(cut_pool, original_lp_, settings_, diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 7cf5ed3680..bfa8a43c3e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -175,7 +175,9 @@ class branch_and_bound_t { simplex::basis_update_mpf_t& basis_update, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& edge_norms); + std::vector& edge_norms, + variable_bounds_t& variable_bounds, + cut_pool_t& cut_pool); i_t find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 7acd7dee0a..95250b3433 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -3550,6 +3550,23 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, } } + return generate_basis_independent_cuts( + lp, settings, Arow, new_slacks, var_types, xstar, ystar, zstar, variable_bounds, start_time); +} + +template +bool cut_generation_t::generate_basis_independent_cuts( + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + const std::vector& new_slacks, + const std::vector& var_types, + const std::vector& xstar, + const std::vector& ystar, + const std::vector& zstar, + variable_bounds_t& variable_bounds, + f_t start_time) +{ // Generate Knapsack cuts if (settings.knapsack_cuts != 0) { if (toc(start_time) >= settings.time_limit) { return true; } diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index fcb6080178..2dba4d29d2 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -691,6 +691,17 @@ class cut_generation_t { variable_bounds_t& variable_bounds, f_t start_time); + bool generate_basis_independent_cuts(const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + const std::vector& new_slacks, + const std::vector& var_types, + const std::vector& xstar, + const std::vector& ystar, + const std::vector& zstar, + variable_bounds_t& variable_bounds, + f_t start_time); + private: // Generate all mixed integer gomory cuts void generate_gomory_cuts(const simplex::lp_problem_t& lp, From e57ba0f0a0f6ae918a9b3d169ef1ea0591a87405 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 10 Aug 2026 20:16:16 -0700 Subject: [PATCH 2/9] Unify root cut generation paths --- cpp/src/branch_and_bound/branch_and_bound.cpp | 73 +++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + cpp/src/cuts/cuts.cpp | 60 +++++++-------- cpp/src/cuts/cuts.hpp | 68 +++++++++-------- .../presolve/conflict_graph/clique_table.cu | 7 +- .../presolve/conflict_graph/clique_table.cuh | 10 +-- cpp/tests/mip/cuts_test.cu | 23 ++++++ 7 files changed, 137 insertions(+), 105 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 7438a0f828..534174647f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -289,6 +289,7 @@ branch_and_bound_t::branch_and_bound_t( solver_status_(mip_status_t::UNSET) { exploration_stats_.start_time = start_time; + clique_table_complete_.store(clique_table_ != nullptr, std::memory_order_relaxed); #ifdef PRINT_CONSTRAINT_MATRIX settings_.log.printf("A"); original_problem_.A.print_matrix(); @@ -3084,29 +3085,37 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( { const f_t cut_start_time = tic(); auto cut_settings = settings_; - // The clique table is still being built asynchronously. Keep this pass independent of it; - // the normal root pass runs clique and zero-half separation after the join. - cut_settings.clique_cuts = 0; - cut_settings.zero_half_cuts = 0; - cut_generation_t relaxation_cut_generation(cut_pool, - original_lp_, - cut_settings, - Arow_, - new_slacks_, - var_types_, - original_problem_, - probing_implied_bound_); + // Consume a completed clique table without stopping or waiting for its producer. If it is + // still being built, leave it running and defer clique/zero-half cuts to the normal pass. + const bool clique_table_ready = clique_table_complete_.load(std::memory_order_acquire); + if (!clique_table_ready) { + cut_settings.clique_cuts = 0; + cut_settings.zero_half_cuts = 0; + } + cut_generation_t relaxation_cut_generation( + cut_pool, + original_lp_, + cut_settings, + Arow_, + new_slacks_, + var_types_, + original_problem_, + probing_implied_bound_, + clique_table_ready ? clique_table_ : nullptr); const bool feasible = - relaxation_cut_generation.generate_basis_independent_cuts(original_lp_, - cut_settings, - Arow_, - new_slacks_, - var_types_, - relaxation_root_x, - relaxation_root_y, - relaxation_root_z, - variable_bounds, - exploration_stats_.start_time); + relaxation_cut_generation.generate_cuts(original_lp_, + cut_settings, + Arow_, + new_slacks_, + var_types_, + std::nullopt, + std::nullopt, + std::nullopt, + relaxation_root_x, + relaxation_root_y, + relaxation_root_z, + variable_bounds, + exploration_stats_.start_time); relaxation_cut_task_elapsed = toc(cut_start_time); relaxation_cut_task_status = feasible ? 1 : -1; } @@ -3247,12 +3256,12 @@ auto branch_and_bound_t::do_cut_pass( Arow_, new_slacks_, var_types_, - basis_update, + std::ref(basis_update), + std::cref(basic_list), + std::cref(nonbasic_list), root_relax_soln_.x, root_relax_soln_.y, root_relax_soln_.z, - basic_list, - nonbasic_list, variable_bounds, exploration_stats_.start_time); if (!problem_feasible) { @@ -3554,6 +3563,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if ((settings_.clique_cuts != 0 || settings_.zero_half_cuts != 0) && clique_table_ == nullptr && omp_get_num_threads() >= CUOPT_MIP_CLIQUE_CUTS_REQUIRED_THREAD_COUNT) { signal_extend_cliques_.store(false, std::memory_order_release); + clique_table_complete_.store(false, std::memory_order_release); typename mip_solver_settings_t::tolerances_t tolerances_for_clique{}; tolerances_for_clique.presolve_absolute_tolerance = settings_.primal_tol; tolerances_for_clique.absolute_tolerance = settings_.primal_tol; @@ -3567,8 +3577,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut { user_problem_t problem_copy = original_problem_; timer_t timer(std::numeric_limits::infinity()); - mip::find_initial_cliques( - problem_copy, tolerances_for_clique, clique_table_, timer, clique_signal); + mip::find_initial_cliques(problem_copy, + tolerances_for_clique, + clique_table_, + timer, + clique_signal, + &clique_table_complete_); } } @@ -3723,8 +3737,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut var_types_, original_problem_, probing_implied_bound_, - clique_table_, - clique_signal); + nullptr, + clique_signal, + std::ref(clique_table_)); std::vector saved_solution; #ifdef CHECK_CUTS_AGAINST_SAVED_SOLUTION diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index bfa8a43c3e..ac4aa91e7a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -197,6 +197,7 @@ class branch_and_bound_t { const probing_implied_bound_t& probing_implied_bound_; std::shared_ptr> clique_table_; omp_atomic_t signal_extend_cliques_{false}; + omp_atomic_t clique_table_complete_{false}; mip_symmetry_t* symmetry_; work_limit_context_t work_unit_context_{"B&B"}; diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 95250b3433..fed461b5e7 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -3264,6 +3264,7 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( signal_extend_->store(true, std::memory_order_release); #pragma omp taskwait depend(in : *signal_extend_) } + if (clique_table_source_.has_value()) { clique_table_ = clique_table_source_->get(); } if (clique_table_ == nullptr) { return; } const bool has_probing_conflicts = @@ -3516,22 +3517,30 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( } template -bool cut_generation_t::generate_cuts(const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - const std::vector& new_slacks, - const std::vector& var_types, - basis_update_mpf_t& basis_update, - const std::vector& xstar, - const std::vector& ystar, - const std::vector& zstar, - const std::vector& basic_list, - const std::vector& nonbasic_list, - variable_bounds_t& variable_bounds, - f_t start_time) +bool cut_generation_t::generate_cuts( + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + const std::vector& new_slacks, + const std::vector& var_types, + std::optional>> basis_update, + std::optional>> basic_list, + std::optional>> nonbasic_list, + const std::vector& xstar, + const std::vector& ystar, + const std::vector& zstar, + variable_bounds_t& variable_bounds, + f_t start_time) { + const bool has_basis = + basis_update.has_value() && basic_list.has_value() && nonbasic_list.has_value(); + cuopt_assert(has_basis || (!basis_update.has_value() && !basic_list.has_value() && + !nonbasic_list.has_value()), + "Cut basis references must be provided together"); + // Generate Gomory and CG Cuts - if (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { + if (has_basis && + (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0)) { if (toc(start_time) >= settings.time_limit) { return true; } f_t cut_start_time = tic(); generate_gomory_cuts(lp, @@ -3539,10 +3548,10 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, Arow, new_slacks, var_types, - basis_update, + basis_update->get(), xstar, - basic_list, - nonbasic_list, + basic_list->get(), + nonbasic_list->get(), start_time); f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { @@ -3550,23 +3559,6 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, } } - return generate_basis_independent_cuts( - lp, settings, Arow, new_slacks, var_types, xstar, ystar, zstar, variable_bounds, start_time); -} - -template -bool cut_generation_t::generate_basis_independent_cuts( - const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - const std::vector& new_slacks, - const std::vector& var_types, - const std::vector& xstar, - const std::vector& ystar, - const std::vector& zstar, - variable_bounds_t& variable_bounds, - f_t start_time) -{ // Generate Knapsack cuts if (settings.knapsack_cuts != 0) { if (toc(start_time) >= settings.time_limit) { return true; } diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 2dba4d29d2..9ae5aa6d70 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -16,9 +16,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -657,50 +659,44 @@ struct fractional_conflict_subgraph_t { template class cut_generation_t { public: - cut_generation_t(cut_pool_t& cut_pool, - const simplex::lp_problem_t& lp, - const simplex::simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - const std::vector& new_slacks, - const std::vector& var_types, - const simplex::user_problem_t& user_problem, - const probing_implied_bound_t& probing_implied_bound, - std::shared_ptr> clique_table = nullptr, - omp_atomic_t* signal_extend = nullptr) + cut_generation_t( + cut_pool_t& cut_pool, + const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + const std::vector& new_slacks, + const std::vector& var_types, + const simplex::user_problem_t& user_problem, + const probing_implied_bound_t& probing_implied_bound, + std::shared_ptr> clique_table = nullptr, + omp_atomic_t* signal_extend = nullptr, + std::optional>>> + clique_table_source = std::nullopt) : cut_pool_(cut_pool), knapsack_generation_(lp, settings, Arow, new_slacks, var_types), flow_cover_generation_(lp, settings, Arow, new_slacks), user_problem_(user_problem), probing_implied_bound_(probing_implied_bound), clique_table_(std::move(clique_table)), - signal_extend_(signal_extend) + signal_extend_(signal_extend), + clique_table_source_(clique_table_source) { } - bool generate_cuts(const simplex::lp_problem_t& lp, - const simplex::simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - const std::vector& new_slacks, - const std::vector& var_types, - simplex::basis_update_mpf_t& basis_update, - const std::vector& xstar, - const std::vector& ystar, - const std::vector& zstar, - const std::vector& basic_list, - const std::vector& nonbasic_list, - variable_bounds_t& variable_bounds, - f_t start_time); - - bool generate_basis_independent_cuts(const simplex::lp_problem_t& lp, - const simplex::simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - const std::vector& new_slacks, - const std::vector& var_types, - const std::vector& xstar, - const std::vector& ystar, - const std::vector& zstar, - variable_bounds_t& variable_bounds, - f_t start_time); + bool generate_cuts( + const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + const std::vector& new_slacks, + const std::vector& var_types, + std::optional>> basis_update, + std::optional>> basic_list, + std::optional>> nonbasic_list, + const std::vector& xstar, + const std::vector& ystar, + const std::vector& zstar, + variable_bounds_t& variable_bounds, + f_t start_time); private: // Generate all mixed integer gomory cuts @@ -783,6 +779,8 @@ class cut_generation_t { const probing_implied_bound_t& probing_implied_bound_; std::shared_ptr> clique_table_; omp_atomic_t* signal_extend_{nullptr}; + std::optional>>> + clique_table_source_; fractional_conflict_subgraph_t sub_cg_; }; diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index a8e6997572..6f1a1c6b9f 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -672,7 +672,8 @@ void find_initial_cliques(user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, std::shared_ptr>& clique_table_out, cuopt::timer_t& timer, - omp_atomic_t* signal_extend) + omp_atomic_t* signal_extend, + omp_atomic_t* complete) { cuopt::timer_t stage_timer(std::numeric_limits::infinity()); #ifdef DEBUG_CLIQUE_TABLE @@ -759,6 +760,7 @@ void find_initial_cliques(user_problem_t& problem, find_work_estimate, extend_work); #endif + if (complete != nullptr) { complete->store(true, std::memory_order_release); } } #define INSTANTIATE(F_TYPE) \ @@ -767,7 +769,8 @@ void find_initial_cliques(user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, \ std::shared_ptr> & clique_table_out, \ cuopt::timer_t & timer, \ - omp_atomic_t * signal_extend); \ + omp_atomic_t * signal_extend, \ + omp_atomic_t * complete); \ template void build_clique_table( \ const user_problem_t& problem, \ clique_table_t& clique_table, \ diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh index fe6db31c26..34553592e4 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh @@ -203,16 +203,16 @@ struct clique_table_t { // Builds the conflict-graph clique table for `problem`. The base cliques are // published to `clique_table_out` before the (optional, signal-gated) extension -// phase begins, so cut generation can pick up the table while extension keeps -// running concurrently. Consumers MUST set `*signal_extend` and join the -// producing task before reading the table (see prepare_fractional_sub_conflict_graph), since -// the extension phase keeps mutating the same object after it is published. +// phase begins. Consumers may read it after an acquire load of `*complete` +// returns true. Otherwise they MUST set `*signal_extend` and join the producing +// task before reading it, since extension keeps mutating the published object. template void find_initial_cliques(simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, std::shared_ptr>& clique_table_out, cuopt::timer_t& timer, - omp_atomic_t* signal_extend = nullptr); + omp_atomic_t* signal_extend = nullptr, + omp_atomic_t* complete = nullptr); template void build_clique_table(const simplex::user_problem_t& problem, diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 5af0754e3d..3089fa2304 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -983,6 +983,29 @@ TEST(cuts, test_duplicate_cuts_detection) cut_pool.check_for_duplicate_cuts(); } +TEST(cuts, clique_initial_builder_publishes_completion) +{ + const raft::handle_t handle{}; + auto model = create_pairwise_triangle_set_packing_problem(); + auto op_problem = mps_data_model_to_optimization_problem(&handle, model); + mip::problem_t mip_problem(op_problem); + simplex::user_problem_t host_problem(op_problem.get_handle_ptr()); + mip_problem.get_host_user_problem(host_problem); + + mip_solver_settings_t settings; + cuopt::timer_t timer(std::numeric_limits::infinity()); + std::shared_ptr> clique_table; + omp_atomic_t stop_requested{false}; + omp_atomic_t complete{false}; + + mip::find_initial_cliques( + host_problem, settings.tolerances, clique_table, timer, &stop_requested, &complete); + + EXPECT_TRUE(complete.load(std::memory_order_acquire)); + ASSERT_NE(clique_table, nullptr); + EXPECT_EQ(clique_table->n_variables, host_problem.num_cols); +} + TEST(cuts, clique_phase1_smoke_conflict_graph_edges) { const raft::handle_t handle{}; From 969f6d4d9dcad9f1d0c9c95e7577f3634d4695ec Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 10 Aug 2026 21:08:16 -0700 Subject: [PATCH 3/9] Track retained speculative root cuts --- cpp/src/branch_and_bound/branch_and_bound.cpp | 17 +++++--- cpp/src/cuts/cuts.cpp | 41 ++++++++++++------- cpp/src/cuts/cuts.hpp | 6 ++- cpp/tests/mip/cuts_test.cu | 33 +++++++++++++++ 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 534174647f..0d0cedbd4c 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3109,11 +3109,11 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( new_slacks_, var_types_, std::nullopt, - std::nullopt, - std::nullopt, relaxation_root_x, relaxation_root_y, relaxation_root_z, + std::nullopt, + std::nullopt, variable_bounds, exploration_stats_.start_time); relaxation_cut_task_elapsed = toc(cut_start_time); @@ -3205,11 +3205,16 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( if (relaxation_cut_task_started) { #pragma omp taskwait depend(in : relaxation_cut_task_status) + const i_t generated_cuts = cut_pool.pool_size(); + const i_t retained_cuts = + generated_cuts == 0 ? 0 : cut_pool.count_violated_cuts(root_relax_soln.x); settings_.log.printf( - "%s root cut pass generated %d candidates in %.2f seconds%s\n", + "%s root cut pass generated %d candidates in %.2f seconds; %d remain violated after the " + "basis solve%s\n", method_to_string(relaxation_cut_method).c_str(), - cut_pool.pool_size(), + generated_cuts, relaxation_cut_task_elapsed, + retained_cuts, relaxation_cut_task_status < 0 ? " (separator reported infeasibility)" : ""); } @@ -3257,11 +3262,11 @@ auto branch_and_bound_t::do_cut_pass( new_slacks_, var_types_, std::ref(basis_update), - std::cref(basic_list), - std::cref(nonbasic_list), root_relax_soln_.x, root_relax_soln_.y, root_relax_soln_.z, + std::cref(basic_list), + std::cref(nonbasic_list), variable_bounds, exploration_stats_.start_time); if (!problem_feasible) { diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index fed461b5e7..6cadb514d4 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -1356,6 +1357,23 @@ void cut_pool_t::check_for_duplicate_cuts() } } +template +auto cut_pool_t::count_violated_cuts(const std::vector& x_relax) -> i_t +{ + check_for_duplicate_cuts(); + if (cut_storage_.m == 0) { return 0; } + + i_t violated_cuts = 0; + const i_t num_tasks = std::min(omp_get_num_threads(), cut_storage_.m); +#pragma omp taskloop num_tasks(num_tasks) default(shared) reduction(+ : violated_cuts) + for (i_t i = 0; i < cut_storage_.m; i++) { + f_t violation; + f_t cut_norm; + if (cut_distance(i, x_relax, violation, cut_norm) > min_cut_distance_) { violated_cuts++; } + } + return violated_cuts; +} + template void cut_pool_t::score_cuts(std::vector& x_relax) { @@ -1363,18 +1381,13 @@ void cut_pool_t::score_cuts(std::vector& x_relax) cut_distances_.resize(cut_storage_.m, 0.0); cut_norms_.resize(cut_storage_.m, 0.0); - const bool verbose = false; - for (i_t i = 0; i < cut_storage_.m; i++) { - f_t violation; - f_t cut_dist = cut_distance(i, x_relax, violation, cut_norms_[i]); - cut_distances_[i] = cut_dist <= min_cut_distance_ ? 0.0 : cut_dist; - if (verbose) { - settings_.log.printf("Cut %d type %d distance %+e violation %+e cut_norm %e\n", - i, - static_cast(cut_type_[i]), - cut_distances_[i], - violation, - cut_norms_[i]); + if (cut_storage_.m > 0) { + const i_t num_tasks = std::min(omp_get_num_threads(), cut_storage_.m); +#pragma omp taskloop num_tasks(num_tasks) default(shared) + for (i_t i = 0; i < cut_storage_.m; i++) { + f_t violation; + f_t cut_dist = cut_distance(i, x_relax, violation, cut_norms_[i]); + cut_distances_[i] = cut_dist <= min_cut_distance_ ? 0.0 : cut_dist; } } @@ -3524,11 +3537,11 @@ bool cut_generation_t::generate_cuts( const std::vector& new_slacks, const std::vector& var_types, std::optional>> basis_update, - std::optional>> basic_list, - std::optional>> nonbasic_list, const std::vector& xstar, const std::vector& ystar, const std::vector& zstar, + std::optional>> basic_list, + std::optional>> nonbasic_list, variable_bounds_t& variable_bounds, f_t start_time) { diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 9ae5aa6d70..3863adc6b4 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -323,6 +323,8 @@ class cut_pool_t { // We expect that the cut is violated by the current relaxation xstar. void add_cut(cut_type_t cut_type, const inequality_t& cut); + i_t count_violated_cuts(const std::vector& x_relax); + void score_cuts(std::vector& x_relax); // We return the cuts in the form best_cuts*x <= best_rhs @@ -690,11 +692,11 @@ class cut_generation_t { const std::vector& new_slacks, const std::vector& var_types, std::optional>> basis_update, - std::optional>> basic_list, - std::optional>> nonbasic_list, const std::vector& xstar, const std::vector& ystar, const std::vector& zstar, + std::optional>> basic_list, + std::optional>> nonbasic_list, variable_bounds_t& variable_bounds, f_t start_time); diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 3089fa2304..07db15a060 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -983,6 +983,39 @@ TEST(cuts, test_duplicate_cuts_detection) cut_pool.check_for_duplicate_cuts(); } +TEST(cuts, count_violated_cuts_deduplicates_and_rescores) +{ + simplex::simplex_solver_settings_t settings; + mip::cut_pool_t cut_pool(2, settings); + + mip::inequality_t cut; + cut.push_back(0, 1.0); + cut.rhs = 1.0; + cut_pool.add_cut(mip::cut_type_t::KNAPSACK, cut); + + mip::inequality_t duplicate; + duplicate.push_back(0, 2.0); + duplicate.rhs = 2.0; + cut_pool.add_cut(mip::cut_type_t::KNAPSACK, duplicate); + + mip::inequality_t satisfied; + satisfied.push_back(1, 1.0); + satisfied.rhs = 0.0; + cut_pool.add_cut(mip::cut_type_t::FLOW_COVER, satisfied); + + int violated_cuts = 0; +#pragma omp parallel num_threads(4) shared(cut_pool, violated_cuts) + { +#pragma omp single + { + violated_cuts = cut_pool.count_violated_cuts({0.0, 0.0}); + } + } + EXPECT_EQ(violated_cuts, 1); + EXPECT_EQ(cut_pool.pool_size(), 2); + EXPECT_EQ(cut_pool.count_violated_cuts({1.0, 0.0}), 0); +} + TEST(cuts, clique_initial_builder_publishes_completion) { const raft::handle_t handle{}; From e5477bb9ab37e9e09ac6f2afd00f1a04a57ef124 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Tue, 11 Aug 2026 20:09:11 -0700 Subject: [PATCH 4/9] Preempt speculative root cut generation Signed-off-by: Hugo Linsenmaier --- cpp/src/branch_and_bound/branch_and_bound.cpp | 111 +++++++++++++----- cpp/src/branch_and_bound/branch_and_bound.hpp | 5 +- cpp/src/cuts/cuts.cpp | 102 +++++++++++----- cpp/tests/mip/cuts_test.cu | 48 ++++++++ 4 files changed, 210 insertions(+), 56 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 0d0cedbd4c..f17bf5bbb3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3023,6 +3023,8 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( { lp_status_t root_status; i_t relaxation_cut_task_status = 0; + std::atomic relaxation_cut_halt{0}; + std::atomic relaxation_cut_task_complete{false}; bool relaxation_cut_task_started{false}; f_t relaxation_cut_task_elapsed{0.0}; method_t relaxation_cut_method{Unset}; @@ -3083,8 +3085,9 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( #pragma omp task default(shared) depend(out : relaxation_cut_task_status) \ priority(CUOPT_DEFAULT_TASK_PRIORITY) { - const f_t cut_start_time = tic(); - auto cut_settings = settings_; + const f_t cut_start_time = tic(); + auto cut_settings = settings_; + cut_settings.concurrent_halt = &relaxation_cut_halt; // Consume a completed clique table without stopping or waiting for its producer. If it is // still being built, leave it running and defer clique/zero-half cuts to the normal pass. const bool clique_table_ready = clique_table_complete_.load(std::memory_order_acquire); @@ -3118,6 +3121,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( exploration_stats_.start_time); relaxation_cut_task_elapsed = toc(cut_start_time); relaxation_cut_task_status = feasible ? 1 : -1; + relaxation_cut_task_complete.store(true, std::memory_order_release); } } @@ -3134,6 +3138,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( // Check if crossover was stopped by dual simplex if (crossover_status == crossover_status_t::OPTIMAL) { + if (relaxation_cut_task_started) { relaxation_cut_halt.store(1, std::memory_order_release); } // Stop dual simplex and then wait it to finish set_root_concurrent_halt(1); #pragma omp taskwait depend(in : root_status) @@ -3193,28 +3198,33 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( } else { // Wait for the dual simplex to finish (after telling PDLP/Barrier to stop) #pragma omp taskwait depend(in : root_status) + if (relaxation_cut_task_started) { relaxation_cut_halt.store(1, std::memory_order_release); } root_relax_solved_by = DualSimplex; exploration_stats_.total_simplex_iters = root_relax_soln_.iterations; } } else { // Wait for the dual simplex to finish (crossover do not produced a solution) #pragma omp taskwait depend(in : root_status) + if (relaxation_cut_task_started) { relaxation_cut_halt.store(1, std::memory_order_release); } root_relax_solved_by = DualSimplex; exploration_stats_.total_simplex_iters = root_relax_soln_.iterations; } if (relaxation_cut_task_started) { + const bool relaxation_cut_task_interrupted = + !relaxation_cut_task_complete.load(std::memory_order_acquire); #pragma omp taskwait depend(in : relaxation_cut_task_status) const i_t generated_cuts = cut_pool.pool_size(); const i_t retained_cuts = generated_cuts == 0 ? 0 : cut_pool.count_violated_cuts(root_relax_soln.x); settings_.log.printf( "%s root cut pass generated %d candidates in %.2f seconds; %d remain violated after the " - "basis solve%s\n", + "basis solve%s%s\n", method_to_string(relaxation_cut_method).c_str(), generated_cuts, relaxation_cut_task_elapsed, retained_cuts, + relaxation_cut_task_interrupted ? " (stopped when the basis became available)" : "", relaxation_cut_task_status < 0 ? " (separator reported infeasibility)" : ""); } @@ -3242,7 +3252,8 @@ auto branch_and_bound_t::do_cut_pass( f_t& last_objective, f_t root_relax_objective, i_t& cut_pool_size, - [[maybe_unused]] const std::vector& saved_solution) -> cut_pass_result_t + [[maybe_unused]] const std::vector& saved_solution, + cut_pass_mode_t mode) -> cut_pass_result_t { #ifdef PRINT_FRACTIONAL_INFO settings_.log.printf("Found %d fractional variables on cut pass %d\n", num_fractional, cut_pass); @@ -3255,35 +3266,42 @@ auto branch_and_bound_t::do_cut_pass( } #endif - f_t cut_start_time = tic(); - bool problem_feasible = cut_generation.generate_cuts(original_lp_, - settings_, - Arow_, - new_slacks_, - var_types_, - std::ref(basis_update), - root_relax_soln_.x, - root_relax_soln_.y, - root_relax_soln_.z, - std::cref(basic_list), - std::cref(nonbasic_list), - variable_bounds, - exploration_stats_.start_time); - if (!problem_feasible) { - if (settings_.heuristic_preemption_callback != nullptr) { - settings_.heuristic_preemption_callback(); + if (mode == cut_pass_mode_t::GENERATE_AND_APPLY) { + f_t cut_start_time = tic(); + bool problem_feasible = cut_generation.generate_cuts(original_lp_, + settings_, + Arow_, + new_slacks_, + var_types_, + std::ref(basis_update), + root_relax_soln_.x, + root_relax_soln_.y, + root_relax_soln_.z, + std::cref(basic_list), + std::cref(nonbasic_list), + variable_bounds, + exploration_stats_.start_time); + if (!problem_feasible) { + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); + } + return {cut_pass_action_t::RETURN, mip_status_t::INFEASIBLE}; + } + if (toc(exploration_stats_.start_time) >= settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return {cut_pass_action_t::RETURN, solver_status_}; + } + f_t cut_generation_time = toc(cut_start_time); + if (cut_generation_time > 1.0) { + settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); } - return {cut_pass_action_t::RETURN, mip_status_t::INFEASIBLE}; } if (toc(exploration_stats_.start_time) >= settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); return {cut_pass_action_t::RETURN, solver_status_}; } - f_t cut_generation_time = toc(cut_start_time); - if (cut_generation_time > 1.0) { - settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); - } // Score the cuts f_t score_start_time = tic(); cut_pool.score_cuts(root_relax_soln_.x); @@ -3294,6 +3312,9 @@ auto branch_and_bound_t::do_cut_pass( std::vector cut_rhs; std::vector cut_types; i_t num_cuts = cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types); + if (mode == cut_pass_mode_t::APPLY_EXISTING_POOL) { + settings_.log.printf("Applying %d speculative root cuts to the basis solution\n", num_cuts); + } if (num_cuts == 0) { return {cut_pass_action_t::BREAK, mip_status_t::UNSET}; } cut_info.record_cut_types(cut_types); #ifdef PRINT_CUT_POOL_TYPES @@ -3506,7 +3527,7 @@ auto branch_and_bound_t::do_cut_pass( f_t change_in_objective = root_objective_ - last_objective; const f_t factor = settings_.cut_change_threshold; const f_t min_objective = 1e-3; - if (factor > 0.0 && + if (mode == cut_pass_mode_t::GENERATE_AND_APPLY && factor > 0.0 && change_in_objective <= factor * std::max(min_objective, std::abs(root_relax_objective))) { settings_.log.printf( "Change in objective %.16e is less than 1e-3 of root relax objective %.16e\n", @@ -3514,6 +3535,8 @@ auto branch_and_bound_t::do_cut_pass( root_relax_objective); return {cut_pass_action_t::BREAK, mip_status_t::UNSET}; } + // Pass 0 must update the baseline for the first ordinary cut pass, but it must not terminate the + // normal loop based on the speculative pass's objective movement. last_objective = root_objective_; return {cut_pass_action_t::CONTINUE, mip_status_t::UNSET}; } @@ -3768,6 +3791,40 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; + + // Pass 0 consumes cuts completed from the PDLP/Barrier relaxation while the winning basis was + // being built. Score them against that basis solution and reoptimize before generating any + // basis-aware cuts. This deliberately does not consume one of max_cut_passes. + if (cut_pool.pool_size() > 0) { + cut_pass_result_t speculative_cut_result = do_cut_pass(-1, + solution, + num_fractional, + fractional, + cut_generation, + basis_update, + basic_list, + nonbasic_list, + variable_bounds, + cut_pool, + cut_info, + lp_settings, + original_rows, + last_upper_bound, + last_objective, + root_relax_objective, + cut_pool_size, + saved_solution, + cut_pass_mode_t::APPLY_EXISTING_POOL); + if (speculative_cut_result.action == cut_pass_action_t::RETURN) { + if (settings_.benchmark_info_ptr != nullptr) { + settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time); + } + signal_extend_cliques_.store(true, std::memory_order_release); +#pragma omp taskwait depend(in : *clique_signal) + return speculative_cut_result.status; + } + } + for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { if (toc(exploration_stats_.start_time) >= settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index ac4aa91e7a..8a245a8dd7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -312,6 +312,8 @@ class branch_and_bound_t { mip_status_t status{mip_status_t::UNSET}; }; + enum class cut_pass_mode_t { GENERATE_AND_APPLY, APPLY_EXISTING_POOL }; + cut_pass_result_t do_cut_pass(i_t cut_pass, simplex::mip_solution_t& solution, i_t& num_fractional, @@ -329,7 +331,8 @@ class branch_and_bound_t { f_t& last_objective, f_t root_relax_objective, i_t& cut_pool_size, - const std::vector& saved_solution); + const std::vector& saved_solution, + cut_pass_mode_t mode = cut_pass_mode_t::GENERATE_AND_APPLY); // Set the solution when found at the root node void set_solution_at_root(simplex::mip_solution_t& solution, diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 6cadb514d4..a8be6e0151 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -47,6 +47,19 @@ namespace { enum class clique_cut_build_status_t : int8_t { NO_CUT = 0, CUT_ADDED = 1, INFEASIBLE = 2 }; +template +bool concurrent_cut_generation_halted(const simplex_solver_settings_t& settings) +{ + return settings.concurrent_halt != nullptr && + settings.concurrent_halt->load(std::memory_order_acquire) != 0; +} + +template +bool cut_generation_stopped(const simplex_solver_settings_t& settings, f_t start_time) +{ + return toc(start_time) >= settings.time_limit || concurrent_cut_generation_halted(settings); +} + // Shared crash-tolerant debug logger: writes a prefixed line to stderr and // flushes immediately so the last line is visible even if the process // aborts/terminates right after. Each channel below enables it through its own @@ -213,6 +226,7 @@ struct bk_bitset_context_t { size_t words; f_t* work_estimate; f_t max_work_estimate; + std::atomic* concurrent_halt; i_t num_calls{0}; bool work_limit_reached{false}; bool call_limit_reached{false}; @@ -231,6 +245,12 @@ struct bk_bitset_context_t { } bool over_call_limit() const { return call_limit_reached || num_calls >= max_calls; } + + bool stopped() const + { + return toc(start_time) >= time_limit || + (concurrent_halt != nullptr && concurrent_halt->load(std::memory_order_acquire) != 0); + } }; inline size_t bitset_words(size_t n) { return (n + 63) / 64; } @@ -277,7 +297,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, f_t weight_R) { if (ctx.over_work_limit() || ctx.over_call_limit()) { return; } - if (toc(ctx.start_time) >= ctx.time_limit) { return; } + if (ctx.stopped()) { return; } ctx.num_calls++; // stop the recursion, for perf reasons if (ctx.num_calls > ctx.max_calls) { @@ -356,7 +376,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, ctx.call_limit_reached = true; return; } - if (toc(ctx.start_time) >= ctx.time_limit) { return; } + if (ctx.stopped()) { return; } R.push_back(v); std::vector P_next(ctx.words, 0); @@ -1043,7 +1063,8 @@ std::vector> find_maximal_cliques_for_test( time_limit, words, &work_estimate, - max_work_estimate}; + max_work_estimate, + nullptr}; std::vector R; std::vector P(words, 0); @@ -1595,6 +1616,7 @@ knapsack_generation_t::knapsack_generation_t( } for (i_t i = 0; i < lp.num_rows; i++) { + if (concurrent_cut_generation_halted(settings)) { break; } inequality_t inequality(Arow, i, lp.rhs[i]); inequality_t rational_inequality = inequality; if (!rational_coefficients(var_types, inequality, rational_inequality)) { continue; } @@ -1669,6 +1691,7 @@ flow_cover_generation_t::flow_cover_generation_t( // generate_cut. flow_cover_constraints_.reserve(2 * lp.num_rows); for (i_t i = 0; i < lp.num_rows; i++) { + if (concurrent_cut_generation_halted(settings)) { break; } if (Arow.row_start[i + 1] <= Arow.row_start[i]) { continue; } i_t slack_col = -1; f_t slack_coeff = 0.0; @@ -2996,6 +3019,9 @@ f_t knapsack_generation_t::solve_knapsack_problem(const std::vector::quiet_NaN(); + } for (i_t v = 0; v <= sum_value; ++v) { // Do not take item i-1 dp(j, v) = dp(j - 1, v); @@ -3069,7 +3095,9 @@ f_t knapsack_generation_t::exact_knapsack_problem_integer_values_fract // 4. Dynamic programming for (i_t j = 1; j <= n; ++j) { - if (toc(start_time) >= settings_.time_limit) { return std::numeric_limits::quiet_NaN(); } + if (cut_generation_stopped(settings_, start_time)) { + return std::numeric_limits::quiet_NaN(); + } for (i_t v = 0; v <= sum_value; ++v) { // Do not take item i-1 dp(j, v) = dp(j - 1, v); @@ -3125,7 +3153,9 @@ void cut_generation_t::generate_implied_bound_cuts( constexpr f_t generated_cut_work = 16.0; for (i_t j = 0; j < n_cols; j++) { - if (work_estimate > max_work_estimate || toc(start_time) >= settings.time_limit) { return; } + if (work_estimate > max_work_estimate || cut_generation_stopped(settings, start_time)) { + return; + } if (var_types[j] == variable_type_t::CONTINUOUS) { continue; } const f_t xstar_j = xstar[j]; @@ -3136,6 +3166,7 @@ void cut_generation_t::generate_implied_bound_cuts( const i_t one_end = probing_implied_bound_.one_offsets[j + 1]; for (i_t p = zero_begin; p < zero_end; p++) { work_estimate += implication_work; + if ((p - zero_begin) % 1024 == 0 && concurrent_cut_generation_halted(settings)) { return; } const i_t i = probing_implied_bound_.zero_variables[p]; if (i == j) { continue; } const f_t l_i = lp.lower[i]; @@ -3184,6 +3215,7 @@ void cut_generation_t::generate_implied_bound_cuts( // x_j = 1 implications for (i_t p = one_begin; p < one_end; p++) { work_estimate += implication_work; + if ((p - one_begin) % 1024 == 0 && concurrent_cut_generation_halted(settings)) { return; } const i_t i = probing_implied_bound_.one_variables[p]; if (i == j) { continue; } const f_t l_i = lp.lower[i]; @@ -3264,7 +3296,7 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( sub_cg_.clear(); if (settings.clique_cuts == 0 && settings.zero_half_cuts == 0) { return; } - if (toc(start_time) >= settings.time_limit) { return; } + if (cut_generation_stopped(settings, start_time)) { return; } // The clique table is produced by a background OpenMP task (spawned in // branch_and_bound). Its base cliques are published early, but the extension @@ -3307,6 +3339,10 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( sub_cg_.weights.reserve(static_cast(num_vars) * 2); for (i_t j = 0; j < num_vars; ++j) { + if (cut_generation_stopped(settings, start_time)) { + sub_cg_.clear(); + return; + } if (user_problem_.var_types[j] == variable_type_t::CONTINUOUS) { continue; } const f_t lower_bound = user_problem_.lower[j]; const f_t upper_bound = user_problem_.upper[j]; @@ -3335,7 +3371,7 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( sub_cg_.vertex_to_local.assign(static_cast(2 * num_vars), -1); sub_cg_.in_subgraph.assign(static_cast(2 * num_vars), 0); for (size_t idx = 0; idx < sub_cg_.vertices.size(); ++idx) { - if (toc(start_time) >= settings.time_limit) { + if (cut_generation_stopped(settings, start_time)) { sub_cg_.clear(); return; } @@ -3353,7 +3389,7 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( size_t total_adj_entries = 0; size_t kept_adj_entries = 0; for (size_t idx = 0; idx < sub_cg_.vertices.size(); ++idx) { - if (toc(start_time) >= settings.time_limit) { + if (cut_generation_stopped(settings, start_time)) { sub_cg_.clear(); return; } @@ -3396,6 +3432,10 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( size_t raw_edge_bound = 0; for (i_t j = 0; j < num_vars; ++j) { + if (cut_generation_stopped(settings, start_time)) { + sub_cg_.clear(); + return; + } if (!sub_cg_.in_subgraph[j]) { continue; } raw_edge_bound += probing_implied_bound_.zero_offsets[j + 1] - probing_implied_bound_.zero_offsets[j]; @@ -3462,7 +3502,7 @@ void cut_generation_t::prepare_fractional_sub_conflict_graph( std::max(source_local, candidate.second)); } } - if (toc(start_time) >= settings.time_limit) { + if (cut_generation_stopped(settings, start_time)) { sub_cg_.clear(); return; } @@ -3554,7 +3594,7 @@ bool cut_generation_t::generate_cuts( // Generate Gomory and CG Cuts if (has_basis && (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0)) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } f_t cut_start_time = tic(); generate_gomory_cuts(lp, settings, @@ -3574,7 +3614,7 @@ bool cut_generation_t::generate_cuts( // Generate Knapsack cuts if (settings.knapsack_cuts != 0) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } f_t cut_start_time = tic(); generate_knapsack_cuts(lp, settings, Arow, new_slacks, var_types, xstar, start_time); f_t cut_generation_time = toc(cut_start_time); @@ -3585,7 +3625,7 @@ bool cut_generation_t::generate_cuts( // Generate Flow Cover cuts if (settings.flow_cover_cuts != 0) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } f_t cut_start_time = tic(); generate_flow_cover_cuts(lp, settings, Arow, var_types, xstar, variable_bounds, start_time); f_t cut_generation_time = toc(cut_start_time); @@ -3596,7 +3636,7 @@ bool cut_generation_t::generate_cuts( // Generate MIR and CG cuts if (settings.mir_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } f_t cut_start_time = tic(); generate_mir_cuts( lp, settings, Arow, new_slacks, var_types, xstar, ystar, variable_bounds, start_time); @@ -3608,7 +3648,7 @@ bool cut_generation_t::generate_cuts( // Generate implied bound cuts if (settings.implied_bound_cuts != 0) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } f_t cut_start_time = tic(); generate_implied_bound_cuts(lp, settings, var_types, xstar, start_time); f_t cut_generation_time = toc(cut_start_time); @@ -3623,12 +3663,12 @@ bool cut_generation_t::generate_cuts( // each recomputing them. Done here, after the cut routines that don't // need the clique table, to give the background clique-table thread as // much time as possible to finish before we join it. - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } prepare_fractional_sub_conflict_graph(settings, xstar, start_time); // Generate Clique cuts (last to give background clique table generation maximum time) if (settings.clique_cuts != 0) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } f_t cut_start_time = tic(); bool feasible = generate_clique_cuts(lp, settings, var_types, xstar, zstar, start_time); if (!feasible) { @@ -3643,7 +3683,7 @@ bool cut_generation_t::generate_cuts( // Generate Zero-half (odd-cycle / odd-wheel) cuts; reuses the clique table built above if (settings.zero_half_cuts != 0) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } ZERO_HALF_DEBUG("generate_cuts: about to call generate_zero_half_cuts"); f_t cut_start_time = tic(); bool feasible = generate_zero_half_cuts( @@ -3677,7 +3717,7 @@ void cut_generation_t::generate_knapsack_cuts( { if (knapsack_generation_.num_knapsack_constraints() > 0) { for (i_t knapsack_row : knapsack_generation_.get_knapsack_constraints()) { - if (toc(start_time) >= settings.time_limit) { return; } + if (cut_generation_stopped(settings, start_time)) { return; } inequality_t cut(lp.num_cols); i_t knapsack_status = knapsack_generation_.generate_knapsack_cut( lp, settings, Arow, new_slacks, var_types, xstar, knapsack_row, cut, start_time); @@ -3698,7 +3738,7 @@ void cut_generation_t::generate_flow_cover_cuts( { if (flow_cover_generation_.num_constraints() > 0) { for (const auto& flow_cover_row : flow_cover_generation_.get_constraints()) { - if (toc(start_time) >= settings.time_limit) { return; } + if (cut_generation_stopped(settings, start_time)) { return; } inequality_t cut(lp.num_cols); i_t status = flow_cover_generation_.generate_cut( lp, settings, Arow, variable_bounds, var_types, xstar, flow_cover_row, cut); @@ -3717,7 +3757,7 @@ bool cut_generation_t::generate_clique_cuts( f_t start_time) { if (settings.clique_cuts == 0) { return true; } - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } const i_t num_vars = user_problem_.num_cols; CLIQUE_CUTS_DEBUG("generate_clique_cuts start num_vars=%lld time_limit=%g elapsed=%g", @@ -3762,6 +3802,7 @@ bool cut_generation_t::generate_clique_cuts( std::vector> adj_bitset(vertices.size(), std::vector(words, 0)); size_t local_adj_entries = 0; for (size_t v = 0; v < adj_local.size(); ++v) { + if (cut_generation_stopped(settings, start_time)) { return true; } local_adj_entries += adj_local[v].size(); for (const i_t neighbor : adj_local[v]) { bitset_set(adj_bitset[v], static_cast(neighbor)); @@ -3781,7 +3822,8 @@ bool cut_generation_t::generate_clique_cuts( settings.time_limit, words, &work_estimate, - max_work_estimate}; + max_work_estimate, + settings.concurrent_halt}; std::vector R; std::vector P(words, 0); std::vector X(words, 0); @@ -3801,7 +3843,7 @@ bool cut_generation_t::generate_clique_cuts( ctx.over_call_limit() ? 1 : 0); if (ctx.over_call_limit()) { return true; } if (ctx.over_work_limit()) { return true; } - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } if (work_estimate > max_work_estimate) { return true; } sparse_vector_t cut(lp.num_cols, 0); @@ -3813,7 +3855,7 @@ bool cut_generation_t::generate_clique_cuts( size_t extension_gain = 0; #endif for (std::vector& clique_local : ctx.cliques) { - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } #if DEBUG_CLIQUE_CUTS candidate_cliques++; #endif @@ -3841,7 +3883,7 @@ bool cut_generation_t::generate_clique_cuts( extension_gain += clique_vertices.size() - size_before_extension; #endif if (work_estimate > max_work_estimate) { return true; } - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } const clique_cut_build_status_t build_status = build_clique_cut(clique_vertices, num_vars, var_types, @@ -3907,7 +3949,7 @@ bool cut_generation_t::generate_zero_half_cuts( f_t start_time) { if (settings.zero_half_cuts == 0) { return true; } - if (toc(start_time) >= settings.time_limit) { return true; } + if (cut_generation_stopped(settings, start_time)) { return true; } const i_t num_vars = user_problem_.num_cols; ZERO_HALF_DEBUG( @@ -3991,7 +4033,7 @@ bool cut_generation_t::generate_zero_half_cuts( dijkstra_scratch_t dijkstra_scratch; for (i_t s = 0; s < num_local; ++s) { - if (toc(start_time) >= settings.time_limit) { break; } + if (cut_generation_stopped(settings, start_time)) { break; } if (work_estimate > max_work_estimate) { break; } if (already_used[s]) { continue; } ZERO_HALF_DEBUG("separation loop s=%lld / %lld", @@ -4117,10 +4159,12 @@ void cut_generation_t::generate_mir_cuts( std::vector scores; complemented_mir.compute_initial_scores_for_rows(lp, settings, Arow, xstar, ystar, scores); + if (cut_generation_stopped(settings, start_time)) { return; } // Push all the scores onto the priority queue std::priority_queue> score_queue; for (i_t i = 0; i < lp.num_rows; i++) { + if (cut_generation_stopped(settings, start_time)) { return; } score_queue.push(std::make_pair(scores[i], i)); } @@ -4134,11 +4178,12 @@ void cut_generation_t::generate_mir_cuts( // Transform the relaxation solution std::vector transformed_xstar; complemented_mir.bound_substitution(lp, variable_bounds, var_types, xstar, transformed_xstar); + if (cut_generation_stopped(settings, start_time)) { return; } f_t work_estimate = 0.0; i_t cuts_processed = 0; while (cuts_processed < max_cuts && !score_queue.empty()) { - if (toc(start_time) >= settings.time_limit) { break; } + if (cut_generation_stopped(settings, start_time)) { break; } // Get the row with the highest score from the queue auto [max_score, i] = score_queue.top(); score_queue.pop(); @@ -4398,7 +4443,7 @@ void cut_generation_t::generate_gomory_cuts( complemented_mir.bound_substitution(lp, variable_bounds, var_types, xstar, transformed_xstar); for (i_t i = 0; i < lp.num_rows; i++) { - if (toc(start_time) >= settings.time_limit) { break; } + if (cut_generation_stopped(settings, start_time)) { break; } inequality_t inequality(lp.num_cols); const i_t j = basic_list[i]; if (var_types[j] != variable_type_t::INTEGER) { continue; } @@ -5043,6 +5088,7 @@ void complemented_mixed_integer_rounding_cut_t::compute_initial_scores // Compute initial scores for all rows scores.resize(lp.num_rows, 0.0); for (i_t i = 0; i < lp.num_rows; i++) { + if (concurrent_cut_generation_halted(settings)) { break; } const i_t row_start = Arow.row_start[i]; const i_t row_end = Arow.row_start[i + 1]; diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 07db15a060..28cd908653 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -1016,6 +1017,53 @@ TEST(cuts, count_violated_cuts_deduplicates_and_rescores) EXPECT_EQ(cut_pool.count_violated_cuts({1.0, 0.0}), 0); } +TEST(cuts, cut_generation_honors_concurrent_halt) +{ + const raft::handle_t handle{}; + auto model = create_cuts_problem_2(); + auto op_problem = mps_data_model_to_optimization_problem(&handle, model); + mip::problem_t mip_problem(op_problem); + simplex::user_problem_t user_problem(op_problem.get_handle_ptr()); + mip_problem.get_host_user_problem(user_problem); + + simplex::simplex_solver_settings_t settings; + std::atomic concurrent_halt{1}; + settings.concurrent_halt = &concurrent_halt; + + simplex::lp_problem_t lp(&handle, 1, 1, 1); + std::vector new_slacks; + simplex::dualize_info_t dualize_info; + simplex::convert_user_problem(user_problem, settings, lp, new_slacks, dualize_info); + std::vector var_types = user_problem.var_types; + var_types.resize(lp.num_cols, simplex::variable_type_t::CONTINUOUS); + csr_matrix_t Arow(0, 0, 0); + lp.A.to_compressed_row(Arow); + + mip::probing_implied_bound_t probing_implied_bound(lp.num_cols); + mip::variable_bounds_t variable_bounds(lp, settings, var_types, Arow, new_slacks); + mip::cut_pool_t cut_pool(lp.num_cols, settings); + mip::cut_generation_t cut_generation( + cut_pool, lp, settings, Arow, new_slacks, var_types, user_problem, probing_implied_bound); + + const std::vector xstar(lp.num_cols, 0.0); + const std::vector ystar(lp.num_rows, 0.0); + const std::vector zstar(lp.num_cols, 0.0); + EXPECT_TRUE(cut_generation.generate_cuts(lp, + settings, + Arow, + new_slacks, + var_types, + std::nullopt, + xstar, + ystar, + zstar, + std::nullopt, + std::nullopt, + variable_bounds, + tic())); + EXPECT_EQ(cut_pool.pool_size(), 0); +} + TEST(cuts, clique_initial_builder_publishes_completion) { const raft::handle_t handle{}; From cea4732e4cd41b10b8127817950e79cd5e23a97d Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 13 Aug 2026 08:29:29 -0700 Subject: [PATCH 5/9] Run cut scoring serially --- cpp/src/cuts/cuts.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index a8be6e0151..d53d071340 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1402,14 +1402,10 @@ void cut_pool_t::score_cuts(std::vector& x_relax) cut_distances_.resize(cut_storage_.m, 0.0); cut_norms_.resize(cut_storage_.m, 0.0); - if (cut_storage_.m > 0) { - const i_t num_tasks = std::min(omp_get_num_threads(), cut_storage_.m); -#pragma omp taskloop num_tasks(num_tasks) default(shared) - for (i_t i = 0; i < cut_storage_.m; i++) { - f_t violation; - f_t cut_dist = cut_distance(i, x_relax, violation, cut_norms_[i]); - cut_distances_[i] = cut_dist <= min_cut_distance_ ? 0.0 : cut_dist; - } + for (i_t i = 0; i < cut_storage_.m; i++) { + f_t violation; + f_t cut_dist = cut_distance(i, x_relax, violation, cut_norms_[i]); + cut_distances_[i] = cut_dist <= min_cut_distance_ ? 0.0 : cut_dist; } std::vector sorted_indices; From 8a6fcc307dc8a2771b0c5a5869a8f3c3b53d0cb1 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Fri, 14 Aug 2026 23:13:57 -0700 Subject: [PATCH 6/9] Clear speculative cut pool after selection --- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 +++- cpp/src/cuts/cuts.cpp | 20 +++++++++++ cpp/src/cuts/cuts.hpp | 2 ++ cpp/tests/mip/cuts_test.cu | 34 +++++++++++++++++++ 4 files changed, 61 insertions(+), 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 f17bf5bbb3..92fccb1445 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3315,7 +3315,10 @@ auto branch_and_bound_t::do_cut_pass( if (mode == cut_pass_mode_t::APPLY_EXISTING_POOL) { settings_.log.printf("Applying %d speculative root cuts to the basis solution\n", num_cuts); } - if (num_cuts == 0) { return {cut_pass_action_t::BREAK, mip_status_t::UNSET}; } + if (num_cuts == 0) { + if (mode == cut_pass_mode_t::APPLY_EXISTING_POOL) { cut_pool.clear(); } + return {cut_pass_action_t::BREAK, mip_status_t::UNSET}; + } cut_info.record_cut_types(cut_types); #ifdef PRINT_CUT_POOL_TYPES cut_pool.print_cutpool_types(); @@ -3335,6 +3338,7 @@ auto branch_and_bound_t::do_cut_pass( #ifdef CHECK_CUTS_AGAINST_SAVED_SOLUTION verify_cuts_against_saved_solution(cuts_to_add, cut_rhs, saved_solution); #endif + if (mode == cut_pass_mode_t::APPLY_EXISTING_POOL) { cut_pool.clear(); } cut_pool_size = cut_pool.pool_size(); // Resolve the LP with the new cuts diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index d53d071340..5f3dced265 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1482,6 +1482,26 @@ void cut_pool_t::age_cuts() } } +template +void cut_pool_t::clear() +{ + cut_storage_.m = 0; + cut_storage_.nz_max = 0; + cut_storage_.row_start.resize(1); + cut_storage_.row_start[0] = 0; + cut_storage_.j.clear(); + cut_storage_.x.clear(); + rhs_storage_.clear(); + cut_age_.clear(); + cut_type_.clear(); + cut_distances_.clear(); + cut_norms_.clear(); + cut_orthogonality_.clear(); + cut_scores_.clear(); + best_cuts_.clear(); + scored_cuts_ = 0; +} + template void cut_pool_t::drop_cuts() { diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 3863adc6b4..c57c7721dc 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -334,6 +334,8 @@ class cut_pool_t { void age_cuts(); + void clear(); + void drop_cuts(); i_t pool_size() const { return cut_storage_.m; } diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 28cd908653..af06aea8dd 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -1017,6 +1017,40 @@ TEST(cuts, count_violated_cuts_deduplicates_and_rescores) EXPECT_EQ(cut_pool.count_violated_cuts({1.0, 0.0}), 0); } +TEST(cuts, clear_cut_pool_resets_state_for_new_cuts) +{ + simplex::simplex_solver_settings_t settings; + mip::cut_pool_t cut_pool(2, settings); + + mip::inequality_t first_cut; + first_cut.push_back(0, 1.0); + first_cut.rhs = 1.0; + cut_pool.add_cut(mip::cut_type_t::KNAPSACK, first_cut); + std::vector relaxation_solution{0.0, 0.0}; + cut_pool.score_cuts(relaxation_solution); + + csr_matrix_t selected_cuts(0, 2, 0); + std::vector selected_rhs; + std::vector selected_types; + EXPECT_EQ(cut_pool.get_best_cuts(selected_cuts, selected_rhs, selected_types), 1); + + cut_pool.clear(); + EXPECT_EQ(cut_pool.pool_size(), 0); + ASSERT_EQ(selected_cuts.m, 1); + ASSERT_EQ(selected_rhs.size(), 1); + ASSERT_EQ(selected_types.size(), 1); + EXPECT_EQ(selected_types[0], mip::cut_type_t::KNAPSACK); + + mip::inequality_t second_cut; + second_cut.push_back(1, 1.0); + second_cut.rhs = 1.0; + cut_pool.add_cut(mip::cut_type_t::FLOW_COVER, second_cut); + cut_pool.score_cuts(relaxation_solution); + EXPECT_EQ(cut_pool.get_best_cuts(selected_cuts, selected_rhs, selected_types), 1); + ASSERT_EQ(selected_types.size(), 1); + EXPECT_EQ(selected_types[0], mip::cut_type_t::FLOW_COVER); +} + TEST(cuts, cut_generation_honors_concurrent_halt) { const raft::handle_t handle{}; From 980780dc2530ad2d6f720ba58b1a48cfc8bffe85 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 17 Aug 2026 09:39:39 -0700 Subject: [PATCH 7/9] Avoid duplicate speculative cut scan --- cpp/src/branch_and_bound/branch_and_bound.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 92fccb1445..39183f7034 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3215,15 +3215,11 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( !relaxation_cut_task_complete.load(std::memory_order_acquire); #pragma omp taskwait depend(in : relaxation_cut_task_status) const i_t generated_cuts = cut_pool.pool_size(); - const i_t retained_cuts = - generated_cuts == 0 ? 0 : cut_pool.count_violated_cuts(root_relax_soln.x); settings_.log.printf( - "%s root cut pass generated %d candidates in %.2f seconds; %d remain violated after the " - "basis solve%s%s\n", + "%s speculative root cut pass generated %d candidates in %.2f seconds%s%s\n", method_to_string(relaxation_cut_method).c_str(), generated_cuts, relaxation_cut_task_elapsed, - retained_cuts, relaxation_cut_task_interrupted ? " (stopped when the basis became available)" : "", relaxation_cut_task_status < 0 ? " (separator reported infeasibility)" : ""); } @@ -3313,7 +3309,8 @@ auto branch_and_bound_t::do_cut_pass( std::vector cut_types; i_t num_cuts = cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types); if (mode == cut_pass_mode_t::APPLY_EXISTING_POOL) { - settings_.log.printf("Applying %d speculative root cuts to the basis solution\n", num_cuts); + settings_.log.printf("Retained and applying %d speculative root cuts to the basis solution\n", + num_cuts); } if (num_cuts == 0) { if (mode == cut_pass_mode_t::APPLY_EXISTING_POOL) { cut_pool.clear(); } From ae2e26bd71256108d503ad9656bd209faff50845 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 20 Aug 2026 16:36:08 -0700 Subject: [PATCH 8/9] Replace first root cut pass with speculative cuts Poll the concurrent halt signal inside long MIR aggregation and heuristic loops so speculative generation yields promptly when a basis becomes available. Signed-off-by: Hugo Linsenmaier --- cpp/src/branch_and_bound/branch_and_bound.cpp | 8 +++-- cpp/src/cuts/cuts.cpp | 33 +++++++++++++++++-- cpp/src/cuts/cuts.hpp | 3 +- cpp/tests/mip/cuts_test.cu | 9 +++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 39183f7034..be17138c17 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3792,10 +3792,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; + i_t first_normal_cut_pass = 0; // Pass 0 consumes cuts completed from the PDLP/Barrier relaxation while the winning basis was // being built. Score them against that basis solution and reoptimize before generating any - // basis-aware cuts. This deliberately does not consume one of max_cut_passes. + // basis-aware cuts. If cuts are applied, this replaces normal cut pass 0. if (cut_pool.pool_size() > 0) { cut_pass_result_t speculative_cut_result = do_cut_pass(-1, solution, @@ -3824,9 +3825,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #pragma omp taskwait depend(in : *clique_signal) return speculative_cut_result.status; } + if (speculative_cut_result.action == cut_pass_action_t::CONTINUE) { + first_normal_cut_pass = 1; + } } - for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { + for (i_t cut_pass = first_normal_cut_pass; cut_pass < settings_.max_cut_passes; cut_pass++) { if (toc(exploration_stats_.start_time) >= settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 5f3dced265..eab6fbd7a0 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -4292,16 +4292,24 @@ void cut_generation_t::generate_mir_cuts( work_estimate += lp.num_cols; while (!add_cut && num_aggregated < max_aggregated) { + if (cut_generation_stopped(settings, start_time)) { return; } + inequality_t transformed_inequality; inequality.squeeze(transformed_inequality); work_estimate += transformed_inequality.size(); complemented_mir.transform_inequality(variable_bounds, var_types, transformed_inequality); work_estimate += transformed_inequality.size(); + if (cut_generation_stopped(settings, start_time)) { return; } inequality_t cut; - bool cut_found = complemented_mir.cut_generation_heuristic( - transformed_inequality, var_types, transformed_xstar, cut, work_estimate); + bool cut_found = complemented_mir.cut_generation_heuristic(transformed_inequality, + var_types, + transformed_xstar, + cut, + work_estimate, + settings.concurrent_halt); + if (cut_generation_stopped(settings, start_time)) { return; } // Note cut is in the transformed variables if (cut_found) { @@ -4326,6 +4334,7 @@ void cut_generation_t::generate_mir_cuts( f_t max_off_bound = 0.0; i_t max_off_bound_var = -1; for (i_t p = 0; p < inequality.size(); p++) { + if ((p & 1023) == 0 && concurrent_cut_generation_halted(settings)) { return; } const i_t j = inequality.index(p); const f_t aj = inequality.coeff(p); if (aj == 0.0) { continue; } @@ -4360,6 +4369,9 @@ void cut_generation_t::generate_mir_cuts( const f_t threshold = 1e-4; for (i_t q = col_start; q < col_end; q++) { + if (((q - col_start) & 1023) == 0 && concurrent_cut_generation_halted(settings)) { + return; + } const i_t i = lp.A.i[q]; const f_t val = lp.A.x[q]; // Can't use rows that have already been aggregated @@ -4370,6 +4382,7 @@ void cut_generation_t::generate_mir_cuts( bool did_aggregate = false; while (!potential_rows.empty()) { + if (cut_generation_stopped(settings, start_time)) { return; } const i_t pivot_row = *std::max_element(potential_rows.begin(), potential_rows.end(), [&](i_t a, i_t b) { return scores[a] < scores[b]; @@ -4382,6 +4395,7 @@ void cut_generation_t::generate_mir_cuts( inequality_t saved_inequality = inequality; f_t multiplier = complemented_mir.combine_rows( lp, Arow, max_off_bound_var, pivot_row_inequality, inequality); + if (cut_generation_stopped(settings, start_time)) { return; } if (max_abs_multiplier / std::abs(multiplier) > 10000 || std::abs(multiplier) / min_abs_multiplier > 10000) { inequality = saved_inequality; @@ -5148,8 +5162,14 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist const std::vector& var_types, const std::vector& transformed_xstar, inequality_t& transformed_cut, - f_t& work_estimate) + f_t& work_estimate, + const std::atomic* concurrent_halt) { + const auto halted = [concurrent_halt]() { + return concurrent_halt != nullptr && + concurrent_halt->load(std::memory_order_acquire) != 0; + }; + std::vector deltas_to_try; deltas_to_try.reserve(transformed_inequality.size()); deltas_to_try.push_back(1.0); @@ -5157,6 +5177,7 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist i_t num_integers = 0; f_t max_coeff = 0.0; for (i_t k = 0; k < transformed_inequality.size(); k++) { + if ((k & 1023) == 0 && halted()) { return false; } const i_t j = transformed_inequality.index(k); const f_t abs_aj = std::abs(transformed_inequality.coeff(k)); if (var_types[j] == variable_type_t::INTEGER) { @@ -5182,6 +5203,7 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist std::vector integer_indices; integer_indices.reserve(num_integers); for (i_t k = 0; k < transformed_inequality.size(); k++) { + if ((k & 1023) == 0 && halted()) { return false; } const i_t j = transformed_inequality.index(k); if (var_types[j] == variable_type_t::INTEGER && new_upper(j) < inf) { const f_t x_j = transformed_xstar[j]; @@ -5209,6 +5231,7 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist // First try without any complementation for (const f_t tmp_delta : deltas_to_try) { + if (halted()) { return false; } bool cut_ok = scale_uncomplement_and_generate_cut(var_types, transformed_xstar, complemented_indices, @@ -5230,6 +5253,7 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist if (!cut_found) { // Complement an integer variable for (const i_t idx : perm) { + if (halted()) { return false; } const i_t l = integer_indices[idx]; const i_t j = complemented_inequality.index(l); // We have an integer variable x_j <= b_j @@ -5250,6 +5274,7 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist complemented_indices.push_back(l); for (const f_t tmp_delta : deltas_to_try) { + if (halted()) { return false; } bool cut_ok = scale_uncomplement_and_generate_cut(var_types, transformed_xstar, complemented_indices, @@ -5276,6 +5301,7 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist // We have found a cut. Now try to improve the violation by scaling the cut by 1/2, 1/4, 1/8, etc. std::vector scaled_deltas_to_try = {delta / 2.0, delta / 4.0, delta / 8.0}; for (const f_t tmp_delta : scaled_deltas_to_try) { + if (halted()) { return false; } inequality_t tmp_cut_delta; bool cut_ok = scale_uncomplement_and_generate_cut(var_types, transformed_xstar, @@ -5304,6 +5330,7 @@ bool complemented_mixed_integer_rounding_cut_t::cut_generation_heurist work_estimate += 4 * transformed_inequality.size(); complemented_indices.clear(); for (const i_t idx : perm) { + if (halted()) { return false; } const i_t l = integer_indices[idx]; const i_t j = complemented_inequality.index(l); // We have an integer variable x_j <= b_j diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index c57c7721dc..bb28a00851 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -1032,7 +1032,8 @@ class complemented_mixed_integer_rounding_cut_t { const std::vector& var_types, const std::vector& transformed_xstar, inequality_t& transformed_cut, - f_t& work_estimate); + f_t& work_estimate, + const std::atomic* concurrent_halt = nullptr); bool scale_uncomplement_and_generate_cut(const std::vector& var_types, const std::vector& transformed_xstar, diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index af06aea8dd..129eb9e51c 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -1096,6 +1096,15 @@ TEST(cuts, cut_generation_honors_concurrent_halt) variable_bounds, tic())); EXPECT_EQ(cut_pool.pool_size(), 0); + + mip::complemented_mixed_integer_rounding_cut_t complemented_mir( + lp, settings, new_slacks); + mip::inequality_t transformed_inequality; + transformed_inequality.push_back(0, 1.0); + mip::inequality_t transformed_cut; + double work_estimate = 0.0; + EXPECT_FALSE(complemented_mir.cut_generation_heuristic( + transformed_inequality, var_types, xstar, transformed_cut, work_estimate, &concurrent_halt)); } TEST(cuts, clique_initial_builder_publishes_completion) From 15d0087f702396d0cc2edb7aef57c76a81575b6f Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 27 Aug 2026 22:16:54 -0700 Subject: [PATCH 9/9] Remove speculative cut tests Signed-off-by: Hugo Linsenmaier --- cpp/tests/mip/cuts_test.cu | 147 ------------------------------------- 1 file changed, 147 deletions(-) diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 129eb9e51c..5af0754e3d 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -984,152 +983,6 @@ TEST(cuts, test_duplicate_cuts_detection) cut_pool.check_for_duplicate_cuts(); } -TEST(cuts, count_violated_cuts_deduplicates_and_rescores) -{ - simplex::simplex_solver_settings_t settings; - mip::cut_pool_t cut_pool(2, settings); - - mip::inequality_t cut; - cut.push_back(0, 1.0); - cut.rhs = 1.0; - cut_pool.add_cut(mip::cut_type_t::KNAPSACK, cut); - - mip::inequality_t duplicate; - duplicate.push_back(0, 2.0); - duplicate.rhs = 2.0; - cut_pool.add_cut(mip::cut_type_t::KNAPSACK, duplicate); - - mip::inequality_t satisfied; - satisfied.push_back(1, 1.0); - satisfied.rhs = 0.0; - cut_pool.add_cut(mip::cut_type_t::FLOW_COVER, satisfied); - - int violated_cuts = 0; -#pragma omp parallel num_threads(4) shared(cut_pool, violated_cuts) - { -#pragma omp single - { - violated_cuts = cut_pool.count_violated_cuts({0.0, 0.0}); - } - } - EXPECT_EQ(violated_cuts, 1); - EXPECT_EQ(cut_pool.pool_size(), 2); - EXPECT_EQ(cut_pool.count_violated_cuts({1.0, 0.0}), 0); -} - -TEST(cuts, clear_cut_pool_resets_state_for_new_cuts) -{ - simplex::simplex_solver_settings_t settings; - mip::cut_pool_t cut_pool(2, settings); - - mip::inequality_t first_cut; - first_cut.push_back(0, 1.0); - first_cut.rhs = 1.0; - cut_pool.add_cut(mip::cut_type_t::KNAPSACK, first_cut); - std::vector relaxation_solution{0.0, 0.0}; - cut_pool.score_cuts(relaxation_solution); - - csr_matrix_t selected_cuts(0, 2, 0); - std::vector selected_rhs; - std::vector selected_types; - EXPECT_EQ(cut_pool.get_best_cuts(selected_cuts, selected_rhs, selected_types), 1); - - cut_pool.clear(); - EXPECT_EQ(cut_pool.pool_size(), 0); - ASSERT_EQ(selected_cuts.m, 1); - ASSERT_EQ(selected_rhs.size(), 1); - ASSERT_EQ(selected_types.size(), 1); - EXPECT_EQ(selected_types[0], mip::cut_type_t::KNAPSACK); - - mip::inequality_t second_cut; - second_cut.push_back(1, 1.0); - second_cut.rhs = 1.0; - cut_pool.add_cut(mip::cut_type_t::FLOW_COVER, second_cut); - cut_pool.score_cuts(relaxation_solution); - EXPECT_EQ(cut_pool.get_best_cuts(selected_cuts, selected_rhs, selected_types), 1); - ASSERT_EQ(selected_types.size(), 1); - EXPECT_EQ(selected_types[0], mip::cut_type_t::FLOW_COVER); -} - -TEST(cuts, cut_generation_honors_concurrent_halt) -{ - const raft::handle_t handle{}; - auto model = create_cuts_problem_2(); - auto op_problem = mps_data_model_to_optimization_problem(&handle, model); - mip::problem_t mip_problem(op_problem); - simplex::user_problem_t user_problem(op_problem.get_handle_ptr()); - mip_problem.get_host_user_problem(user_problem); - - simplex::simplex_solver_settings_t settings; - std::atomic concurrent_halt{1}; - settings.concurrent_halt = &concurrent_halt; - - simplex::lp_problem_t lp(&handle, 1, 1, 1); - std::vector new_slacks; - simplex::dualize_info_t dualize_info; - simplex::convert_user_problem(user_problem, settings, lp, new_slacks, dualize_info); - std::vector var_types = user_problem.var_types; - var_types.resize(lp.num_cols, simplex::variable_type_t::CONTINUOUS); - csr_matrix_t Arow(0, 0, 0); - lp.A.to_compressed_row(Arow); - - mip::probing_implied_bound_t probing_implied_bound(lp.num_cols); - mip::variable_bounds_t variable_bounds(lp, settings, var_types, Arow, new_slacks); - mip::cut_pool_t cut_pool(lp.num_cols, settings); - mip::cut_generation_t cut_generation( - cut_pool, lp, settings, Arow, new_slacks, var_types, user_problem, probing_implied_bound); - - const std::vector xstar(lp.num_cols, 0.0); - const std::vector ystar(lp.num_rows, 0.0); - const std::vector zstar(lp.num_cols, 0.0); - EXPECT_TRUE(cut_generation.generate_cuts(lp, - settings, - Arow, - new_slacks, - var_types, - std::nullopt, - xstar, - ystar, - zstar, - std::nullopt, - std::nullopt, - variable_bounds, - tic())); - EXPECT_EQ(cut_pool.pool_size(), 0); - - mip::complemented_mixed_integer_rounding_cut_t complemented_mir( - lp, settings, new_slacks); - mip::inequality_t transformed_inequality; - transformed_inequality.push_back(0, 1.0); - mip::inequality_t transformed_cut; - double work_estimate = 0.0; - EXPECT_FALSE(complemented_mir.cut_generation_heuristic( - transformed_inequality, var_types, xstar, transformed_cut, work_estimate, &concurrent_halt)); -} - -TEST(cuts, clique_initial_builder_publishes_completion) -{ - const raft::handle_t handle{}; - auto model = create_pairwise_triangle_set_packing_problem(); - auto op_problem = mps_data_model_to_optimization_problem(&handle, model); - mip::problem_t mip_problem(op_problem); - simplex::user_problem_t host_problem(op_problem.get_handle_ptr()); - mip_problem.get_host_user_problem(host_problem); - - mip_solver_settings_t settings; - cuopt::timer_t timer(std::numeric_limits::infinity()); - std::shared_ptr> clique_table; - omp_atomic_t stop_requested{false}; - omp_atomic_t complete{false}; - - mip::find_initial_cliques( - host_problem, settings.tolerances, clique_table, timer, &stop_requested, &complete); - - EXPECT_TRUE(complete.load(std::memory_order_acquire)); - ASSERT_NE(clique_table, nullptr); - EXPECT_EQ(clique_table->n_variables, host_problem.num_cols); -} - TEST(cuts, clique_phase1_smoke_conflict_graph_edges) { const raft::handle_t handle{};