Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a7f5121
logger_t now can support formatting strings based on `std::format`
nguidotti May 27, 2026
27f2973
moved search_tree object to a separated file. added a depth-first cle…
nguidotti May 27, 2026
501220b
track search progress using tree the weight metric. migrate the B&B l…
nguidotti May 27, 2026
9672194
simplified log lines construction in B&B
nguidotti May 29, 2026
8367299
switched to std::format for compile-time checks
nguidotti May 29, 2026
b865ae9
migrate node_id from int to uint64_t. bug fixes
nguidotti May 29, 2026
6d7f0f5
revert changes to node_id. added exception handling to the destructor.
nguidotti Jun 1, 2026
e5ea499
address coderabbit
nguidotti Jun 1, 2026
da1015f
revert log changes
nguidotti Jun 1, 2026
87cdf92
Merge branch 'main' into tree-progress
nguidotti Jun 2, 2026
6c04ed4
Merge branch 'main' into tree-progress
nguidotti Jun 3, 2026
f79029f
Merge remote-tracking branch 'origin/tree-progress' into tree-progress
nguidotti Jun 3, 2026
3ff82cc
implemented a simple restart procedure for B&B.
nguidotti Jun 3, 2026
27c4a79
added total nodes explored in the restart heuristic. tweak parameters
nguidotti Jun 3, 2026
ffdc469
removed search progress from the logs. It is too inaccurate, especial…
nguidotti Jun 8, 2026
e47c61f
removed try-catch block in the destructor.
nguidotti Jun 8, 2026
e6db58e
Merge branch 'tree-progress' into simple-restart
nguidotti Jun 8, 2026
683dc52
fixed time gap between restarts. call reduced cost fixing after each …
nguidotti Jun 8, 2026
68ecb98
fixed incorrect lp for workers. fixed node limit when restarting
nguidotti Jun 8, 2026
d8301b0
fixed total nodes explored
nguidotti Jun 8, 2026
af3c3b9
fixed logs
nguidotti Jun 8, 2026
4f32e52
revert log changes
nguidotti Jun 9, 2026
518dc8e
fixed halt_flag to cpu fj in RINS. removed debug code
nguidotti Jun 9, 2026
aa5952e
re-enabled restarts
nguidotti Jun 9, 2026
4ea3cc2
tentative fix for crash on symmetry after some generators are pruned
nguidotti Jun 9, 2026
a12fb55
address coderabbit comments
nguidotti Jun 10, 2026
3037981
Merge branch 'main' into simple-restart
nguidotti Jun 10, 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
357 changes: 234 additions & 123 deletions cpp/src/branch_and_bound/branch_and_bound.cpp

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions cpp/src/branch_and_bound/branch_and_bound.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <branch_and_bound/mip_node.hpp>
#include <branch_and_bound/node_queue.hpp>
#include <branch_and_bound/pseudo_costs.hpp>
#include <branch_and_bound/search_tree.hpp>
#include <branch_and_bound/worker.hpp>
#include <branch_and_bound/worker_pool.hpp>

Expand Down Expand Up @@ -59,6 +60,7 @@ enum class mip_status_t {
NUMERICAL = 5, // The solver encountered a numerical error
UNSET = 6, // The status is not set
WORK_LIMIT = 7, // The solver reached a deterministic work limit
RESTART = 8, // The solver triggered a restart
};

template <typename i_t, typename f_t>
Expand All @@ -82,6 +84,7 @@ class branch_and_bound_t {
branch_and_bound_t(const user_problem_t<i_t, f_t>& user_problem,
const simplex_solver_settings_t<i_t, f_t>& solver_settings,
f_t start_time,
std::atomic<int>* restart_concurrent_halt,
const probing_implied_bound_t<i_t, f_t>& probing_implied_bound,
std::shared_ptr<detail::clique_table_t<i_t, f_t>> clique_table = nullptr,
mip_symmetry_t<i_t, f_t>* symmetry = nullptr);
Expand Down Expand Up @@ -234,6 +237,7 @@ class branch_and_bound_t {
bool enable_concurrent_lp_root_solve_{false};
std::atomic<int> root_concurrent_halt_{0};
std::atomic<int> node_concurrent_halt_{0};
std::atomic<int>* restart_concurrent_halt_{nullptr};
bool is_root_solution_set{false};

// Pseudocosts
Expand Down Expand Up @@ -263,6 +267,8 @@ class branch_and_bound_t {
omp_atomic_t<f_t> lower_bound_numerical_;
std::function<void(f_t)> user_bound_callback_;

i_t restart_count_;

void report_heuristic(f_t obj);
void report(char symbol,
f_t obj,
Expand Down Expand Up @@ -314,6 +320,8 @@ class branch_and_bound_t {
// Repairs low-quality solutions from the heuristics, if it is applicable.
void repair_heuristic_solutions();

bool should_restart(f_t current_abs_gap);

// Launch a new diving worker from a given best-first worker.
bool launch_diving_worker(bfs_worker_t<i_t, f_t>* bfs_worker);

Expand Down
134 changes: 2 additions & 132 deletions cpp/src/branch_and_bound/mip_node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,43 +41,8 @@ inline bool inactive_status(node_status_t status)
template <typename i_t, typename f_t>
class mip_node_t {
public:
~mip_node_t()
{
// Iterative teardown to avoid stack overflow on deep trees.
// Detach all descendants breadth-first, then destroy them as leaves.
// vector::push_back can throw bad_alloc; the catch-all keeps the destructor
// exception-free. Under OOM, any not-yet-detached descendants are destroyed
// via the recursive unique_ptr chain in `children` as this frame unwinds.
try {
std::vector<std::unique_ptr<mip_node_t>> nodes;
for (auto& c : children) {
if (c) { nodes.push_back(std::move(c)); }
}
// nodes.size() grows so that this loop only terminates when only leaves remain
for (size_t i = 0; i < nodes.size(); ++i) {
for (auto& c : nodes[i]->children) {
if (c) { nodes.push_back(std::move(c)); }
}
}

// scope-exit ensure destruction of all detached leaves
} catch (const std::exception& e) {
// fprintf to stderr is allocation-free and cannot throw; using the
// project logger here would risk a secondary bad_alloc that would
// escape the destructor and re-introduce std::terminate.
std::fprintf(stderr,
"mip_node_t destructor: iterative teardown failed (%s); falling back to "
"recursive unique_ptr destruction.\n",
e.what());
} catch (...) {
std::fprintf(stderr,
"mip_node_t destructor: iterative teardown failed (unknown exception); "
"falling back to recursive unique_ptr destruction.\n");
}
}

mip_node_t(mip_node_t&&) noexcept = default;
mip_node_t& operator=(mip_node_t&&) noexcept = default;
mip_node_t(mip_node_t&&) = default;
mip_node_t& operator=(mip_node_t&&) = default;

mip_node_t()
: status(node_status_t::PENDING),
Expand Down Expand Up @@ -364,99 +329,4 @@ void remove_fathomed_nodes(std::vector<mip_node_t<i_t, f_t>*>& stack)
}
}

template <typename i_t, typename f_t>
class search_tree_t {
public:
search_tree_t() : num_nodes(0) {}

search_tree_t(mip_node_t<i_t, f_t>&& node) : root(std::move(node)), num_nodes(0) {}

void update(mip_node_t<i_t, f_t>* node_ptr, node_status_t status)
{
std::lock_guard<omp_mutex_t> lock(mutex);
std::vector<mip_node_t<i_t, f_t>*> stack;
node_ptr->set_status(status, stack);
remove_fathomed_nodes(stack);
}

void branch(mip_node_t<i_t, f_t>* parent_node,
const i_t branch_var,
const f_t fractional_val,
const i_t integer_infeasible,
const std::vector<variable_status_t>& parent_vstatus,
const lp_problem_t<i_t, f_t>& original_lp,
logger_t& log)
{
i_t id = num_nodes.fetch_add(2);

auto down_child = std::make_unique<mip_node_t<i_t, f_t>>(original_lp,
parent_node,
++id,
branch_var,
branch_direction_t::DOWN,
fractional_val,
integer_infeasible,
parent_vstatus);
graphviz_edge(log,
parent_node,
down_child.get(),
branch_var,
branch_direction_t::DOWN,
std::floor(fractional_val));

auto up_child = std::make_unique<mip_node_t<i_t, f_t>>(original_lp,
parent_node,
++id,
branch_var,
branch_direction_t::UP,
fractional_val,
integer_infeasible,
parent_vstatus);

graphviz_edge(log,
parent_node,
up_child.get(),
branch_var,
branch_direction_t::UP,
std::ceil(fractional_val));

assert(parent_vstatus.size() == original_lp.num_cols);
parent_node->add_children(std::move(down_child),
std::move(up_child)); // child pointers moved into the tree
}

void graphviz_node(logger_t& log,
const mip_node_t<i_t, f_t>* node_ptr,
const std::string label,
const f_t val)
{
if (write_graphviz) {
log.printf("Node%d [label=\"%s %.16e\"]\n", node_ptr->node_id, label.c_str(), val);
}
}

void graphviz_edge(logger_t& log,
const mip_node_t<i_t, f_t>* origin_ptr,
const mip_node_t<i_t, f_t>* dest_ptr,
const i_t branch_var,
branch_direction_t branch_dir,
const f_t bound)
{
if (write_graphviz) {
log.printf("Node%d -> Node%d [label=\"x%d %s %e\"]\n",
origin_ptr->node_id,
dest_ptr->node_id,
branch_var,
branch_dir == branch_direction_t::DOWN ? "<=" : ">=",
bound);
}
}

mip_node_t<i_t, f_t> root;
omp_mutex_t mutex;
omp_atomic_t<i_t> num_nodes;

static constexpr bool write_graphviz = false;
};

} // namespace cuopt::linear_programming::dual_simplex
9 changes: 8 additions & 1 deletion cpp/src/branch_and_bound/node_queue.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class heap_t {

void clear()
{
buffer.clear();
buffer = {};
num_entries_ = 0;
}

Expand Down Expand Up @@ -157,6 +157,13 @@ class node_queue_t {
return best_first_heap_.empty() ? std::numeric_limits<f_t>::infinity() : lower_bound_.load();
}

void clear()
{
std::lock_guard lock(mutex_);
best_first_heap_.clear();
diving_heap_.clear();
}

private:
struct heap_entry_t {
mip_node_t<i_t, f_t>* node = nullptr;
Expand Down
14 changes: 10 additions & 4 deletions cpp/src/branch_and_bound/pseudo_costs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ std::pair<f_t, dual::status_t> trial_branching(const lp_problem_t<i_t, f_t>& ori
const basis_update_mpf_t<i_t, f_t>& basis_factors,
const std::vector<i_t>& basic_list,
const std::vector<i_t>& nonbasic_list,
std::atomic<int>* concurrent_halt,
i_t branch_var,
f_t branch_var_lower,
f_t branch_var_upper,
Expand All @@ -482,6 +483,7 @@ std::pair<f_t, dual::status_t> trial_branching(const lp_problem_t<i_t, f_t>& ori
child_settings.scale_columns = false;
child_settings.cut_off =
objective_upper_bound(child_problem, upper_bound, child_settings.dual_tol);
child_settings.concurrent_halt = concurrent_halt;

lp_solution_t<i_t, f_t> solution(original_lp.num_rows, original_lp.num_cols);
iter = 0;
Expand Down Expand Up @@ -1707,15 +1709,15 @@ i_t pseudo_costs_t<i_t, f_t>::reliable_variable_selection(
std::vector<f_t> pdlp_obj_down(num_candidates, std::numeric_limits<f_t>::quiet_NaN());
std::vector<f_t> pdlp_obj_up(num_candidates, std::numeric_limits<f_t>::quiet_NaN());

std::atomic<int> concurrent_halt{0};
std::atomic<int> pdlp_concurrent_halt{0};

if (use_pdlp) {
#pragma omp task default(shared) priority(CUOPT_HIGH_TASK_PRIORITY)
batch_pdlp_reliability_branching_task(settings.log,
rb_mode,
num_candidates,
start_time,
concurrent_halt,
pdlp_concurrent_halt,
original_lp,
new_slacks,
leaf_solution.x,
Expand All @@ -1731,7 +1733,7 @@ i_t pseudo_costs_t<i_t, f_t>::reliable_variable_selection(
if (toc(start_time) > settings.time_limit) {
settings.log.debug("Time limit reached\n");
if (use_pdlp) {
concurrent_halt.store(1);
pdlp_concurrent_halt.store(1);
#pragma omp taskwait // Wait for the batch PDLP task to finish
}
return branch_var;
Expand All @@ -1748,6 +1750,8 @@ i_t pseudo_costs_t<i_t, f_t>::reliable_variable_selection(
#pragma omp taskloop if (num_tasks > 1) priority(CUOPT_HIGH_TASK_PRIORITY) \
num_tasks(num_tasks) default(shared)
for (i_t i = 0; i < num_candidates; ++i) {
if (*concurrent_halt_ == 1) continue; // OpenMP does not allow to break out of the loop

auto [score, j] = unreliable_list[i];

if (toc(start_time) > settings.time_limit) { continue; }
Expand All @@ -1768,6 +1772,7 @@ i_t pseudo_costs_t<i_t, f_t>::reliable_variable_selection(
worker->basis_factors,
worker->basic_list,
worker->nonbasic_list,
concurrent_halt_,
j,
worker->leaf_problem.lower[j],
std::floor(leaf_solution.x[j]),
Expand Down Expand Up @@ -1813,6 +1818,7 @@ i_t pseudo_costs_t<i_t, f_t>::reliable_variable_selection(
worker->basis_factors,
worker->basic_list,
worker->nonbasic_list,
concurrent_halt_,
j,
std::ceil(leaf_solution.x[j]),
worker->leaf_problem.upper[j],
Expand Down Expand Up @@ -1849,7 +1855,7 @@ i_t pseudo_costs_t<i_t, f_t>::reliable_variable_selection(
score_mutex.unlock();
}

concurrent_halt.store(1);
pdlp_concurrent_halt.store(1);
}

f_t dual_simplex_elapsed = toc(dual_simplex_start_time);
Expand Down
1 change: 1 addition & 0 deletions cpp/src/branch_and_bound/pseudo_costs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ class pseudo_costs_t {

std::shared_ptr<csc_matrix_t<i_t, f_t>> AT; // Transpose of the constraint matrix A
std::shared_ptr<batch_pdlp_warm_cache_t<i_t, f_t>> pdlp_warm_cache;
std::atomic<int>* concurrent_halt_;

reliability_branching_settings_t<i_t, f_t> reliability_branching_settings;
simplex_solver_settings_t<i_t, f_t> settings;
Expand Down
Loading
Loading