Skip to content

Commit 9fd1763

Browse files
authored
Unified pseudocost object for the regular and deterministic mode (#1020)
This PR simplify the pseudo cost class in such a way that both regular and deterministic B&B use the same code path as much as possible. It also disable mutexes and atomics when running in deterministic mode as each thread has its own snapshot of the pseudocost. It also move the routines related with the diving heuristics back to the `diving_heuristics.cpp`, renamed `branch_and_bound_worker.hpp` to `worker.hpp` to match the new file structure and moved `worker_pool_t` to a dedicated header. Regular mode (GH200, 10min): ``` ================================================================================ main-190326-2 (1) vs simplify-pseudocost (2) ================================================================================ ------------------------------------------------------------------------------------------------------------------------------ | | Run 1 | Run 2 | Abs. Diff. | Rel. Diff. (%) | ------------------------------------------------------------------------------------------------------------------------------ | Feasible 226 226 +0 --- | | Optimal 70 67 -3 --- | | Solutions with <0.1% primal gap 121 122 +1 --- | | Nodes explored (mean) 4283972.9121 4455377.8117 +171404.8996 +3.847 | | Nodes explored (shifted geomean) 6202.3471 7062.2682 +859.9210 +12.176 | | Relative MIP gap (mean) 0.3382 0.3337 -0.0045 -1.325 | | Relative MIP gap (shifted geomean) 0.1193 0.1166 -0.0027 -2.293 | | Solve time (mean) 450.2347 452.9154 +2.6806 +0.592 | | Solve time (shifted geomean) 221.4772 227.6381 +6.1609 +2.706 | | Primal gap (mean) 11.4459 11.0482 -0.3976 -3.474 | | Primal gap (shifted geomean) 0.6591 0.6008 -0.0582 -8.838 | | Primal integral (mean) 49.9109 54.6941 +4.7832 +8.745 | | Primal integral (shifted geomean) 11.5672 13.9826 +2.4153 +17.274 | ------------------------------------------------------------------------------------------------------------------------------ ``` Determinism mode (GH200, 5min): ``` ================================================================================ main-240426-determinism (1) vs simplify-pseudocost-determinism (2) ================================================================================ ------------------------------------------------------------------------------------------------------------------------------ | | Run 1 | Run 2 | Abs. Diff. | Rel. Diff. (%) | ------------------------------------------------------------------------------------------------------------------------------ | Feasible 179 179 +0 --- | | Optimal 45 46 +1 --- | | Solutions with <0.1% primal gap 64 64 +0 --- | | Nodes explored (mean) 1.556e+06 1.511e+06 -4.526e+04 -2.91 | | Nodes explored (shifted geomean) 1895 1900 +5.427 +0.286 | | Relative MIP gap (mean) 7.038 0.7827 -6.255 -88.9 | | Relative MIP gap (shifted geomean) 0.2039 0.1841 -0.01984 -9.73 | | Solve time (mean) 249.5 251.7 +2.153 +0.855 | | Solve time (shifted geomean) 153.8 160.6 +6.767 +4.22 | | Primal gap (mean) 39.8 39.43 -0.3723 -0.935 | | Primal gap (shifted geomean) 5.315 5.314 -0.001059 -0.0199 | | Primal integral (mean) 292 299.2 +7.201 +2.41 | | Primal integral (shifted geomean) 49.42 50.91 +1.486 +2.92 | ------------------------------------------------------------------------------------------------------------------------------ ``` Authors: - Nicolas L. Guidotti (https://github.com/nguidotti) Approvers: - Alice Boucher (https://github.com/aliceb-nv) - Trevor McKay (https://github.com/tmckayus) - Chris Maes (https://github.com/chris-maes) URL: #1020
1 parent 285990b commit 9fd1763

13 files changed

Lines changed: 705 additions & 890 deletions

cpp/src/branch_and_bound/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
set(BRANCH_AND_BOUND_SRC_FILES
77
${CMAKE_CURRENT_SOURCE_DIR}/branch_and_bound.cpp
8-
${CMAKE_CURRENT_SOURCE_DIR}/mip_node.cpp
98
${CMAKE_CURRENT_SOURCE_DIR}/pseudo_costs.cpp
109
${CMAKE_CURRENT_SOURCE_DIR}/diving_heuristics.cpp
1110
)

cpp/src/branch_and_bound/branch_and_bound.cpp

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/* clang-format on */
77

88
#include <branch_and_bound/branch_and_bound.hpp>
9+
#include <branch_and_bound/diving_heuristics.hpp>
910
#include <branch_and_bound/mip_node.hpp>
1011
#include <branch_and_bound/pseudo_costs.hpp>
1112

@@ -35,15 +36,12 @@
3536
#include <deque>
3637
#include <future>
3738
#include <limits>
38-
#include <map>
3939
#include <optional>
4040
#include <string>
4141
#include <thread>
42-
#include <unordered_map>
4342
#include <vector>
4443

4544
namespace cuopt::linear_programming::dual_simplex {
46-
4745
namespace {
4846

4947
template <typename f_t>
@@ -258,7 +256,7 @@ branch_and_bound_t<i_t, f_t>::branch_and_bound_t(
258256
incumbent_(1),
259257
root_relax_soln_(1, 1),
260258
root_crossover_soln_(1, 1),
261-
pc_(1),
259+
pc_(1, solver_settings),
262260
solver_status_(mip_status_t::UNSET)
263261
{
264262
exploration_stats_.start_time = start_time;
@@ -810,7 +808,7 @@ void branch_and_bound_t<i_t, f_t>::add_feasible_solution(f_t leaf_objective,
810808
// Technische Universit¨at Berlin, Berlin, 1999. Accessed: Aug. 08, 2025.
811809
// [Online]. Available: https://opus4.kobv.de/opus4-zib/frontdoor/index/index/docId/391
812810
template <typename f_t>
813-
rounding_direction_t martin_criteria(f_t val, f_t root_val)
811+
branch_direction_t martin_criteria(f_t val, f_t root_val)
814812
{
815813
const f_t down_val = std::floor(root_val);
816814
const f_t up_val = std::ceil(root_val);
@@ -819,10 +817,10 @@ rounding_direction_t martin_criteria(f_t val, f_t root_val)
819817
constexpr f_t eps = 1e-6;
820818

821819
if (down_dist < up_dist + eps) {
822-
return rounding_direction_t::DOWN;
820+
return branch_direction_t::DOWN;
823821

824822
} else {
825-
return rounding_direction_t::UP;
823+
return branch_direction_t::UP;
826824
}
827825
}
828826

@@ -833,9 +831,9 @@ branch_variable_t<i_t> branch_and_bound_t<i_t, f_t>::variable_selection(
833831
branch_and_bound_worker_t<i_t, f_t>* worker)
834832
{
835833
logger_t log;
836-
log.log = false;
837-
i_t branch_var = -1;
838-
rounding_direction_t round_dir = rounding_direction_t::NONE;
834+
log.log = false;
835+
i_t branch_var = -1;
836+
branch_direction_t round_dir = branch_direction_t::NONE;
839837
std::vector<f_t> current_incumbent;
840838
std::vector<f_t>& solution = worker->leaf_solution.x;
841839

@@ -848,14 +846,12 @@ branch_variable_t<i_t> branch_and_bound_t<i_t, f_t>::variable_selection(
848846
worker,
849847
var_types_,
850848
exploration_stats_,
851-
settings_,
852849
upper_bound_,
853850
worker_pool_.num_idle_workers(),
854-
log,
855851
new_slacks_,
856852
original_lp_);
857853
} else {
858-
branch_var = pc_.variable_selection(fractional, solution, log);
854+
branch_var = pc_.variable_selection(fractional, solution);
859855
}
860856

861857
round_dir = martin_criteria(solution[branch_var], root_relax_soln_.x[branch_var]);
@@ -880,7 +876,7 @@ branch_variable_t<i_t> branch_and_bound_t<i_t, f_t>::variable_selection(
880876

881877
default:
882878
log.debug("Unknown variable selection method: %d\n", worker->search_strategy);
883-
return {-1, rounding_direction_t::NONE};
879+
return {-1, branch_direction_t::NONE};
884880
}
885881
}
886882

@@ -907,7 +903,7 @@ struct tree_update_policy_t {
907903
const std::vector<f_t>& x) = 0;
908904
virtual void on_node_completed(mip_node_t<i_t, f_t>* node,
909905
node_status_t status,
910-
rounding_direction_t dir) = 0;
906+
branch_direction_t dir) = 0;
911907
virtual void on_numerical_issue(mip_node_t<i_t, f_t>*) = 0;
912908
virtual void graphviz(search_tree_t<i_t, f_t>&, mip_node_t<i_t, f_t>*, const char*, f_t) = 0;
913909
virtual void on_optimal_callback(const std::vector<f_t>&, f_t) = 0;
@@ -952,9 +948,7 @@ struct nondeterministic_policy_t : tree_update_policy_t<i_t, f_t> {
952948
const std::vector<f_t>& x) override
953949
{
954950
if (worker->search_strategy == search_strategy_t::BEST_FIRST) {
955-
logger_t pc_log;
956-
pc_log.log = false;
957-
node->objective_estimate = bnb.pc_.obj_estimate(fractional, x, node->lower_bound, pc_log);
951+
node->objective_estimate = bnb.pc_.obj_estimate(fractional, x, node->lower_bound);
958952
}
959953
}
960954

@@ -986,7 +980,7 @@ struct nondeterministic_policy_t : tree_update_policy_t<i_t, f_t> {
986980
}
987981
}
988982

989-
void on_node_completed(mip_node_t<i_t, f_t>*, node_status_t, rounding_direction_t) override {}
983+
void on_node_completed(mip_node_t<i_t, f_t>*, node_status_t, branch_direction_t) override {}
990984
};
991985

992986
template <typename i_t, typename f_t, typename WorkerT>
@@ -1005,7 +999,7 @@ struct deterministic_policy_base_t : tree_update_policy_t<i_t, f_t> {
1005999
{
10061000
if (node->branch_var < 0) return;
10071001
f_t change = std::max(leaf_obj - node->lower_bound, f_t(0));
1008-
f_t frac = node->branch_dir == rounding_direction_t::DOWN
1002+
f_t frac = node->branch_dir == branch_direction_t::DOWN
10091003
? node->fractional_val - std::floor(node->fractional_val)
10101004
: std::ceil(node->fractional_val) - node->fractional_val;
10111005
if (frac > 1e-10) {
@@ -1049,13 +1043,15 @@ struct deterministic_bfs_policy_t
10491043
const std::vector<i_t>& fractional,
10501044
const std::vector<f_t>& x) override
10511045
{
1046+
logger_t log;
1047+
log.log = false;
10521048
node->objective_estimate =
10531049
this->worker.pc_snapshot.obj_estimate(fractional, x, node->lower_bound);
10541050
}
10551051

10561052
void on_node_completed(mip_node_t<i_t, f_t>* node,
10571053
node_status_t status,
1058-
rounding_direction_t dir) override
1054+
branch_direction_t dir) override
10591055
{
10601056
switch (status) {
10611057
case node_status_t::INFEASIBLE: this->worker.record_infeasible(node); break;
@@ -1115,33 +1111,36 @@ struct deterministic_diving_policy_t
11151111
const std::vector<i_t>& fractional,
11161112
const std::vector<f_t>& x) override
11171113
{
1114+
logger_t log;
1115+
log.log = false;
1116+
11181117
switch (this->worker.diving_type) {
11191118
case search_strategy_t::PSEUDOCOST_DIVING:
1120-
return this->worker.variable_selection_from_snapshot(fractional, x);
1119+
return pseudocost_diving(
1120+
this->worker.pc_snapshot, fractional, x, *this->worker.root_solution, log);
11211121

11221122
case search_strategy_t::LINE_SEARCH_DIVING:
1123-
if (this->worker.root_solution) {
1124-
logger_t log;
1125-
log.log = false;
1126-
return line_search_diving<i_t, f_t>(fractional, x, *this->worker.root_solution, log);
1127-
}
1128-
return this->worker.variable_selection_from_snapshot(fractional, x);
1123+
return line_search_diving<i_t, f_t>(fractional, x, *this->worker.root_solution, log);
11291124

11301125
case search_strategy_t::GUIDED_DIVING:
1131-
return this->worker.guided_variable_selection(fractional, x);
1126+
if (this->worker.incumbent_snapshot.empty()) {
1127+
return pseudocost_diving(
1128+
this->worker.pc_snapshot, fractional, x, *this->worker.root_solution, log);
1129+
} else {
1130+
return guided_diving(
1131+
this->worker.pc_snapshot, fractional, x, this->worker.incumbent_snapshot, log);
1132+
}
11321133

11331134
case search_strategy_t::COEFFICIENT_DIVING: {
1134-
logger_t log;
1135-
log.log = false;
1136-
return coefficient_diving<i_t, f_t>(this->bnb.original_lp_,
1135+
return coefficient_diving<i_t, f_t>(this->worker.leaf_problem,
11371136
fractional,
11381137
x,
11391138
this->bnb.var_up_locks_,
11401139
this->bnb.var_down_locks_,
11411140
log);
11421141
}
11431142

1144-
default: return this->worker.variable_selection_from_snapshot(fractional, x);
1143+
default: CUOPT_LOG_ERROR("Invalid diving method!"); return {-1, branch_direction_t::NONE};
11451144
}
11461145
}
11471146

@@ -1153,10 +1152,10 @@ struct deterministic_diving_policy_t
11531152

11541153
void on_node_completed(mip_node_t<i_t, f_t>* node,
11551154
node_status_t status,
1156-
rounding_direction_t dir) override
1155+
branch_direction_t dir) override
11571156
{
11581157
if (status == node_status_t::HAS_CHILDREN) {
1159-
if (dir == rounding_direction_t::UP) {
1158+
if (dir == branch_direction_t::UP) {
11601159
stack.push_front(node->get_down_child());
11611160
stack.push_front(node->get_up_child());
11621161
} else {
@@ -1175,7 +1174,7 @@ struct deterministic_diving_policy_t
11751174

11761175
template <typename i_t, typename f_t>
11771176
template <typename WorkerT, typename Policy>
1178-
std::pair<node_status_t, rounding_direction_t> branch_and_bound_t<i_t, f_t>::update_tree_impl(
1177+
std::pair<node_status_t, branch_direction_t> branch_and_bound_t<i_t, f_t>::update_tree_impl(
11791178
mip_node_t<i_t, f_t>* node_ptr,
11801179
search_tree_t<i_t, f_t>& search_tree,
11811180
WorkerT* worker,
@@ -1187,7 +1186,10 @@ std::pair<node_status_t, rounding_direction_t> branch_and_bound_t<i_t, f_t>::upd
11871186
lp_solution_t<i_t, f_t>& leaf_solution = worker->leaf_solution;
11881187
const f_t upper_bound = policy.upper_bound();
11891188
node_status_t status = node_status_t::PENDING;
1190-
rounding_direction_t round_dir = rounding_direction_t::NONE;
1189+
branch_direction_t round_dir = branch_direction_t::NONE;
1190+
1191+
worker->recompute_basis = true;
1192+
worker->recompute_bounds = true;
11911193

11921194
if (lp_status == dual::status_t::DUAL_UNBOUNDED) {
11931195
node_ptr->lower_bound = inf;
@@ -1245,9 +1247,11 @@ std::pair<node_status_t, rounding_direction_t> branch_and_bound_t<i_t, f_t>::upd
12451247

12461248
assert(node_ptr->vstatus.size() == leaf_problem.num_cols);
12471249
assert(branch_var >= 0);
1248-
assert(dir != rounding_direction_t::NONE);
1250+
assert(dir != branch_direction_t::NONE);
12491251

12501252
policy.update_objective_estimate(node_ptr, leaf_fractional, leaf_solution.x);
1253+
worker->recompute_basis = false;
1254+
worker->recompute_bounds = false;
12511255

12521256
logger_t log;
12531257
log.log = false;
@@ -1284,7 +1288,7 @@ std::pair<node_status_t, rounding_direction_t> branch_and_bound_t<i_t, f_t>::upd
12841288
}
12851289

12861290
template <typename i_t, typename f_t>
1287-
std::pair<node_status_t, rounding_direction_t> branch_and_bound_t<i_t, f_t>::update_tree(
1291+
std::pair<node_status_t, branch_direction_t> branch_and_bound_t<i_t, f_t>::update_tree(
12881292
mip_node_t<i_t, f_t>* node_ptr,
12891293
search_tree_t<i_t, f_t>& search_tree,
12901294
branch_and_bound_worker_t<i_t, f_t>* worker,
@@ -1377,7 +1381,7 @@ dual::status_t branch_and_bound_t<i_t, f_t>::solve_node_lp(
13771381
node_ptr->node_id,
13781382
node_ptr->depth,
13791383
node_ptr->branch_var,
1380-
node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP",
1384+
node_ptr->branch_dir == branch_direction_t::DOWN ? "DOWN" : "UP",
13811385
node_ptr->fractional_val,
13821386
node_ptr->branch_var_lower,
13831387
node_ptr->branch_var_upper,
@@ -1511,7 +1515,7 @@ void branch_and_bound_t<i_t, f_t>::plunge_with(branch_and_bound_worker_t<i_t, f_
15111515

15121516
exploration_stats_.nodes_unexplored += 2;
15131517

1514-
if (round_dir == rounding_direction_t::UP) {
1518+
if (round_dir == branch_direction_t::UP) {
15151519
if (node_queue_.best_first_queue_size() < min_node_queue_size_) {
15161520
node_queue_.push(node_ptr->get_down_child());
15171521
} else {
@@ -1623,7 +1627,7 @@ void branch_and_bound_t<i_t, f_t>::dive_with(branch_and_bound_worker_t<i_t, f_t>
16231627
worker->recompute_bounds = node_status != node_status_t::HAS_CHILDREN;
16241628

16251629
if (node_status == node_status_t::HAS_CHILDREN) {
1626-
if (round_dir == rounding_direction_t::UP) {
1630+
if (round_dir == branch_direction_t::UP) {
16271631
stack.push_front(node_ptr->get_down_child());
16281632
stack.push_front(node_ptr->get_up_child());
16291633
} else {
@@ -2507,7 +2511,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
25072511
set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_);
25082512

25092513
pc_.resize(original_lp_.num_cols);
2510-
original_lp_.A.transpose(pc_.AT);
2514+
original_lp_.A.transpose(*pc_.AT);
25112515
{
25122516
raft::common::nvtx::range scope_sb("BB::strong_branching");
25132517
strong_branching<i_t, f_t>(original_lp_,
@@ -2578,7 +2582,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
25782582
}
25792583

25802584
// Choose variable to branch on
2581-
i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x, log);
2585+
i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x);
25822586

25832587
search_tree_.root = std::move(mip_node_t<i_t, f_t>(root_objective_, root_vstatus_));
25842588
search_tree_.num_nodes = 0;
@@ -3322,11 +3326,12 @@ template <typename PoolT>
33223326
void branch_and_bound_t<i_t, f_t>::deterministic_broadcast_snapshots(
33233327
PoolT& pool, const std::vector<f_t>& incumbent_snapshot)
33243328
{
3325-
deterministic_snapshot_t<i_t, f_t> snap;
3326-
snap.upper_bound = upper_bound_.load();
3327-
snap.total_lp_iters = exploration_stats_.total_lp_iters.load();
3328-
snap.incumbent = incumbent_snapshot;
3329-
snap.pc_snapshot = pc_.create_snapshot();
3329+
deterministic_snapshot_t<i_t, f_t> snap{
3330+
.upper_bound = upper_bound_,
3331+
.pc_snapshot = pc_,
3332+
.incumbent = incumbent_snapshot,
3333+
.total_lp_iters = exploration_stats_.total_lp_iters,
3334+
};
33303335

33313336
for (auto& worker : pool) {
33323337
worker.set_snapshots(snap);

cpp/src/branch_and_bound/branch_and_bound.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
#pragma once
99

1010
#include <branch_and_bound/bb_event.hpp>
11-
#include <branch_and_bound/branch_and_bound_worker.hpp>
1211
#include <branch_and_bound/deterministic_workers.hpp>
13-
#include <branch_and_bound/diving_heuristics.hpp>
1412
#include <branch_and_bound/mip_node.hpp>
1513
#include <branch_and_bound/node_queue.hpp>
1614
#include <branch_and_bound/pseudo_costs.hpp>
15+
#include <branch_and_bound/worker.hpp>
16+
#include <branch_and_bound/worker_pool.hpp>
1717

1818
#include <cuts/cuts.hpp>
1919

@@ -318,15 +318,15 @@ class branch_and_bound_t {
318318

319319
// Policy-based tree update shared between opportunistic and deterministic codepaths.
320320
template <typename WorkerT, typename Policy>
321-
std::pair<node_status_t, rounding_direction_t> update_tree_impl(
321+
std::pair<node_status_t, branch_direction_t> update_tree_impl(
322322
mip_node_t<i_t, f_t>* node_ptr,
323323
search_tree_t<i_t, f_t>& search_tree,
324324
WorkerT* worker,
325325
dual::status_t lp_status,
326326
Policy& policy);
327327

328328
// Opportunistic tree update wrapper.
329-
std::pair<node_status_t, rounding_direction_t> update_tree(
329+
std::pair<node_status_t, branch_direction_t> update_tree(
330330
mip_node_t<i_t, f_t>* node_ptr,
331331
search_tree_t<i_t, f_t>& search_tree,
332332
branch_and_bound_worker_t<i_t, f_t>* worker,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* clang-format off */
2+
/*
3+
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
/* clang-format on */
7+
8+
#pragma once
9+
10+
namespace cuopt::linear_programming::dual_simplex {
11+
12+
constexpr int num_search_strategies = 5;
13+
14+
// Indicate the search and variable selection algorithms used by each thread
15+
// in B&B (See [1]).
16+
//
17+
// [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin,
18+
// Berlin, 2007. doi: 10.14279/depositonce-1634.
19+
enum search_strategy_t : int {
20+
BEST_FIRST = 0, // Best-First + Plunging.
21+
PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5)
22+
LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4)
23+
GUIDED_DIVING = 3, // Guided diving (9.2.3).
24+
COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1)
25+
};
26+
27+
enum class branch_direction_t { NONE = -1, DOWN = 0, UP = 1 };
28+
29+
enum class branch_and_bound_mode_t { PARALLEL = 0, DETERMINISTIC = 1 };
30+
31+
} // namespace cuopt::linear_programming::dual_simplex

0 commit comments

Comments
 (0)