From 24edb3ee019eb1853f0fd333d229e0bf4aaf6b3e Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 26 Aug 2026 14:39:32 +0200 Subject: [PATCH 1/3] stop the plunge based on its depth and the local gap. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 13 +++++++++++++ cpp/src/branch_and_bound/worker.hpp | 1 + cpp/src/dual_simplex/simplex_solver_settings.hpp | 6 ++++++ cpp/src/utilities/omp_helpers.hpp | 12 ++++++++++++ 4 files changed, 32 insertions(+) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 29174148b7..85963c3a7e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1722,6 +1722,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, // relaxation // - The lower bound of the parent is lower or equal to its children worker->lower_bound = node_ptr->lower_bound; + fetch_max(exploration_stats_.max_node_depth, node_ptr->depth); if (node_ptr->lower_bound > upper_bound_.load()) { search_tree_.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); @@ -1772,6 +1773,18 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, break; } + i_t max_node_depth = exploration_stats_.max_node_depth; + i_t plunge_depth = node_ptr->depth - start_node->depth; + + if (plunge_depth >= settings_.bnb_min_plunge_depth * max_node_depth) { + f_t max_bound = lower_bound + settings_.bnb_plunge_gap_factor * (upper_bound - lower_bound); + if (node_ptr->lower_bound >= max_bound || + plunge_depth >= settings_.bnb_max_plunge_depth * max_node_depth) { + stack.push_front(node_ptr); + break; + } + } + decompress_vstatus( node_ptr->packed_vstatus, worker->leaf_problem.num_cols, worker->leaf_vstatus); assert(worker->leaf_vstatus.size() == worker->leaf_problem.num_cols); diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index bbc2836d43..7031a30178 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -30,6 +30,7 @@ struct branch_and_bound_stats_t { 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 max_node_depth = 0; omp_atomic_t total_simplex_iters = 0; omp_atomic_t nodes_since_last_log = 0; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 1247bcb460..1cad48d685 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -108,6 +108,9 @@ struct simplex_solver_settings_t { bnb_steal_chance(-1), bnb_nodes_per_steal(-1), bnb_max_steal_attempts(-1), + bnb_plunge_gap_factor(0.25), + bnb_min_plunge_depth(0.1), + bnb_max_plunge_depth(0.5), reliability_branching(-1), inside_mip(0), inside_submip(0), @@ -222,6 +225,9 @@ struct simplex_solver_settings_t { f_t bnb_steal_chance; i_t bnb_nodes_per_steal; i_t bnb_max_steal_attempts; + f_t bnb_plunge_gap_factor; + f_t bnb_min_plunge_depth; + f_t bnb_max_plunge_depth; // Settings for the reliability branching. // - -1: automatic diff --git a/cpp/src/utilities/omp_helpers.hpp b/cpp/src/utilities/omp_helpers.hpp index 3fd31d169e..7bf56f0859 100644 --- a/cpp/src/utilities/omp_helpers.hpp +++ b/cpp/src/utilities/omp_helpers.hpp @@ -219,6 +219,7 @@ class omp_atomic_t { friend double fetch_min(omp_atomic_t& atomic_var, double other); friend double fetch_max(omp_atomic_t& atomic_var, double other); + friend double fetch_max(omp_atomic_t& atomic_var, int other); }; // Free non-template functions are necessary because of a clang 20 bug @@ -246,6 +247,17 @@ inline double fetch_max(omp_atomic_t& atomic_var, double other) return old; } +inline double fetch_max(omp_atomic_t& atomic_var, int other) +{ + double old; +#pragma omp atomic compare capture + { + old = atomic_var.val; + if (other > atomic_var.val) { atomic_var.val = other; } + } + return old; +} + } // namespace cuopt #endif From 7fc1ee546865f6ba11c9cc7e364a49cc659146f8 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 27 Aug 2026 13:04:03 +0200 Subject: [PATCH 2/3] addressed reviews Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 7 ++- cpp/src/utilities/omp_helpers.hpp | 55 ++++++------------- 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 85963c3a7e..e483c37035 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1124,7 +1124,7 @@ struct nondeterministic_policy_t : tree_update_policy_t { void on_numerical_issue(mip_node_t* node) override { if (worker->search_strategy == search_strategy_t::BEST_FIRST) { - fetch_min(bnb.lower_bound_numerical_, node->lower_bound); + bnb.lower_bound_numerical_.fetch_min(node->lower_bound); 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())); @@ -1722,7 +1722,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, // relaxation // - The lower bound of the parent is lower or equal to its children worker->lower_bound = node_ptr->lower_bound; - fetch_max(exploration_stats_.max_node_depth, node_ptr->depth); + exploration_stats_.max_node_depth.fetch_max(node_ptr->depth); if (node_ptr->lower_bound > upper_bound_.load()) { search_tree_.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); @@ -1781,6 +1781,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, if (node_ptr->lower_bound >= max_bound || plunge_depth >= settings_.bnb_max_plunge_depth * max_node_depth) { stack.push_front(node_ptr); + --exploration_stats_.nodes_being_solved; break; } } @@ -3470,6 +3471,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_lp_current_lower_bound_ = -inf; exploration_stats_.nodes_unexplored = 0; exploration_stats_.nodes_explored = 0; + exploration_stats_.max_node_depth = 0; original_lp_.A.to_compressed_row(Arow_); settings_.log.debug("Reduced cost strengthening enabled: %d\n", @@ -3900,6 +3902,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_.max_node_depth = 0; exploration_stats_.nodes_since_last_log = 0; exploration_stats_.last_log = tic(); min_node_queue_size_ = 20; diff --git a/cpp/src/utilities/omp_helpers.hpp b/cpp/src/utilities/omp_helpers.hpp index 7bf56f0859..06ebf73a24 100644 --- a/cpp/src/utilities/omp_helpers.hpp +++ b/cpp/src/utilities/omp_helpers.hpp @@ -214,50 +214,31 @@ class omp_atomic_t { T& underlying() { return val; } T underlying() const { return val; } - private: - T val; - - friend double fetch_min(omp_atomic_t& atomic_var, double other); - friend double fetch_max(omp_atomic_t& atomic_var, double other); - friend double fetch_max(omp_atomic_t& atomic_var, int other); -}; - -// Free non-template functions are necessary because of a clang 20 bug -// when omp atomic compare is used within a templated context. -// see https://github.com/llvm/llvm-project/issues/127466 -inline double fetch_min(omp_atomic_t& atomic_var, double other) -{ - double old; -#pragma omp atomic compare capture + T fetch_min(T other) { - old = atomic_var.val; - if (other < atomic_var.val) { atomic_var.val = other; } - } - return old; -} - -inline double fetch_max(omp_atomic_t& atomic_var, double other) -{ - double old; + T old; #pragma omp atomic compare capture - { - old = atomic_var.val; - if (other > atomic_var.val) { atomic_var.val = other; } + { + old = val; + if (other < val) { val = other; } + } + return old; } - return old; -} -inline double fetch_max(omp_atomic_t& atomic_var, int other) -{ - double old; -#pragma omp atomic compare capture + T fetch_max(T other) { - old = atomic_var.val; - if (other > atomic_var.val) { atomic_var.val = other; } + T old; +#pragma omp atomic compare capture + { + old = val; + if (other > val) { val = other; } + } + return old; } - return old; -} + private: + T val; +}; } // namespace cuopt #endif From f8f05460e09ef33d68ba7d226ef71e34666c7b76 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 27 Aug 2026 16:40:21 +0200 Subject: [PATCH 3/3] fixed missing changes to fetch_min Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e483c37035..31ad66abe5 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -4819,7 +4819,7 @@ void branch_and_bound_t::deterministic_sort_replay_events( deterministic_merge_pseudo_cost_updates(*deterministic_workers_); for (const auto& worker : *deterministic_workers_) { - fetch_min(lower_bound_numerical_, worker.local_lower_bound_ceiling); + lower_bound_numerical_.fetch_min(worker.local_lower_bound_ceiling); } }