diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e2ea33bb6f..6fe3399e8f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -290,6 +290,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(); @@ -3033,9 +3034,20 @@ 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; + 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}; + 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) @@ -3085,6 +3097,57 @@ 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_; + 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); + 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_cuts(original_lp_, + cut_settings, + Arow_, + new_slacks_, + var_types_, + 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); + relaxation_cut_task_status = feasible ? 1 : -1; + relaxation_cut_task_complete.store(true, std::memory_order_release); + } + } + // Call crossover on the crushed solution auto root_crossover_settings = settings_; root_crossover_settings.log.log = false; @@ -3098,6 +3161,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) @@ -3157,16 +3221,32 @@ 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(); + settings_.log.printf( + "%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, + relaxation_cut_task_interrupted ? " (stopped when the basis became available)" : "", + relaxation_cut_task_status < 0 ? " (separator reported infeasibility)" : ""); + } + is_root_solution_set = true; return root_status; @@ -3191,7 +3271,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_action_t + [[maybe_unused]] const std::vector& saved_solution, + cut_pass_mode_t mode) -> cut_pass_action_t { #ifdef PRINT_FRACTIONAL_INFO settings_.log.printf("Found %d fractional variables on cut pass %d\n", num_fractional, cut_pass); @@ -3204,37 +3285,43 @@ 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_, - basis_update, - 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) { - 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(); + } + solver_status_ = mip_status_t::INFEASIBLE; + return cut_pass_action_t::RETURN; + } + if (toc(exploration_stats_.start_time) >= settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return cut_pass_action_t::RETURN; + } + 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); } - - solver_status_ = mip_status_t::INFEASIBLE; - return cut_pass_action_t::RETURN; } if (toc(exploration_stats_.start_time) >= settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); return cut_pass_action_t::RETURN; } - 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); @@ -3245,7 +3332,14 @@ auto branch_and_bound_t::do_cut_pass( std::vector cut_rhs; std::vector cut_types; i_t num_cuts = cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types); - if (num_cuts == 0) { return cut_pass_action_t::BREAK; } + if (mode == cut_pass_mode_t::APPLY_EXISTING_POOL) { + 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(); } + return cut_pass_action_t::BREAK; + } cut_info.record_cut_types(cut_types); #ifdef PRINT_CUT_POOL_TYPES cut_pool.print_cutpool_types(); @@ -3266,6 +3360,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 @@ -3486,7 +3581,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", @@ -3494,6 +3589,8 @@ auto branch_and_bound_t::do_cut_pass( root_relax_objective); return cut_pass_action_t::BREAK; } + // 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; } @@ -3518,6 +3615,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"); @@ -3547,6 +3645,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; @@ -3560,8 +3659,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_); } } @@ -3609,7 +3712,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"); @@ -3710,7 +3815,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_, @@ -3719,8 +3823,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 @@ -3746,8 +3851,42 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t cut_pool_size = 0; lp_settings.concurrent_halt = settings_.concurrent_halt; lp_settings.inside_mip = 2; + 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. If cuts are applied, this replaces normal cut pass 0. + if (cut_pool.pool_size() > 0) { + cut_pass_action_t speculative_cut_action = 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_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); + } + assert(solver_status_ != mip_status_t::UNSET); + return solver_status_; + } + if (speculative_cut_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/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 17ebdafa33..1ac010d41f 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, @@ -195,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"}; @@ -310,6 +313,7 @@ class branch_and_bound_t { } enum class cut_pass_action_t { CONTINUE, BREAK, RETURN }; + enum class cut_pass_mode_t { GENERATE_AND_APPLY, APPLY_EXISTING_POOL }; cut_pass_action_t do_cut_pass(i_t cut_pass, simplex::mip_solution_t& solution, @@ -328,7 +332,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 7acd7dee0a..eab6fbd7a0 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -46,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 @@ -212,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}; @@ -230,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; } @@ -276,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) { @@ -355,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); @@ -1042,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); @@ -1356,6 +1378,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,19 +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); - 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]); - } } std::vector sorted_indices; @@ -1452,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() { @@ -1582,6 +1632,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; } @@ -1656,6 +1707,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; @@ -2983,6 +3035,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); @@ -3056,7 +3111,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); @@ -3112,7 +3169,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]; @@ -3123,6 +3182,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]; @@ -3171,6 +3231,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]; @@ -3251,7 +3312,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 @@ -3264,6 +3325,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 = @@ -3293,6 +3355,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]; @@ -3321,7 +3387,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; } @@ -3339,7 +3405,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; } @@ -3382,6 +3448,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]; @@ -3448,7 +3518,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; } @@ -3516,33 +3586,41 @@ 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, + 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) { + 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 (toc(start_time) >= settings.time_limit) { return true; } + if (has_basis && + (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0)) { + if (cut_generation_stopped(settings, start_time)) { return true; } f_t cut_start_time = tic(); generate_gomory_cuts(lp, settings, 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) { @@ -3552,7 +3630,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, // 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); @@ -3563,7 +3641,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, // 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); @@ -3574,7 +3652,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, // 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); @@ -3586,7 +3664,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, // 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); @@ -3601,12 +3679,12 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, // 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) { @@ -3621,7 +3699,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, // 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( @@ -3655,7 +3733,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); @@ -3676,7 +3754,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); @@ -3695,7 +3773,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", @@ -3740,6 +3818,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)); @@ -3759,7 +3838,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); @@ -3779,7 +3859,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); @@ -3791,7 +3871,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 @@ -3819,7 +3899,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, @@ -3885,7 +3965,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( @@ -3969,7 +4049,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", @@ -4095,10 +4175,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)); } @@ -4112,11 +4194,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(); @@ -4209,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) { @@ -4243,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; } @@ -4277,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 @@ -4287,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]; @@ -4299,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; @@ -4376,7 +4473,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; } @@ -5021,6 +5118,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]; @@ -5064,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); @@ -5073,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) { @@ -5098,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]; @@ -5125,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, @@ -5146,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 @@ -5166,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, @@ -5192,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, @@ -5220,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 fcb6080178..bb28a00851 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 @@ -321,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 @@ -330,6 +334,8 @@ class cut_pool_t { void age_cuts(); + void clear(); + void drop_cuts(); i_t pool_size() const { return cut_storage_.m; } @@ -657,39 +663,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_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, + 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); private: // Generate all mixed integer gomory cuts @@ -772,6 +783,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_; }; @@ -1019,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/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,