From a0ce98760fb1e3886f97beae865134a84bfe43d8 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 1 Apr 2026 03:19:13 -0700 Subject: [PATCH 01/25] initial commit --- cpp/src/branch_and_bound/branch_and_bound.cpp | 42 ++++++++++++++---- cpp/src/branch_and_bound/branch_and_bound.hpp | 44 +++++++++++-------- .../mip_heuristics/diversity/population.cuh | 4 +- cpp/src/mip_heuristics/solve.cu | 21 ++++++--- cpp/src/mip_heuristics/solver.cu | 5 ++- cpp/src/mip_heuristics/solver_context.cuh | 6 ++- 6 files changed, 86 insertions(+), 36 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c9b4568399..a045678dd3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -250,6 +250,7 @@ branch_and_bound_t::branch_and_bound_t( original_lp_(user_problem.handle_ptr, 1, 1, 1), Arow_(1, 1, 0), incumbent_(1), + external_incumbent_(1), root_relax_soln_(1, 1), root_crossover_soln_(1, 1), pc_(1), @@ -307,6 +308,24 @@ f_t branch_and_bound_t::get_lower_bound() } } +template +void branch_and_bound_t::set_initial_cutoff(f_t bound) +{ + std::lock_guard lock(mutex_upper_); + if (bound < upper_bound_) { upper_bound_ = bound; } +} + +template +void branch_and_bound_t::set_external_incumbent(f_t objective, + const std::vector& solution) +{ + std::lock_guard lock(mutex_upper_); + if (objective < upper_bound_) { upper_bound_ = objective; } + if (!external_incumbent_.has_incumbent || objective <= external_incumbent_.objective) { + external_incumbent_.set_incumbent_solution(objective, solution); + } +} + template void branch_and_bound_t::report_heuristic(f_t obj) { @@ -673,6 +692,7 @@ void branch_and_bound_t::set_solution_at_root(mip_solution_t // We should be done here uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, solution.x); solution.objective = incumbent_.objective; + solution.has_incumbent = true; solution.lower_bound = root_objective_; solution.nodes_explored = 0; solution.simplex_iterations = root_relax_soln_.iterations; @@ -734,7 +754,9 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& if (gap <= settings_.absolute_mip_gap_tol || gap_rel <= settings_.relative_mip_gap_tol) { solver_status_ = mip_status_t::OPTIMAL; #ifdef CHECK_CUTS_AGAINST_SAVED_SOLUTION - if (settings_.sub_mip == 0) { write_solution_for_cut_verification(original_lp_, incumbent_.x); } + if (settings_.sub_mip == 0 && has_solver_space_incumbent()) { + write_solution_for_cut_verification(original_lp_, incumbent_.x); + } #endif if (gap > 0 && gap <= settings_.absolute_mip_gap_tol) { settings_.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", @@ -752,7 +774,7 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& if (solver_status_ == mip_status_t::UNSET) { if (exploration_stats_.nodes_explored > 0 && exploration_stats_.nodes_unexplored == 0 && - upper_bound_ == inf) { + !has_any_incumbent()) { settings_.log.printf("Integer infeasible.\n"); solver_status_ = mip_status_t::INFEASIBLE; if (settings_.heuristic_preemption_callback != nullptr) { @@ -761,11 +783,15 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& } } - if (upper_bound_ != inf) { - assert(incumbent_.has_incumbent); + if (has_solver_space_incumbent()) { uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, solution.x); + solution.objective = incumbent_.objective; + solution.has_incumbent = true; + } else if (has_external_incumbent()) { + solution.x = external_incumbent_.x; + solution.objective = external_incumbent_.objective; + solution.has_incumbent = true; } - solution.objective = incumbent_.objective; solution.lower_bound = lower_bound; solution.nodes_explored = exploration_stats_.nodes_explored; solution.simplex_iterations = exploration_stats_.total_lp_iters; @@ -1610,7 +1636,7 @@ void branch_and_bound_t::run_scheduler() diving_heuristics_settings_t diving_settings = settings_.diving_settings; const i_t num_workers = 2 * settings_.num_threads; - if (!std::isfinite(upper_bound_)) { diving_settings.guided_diving = false; } + if (!has_solver_space_incumbent()) { diving_settings.guided_diving = false; } std::vector strategies = get_search_strategies(diving_settings); std::array max_num_workers_per_type = get_max_workers(num_workers, strategies); @@ -1646,7 +1672,7 @@ void branch_and_bound_t::run_scheduler() // If the guided diving was disabled previously due to the lack of an incumbent solution, // re-enable as soon as a new incumbent is found. if (settings_.diving_settings.guided_diving != diving_settings.guided_diving) { - if (std::isfinite(upper_bound_)) { + if (has_solver_space_incumbent()) { diving_settings.guided_diving = settings_.diving_settings.guided_diving; strategies = get_search_strategies(diving_settings); max_num_workers_per_type = get_max_workers(num_workers, strategies); @@ -3535,7 +3561,7 @@ f_t branch_and_bound_t::deterministic_compute_lower_bound() } // Tree is exhausted - if (lower_bound == std::numeric_limits::infinity() && incumbent_.has_incumbent) { + if (lower_bound == std::numeric_limits::infinity() && has_any_incumbent()) { lower_bound = upper_bound_.load(); } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 765f6095b6..59cf0a372a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -115,19 +115,23 @@ class branch_and_bound_t { void set_concurrent_lp_root_solve(bool enable) { enable_concurrent_lp_root_solve_ = enable; } - // Set a cutoff bound from an external source (e.g., early FJ during presolve). - // Used for node pruning and reduced cost strengthening but NOT for gap computation. - // Unlike upper_bound_, this does not imply a verified incumbent solution exists. - // - // IMPORTANT: `bound` must be in B&B's internal objective space, i.e. the space of - // original_lp_ where: user_obj = obj_scale * (internal_obj + obj_constant). - // The caller (solver.cu) converts from user-space via - // problem_ptr->get_solver_obj_from_user_obj(user_cutoff) - // which accounts for both the presolve objective offset and maximization. - void set_initial_cutoff(f_t bound) { initial_cutoff_ = bound; } - - // Effective cutoff for node pruning: min of verified incumbent and external cutoff. - f_t get_cutoff() const { return std::min(upper_bound_.load(), initial_cutoff_); } + // Seed the global pruning upper bound from an external source (e.g., early FJ during presolve). + // `bound` must be in B&B's internal objective space. + void set_initial_cutoff(f_t bound); + + // Store an incumbent that exists outside of B&B's solver space. The objective must match + // B&B's internal objective space while `solution` lives in the caller's original output space. + void set_external_incumbent(f_t objective, const std::vector& solution); + + // Effective pruning upper bound. + f_t get_cutoff() const { return upper_bound_.load(); } + + bool has_solver_space_incumbent() const { return incumbent_.has_incumbent; } + bool has_external_incumbent() const { return external_incumbent_.has_incumbent; } + bool has_any_incumbent() const + { + return has_solver_space_incumbent() || has_external_incumbent(); + } // Repair a low-quality solution from the heuristics. bool repair_solution(const std::vector& leaf_edge_norms, @@ -193,16 +197,18 @@ class branch_and_bound_t { // Mutex for upper bound omp_mutex_t mutex_upper_; - // Verified incumbent bound (only set when B&B has an actual integer-feasible solution). + // Global numeric pruning upper bound in B&B's internal objective space. + // During the transition away from cutoff as a separate concept, this may become finite before a + // solver-space incumbent is available in `incumbent_`. omp_atomic_t upper_bound_; - // External cutoff from early heuristics (for pruning only, no verified solution). - // Must be in B&B internal objective space (see set_initial_cutoff). - f_t initial_cutoff_{std::numeric_limits::infinity()}; - - // Global variable for incumbent. The incumbent should be updated with the upper bound + // Solver-space incumbent tracked directly by B&B. mip_solution_t incumbent_; + // External incumbent stored in the caller-visible output space for cases where we have an + // incumbent objective but cannot materialize a solver-space incumbent vector. + mip_solution_t external_incumbent_; + // Structure with the general info of the solver. branch_and_bound_stats_t exploration_stats_; diff --git a/cpp/src/mip_heuristics/diversity/population.cuh b/cpp/src/mip_heuristics/diversity/population.cuh index 2509ae17df..8f2fbdb7cd 100644 --- a/cpp/src/mip_heuristics/diversity/population.cuh +++ b/cpp/src/mip_heuristics/diversity/population.cuh @@ -207,7 +207,9 @@ class population_t { std::mutex solution_mutex; std::atomic early_exit_primal_generation = false; std::atomic solutions_in_external_queue_ = false; - f_t best_feasible_objective = std::numeric_limits::max(); + // Best known primal upper bound used to gate callbacks and external-solution handling. This may + // be seeded from an incumbent objective before a matching population solution exists. + f_t best_feasible_objective = std::numeric_limits::max(); assignment_hash_map_t population_hash_map; cuopt::timer_t timer; }; diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 4b525e4d30..0eacef73fc 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -83,7 +83,8 @@ template mip_solution_t run_mip(detail::problem_t& problem, mip_solver_settings_t const& settings, timer_t& timer, - f_t initial_cutoff = std::numeric_limits::infinity()) + f_t initial_cutoff = std::numeric_limits::infinity(), + const std::vector& initial_incumbent_assignment = {}) { try { raft::common::nvtx::range fun_scope("run_mip"); @@ -185,7 +186,8 @@ mip_solution_t run_mip(detail::problem_t& problem, detail::mip_solver_t solver(scaled_problem, settings, scaling, timer); // initial_cutoff is in user-space (representation-invariant). // It will be converted to the target solver-space at each consumption point. - solver.context.initial_cutoff = initial_cutoff; + solver.context.initial_cutoff = initial_cutoff; + solver.context.initial_incumbent_assignment = initial_incumbent_assignment; if (timer.check_time_limit()) { CUOPT_LOG_INFO("Time limit reached before main solve"); detail::solution_t sol(problem); @@ -210,10 +212,14 @@ mip_solution_t run_mip(detail::problem_t& problem, auto mip_callbacks = settings.get_mip_callbacks(); f_t no_bound = problem.presolve_data.objective_scaling_factor >= 0 ? (f_t)-1e20 : (f_t)1e20; auto incumbent_callback = - [presolver_ptr, mip_callbacks, no_bound]( + [presolver_ptr, + mip_callbacks, + no_bound, + initial_incumbent_assignment_ptr = &solver.context.initial_incumbent_assignment]( f_t solver_obj, f_t user_obj, const std::vector& assignment) { std::vector user_assignment; presolver_ptr->uncrush_primal_solution(assignment, user_assignment); + *initial_incumbent_assignment_ptr = user_assignment; invoke_solution_callbacks(mip_callbacks, user_obj, user_assignment, no_bound); }; early_cpufj = std::make_unique>( @@ -349,6 +355,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, // passed to run_mip for correct cross-space conversion. std::atomic early_best_objective{std::numeric_limits::infinity()}; f_t early_best_user_obj{std::numeric_limits::infinity()}; + std::vector early_best_user_assignment; std::mutex early_callback_mutex; bool run_early_fj = run_presolve && settings.determinism_mode != CUOPT_MODE_DETERMINISTIC && @@ -357,6 +364,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, if (run_early_fj) { auto early_fj_callback = [&early_best_objective, &early_best_user_obj, + &early_best_user_assignment, &early_callback_mutex, mip_callbacks = settings.get_mip_callbacks(), no_bound]( @@ -364,8 +372,9 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, std::lock_guard lock(early_callback_mutex); if (solver_obj >= early_best_objective.load()) { return; } early_best_objective.store(solver_obj); - early_best_user_obj = user_obj; - auto user_assignment = assignment; + early_best_user_obj = user_obj; + early_best_user_assignment = assignment; + auto user_assignment = assignment; invoke_solution_callbacks(mip_callbacks, user_obj, user_assignment, no_bound); }; @@ -451,7 +460,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, // early_best_user_obj is in user-space. // run_mip stores it in context.initial_cutoff and converts to target spaces as needed. - auto sol = run_mip(problem, settings, timer, early_best_user_obj); + auto sol = run_mip(problem, settings, timer, early_best_user_obj, early_best_user_assignment); if (run_presolve) { auto status_to_skip = sol.get_termination_status() == mip_termination_status_t::TimeLimit || diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index ee27c79052..c636a70718 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -290,12 +290,15 @@ solution_t mip_solver_t::run_solver() context.problem_ptr->clique_table); context.branch_and_bound_ptr = branch_and_bound.get(); - // Convert initial_cutoff from user-space to B&B's internal objective space. + // Convert the best external upper bound from user-space to B&B's internal objective space. // context.problem_ptr is the post-trivial-presolve problem, whose get_solver_obj_from_user_obj // produces values in the same space as B&B node lower bounds. if (std::isfinite(context.initial_cutoff)) { f_t bb_cutoff = context.problem_ptr->get_solver_obj_from_user_obj(context.initial_cutoff); branch_and_bound->set_initial_cutoff(bb_cutoff); + if (!context.initial_incumbent_assignment.empty()) { + branch_and_bound->set_external_incumbent(bb_cutoff, context.initial_incumbent_assignment); + } dm.population.best_feasible_objective = bb_cutoff; CUOPT_LOG_INFO("B&B using initial cutoff %.6e (user-space: %.6e) from early heuristics", bb_cutoff, diff --git a/cpp/src/mip_heuristics/solver_context.cuh b/cpp/src/mip_heuristics/solver_context.cuh index 8fa852609b..785fbe5099 100644 --- a/cpp/src/mip_heuristics/solver_context.cuh +++ b/cpp/src/mip_heuristics/solver_context.cuh @@ -70,8 +70,12 @@ struct mip_solver_context_t { // Must be converted to the target solver-space before use: // - B&B: problem_ptr->get_solver_obj_from_user_obj(initial_cutoff) // - CPUFJ: papilo_problem.get_solver_obj_from_user_obj(initial_cutoff) - // Use std::isfinite() to check whether a valid cutoff exists. + // Use std::isfinite() to check whether a valid upper bound exists. f_t initial_cutoff{std::numeric_limits::infinity()}; + + // Matching incumbent in the original output space when the pruning upper bound comes from + // external heuristics rather than a B&B solver-space incumbent. + std::vector initial_incumbent_assignment{}; }; } // namespace cuopt::linear_programming::detail From cfa1758561e14156ac407d53bc6d117300aee194 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Apr 2026 03:36:33 -0700 Subject: [PATCH 02/25] remove separate cutoff value --- cpp/src/branch_and_bound/branch_and_bound.cpp | 61 ++++++--------- cpp/src/branch_and_bound/branch_and_bound.hpp | 27 ++----- .../mip_heuristics/diversity/population.cuh | 2 +- cpp/src/mip_heuristics/early_heuristic.cuh | 16 ++-- cpp/src/mip_heuristics/solve.cu | 78 +++++++++++-------- cpp/src/mip_heuristics/solver.cu | 45 +++++++---- cpp/src/mip_heuristics/solver_context.cuh | 12 ++- cpp/tests/mip/incumbent_callback_test.cu | 40 ++++++++++ 8 files changed, 157 insertions(+), 124 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index a027502d24..aa1adccc78 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -256,7 +256,6 @@ branch_and_bound_t::branch_and_bound_t( original_lp_(user_problem.handle_ptr, 1, 1, 1), Arow_(1, 1, 0), incumbent_(1), - external_incumbent_(1), root_relax_soln_(1, 1), root_crossover_soln_(1, 1), pc_(1), @@ -315,21 +314,11 @@ f_t branch_and_bound_t::get_lower_bound() } template -void branch_and_bound_t::set_initial_cutoff(f_t bound) +void branch_and_bound_t::set_initial_upper_bound(f_t bound) { - std::lock_guard lock(mutex_upper_); - if (bound < upper_bound_) { upper_bound_ = bound; } -} - -template -void branch_and_bound_t::set_external_incumbent(f_t objective, - const std::vector& solution) -{ - std::lock_guard lock(mutex_upper_); - if (objective < upper_bound_) { upper_bound_ = objective; } - if (!external_incumbent_.has_incumbent || objective <= external_incumbent_.objective) { - external_incumbent_.set_incumbent_solution(objective, solution); - } + upper_bound_ = bound; + CUOPT_LOG_DEBUG( + "B&B using initial upper bound %.6e (user-space: %.6e) from early heuristics", bound, bound); } template @@ -699,7 +688,6 @@ void branch_and_bound_t::set_solution_at_root(mip_solution_t // We should be done here uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, solution.x); solution.objective = incumbent_.objective; - solution.has_incumbent = true; solution.lower_bound = root_objective_; solution.nodes_explored = 0; solution.simplex_iterations = root_relax_soln_.iterations; @@ -775,7 +763,7 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& if (solver_status_ == mip_status_t::UNSET) { if (exploration_stats_.nodes_explored > 0 && exploration_stats_.nodes_unexplored == 0 && - !has_any_incumbent()) { + upper_bound_ == inf) { settings_.log.printf("Integer infeasible.\n"); solver_status_ = mip_status_t::INFEASIBLE; if (settings_.heuristic_preemption_callback != nullptr) { @@ -786,12 +774,7 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& if (has_solver_space_incumbent()) { uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, solution.x); - solution.objective = incumbent_.objective; - solution.has_incumbent = true; - } else if (has_external_incumbent()) { - solution.x = external_incumbent_.x; - solution.objective = external_incumbent_.objective; - solution.has_incumbent = true; + solution.objective = incumbent_.objective; } solution.lower_bound = lower_bound; solution.nodes_explored = exploration_stats_.nodes_explored; @@ -945,7 +928,7 @@ struct nondeterministic_policy_t : tree_update_policy_t { { } - f_t upper_bound() const override { return bnb.get_cutoff(); } + f_t upper_bound() const override { return bnb.get_upper_bound(); } void update_pseudo_costs(mip_node_t* node, f_t leaf_obj) override { @@ -1363,7 +1346,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( simplex_solver_settings_t lp_settings = settings_; lp_settings.concurrent_halt = &node_concurrent_halt_; lp_settings.set_log(false); - f_t cutoff = get_cutoff(); + f_t cutoff = upper_bound_.load(); if (original_lp_.objective_is_integral) { lp_settings.cut_off = std::ceil(cutoff - settings_.integer_tol) + settings_.dual_tol; } else { @@ -1476,7 +1459,7 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_tlower_bound = node_ptr->lower_bound; - if (node_ptr->lower_bound > get_cutoff()) { + if (node_ptr->lower_bound > upper_bound_.load()) { search_tree_.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); search_tree_.update(node_ptr, node_status_t::FATHOMED); worker->recompute_basis = true; @@ -1614,7 +1597,7 @@ void branch_and_bound_t::dive_with(branch_and_bound_worker_t worker->lower_bound = node_ptr->lower_bound; - if (node_ptr->lower_bound > get_cutoff()) { + if (node_ptr->lower_bound > upper_bound_.load()) { worker->recompute_basis = true; worker->recompute_bounds = true; continue; @@ -1757,7 +1740,7 @@ void branch_and_bound_t::run_scheduler() std::optional*> start_node = node_queue_.pop_best_first(); if (!start_node.has_value()) { continue; } - if (get_cutoff() < start_node.value()->lower_bound) { + if (upper_bound_.load() < start_node.value()->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound search_tree_.graphviz_node( @@ -1781,7 +1764,7 @@ void branch_and_bound_t::run_scheduler() std::optional*> start_node = node_queue_.pop_diving(); if (!start_node.has_value()) { continue; } - if (get_cutoff() < start_node.value()->lower_bound || + if (upper_bound_.load() < start_node.value()->lower_bound || start_node.value()->depth < diving_settings.min_node_depth) { continue; } @@ -1855,7 +1838,7 @@ void branch_and_bound_t::single_threaded_solve() std::optional*> start_node = node_queue_.pop_best_first(); if (!start_node.has_value()) { continue; } - if (get_cutoff() < start_node.value()->lower_bound) { + if (upper_bound_.load() < start_node.value()->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound search_tree_.graphviz_node( @@ -2355,12 +2338,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::NUMERICAL; } - if (settings_.reduced_cost_strengthening >= 1 && get_cutoff() < last_upper_bound) { + if (settings_.reduced_cost_strengthening >= 1 && upper_bound_.load() < last_upper_bound) { mutex_upper_.lock(); - last_upper_bound = get_cutoff(); + last_upper_bound = upper_bound_.load(); std::vector lower_bounds; std::vector upper_bounds; - find_reduced_cost_fixings(get_cutoff(), lower_bounds, upper_bounds); + find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); mutex_upper_.unlock(); mutex_original_lp_.lock(); original_lp_.lower = lower_bounds; @@ -2552,10 +2535,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return solver_status_; } - if (settings_.reduced_cost_strengthening >= 2 && get_cutoff() < last_upper_bound) { + if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { std::vector lower_bounds; std::vector upper_bounds; - i_t num_fixed = find_reduced_cost_fixings(get_cutoff(), lower_bounds, upper_bounds); + i_t num_fixed = find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); if (num_fixed > 0) { std::vector bounds_changed(original_lp_.num_cols, true); std::vector row_sense; @@ -2660,7 +2643,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::optional*> start_node = node_queue_.pop_best_first(); if (!start_node.has_value()) { continue; } - if (get_cutoff() < start_node.value()->lower_bound) { + if (upper_bound_.load() < start_node.value()->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound search_tree_.graphviz_node( @@ -3502,7 +3485,7 @@ void branch_and_bound_t::deterministic_sort_replay_events( template void branch_and_bound_t::deterministic_prune_worker_nodes_vs_incumbent() { - f_t upper_bound = get_cutoff(); + f_t upper_bound = upper_bound_.load(); for (auto& worker : *deterministic_workers_) { // Check nodes in plunge stack - filter in place @@ -3620,7 +3603,7 @@ f_t branch_and_bound_t::deterministic_compute_lower_bound() } // Tree is exhausted - if (lower_bound == std::numeric_limits::infinity() && has_any_incumbent()) { + if (lower_bound == std::numeric_limits::infinity() && incumbent_.has_incumbent) { lower_bound = upper_bound_.load(); } @@ -3638,7 +3621,7 @@ void branch_and_bound_t::deterministic_populate_diving_heap() const int num_diving = deterministic_diving_workers_->size(); constexpr int target_nodes_per_worker = 10; const int target_total = num_diving * target_nodes_per_worker; - f_t cutoff = get_cutoff(); + f_t cutoff = upper_bound_.load(); // Collect candidate nodes from BFS worker backlog heaps std::vector*, f_t>> candidates; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 6edc6204ee..0f86040462 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -120,23 +120,12 @@ class branch_and_bound_t { void set_concurrent_lp_root_solve(bool enable) { enable_concurrent_lp_root_solve_ = enable; } - // Seed the global pruning upper bound from an external source (e.g., early FJ during presolve). + // Seed the global upper bound from an external source (e.g., early FJ during presolve). // `bound` must be in B&B's internal objective space. - void set_initial_cutoff(f_t bound); - - // Store an incumbent that exists outside of B&B's solver space. The objective must match - // B&B's internal objective space while `solution` lives in the caller's original output space. - void set_external_incumbent(f_t objective, const std::vector& solution); - - // Effective pruning upper bound. - f_t get_cutoff() const { return upper_bound_.load(); } + void set_initial_upper_bound(f_t bound); + f_t get_upper_bound() const { return upper_bound_.load(); } bool has_solver_space_incumbent() const { return incumbent_.has_incumbent; } - bool has_external_incumbent() const { return external_incumbent_.has_incumbent; } - bool has_any_incumbent() const - { - return has_solver_space_incumbent() || has_external_incumbent(); - } // Repair a low-quality solution from the heuristics. bool repair_solution(const std::vector& leaf_edge_norms, @@ -203,18 +192,14 @@ class branch_and_bound_t { // Mutex for upper bound omp_mutex_t mutex_upper_; - // Global numeric pruning upper bound in B&B's internal objective space. - // During the transition away from cutoff as a separate concept, this may become finite before a - // solver-space incumbent is available in `incumbent_`. + // Global upper bound in B&B's internal objective space. + // A finite value implies an incumbent exists somewhere (solver-space in incumbent_, or + // original-space in the mip_solver_context_t), but does NOT imply incumbent_.has_incumbent. omp_atomic_t upper_bound_; // Solver-space incumbent tracked directly by B&B. mip_solution_t incumbent_; - // External incumbent stored in the caller-visible output space for cases where we have an - // incumbent objective but cannot materialize a solver-space incumbent vector. - mip_solution_t external_incumbent_; - // Structure with the general info of the solver. branch_and_bound_stats_t exploration_stats_; diff --git a/cpp/src/mip_heuristics/diversity/population.cuh b/cpp/src/mip_heuristics/diversity/population.cuh index 8f2fbdb7cd..c83a4bfb83 100644 --- a/cpp/src/mip_heuristics/diversity/population.cuh +++ b/cpp/src/mip_heuristics/diversity/population.cuh @@ -208,7 +208,7 @@ class population_t { std::atomic early_exit_primal_generation = false; std::atomic solutions_in_external_queue_ = false; // Best known primal upper bound used to gate callbacks and external-solution handling. This may - // be seeded from an incumbent objective before a matching population solution exists. + // be seeded from an early-FJ incumbent objective before a matching population solution exists. f_t best_feasible_objective = std::numeric_limits::max(); assignment_hash_map_t population_hash_map; cuopt::timer_t timer; diff --git a/cpp/src/mip_heuristics/early_heuristic.cuh b/cpp/src/mip_heuristics/early_heuristic.cuh index ab924f3441..090cfd4901 100644 --- a/cpp/src/mip_heuristics/early_heuristic.cuh +++ b/cpp/src/mip_heuristics/early_heuristic.cuh @@ -24,8 +24,8 @@ namespace cuopt::linear_programming::detail { template -using early_incumbent_callback_t = - std::function& assignment)>; +using early_incumbent_callback_t = std::function& assignment, const char* heuristic_name)>; // CRTP base for early heuristics that run on the original (or papilo-presolved) problem // during presolve to find incumbents as early as possible. @@ -89,13 +89,11 @@ class early_heuristic_t { best_assignment_ = user_assignment; solution_found_ = true; f_t user_obj = problem_ptr_->get_user_obj_from_solver_obj(solver_obj); - double elapsed = - std::chrono::duration(std::chrono::steady_clock::now() - start_time_).count(); - CUOPT_LOG_INFO("Early heuristics (%s) lowered the primal bound. Objective %g. Time %.2f", - Derived::name(), - user_obj, - elapsed); - if (incumbent_callback_) { incumbent_callback_(solver_obj, user_obj, user_assignment); } + // Log and callback are deferred to the shared incumbent_callback_ which enforces + // global monotonicity across all early heuristic instances. + if (incumbent_callback_) { + incumbent_callback_(solver_obj, user_obj, user_assignment, Derived::name()); + } } int device_id_{0}; diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index d2c7b7ab10..88dba278b1 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -83,7 +83,7 @@ template mip_solution_t run_mip(detail::problem_t& problem, mip_solver_settings_t const& settings, timer_t& timer, - f_t initial_cutoff = std::numeric_limits::infinity(), + f_t initial_upper_bound = std::numeric_limits::infinity(), const std::vector& initial_incumbent_assignment = {}) { try { @@ -184,9 +184,9 @@ mip_solution_t run_mip(detail::problem_t& problem, detail::trivial_presolve(scaled_problem); detail::mip_solver_t solver(scaled_problem, settings, scaling, timer); - // initial_cutoff is in user-space (representation-invariant). + // initial_upper_bound is in user-space (representation-invariant). // It will be converted to the target solver-space at each consumption point. - solver.context.initial_cutoff = initial_cutoff; + solver.context.initial_upper_bound = initial_upper_bound; solver.context.initial_incumbent_assignment = initial_incumbent_assignment; if (timer.check_time_limit()) { CUOPT_LOG_INFO("Time limit reached before main solve"); @@ -198,11 +198,11 @@ mip_solution_t run_mip(detail::problem_t& problem, } // Run early CPUFJ on papilo-presolved problem during cuOpt presolve (probing cache). - // Stopped by run_solver after presolve completes; its best objective feeds into initial_cutoff. - // This CPUFJ operates on *problem.original_problem_ptr (papilo-presolved + // Stopped by run_solver after presolve completes; its best objective feeds into + // initial_upper_bound. This CPUFJ operates on *problem.original_problem_ptr (papilo-presolved // optimization_problem_t). Its solver-space differs from both the first-pass FJ (original - // problem) and B&B (post-trivial- presolve), so initial_cutoff (user-space) is converted via - // problem.get_solver_obj_from_user_obj. + // problem) and B&B (post-trivial- presolve), so initial_upper_bound (user-space) is converted + // via problem.get_solver_obj_from_user_obj. std::unique_ptr> early_cpufj; bool run_early_cpufj = problem.has_papilo_presolve_data() && settings.determinism_mode != CUOPT_MODE_DETERMINISTIC && @@ -211,23 +211,28 @@ mip_solution_t run_mip(detail::problem_t& problem, auto* presolver_ptr = problem.presolve_data.papilo_presolve_ptr; auto mip_callbacks = settings.get_mip_callbacks(); f_t no_bound = problem.presolve_data.objective_scaling_factor >= 0 ? (f_t)-1e20 : (f_t)1e20; - auto incumbent_callback = - [presolver_ptr, - mip_callbacks, - no_bound, - initial_incumbent_assignment_ptr = &solver.context.initial_incumbent_assignment]( - f_t solver_obj, f_t user_obj, const std::vector& assignment) { - std::vector user_assignment; - presolver_ptr->uncrush_primal_solution(assignment, user_assignment); - *initial_incumbent_assignment_ptr = user_assignment; - invoke_solution_callbacks(mip_callbacks, user_obj, user_assignment, no_bound); - }; + auto incumbent_callback = [presolver_ptr, + mip_callbacks, + no_bound, + initial_incumbent_assignment_ptr = + &solver.context.initial_incumbent_assignment]( + f_t solver_obj, + f_t user_obj, + const std::vector& assignment, + const char* heuristic_name) { + std::vector user_assignment; + presolver_ptr->uncrush_primal_solution(assignment, user_assignment); + *initial_incumbent_assignment_ptr = user_assignment; + CUOPT_LOG_INFO( + "New solution from early primal heuristics (%s). Objective %g", heuristic_name, user_obj); + invoke_solution_callbacks(mip_callbacks, user_obj, user_assignment, no_bound); + }; early_cpufj = std::make_unique>( *problem.original_problem_ptr, settings.get_tolerances(), incumbent_callback); - // Convert initial_cutoff from user-space to the CPUFJ's solver-space (papilo-presolved). + // Convert initial_upper_bound from user-space to the CPUFJ's solver-space (papilo-presolved). // problem.get_solver_obj_from_user_obj uses the papilo offset/scale (matching the CPUFJ). - if (std::isfinite(initial_cutoff)) { - early_cpufj->set_best_objective(problem.get_solver_obj_from_user_obj(initial_cutoff)); + if (std::isfinite(initial_upper_bound)) { + early_cpufj->set_best_objective(problem.get_solver_obj_from_user_obj(initial_upper_bound)); } early_cpufj->start(); solver.context.early_cpufj_ptr = early_cpufj.get(); @@ -362,19 +367,29 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, op_problem.get_n_integers() > 0 && op_problem.get_n_constraints() > 0; f_t no_bound = problem.presolve_data.objective_scaling_factor >= 0 ? (f_t)-1e20 : (f_t)1e20; if (run_early_fj) { + auto early_fj_start = std::chrono::steady_clock::now(); auto early_fj_callback = [&early_best_objective, &early_best_user_obj, &early_best_user_assignment, &early_callback_mutex, + &early_fj_start, mip_callbacks = settings.get_mip_callbacks(), - no_bound]( - f_t solver_obj, f_t user_obj, const std::vector& assignment) { + no_bound](f_t solver_obj, + f_t user_obj, + const std::vector& assignment, + const char* heuristic_name) { std::lock_guard lock(early_callback_mutex); if (solver_obj >= early_best_objective.load()) { return; } early_best_objective.store(solver_obj); early_best_user_obj = user_obj; early_best_user_assignment = assignment; - auto user_assignment = assignment; + double elapsed = + std::chrono::duration(std::chrono::steady_clock::now() - early_fj_start).count(); + CUOPT_LOG_INFO("New solution from early primal heuristics (%s). Objective %g. Time %.2f", + heuristic_name, + user_obj, + elapsed); + auto user_assignment = assignment; invoke_solution_callbacks(mip_callbacks, user_obj, user_assignment, no_bound); }; @@ -382,13 +397,13 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, early_cpufj = std::make_unique>( op_problem, settings.get_tolerances(), early_fj_callback); early_cpufj->start(); - CUOPT_LOG_INFO("Started early CPUFJ on original problem"); + CUOPT_LOG_DEBUG("Started early CPUFJ on original problem"); // Start early GPU FJ (uses GPU while CPU is busy with Papilo) early_gpufj = std::make_unique>(op_problem, settings, early_fj_callback); early_gpufj->start(); - CUOPT_LOG_INFO("Started early GPUFJ during presolve"); + CUOPT_LOG_DEBUG("Started early GPUFJ during presolve"); } auto constexpr const dual_postsolve = false; @@ -444,8 +459,8 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, if (early_gpufj) { early_gpufj->stop(); if (early_gpufj->solution_found()) { - CUOPT_LOG_INFO("Early GPU FJ found incumbent with objective %.6e during presolve", - early_gpufj->get_best_objective()); + CUOPT_LOG_DEBUG("Early GPU FJ found incumbent with objective %.6e during presolve", + early_gpufj->get_best_objective()); } early_gpufj.reset(); // Free GPU memory } @@ -453,8 +468,9 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, if (early_cpufj && run_presolve && presolve_result_opt.has_value()) { early_cpufj->stop(); if (early_cpufj->solution_found()) { - CUOPT_LOG_INFO("Early CPUFJ (original) found incumbent with objective %.6e", - early_cpufj->get_best_objective()); + CUOPT_LOG_DEBUG( + "Early CPUFJ (original) found incumbent with objective %.6e during presolve", + early_cpufj->get_best_objective()); } early_cpufj.reset(); } @@ -469,7 +485,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, } // early_best_user_obj is in user-space. - // run_mip stores it in context.initial_cutoff and converts to target spaces as needed. + // run_mip stores it in context.initial_upper_bound and converts to target spaces as needed. auto sol = run_mip(problem, settings, timer, early_best_user_obj, early_best_user_assignment); if (run_presolve) { diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 50a3973860..4e4b91853f 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -221,13 +221,13 @@ solution_t mip_solver_t::run_solver() if (context.early_cpufj_ptr) { context.early_cpufj_ptr->stop(); if (context.early_cpufj_ptr->solution_found()) { - // Compare in user-space (representation-invariant) to pick the tighter cutoff. + // Compare in user-space (representation-invariant) to pick the tighter upper bound. f_t cpufj_user_obj = context.early_cpufj_ptr->get_best_user_objective(); bool should_update = - !std::isfinite(context.initial_cutoff) || - (context.problem_ptr->maximize ? cpufj_user_obj > context.initial_cutoff - : cpufj_user_obj < context.initial_cutoff); - if (should_update) { context.initial_cutoff = cpufj_user_obj; } + !std::isfinite(context.initial_upper_bound) || + (context.problem_ptr->maximize ? cpufj_user_obj > context.initial_upper_bound + : cpufj_user_obj < context.initial_upper_bound); + if (should_update) { context.initial_upper_bound = cpufj_user_obj; } CUOPT_LOG_INFO("Early CPUFJ found incumbent with user-space objective %g during presolve", cpufj_user_obj); } @@ -404,16 +404,13 @@ solution_t mip_solver_t::run_solver() // Convert the best external upper bound from user-space to B&B's internal objective space. // context.problem_ptr is the post-trivial-presolve problem, whose get_solver_obj_from_user_obj // produces values in the same space as B&B node lower bounds. - if (std::isfinite(context.initial_cutoff)) { - f_t bb_cutoff = context.problem_ptr->get_solver_obj_from_user_obj(context.initial_cutoff); - branch_and_bound->set_initial_cutoff(bb_cutoff); - if (!context.initial_incumbent_assignment.empty()) { - branch_and_bound->set_external_incumbent(bb_cutoff, context.initial_incumbent_assignment); - } - dm.population.best_feasible_objective = bb_cutoff; - CUOPT_LOG_INFO("B&B using initial cutoff %.6e (user-space: %.6e) from early heuristics", - bb_cutoff, - context.initial_cutoff); + if (std::isfinite(context.initial_upper_bound)) { + f_t bb_ub = context.problem_ptr->get_solver_obj_from_user_obj(context.initial_upper_bound); + branch_and_bound->set_initial_upper_bound(bb_ub); + dm.population.best_feasible_objective = bb_ub; + CUOPT_LOG_DEBUG("B&B using initial upper bound %.6e (user-space: %.6e) from early heuristics", + bb_ub, + context.initial_upper_bound); } auto* stats_ptr = &context.stats; @@ -484,9 +481,25 @@ solution_t mip_solver_t::run_solver() context.stats.num_simplex_iterations = branch_and_bound_solution.simplex_iterations; } sol.compute_feasibility(); + + // If the population has no feasible solution but early heuristics found an OG-space incumbent, + // use that instead. Any population incumbent is guaranteed at least as good + // (best_feasible_objective was seeded from the early heuristic bound), so this only fires when + // the population is empty. + if (!sol.get_feasible() && !context.initial_incumbent_assignment.empty()) { + cuopt_assert( + context.initial_incumbent_assignment.size() == (size_t)context.problem_ptr->n_variables, + "Early heuristic incumbent size mismatch"); + raft::copy(sol.assignment.data(), + context.initial_incumbent_assignment.data(), + context.initial_incumbent_assignment.size(), + sol.handle_ptr->get_stream()); + sol.compute_feasibility(); + CUOPT_LOG_DEBUG("Using early heuristic incumbent (no solver-space incumbent found)"); + } + rmm::device_scalar is_feasible(sol.handle_ptr->get_stream()); sol.test_variable_bounds(true, is_feasible.data()); - // test_variable_bounds clears is_feasible if the test is failed if (!is_feasible.value(sol.handle_ptr->get_stream())) { CUOPT_LOG_ERROR( "Solution is not feasible due to variable bounds, returning infeasible solution!"); diff --git a/cpp/src/mip_heuristics/solver_context.cuh b/cpp/src/mip_heuristics/solver_context.cuh index 785fbe5099..86fc382a04 100644 --- a/cpp/src/mip_heuristics/solver_context.cuh +++ b/cpp/src/mip_heuristics/solver_context.cuh @@ -66,15 +66,13 @@ struct mip_solver_context_t { work_unit_scheduler_t work_unit_scheduler_{5.0}; early_cpufj_t* early_cpufj_ptr{nullptr}; - // Best objective from early heuristics, in user-space. + // Best upper bound from early heuristics, in user-space. // Must be converted to the target solver-space before use: - // - B&B: problem_ptr->get_solver_obj_from_user_obj(initial_cutoff) - // - CPUFJ: papilo_problem.get_solver_obj_from_user_obj(initial_cutoff) - // Use std::isfinite() to check whether a valid upper bound exists. - f_t initial_cutoff{std::numeric_limits::infinity()}; + // - B&B: problem_ptr->get_solver_obj_from_user_obj(initial_upper_bound) + // - CPUFJ: papilo_problem.get_solver_obj_from_user_obj(initial_upper_bound) + f_t initial_upper_bound{std::numeric_limits::infinity()}; - // Matching incumbent in the original output space when the pruning upper bound comes from - // external heuristics rather than a B&B solver-space incumbent. + // Matching incumbent assignment in original output space from early heuristics. std::vector initial_incumbent_assignment{}; }; diff --git a/cpp/tests/mip/incumbent_callback_test.cu b/cpp/tests/mip/incumbent_callback_test.cu index a9593fa559..92ce2dd69c 100644 --- a/cpp/tests/mip/incumbent_callback_test.cu +++ b/cpp/tests/mip/incumbent_callback_test.cu @@ -154,4 +154,44 @@ TEST(mip_solve, incumbent_get_set_callback_test) } } +// Verify that when only early heuristics find a feasible incumbent but the solver-space +// pipeline (B&B + GPU heuristics) does not, the solver still returns that incumbent. +// B&B runs but exits immediately (node_limit=0); GPU heuristics are disabled so the +// population stays empty. The fallback in solver.cu must use the OG-space incumbent. +TEST(mip_solve, early_heuristic_incumbent_fallback) +{ + setenv("CUOPT_DISABLE_GPU_HEURISTICS", "1", 1); + + const raft::handle_t handle_{}; + auto path = make_path_absolute("mip/pk1.mps"); + cuopt::mps_parser::mps_data_model_t mps_problem = + cuopt::mps_parser::parse_mps(path, false); + handle_.sync_stream(); + auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); + + auto settings = mip_solver_settings_t{}; + settings.time_limit = 10.; + settings.presolver = presolver_t::Papilo; + settings.node_limit = 0; + + int user_data = 0; + std::vector, double>> callback_solutions; + test_get_solution_callback_t get_cb(callback_solutions, op_problem.get_n_variables(), &user_data); + settings.set_mip_callback(&get_cb, &user_data); + + auto solution = solve_mip(op_problem, settings); + + unsetenv("CUOPT_DISABLE_GPU_HEURISTICS"); + + EXPECT_GE(get_cb.n_calls, 1) << "Early heuristics should have emitted at least one incumbent"; + auto status = solution.get_termination_status(); + EXPECT_TRUE(status == mip_termination_status_t::FeasibleFound || + status == mip_termination_status_t::Optimal) + << "Expected feasible result, got " + << mip_solution_t::get_termination_status_string(status); + EXPECT_TRUE(std::isfinite(solution.get_objective_value())); + + if (!callback_solutions.empty()) { check_solutions(get_cb, mps_problem, settings); } +} + } // namespace cuopt::linear_programming::test From cc7e99ff67a6de40fcf484c8acd0108a3ce74a47 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Apr 2026 04:23:52 -0700 Subject: [PATCH 03/25] ai review comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 +- cpp/src/mip_heuristics/solve.cu | 44 +++++++++++++++---- cpp/src/mip_heuristics/solver.cu | 27 +----------- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index aa1adccc78..5796e8cb43 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2475,7 +2475,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); f_t abs_gap = upper_bound_.load() - root_objective_; if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { - set_solution_at_root(solution, cut_info); + if (num_fractional == 0) { set_solution_at_root(solution, cut_info); } set_final_solution(solution, root_objective_); return mip_status_t::OPTIMAL; } diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 88dba278b1..d0bc810228 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -83,8 +83,8 @@ template mip_solution_t run_mip(detail::problem_t& problem, mip_solver_settings_t const& settings, timer_t& timer, - f_t initial_upper_bound = std::numeric_limits::infinity(), - const std::vector& initial_incumbent_assignment = {}) + f_t& initial_upper_bound, + std::vector& initial_incumbent_assignment) { try { raft::common::nvtx::range fun_scope("run_mip"); @@ -211,20 +211,17 @@ mip_solution_t run_mip(detail::problem_t& problem, auto* presolver_ptr = problem.presolve_data.papilo_presolve_ptr; auto mip_callbacks = settings.get_mip_callbacks(); f_t no_bound = problem.presolve_data.objective_scaling_factor >= 0 ? (f_t)-1e20 : (f_t)1e20; - auto incumbent_callback = [presolver_ptr, - mip_callbacks, - no_bound, - initial_incumbent_assignment_ptr = - &solver.context.initial_incumbent_assignment]( + auto incumbent_callback = [presolver_ptr, mip_callbacks, no_bound, ctx_ptr = &solver.context]( f_t solver_obj, f_t user_obj, const std::vector& assignment, const char* heuristic_name) { std::vector user_assignment; presolver_ptr->uncrush_primal_solution(assignment, user_assignment); - *initial_incumbent_assignment_ptr = user_assignment; + ctx_ptr->initial_incumbent_assignment = user_assignment; + ctx_ptr->initial_upper_bound = user_obj; CUOPT_LOG_INFO( - "New solution from early primal heuristics (%s). Objective %g", heuristic_name, user_obj); + "New solution from early %s (presolved). Objective %g", heuristic_name, user_obj); invoke_solution_callbacks(mip_callbacks, user_obj, user_assignment, no_bound); }; early_cpufj = std::make_unique>( @@ -256,6 +253,10 @@ mip_solution_t run_mip(detail::problem_t& problem, auto sol = scaled_sol.get_solution( is_feasible_before_scaling || is_feasible_after_unscaling, solver.get_solver_stats(), false); + // Write back the (possibly updated) incumbent from the papilo-phase callback. + initial_upper_bound = solver.context.initial_upper_bound; + initial_incumbent_assignment = solver.context.initial_incumbent_assignment; + int hidesol = std::getenv("CUOPT_MIP_HIDE_SOLUTION") ? atoi(std::getenv("CUOPT_MIP_HIDE_SOLUTION")) : 0; if (!hidesol) { detail::print_solution(scaled_problem.handle_ptr, sol.get_solution()); } @@ -532,6 +533,31 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, } } + // Use the early heuristic OG-space incumbent if it is better than what the solver-space + // pipeline returned (or if the pipeline returned no feasible solution at all). + if (!early_best_user_assignment.empty()) { + bool sol_has_incumbent = + sol.get_termination_status() == mip_termination_status_t::FeasibleFound || + sol.get_termination_status() == mip_termination_status_t::Optimal; + bool is_maximization = problem.presolve_data.objective_scaling_factor < 0; + bool early_heuristic_is_better = + !sol_has_incumbent || (is_maximization ? early_best_user_obj > sol.get_objective_value() + : early_best_user_obj < sol.get_objective_value()); + if (early_heuristic_is_better) { + detail::problem_t full_problem(op_problem); + detail::solution_t fallback_sol(full_problem); + fallback_sol.copy_new_assignment(early_best_user_assignment); + fallback_sol.compute_feasibility(); + if (fallback_sol.get_feasible()) { + auto stats = sol.get_stats(); + stats.presolve_time += presolve_time; + fallback_sol.post_process_completed = true; + sol = fallback_sol.get_solution(true, stats); + CUOPT_LOG_DEBUG("Using early heuristic incumbent (objective %g)", early_best_user_obj); + } + } + } + if (settings.sol_file != "") { CUOPT_LOG_INFO("Writing solution to file %s", settings.sol_file.c_str()); sol.write_to_sol_file(settings.sol_file, op_problem.get_handle_ptr()->get_stream()); diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 4e4b91853f..d75f41cd75 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -217,19 +217,12 @@ solution_t mip_solver_t::run_solver() : presolve_time_limit; bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit, timer_) : true; - // Stop early CPUFJ after cuopt presolve (probing cache) but before main solve + // Stop early CPUFJ after cuopt presolve (probing cache) but before main solve. if (context.early_cpufj_ptr) { context.early_cpufj_ptr->stop(); if (context.early_cpufj_ptr->solution_found()) { - // Compare in user-space (representation-invariant) to pick the tighter upper bound. - f_t cpufj_user_obj = context.early_cpufj_ptr->get_best_user_objective(); - bool should_update = - !std::isfinite(context.initial_upper_bound) || - (context.problem_ptr->maximize ? cpufj_user_obj > context.initial_upper_bound - : cpufj_user_obj < context.initial_upper_bound); - if (should_update) { context.initial_upper_bound = cpufj_user_obj; } CUOPT_LOG_INFO("Early CPUFJ found incumbent with user-space objective %g during presolve", - cpufj_user_obj); + context.early_cpufj_ptr->get_best_user_objective()); } } @@ -482,22 +475,6 @@ solution_t mip_solver_t::run_solver() } sol.compute_feasibility(); - // If the population has no feasible solution but early heuristics found an OG-space incumbent, - // use that instead. Any population incumbent is guaranteed at least as good - // (best_feasible_objective was seeded from the early heuristic bound), so this only fires when - // the population is empty. - if (!sol.get_feasible() && !context.initial_incumbent_assignment.empty()) { - cuopt_assert( - context.initial_incumbent_assignment.size() == (size_t)context.problem_ptr->n_variables, - "Early heuristic incumbent size mismatch"); - raft::copy(sol.assignment.data(), - context.initial_incumbent_assignment.data(), - context.initial_incumbent_assignment.size(), - sol.handle_ptr->get_stream()); - sol.compute_feasibility(); - CUOPT_LOG_DEBUG("Using early heuristic incumbent (no solver-space incumbent found)"); - } - rmm::device_scalar is_feasible(sol.handle_ptr->get_stream()); sol.test_variable_bounds(true, is_feasible.data()); if (!is_feasible.value(sol.handle_ptr->get_stream())) { From 62b1ba58e01a90c7afdce5eae357d7c9e92d1a00 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Apr 2026 06:11:07 -0700 Subject: [PATCH 04/25] ai review comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 -- cpp/src/dual_simplex/solution.hpp | 3 ++- cpp/src/mip_heuristics/solve.cu | 7 ++++--- 3 files changed, 6 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 5796e8cb43..1762a2963e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -317,8 +317,6 @@ template void branch_and_bound_t::set_initial_upper_bound(f_t bound) { upper_bound_ = bound; - CUOPT_LOG_DEBUG( - "B&B using initial upper bound %.6e (user-space: %.6e) from early heuristics", bound, bound); } template diff --git a/cpp/src/dual_simplex/solution.hpp b/cpp/src/dual_simplex/solution.hpp index 5739cedaea..86213c86e9 100644 --- a/cpp/src/dual_simplex/solution.hpp +++ b/cpp/src/dual_simplex/solution.hpp @@ -9,6 +9,7 @@ #include +#include #include namespace cuopt::linear_programming::dual_simplex { @@ -74,7 +75,7 @@ class mip_solution_t { f_t lower_bound; int64_t nodes_explored; int64_t simplex_iterations; - bool has_incumbent; + omp_atomic_t has_incumbent; }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index d0bc810228..f177d1752a 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -488,6 +488,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, // early_best_user_obj is in user-space. // run_mip stores it in context.initial_upper_bound and converts to target spaces as needed. auto sol = run_mip(problem, settings, timer, early_best_user_obj, early_best_user_assignment); + const f_t cuopt_presolve_time = sol.get_stats().presolve_time; if (run_presolve) { auto status_to_skip = sol.get_termination_status() == mip_termination_status_t::TimeLimit || @@ -524,7 +525,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, auto full_stats = sol.get_stats(); // add third party presolve time to cuopt presolve time - full_stats.presolve_time += presolve_time; + full_stats.presolve_time = cuopt_presolve_time + presolve_time; // FIXME:: reduced_solution.get_stats() is not correct, we need to compute the stats for // the full problem @@ -549,8 +550,8 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, fallback_sol.copy_new_assignment(early_best_user_assignment); fallback_sol.compute_feasibility(); if (fallback_sol.get_feasible()) { - auto stats = sol.get_stats(); - stats.presolve_time += presolve_time; + auto stats = sol.get_stats(); + stats.presolve_time = cuopt_presolve_time + presolve_time; fallback_sol.post_process_completed = true; sol = fallback_sol.get_solution(true, stats); CUOPT_LOG_DEBUG("Using early heuristic incumbent (objective %g)", early_best_user_obj); From 15aa2d8bb176dfd408413a9e3fff75837ff4be23 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Apr 2026 09:00:27 -0700 Subject: [PATCH 05/25] fix duplicate log line --- .../mip/solver_solution.hpp | 1 + cpp/src/mip_heuristics/solution/solution.cu | 41 +++++-------------- cpp/src/mip_heuristics/solve.cu | 9 +++- cpp/src/mip_heuristics/solver_solution.cu | 20 +++++++++ 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_solution.hpp b/cpp/include/cuopt/linear_programming/mip/solver_solution.hpp index 70c2e4bcac..b8fa884540 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_solution.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_solution.hpp @@ -75,6 +75,7 @@ class mip_solution_t : public base_solution_t { const std::vector& get_variable_names() const; const std::vector>& get_solution_pool() const; void write_to_sol_file(std::string_view filename, rmm::cuda_stream_view stream_view) const; + void log_detailed_summary() const; void log_summary() const; private: diff --git a/cpp/src/mip_heuristics/solution/solution.cu b/cpp/src/mip_heuristics/solution/solution.cu index 531d54372c..f8844d1430 100644 --- a/cpp/src/mip_heuristics/solution/solution.cu +++ b/cpp/src/mip_heuristics/solution/solution.cu @@ -616,42 +616,23 @@ mip_solution_t solution_t::get_solution(bool output_feasible f_t max_constraint_violation = compute_max_constraint_violation(); f_t max_int_violation = compute_max_int_violation(); f_t max_variable_bound_violation = compute_max_variable_violation(); - f_t total_solve_time = stats.total_solve_time; - f_t presolve_time = stats.presolve_time; - i_t num_nodes = stats.num_nodes; - i_t num_simplex_iterations = stats.num_simplex_iterations; handle_ptr->sync_stream(); - if (log_stats) { - CUOPT_LOG_INFO( - "Solution objective: %f , relative_mip_gap %f solution_bound %f presolve_time %f " - "total_solve_time %f " - "max constraint violation %f max int violation %f max var bounds violation %f " - "nodes %d simplex_iterations %d", - h_user_obj, - rel_mip_gap, - solution_bound, - presolve_time, - total_solve_time, - max_constraint_violation, - max_int_violation, - max_variable_bound_violation, - num_nodes, - num_simplex_iterations); - } const bool not_optimal = rel_mip_gap > problem_ptr->tolerances.relative_mip_gap && abs_mip_gap > problem_ptr->tolerances.absolute_mip_gap; auto term_reason = not_optimal ? mip_termination_status_t::FeasibleFound : mip_termination_status_t::Optimal; if (is_problem_fully_reduced) { term_reason = mip_termination_status_t::Optimal; } - return mip_solution_t(std::move(assignment), - problem_ptr->var_names, - h_user_obj, - rel_mip_gap, - term_reason, - max_constraint_violation, - max_int_violation, - max_variable_bound_violation, - stats); + auto sol = mip_solution_t(std::move(assignment), + problem_ptr->var_names, + h_user_obj, + rel_mip_gap, + term_reason, + max_constraint_violation, + max_int_violation, + max_variable_bound_violation, + stats); + if (log_stats) { sol.log_detailed_summary(); } + return sol; } else { return mip_solution_t{is_problem_fully_reduced ? mip_termination_status_t::Infeasible : mip_termination_status_t::TimeLimit, diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index f177d1752a..00cf923ef0 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -530,7 +530,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, // FIXME:: reduced_solution.get_stats() is not correct, we need to compute the stats for // the full problem full_sol.post_process_completed = true; // hack - sol = full_sol.get_solution(true, full_stats); + sol = full_sol.get_solution(true, full_stats, false); } } @@ -553,12 +553,17 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, auto stats = sol.get_stats(); stats.presolve_time = cuopt_presolve_time + presolve_time; fallback_sol.post_process_completed = true; - sol = fallback_sol.get_solution(true, stats); + sol = fallback_sol.get_solution(true, stats, false); CUOPT_LOG_DEBUG("Using early heuristic incumbent (objective %g)", early_best_user_obj); } } } + if (sol.get_termination_status() == mip_termination_status_t::FeasibleFound || + sol.get_termination_status() == mip_termination_status_t::Optimal) { + sol.log_detailed_summary(); + } + if (settings.sol_file != "") { CUOPT_LOG_INFO("Writing solution to file %s", settings.sol_file.c_str()); sol.write_to_sol_file(settings.sol_file, op_problem.get_handle_ptr()->get_stream()); diff --git a/cpp/src/mip_heuristics/solver_solution.cu b/cpp/src/mip_heuristics/solver_solution.cu index a9bc6c5416..8f6f8de05f 100644 --- a/cpp/src/mip_heuristics/solver_solution.cu +++ b/cpp/src/mip_heuristics/solver_solution.cu @@ -235,6 +235,26 @@ void mip_solution_t::log_summary() const CUOPT_LOG_INFO("Total Solve Time: %f", get_total_solve_time()); } +template +void mip_solution_t::log_detailed_summary() const +{ + CUOPT_LOG_INFO( + "Solution objective: %f , relative_mip_gap %f solution_bound %f presolve_time %f " + "total_solve_time %f " + "max constraint violation %f max int violation %f max var bounds violation %f " + "nodes %d simplex_iterations %d", + objective_, + mip_gap_, + stats_.get_solution_bound(), + stats_.presolve_time, + stats_.total_solve_time, + max_constraint_violation_, + max_int_violation_, + max_variable_bound_violation_, + stats_.num_nodes, + stats_.num_simplex_iterations); +} + #if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT template class mip_solution_t; #endif From 32cca6235f177b9f3d8af78dd2c8e628ecd567b4 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Apr 2026 09:11:06 -0700 Subject: [PATCH 06/25] address chris' comments (thanks!) --- cpp/src/mip_heuristics/solve.cu | 24 ++++++++++++++++-------- cpp/src/mip_heuristics/solver.cu | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 00cf923ef0..df046344c2 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -208,20 +208,28 @@ mip_solution_t run_mip(detail::problem_t& problem, settings.determinism_mode != CUOPT_MODE_DETERMINISTIC && problem.original_problem_ptr->get_n_integers() > 0; if (run_early_cpufj) { + auto early_fj_start = std::chrono::steady_clock::now(); auto* presolver_ptr = problem.presolve_data.papilo_presolve_ptr; auto mip_callbacks = settings.get_mip_callbacks(); f_t no_bound = problem.presolve_data.objective_scaling_factor >= 0 ? (f_t)-1e20 : (f_t)1e20; - auto incumbent_callback = [presolver_ptr, mip_callbacks, no_bound, ctx_ptr = &solver.context]( - f_t solver_obj, - f_t user_obj, - const std::vector& assignment, - const char* heuristic_name) { + auto incumbent_callback = [presolver_ptr, + mip_callbacks, + no_bound, + ctx_ptr = &solver.context, + early_fj_start](f_t solver_obj, + f_t user_obj, + const std::vector& assignment, + const char* heuristic_name) { std::vector user_assignment; presolver_ptr->uncrush_primal_solution(assignment, user_assignment); ctx_ptr->initial_incumbent_assignment = user_assignment; ctx_ptr->initial_upper_bound = user_obj; - CUOPT_LOG_INFO( - "New solution from early %s (presolved). Objective %g", heuristic_name, user_obj); + double elapsed = + std::chrono::duration(std::chrono::steady_clock::now() - early_fj_start).count(); + CUOPT_LOG_INFO("New solution from early primal heuristics (%s). Objective %+.6e. Time %.2f", + heuristic_name, + user_obj, + elapsed); invoke_solution_callbacks(mip_callbacks, user_obj, user_assignment, no_bound); }; early_cpufj = std::make_unique>( @@ -386,7 +394,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, early_best_user_assignment = assignment; double elapsed = std::chrono::duration(std::chrono::steady_clock::now() - early_fj_start).count(); - CUOPT_LOG_INFO("New solution from early primal heuristics (%s). Objective %g. Time %.2f", + CUOPT_LOG_INFO("New solution from early primal heuristics (%s). Objective %+.6e. Time %.2f", heuristic_name, user_obj, elapsed); diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index d75f41cd75..f6a4a23b50 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -221,8 +221,8 @@ solution_t mip_solver_t::run_solver() if (context.early_cpufj_ptr) { context.early_cpufj_ptr->stop(); if (context.early_cpufj_ptr->solution_found()) { - CUOPT_LOG_INFO("Early CPUFJ found incumbent with user-space objective %g during presolve", - context.early_cpufj_ptr->get_best_user_objective()); + CUOPT_LOG_DEBUG("Early CPUFJ found incumbent with user-space objective %g during presolve", + context.early_cpufj_ptr->get_best_user_objective()); } } From 35dc789589cee7771576936e2b4e591f470e43b7 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Apr 2026 10:20:56 -0700 Subject: [PATCH 07/25] fix grpc test using very large coefficients instead of infinity --- cpp/tests/linear_programming/grpc/grpc_integration_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp index fe1db25490..dceb459eec 100644 --- a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp @@ -409,7 +409,7 @@ class GrpcIntegrationTestBase : public ::testing::Test { problem.set_variable_types(var_types.data(), 2); std::vector con_lb = {1.0}; - std::vector con_ub = {1e20}; + std::vector con_ub = {std::numeric_limits::infinity()}; problem.set_constraint_lower_bounds(con_lb.data(), 1); problem.set_constraint_upper_bounds(con_ub.data(), 1); From 980cd2427e3e5a2a0b65d9d0d9d8bef409f29f6c Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 01:34:01 -0700 Subject: [PATCH 08/25] stop rins before the population flush to avoid missing on new solutions --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 2 ++ cpp/src/mip_heuristics/solver.cu | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index e821c016c2..764c40eb5d 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -603,12 +603,14 @@ solution_t diversity_manager_t::run_solver() generate_solution(timer.remaining_time(), false); if (timer.check_time_limit()) { + dm.rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); } if (check_b_b_preemption()) { return population.best_feasible(); } run_fp_alone(); + dm.rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); }; diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index bb634ba730..816c403036 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -487,7 +487,6 @@ solution_t mip_solver_t::run_solver() } context.stats.total_solve_time = timer_.elapsed_time(); context.problem_ptr->post_process_solution(sol); - dm.rins.stop_rins(); return sol; } From fcc3cd2665cbb3f027bab953f8c4ece49b8cb528 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 01:34:20 -0700 Subject: [PATCH 09/25] bump1 --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index e01e533a65..d174a872f6 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -220,7 +220,7 @@ int run_single_file(std::string file_path, auto start_run_solver = std::chrono::high_resolution_clock::now(); auto solution = cuopt::linear_programming::solve_mip(&handle_, mps_data_model, settings); CUOPT_LOG_INFO( - "first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", + "1first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", benchmark_info.objective_of_initial_population, benchmark_info.last_improvement_of_best_feasible, benchmark_info.last_improvement_after_recombination); From d2fba9652fc117f3deedfdc9a4723a1667be2cb8 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 01:36:23 -0700 Subject: [PATCH 10/25] bump2 --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index d174a872f6..4b070a7c90 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -220,7 +220,7 @@ int run_single_file(std::string file_path, auto start_run_solver = std::chrono::high_resolution_clock::now(); auto solution = cuopt::linear_programming::solve_mip(&handle_, mps_data_model, settings); CUOPT_LOG_INFO( - "1first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", + "2first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", benchmark_info.objective_of_initial_population, benchmark_info.last_improvement_of_best_feasible, benchmark_info.last_improvement_after_recombination); From 4fc995390e6bf346316245917c2f3e9e5499135f Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 01:36:40 -0700 Subject: [PATCH 11/25] bump3 --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 4b070a7c90..e01e533a65 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -220,7 +220,7 @@ int run_single_file(std::string file_path, auto start_run_solver = std::chrono::high_resolution_clock::now(); auto solution = cuopt::linear_programming::solve_mip(&handle_, mps_data_model, settings); CUOPT_LOG_INFO( - "2first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", + "first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", benchmark_info.objective_of_initial_population, benchmark_info.last_improvement_of_best_feasible, benchmark_info.last_improvement_after_recombination); From 751b8c4e3b879354d6fa11732b50dfed71cf827d Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 02:37:09 -0700 Subject: [PATCH 12/25] fix compilation --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 764c40eb5d..30dfa58501 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -603,14 +603,14 @@ solution_t diversity_manager_t::run_solver() generate_solution(timer.remaining_time(), false); if (timer.check_time_limit()) { - dm.rins.stop_rins(); + rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); } if (check_b_b_preemption()) { return population.best_feasible(); } run_fp_alone(); - dm.rins.stop_rins(); + rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); }; From c3f6b6f5cb4982b4ce2032fa3abf45362931bd4e Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 02:45:43 -0700 Subject: [PATCH 13/25] bump1 --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index e01e533a65..d174a872f6 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -220,7 +220,7 @@ int run_single_file(std::string file_path, auto start_run_solver = std::chrono::high_resolution_clock::now(); auto solution = cuopt::linear_programming::solve_mip(&handle_, mps_data_model, settings); CUOPT_LOG_INFO( - "first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", + "1first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", benchmark_info.objective_of_initial_population, benchmark_info.last_improvement_of_best_feasible, benchmark_info.last_improvement_after_recombination); From a4954b54e6df8569af883e4aa9b1bd204745e6ca Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 02:46:02 -0700 Subject: [PATCH 14/25] bump2 --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index d174a872f6..4b070a7c90 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -220,7 +220,7 @@ int run_single_file(std::string file_path, auto start_run_solver = std::chrono::high_resolution_clock::now(); auto solution = cuopt::linear_programming::solve_mip(&handle_, mps_data_model, settings); CUOPT_LOG_INFO( - "1first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", + "2first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", benchmark_info.objective_of_initial_population, benchmark_info.last_improvement_of_best_feasible, benchmark_info.last_improvement_after_recombination); From fee8edbfc8dbb3ebfc333b3fa67d776f2fa7b30d Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Sat, 4 Apr 2026 02:46:24 -0700 Subject: [PATCH 15/25] bump3 --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 4b070a7c90..e01e533a65 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -220,7 +220,7 @@ int run_single_file(std::string file_path, auto start_run_solver = std::chrono::high_resolution_clock::now(); auto solution = cuopt::linear_programming::solve_mip(&handle_, mps_data_model, settings); CUOPT_LOG_INFO( - "2first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", + "first obj: %f last improvement of best feasible: %f last improvement after recombination: %f", benchmark_info.objective_of_initial_population, benchmark_info.last_improvement_of_best_feasible, benchmark_info.last_improvement_after_recombination); From 2202b88e9c0f89989d910f02721e67543e133fa5 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Mon, 6 Apr 2026 08:15:26 -0700 Subject: [PATCH 16/25] missing dataset --- datasets/mip/download_miplib_test_dataset.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/datasets/mip/download_miplib_test_dataset.sh b/datasets/mip/download_miplib_test_dataset.sh index 3040f0f543..d9cefbc32d 100755 --- a/datasets/mip/download_miplib_test_dataset.sh +++ b/datasets/mip/download_miplib_test_dataset.sh @@ -25,6 +25,7 @@ INSTANCES=( "enlight_hard" "enlight11" "supportcase22" + "pk1" ) BASE_URL="https://miplib.zib.de/WebData/instances" From c3e9c2bb9bfa455873fa90676bde6475123be9eb Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Mon, 6 Apr 2026 08:18:58 -0700 Subject: [PATCH 17/25] cleanup --- cpp/src/mip_heuristics/solve.cu | 4 ---- cpp/src/mip_heuristics/solver.cu | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 3005598358..be01516657 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -237,10 +237,6 @@ mip_solution_t run_mip(detail::problem_t& problem, initial_upper_bound = solver.context.initial_upper_bound; initial_incumbent_assignment = solver.context.initial_incumbent_assignment; - // Write back the (possibly updated) incumbent from the papilo-phase callback. - initial_upper_bound = solver.context.initial_upper_bound; - initial_incumbent_assignment = solver.context.initial_incumbent_assignment; - int hidesol = std::getenv("CUOPT_MIP_HIDE_SOLUTION") ? atoi(std::getenv("CUOPT_MIP_HIDE_SOLUTION")) : 0; if (!hidesol) { detail::print_solution(scaled_problem.handle_ptr, sol.get_solution()); } diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 816c403036..ce6b602fba 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -216,7 +216,7 @@ solution_t mip_solver_t::run_solver() CUOPT_LOG_DEBUG("Presolve time limit: %g", presolve_time_limit); bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit, timer_) : true; - // Stop early CPUFJ after cuopt presolve (probing cache) but before main solve. + // Stop early CPUFJ after cuopt presolve (probing cache) but before main solve if (context.early_cpufj_ptr) { context.early_cpufj_ptr->stop(); if (context.early_cpufj_ptr->solution_found()) { @@ -478,6 +478,7 @@ solution_t mip_solver_t::run_solver() rmm::device_scalar is_feasible(sol.handle_ptr->get_stream()); sol.test_variable_bounds(true, is_feasible.data()); + // test_variable_bounds clears is_feasible if the test is failed if (!is_feasible.value(sol.handle_ptr->get_stream())) { CUOPT_LOG_ERROR( "Solution is not feasible due to variable bounds, returning infeasible solution!"); From 64720581df72fc74c7376815bf0e7afc432a9d1c Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Mon, 6 Apr 2026 08:56:33 -0700 Subject: [PATCH 18/25] ai review comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 27 +++++++++---------- cpp/src/branch_and_bound/branch_and_bound.hpp | 9 +++++++ .../diversity/diversity_manager.cu | 3 +-- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index a7f6b95a79..370ecdae19 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -475,10 +475,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu mutex_original_lp_.unlock(); bool is_feasible = false; bool attempt_repair = false; - mutex_upper_.lock(); - f_t current_upper_bound = upper_bound_; - mutex_upper_.unlock(); - if (obj < current_upper_bound) { + if (!incumbent_.has_incumbent || obj < incumbent_.objective) { f_t primal_err; f_t bound_err; i_t num_fractional; @@ -493,8 +490,8 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu original_lp_, settings_, var_types_, crushed_solution, primal_err, bound_err, num_fractional); mutex_original_lp_.unlock(); mutex_upper_.lock(); - if (is_feasible && obj < upper_bound_) { - upper_bound_ = obj; + if (is_feasible && improves_incumbent(obj)) { + upper_bound_ = std::min(upper_bound_.load(), obj); incumbent_.set_incumbent_solution(obj, crushed_solution); } else { attempt_repair = true; @@ -654,8 +651,8 @@ void branch_and_bound_t::repair_heuristic_solutions() if (is_feasible) { mutex_upper_.lock(); - if (repaired_obj < upper_bound_) { - upper_bound_ = repaired_obj; + if (improves_incumbent(repaired_obj)) { + upper_bound_ = std::min(upper_bound_.load(), repaired_obj); incumbent_.set_incumbent_solution(repaired_obj, repaired_solution); report_heuristic(repaired_obj); @@ -792,9 +789,9 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, compute_user_objective(original_lp_, leaf_objective)); mutex_upper_.lock(); - if (leaf_objective < upper_bound_) { + if (improves_incumbent(leaf_objective)) { incumbent_.set_incumbent_solution(leaf_objective, leaf_solution); - upper_bound_ = leaf_objective; + upper_bound_ = std::min(upper_bound_.load(), leaf_objective); report(feasible_solution_symbol(thread_type), leaf_objective, get_lower_bound(), leaf_depth, 0); send_solution = true; } @@ -802,7 +799,7 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, if (send_solution && settings_.solution_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, original_x); - settings_.solution_callback(original_x, upper_bound_); + settings_.solution_callback(original_x, leaf_objective); } mutex_upper_.unlock(); } @@ -3286,8 +3283,8 @@ void branch_and_bound_t::deterministic_process_worker_solutions( deterministic_current_horizon_); bool improved = false; - if (sol->objective < upper_bound_) { - upper_bound_ = sol->objective; + if (improves_incumbent(sol->objective)) { + upper_bound_ = std::min(upper_bound_.load(), sol->objective); incumbent_.set_incumbent_solution(sol->objective, sol->solution); current_upper = sol->objective; improved = true; @@ -3449,8 +3446,8 @@ void branch_and_bound_t::deterministic_sort_replay_events( // Process heuristic solution at its correct work unit timestamp position f_t new_upper = std::numeric_limits::infinity(); - if (hsol.objective < upper_bound_) { - upper_bound_ = hsol.objective; + if (improves_incumbent(hsol.objective)) { + upper_bound_ = std::min(upper_bound_.load(), hsol.objective); incumbent_.set_incumbent_solution(hsol.objective, hsol.solution); new_upper = hsol.objective; } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 0f86040462..f2917ba930 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -200,6 +200,15 @@ class branch_and_bound_t { // Solver-space incumbent tracked directly by B&B. mip_solution_t incumbent_; + // Whether obj should replace the stored incumbent. Must be called under mutex_upper_. + // Compares against the stored incumbent's objective, NOT against upper_bound_, because + // set_initial_upper_bound can set a tighter bound from an OG-space solution that has no + // corresponding solver-space incumbent (e.g. papilo can't crush it back). + bool improves_incumbent(f_t obj) const + { + return !incumbent_.has_incumbent || obj < incumbent_.objective; + } + // Structure with the general info of the solver. branch_and_bound_stats_t exploration_stats_; diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 30dfa58501..04c19697ac 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -600,17 +600,16 @@ solution_t diversity_manager_t::run_solver() return sol; } rins.enable(); + cuopt::scope_guard rins_guard([&]() { rins.stop_rins(); }); generate_solution(timer.remaining_time(), false); if (timer.check_time_limit()) { - rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); } if (check_b_b_preemption()) { return population.best_feasible(); } run_fp_alone(); - rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); }; From e0398515cc787b0f76f4c4dffa21a3aa9aee7f76 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 7 Apr 2026 01:39:41 -0700 Subject: [PATCH 19/25] AI review comment --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 04c19697ac..f189f4b3db 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -600,16 +600,20 @@ solution_t diversity_manager_t::run_solver() return sol; } rins.enable(); - cuopt::scope_guard rins_guard([&]() { rins.stop_rins(); }); generate_solution(timer.remaining_time(), false); if (timer.check_time_limit()) { + rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); } - if (check_b_b_preemption()) { return population.best_feasible(); } + if (check_b_b_preemption()) { + rins.stop_rins(); + return population.best_feasible(); + } run_fp_alone(); + rins.stop_rins(); population.add_external_solutions_to_population(); return population.best_feasible(); }; From 4109e004ecb5aa48cb4bb6fee09c8b6eb347000a Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 7 Apr 2026 01:52:57 -0700 Subject: [PATCH 20/25] oversight --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index f189f4b3db..b8dc3d33bf 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -609,6 +609,7 @@ solution_t diversity_manager_t::run_solver() } if (check_b_b_preemption()) { rins.stop_rins(); + population.add_external_solutions_to_population(); return population.best_feasible(); } From 14b86e40e7361bb6d1df59e48c6836b4731437ca Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 7 Apr 2026 10:19:28 -0500 Subject: [PATCH 21/25] Empty commit From 0e5d7557dfd58a1104eec179b4d97249e3607808 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 7 Apr 2026 10:40:14 -0500 Subject: [PATCH 22/25] fix grpc versions --- conda/environments/all_cuda-129_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-129_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-131_arch-aarch64.yaml | 2 +- conda/environments/all_cuda-131_arch-x86_64.yaml | 2 +- conda/recipes/libcuopt/recipe.yaml | 8 ++++---- dependencies.yaml | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 3f87fff34b..970b16fdfa 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -34,7 +34,7 @@ dependencies: - libcurand-dev - libcusolver-dev - libcusparse-dev -- libgrpc +- libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libraft-headers==26.4.*,>=0.0.0a0 - librmm==26.4.*,>=0.0.0a0 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 490e3798cb..96d571087e 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -34,7 +34,7 @@ dependencies: - libcurand-dev - libcusolver-dev - libcusparse-dev -- libgrpc +- libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libraft-headers==26.4.*,>=0.0.0a0 - librmm==26.4.*,>=0.0.0a0 diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index bf7b0de734..1e25d9b39e 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -34,7 +34,7 @@ dependencies: - libcurand-dev - libcusolver-dev - libcusparse-dev -- libgrpc +- libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libraft-headers==26.4.*,>=0.0.0a0 - librmm==26.4.*,>=0.0.0a0 diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index 6f554809b1..6beee00f5b 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -34,7 +34,7 @@ dependencies: - libcurand-dev - libcusolver-dev - libcusparse-dev -- libgrpc +- libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libraft-headers==26.4.*,>=0.0.0a0 - librmm==26.4.*,>=0.0.0a0 diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 789ab55c33..0b2516c904 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -93,7 +93,7 @@ cache: - bzip2 - openssl - c-ares - - libgrpc + - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libabseil - re2 @@ -174,7 +174,7 @@ outputs: - openssl - c-ares - libuuid - - libgrpc + - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libabseil - re2 @@ -188,7 +188,7 @@ outputs: - openssl - c-ares - libuuid - - libgrpc + - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libabseil ignore_run_exports: @@ -235,7 +235,7 @@ outputs: - libcusparse-dev - openssl - c-ares - - libgrpc + - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libabseil run: diff --git a/dependencies.yaml b/dependencies.yaml index 84dc5eed00..6a9fe8dd59 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -304,7 +304,7 @@ dependencies: - bzip2 - openssl - c-ares - - libgrpc + - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libabseil - re2 From 3c9b1d145cbdd0f58c6af7bca0419ed7d2f2a55f Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 7 Apr 2026 10:47:08 -0500 Subject: [PATCH 23/25] update ignore run exports for mps --- conda/recipes/libcuopt/recipe.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 0b2516c904..692b7ccf50 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -127,14 +127,22 @@ outputs: - bzip2 ignore_run_exports: by_name: + - c-ares - cuda-nvtx - cuda-version - - libcurand + - libabseil + - libboost - libcudss + - libcurand - libcusparse + - libgrpc + - libprotobuf - librmm - - libzlib - libbz2 + - libzlib + - openssl + - re2 + - tbb tests: - package_contents: files: From 19f54b92c4f6ee2ec6cefc6c3b77eb6d9c26f45e Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 7 Apr 2026 11:13:21 -0500 Subject: [PATCH 24/25] fix rbb overlinking failuret --- conda/recipes/libcuopt/recipe.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 692b7ccf50..442bd8581c 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -199,6 +199,7 @@ outputs: - libgrpc >=1.78.0,<1.80.0a0 - libprotobuf - libabseil + - tbb ignore_run_exports: by_name: - cuda-nvtx From 5f12bf33059ed7b8ed81d7b2c76ebbb1bbcbd919 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 7 Apr 2026 11:43:06 -0500 Subject: [PATCH 25/25] fix tbb --- conda/recipes/libcuopt/recipe.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 442bd8581c..682f9d33ef 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -186,6 +186,7 @@ outputs: - libprotobuf - libabseil - re2 + - tbb-devel run: - ${{ pin_compatible("cuda-version", upper_bound="x", lower_bound="x") }} - ${{ pin_subpackage("libmps-parser", exact=True) }}