From a7f51212f8f506398abc26ef9a02b52decb68b19 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 16:26:05 +0200 Subject: [PATCH 01/24] logger_t now can support formatting strings based on `std::format` Signed-off-by: Nicolas L. Guidotti --- cpp/src/dual_simplex/logger.hpp | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index f813086708..9f92561532 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace cuopt::linear_programming::dual_simplex { @@ -84,6 +85,26 @@ class logger_t { } } + template + void print_format(std::format_string fmt, Args... args) + { + if (log) { + std::string msg = std::format(fmt, args...); + if (log_to_console) { +#ifdef CUOPT_LOG_ACTIVE_LEVEL + CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); +#else + std::printf("%s", msg.c_str()); + fflush(stdout); +#endif + } + if (log_to_file && log_file != nullptr) { + std::fprintf(log_file, "%s", msg.c_str()); + fflush(log_file); + } + } + } + void debug([[maybe_unused]] const char* fmt, ...) { if (log) { @@ -118,6 +139,26 @@ class logger_t { } } + template + void debug_format(std::format_string fmt, Args... args) + { + if (log) { + std::string msg = std::format(fmt, args...); + if (log_to_console) { +#ifdef CUOPT_LOG_ACTIVE_LEVEL + CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); +#else + std::printf("%s", msg.c_str()); + fflush(stdout); +#endif + } + if (log_to_file && log_file != nullptr) { + std::fprintf(log_file, "%s", msg.c_str()); + fflush(log_file); + } + } + } + bool log; bool log_to_console; std::string log_prefix; From 27f2973ab40710f8843fa0a0c8a4e560d603a3cd Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 16:29:30 +0200 Subject: [PATCH 02/24] moved search_tree object to a separated file. added a depth-first clean method. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + cpp/src/branch_and_bound/mip_node.hpp | 134 +----------------- cpp/src/branch_and_bound/search_tree.hpp | 129 +++++++++++++++++ 3 files changed, 132 insertions(+), 132 deletions(-) create mode 100644 cpp/src/branch_and_bound/search_tree.hpp diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 0b32b2ece5..4c7df73bb4 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 2c0968ffce..ad53aaabb9 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -41,43 +41,8 @@ inline bool inactive_status(node_status_t status) template 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> 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), @@ -364,99 +329,4 @@ void remove_fathomed_nodes(std::vector*>& stack) } } -template -class search_tree_t { - public: - search_tree_t() : num_nodes(0) {} - - search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} - - void update(mip_node_t* node_ptr, node_status_t status) - { - std::lock_guard lock(mutex); - std::vector*> stack; - node_ptr->set_status(status, stack); - remove_fathomed_nodes(stack); - } - - void branch(mip_node_t* parent_node, - const i_t branch_var, - const f_t fractional_val, - const i_t integer_infeasible, - const std::vector& parent_vstatus, - const lp_problem_t& original_lp, - logger_t& log) - { - i_t id = num_nodes.fetch_add(2); - - auto down_child = std::make_unique>(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>(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* 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* origin_ptr, - const mip_node_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 root; - omp_mutex_t mutex; - omp_atomic_t num_nodes; - - static constexpr bool write_graphviz = false; -}; - } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp new file mode 100644 index 0000000000..5057d44f05 --- /dev/null +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -0,0 +1,129 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +class search_tree_t { + public: + search_tree_t() : num_nodes(0) {} + + search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} + + ~search_tree_t() { clean(); } + + void update(mip_node_t* node_ptr, node_status_t status) + { + std::lock_guard lock(mutex); + std::vector*> stack; + node_ptr->set_status(status, stack); + remove_fathomed_nodes(stack); + } + + void branch(mip_node_t* parent_node, + const i_t branch_var, + const f_t fractional_val, + const i_t integer_infeasible, + const std::vector& parent_vstatus, + const lp_problem_t& original_lp, + logger_t& log) + { + i_t id = num_nodes.fetch_add(2); + + auto down_child = std::make_unique>(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>(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 + } + + static void graphviz_node(logger_t& log, + const mip_node_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); + } + } + + static void graphviz_edge(logger_t& log, + const mip_node_t* origin_ptr, + const mip_node_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); + } + } + + // Clean the tree using a depth first scheme + void clean() + { + std::vector>> stack; + + if (root.children[0]) stack.push_back(std::move(root.children[0])); + if (root.children[1]) stack.push_back(std::move(root.children[1])); + + while (!stack.empty()) { + auto node = std::move(stack.back()); + std::cout << std::format("destroying node {}", node->node_id) << std::endl; + stack.pop_back(); + if (node->children[0]) stack.push_back(std::move(node->children[0])); + if (node->children[1]) stack.push_back(std::move(node->children[1])); + // Implicitly call destructor for `node` + } + } + + mip_node_t root; + omp_mutex_t mutex; + omp_atomic_t num_nodes; + + static constexpr bool write_graphviz = false; +}; + +} // namespace cuopt::linear_programming::dual_simplex From 501220b61fe44ba7f4aa238ede71559ac68cad18 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 19:28:27 +0200 Subject: [PATCH 03/24] track search progress using tree the weight metric. migrate the B&B logs to use the std::format variant. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 158 +++++++++++------- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + cpp/src/branch_and_bound/search_tree.hpp | 27 ++- cpp/src/dual_simplex/logger.hpp | 9 +- 4 files changed, 123 insertions(+), 72 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 0222ad6fe9..2db23764b9 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -195,22 +195,12 @@ f_t user_relative_gap(const lp_problem_t& lp, f_t obj_value, f_t lower return user_mip_gap; } -template -std::string user_mip_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) +template +std::string to_percentage(f_t value) { - const f_t user_mip_gap = user_relative_gap(lp, obj_value, lower_bound); - if (user_mip_gap == std::numeric_limits::infinity()) { - return " - "; - } else { - constexpr int BUFFER_LEN = 32; - char buffer[BUFFER_LEN]; - if (user_mip_gap > 1e-3) { - snprintf(buffer, BUFFER_LEN - 1, "%5.1f%%", user_mip_gap * 100); - } else { - snprintf(buffer, BUFFER_LEN - 1, "%5.2f%%", user_mip_gap * 100); - } - return std::string(buffer); - } + if (value == std::numeric_limits::infinity()) return "---"; + if (value > 1e-3) { return std::format("{:5.1f}%", value * 100); } + return std::format("{:5.2f}%", value * 100); } #ifdef SHOW_DIVING_TYPE @@ -326,29 +316,72 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) upper_bound_ = bound; } +template +void branch_and_bound_t::print_table_header() +{ + if (settings_.deterministic) { + settings_.log.print_format( + "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|{:^8}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap", + "Completion", + "Work", + "Time"); + } else { + settings_.log.print_format( + "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap", + "Completion", + "Time"); + } +} + template void branch_and_bound_t::report_heuristic(f_t obj) { if (is_running_) { - f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); - std::string user_gap = user_mip_gap(original_lp_, obj, get_lower_bound()); - - settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + f_t lower_bound = get_lower_bound(); + f_t user_obj = compute_user_objective(original_lp_, obj); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + std::string user_gap_text = to_percentage(user_gap); + + settings_.log.print_format( + "H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15} {:>8.2f}", + "", // nodes explored + "", // nodes unexplored user_obj, user_lower, - user_gap.c_str(), + "", // integer infeasible + "", // depth + "", // iter/node + user_gap_text.c_str(), + "", // tree progress toc(exploration_stats_.start_time)); } else { if (solving_root_relaxation_.load()) { f_t user_obj = compute_user_objective(original_lp_, obj); - std::string user_gap = - user_mip_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); + f_t user_gap = user_relative_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); + std::string user_gap_text = to_percentage(user_gap); settings_.log.printf( "New solution from primal heuristics. Objective %+.6e. Gap %s. Time %.2f\n", user_obj, - user_gap.c_str(), + user_gap_text.c_str(), toc(exploration_stats_.start_time)); } else { settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", @@ -363,16 +396,20 @@ void branch_and_bound_t::report( char symbol, f_t obj, f_t lower_bound, i_t node_depth, i_t node_int_infeas, double work_time) { update_user_bound(lower_bound); - const i_t nodes_explored = exploration_stats_.nodes_explored; - const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - const f_t user_obj = compute_user_objective(original_lp_, obj); - const f_t user_lower = compute_user_objective(original_lp_, lower_bound); - const f_t iters = static_cast(exploration_stats_.total_lp_iters); - const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; - const std::string user_gap = user_mip_gap(original_lp_, obj, lower_bound); + const i_t nodes_explored = exploration_stats_.nodes_explored; + const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + const f_t user_obj = compute_user_objective(original_lp_, obj); + const f_t user_lower = compute_user_objective(original_lp_, lower_bound); + const f_t iters = static_cast(exploration_stats_.total_lp_iters); + const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; + f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + std::string user_gap_text = to_percentage(user_gap); + std::string tree_completion = to_percentage(search_tree_.progress.load()); + if (work_time >= 0) { - settings_.log.printf( - "%c %10d %10lu %+13.6e %+10.6e %6d %6d %7.1e %s %9.2f %9.2f\n", + settings_.log.print_format( + "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f} " + "{:>8.2f}", symbol, nodes_explored, nodes_unexplored, @@ -381,21 +418,24 @@ void branch_and_bound_t::report( node_int_infeas, node_depth, iter_node, - user_gap.c_str(), + user_gap_text.c_str(), + tree_completion.c_str(), work_time, toc(exploration_stats_.start_time)); } else { - settings_.log.printf("%c %10d %10lu %+13.6e %+10.6e %6d %6d %7.1e %s %9.2f\n", - symbol, - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_int_infeas, - node_depth, - iter_node, - user_gap.c_str(), - toc(exploration_stats_.start_time)); + settings_.log.print_format( + "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f}", + symbol, + nodes_explored, + nodes_unexplored, + user_obj, + user_lower, + node_int_infeas, + node_depth, + iter_node, + user_gap_text.c_str(), + tree_completion.c_str(), + toc(exploration_stats_.start_time)); } } @@ -2597,11 +2637,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut is_running_ = true; lower_bound_numerical_ = inf; - if (num_fractional != 0 && settings_.max_cut_passes > 0) { - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node | " - "Gap | Time |\n"); - } + if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } cut_pool_t cut_pool(original_lp_.num_cols, settings_); cut_generation_t cut_generation(cut_pool, @@ -2864,16 +2900,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.diving_settings.coefficient_diving != 0) { calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); } - - if (settings_.deterministic) { - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node " - "| Gap | Work | Time |\n"); - } else { - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node " - "| Gap | Time |\n"); - } + print_table_header(); #pragma omp taskgroup { @@ -3385,9 +3412,10 @@ void branch_and_bound_t::deterministic_sync_callback() exploration_stats_.last_log = tic(); } - f_t obj = compute_user_objective(original_lp_, upper_bound); - f_t user_lower = compute_user_objective(original_lp_, lower_bound); - std::string gap_user = user_mip_gap(original_lp_, upper_bound, lower_bound); + f_t obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + f_t user_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + std::string user_gap_text = to_percentage(user_gap); std::string idle_workers; i_t idle_count = 0; @@ -3403,7 +3431,7 @@ void branch_and_bound_t::deterministic_sync_callback() exploration_stats_.nodes_unexplored, obj, user_lower, - gap_user.c_str(), + user_gap_text.c_str(), toc(exploration_stats_.start_time), state_hash, idle_workers.empty() ? "" : " ", diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 4c7df73bb4..77d02ccfb1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -264,6 +264,7 @@ class branch_and_bound_t { omp_atomic_t lower_bound_numerical_; std::function user_bound_callback_; + void print_table_header(); void report_heuristic(f_t obj); void report(char symbol, f_t obj, diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index 5057d44f05..e3a6ed9901 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -22,7 +22,16 @@ class search_tree_t { void update(mip_node_t* node_ptr, node_status_t status) { - std::lock_guard lock(mutex); + std::lock_guard lock(mutex); + + --num_open_nodes; + if (status == node_status_t::HAS_CHILDREN) { + ++num_inner_nodes; + } else { + ++num_final_nodes; + progress += std::pow(2, -node_ptr->depth); + } + std::vector*> stack; node_ptr->set_status(status, stack); remove_fathomed_nodes(stack); @@ -72,6 +81,7 @@ class search_tree_t { 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 + num_open_nodes += 2; } static void graphviz_node(logger_t& log, @@ -111,7 +121,6 @@ class search_tree_t { while (!stack.empty()) { auto node = std::move(stack.back()); - std::cout << std::format("destroying node {}", node->node_id) << std::endl; stack.pop_back(); if (node->children[0]) stack.push_back(std::move(node->children[0])); if (node->children[1]) stack.push_back(std::move(node->children[1])); @@ -121,7 +130,19 @@ class search_tree_t { mip_node_t root; omp_mutex_t mutex; - omp_atomic_t num_nodes; + omp_atomic_t num_nodes; + + // Number of nodes that still needs to be explored + omp_atomic_t num_open_nodes; + + // Number of integer feasible, infeasible or fathomed nodes + omp_atomic_t num_final_nodes; + + // Number of inner nodes + omp_atomic_t num_inner_nodes; + + // Track the solver progress based on how much the tree was explored + omp_atomic_t progress = 0.0; static constexpr bool write_graphviz = false; }; diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 9f92561532..53c3cef9db 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -12,6 +12,7 @@ #endif #include +#include #include #include @@ -86,10 +87,10 @@ class logger_t { } template - void print_format(std::format_string fmt, Args... args) + void print_format(std::string_view fmt, Args&&... args) { if (log) { - std::string msg = std::format(fmt, args...); + const std::string msg = std::vformat(fmt, std::make_format_args(args...)); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); @@ -140,10 +141,10 @@ class logger_t { } template - void debug_format(std::format_string fmt, Args... args) + void debug_format(std::string_view fmt, Args&&... args) { if (log) { - std::string msg = std::format(fmt, args...); + std::string msg = std::vformat(fmt, std::make_format_args(args...)); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); From 9672194b7f94736f6bf343ceff9b64cc596f5180 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 15:44:44 +0200 Subject: [PATCH 04/24] simplified log lines construction in B&B Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 119 +++++++----------- 1 file changed, 46 insertions(+), 73 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 2db23764b9..6aa75dc392 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -319,36 +319,21 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) template void branch_and_bound_t::print_table_header() { - if (settings_.deterministic) { - settings_.log.print_format( - "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|{:^8}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap", - "Completion", - "Work", - "Time"); - } else { - settings_.log.print_format( - "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap", - "Completion", - "Time"); - } + std::string header = + std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap", + "Completion"); + if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } + header += std::format("{:^8}|", "Time"); + settings_.log.printf("%s", header.c_str()); } template @@ -361,18 +346,22 @@ void branch_and_bound_t::report_heuristic(f_t obj) f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); std::string user_gap_text = to_percentage(user_gap); - settings_.log.print_format( - "H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15} {:>8.2f}", - "", // nodes explored - "", // nodes unexplored - user_obj, - user_lower, - "", // integer infeasible - "", // depth - "", // iter/node - user_gap_text.c_str(), - "", // tree progress - toc(exploration_stats_.start_time)); + std::string log_line = + std::format("H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", + "", // nodes explored + "", // nodes unexplored + user_obj, + user_lower, + "", // integer infeasible + "", // depth + "", // iter/node + user_gap_text, + "" // tree progress + ); + + if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } + log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); + settings_.log.printf("%s", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { f_t user_obj = compute_user_objective(original_lp_, obj); @@ -406,37 +395,21 @@ void branch_and_bound_t::report( std::string user_gap_text = to_percentage(user_gap); std::string tree_completion = to_percentage(search_tree_.progress.load()); - if (work_time >= 0) { - settings_.log.print_format( - "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f} " - "{:>8.2f}", - symbol, - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_int_infeas, - node_depth, - iter_node, - user_gap_text.c_str(), - tree_completion.c_str(), - work_time, - toc(exploration_stats_.start_time)); - } else { - settings_.log.print_format( - "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f}", - symbol, - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_int_infeas, - node_depth, - iter_node, - user_gap_text.c_str(), - tree_completion.c_str(), - toc(exploration_stats_.start_time)); - } + std::string log_line = + std::format("{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", + symbol, + nodes_explored, + nodes_unexplored, + user_obj, + user_lower, + node_int_infeas, + node_depth, + iter_node, + user_gap_text, + tree_completion); + if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } + log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); + settings_.log.printf("%s", log_line.c_str()); } template From 83672994bf34826fa9214e7277228ad9d06ac261 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 15:56:03 +0200 Subject: [PATCH 05/24] switched to std::format for compile-time checks Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 13 +++++++------ cpp/src/dual_simplex/logger.hpp | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 6aa75dc392..13cca92ece 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -367,15 +367,16 @@ void branch_and_bound_t::report_heuristic(f_t obj) f_t user_obj = compute_user_objective(original_lp_, obj); f_t user_gap = user_relative_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); std::string user_gap_text = to_percentage(user_gap); - settings_.log.printf( - "New solution from primal heuristics. Objective %+.6e. Gap %s. Time %.2f\n", + settings_.log.print_format( + "New solution from primal heuristics. Objective {:+.6e}. Gap {}. Time {:.2f}\n", user_obj, - user_gap_text.c_str(), + user_gap_text, toc(exploration_stats_.start_time)); } else { - settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", - compute_user_objective(original_lp_, obj), - toc(exploration_stats_.start_time)); + settings_.log.print_format( + "New solution from primal heuristics. Objective {:+.6e}. Time {:.2f}\n", + compute_user_objective(original_lp_, obj), + toc(exploration_stats_.start_time)); } } } diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 53c3cef9db..93f4230e91 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -87,10 +87,10 @@ class logger_t { } template - void print_format(std::string_view fmt, Args&&... args) + void print_format(std::format_string fmt, Args&&... args) { if (log) { - const std::string msg = std::vformat(fmt, std::make_format_args(args...)); + const std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); @@ -141,10 +141,10 @@ class logger_t { } template - void debug_format(std::string_view fmt, Args&&... args) + void debug_format(std::format_string fmt, Args&&... args) { if (log) { - std::string msg = std::vformat(fmt, std::make_format_args(args...)); + std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); From b865ae95937bf2c46faf173af5fc6330c389d9ee Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 17:03:06 +0200 Subject: [PATCH 06/24] migrate node_id from int to uint64_t. bug fixes Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/bb_event.hpp | 8 ++-- cpp/src/branch_and_bound/branch_and_bound.cpp | 35 ++++++++--------- .../deterministic_workers.hpp | 7 +++- cpp/src/branch_and_bound/mip_node.hpp | 4 +- cpp/src/branch_and_bound/search_tree.hpp | 38 +++++++++++-------- cpp/src/dual_simplex/logger.hpp | 4 +- 6 files changed, 54 insertions(+), 42 deletions(-) diff --git a/cpp/src/branch_and_bound/bb_event.hpp b/cpp/src/branch_and_bound/bb_event.hpp index b5ef80a167..20b014fb70 100644 --- a/cpp/src/branch_and_bound/bb_event.hpp +++ b/cpp/src/branch_and_bound/bb_event.hpp @@ -25,8 +25,8 @@ enum class bb_event_type_t : int8_t { template struct branched_payload_t { - i_t down_child_id; - i_t up_child_id; + uint64_t down_child_id; + uint64_t up_child_id; f_t node_lower_bound; i_t branch_var; f_t branch_value; @@ -75,8 +75,8 @@ struct bb_event_t { static bb_event_t make_branched(double work_unit_ts, int worker, i_t node, - i_t down_id, - i_t up_id, + uint64_t down_id, + uint64_t up_id, f_t lower_bound, i_t branch_var, f_t branch_val) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 13cca92ece..45c95d7bf1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -330,10 +330,10 @@ void branch_and_bound_t::print_table_header() "Depth", "Iter/Node", "Gap", - "Completion"); + "Est. Progress"); if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } header += std::format("{:^8}|", "Time"); - settings_.log.printf("%s", header.c_str()); + settings_.log.printf("%s\n", header.c_str()); } template @@ -361,7 +361,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); - settings_.log.printf("%s", log_line.c_str()); + settings_.log.printf("%s\n", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { f_t user_obj = compute_user_objective(original_lp_, obj); @@ -410,7 +410,7 @@ void branch_and_bound_t::report( tree_completion); if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); - settings_.log.printf("%s", log_line.c_str()); + settings_.log.printf("%s\n", log_line.c_str()); } template @@ -999,8 +999,8 @@ struct nondeterministic_policy_t : tree_update_policy_t { { if (worker->search_strategy == search_strategy_t::BEST_FIRST) { fetch_min(bnb.lower_bound_numerical_, node->lower_bound); - log.printf("LP returned numerical issue on node %d. Best bound set to %+10.6e.\n", - node->node_id, + log.print_format("LP returned numerical issue on node {}. Best bound set to {:+10.6e}.\n", + node->node_id, compute_user_objective(bnb.original_lp_, bnb.lower_bound_numerical_.load())); } } @@ -1254,15 +1254,15 @@ std::pair branch_and_bound_t::updat #ifdef DEBUG_FRACTIONAL_FIXED for (i_t j : leaf_fractional) { if (leaf_problem.lower[j] == leaf_problem.upper[j]) { - printf( - "Node %d: Fixed variable %d has a fractional value %e. Lower %e upper %e. Variable " - "status %d\n", + settings_.log.print_format( + "Node {}: Fixed variable {} has a fractional value {:e}. Lower {:e} upper {:e}. " + "Variable status {}\n", node_ptr->node_id, j, leaf_solution.x[j], leaf_problem.lower[j], leaf_problem.upper[j], - node_ptr->vstatus[j]); + static_cast(node_ptr->vstatus[j])); } } #endif @@ -1480,10 +1480,10 @@ dual::status_t branch_and_bound_t::solve_node_lp( ss >> logname; lp_settings.log.set_log_file(logname, "a"); lp_settings.log.log_to_console = false; - lp_settings.log.printf( - "%scurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " - "%f, variable lower bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", - settings_.log.log_prefix.c_str(), + lp_settings.log.print_format( + "{}current node: id = {}, depth = {}, branch var = {}, branch dir = {}, fractional val = " + "{:f}, variable lower bound = {:f}, variable upper bound = {:f}, branch vstatus = {}\n\n", + settings_.log.log_prefix, node_ptr->node_id, node_ptr->depth, node_ptr->branch_var, @@ -1491,7 +1491,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( node_ptr->fractional_val, node_ptr->branch_var_lower, node_ptr->branch_var_upper, - node_ptr->vstatus[node_ptr->branch_var]); + static_cast(node_ptr->vstatus[node_ptr->branch_var])); #endif bool feasible = worker->set_lp_variable_bounds(node_ptr, settings_); @@ -1523,7 +1523,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( worker->leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.debug("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); + log.debug_format("Numerical issue node {}. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker->leaf_problem, lp_start_time, @@ -3491,7 +3491,8 @@ node_status_t branch_and_bound_t::solve_node_deterministic( &worker.work_context); if (lp_status == dual::status_t::NUMERICAL) { - settings_.log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); + settings_.log.print_format("Numerical issue node {}. Resolving from scratch.\n", + node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker.leaf_problem, lp_start_time, lp_settings, diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index fb1c0f450f..835c81f388 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -210,8 +210,11 @@ class deterministic_bfs_worker_t events.add(std::move(event)); } - void record_branched( - mip_node_t* node, i_t down_child_id, i_t up_child_id, i_t branch_var, f_t branch_val) + void record_branched(mip_node_t* node, + uint64_t down_child_id, + uint64_t up_child_id, + i_t branch_var, + f_t branch_val) { record_event(bb_event_t::make_branched(this->clock, this->worker_id, diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index ad53aaabb9..56dbf3f603 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -220,7 +220,7 @@ class mip_node_t { if (current_node->children[0] == nullptr && current_node->children[1] == nullptr && current_node->depth < 10) { - printf("Node %d with no children at depth %d lower bound %e. status %d\n", + printf("Node %ld with no children at depth %d lower bound %e. status %d\n", current_node->node_id, current_node->depth, current_node->lower_bound, @@ -269,7 +269,7 @@ class mip_node_t { f_t lower_bound; f_t objective_estimate; i_t depth; - i_t node_id; + uint64_t node_id; i_t branch_var; branch_direction_t branch_dir; f_t branch_var_lower; diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index e3a6ed9901..0ec7c36f70 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -14,9 +14,9 @@ namespace cuopt::linear_programming::dual_simplex { template class search_tree_t { public: - search_tree_t() : num_nodes(0) {} + search_tree_t() = default; - search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} + search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } ~search_tree_t() { clean(); } @@ -29,7 +29,7 @@ class search_tree_t { ++num_inner_nodes; } else { ++num_final_nodes; - progress += std::pow(2, -node_ptr->depth); + progress += std::ldexp(f_t(1), -node_ptr->depth); } std::vector*> stack; @@ -45,7 +45,7 @@ class search_tree_t { const lp_problem_t& original_lp, logger_t& log) { - i_t id = num_nodes.fetch_add(2); + uint64_t id = num_nodes.fetch_add(2); auto down_child = std::make_unique>(original_lp, parent_node, @@ -90,7 +90,7 @@ class search_tree_t { const f_t val) { if (write_graphviz) { - log.printf("Node%d [label=\"%s %.16e\"]\n", node_ptr->node_id, label.c_str(), val); + log.print_format("Node{} [label=\"{} {:.16e}\"]\n", node_ptr->node_id, label, val); } } @@ -102,12 +102,12 @@ class search_tree_t { 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); + log.print_format("Node{} -> Node{} [label=\"x{} {} {:e}\"]\n", + origin_ptr->node_id, + dest_ptr->node_id, + branch_var, + branch_dir == branch_direction_t::DOWN ? "<=" : ">=", + bound); } } @@ -126,23 +126,29 @@ class search_tree_t { if (node->children[1]) stack.push_back(std::move(node->children[1])); // Implicitly call destructor for `node` } + + num_nodes = 0; + num_open_nodes = 0; + num_final_nodes = 0; + num_inner_nodes = 0; + progress = 0; } mip_node_t root; omp_mutex_t mutex; - omp_atomic_t num_nodes; + omp_atomic_t num_nodes = 0; // Number of nodes that still needs to be explored - omp_atomic_t num_open_nodes; + omp_atomic_t num_open_nodes = 0; // Number of integer feasible, infeasible or fathomed nodes - omp_atomic_t num_final_nodes; + omp_atomic_t num_final_nodes = 0; // Number of inner nodes - omp_atomic_t num_inner_nodes; + omp_atomic_t num_inner_nodes = 0; // Track the solver progress based on how much the tree was explored - omp_atomic_t progress = 0.0; + omp_atomic_t progress = 0; static constexpr bool write_graphviz = false; }; diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 93f4230e91..2ed8045ec4 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -90,9 +90,10 @@ class logger_t { void print_format(std::format_string fmt, Args&&... args) { if (log) { - const std::string msg = std::format(fmt, std::forward(args)...); + std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL + if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); #else std::printf("%s", msg.c_str()); @@ -147,6 +148,7 @@ class logger_t { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL + if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); #else std::printf("%s", msg.c_str()); From 6d7f0f5c63eccaf7f615bd9e57416547ec59f6cd Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 11:15:10 +0200 Subject: [PATCH 07/24] revert changes to node_id. added exception handling to the destructor. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/bb_event.hpp | 8 +++--- cpp/src/branch_and_bound/branch_and_bound.cpp | 24 ++++++++--------- .../deterministic_workers.hpp | 7 ++--- cpp/src/branch_and_bound/search_tree.hpp | 27 +++++++++++++++---- cpp/src/dual_simplex/logger.hpp | 1 - 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/cpp/src/branch_and_bound/bb_event.hpp b/cpp/src/branch_and_bound/bb_event.hpp index 20b014fb70..b5ef80a167 100644 --- a/cpp/src/branch_and_bound/bb_event.hpp +++ b/cpp/src/branch_and_bound/bb_event.hpp @@ -25,8 +25,8 @@ enum class bb_event_type_t : int8_t { template struct branched_payload_t { - uint64_t down_child_id; - uint64_t up_child_id; + i_t down_child_id; + i_t up_child_id; f_t node_lower_bound; i_t branch_var; f_t branch_value; @@ -75,8 +75,8 @@ struct bb_event_t { static bb_event_t make_branched(double work_unit_ts, int worker, i_t node, - uint64_t down_id, - uint64_t up_id, + i_t down_id, + i_t up_id, f_t lower_bound, i_t branch_var, f_t branch_val) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 45c95d7bf1..2e2070b9c6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -999,8 +999,8 @@ struct nondeterministic_policy_t : tree_update_policy_t { { if (worker->search_strategy == search_strategy_t::BEST_FIRST) { fetch_min(bnb.lower_bound_numerical_, node->lower_bound); - log.print_format("LP returned numerical issue on node {}. Best bound set to {:+10.6e}.\n", - node->node_id, + log.printf("LP returned numerical issue on node %d. Best bound set to %+10.6e.\n", + node->node_id, compute_user_objective(bnb.original_lp_, bnb.lower_bound_numerical_.load())); } } @@ -1254,15 +1254,15 @@ std::pair branch_and_bound_t::updat #ifdef DEBUG_FRACTIONAL_FIXED for (i_t j : leaf_fractional) { if (leaf_problem.lower[j] == leaf_problem.upper[j]) { - settings_.log.print_format( - "Node {}: Fixed variable {} has a fractional value {:e}. Lower {:e} upper {:e}. " - "Variable status {}\n", + printf( + "Node %d: Fixed variable %d has a fractional value %e. Lower %e upper %e. Variable " + "status %d\n", node_ptr->node_id, j, leaf_solution.x[j], leaf_problem.lower[j], leaf_problem.upper[j], - static_cast(node_ptr->vstatus[j])); + node_ptr->vstatus[j]); } } #endif @@ -1480,10 +1480,10 @@ dual::status_t branch_and_bound_t::solve_node_lp( ss >> logname; lp_settings.log.set_log_file(logname, "a"); lp_settings.log.log_to_console = false; - lp_settings.log.print_format( - "{}current node: id = {}, depth = {}, branch var = {}, branch dir = {}, fractional val = " - "{:f}, variable lower bound = {:f}, variable upper bound = {:f}, branch vstatus = {}\n\n", - settings_.log.log_prefix, + lp_settings.log.printf( + "%scurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = " + "%f, variable lower bound = %f, variable upper bound = %f, branch vstatus = %d\n\n", + settings_.log.log_prefix.c_str(), node_ptr->node_id, node_ptr->depth, node_ptr->branch_var, @@ -1491,7 +1491,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( node_ptr->fractional_val, node_ptr->branch_var_lower, node_ptr->branch_var_upper, - static_cast(node_ptr->vstatus[node_ptr->branch_var])); + node_ptr->vstatus[node_ptr->branch_var]); #endif bool feasible = worker->set_lp_variable_bounds(node_ptr, settings_); @@ -1523,7 +1523,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( worker->leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.debug_format("Numerical issue node {}. Resolving from scratch.\n", node_ptr->node_id); + log.debug("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker->leaf_problem, lp_start_time, diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index 835c81f388..fb1c0f450f 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -210,11 +210,8 @@ class deterministic_bfs_worker_t events.add(std::move(event)); } - void record_branched(mip_node_t* node, - uint64_t down_child_id, - uint64_t up_child_id, - i_t branch_var, - f_t branch_val) + void record_branched( + mip_node_t* node, i_t down_child_id, i_t up_child_id, i_t branch_var, f_t branch_val) { record_event(bb_event_t::make_branched(this->clock, this->worker_id, diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index 0ec7c36f70..85d001e773 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -18,7 +18,24 @@ class search_tree_t { search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } - ~search_tree_t() { clean(); } + ~search_tree_t() + { + try { + clean(); // 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, + "search_tree_t destructor: iterative teardown failed (%s); falling back to " + "recursive unique_ptr destruction.\n", + e.what()); + } catch (...) { + std::fprintf(stderr, + "search_tree_t destructor: iterative teardown failed (unknown exception); " + "falling back to recursive unique_ptr destruction.\n"); + } + } void update(mip_node_t* node_ptr, node_status_t status) { @@ -136,16 +153,16 @@ class search_tree_t { mip_node_t root; omp_mutex_t mutex; - omp_atomic_t num_nodes = 0; + omp_atomic_t num_nodes = 0; // Number of nodes that still needs to be explored - omp_atomic_t num_open_nodes = 0; + omp_atomic_t num_open_nodes = 0; // Number of integer feasible, infeasible or fathomed nodes - omp_atomic_t num_final_nodes = 0; + omp_atomic_t num_final_nodes = 0; // Number of inner nodes - omp_atomic_t num_inner_nodes = 0; + omp_atomic_t num_inner_nodes = 0; // Track the solver progress based on how much the tree was explored omp_atomic_t progress = 0; diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 2ed8045ec4..f203ee1efb 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -12,7 +12,6 @@ #endif #include -#include #include #include From e5ea4992110d0d8c7f44c65fcea62b03ab35f127 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 13:15:19 +0200 Subject: [PATCH 08/24] address coderabbit Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 +++--- cpp/src/branch_and_bound/mip_node.hpp | 2 +- cpp/src/branch_and_bound/search_tree.hpp | 10 +++++++++- cpp/src/dual_simplex/logger.hpp | 7 ++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 2e2070b9c6..ed66e681af 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -347,7 +347,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", + std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", "", // nodes explored "", // nodes unexplored user_obj, @@ -397,7 +397,7 @@ void branch_and_bound_t::report( std::string tree_completion = to_percentage(search_tree_.progress.load()); std::string log_line = - std::format("{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", + std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", symbol, nodes_explored, nodes_unexplored, @@ -1523,7 +1523,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( worker->leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.debug("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); + log.debug_format("Numerical issue node {}. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker->leaf_problem, lp_start_time, diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 56dbf3f603..8fd27d24f2 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -269,7 +269,7 @@ class mip_node_t { f_t lower_bound; f_t objective_estimate; i_t depth; - uint64_t node_id; + i_t node_id; i_t branch_var; branch_direction_t branch_dir; f_t branch_var_lower; diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index 85d001e773..f61588e2ee 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -9,6 +9,14 @@ #include +#include + +#include +#include +#include +#include +#include + namespace cuopt::linear_programming::dual_simplex { template @@ -62,7 +70,7 @@ class search_tree_t { const lp_problem_t& original_lp, logger_t& log) { - uint64_t id = num_nodes.fetch_add(2); + i_t id = num_nodes.fetch_add(2); auto down_child = std::make_unique>(original_lp, parent_node, diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index f203ee1efb..3f384e1952 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace cuopt::linear_programming::dual_simplex { @@ -92,7 +93,7 @@ class logger_t { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL - if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } + std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); #else std::printf("%s", msg.c_str()); @@ -147,8 +148,8 @@ class logger_t { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL - if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } - CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); + std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; + CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg_view.c_str()); #else std::printf("%s", msg.c_str()); fflush(stdout); From da1015f2f57d2b57f2e042942f28d28d635bd7ab Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 19:40:30 +0200 Subject: [PATCH 09/24] revert log changes Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 12 +++++------- cpp/src/branch_and_bound/mip_node.hpp | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index ed66e681af..9f2434be43 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -373,10 +373,9 @@ void branch_and_bound_t::report_heuristic(f_t obj) user_gap_text, toc(exploration_stats_.start_time)); } else { - settings_.log.print_format( - "New solution from primal heuristics. Objective {:+.6e}. Time {:.2f}\n", - compute_user_objective(original_lp_, obj), - toc(exploration_stats_.start_time)); + settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", + compute_user_objective(original_lp_, obj), + toc(exploration_stats_.start_time)); } } } @@ -1523,7 +1522,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( worker->leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.debug_format("Numerical issue node {}. Resolving from scratch.\n", node_ptr->node_id); + log.debug("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker->leaf_problem, lp_start_time, @@ -3491,8 +3490,7 @@ node_status_t branch_and_bound_t::solve_node_deterministic( &worker.work_context); if (lp_status == dual::status_t::NUMERICAL) { - settings_.log.print_format("Numerical issue node {}. Resolving from scratch.\n", - node_ptr->node_id); + settings_.log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker.leaf_problem, lp_start_time, lp_settings, diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 8fd27d24f2..ad53aaabb9 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -220,7 +220,7 @@ class mip_node_t { if (current_node->children[0] == nullptr && current_node->children[1] == nullptr && current_node->depth < 10) { - printf("Node %ld with no children at depth %d lower bound %e. status %d\n", + printf("Node %d with no children at depth %d lower bound %e. status %d\n", current_node->node_id, current_node->depth, current_node->lower_bound, From 3ff82cc526d1e1960c7a38473fbd2e95728c00d2 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 3 Jun 2026 17:05:23 +0200 Subject: [PATCH 10/24] implemented a simple restart procedure for B&B. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 171 +++++++++++++----- cpp/src/branch_and_bound/branch_and_bound.hpp | 5 + cpp/src/branch_and_bound/node_queue.hpp | 9 +- cpp/src/branch_and_bound/search_tree.hpp | 1 + cpp/src/branch_and_bound/worker.hpp | 29 ++- cpp/src/branch_and_bound/worker_pool.hpp | 11 ++ .../dual_simplex/simplex_solver_settings.hpp | 6 + 7 files changed, 178 insertions(+), 54 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 9f2434be43..1ba4af56cf 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1549,6 +1549,49 @@ dual::status_t branch_and_bound_t::solve_node_lp( return lp_status; } +template +bool branch_and_bound_t::should_restart(f_t current_abs_gap) +{ + if (settings_.sub_mip || restart_count_ >= settings_.max_restarts) return false; + + i_t num_nodes = exploration_stats_.nodes_explored; + if (num_nodes < settings_.restart_min_nodes) return false; + + i_t nodes_since_last_check = num_nodes - exploration_stats_.restart_nodes_at_last_check; + if (nodes_since_last_check < settings_.restart_check_freq) return false; + + f_t current_progress = search_tree_.progress; + f_t progress_since_last_check = + std::max(current_progress - exploration_stats_.restart_progress_at_last_check, 1E-6); + i_t tree_size_estimate = + nodes_since_last_check * (1.0 - current_progress) / progress_since_last_check; + + f_t gap_reduction = exploration_stats_.restart_gap_at_last_check / current_abs_gap; + + settings_.log.debug_format( + "[Restart] Current: explored={}, progress={:.4g}, gap={:.4g}. Since last: explored={}, " + "progress={:.4g}, gap={:.4g}. Tree size estimate={}", + num_nodes, + current_progress, + current_abs_gap, + nodes_since_last_check, + progress_since_last_check, + gap_reduction, + tree_size_estimate); + + if (gap_reduction < 1.0 && tree_size_estimate >= settings_.restart_tree_size_factor * num_nodes) { + ++exploration_stats_.restart_large_tree_count; + return exploration_stats_.restart_large_tree_count >= + settings_.restart_consecutive_large_tree_estimate; + } + + exploration_stats_.restart_large_tree_count = 0; + exploration_stats_.restart_gap_at_last_check = current_abs_gap; + exploration_stats_.restart_progress_at_last_check = current_progress; + exploration_stats_.restart_nodes_at_last_check = num_nodes; + return false; +} + template void branch_and_bound_t::plunge_with(bfs_worker_t* worker, mip_node_t* start_node) @@ -1572,7 +1615,14 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { - if (worker->worker_id == 0) { repair_heuristic_solutions(); } + if (worker->worker_id == 0) { + if (should_restart(abs_gap)) { + solver_status_ = mip_status_t::RESTART; + break; + } + + repair_heuristic_solutions(); + } if (worker->total_active_diving_workers < worker->total_max_diving_workers && worker->node_queue.diving_queue_size() > 0) { @@ -2835,20 +2885,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - // Choose variable to branch on - i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x); - - search_tree_.root = std::move(mip_node_t(root_objective_, root_vstatus_)); - search_tree_.num_nodes = 0; - search_tree_.graphviz_node(settings_.log, &search_tree_.root, "lower bound", root_objective_); - search_tree_.branch(&search_tree_.root, - branch_var, - root_relax_soln_.x[branch_var], - num_fractional, - root_vstatus_, - original_lp_, - log); - if (symmetry_ != nullptr) { i_t removed = symmetry_->generators.template prune_by_bounds(original_lp_.lower, original_lp_.upper); @@ -2861,49 +2897,84 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } + if (settings_.diving_settings.coefficient_diving != 0) { + calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); + } + settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); - node_concurrent_halt_ = 0; - exploration_stats_.nodes_explored = 0; - exploration_stats_.nodes_unexplored = 2; - exploration_stats_.nodes_since_last_log = 0; - exploration_stats_.last_log = tic(); - min_node_queue_size_ = 20; + const i_t num_workers = settings_.num_threads; + const i_t num_bfs_workers = std::max(settings_.num_threads / 2, 1); + const i_t num_diving_workers = num_workers - num_bfs_workers; + bfs_worker_pool_.init(num_bfs_workers, original_lp_, Arow_, var_types_, symmetry_, settings_); - if (settings_.diving_settings.coefficient_diving != 0) { - calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); + if (num_diving_workers > 0) { + diving_worker_pool_.init( + num_diving_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, num_bfs_workers); } - print_table_header(); -#pragma omp taskgroup - { - if (settings_.deterministic) { - run_deterministic_coordinator(Arow_); - } else { - const i_t num_workers = settings_.num_threads; - const i_t num_bfs_workers = std::max(settings_.num_threads / 2, 1); - const i_t num_diving_workers = num_workers - num_bfs_workers; - bfs_worker_pool_.init(num_bfs_workers, original_lp_, Arow_, var_types_, symmetry_, settings_); + restart_count_ = 0; + min_node_queue_size_ = 20; - if (num_diving_workers > 0) { - diving_worker_pool_.init(num_diving_workers, - original_lp_, - Arow_, - var_types_, - symmetry_, - settings_, - num_bfs_workers); - } + do { + if (toc(exploration_stats_.start_time) > settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + break; + } - bfs_worker_t* initial_worker = bfs_worker_pool_.pop_idle_worker(); - node_queue_t& node_queue = initial_worker->node_queue; - node_queue.push_lockfree(search_tree_.root.get_down_child()); - node_queue.push_lockfree(search_tree_.root.get_up_child()); - initial_worker->lower_bound = initial_worker->node_queue.get_lower_bound(); - initial_worker->set_active(); - best_first_search_with(initial_worker); + if (solver_status_ == mip_status_t::RESTART) { + settings_.log.print_format("\n\nRestarting B&B after {}s and {} nodes\n", + toc(exploration_stats_.start_time), + exploration_stats_.nodes_explored.load()); + search_tree_.clean(); + bfs_worker_pool_.reset(); + diving_worker_pool_.reset(); + solver_status_ = mip_status_t::UNSET; + ++restart_count_; } - } // Implicit barrier for all tasks created within the group (RINS, B&B workers) + + // Choose variable to branch on + i_t branch_var = pc_.variable_selection(fractional, root_relax_soln_.x); + + search_tree_.root = std::move(mip_node_t(root_objective_, root_vstatus_)); + search_tree_.num_nodes = 0; + search_tree_.graphviz_node(settings_.log, &search_tree_.root, "lower bound", root_objective_); + search_tree_.branch(&search_tree_.root, + branch_var, + root_relax_soln_.x[branch_var], + num_fractional, + root_vstatus_, + original_lp_, + log); + + node_concurrent_halt_ = 0; + exploration_stats_.nodes_explored = 0; + exploration_stats_.nodes_unexplored = 2; + exploration_stats_.nodes_since_last_log = 0; + exploration_stats_.last_log = tic(); + exploration_stats_.restart_large_tree_count = 0; + exploration_stats_.restart_gap_at_last_check = upper_bound_ - get_lower_bound(); + exploration_stats_.restart_nodes_at_last_check = 0; + exploration_stats_.restart_progress_at_last_check = 0; + + print_table_header(); + +#pragma omp taskgroup + { + if (settings_.deterministic) { + run_deterministic_coordinator(Arow_); + } else { + bfs_worker_t* initial_worker = bfs_worker_pool_.pop_idle_worker(); + node_queue_t& node_queue = initial_worker->node_queue; + node_queue.push_lockfree(search_tree_.root.get_down_child()); + node_queue.push_lockfree(search_tree_.root.get_up_child()); + initial_worker->lower_bound = initial_worker->node_queue.get_lower_bound(); + initial_worker->set_active(); + best_first_search_with(initial_worker); + } + } // Implicit barrier for all tasks created within the group (RINS, B&B workers) + + } while (solver_status_ == mip_status_t::RESTART); is_running_ = false; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 77d02ccfb1..31d050008e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -60,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 @@ -264,6 +265,8 @@ class branch_and_bound_t { omp_atomic_t lower_bound_numerical_; std::function user_bound_callback_; + i_t restart_count_; + void print_table_header(); void report_heuristic(f_t obj); void report(char symbol, @@ -316,6 +319,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* bfs_worker); diff --git a/cpp/src/branch_and_bound/node_queue.hpp b/cpp/src/branch_and_bound/node_queue.hpp index b07b371b81..f63aa4587f 100644 --- a/cpp/src/branch_and_bound/node_queue.hpp +++ b/cpp/src/branch_and_bound/node_queue.hpp @@ -62,7 +62,7 @@ class heap_t { void clear() { - buffer.clear(); + buffer = {}; num_entries_ = 0; } @@ -157,6 +157,13 @@ class node_queue_t { return best_first_heap_.empty() ? std::numeric_limits::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* node = nullptr; diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index f61588e2ee..570c1790cd 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -173,6 +173,7 @@ class search_tree_t { omp_atomic_t num_inner_nodes = 0; // Track the solver progress based on how much the tree was explored + // using the tree weight metric omp_atomic_t progress = 0; static constexpr bool write_graphviz = false; diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 29a3164dfc..4a0bf1361c 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -23,17 +23,24 @@ namespace cuopt::linear_programming::dual_simplex { template struct branch_and_bound_stats_t { - f_t start_time = 0.0; - omp_atomic_t total_lp_solve_time = 0.0; + f_t start_time = 0.0; + + omp_atomic_t total_lp_solve_time = 0.0; + omp_atomic_t total_lp_iters = 0; + omp_atomic_t nodes_explored = 0; omp_atomic_t nodes_unexplored = 0; // Tracks the number of nodes being solved by the workers at a given time omp_atomic_t nodes_being_solved = 0; - omp_atomic_t total_lp_iters = 0; omp_atomic_t nodes_since_last_log = 0; omp_atomic_t last_log = 0.0; + i_t restart_nodes_at_last_check = 0; + f_t restart_progress_at_last_check = 0; + f_t restart_gap_at_last_check = 0; + i_t restart_large_tree_count = 0; + omp_atomic_t orbital_fixing_nodes = 0; omp_atomic_t orbital_fixings_applied = 0; omp_atomic_t orbital_conflict_nodes = 0; @@ -219,6 +226,15 @@ class bfs_worker_t : public branch_and_bound_worker_t { } } + void reset_state() + { + node_queue.clear(); + total_max_diving_workers = 0; + total_active_diving_workers = 0; + this->is_active = false; + this->lower_bound = -std::numeric_limits::infinity(); + } + // The worker-local node heap. node_queue_t node_queue; @@ -265,6 +281,13 @@ class diving_worker_t : public branch_and_bound_worker_t { f_t get_lower_bound() { return this->lower_bound; } + void reset_state() + { + this->is_active = false; + this->lower_bound = -std::numeric_limits::infinity(); + bfs_worker = nullptr; + } + mip_node_t start_node; // The best-first worker that is associated with this diving worker. Used for controlling the diff --git a/cpp/src/branch_and_bound/worker_pool.hpp b/cpp/src/branch_and_bound/worker_pool.hpp index d75794bbf0..858d3949be 100644 --- a/cpp/src/branch_and_bound/worker_pool.hpp +++ b/cpp/src/branch_and_bound/worker_pool.hpp @@ -91,6 +91,17 @@ class worker_pool_t { i_t num_idle() const { return num_idle_workers_; } i_t size() const { return workers_.size(); } + void reset() + { + std::lock_guard lock(mutex_); + num_idle_workers_ = workers_.size(); + idle_workers_.clear_resize(workers_.size()); + for (i_t i = 0; i < workers_.size(); ++i) { + workers_[i]->reset_state(); + idle_workers_.push_back(i); + } + } + private: std::vector> workers_; bool is_initialized_ = false; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 27eac7f985..c004f9a6ae 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -224,6 +224,12 @@ struct simplex_solver_settings_t { i_t bnb_nodes_per_steal; i_t bnb_max_steal_attempts; + i_t restart_min_nodes = 10000; + i_t restart_consecutive_large_tree_estimate = 10; + i_t restart_tree_size_factor = 25; + i_t restart_check_freq = 100; + i_t max_restarts = 3; + // Settings for the reliability branching. // - -1: automatic // - 0: disable (use pseudocost branching instead) From 27c4a791ee3b24e6ace2f786b858b2641d51cf8e Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 3 Jun 2026 18:06:35 +0200 Subject: [PATCH 11/24] added total nodes explored in the restart heuristic. tweak parameters Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 31 ++++++++++++------- cpp/src/branch_and_bound/worker.hpp | 5 +-- .../dual_simplex/simplex_solver_settings.hpp | 12 ++++--- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 1ba4af56cf..1b62162027 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1554,7 +1554,8 @@ bool branch_and_bound_t::should_restart(f_t current_abs_gap) { if (settings_.sub_mip || restart_count_ >= settings_.max_restarts) return false; - i_t num_nodes = exploration_stats_.nodes_explored; + i_t num_nodes = exploration_stats_.nodes_explored; + i_t total_nodes = exploration_stats_.total_nodes_explored; if (num_nodes < settings_.restart_min_nodes) return false; i_t nodes_since_last_check = num_nodes - exploration_stats_.restart_nodes_at_last_check; @@ -1564,6 +1565,7 @@ bool branch_and_bound_t::should_restart(f_t current_abs_gap) f_t progress_since_last_check = std::max(current_progress - exploration_stats_.restart_progress_at_last_check, 1E-6); i_t tree_size_estimate = + exploration_stats_.restart_nodes_at_last_check + nodes_since_last_check * (1.0 - current_progress) / progress_since_last_check; f_t gap_reduction = exploration_stats_.restart_gap_at_last_check / current_abs_gap; @@ -1579,10 +1581,13 @@ bool branch_and_bound_t::should_restart(f_t current_abs_gap) gap_reduction, tree_size_estimate); - if (gap_reduction < 1.0 && tree_size_estimate >= settings_.restart_tree_size_factor * num_nodes) { + if (gap_reduction < 1.05 && + tree_size_estimate >= settings_.restart_tree_size_factor * total_nodes) { ++exploration_stats_.restart_large_tree_count; + i_t min_count = + settings_.restart_min_estimates + total_nodes * settings_.restart_threshold_grow_per_node; return exploration_stats_.restart_large_tree_count >= - settings_.restart_consecutive_large_tree_estimate; + min_count * std::pow(settings_.restart_threshold_grow_per_restart, restart_count_); } exploration_stats_.restart_large_tree_count = 0; @@ -1690,6 +1695,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, dual::status_t lp_status = solve_node_lp(node_ptr, worker, exploration_stats_, settings_.log); ++exploration_stats_.nodes_since_last_log; ++exploration_stats_.nodes_explored; + ++exploration_stats_.total_nodes_explored; --exploration_stats_.nodes_unexplored; --exploration_stats_.nodes_being_solved; @@ -2488,13 +2494,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut raft::common::nvtx::range scope("BB::solve"); logger_t log; - log.log = false; - log.log_prefix = settings_.log.log_prefix; - solver_status_ = mip_status_t::UNSET; - is_running_ = false; - root_lp_current_lower_bound_ = -inf; - exploration_stats_.nodes_unexplored = 0; - exploration_stats_.nodes_explored = 0; + log.log = false; + log.log_prefix = settings_.log.log_prefix; + solver_status_ = mip_status_t::UNSET; + is_running_ = false; + root_lp_current_lower_bound_ = -inf; + exploration_stats_.nodes_unexplored = 0; + exploration_stats_.total_nodes_explored = 0; + exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); settings_.log.printf("Reduced cost strengthening enabled: %d\n", @@ -2923,7 +2930,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (solver_status_ == mip_status_t::RESTART) { - settings_.log.print_format("\n\nRestarting B&B after {}s and {} nodes\n", + settings_.log.print_format("\n\nRestarting B&B after {:.2f}s and {} nodes\n", toc(exploration_stats_.start_time), exploration_stats_.nodes_explored.load()); search_tree_.clean(); @@ -2951,7 +2958,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_explored = 0; exploration_stats_.nodes_unexplored = 2; exploration_stats_.nodes_since_last_log = 0; - exploration_stats_.last_log = tic(); + exploration_stats_.last_log = 0; exploration_stats_.restart_large_tree_count = 0; exploration_stats_.restart_gap_at_last_check = upper_bound_ - get_lower_bound(); exploration_stats_.restart_nodes_at_last_check = 0; diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 4a0bf1361c..80a7375867 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -28,8 +28,9 @@ struct branch_and_bound_stats_t { omp_atomic_t total_lp_solve_time = 0.0; omp_atomic_t total_lp_iters = 0; - omp_atomic_t nodes_explored = 0; - omp_atomic_t nodes_unexplored = 0; + omp_atomic_t nodes_explored = 0; + omp_atomic_t total_nodes_explored = 0; + omp_atomic_t nodes_unexplored = 0; // Tracks the number of nodes being solved by the workers at a given time omp_atomic_t nodes_being_solved = 0; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index c004f9a6ae..04ab5537ee 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -224,11 +224,13 @@ struct simplex_solver_settings_t { i_t bnb_nodes_per_steal; i_t bnb_max_steal_attempts; - i_t restart_min_nodes = 10000; - i_t restart_consecutive_large_tree_estimate = 10; - i_t restart_tree_size_factor = 25; - i_t restart_check_freq = 100; - i_t max_restarts = 3; + i_t restart_min_nodes = 1000; + i_t restart_min_estimates = 10; + f_t restart_threshold_grow_per_node = 0.001; + f_t restart_threshold_grow_per_restart = 1.5; + i_t restart_tree_size_factor = 50; + i_t restart_check_freq = 100; + i_t max_restarts = 50; // Settings for the reliability branching. // - -1: automatic From ffdc469f891289db9225caa727c90a44ac4d87f2 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:01:29 +0200 Subject: [PATCH 12/24] removed search progress from the logs. It is too inaccurate, especially with restarts. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 9f2434be43..5af526d1f7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -319,18 +319,16 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) template void branch_and_bound_t::print_table_header() { - std::string header = - std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap", - "Est. Progress"); + std::string header = std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap"); if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } header += std::format("{:^8}|", "Time"); settings_.log.printf("%s\n", header.c_str()); @@ -347,7 +345,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", + std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11}", "", // nodes explored "", // nodes unexplored user_obj, @@ -355,9 +353,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) "", // integer infeasible "", // depth "", // iter/node - user_gap_text, - "" // tree progress - ); + user_gap_text); if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); @@ -396,7 +392,7 @@ void branch_and_bound_t::report( std::string tree_completion = to_percentage(search_tree_.progress.load()); std::string log_line = - std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", + std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11}", symbol, nodes_explored, nodes_unexplored, @@ -405,8 +401,7 @@ void branch_and_bound_t::report( node_int_infeas, node_depth, iter_node, - user_gap_text, - tree_completion); + user_gap_text); if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); settings_.log.printf("%s\n", log_line.c_str()); From e47c61f32174455b54a634b306b131e17a723a9f Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:02:38 +0200 Subject: [PATCH 13/24] removed try-catch block in the destructor. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/search_tree.hpp | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index f61588e2ee..6450cf30dc 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -26,24 +26,7 @@ class search_tree_t { search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } - ~search_tree_t() - { - try { - clean(); // 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, - "search_tree_t destructor: iterative teardown failed (%s); falling back to " - "recursive unique_ptr destruction.\n", - e.what()); - } catch (...) { - std::fprintf(stderr, - "search_tree_t destructor: iterative teardown failed (unknown exception); " - "falling back to recursive unique_ptr destruction.\n"); - } - } + ~search_tree_t() { clean(); } void update(mip_node_t* node_ptr, node_status_t status) { From 683dc527276739e193f529b2d12e593d4741d34e Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 13:33:33 +0200 Subject: [PATCH 14/24] fixed time gap between restarts. call reduced cost fixing after each restart Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 207 ++++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 2 + cpp/src/branch_and_bound/pseudo_costs.cpp | 14 +- cpp/src/branch_and_bound/pseudo_costs.hpp | 1 + cpp/src/dual_simplex/solve.cpp | 8 +- cpp/src/mip_heuristics/diversity/lns/rins.cu | 8 +- .../diversity/recombiners/sub_mip.cuh | 8 +- cpp/src/mip_heuristics/solver.cu | 1 + cpp/src/mip_heuristics/solver_context.cuh | 2 + 9 files changed, 153 insertions(+), 98 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 9deb9777eb..519e30c081 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -236,6 +236,7 @@ branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, f_t start_time, + std::atomic* restart_concurrent_halt, const probing_implied_bound_t& probing_implied_bound, std::shared_ptr> clique_table, mip_symmetry_t* symmetry) @@ -249,6 +250,7 @@ branch_and_bound_t::branch_and_bound_t( incumbent_(1), root_relax_soln_(1, 1), root_crossover_soln_(1, 1), + restart_concurrent_halt_(restart_concurrent_halt), pc_(1, solver_settings), solver_status_(mip_status_t::UNSET) { @@ -285,6 +287,7 @@ branch_and_bound_t::branch_and_bound_t( upper_bound_ = inf; root_objective_ = std::numeric_limits::quiet_NaN(); root_lp_current_lower_bound_ = -inf; + pc_.concurrent_halt_ = &node_concurrent_halt_; } template @@ -381,15 +384,14 @@ void branch_and_bound_t::report( char symbol, f_t obj, f_t lower_bound, i_t node_depth, i_t node_int_infeas, double work_time) { update_user_bound(lower_bound); - const i_t nodes_explored = exploration_stats_.nodes_explored; - const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - const f_t user_obj = compute_user_objective(original_lp_, obj); - const f_t user_lower = compute_user_objective(original_lp_, lower_bound); - const f_t iters = static_cast(exploration_stats_.total_lp_iters); - const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; - f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); - std::string user_gap_text = to_percentage(user_gap); - std::string tree_completion = to_percentage(search_tree_.progress.load()); + const i_t nodes_explored = exploration_stats_.nodes_explored; + const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + const f_t user_obj = compute_user_objective(original_lp_, obj); + const f_t user_lower = compute_user_objective(original_lp_, lower_bound); + const f_t iters = static_cast(exploration_stats_.total_lp_iters); + const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; + f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + std::string user_gap_text = to_percentage(user_gap); std::string log_line = std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11}", @@ -1596,6 +1598,8 @@ template void branch_and_bound_t::plunge_with(bfs_worker_t* worker, mip_node_t* start_node) { + raft::common::nvtx::range scope_guess("BB::plunge"); + assert(worker != nullptr && worker->is_active.load()); assert(start_node != nullptr); @@ -1617,21 +1621,18 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { if (worker->worker_id == 0) { if (should_restart(abs_gap)) { - solver_status_ = mip_status_t::RESTART; + mip_node_t* node = stack.front(); + report(' ', upper_bound_, lower_bound, node->depth, node->integer_infeasible); + solver_status_ = mip_status_t::RESTART; + node_concurrent_halt_ = 1; + *restart_concurrent_halt_ = 1; break; } repair_heuristic_solutions(); } - if (worker->total_active_diving_workers < worker->total_max_diving_workers && - worker->node_queue.diving_queue_size() > 0) { - launch_diving_worker(worker); - } - - if (bfs_worker_pool_.num_idle() > 0 && worker->node_queue.best_first_queue_size() > 0) { - launch_bfs_worker(worker); - } + if (*restart_concurrent_halt_ == 1) { break; } assert(stack.size() <= 2); mip_node_t* node_ptr = stack.front(); @@ -1752,6 +1753,15 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, upper_bound = upper_bound_; rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); + + if (worker->total_active_diving_workers < worker->total_max_diving_workers && + worker->node_queue.diving_queue_size() > 0) { + launch_diving_worker(worker); + } + + if (bfs_worker_pool_.num_idle() > 0 && worker->node_queue.best_first_queue_size() > 0) { + launch_bfs_worker(worker); + } } // If the solver exits early without consuming the local stack, or converged according to @@ -1771,6 +1781,8 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, template void branch_and_bound_t::launch_bfs_worker(bfs_worker_t* worker) { + raft::common::nvtx::range scope_guess("BB::launch_bfs_worker"); + bfs_worker_t* idle_worker = bfs_worker_pool_.pop_idle_worker(); if (!idle_worker) return; @@ -1804,6 +1816,8 @@ void branch_and_bound_t::launch_bfs_worker(bfs_worker_t* wor template void branch_and_bound_t::work_stealing(bfs_worker_t* worker) { + raft::common::nvtx::range scope_guess("BB::work_stealing"); + i_t nodes_to_steal = settings_.bnb_nodes_per_steal >= 0 ? settings_.bnb_nodes_per_steal : MIP_DEFAULT_NODES_PER_STEAL; i_t max_attempts = settings_.bnb_max_steal_attempts >= 0 ? settings_.bnb_max_steal_attempts @@ -1818,6 +1832,8 @@ void branch_and_bound_t::work_stealing(bfs_worker_t* worker) template void branch_and_bound_t::best_first_search_with(bfs_worker_t* worker) { + raft::common::nvtx::range scope_guess("BB::bfs_worker"); + f_t lower_bound = get_lower_bound(); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), lower_bound); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); @@ -1849,6 +1865,8 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t break; } + if (*restart_concurrent_halt_ == 1) { break; } + // Pre-emptively set the lower bound of the worker worker->lower_bound = node_queue.get_lower_bound(); mip_node_t* start_node = node_queue.pop(); @@ -1876,6 +1894,8 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t break; } + if (solver_status_ == mip_status_t::RESTART) { break; } + // Steal a node with some probability or when it is empty. The victim is determined at random. if (node_queue.best_first_queue_size() == 0 || worker->rng.next_double() < steal_chance) { work_stealing(worker); @@ -1893,7 +1913,7 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t template void branch_and_bound_t::dive_with(diving_worker_t* worker) { - raft::common::nvtx::range scope("BB::diving_thread"); + raft::common::nvtx::range scope("BB::diving_worker"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } logger_t log; log.log = false; @@ -1942,6 +1962,7 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker) break; } if (dive_stats.nodes_explored >= diving_node_limit) { break; } + if (*restart_concurrent_halt_ == 1) { break; } dual::status_t lp_status = solve_node_lp(node_ptr, worker, dive_stats, log); ++dive_stats.nodes_explored; @@ -1987,6 +2008,8 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker) template bool branch_and_bound_t::launch_diving_worker(bfs_worker_t* bfs_worker) { + raft::common::nvtx::range scope_guess("BB::launch_diving_worker"); + // Get an idle worker. diving_worker_t* diving_worker = diving_worker_pool_.pop_idle_worker(); if (diving_worker == nullptr) { return false; } @@ -2056,6 +2079,8 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( std::vector& nonbasic_list, std::vector& edge_norms) { + raft::common::nvtx::range scope_guess("BB::solve_root_relaxation"); + f_t start_time = tic(); f_t user_objective = 0; i_t iter = 0; @@ -2227,6 +2252,8 @@ auto branch_and_bound_t::do_cut_pass( i_t& cut_pool_size, [[maybe_unused]] const std::vector& saved_solution) -> cut_pass_result_t { + raft::common::nvtx::range scope_guess("BB::cut_pass"); + #ifdef PRINT_FRACTIONAL_INFO settings_.log.printf("Found %d fractional variables on cut pass %d\n", num_fractional, cut_pass); for (i_t j : fractional) { @@ -2493,12 +2520,28 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut log.log_prefix = settings_.log.log_prefix; solver_status_ = mip_status_t::UNSET; is_running_ = false; + restart_count_ = 0; + min_node_queue_size_ = 20; root_lp_current_lower_bound_ = -inf; exploration_stats_.nodes_unexplored = 0; exploration_stats_.total_nodes_explored = 0; exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); + if (settings_.diving_settings.coefficient_diving != 0) { + calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); + } + + const i_t num_workers = settings_.num_threads; + const i_t num_bfs_workers = std::max(settings_.num_threads / 2, 1); + const i_t num_diving_workers = num_workers - num_bfs_workers; + bfs_worker_pool_.init(num_bfs_workers, original_lp_, Arow_, var_types_, symmetry_, settings_); + + if (num_diving_workers > 0) { + diving_worker_pool_.init( + num_diving_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, num_bfs_workers); + } + settings_.log.printf("Reduced cost strengthening enabled: %d\n", settings_.reduced_cost_strengthening); @@ -2843,82 +2886,69 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return solver_status_; } - 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(upper_bound_.load(), lower_bounds, upper_bounds); - if (num_fixed > 0) { - std::vector bounds_changed(original_lp_.num_cols, true); - std::vector row_sense; - - bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); - - mutex_original_lp_.lock(); - original_lp_.lower = lower_bounds; - original_lp_.upper = upper_bounds; - bool feasible = node_presolve.bounds_strengthening( - settings_, bounds_changed, original_lp_.lower, original_lp_.upper); - mutex_original_lp_.unlock(); - if (!feasible) { - settings_.log.printf("Bound strengthening failed\n"); - return mip_status_t::NUMERICAL; // We had a feasible integer solution, but bound - // strengthening thinks we are infeasible. - } - // Go through and check the fractional variables and remove any that are now fixed to their - // bounds - std::vector to_remove(fractional.size(), 0); - i_t num_to_remove = 0; - for (i_t k = 0; k < fractional.size(); k++) { - const i_t j = fractional[k]; - if (std::abs(original_lp_.upper[j] - original_lp_.lower[j]) < settings_.fixed_tol) { - to_remove[k] = 1; - num_to_remove++; + do { + if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { + last_upper_bound = upper_bound_.load(); + + std::vector lower_bounds; + std::vector 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; + + bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); + + mutex_original_lp_.lock(); + original_lp_.lower = lower_bounds; + original_lp_.upper = upper_bounds; + bool feasible = node_presolve.bounds_strengthening( + settings_, bounds_changed, original_lp_.lower, original_lp_.upper); + mutex_original_lp_.unlock(); + if (!feasible) { + settings_.log.printf("Bound strengthening failed\n"); + return mip_status_t::NUMERICAL; // We had a feasible integer solution, but bound + // strengthening thinks we are infeasible. } - } - if (num_to_remove > 0) { - std::vector new_fractional; - new_fractional.reserve(fractional.size() - num_to_remove); + // Go through and check the fractional variables and remove any that are now fixed to their + // bounds + std::vector to_remove(fractional.size(), 0); + i_t num_to_remove = 0; for (i_t k = 0; k < fractional.size(); k++) { - if (!to_remove[k]) { new_fractional.push_back(fractional[k]); } + const i_t j = fractional[k]; + if (std::abs(original_lp_.upper[j] - original_lp_.lower[j]) < settings_.fixed_tol) { + to_remove[k] = 1; + num_to_remove++; + } + } + if (num_to_remove > 0) { + std::vector new_fractional; + new_fractional.reserve(fractional.size() - num_to_remove); + for (i_t k = 0; k < fractional.size(); k++) { + if (!to_remove[k]) { new_fractional.push_back(fractional[k]); } + } + fractional = new_fractional; + num_fractional = fractional.size(); } - fractional = new_fractional; - num_fractional = fractional.size(); } } - } - if (symmetry_ != nullptr) { - i_t removed = - symmetry_->generators.template prune_by_bounds(original_lp_.lower, original_lp_.upper); - if (removed > 0) { - symmetry_->num_generators = static_cast(symmetry_->generators.num_generators()); - settings_.log.printf( - "Pruned %d generators invalidated by root-level bound tightening, %d remain\n", - removed, - symmetry_->num_generators); + if (symmetry_ != nullptr) { + i_t removed = + symmetry_->generators.template prune_by_bounds(original_lp_.lower, original_lp_.upper); + if (removed > 0) { + symmetry_->num_generators = static_cast(symmetry_->generators.num_generators()); + settings_.log.printf( + "Pruned %d generators invalidated by root-level bound tightening, %d remain\n", + removed, + symmetry_->num_generators); + } } - } - if (settings_.diving_settings.coefficient_diving != 0) { - calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); - } - - settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); - - const i_t num_workers = settings_.num_threads; - const i_t num_bfs_workers = std::max(settings_.num_threads / 2, 1); - const i_t num_diving_workers = num_workers - num_bfs_workers; - bfs_worker_pool_.init(num_bfs_workers, original_lp_, Arow_, var_types_, symmetry_, settings_); - - if (num_diving_workers > 0) { - diving_worker_pool_.init( - num_diving_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, num_bfs_workers); - } - - restart_count_ = 0; - min_node_queue_size_ = 20; + if (restart_count_ == 0) { + settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); + } - do { if (toc(exploration_stats_.start_time) > settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; break; @@ -2949,7 +2979,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - node_concurrent_halt_ = 0; + if (!settings_.sub_mip) *restart_concurrent_halt_ = 0; + node_concurrent_halt_ = 0; + exploration_stats_.nodes_explored = 0; exploration_stats_.nodes_unexplored = 2; exploration_stats_.nodes_since_last_log = 0; @@ -2975,7 +3007,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut best_first_search_with(initial_worker); } } // Implicit barrier for all tasks created within the group (RINS, B&B workers) - } while (solver_status_ == mip_status_t::RESTART); is_running_ = false; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 31d050008e..f888ee0e7a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -84,6 +84,7 @@ class branch_and_bound_t { branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, f_t start_time, + std::atomic* restart_concurrent_halt, const probing_implied_bound_t& probing_implied_bound, std::shared_ptr> clique_table = nullptr, mip_symmetry_t* symmetry = nullptr); @@ -236,6 +237,7 @@ class branch_and_bound_t { bool enable_concurrent_lp_root_solve_{false}; std::atomic root_concurrent_halt_{0}; std::atomic node_concurrent_halt_{0}; + std::atomic* restart_concurrent_halt_{nullptr}; bool is_root_solution_set{false}; // Pseudocosts diff --git a/cpp/src/branch_and_bound/pseudo_costs.cpp b/cpp/src/branch_and_bound/pseudo_costs.cpp index fc3ee3bb3f..0ecf719bba 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.cpp +++ b/cpp/src/branch_and_bound/pseudo_costs.cpp @@ -462,6 +462,7 @@ std::pair trial_branching(const lp_problem_t& ori const basis_update_mpf_t& basis_factors, const std::vector& basic_list, const std::vector& nonbasic_list, + std::atomic* concurrent_halt, i_t branch_var, f_t branch_var_lower, f_t branch_var_upper, @@ -482,6 +483,7 @@ std::pair trial_branching(const lp_problem_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 solution(original_lp.num_rows, original_lp.num_cols); iter = 0; @@ -1707,7 +1709,7 @@ i_t pseudo_costs_t::reliable_variable_selection( std::vector pdlp_obj_down(num_candidates, std::numeric_limits::quiet_NaN()); std::vector pdlp_obj_up(num_candidates, std::numeric_limits::quiet_NaN()); - std::atomic concurrent_halt{0}; + std::atomic pdlp_concurrent_halt{0}; if (use_pdlp) { #pragma omp task default(shared) priority(CUOPT_HIGH_TASK_PRIORITY) @@ -1715,7 +1717,7 @@ i_t pseudo_costs_t::reliable_variable_selection( rb_mode, num_candidates, start_time, - concurrent_halt, + pdlp_concurrent_halt, original_lp, new_slacks, leaf_solution.x, @@ -1731,7 +1733,7 @@ i_t pseudo_costs_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; @@ -1748,6 +1750,8 @@ i_t pseudo_costs_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; } @@ -1768,6 +1772,7 @@ i_t pseudo_costs_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]), @@ -1813,6 +1818,7 @@ i_t pseudo_costs_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], @@ -1849,7 +1855,7 @@ i_t pseudo_costs_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); diff --git a/cpp/src/branch_and_bound/pseudo_costs.hpp b/cpp/src/branch_and_bound/pseudo_costs.hpp index a370a00de8..08d1ba1668 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.hpp +++ b/cpp/src/branch_and_bound/pseudo_costs.hpp @@ -209,6 +209,7 @@ class pseudo_costs_t { std::shared_ptr> AT; // Transpose of the constraint matrix A std::shared_ptr> pdlp_warm_cache; + std::atomic* concurrent_halt_; reliability_branching_settings_t reliability_branching_settings; simplex_solver_settings_t settings; diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index d81265358a..124567115a 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -725,8 +725,10 @@ i_t solve(const user_problem_t& problem, { i_t status; if (is_mip(problem) && !settings.relaxation) { + std::atomic restart_concurrent_halt; probing_implied_bound_t empty_probing(problem.num_cols); - branch_and_bound_t branch_and_bound(problem, settings, tic(), empty_probing); + branch_and_bound_t branch_and_bound( + problem, settings, tic(), &restart_concurrent_halt, empty_probing); mip_solution_t mip_solution(problem.num_cols); mip_status_t mip_status = branch_and_bound.solve(mip_solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -766,7 +768,9 @@ i_t solve_mip_with_guess(const user_problem_t& problem, i_t status; if (is_mip(problem)) { probing_implied_bound_t empty_probing(problem.num_cols); - branch_and_bound_t branch_and_bound(problem, settings, tic(), empty_probing); + std::atomic restart_concurrent_halt; + branch_and_bound_t branch_and_bound( + problem, settings, tic(), &restart_concurrent_halt, empty_probing); branch_and_bound.set_initial_guess(guess); mip_status_t mip_status = branch_and_bound.solve(solution); if (mip_status == mip_status_t::OPTIMAL) { diff --git a/cpp/src/mip_heuristics/diversity/lns/rins.cu b/cpp/src/mip_heuristics/diversity/lns/rins.cu index e1318edf4d..cdaa069782 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cu +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cu @@ -263,8 +263,11 @@ void rins_t::run_rins() rins_solution_queue.push_back(solution); }; dual_simplex::probing_implied_bound_t empty_probing(branch_and_bound_problem.num_cols); - dual_simplex::branch_and_bound_t branch_and_bound( - branch_and_bound_problem, branch_and_bound_settings, dual_simplex::tic(), empty_probing); + dual_simplex::branch_and_bound_t branch_and_bound(branch_and_bound_problem, + branch_and_bound_settings, + dual_simplex::tic(), + &context.restart_concurrent_halt, + empty_probing); branch_and_bound.set_initial_guess(cuopt::host_copy(fixed_assignment, rins_handle.get_stream())); branch_and_bound_status = branch_and_bound.solve(branch_and_bound_solution); @@ -298,6 +301,7 @@ void rins_t::run_rins() static_cast(context.settings.heuristic_params.rins_max_time_limit)); } + fj_cpu->preemption_flag = 1; #pragma omp taskwait // Wait for the CPU FJ (RINS) to finish CUOPT_LOG_DEBUG("CPUFJ (RINS) task was stopped"); diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index 1d0b9245d7..a9667cf0a3 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -122,8 +122,12 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.log.log = false; dual_simplex::probing_implied_bound_t empty_probing( branch_and_bound_problem.num_cols); - dual_simplex::branch_and_bound_t branch_and_bound( - branch_and_bound_problem, branch_and_bound_settings, dual_simplex::tic(), empty_probing); + std::atomic concurrent_halt = 0; + dual_simplex::branch_and_bound_t branch_and_bound(branch_and_bound_problem, + branch_and_bound_settings, + dual_simplex::tic(), + &concurrent_halt, + empty_probing); branch_and_bound_status = branch_and_bound.solve(branch_and_bound_solution); if (solution_vector.size() > 0) { cuopt_assert(fixed_assignment.size() == branch_and_bound_solution.x.size(), diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index c25ade0c05..f049b75318 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -425,6 +425,7 @@ solution_t mip_solver_t::run_solver() branch_and_bound_problem, branch_and_bound_settings, timer_.get_tic_start(), + &context.restart_concurrent_halt, probing_implied_bound, context.problem_ptr->clique_table, context.symmetry.get()); diff --git a/cpp/src/mip_heuristics/solver_context.cuh b/cpp/src/mip_heuristics/solver_context.cuh index 739f7d130a..6a5b84c636 100644 --- a/cpp/src/mip_heuristics/solver_context.cuh +++ b/cpp/src/mip_heuristics/solver_context.cuh @@ -77,6 +77,8 @@ struct mip_solver_context_t { // Symmetry information for orbital fixing during B&B. Null if no exploitable symmetry. std::unique_ptr> symmetry; + + std::atomic restart_concurrent_halt{0}; }; } // namespace cuopt::linear_programming::detail From 68ecb980a95d324e8fbe01f508cc4a7bf7436197 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 14:02:46 +0200 Subject: [PATCH 15/24] fixed incorrect lp for workers. fixed node limit when restarting Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 519e30c081..3ee3f9a9e5 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1680,7 +1680,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, break; } - if (exploration_stats_.nodes_explored + exploration_stats_.nodes_being_solved > + if (exploration_stats_.total_nodes_explored + exploration_stats_.nodes_being_solved > settings_.node_limit) { solver_status_ = mip_status_t::NODE_LIMIT; stack.push_front(node_ptr); @@ -2528,20 +2528,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); - if (settings_.diving_settings.coefficient_diving != 0) { - calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); - } - - const i_t num_workers = settings_.num_threads; - const i_t num_bfs_workers = std::max(settings_.num_threads / 2, 1); - const i_t num_diving_workers = num_workers - num_bfs_workers; - bfs_worker_pool_.init(num_bfs_workers, original_lp_, Arow_, var_types_, symmetry_, settings_); - - if (num_diving_workers > 0) { - diving_worker_pool_.init( - num_diving_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, num_bfs_workers); - } - settings_.log.printf("Reduced cost strengthening enabled: %d\n", settings_.reduced_cost_strengthening); @@ -2886,6 +2872,20 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return solver_status_; } + if (settings_.diving_settings.coefficient_diving != 0) { + calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); + } + + const i_t num_workers = settings_.num_threads; + const i_t num_bfs_workers = std::max(settings_.num_threads / 2, 1); + const i_t num_diving_workers = num_workers - num_bfs_workers; + bfs_worker_pool_.init(num_bfs_workers, original_lp_, Arow_, var_types_, symmetry_, settings_); + + if (num_diving_workers > 0) { + diving_worker_pool_.init( + num_diving_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, num_bfs_workers); + } + do { if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { last_upper_bound = upper_bound_.load(); From d8301b0c287601f5c4f9b875afc35ffecf6ba6b1 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 14:31:34 +0200 Subject: [PATCH 16/24] fixed total nodes explored Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3ee3f9a9e5..56924e2cd1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -748,7 +748,7 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& bool is_maximization = original_lp_.obj_scale < 0.0; settings_.log.printf("Explored %d nodes in %.2fs.\n", - exploration_stats_.nodes_explored, + exploration_stats_.total_nodes_explored, toc(exploration_stats_.start_time)); if (exploration_stats_.orbital_fixing_nodes.load() > 0 || exploration_stats_.orbital_conflict_nodes.load() > 0) { @@ -809,7 +809,7 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& solution.objective = incumbent_.objective; } solution.lower_bound = lower_bound; - solution.nodes_explored = exploration_stats_.nodes_explored; + solution.nodes_explored = exploration_stats_.total_nodes_explored; solution.simplex_iterations = exploration_stats_.total_lp_iters; } From af3c3b951dbd2652f2335fa869074032f8d21030 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 14:48:23 +0200 Subject: [PATCH 17/24] fixed logs Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 56924e2cd1..2cf712b686 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2945,17 +2945,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - if (restart_count_ == 0) { - settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); - } - if (toc(exploration_stats_.start_time) > settings_.time_limit) { solver_status_ = mip_status_t::TIME_LIMIT; break; } if (solver_status_ == mip_status_t::RESTART) { - settings_.log.print_format("\n\nRestarting B&B after {:.2f}s and {} nodes\n", + settings_.log.print_format("\nRestarting B&B after {:.2f}s and {} nodes\n", toc(exploration_stats_.start_time), exploration_stats_.nodes_explored.load()); search_tree_.clean(); @@ -2993,6 +2989,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut print_table_header(); + if (restart_count_ == 0) { + settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); + } + #pragma omp taskgroup { if (settings_.deterministic) { From 4f32e52a56f995ae35553fce4818297b3457c575 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 17:20:35 +0200 Subject: [PATCH 18/24] revert log changes Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 162 +++++++++--------- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 - cpp/src/branch_and_bound/search_tree.hpp | 14 +- cpp/src/dual_simplex/logger.hpp | 44 ----- .../dual_simplex/simplex_solver_settings.hpp | 2 +- .../mip_heuristics/diversity/population.cu | 1 + 6 files changed, 93 insertions(+), 131 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 2cf712b686..d9955f4ab6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -195,12 +195,22 @@ f_t user_relative_gap(const lp_problem_t& lp, f_t obj_value, f_t lower return user_mip_gap; } -template -std::string to_percentage(f_t value) +template +std::string user_mip_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) { - if (value == std::numeric_limits::infinity()) return "---"; - if (value > 1e-3) { return std::format("{:5.1f}%", value * 100); } - return std::format("{:5.2f}%", value * 100); + const f_t user_mip_gap = user_relative_gap(lp, obj_value, lower_bound); + if (user_mip_gap == std::numeric_limits::infinity()) { + return " - "; + } else { + constexpr int BUFFER_LEN = 32; + char buffer[BUFFER_LEN]; + if (user_mip_gap > 1e-3) { + snprintf(buffer, BUFFER_LEN - 1, "%5.1f%%", user_mip_gap * 100); + } else { + snprintf(buffer, BUFFER_LEN - 1, "%5.2f%%", user_mip_gap * 100); + } + return std::string(buffer); + } } #ifdef SHOW_DIVING_TYPE @@ -319,57 +329,29 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) upper_bound_ = bound; } -template -void branch_and_bound_t::print_table_header() -{ - std::string header = std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap"); - if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } - header += std::format("{:^8}|", "Time"); - settings_.log.printf("%s\n", header.c_str()); -} - template void branch_and_bound_t::report_heuristic(f_t obj) { if (is_running_) { - f_t lower_bound = get_lower_bound(); - f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_lower = compute_user_objective(original_lp_, lower_bound); - f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); - std::string user_gap_text = to_percentage(user_gap); - - std::string log_line = - std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11}", - "", // nodes explored - "", // nodes unexplored - user_obj, - user_lower, - "", // integer infeasible - "", // depth - "", // iter/node - user_gap_text); - - if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } - log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); - settings_.log.printf("%s\n", log_line.c_str()); + f_t user_obj = compute_user_objective(original_lp_, obj); + f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); + std::string user_gap = user_mip_gap(original_lp_, obj, get_lower_bound()); + + settings_.log.printf( + "H %+13.6e %+10.6e %s %9.2f\n", + user_obj, + user_lower, + user_gap.c_str(), + toc(exploration_stats_.start_time)); } else { if (solving_root_relaxation_.load()) { f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_gap = user_relative_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); - std::string user_gap_text = to_percentage(user_gap); - settings_.log.print_format( - "New solution from primal heuristics. Objective {:+.6e}. Gap {}. Time {:.2f}\n", + std::string user_gap = + user_mip_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); + settings_.log.printf( + "New solution from primal heuristics. Objective %+.6e. Gap %s. Time %.2f\n", user_obj, - user_gap_text, + user_gap.c_str(), toc(exploration_stats_.start_time)); } else { settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", @@ -390,23 +372,34 @@ void branch_and_bound_t::report( const f_t user_lower = compute_user_objective(original_lp_, lower_bound); const f_t iters = static_cast(exploration_stats_.total_lp_iters); const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; - f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); - std::string user_gap_text = to_percentage(user_gap); - - std::string log_line = - std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11}", - symbol, - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_int_infeas, - node_depth, - iter_node, - user_gap_text); - if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } - log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); - settings_.log.printf("%s\n", log_line.c_str()); + const std::string user_gap = user_mip_gap(original_lp_, obj, lower_bound); + if (work_time >= 0) { + settings_.log.printf( + "%c %10d %10lu %+13.6e %+10.6e %6d %6d %7.1e %s %9.2f %9.2f\n", + symbol, + nodes_explored, + nodes_unexplored, + user_obj, + user_lower, + node_int_infeas, + node_depth, + iter_node, + user_gap.c_str(), + work_time, + toc(exploration_stats_.start_time)); + } else { + settings_.log.printf("%c %10d %10lu %+13.6e %+10.6e %6d %6d %7.1e %s %9.2f\n", + symbol, + nodes_explored, + nodes_unexplored, + user_obj, + user_lower, + node_int_infeas, + node_depth, + iter_node, + user_gap.c_str(), + toc(exploration_stats_.start_time)); + } } template @@ -811,6 +804,8 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& solution.lower_bound = lower_bound; solution.nodes_explored = exploration_stats_.total_nodes_explored; solution.simplex_iterations = exploration_stats_.total_lp_iters; + + std::cout << std::format("{}", solution.objective) << std::endl; } template @@ -1567,9 +1562,9 @@ bool branch_and_bound_t::should_restart(f_t current_abs_gap) f_t gap_reduction = exploration_stats_.restart_gap_at_last_check / current_abs_gap; - settings_.log.debug_format( - "[Restart] Current: explored={}, progress={:.4g}, gap={:.4g}. Since last: explored={}, " - "progress={:.4g}, gap={:.4g}. Tree size estimate={}", + settings_.log.debug( + "[Restart] Current: explored=%d, progress=%.4f, gap=%.4f. Since last: explored={}, " + "progress=%.4f, gap=%.4f. Tree size estimate=%d", num_nodes, current_progress, current_abs_gap, @@ -2691,7 +2686,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut is_running_ = true; lower_bound_numerical_ = inf; - if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } + if (num_fractional != 0 && settings_.max_cut_passes > 0) { + settings_.log.printf( + " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node | " + "Gap | Time |\n"); + } cut_pool_t cut_pool(original_lp_.num_cols, settings_); cut_generation_t cut_generation(cut_pool, @@ -2951,9 +2950,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (solver_status_ == mip_status_t::RESTART) { - settings_.log.print_format("\nRestarting B&B after {:.2f}s and {} nodes\n", - toc(exploration_stats_.start_time), - exploration_stats_.nodes_explored.load()); + settings_.log.printf("\nRestarting B&B after %.2fs and %d nodes\n", + toc(exploration_stats_.start_time), + exploration_stats_.nodes_explored.load()); search_tree_.clean(); bfs_worker_pool_.reset(); diving_worker_pool_.reset(); @@ -2987,7 +2986,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.restart_nodes_at_last_check = 0; exploration_stats_.restart_progress_at_last_check = 0; - print_table_header(); + if (settings_.deterministic) { + settings_.log.printf( + " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node " + "| Gap | Work | Time |\n"); + } else { + settings_.log.printf( + " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node " + "| Gap | Time |\n"); + } if (restart_count_ == 0) { settings_.log.printf("Exploring the B&B tree using %d threads\n\n", settings_.num_threads); @@ -3489,10 +3496,9 @@ void branch_and_bound_t::deterministic_sync_callback() exploration_stats_.last_log = tic(); } - f_t obj = compute_user_objective(original_lp_, upper_bound); - f_t user_lower = compute_user_objective(original_lp_, lower_bound); - f_t user_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); - std::string user_gap_text = to_percentage(user_gap); + f_t obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + std::string gap_user = user_mip_gap(original_lp_, upper_bound, lower_bound); std::string idle_workers; i_t idle_count = 0; @@ -3508,7 +3514,7 @@ void branch_and_bound_t::deterministic_sync_callback() exploration_stats_.nodes_unexplored, obj, user_lower, - user_gap_text.c_str(), + gap_user.c_str(), toc(exploration_stats_.start_time), state_hash, idle_workers.empty() ? "" : " ", diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index f888ee0e7a..e891f676d9 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -269,7 +269,6 @@ class branch_and_bound_t { i_t restart_count_; - void print_table_header(); void report_heuristic(f_t obj); void report(char symbol, f_t obj, diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index 27ee3fc761..0f5fc2258e 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -98,7 +98,7 @@ class search_tree_t { const f_t val) { if (write_graphviz) { - log.print_format("Node{} [label=\"{} {:.16e}\"]\n", node_ptr->node_id, label, val); + log.printf("Node%d [label=\"%s %.16e\"]\n", node_ptr->node_id, label.c_str(), val); } } @@ -110,12 +110,12 @@ class search_tree_t { const f_t bound) { if (write_graphviz) { - log.print_format("Node{} -> Node{} [label=\"x{} {} {:e}\"]\n", - origin_ptr->node_id, - dest_ptr->node_id, - branch_var, - branch_dir == branch_direction_t::DOWN ? "<=" : ">=", - bound); + 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); } } diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 3f384e1952..f813086708 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -16,8 +16,6 @@ #include #include #include -#include -#include namespace cuopt::linear_programming::dual_simplex { @@ -86,27 +84,6 @@ class logger_t { } } - template - void print_format(std::format_string fmt, Args&&... args) - { - if (log) { - std::string msg = std::format(fmt, std::forward(args)...); - if (log_to_console) { -#ifdef CUOPT_LOG_ACTIVE_LEVEL - std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; - CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); -#else - std::printf("%s", msg.c_str()); - fflush(stdout); -#endif - } - if (log_to_file && log_file != nullptr) { - std::fprintf(log_file, "%s", msg.c_str()); - fflush(log_file); - } - } - } - void debug([[maybe_unused]] const char* fmt, ...) { if (log) { @@ -141,27 +118,6 @@ class logger_t { } } - template - void debug_format(std::format_string fmt, Args&&... args) - { - if (log) { - std::string msg = std::format(fmt, std::forward(args)...); - if (log_to_console) { -#ifdef CUOPT_LOG_ACTIVE_LEVEL - std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; - CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg_view.c_str()); -#else - std::printf("%s", msg.c_str()); - fflush(stdout); -#endif - } - if (log_to_file && log_file != nullptr) { - std::fprintf(log_file, "%s", msg.c_str()); - fflush(log_file); - } - } - } - bool log; bool log_to_console; std::string log_prefix; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 04ab5537ee..a6e56a9b27 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -230,7 +230,7 @@ struct simplex_solver_settings_t { f_t restart_threshold_grow_per_restart = 1.5; i_t restart_tree_size_factor = 50; i_t restart_check_freq = 100; - i_t max_restarts = 50; + i_t max_restarts = 0; // Settings for the reliability branching. // - -1: automatic diff --git a/cpp/src/mip_heuristics/diversity/population.cu b/cpp/src/mip_heuristics/diversity/population.cu index a870f654de..0e4ab152d1 100644 --- a/cpp/src/mip_heuristics/diversity/population.cu +++ b/cpp/src/mip_heuristics/diversity/population.cu @@ -152,6 +152,7 @@ void population_t::add_external_solution(const std::vector& solut external_solution_queue_cpufj.emplace_back(solution, objective, origin); } else { external_solution_queue.emplace_back(solution, objective, origin); + std::cout << std::format("best = {}", best_feasible().h_obj) << std::endl; } // Prevent CPUFJ scratch solutions from flooding the queue From 518dc8e78721dfd0b9a1ed058a0539fae95f9ee1 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 17:38:25 +0200 Subject: [PATCH 19/24] fixed halt_flag to cpu fj in RINS. removed debug code Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 -- cpp/src/mip_heuristics/diversity/lns/rins.cu | 6 ++++-- cpp/src/mip_heuristics/diversity/population.cu | 1 - cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu | 8 ++++---- cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index d9955f4ab6..7323dea703 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -804,8 +804,6 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& solution.lower_bound = lower_bound; solution.nodes_explored = exploration_stats_.total_nodes_explored; solution.simplex_iterations = exploration_stats_.total_lp_iters; - - std::cout << std::format("{}", solution.objective) << std::endl; } template diff --git a/cpp/src/mip_heuristics/diversity/lns/rins.cu b/cpp/src/mip_heuristics/diversity/lns/rins.cu index cdaa069782..d70ee86520 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cu +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cu @@ -214,6 +214,7 @@ void rins_t::run_rins() fj_solution.copy_new_assignment(cuopt::host_copy(fixed_assignment, rins_handle.get_stream())); std::vector default_weights(fixed_problem.n_constraints, 1.); + std::atomic fj_halt_flag = false; std::unique_ptr> fj_cpu = fj.create_cpu_climber(fj_solution, default_weights, @@ -222,7 +223,8 @@ void rins_t::run_rins() context.preempt_heuristic_solver_, fj_settings_t{}, true); - fj_cpu->log_prefix = "[RINS] "; + fj_cpu->log_prefix = "[RINS] "; + fj_cpu->preemption_flag = &fj_halt_flag; CUOPT_LOG_DEBUG("Launching CPUFJ (RINS) task"); #pragma omp task shared(fj_cpu) firstprivate(time_limit) \ @@ -301,7 +303,7 @@ void rins_t::run_rins() static_cast(context.settings.heuristic_params.rins_max_time_limit)); } - fj_cpu->preemption_flag = 1; + fj_halt_flag = true; #pragma omp taskwait // Wait for the CPU FJ (RINS) to finish CUOPT_LOG_DEBUG("CPUFJ (RINS) task was stopped"); diff --git a/cpp/src/mip_heuristics/diversity/population.cu b/cpp/src/mip_heuristics/diversity/population.cu index 0e4ab152d1..a870f654de 100644 --- a/cpp/src/mip_heuristics/diversity/population.cu +++ b/cpp/src/mip_heuristics/diversity/population.cu @@ -152,7 +152,6 @@ void population_t::add_external_solution(const std::vector& solut external_solution_queue_cpufj.emplace_back(solution, objective, origin); } else { external_solution_queue.emplace_back(solution, objective, origin); - std::cout << std::format("best = {}", best_feasible().h_obj) << std::endl; } // Prevent CPUFJ scratch solutions from flooding the queue diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index a537426457..0382d700bf 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -1614,7 +1614,7 @@ void cpufj_solve(fj_cpu_climber_t* fj_cpu, f_t in_time_limit, double w fj_cpu->prev_best_objective = fj_cpu->h_best_objective; fj_cpu->iterations_since_best = 0; - while (!fj_cpu->halted && !fj_cpu->preemption_flag.load()) { + while (!fj_cpu->halted && !fj_cpu->preemption_flag->load()) { // Check if 5 seconds have passed auto now = std::chrono::high_resolution_clock::now(); if (in_time_limit < std::numeric_limits::infinity() && @@ -1823,9 +1823,9 @@ template void stop_fj_cpu_task(fj_cpu_task_t& task) { if (task.fj_cpu) { - auto& fj_cpu = *task.fj_cpu; - fj_cpu.preemption_flag = true; - fj_cpu.halted = true; + auto& fj_cpu = *task.fj_cpu; + fj_cpu.preemption_flag->store(true); + fj_cpu.halted = true; } } diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh index cdf3a2f58a..47383ac759 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh @@ -24,7 +24,7 @@ namespace cuopt::linear_programming::detail { // Maintaining a single source of truth for all members would be nice template struct fj_cpu_climber_t { - fj_cpu_climber_t(std::atomic& preemption_flag) : preemption_flag(preemption_flag) + fj_cpu_climber_t(std::atomic& preemption_flag) : preemption_flag(&preemption_flag) { #define ADD_INSTRUMENTED(var) \ std::make_pair(#var, std::ref(static_cast(var))) @@ -187,7 +187,7 @@ struct fj_cpu_climber_t { // Memory instrumentation aggregator instrumentation_aggregator_t memory_aggregator; // TODO atomic ref? c++20 - std::atomic& preemption_flag; + std::atomic* preemption_flag; }; template From aa5952ed55f5daf088e83be92505da5c3202f7b9 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 17:45:51 +0200 Subject: [PATCH 20/24] re-enabled restarts Signed-off-by: Nicolas L. Guidotti --- cpp/src/dual_simplex/simplex_solver_settings.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index a6e56a9b27..04ab5537ee 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -230,7 +230,7 @@ struct simplex_solver_settings_t { f_t restart_threshold_grow_per_restart = 1.5; i_t restart_tree_size_factor = 50; i_t restart_check_freq = 100; - i_t max_restarts = 0; + i_t max_restarts = 50; // Settings for the reliability branching. // - -1: automatic From 4ea3cc2e1e85f6438188ec515277647246b31bf8 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 18:51:06 +0200 Subject: [PATCH 21/24] tentative fix for crash on symmetry after some generators are pruned Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/symmetry.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/branch_and_bound/symmetry.hpp b/cpp/src/branch_and_bound/symmetry.hpp index bcce5ece2a..231e1e0902 100644 --- a/cpp/src/branch_and_bound/symmetry.hpp +++ b/cpp/src/branch_and_bound/symmetry.hpp @@ -319,7 +319,7 @@ class orbital_fixing_t { node = node->parent; } - surviving_generators_.resize(max_generators_); + surviving_generators_.resize(symmetry->num_generators); std::iota(surviving_generators_.begin(), surviving_generators_.end(), 0); // Seed cumulative fixings from the parent's stored orbital fixings. From a12fb55915be3ab585ee7d1c52336dd1f9649e69 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 10 Jun 2026 13:25:25 +0200 Subject: [PATCH 22/24] address coderabbit comments Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 16 +++++++++------- cpp/src/branch_and_bound/worker.hpp | 5 +++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 7323dea703..de905c61a7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1561,7 +1561,7 @@ bool branch_and_bound_t::should_restart(f_t current_abs_gap) f_t gap_reduction = exploration_stats_.restart_gap_at_last_check / current_abs_gap; settings_.log.debug( - "[Restart] Current: explored=%d, progress=%.4f, gap=%.4f. Since last: explored={}, " + "[Restart] Current: explored=%d, progress=%.4f, gap=%.4f. Since last: explored=%d, " "progress=%.4f, gap=%.4f. Tree size estimate=%d", num_nodes, current_progress, @@ -2975,12 +2975,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (!settings_.sub_mip) *restart_concurrent_halt_ = 0; node_concurrent_halt_ = 0; - exploration_stats_.nodes_explored = 0; - exploration_stats_.nodes_unexplored = 2; - exploration_stats_.nodes_since_last_log = 0; - exploration_stats_.last_log = 0; - exploration_stats_.restart_large_tree_count = 0; - exploration_stats_.restart_gap_at_last_check = upper_bound_ - get_lower_bound(); + exploration_stats_.nodes_explored = 0; + exploration_stats_.nodes_unexplored = 2; + exploration_stats_.nodes_since_last_log = 0; + exploration_stats_.last_log = 0; + exploration_stats_.restart_large_tree_count = 0; + exploration_stats_.restart_gap_at_last_check = + compute_user_abs_gap(original_lp_, upper_bound_.load(), root_relax_objective); exploration_stats_.restart_nodes_at_last_check = 0; exploration_stats_.restart_progress_at_last_check = 0; @@ -3618,6 +3619,7 @@ node_status_t branch_and_bound_t::solve_node_deterministic( exploration_stats_.total_lp_solve_time += toc(lp_start_time); exploration_stats_.total_lp_iters += node_iter; ++exploration_stats_.nodes_explored; + ++exploration_stats_.total_nodes_explored; --exploration_stats_.nodes_unexplored; deterministic_bfs_policy_t policy{*this, worker}; diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 80a7375867..66498c57d4 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -232,8 +232,9 @@ class bfs_worker_t : public branch_and_bound_worker_t { node_queue.clear(); total_max_diving_workers = 0; total_active_diving_workers = 0; - this->is_active = false; - this->lower_bound = -std::numeric_limits::infinity(); + active_diving_workers.fill(0); + this->is_active = false; + this->lower_bound = -std::numeric_limits::infinity(); } // The worker-local node heap. From 57a0073dc9c174b0b289b8daaa661017a1a0a85f Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 11 Jun 2026 15:26:26 +0200 Subject: [PATCH 23/24] update restart heuristic to more closely match HiGHS Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 136 +++++++++++------- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + cpp/src/branch_and_bound/worker.hpp | 9 +- .../dual_simplex/simplex_solver_settings.hpp | 40 +++++- 4 files changed, 120 insertions(+), 66 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c5a31cb4ad..6e832662ba 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -342,7 +342,7 @@ void branch_and_bound_t::report( char symbol, f_t obj, f_t lower_bound, i_t node_depth, i_t node_int_infeas, double work_time) { update_user_bound(lower_bound); - const i_t nodes_explored = exploration_stats_.nodes_explored; + const i_t nodes_explored = exploration_stats_.total_nodes_explored; const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; const f_t user_obj = compute_user_objective(original_lp_, obj); const f_t user_lower = compute_user_objective(original_lp_, lower_bound); @@ -1523,48 +1523,62 @@ dual::status_t branch_and_bound_t::solve_node_lp( template bool branch_and_bound_t::should_restart(f_t current_abs_gap) { - if (settings_.sub_mip || restart_count_ >= settings_.max_restarts) return false; + mip_restart_settings_t restart_settings = settings_.restart_settings; - i_t num_nodes = exploration_stats_.nodes_explored; - i_t total_nodes = exploration_stats_.total_nodes_explored; - if (num_nodes < settings_.restart_min_nodes) return false; + if (settings_.sub_mip || !std::isfinite(current_abs_gap) || + restart_count_ >= settings_.restart_settings.max_restarts) + return false; - i_t nodes_since_last_check = num_nodes - exploration_stats_.restart_nodes_at_last_check; - if (nodes_since_last_check < settings_.restart_check_freq) return false; + int64_t num_nodes = exploration_stats_.nodes_explored; + if (num_nodes < restart_settings.min_nodes || num_nodes < exploration_stats_.restart_next_check) + return false; - f_t current_progress = search_tree_.progress; + int64_t nodes_since_last_check = num_nodes - exploration_stats_.restart_nodes_at_last_check; + i_t num_leaves = search_tree_.num_final_nodes; + f_t current_progress = search_tree_.progress; f_t progress_since_last_check = std::max(current_progress - exploration_stats_.restart_progress_at_last_check, 1E-6); - i_t tree_size_estimate = + int64_t tree_size_estimate = exploration_stats_.restart_nodes_at_last_check + nodes_since_last_check * (1.0 - current_progress) / progress_since_last_check; - f_t gap_reduction = exploration_stats_.restart_gap_at_last_check / current_abs_gap; - - settings_.log.debug( - "[Restart] Current: explored=%d, progress=%.4f, gap=%.4f. Since last: explored=%d, " - "progress=%.4f, gap=%.4f. Tree size estimate=%d", - num_nodes, - current_progress, - current_abs_gap, - nodes_since_last_check, - progress_since_last_check, - gap_reduction, - tree_size_estimate); - - if (gap_reduction < 1.05 && - tree_size_estimate >= settings_.restart_tree_size_factor * total_nodes) { - ++exploration_stats_.restart_large_tree_count; - i_t min_count = - settings_.restart_min_estimates + total_nodes * settings_.restart_threshold_grow_per_node; - return exploration_stats_.restart_large_tree_count >= - min_count * std::pow(settings_.restart_threshold_grow_per_restart, restart_count_); - } - - exploration_stats_.restart_large_tree_count = 0; + // HiGHs uses the square of this value (probably because fixed variables cascades + // during presolve via bounds propagations, implications, etc.). Since we are not + // applying presolve after each restart, keep as linear. + f_t active_ratio = 1 - fixed_int_var_ratio_; + f_t gap_reduction = + std::isfinite(current_abs_gap) && std::isfinite(exploration_stats_.restart_gap_at_last_check) + ? exploration_stats_.restart_gap_at_last_check / current_abs_gap + : 0; + + if (gap_reduction < 1 + restart_settings.max_gap_improvement / active_ratio && + tree_size_estimate >= restart_settings.tree_size_multiple * num_nodes * active_ratio) { + ++exploration_stats_.restart_huge_tree_count; + exploration_stats_.restart_next_check = num_nodes + restart_settings.check_freq; + + settings_.log.debug( + "[Restart] Current: explored=%ld, progress=%.4f, gap=%.4f. Since last: explored=%ld, " + "progress=%.4f, gap=%.4f. Tree size estimate=%ld", + num_nodes, + current_progress, + current_abs_gap, + nodes_since_last_check, + progress_since_last_check, + gap_reduction, + tree_size_estimate); + + i_t min_count = restart_settings.min_huge_tree_estimates; + f_t threshold_node = num_leaves * restart_settings.threshold_grow_per_leaf; + f_t threshold_restarts = std::pow(restart_settings.threshold_grow_per_restart, restart_count_); + i_t threshold = std::ceil(active_ratio * (min_count + threshold_node) * threshold_restarts); + return exploration_stats_.restart_huge_tree_count >= threshold; + } + + exploration_stats_.restart_huge_tree_count = 0; exploration_stats_.restart_gap_at_last_check = current_abs_gap; exploration_stats_.restart_progress_at_last_check = current_progress; exploration_stats_.restart_nodes_at_last_check = num_nodes; + exploration_stats_.restart_next_check = num_nodes; return false; } @@ -2972,6 +2986,18 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut break; } + i_t num_integers = 0; + i_t num_fixed_integers = 0; + for (i_t j = 0; j < original_lp_.num_cols; j++) { + if (var_types_[j] == variable_type_t::INTEGER) { + ++num_integers; + if (std::abs(original_lp_.upper[j] - original_lp_.lower[j]) < settings_.fixed_tol) { + ++num_fixed_integers; + } + } + } + fixed_int_var_ratio_ = (f_t)num_fixed_integers / num_integers; + if (solver_status_ == mip_status_t::RESTART) { settings_.log.printf("\nRestarting B&B after %.2fs and %d nodes\n", toc(exploration_stats_.start_time), @@ -3000,11 +3026,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (!settings_.sub_mip) *restart_concurrent_halt_ = 0; node_concurrent_halt_ = 0; - exploration_stats_.nodes_explored = 0; - exploration_stats_.nodes_unexplored = 2; - exploration_stats_.nodes_since_last_log = 0; - exploration_stats_.last_log = 0; - exploration_stats_.restart_large_tree_count = 0; + exploration_stats_.nodes_explored = 0; + exploration_stats_.nodes_unexplored = 2; + exploration_stats_.nodes_since_last_log = 0; + exploration_stats_.last_log = 0; + exploration_stats_.restart_huge_tree_count = 0; exploration_stats_.restart_gap_at_last_check = compute_user_abs_gap(original_lp_, upper_bound_.load(), root_relax_objective); exploration_stats_.restart_nodes_at_last_check = 0; @@ -3131,8 +3157,8 @@ Work Units: 0 0.5 1. ──────────────────────────────────────────────────────────────────────────────────────────► Work Unit Time -Legend: ▓▓▓ = actively working ░░░ = waiting at barrier [hash] = state hash for verification - wut = work unit timestamp PC = pseudo-costs snap = snapshot (local copy) +Legend: ▓▓▓ = actively working ░░░ = waiting at barrier [hash] = state hash for +verification wut = work unit timestamp PC = pseudo-costs snap = snapshot (local copy) */ @@ -3170,23 +3196,22 @@ Producer Sync: Producing solutions in the past would break determinism, therefore this unidirectional sync ensures no such thing can occur. Instrumentation Aggregator: Collects multiple instrument vectors into a single aggregation point for estimating work from memory operations. Worker Context: Object -representing the "context" (e.g.: the worker) that should register the amount of work recorded There -is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure they -remained synchronized together. Queued Integer Solutions: New integer solutions found within -horizons are queued with a work unit timestamp, in order to be sorted and played in order during the -sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is used -to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently +representing the "context" (e.g.: the worker) that should register the amount of work recorded +There is a 1context:1worker mapping. The Work Unit Scheduler registers such contexts and ensure +they remained synchronized together. Queued Integer Solutions: New integer solutions found within +horizons are queued with a work unit timestamp, in order to be sorted and played in order during +the sync callback. Creation Sequence: In nondeterministic mode, a single global atomic integer is +used to generate sequential IDs for the nodes. Since this is a global atomic, it is inherently nondeterministic. To fix this, in deterministic mode, nodes are addressed by a tuple - where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential ID -local to the worker.\ This sequential ID is similar in principle to the global atomic ID sequence of -the nondeterminsitic mode but since it is local to each worker, it is updated serially and thus is -deterministic. worker IDs are unique, and sequence IDs are unique to their workers, therefor - is a globally unique node identifier. -Pseudocost Update: - Each worker updates its local pseudocosts when branching. These updates are queued within -horizons. During the horizon sync, these updates are all played in order, and the newly updated -global pseudocosts are broadcast to the worker's pseudocost snapshots for the coming horizon. + where "worker_id" is the ID of the worker that created this node, and "seq_id" is a sequential +ID local to the worker.\ This sequential ID is similar in principle to the global atomic ID +sequence of the nondeterminsitic mode but since it is local to each worker, it is updated serially +and thus is deterministic. worker IDs are unique, and sequence IDs are unique to their workers, +therefor is a globally unique node identifier. Pseudocost Update: Each worker +updates its local pseudocosts when branching. These updates are queued within horizons. During the +horizon sync, these updates are all played in order, and the newly updated global pseudocosts are +broadcast to the worker's pseudocost snapshots for the coming horizon. */ @@ -3296,7 +3321,8 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri "Sync%% | NoWork\n"); settings_.log.printf( " " - "-------+---------+----------+--------+---------+--------+----------+----------+-------+-------" + "-------+---------+----------+--------+---------+--------+----------+----------+-------+-----" + "--" "\n"); for (const auto& worker : *deterministic_workers_) { double sync_time = worker.work_context.total_sync_time; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index e891f676d9..1a243976bf 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -268,6 +268,7 @@ class branch_and_bound_t { std::function user_bound_callback_; i_t restart_count_; + f_t fixed_int_var_ratio_; void report_heuristic(f_t obj); void report(char symbol, diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 739d4cc6b2..65c34b0b69 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -38,10 +38,11 @@ struct branch_and_bound_stats_t { omp_atomic_t nodes_since_last_log = 0; omp_atomic_t last_log = 0.0; - i_t restart_nodes_at_last_check = 0; - f_t restart_progress_at_last_check = 0; - f_t restart_gap_at_last_check = 0; - i_t restart_large_tree_count = 0; + int64_t restart_nodes_at_last_check = 0; + f_t restart_progress_at_last_check = 0; + f_t restart_gap_at_last_check = 0; + i_t restart_huge_tree_count = 0; + int64_t restart_next_check = 0; omp_atomic_t orbital_fixing_nodes = 0; omp_atomic_t orbital_fixings_applied = 0; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index ef94f6db72..eb35a75e0f 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -26,6 +26,38 @@ struct benchmark_info_t; namespace cuopt::linear_programming::dual_simplex { +template +struct mip_restart_settings_t { + // Minimum number of nodes that needs to be explored before triggering a restart. + i_t min_nodes = 1000; + + // Minimum number of "huge" tree estimations before triggering a restart. + i_t min_huge_tree_estimates = 10; + + // Indicates how the threshold (regarding the number of "huge" tree estimations) grows + // with the number of nodes explored. Make it harder to restart if the tree is large + // (nodes * restart_threshold_grow_per_node). + f_t threshold_grow_per_leaf = 0.01; + + // Indicates how the threshold (regarding the number of "huge" tree estimations) grows + // with the number of restarts. Each restart make it harder to trigger another restart + // (restart_count ^ 1.5). + f_t threshold_grow_per_restart = 1.5; + + // Indicates the multiple of the current number of explored nodes for the tree to be considered + // "huge". + i_t tree_size_multiple = 50; + + // The maximum improvement in the absolute gap for the solver to be considered stagnated + f_t max_gap_improvement = 0.05; + + // The frequency in terms of the nodes for checking if we should restart + i_t check_freq = 1000; + + // Maximum number of restarts allowed + i_t max_restarts = 50; +}; + template struct simplex_solver_settings_t { public: @@ -207,13 +239,7 @@ struct simplex_solver_settings_t { i_t bnb_nodes_per_steal; i_t bnb_max_steal_attempts; - i_t restart_min_nodes = 1000; - i_t restart_min_estimates = 10; - f_t restart_threshold_grow_per_node = 0.001; - f_t restart_threshold_grow_per_restart = 1.5; - i_t restart_tree_size_factor = 50; - i_t restart_check_freq = 100; - i_t max_restarts = 50; + mip_restart_settings_t restart_settings; // Settings for the reliability branching. // - -1: automatic From ed1023065ff538551e46050d6a3533b9ea4eea27 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 11 Jun 2026 16:27:35 +0200 Subject: [PATCH 24/24] tune to the settings to reduce the restart frequency Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 24 +++++++++---------- .../dual_simplex/simplex_solver_settings.hpp | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 6e832662ba..5840a0102a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1545,32 +1545,32 @@ bool branch_and_bound_t::should_restart(f_t current_abs_gap) // HiGHs uses the square of this value (probably because fixed variables cascades // during presolve via bounds propagations, implications, etc.). Since we are not // applying presolve after each restart, keep as linear. - f_t active_ratio = 1 - fixed_int_var_ratio_; - f_t gap_reduction = - std::isfinite(current_abs_gap) && std::isfinite(exploration_stats_.restart_gap_at_last_check) - ? exploration_stats_.restart_gap_at_last_check / current_abs_gap - : 0; + f_t active_ratio = 1 - fixed_int_var_ratio_; + f_t gap_reduction = exploration_stats_.restart_gap_at_last_check / current_abs_gap; if (gap_reduction < 1 + restart_settings.max_gap_improvement / active_ratio && tree_size_estimate >= restart_settings.tree_size_multiple * num_nodes * active_ratio) { ++exploration_stats_.restart_huge_tree_count; exploration_stats_.restart_next_check = num_nodes + restart_settings.check_freq; + i_t min_count = restart_settings.min_huge_tree_estimates; + f_t threshold_node = num_leaves * restart_settings.threshold_grow_per_leaf; + f_t threshold_restarts = std::pow(restart_settings.threshold_grow_per_restart, restart_count_); + i_t threshold = std::ceil(active_ratio * (min_count + threshold_node) * threshold_restarts); + settings_.log.debug( - "[Restart] Current: explored=%ld, progress=%.4f, gap=%.4f. Since last: explored=%ld, " - "progress=%.4f, gap=%.4f. Tree size estimate=%ld", + "[Restart] %d Current: explored=%ld, progress=%.4f, gap=%.4f. Since last: explored=%ld, " + "progress=%.4f, gap_ratio=%.4f. Tree size=%ld. Threshold=%d", + exploration_stats_.restart_huge_tree_count, num_nodes, current_progress, current_abs_gap, nodes_since_last_check, progress_since_last_check, gap_reduction, - tree_size_estimate); + tree_size_estimate, + threshold); - i_t min_count = restart_settings.min_huge_tree_estimates; - f_t threshold_node = num_leaves * restart_settings.threshold_grow_per_leaf; - f_t threshold_restarts = std::pow(restart_settings.threshold_grow_per_restart, restart_count_); - i_t threshold = std::ceil(active_ratio * (min_count + threshold_node) * threshold_restarts); return exploration_stats_.restart_huge_tree_count >= threshold; } diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index eb35a75e0f..bfbfe2e0ef 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -29,7 +29,7 @@ namespace cuopt::linear_programming::dual_simplex { template struct mip_restart_settings_t { // Minimum number of nodes that needs to be explored before triggering a restart. - i_t min_nodes = 1000; + i_t min_nodes = 10000; // Minimum number of "huge" tree estimations before triggering a restart. i_t min_huge_tree_estimates = 10; @@ -37,7 +37,7 @@ struct mip_restart_settings_t { // Indicates how the threshold (regarding the number of "huge" tree estimations) grows // with the number of nodes explored. Make it harder to restart if the tree is large // (nodes * restart_threshold_grow_per_node). - f_t threshold_grow_per_leaf = 0.01; + f_t threshold_grow_per_leaf = 0.0015; // Indicates how the threshold (regarding the number of "huge" tree estimations) grows // with the number of restarts. Each restart make it harder to trigger another restart @@ -52,7 +52,7 @@ struct mip_restart_settings_t { f_t max_gap_improvement = 0.05; // The frequency in terms of the nodes for checking if we should restart - i_t check_freq = 1000; + i_t check_freq = 100; // Maximum number of restarts allowed i_t max_restarts = 50;