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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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 ffdc469f891289db9225caa727c90a44ac4d87f2 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:01:29 +0200 Subject: [PATCH 10/14] 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 11/14] 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 5eb9730f21c540a872e70e7ade882c72c4dfac5a Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:09:08 +0200 Subject: [PATCH 12/14] removed changes to the search tree 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 | 164 ------------------ 3 files changed, 132 insertions(+), 167 deletions(-) delete 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 77d02ccfb1..15a6030dbc 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -12,7 +12,6 @@ #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 ad53aaabb9..2c0968ffce 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -41,8 +41,43 @@ inline bool inactive_status(node_status_t status) template class mip_node_t { public: - mip_node_t(mip_node_t&&) = default; - mip_node_t& operator=(mip_node_t&&) = default; + ~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() : status(node_status_t::PENDING), @@ -329,4 +364,99 @@ 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 deleted file mode 100644 index 6450cf30dc..0000000000 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ /dev/null @@ -1,164 +0,0 @@ -/* 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 - -#include - -#include -#include -#include -#include -#include - -namespace cuopt::linear_programming::dual_simplex { - -template -class search_tree_t { - public: - search_tree_t() = default; - - search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } - - ~search_tree_t() { clean(); } - - void update(mip_node_t* node_ptr, node_status_t status) - { - std::lock_guard lock(mutex); - - --num_open_nodes; - if (status == node_status_t::HAS_CHILDREN) { - ++num_inner_nodes; - } else { - ++num_final_nodes; - progress += std::ldexp(f_t(1), -node_ptr->depth); - } - - 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 - num_open_nodes += 2; - } - - 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.print_format("Node{} [label=\"{} {:.16e}\"]\n", node_ptr->node_id, label, 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.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); - } - } - - // 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()); - 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` - } - - 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 = 0; - - // Number of nodes that still needs to be explored - omp_atomic_t num_open_nodes = 0; - - // Number of integer feasible, infeasible or fathomed nodes - omp_atomic_t num_final_nodes = 0; - - // Number of 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; - - static constexpr bool write_graphviz = false; -}; - -} // namespace cuopt::linear_programming::dual_simplex From 739aab2b1410f7f5f2b9585733ec6ea41ed1be08 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:28:43 +0200 Subject: [PATCH 13/14] fixed compilation Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 17 ++++++++--------- 1 file changed, 8 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 5af526d1f7..c45f1a730c 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -381,15 +381,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}", From 3b9f99c179e0570b42ba094a16b2b44b059899ce Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 14:37:47 +0200 Subject: [PATCH 14/14] use constexpr constants to determine column width Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c45f1a730c..c76e30b647 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,6 +45,19 @@ namespace cuopt::linear_programming::dual_simplex { namespace { +// Column widths for the branch-and-bound progress table. +constexpr int SYMBOL_WIDTH = 1; +constexpr int EXPLORED_WIDTH = 12; +constexpr int UNEXPLORED_WIDTH = 12; +constexpr int OBJECTIVE_WIDTH = 19; +constexpr int BOUND_WIDTH = 15; +constexpr int INTEGER_INFEASIBLE_WIDTH = 8; +constexpr int DEPTH_WIDTH = 7; +constexpr int ITER_NODE_WIDTH = 11; +constexpr int GAP_WIDTH = 11; +constexpr int WORK_WIDTH = 8; +constexpr int TIME_WIDTH = 8; + template bool is_fractional(f_t x, variable_type_t var_type, f_t integer_tol) { @@ -319,18 +332,28 @@ 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}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap"); - if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } - header += std::format("{:^8}|", "Time"); + std::string header = + std::format("{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|", + "", + SYMBOL_WIDTH, + "Explored", + EXPLORED_WIDTH, + "Unexplored", + UNEXPLORED_WIDTH, + "Objective", + OBJECTIVE_WIDTH, + "Bound", + BOUND_WIDTH, + "IntInf", + INTEGER_INFEASIBLE_WIDTH, + "Depth", + DEPTH_WIDTH, + "Iter/Node", + ITER_NODE_WIDTH, + "Gap", + GAP_WIDTH); + if (settings_.deterministic) { header += std::format("{:^{}}|", "Work", WORK_WIDTH); } + header += std::format("{:^{}}|", "Time", TIME_WIDTH); settings_.log.printf("%s\n", header.c_str()); } @@ -345,18 +368,28 @@ 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}", + std::format("{:^{}} {:>{}} {:>{}} {:^+{}.6e} {:^+{}.6e} {:>{}} {:>{}} {:^{}} {:^{}}", + "H", + SYMBOL_WIDTH, "", // nodes explored + EXPLORED_WIDTH, "", // nodes unexplored + UNEXPLORED_WIDTH, user_obj, + OBJECTIVE_WIDTH, user_lower, + BOUND_WIDTH, "", // integer infeasible + INTEGER_INFEASIBLE_WIDTH, "", // depth + DEPTH_WIDTH, "", // iter/node - user_gap_text); + ITER_NODE_WIDTH, + user_gap_text, + GAP_WIDTH); - if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } - log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); + if (settings_.deterministic) { log_line += std::format("{:^{}}", "", WORK_WIDTH); } + log_line += std::format(" {:>{}.2f}", toc(exploration_stats_.start_time), TIME_WIDTH); settings_.log.printf("%s\n", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { @@ -391,18 +424,27 @@ void branch_and_bound_t::report( 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}", + std::format("{:^{}} {:>{}} {:>{}} {:^+{}.6e} {:^+{}.6e} {:>{}} {:>{}} {:^{}.1e} {:^{}}", symbol, + SYMBOL_WIDTH, nodes_explored, + EXPLORED_WIDTH, nodes_unexplored, + UNEXPLORED_WIDTH, user_obj, + OBJECTIVE_WIDTH, user_lower, + BOUND_WIDTH, node_int_infeas, + INTEGER_INFEASIBLE_WIDTH, node_depth, + DEPTH_WIDTH, 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)); + ITER_NODE_WIDTH, + user_gap_text, + GAP_WIDTH); + if (work_time >= 0) { log_line += std::format(" {:>{}.2f}", work_time, WORK_WIDTH); } + log_line += std::format(" {:>{}.2f}", toc(exploration_stats_.start_time), TIME_WIDTH); settings_.log.printf("%s\n", log_line.c_str()); }