From fb5af3a42c8ed8c3beb65ea5fc2b27804183eac0 Mon Sep 17 00:00:00 2001 From: Lukas Innig Date: Mon, 24 Aug 2026 16:56:01 +0100 Subject: [PATCH] Fix concurrent clique table publication Signed-off-by: Lukas Innig --- cpp/src/branch_and_bound/branch_and_bound.cpp | 10 +++++-- .../presolve/conflict_graph/clique_table.cu | 28 +++++++------------ .../presolve/conflict_graph/clique_table.cuh | 12 ++++---- cpp/tests/mip/miplib_test.cu | 22 +++++++++++++++ 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index fd7987ae1c..e0a5dd405e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3268,6 +3268,12 @@ 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); + mip::clique_config_t clique_config; + clique_table_ = + std::make_shared>(2 * original_problem_.num_cols, + clique_config.min_clique_size, + clique_config.max_clique_size_for_extension); + auto* initial_clique_table = clique_table_.get(); 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; @@ -3277,12 +3283,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut tolerances_for_clique.relative_mip_gap = settings_.relative_mip_gap_tol; #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) depend(out : *clique_signal) \ - firstprivate(tolerances_for_clique) + firstprivate(tolerances_for_clique, initial_clique_table) { 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); + problem_copy, tolerances_for_clique, *initial_clique_table, timer, clique_signal); } } 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..850637c2e6 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -670,7 +670,7 @@ void print_clique_table(const clique_table_t& clique_table) template void find_initial_cliques(user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, - std::shared_ptr>& clique_table_out, + clique_table_t& clique_table, cuopt::timer_t& timer, omp_atomic_t* signal_extend) { @@ -701,48 +701,40 @@ void find_initial_cliques(user_problem_t& problem, t_sort = stage_timer.elapsed_time(); #endif clique_config_t clique_config; - auto clique_table = - std::make_shared>(2 * problem.num_cols, - clique_config.min_clique_size, - clique_config.max_clique_size_for_extension); - clique_table->tolerances = tolerances; + clique_table.tolerances = tolerances; double time_limit_for_additional_cliques = timer.remaining_time() / 2; cuopt::timer_t additional_cliques_timer(time_limit_for_additional_cliques); double find_work_estimate = 0.0; // Always build base cliques in full; signal_extend only gates the extension phase. for (const auto& knapsack_constraint : knapsack_constraints) { if (timer.check_time_limit()) { break; } - find_cliques_from_constraint(knapsack_constraint, *clique_table, additional_cliques_timer); + find_cliques_from_constraint(knapsack_constraint, clique_table, additional_cliques_timer); find_work_estimate += knapsack_constraint.entries.size(); } #ifdef DEBUG_CLIQUE_TABLE t_find = stage_timer.elapsed_time(); #endif CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d, find_work=%.0f", - clique_table->first.size(), - clique_table->addtl_cliques.size(), + clique_table.first.size(), + clique_table.addtl_cliques.size(), find_work_estimate); - remove_small_cliques(*clique_table, timer); + remove_small_cliques(clique_table, timer); #ifdef DEBUG_CLIQUE_TABLE t_small = stage_timer.elapsed_time(); #endif - fill_var_clique_maps(*clique_table); + fill_var_clique_maps(clique_table); #ifdef DEBUG_CLIQUE_TABLE t_maps = stage_timer.elapsed_time(); #endif - // Publish the base table so cut generation can start using it; the extension - // phase below keeps mutating *clique_table, so the consumer must signal this - // task to stop and join it (taskwait) before reading the table. - clique_table_out = clique_table; double extend_work = 0.0; i_t n_extended_cliques = extend_cliques(knapsack_constraints, - *clique_table, + clique_table, timer, &extend_work, clique_config.min_extend_work, clique_config.max_extend_work, signal_extend); - if (n_extended_cliques > 0) { fill_var_clique_maps(*clique_table); } + if (n_extended_cliques > 0) { fill_var_clique_maps(clique_table); } #ifdef DEBUG_CLIQUE_TABLE t_extend = stage_timer.elapsed_time(); CUOPT_LOG_DEBUG( @@ -765,7 +757,7 @@ void find_initial_cliques(user_problem_t& problem, template void find_initial_cliques( \ user_problem_t & problem, \ typename mip_solver_settings_t::tolerances_t tolerances, \ - std::shared_ptr> & clique_table_out, \ + clique_table_t & clique_table, \ cuopt::timer_t & timer, \ omp_atomic_t * signal_extend); \ template void build_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..f1c1082aaf 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh @@ -201,16 +201,14 @@ struct clique_table_t { typename mip_solver_settings_t::tolerances_t tolerances; }; -// 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. +// Builds the conflict-graph clique table for `problem`. 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 table after its base cliques are built. template void find_initial_cliques(simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, - std::shared_ptr>& clique_table_out, + clique_table_t& clique_table, cuopt::timer_t& timer, omp_atomic_t* signal_extend = nullptr); diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index 924763c437..d23de41569 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -90,6 +90,28 @@ TEST(mip_solve, low_thread_count_test) test_variable_bounds(problem, solution.get_solution(), settings); } +TEST(mip_solve, repeated_concurrent_initial_clique_generation) +{ + mip_solver_settings_t settings; + settings.num_cpu_threads = 8; + settings.time_limit = 30; + settings.work_limit = 1; + settings.clique_cuts = 1; + settings.zero_half_cuts = 1; + + const raft::handle_t handle_{}; + + auto path = make_path_absolute("mip/gen-ip054.mps"); + auto problem = cuopt::mathematical_optimization::io::read_mps(path, false); + handle_.sync_stream(); + + constexpr int num_solves = 25; + for (int solve = 0; solve < num_solves; ++solve) { + auto solution = solve_mip(&handle_, problem, settings); + EXPECT_NE(solution.get_termination_status(), mip_termination_status_t::NoTermination); + } +} + // Verify --node-limit is respected: swath1 normally requires several thousand B&B // nodes to prove optimality, so capping at 1000 forces the solver to stop early with // a feasible (but not necessarily optimal) solution.