Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a0ce987
initial commit
aliceb-nv Apr 1, 2026
5e06221
Merge branch 'release/26.04' into no-cutoff-bb
aliceb-nv Apr 3, 2026
cfa1758
remove separate cutoff value
aliceb-nv Apr 3, 2026
cc7e99f
ai review comments
aliceb-nv Apr 3, 2026
62b1ba5
ai review comments
aliceb-nv Apr 3, 2026
15aa2d8
fix duplicate log line
aliceb-nv Apr 3, 2026
32cca62
address chris' comments (thanks!)
aliceb-nv Apr 3, 2026
35dc789
fix grpc test using very large coefficients instead of infinity
aliceb-nv Apr 3, 2026
a1ef25e
Merge branch 'release/26.04' into no-cutoff-bb
aliceb-nv Apr 4, 2026
980cd24
stop rins before the population flush to avoid missing on new solutions
aliceb-nv Apr 4, 2026
fcc3cd2
bump1
aliceb-nv Apr 4, 2026
d2fba96
bump2
aliceb-nv Apr 4, 2026
4fc9953
bump3
aliceb-nv Apr 4, 2026
751b8c4
fix compilation
aliceb-nv Apr 4, 2026
c3f6b6f
bump1
aliceb-nv Apr 4, 2026
a4954b5
bump2
aliceb-nv Apr 4, 2026
fee8edb
bump3
aliceb-nv Apr 4, 2026
02b794a
Merge branch 'release/26.04' into no-cutoff-bb
aliceb-nv Apr 6, 2026
2202b88
missing dataset
aliceb-nv Apr 6, 2026
c3e9c2b
cleanup
aliceb-nv Apr 6, 2026
6472058
ai review comments
aliceb-nv Apr 6, 2026
e039851
AI review comment
aliceb-nv Apr 7, 2026
4109e00
oversight
aliceb-nv Apr 7, 2026
6357f8a
Merge branch 'release/26.04' into no-cutoff-bb
aliceb-nv Apr 7, 2026
14b86e4
Empty commit
rgsl888prabhu Apr 7, 2026
0e5d755
fix grpc versions
rgsl888prabhu Apr 7, 2026
3c9b1d1
update ignore run exports for mps
rgsl888prabhu Apr 7, 2026
19f54b9
fix rbb overlinking failuret
rgsl888prabhu Apr 7, 2026
5f12bf3
fix tbb
rgsl888prabhu Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-129_arch-aarch64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-129_arch-x86_64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-131_arch-aarch64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion conda/environments/all_cuda-131_arch-x86_64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions conda/recipes/libcuopt/recipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ cache:
- bzip2
- openssl
- c-ares
- libgrpc
- libgrpc >=1.78.0,<1.80.0a0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pin libgrpc >=1.78.0,<1.80.0a0 to avoid version conflicts with RAPIDS packages (libarrow, libopentelemetry-cpp) that require libgrpc 1.78.x.

What specifically were the conflicts? Would it be sufficient to just make this a floor and not include a ceiling?

A tight pin like this is going to cause environment conflicts, especially in the all-of-RAPIDS conda environment in the devcontainers: https://github.com/rapidsai/devcontainers/blob/51ebbe3e6ae8120b9aba229729ef322bc18bbd4a/.github/workflows/test.yml#L29

I'd love the opportunity to help you find a less-restrictive fix.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue was libcuopt built with latest libgrpc 1.8x, but pyarrow used libgrpc 1.7x, so while testing they diverged and started breaking due to dependency issue. https://github.com/NVIDIA/cuopt/actions/runs/24077889439/job/70243128275?pr=1033

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oy 😫

Repeating what I wrote privately so we have it here in GitHub, I guess it's this part?

│  │     │  │     └─ pyarrow [20.0.0|21.0.0|22.0.0|23.0.0|23.0.1] would require
    │  │     │  │        ├─ libparquet [=20.0.0 *|=21.0.0 *|=22.0.0 *|=23.0.1 *] with the potential options
...
    │  │     │  │        │  └─ libparquet [20.0.0|21.0.0|22.0.0|23.0.1] would require
    │  │     │  │        │     └─ libarrow [==20.0.0 h54ffee7_42_cuda|==20.0.0 h6053680_44_cuda|...|==23.0.1 he2a4bb3_8_cuda], which requires
    │  │     │  │        │        ├─ libgoogle-cloud >=3.3.0,<3.4.0a0 * with the potential options
    │  │     │  │        │        │  ├─ libgoogle-cloud 3.3.0 would require
    │  │     │  │        │        │  │  └─ libgrpc >=1.78.1,<1.79.0a0 *, which can be installed;
    │  │     │  │        │        │  └─ libgoogle-cloud 3.3.0 would require
...
    │  │     └─ libcuopt =26.4.0a196 *, which requires
    │  │        ├─ libabseil >=20260107.1,<20260108.0a0 *, which conflicts with any installable versions previously reported;
    │  │        └─ libgrpc >=1.80.0,<1.81.0a0 *, which conflicts with any installable versions previously reported;

cuopt picks up a very-new libgrpc (looks like 1.80.0 was released earlier today), and that's fine at build time but causes issues at runtime because cudf needs pyarrow and pyarrow's dependencies eventually need libgrpc and only support older versions.

blegh let me think a bit about how we could work around this. Please feel free to merge this PR for now if it's blocking other work, we can fix the pin in a follow-up.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @jameslamb

- libprotobuf
- libabseil
- re2
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -174,10 +182,11 @@ outputs:
- openssl
- c-ares
- libuuid
- libgrpc
- libgrpc >=1.78.0,<1.80.0a0
- libprotobuf
- libabseil
- re2
- tbb-devel
run:
- ${{ pin_compatible("cuda-version", upper_bound="x", lower_bound="x") }}
- ${{ pin_subpackage("libmps-parser", exact=True) }}
Expand All @@ -188,9 +197,10 @@ outputs:
- openssl
- c-ares
- libuuid
- libgrpc
- libgrpc >=1.78.0,<1.80.0a0
- libprotobuf
- libabseil
- tbb
ignore_run_exports:
by_name:
- cuda-nvtx
Expand Down Expand Up @@ -235,7 +245,7 @@ outputs:
- libcusparse-dev
- openssl
- c-ares
- libgrpc
- libgrpc >=1.78.0,<1.80.0a0
- libprotobuf
- libabseil
run:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class mip_solution_t : public base_solution_t {
const std::vector<std::string>& get_variable_names() const;
const std::vector<rmm::device_uvector<f_t>>& 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:
Expand Down
78 changes: 41 additions & 37 deletions cpp/src/branch_and_bound/branch_and_bound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,12 @@ f_t branch_and_bound_t<i_t, f_t>::get_lower_bound()
}
}

template <typename i_t, typename f_t>
void branch_and_bound_t<i_t, f_t>::set_initial_upper_bound(f_t bound)
{
upper_bound_ = bound;
}

template <typename i_t, typename f_t>
void branch_and_bound_t<i_t, f_t>::report_heuristic(f_t obj)
{
Expand Down Expand Up @@ -469,10 +475,7 @@ void branch_and_bound_t<i_t, f_t>::set_new_solution(const std::vector<f_t>& 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;
Expand All @@ -487,8 +490,8 @@ void branch_and_bound_t<i_t, f_t>::set_new_solution(const std::vector<f_t>& 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;
Expand Down Expand Up @@ -648,8 +651,8 @@ void branch_and_bound_t<i_t, f_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);

Expand Down Expand Up @@ -735,7 +738,9 @@ void branch_and_bound_t<i_t, f_t>::set_final_solution(mip_solution_t<i_t, f_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",
Expand All @@ -762,11 +767,10 @@ void branch_and_bound_t<i_t, f_t>::set_final_solution(mip_solution_t<i_t, f_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.objective = incumbent_.objective;
solution.lower_bound = lower_bound;
solution.nodes_explored = exploration_stats_.nodes_explored;
solution.simplex_iterations = exploration_stats_.total_lp_iters;
Expand All @@ -785,17 +789,17 @@ void branch_and_bound_t<i_t, f_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;
}

if (send_solution && settings_.solution_callback != nullptr) {
std::vector<f_t> 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();
}
Expand Down Expand Up @@ -921,7 +925,7 @@ struct nondeterministic_policy_t : tree_update_policy_t<i_t, f_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<i_t, f_t>* node, f_t leaf_obj) override
{
Expand Down Expand Up @@ -1339,7 +1343,7 @@ dual::status_t branch_and_bound_t<i_t, f_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 {
Expand Down Expand Up @@ -1452,7 +1456,7 @@ void branch_and_bound_t<i_t, f_t>::plunge_with(branch_and_bound_worker_t<i_t, f_
// - The lower bound of the parent is lower or equal to its children
worker->lower_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;
Expand Down Expand Up @@ -1590,7 +1594,7 @@ void branch_and_bound_t<i_t, f_t>::dive_with(branch_and_bound_worker_t<i_t, f_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;
Expand Down Expand Up @@ -1649,7 +1653,7 @@ void branch_and_bound_t<i_t, f_t>::run_scheduler()
diving_heuristics_settings_t<i_t, f_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<search_strategy_t> strategies = get_search_strategies(diving_settings);
std::array<i_t, num_search_strategies> max_num_workers_per_type =
get_max_workers(num_workers, strategies);
Expand Down Expand Up @@ -1682,7 +1686,7 @@ void branch_and_bound_t<i_t, f_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);
Expand Down Expand Up @@ -1733,7 +1737,7 @@ void branch_and_bound_t<i_t, f_t>::run_scheduler()
std::optional<mip_node_t<i_t, f_t>*> 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(
Expand All @@ -1757,7 +1761,7 @@ void branch_and_bound_t<i_t, f_t>::run_scheduler()
std::optional<mip_node_t<i_t, f_t>*> 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;
}
Expand Down Expand Up @@ -1831,7 +1835,7 @@ void branch_and_bound_t<i_t, f_t>::single_threaded_solve()
std::optional<mip_node_t<i_t, f_t>*> 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(
Expand Down Expand Up @@ -2331,12 +2335,12 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_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<f_t> lower_bounds;
std::vector<f_t> 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;
Expand Down Expand Up @@ -2468,7 +2472,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_);
f_t abs_gap = compute_user_abs_gap(original_lp_, 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;
}
Expand Down Expand Up @@ -2528,10 +2532,10 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_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<f_t> lower_bounds;
std::vector<f_t> 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<bool> bounds_changed(original_lp_.num_cols, true);
std::vector<char> row_sense;
Expand Down Expand Up @@ -2636,7 +2640,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
std::optional<mip_node_t<i_t, f_t>*> 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(
Expand Down Expand Up @@ -3279,8 +3283,8 @@ void branch_and_bound_t<i_t, f_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;
Expand Down Expand Up @@ -3442,8 +3446,8 @@ void branch_and_bound_t<i_t, f_t>::deterministic_sort_replay_events(
// Process heuristic solution at its correct work unit timestamp position
f_t new_upper = std::numeric_limits<f_t>::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;
}
Expand Down Expand Up @@ -3477,7 +3481,7 @@ void branch_and_bound_t<i_t, f_t>::deterministic_sort_replay_events(
template <typename i_t, typename f_t>
void branch_and_bound_t<i_t, f_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
Expand Down Expand Up @@ -3613,7 +3617,7 @@ void branch_and_bound_t<i_t, f_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<std::pair<mip_node_t<i_t, f_t>*, f_t>> candidates;
Expand Down
Loading
Loading