From 181aef4849b915948b4d06b25114473e863094ce Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 9 Sep 2025 15:25:22 +0200 Subject: [PATCH 01/86] added a dual simplex method for calculating the phase 2 from an existing basis_update_mpf_t. --- cpp/src/dual_simplex/basis_updates.cpp | 63 ++++++++++++++ cpp/src/dual_simplex/basis_updates.hpp | 51 ++++++++++- cpp/src/dual_simplex/phase2.cpp | 115 +++++++++++++------------ cpp/src/dual_simplex/phase2.hpp | 14 +++ 4 files changed, 183 insertions(+), 60 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 18f74ccc2e..0dba9910dc 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -15,7 +15,9 @@ * limitations under the License. */ +#include #include +#include #include #include @@ -2046,6 +2048,67 @@ void basis_update_mpf_t::multiply_lu(csc_matrix_t& out) cons out.nz_max = B_nz; } +template +int basis_update_mpf_t::factorize_basis( + const csc_matrix_t& A, + const simplex_solver_settings_t& settings, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus) +{ + std::vector deficient; + std::vector slacks_needed; + + if (dual_simplex::factorize_basis(A, + settings, + basic_list, + L0_, + U0_, + row_permutation_, + inverse_row_permutation_, + col_permutation_, + deficient, + slacks_needed) == -1) { + settings.log.debug("Initial factorization failed\n"); + basis_repair(A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); + +#ifdef CHECK_BASIS_REPAIR + csc_matrix_t B(m, m, 0); + form_b(A, basic_list, B); + for (i_t k = 0; k < deficient.size(); ++k) { + const i_t j = deficient[k]; + const i_t col_start = B.col_start[j]; + const i_t col_end = B.col_start[j + 1]; + const i_t col_nz = col_end - col_start; + if (col_nz != 1) { settings.log.printf("Deficient column %d has %d nonzeros\n", j, col_nz); } + const i_t i = B.i[col_start]; + if (i != slacks_needed[k]) { + settings.log.printf("Slack %d needed but found %d instead\n", slacks_needed[k], i); + } + } +#endif + + if (dual_simplex::factorize_basis(A, + settings, + basic_list, + L0_, + U0_, + row_permutation_, + inverse_row_permutation_, + col_permutation_, + deficient, + slacks_needed) == -1) { + return deficient.size(); + } + settings.log.printf("Basis repaired\n"); + } + + assert(col_permutation_.size() == A.m); + reorder_basic_list(col_permutation_, basic_list); + reset(); + return 0; +} + #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE template class basis_update_t; template class basis_update_mpf_t; diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index b12950abb7..3198ccd4b8 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -17,6 +17,8 @@ #pragma once +#include +#include #include #include #include @@ -176,6 +178,33 @@ class basis_update_t { template class basis_update_mpf_t { public: + basis_update_mpf_t(i_t n, const i_t refactor_frequency) + : L0_(n, n, 1), + U0_(n, n, 1), + row_permutation_(n), + inverse_row_permutation_(n), + S_(n, 0, 0), + col_permutation_(n), + inverse_col_permutation_(n), + xi_workspace_(2 * n, 0), + x_workspace_(n, 0.0), + U0_transpose_(1, 1, 1), + L0_transpose_(1, 1, 1), + refactor_frequency_(refactor_frequency), + total_sparse_L_transpose_(0), + total_dense_L_transpose_(0), + total_sparse_L_(0), + total_dense_L_(0), + total_sparse_U_transpose_(0), + total_dense_U_transpose_(0), + total_sparse_U_(0), + total_dense_U_(0), + hypersparse_threshold_(0.05) + { + clear(); + reset_stats(); + } + basis_update_mpf_t(const csc_matrix_t& Linit, const csc_matrix_t& Uinit, const std::vector& p, @@ -205,7 +234,7 @@ class basis_update_mpf_t { inverse_permutation(row_permutation_, inverse_row_permutation_); clear(); compute_transposes(); - reset_stas(); + reset_stats(); } void print_stats() const @@ -226,7 +255,7 @@ class basis_update_mpf_t { // clang-format on } - void reset_stas() + void reset_stats() { num_calls_L_ = 0; num_calls_U_ = 0; @@ -249,7 +278,16 @@ class basis_update_mpf_t { inverse_permutation(row_permutation_, inverse_row_permutation_); clear(); compute_transposes(); - reset_stas(); + reset_stats(); + return 0; + } + + i_t reset() + { + inverse_permutation(row_permutation_, inverse_row_permutation_); + clear(); + compute_transposes(); + reset_stats(); return 0; } @@ -332,6 +370,13 @@ class basis_update_mpf_t { void multiply_lu(csc_matrix_t& out) const; + // Compute L*U = A(p, basic_list) + int factorize_basis(const csc_matrix_t& A, + const simplex_solver_settings_t& settings, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus); + private: void clear() { diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 6e468f9154..f83dfe4a5d 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2195,6 +2195,57 @@ dual::status_t dual_phase2(i_t phase, std::vector nonbasic_list; std::vector superbasic_list; + phase2::bound_info(lp, settings); + get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); + assert(superbasic_list.size() == 0); + assert(nonbasic_list.size() == n - m); + + basis_update_mpf_t ft(m, settings.refactor_frequency); + + if (ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + return dual::status_t::NUMERICAL; + } + + if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + return dual_phase2_with_basis_update(phase, + slack_basis, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + delta_y_steepest_edge); +} + +template +dual::status_t dual_phase2_with_basis_update(i_t phase, + i_t slack_basis, + f_t start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + i_t& iter, + std::vector& delta_y_steepest_edge) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + assert(m <= n); + assert(vstatus.size() == n); + assert(lp.A.m == m); + assert(lp.A.n == n); + assert(lp.objective.size() == n); + assert(lp.lower.size() == n); + assert(lp.upper.size() == n); + assert(lp.rhs.size() == m); + std::vector& x = sol.x; std::vector& y = sol.y; std::vector& z = sol.z; @@ -2208,35 +2259,6 @@ dual::status_t dual_phase2(i_t phase, std::vector vstatus_old = vstatus; std::vector z_old = z; - phase2::bound_info(lp, settings); - get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); - assert(superbasic_list.size() == 0); - assert(nonbasic_list.size() == n - m); - - // Compute L*U = A(p, basic_list) - csc_matrix_t L(m, m, 1); - csc_matrix_t U(m, m, 1); - std::vector pinv(m); - std::vector p; - std::vector q; - std::vector deficient; - std::vector slacks_needed; - - if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == - -1) { - settings.log.debug("Initial factorization failed\n"); - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); - if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == - -1) { - return dual::status_t::NUMERICAL; - } - settings.log.printf("Basis repaired\n"); - } - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } - assert(q.size() == m); - reorder_basic_list(q, basic_list); - basis_update_mpf_t ft(L, U, p, settings.refactor_frequency); - std::vector c_basic(m); for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; @@ -2872,48 +2894,27 @@ dual::status_t dual_phase2(i_t phase, #endif if (should_refactor) { bool should_recompute_x = false; - if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == - -1) { + if (ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } - basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); i_t count = 0; - while (factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { + i_t deficient_size; + while ((deficient_size = + ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus)) > 0) { settings.log.printf("Failed to repair basis. Iteration %d. %d deficient columns.\n", iter, - static_cast(deficient.size())); + static_cast(deficient_size)); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } settings.threshold_partial_pivoting_tol = 1.0; + count++; if (count > 10) { return dual::status_t::NUMERICAL; } - basis_repair( - lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); - -#ifdef CHECK_BASIS_REPAIR - csc_matrix_t B(m, m, 0); - form_b(lp.A, basic_list, B); - for (i_t k = 0; k < deficient.size(); ++k) { - const i_t j = deficient[k]; - const i_t col_start = B.col_start[j]; - const i_t col_end = B.col_start[j + 1]; - const i_t col_nz = col_end - col_start; - if (col_nz != 1) { - settings.log.printf("Deficient column %d has %d nonzeros\n", j, col_nz); - } - const i_t i = B.i[col_start]; - if (i != slacks_needed[k]) { - settings.log.printf("Slack %d needed but found %d instead\n", slacks_needed[k], i); - } - } -#endif } settings.log.printf("Successfully repaired basis. Iteration %d\n", iter); } - reorder_basic_list(q, basic_list); - ft.reset(L, U, p); + phase2::reset_basis_mark(basic_list, nonbasic_list, basic_mark, nonbasic_mark); if (should_recompute_x) { std::vector unperturbed_x(n); diff --git a/cpp/src/dual_simplex/phase2.hpp b/cpp/src/dual_simplex/phase2.hpp index 39311e0607..0b50453113 100644 --- a/cpp/src/dual_simplex/phase2.hpp +++ b/cpp/src/dual_simplex/phase2.hpp @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -66,4 +67,17 @@ dual::status_t dual_phase2(i_t phase, i_t& iter, std::vector& steepest_edge_norms); +template +dual::status_t dual_phase2_with_basis_update(i_t phase, + i_t slack_basis, + f_t start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + i_t& iter, + std::vector& delta_y_steepest_edge); } // namespace cuopt::linear_programming::dual_simplex From 09a5e7e74091e33da22889a86d953635bcf23326 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 6 Oct 2025 15:31:50 +0200 Subject: [PATCH 02/86] child nodes now reuse the bounds of the parent. fixed incorrect bounds when detaching the node from the tree. add backtracking argument to diving. --- cpp/src/dual_simplex/branch_and_bound.cpp | 88 +++++++++++++++-------- cpp/src/dual_simplex/branch_and_bound.hpp | 55 ++++++++++---- cpp/src/dual_simplex/mip_node.hpp | 31 ++++---- 3 files changed, 119 insertions(+), 55 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index cf6fd69798..027a27e95a 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -556,9 +557,10 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& mip_node_t* node_ptr, lp_problem_t& leaf_problem, const csc_matrix_t& Arow, + const std::vector& bounds_changed, + char thread_type, f_t upper_bound, - logger_t& log, - char thread_type) + logger_t& log) { f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; @@ -566,16 +568,6 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); assert(leaf_vstatus.size() == leaf_problem.num_cols); - // Set the correct bounds for the leaf problem - leaf_problem.lower = original_lp_.lower; - leaf_problem.upper = original_lp_.upper; - - std::vector bounds_changed(leaf_problem.num_cols, false); - // Technically, we can get the already strengthened bounds from the node/parent instead of - // getting it from the original problem and re-strengthening. But this requires storing - // two vectors at each node and potentially cause memory issues - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); - i_t node_iter = 0; f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; @@ -744,8 +736,16 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* status_ = mip_exploration_status_t::TIME_LIMIT; return; } - node_status_t node_status = - solve_node(*search_tree, node, leaf_problem, Arow, upper_bound, settings_.log, 'B'); + + std::vector bounds_changed(leaf_problem.num_cols, false); + + // Set the correct bounds for the leaf problem + leaf_problem.lower = original_lp_.lower; + leaf_problem.upper = original_lp_.upper; + node->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); + + node_status_t node_status = solve_node( + *search_tree, node, leaf_problem, Arow, bounds_changed, 'B', upper_bound, settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -780,9 +780,12 @@ void branch_and_bound_t::explore_subtree(i_t id, lp_problem_t& leaf_problem, const csc_matrix_t& Arow) { + std::vector bounds_changed(leaf_problem.num_cols); std::deque*> stack; stack.push_front(start_node); + bool recompute_bounds = true; + while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { if (omp_get_thread_num() == 0) { repair_heuristic_solutions(); } @@ -842,8 +845,20 @@ void branch_and_bound_t::explore_subtree(i_t id, return; } - node_status_t node_status = - solve_node(search_tree, node_ptr, leaf_problem, Arow, upper_bound, settings_.log, 'B'); + // Reset the bounds for the leaf problem to the original problem + if (recompute_bounds) { + leaf_problem.lower = original_lp_.lower; + leaf_problem.upper = original_lp_.upper; + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); + + } else { + node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); + } + + node_status_t node_status = solve_node( + search_tree, node_ptr, leaf_problem, Arow, bounds_changed, 'B', upper_bound, settings_.log); + recompute_bounds = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -866,7 +881,7 @@ void branch_and_bound_t::explore_subtree(i_t id, // would be better if we discard the node instead. if (get_heap_size() > settings_.num_bfs_threads) { mutex_dive_queue_.lock(); - dive_queue_.push(node->detach_copy()); + dive_queue_.emplace(node->detach_copy(), leaf_problem.lower, leaf_problem.upper); mutex_dive_queue_.unlock(); } @@ -943,26 +958,31 @@ void branch_and_bound_t::best_first_thread(i_t id, template void branch_and_bound_t::diving_thread(lp_problem_t& leaf_problem, - const csc_matrix_t& Arow) + const csc_matrix_t& Arow, + i_t backtracking) { logger_t log; log.log = false; + std::vector bounds_changed(leaf_problem.num_cols); + while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { - std::optional> start_node; + std::optional> start_node; mutex_dive_queue_.lock(); if (dive_queue_.size() > 0) { start_node = dive_queue_.pop(); } mutex_dive_queue_.unlock(); if (start_node.has_value()) { - if (get_upper_bound() < start_node->lower_bound) { continue; } + if (get_upper_bound() < start_node->node.lower_bound) { continue; } - search_tree_t subtree(std::move(start_node.value())); + search_tree_t subtree(std::move(start_node->node)); std::deque*> stack; stack.push_front(&subtree.root); + bool recompute_bounds = true; + while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -975,8 +995,19 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr if (toc(stats_.start_time) > settings_.time_limit) { return; } + // Reset the bounds for the leaf problem based on the starting node + if (recompute_bounds) { + leaf_problem.lower = start_node->lp_lower; + leaf_problem.upper = start_node->lp_upper; + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); + } else { + node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); + } + node_status_t node_status = - solve_node(subtree, node_ptr, leaf_problem, Arow, upper_bound, log, 'D'); + solve_node(subtree, node_ptr, leaf_problem, Arow, bounds_changed, 'D', upper_bound, log); + recompute_bounds = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { return; @@ -985,16 +1016,15 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr auto [first, second] = child_selection(node_ptr); stack.push_front(second); stack.push_front(first); + } - // If the diving thread is consuming the nodes faster than the - // best first search, then we split the current subtree at the - // lowest possible point and move to the queue, so it can - // be picked by another thread. - if (dive_queue_.size() < min_diving_queue_size_) { + if (stack.size() > 1) { + if (dive_queue_.size() < min_diving_queue_size_ || + (stack.front()->depth - stack.back()->depth) > backtracking) { mutex_dive_queue_.lock(); mip_node_t* new_node = stack.back(); stack.pop_back(); - dive_queue_.push(new_node->detach_copy()); + dive_queue_.emplace(new_node->detach_copy(), leaf_problem.lower, leaf_problem.upper); mutex_dive_queue_.unlock(); } } @@ -1192,7 +1222,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(leaf_problem, Arow); + diving_thread(leaf_problem, Arow, 10); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 7b80f88fa9..b97a4206ba 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -55,36 +55,64 @@ enum class mip_exploration_status_t { template void upper_bound_callback(f_t upper_bound); +template +struct diving_root_t { + mip_node_t node; + std::vector lp_lower; + std::vector lp_upper; + + diving_root_t(mip_node_t&& node, + const std::vector& lower, + const std::vector& upper) + : node(std::move(node)), lp_upper(upper), lp_lower(lower) + { + } + + friend bool operator>(const diving_root_t& a, const diving_root_t& b) + { + return a.node.lower_bound > b.node.lower_bound; + } +}; + // A min-heap for storing the starting nodes for the dives. -// This has a maximum size of 8192, such that the container +// This has a maximum size of 256, such that the container // will discard the least promising node if the queue is full. template class dive_queue_t { private: - std::vector> buffer; - static constexpr i_t max_size_ = 2048; + std::vector> buffer; + static constexpr i_t max_size_ = 256; public: dive_queue_t() { buffer.reserve(max_size_); } - void push(mip_node_t&& node) + void push(diving_root_t&& node) { buffer.push_back(std::move(node)); - std::push_heap(buffer.begin(), buffer.end(), node_compare_t()); + std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); + if (buffer.size() > max_size()) { buffer.pop_back(); } + } + + void emplace(mip_node_t&& node, + const std::vector& lower, + const std::vector& upper) + { + buffer.emplace_back(std::move(node), lower, upper); + std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); if (buffer.size() > max_size()) { buffer.pop_back(); } } - mip_node_t pop() + diving_root_t pop() { - std::pop_heap(buffer.begin(), buffer.end(), node_compare_t()); - mip_node_t node = std::move(buffer.back()); + std::pop_heap(buffer.begin(), buffer.end(), std::greater<>()); + diving_root_t node = std::move(buffer.back()); buffer.pop_back(); return node; } i_t size() const { return buffer.size(); } constexpr i_t max_size() const { return max_size_; } - const mip_node_t& top() const { return buffer.front(); } + const diving_root_t& top() const { return buffer.front(); } void clear() { buffer.clear(); } }; @@ -222,16 +250,19 @@ class branch_and_bound_t { // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(lp_problem_t& leaf_problem, const csc_matrix_t& Arow); + void diving_thread(lp_problem_t& leaf_problem, + const csc_matrix_t& Arow, + i_t backtracking); // Solve the LP relaxation of a leaf node and update the tree. node_status_t solve_node(search_tree_t& search_tree, mip_node_t* node_ptr, lp_problem_t& leaf_problem, const csc_matrix_t& Arow, + const std::vector& bounds_changed, + char thread_type, f_t upper_bound, - logger_t& log, - char thread_type); + logger_t& log); // Sort the children based on the Martin's criteria. std::pair*, mip_node_t*> child_selection( diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index a3a34eb81d..23ea214eb2 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -87,26 +87,29 @@ class mip_node_t { std::vector& upper, std::vector& bounds_changed) const { - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - // Apply the bounds at the current node - assert(lower.size() > branch_var); - assert(upper.size() > branch_var); - lower[branch_var] = branch_var_lower; - upper[branch_var] = branch_var_upper; - bounds_changed[branch_var] = true; + update_variable_bound(lower, upper, bounds_changed); + mip_node_t* parent_ptr = parent; while (parent_ptr != nullptr) { if (parent_ptr->node_id == 0) { break; } - assert(parent_ptr->branch_var >= 0); - assert(lower.size() > parent_ptr->branch_var); - assert(upper.size() > parent_ptr->branch_var); - lower[parent_ptr->branch_var] = parent_ptr->branch_var_lower; - upper[parent_ptr->branch_var] = parent_ptr->branch_var_upper; - bounds_changed[parent_ptr->branch_var] = true; - parent_ptr = parent_ptr->parent; + parent_ptr->update_variable_bound(lower, upper, bounds_changed); + parent_ptr = parent_ptr->parent; } } + void update_variable_bound(std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) const + { + // Apply the bounds at the current node + assert(branch_var >= 0); + assert(lower.size() > branch_var); + assert(upper.size() > branch_var); + lower[branch_var] = branch_var_lower; + upper[branch_var] = branch_var_upper; + bounds_changed[branch_var] = true; + } + mip_node_t* get_down_child() const { return children[0].get(); } mip_node_t* get_up_child() const { return children[1].get(); } From 0a7a7ebecef44ca54301f6090e5753b6ea1fce2a Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 9 Oct 2025 12:17:44 +0200 Subject: [PATCH 03/86] refactor based on the reuse-parent-branch --- cpp/src/dual_simplex/branch_and_bound.cpp | 132 ++++++++++++++-------- cpp/src/dual_simplex/branch_and_bound.hpp | 27 +++-- cpp/src/dual_simplex/mip_node.hpp | 2 +- 3 files changed, 100 insertions(+), 61 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 027a27e95a..f4122fa1bc 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -553,28 +553,20 @@ branch_and_bound_t::child_selection(mip_node_t* node_ptr) } template -node_status_t branch_and_bound_t::solve_node(search_tree_t& search_tree, - mip_node_t* node_ptr, - lp_problem_t& leaf_problem, - const csc_matrix_t& Arow, - const std::vector& bounds_changed, - char thread_type, - f_t upper_bound, - logger_t& log) +dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + lp_solution_t& leaf_solution, + const csc_matrix_t& Arow, + const std::vector& bounds_changed, + logger_t& log) { - f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; - + std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; std::vector& leaf_vstatus = node_ptr->vstatus; - lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); assert(leaf_vstatus.size() == leaf_problem.num_cols); - i_t node_iter = 0; - f_t lp_start_time = tic(); - std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; - simplex_solver_settings_t lp_settings = settings_; lp_settings.set_log(false); - lp_settings.cut_off = upper_bound + settings_.dual_tol; + lp_settings.cut_off = get_upper_bound() + settings_.dual_tol; lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); @@ -586,7 +578,9 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; if (feasible) { - lp_status = dual_phase2(2, + i_t node_iter = 0; + f_t lp_start_time = tic(); + lp_status = dual_phase2(2, 0, lp_start_time, leaf_problem, @@ -602,16 +596,32 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } + + stats_.total_lp_solve_time += toc(lp_start_time); + stats_.total_lp_iters += node_iter; } - stats_.total_lp_solve_time += toc(lp_start_time); - stats_.total_lp_iters += node_iter; + return lp_status; +} + +template +node_status_t branch_and_bound_t::update_tree( + search_tree_t& search_tree, + mip_node_t* node_ptr, + const lp_problem_t& leaf_problem, + const lp_solution_t& leaf_solution, + dual::status_t lp_status, + char thread_type, + logger_t& log) +{ + f_t upper_bound = get_upper_bound(); + f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; if (lp_status == dual::status_t::DUAL_UNBOUNDED) { // Node was infeasible. Do not branch node_ptr->lower_bound = inf; search_tree.graphviz_node(log, node_ptr, "infeasible", 0.0); - search_tree.update_tree(node_ptr, node_status_t::INFEASIBLE); + search_tree.update(node_ptr, node_status_t::INFEASIBLE); return node_status_t::INFEASIBLE; } else if (lp_status == dual::status_t::CUTOFF) { @@ -619,7 +629,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& node_ptr->lower_bound = upper_bound; f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); search_tree.graphviz_node(log, node_ptr, "cut off", leaf_objective); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); return node_status_t::FATHOMED; } else if (lp_status == dual::status_t::OPTIMAL) { @@ -637,28 +647,30 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& // Found a integer feasible solution add_feasible_solution(leaf_objective, leaf_solution.x, node_ptr->depth, thread_type); search_tree.graphviz_node(log, node_ptr, "integer feasible", leaf_objective); - search_tree.update_tree(node_ptr, node_status_t::INTEGER_FEASIBLE); + search_tree.update(node_ptr, node_status_t::INTEGER_FEASIBLE); return node_status_t::INTEGER_FEASIBLE; } else if (leaf_objective <= upper_bound + abs_fathom_tol) { + logger_t pc_log = log; + pc_log.log = false; + // Choose fractional variable to branch on - const i_t branch_var = - pc_.variable_selection(leaf_fractional, leaf_solution.x, lp_settings.log); + const i_t branch_var = pc_.variable_selection(leaf_fractional, leaf_solution.x, pc_log); - assert(leaf_vstatus.size() == leaf_problem.num_cols); + assert(node_ptr->vstatus.size() == leaf_problem.num_cols); search_tree.branch( - node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, original_lp_, log); + node_ptr, branch_var, leaf_solution.x[branch_var], node_ptr->vstatus, original_lp_, log); node_ptr->status = node_status_t::HAS_CHILDREN; return node_status_t::HAS_CHILDREN; } else { search_tree.graphviz_node(log, node_ptr, "fathomed", leaf_objective); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); return node_status_t::FATHOMED; } } else if (lp_status == dual::status_t::TIME_LIMIT) { search_tree.graphviz_node(log, node_ptr, "timeout", 0.0); - search_tree.update_tree(node_ptr, node_status_t::TIME_LIMIT); + search_tree.update(node_ptr, node_status_t::TIME_LIMIT); return node_status_t::TIME_LIMIT; } else { @@ -674,7 +686,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& } search_tree.graphviz_node(log, node_ptr, "numerical", 0.0); - search_tree.update_tree(node_ptr, node_status_t::NUMERICAL); + search_tree.update(node_ptr, node_status_t::NUMERICAL); return node_status_t::NUMERICAL; } } @@ -702,7 +714,7 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree->graphviz_node(settings_.log, node, "cutoff", node->lower_bound); - search_tree->update_tree(node, node_status_t::FATHOMED); + search_tree->update(node, node_status_t::FATHOMED); return; } @@ -728,6 +740,7 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* gap_user.c_str(), now); + stats_.last_log = tic(); stats_.nodes_since_last_log = 0; } } @@ -737,15 +750,21 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* return; } - std::vector bounds_changed(leaf_problem.num_cols, false); + const i_t m = leaf_problem.num_rows; + const i_t n = leaf_problem.num_cols; + std::vector bounds_changed(n, false); + lp_solution_t leaf_solution(m, n); // Set the correct bounds for the leaf problem leaf_problem.lower = original_lp_.lower; leaf_problem.upper = original_lp_.upper; node->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); - node_status_t node_status = solve_node( - *search_tree, node, leaf_problem, Arow, bounds_changed, 'B', upper_bound, settings_.log); + dual::status_t lp_status = + solve_node_lp(node, leaf_problem, leaf_solution, Arow, bounds_changed, settings_.log); + + node_status_t node_status = + update_tree(*search_tree, node, leaf_problem, leaf_solution, lp_status, 'B', settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -780,12 +799,15 @@ void branch_and_bound_t::explore_subtree(i_t id, lp_problem_t& leaf_problem, const csc_matrix_t& Arow) { - std::vector bounds_changed(leaf_problem.num_cols); + const i_t m = leaf_problem.num_rows; + const i_t n = leaf_problem.num_cols; + bool recompute = true; + std::vector bounds_changed(n); + lp_solution_t leaf_solution(m, n); + std::deque*> stack; stack.push_front(start_node); - bool recompute_bounds = true; - while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { if (omp_get_thread_num() == 0) { repair_heuristic_solutions(); } @@ -811,7 +833,8 @@ void branch_and_bound_t::explore_subtree(i_t id, if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); + recompute = true; continue; } @@ -845,8 +868,8 @@ void branch_and_bound_t::explore_subtree(i_t id, return; } - // Reset the bounds for the leaf problem to the original problem - if (recompute_bounds) { + // Recompute the bounds + if (recompute) { leaf_problem.lower = original_lp_.lower; leaf_problem.upper = original_lp_.upper; std::fill(bounds_changed.begin(), bounds_changed.end(), false); @@ -856,9 +879,11 @@ void branch_and_bound_t::explore_subtree(i_t id, node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); } - node_status_t node_status = solve_node( - search_tree, node_ptr, leaf_problem, Arow, bounds_changed, 'B', upper_bound, settings_.log); - recompute_bounds = node_status != node_status_t::HAS_CHILDREN; + dual::status_t lp_status = + solve_node_lp(node_ptr, leaf_problem, leaf_solution, Arow, bounds_changed, settings_.log); + node_status_t node_status = update_tree( + search_tree, node_ptr, leaf_problem, leaf_solution, lp_status, 'B', settings_.log); + recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -929,7 +954,7 @@ void branch_and_bound_t::best_first_thread(i_t id, // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); active_subtrees_--; continue; } @@ -964,7 +989,8 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr logger_t log; log.log = false; - std::vector bounds_changed(leaf_problem.num_cols); + const i_t m = leaf_problem.num_rows; + const i_t n = leaf_problem.num_cols; while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { @@ -977,12 +1003,13 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr if (start_node.has_value()) { if (get_upper_bound() < start_node->node.lower_bound) { continue; } + bool recompute = true; + std::vector bounds_changed(n); + lp_solution_t leaf_solution(m, n); search_tree_t subtree(std::move(start_node->node)); std::deque*> stack; stack.push_front(&subtree.root); - bool recompute_bounds = true; - while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); @@ -990,13 +1017,14 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node_ptr->lower_bound); if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { + recompute = true; continue; } if (toc(stats_.start_time) > settings_.time_limit) { return; } - // Reset the bounds for the leaf problem based on the starting node - if (recompute_bounds) { + // Recompute the bounds + if (recompute) { leaf_problem.lower = start_node->lp_lower; leaf_problem.upper = start_node->lp_upper; std::fill(bounds_changed.begin(), bounds_changed.end(), false); @@ -1005,9 +1033,12 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); } + dual::status_t lp_status = + solve_node_lp(node_ptr, leaf_problem, leaf_solution, Arow, bounds_changed, log); + node_status_t node_status = - solve_node(subtree, node_ptr, leaf_problem, Arow, bounds_changed, 'D', upper_bound, log); - recompute_bounds = node_status != node_status_t::HAS_CHILDREN; + update_tree(subtree, node_ptr, leaf_problem, leaf_solution, lp_status, 'D', log); + recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { return; @@ -1068,6 +1099,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, stats_.start_time, lp_settings, root_relax_soln_, root_vstatus_, edge_norms_); stats_.total_lp_iters = root_relax_soln_.iterations; stats_.total_lp_solve_time = toc(stats_.start_time); + if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); // FIXME: rarely dual simplex detects infeasible whereas it is feasible. diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index b97a4206ba..f27d9476ff 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -216,7 +216,7 @@ class branch_and_bound_t { // Set the final solution. mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); - // Update the incumbent solution with the new feasible solution. + // Update the incumbent solution with the new feasible solution // found during branch and bound. void add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, @@ -254,15 +254,22 @@ class branch_and_bound_t { const csc_matrix_t& Arow, i_t backtracking); - // Solve the LP relaxation of a leaf node and update the tree. - node_status_t solve_node(search_tree_t& search_tree, - mip_node_t* node_ptr, - lp_problem_t& leaf_problem, - const csc_matrix_t& Arow, - const std::vector& bounds_changed, - char thread_type, - f_t upper_bound, - logger_t& log); + // Solve the LP relaxation of a leaf node. + dual::status_t solve_node_lp(mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + lp_solution_t& leaf_solution, + const csc_matrix_t& Arow, + const std::vector& bounds_changed, + logger_t& log); + + // Update the tree after solving the LP relaxation. + node_status_t update_tree(search_tree_t& search_tree, + mip_node_t* node_ptr, + const lp_problem_t& leaf_problem, + const lp_solution_t& leaf_solution, + dual::status_t lp_status, + char thread_type, + logger_t& log); // Sort the children based on the Martin's criteria. std::pair*, mip_node_t*> child_selection( diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 23ea214eb2..0eb9142acf 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -273,7 +273,7 @@ class search_tree_t { search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} - void update_tree(mip_node_t* node_ptr, node_status_t status) + void update(mip_node_t* node_ptr, node_status_t status) { mutex.lock(); std::vector*> stack; From 7997b12952b21a9b62c259db8eec5b5d4ec53188 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 9 Oct 2025 13:07:45 +0200 Subject: [PATCH 04/86] fixed value of the bounds_change for the child --- cpp/src/dual_simplex/branch_and_bound.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index f4122fa1bc..75c2aba29d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -868,11 +868,13 @@ void branch_and_bound_t::explore_subtree(i_t id, return; } + // Reset the bound_changed markers + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + // Recompute the bounds if (recompute) { leaf_problem.lower = original_lp_.lower; leaf_problem.upper = original_lp_.upper; - std::fill(bounds_changed.begin(), bounds_changed.end(), false); node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); } else { @@ -1023,11 +1025,13 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr if (toc(stats_.start_time) > settings_.time_limit) { return; } + // Reset the bound_changed markers + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + // Recompute the bounds if (recompute) { leaf_problem.lower = start_node->lp_lower; leaf_problem.upper = start_node->lp_upper; - std::fill(bounds_changed.begin(), bounds_changed.end(), false); node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); } else { node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); From 0cb0dcf73c8fb3eb185080088929f3729d722f1a Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 9 Oct 2025 14:03:18 +0200 Subject: [PATCH 05/86] changed dual simplex to keep the `basis_update_mpf_t` for future use --- cpp/src/dual_simplex/basis_solves.cpp | 8 +- cpp/src/dual_simplex/basis_updates.cpp | 2 +- cpp/src/dual_simplex/phase2.cpp | 61 +++++++++-- cpp/src/dual_simplex/phase2.hpp | 10 ++ cpp/src/dual_simplex/solve.cpp | 140 +++++++++++++++++++++---- cpp/src/dual_simplex/solve.hpp | 15 +++ 6 files changed, 202 insertions(+), 34 deletions(-) diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index 2120152c53..6aac8a9362 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -46,6 +46,13 @@ void get_basis_from_vstatus(i_t m, i_t n = vstatus.size(); i_t num_basic = 0; i_t num_non_basic = 0; + + assert(n >= m); + nonbasic_list.clear(); + superbasic_list.clear(); + nonbasic_list.reserve(n - m); + basis_list.resize(m); + for (i_t j = 0; j < n; ++j) { if (vstatus[j] == variable_status_t::BASIC) { basis_list[num_basic++] = j; @@ -61,7 +68,6 @@ void get_basis_from_vstatus(i_t m, superbasic_list.push_back(j); } } - i_t num_super_basic = superbasic_list.size(); assert(num_basic == m); } diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 0dba9910dc..f97413ff7f 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2100,7 +2100,7 @@ int basis_update_mpf_t::factorize_basis( slacks_needed) == -1) { return deficient.size(); } - settings.log.printf("Basis repaired\n"); + settings.log.debug("Basis repaired\n"); } assert(col_permutation_.size() == A.m); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index f83dfe4a5d..2efac12461 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -778,7 +778,7 @@ i_t steepest_edge_pricing_with_infeasibilities(const lp_problem_t& lp, const i_t j = infeasibility_indices[k]; const f_t squared_infeas = squared_infeasibilities[j]; const f_t val = squared_infeas / dy_steepest_edge[j]; - if (val > max_val || val == max_val && j > leaving_index) { + if (val > max_val || (val == max_val && j > leaving_index)) { max_val = val; leaving_index = j; const f_t lower_infeas = lp.lower[j] - x[j]; @@ -2169,6 +2169,33 @@ class phase2_timers_t { }; } // namespace phase2 +template +dual::status_t factorize_basis(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + f_t start_time) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + assert(m <= n); + assert(vstatus.size() == n); + + std::vector superbasic_list; + get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); + assert(superbasic_list.size() == 0); + assert(nonbasic_list.size() == n - m); + + if (ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + return dual::status_t::NUMERICAL; + } + + if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + + return dual::status_t::UNSET; +} template dual::status_t dual_phase2(i_t phase, @@ -2183,14 +2210,6 @@ dual::status_t dual_phase2(i_t phase, { const i_t m = lp.num_rows; const i_t n = lp.num_cols; - assert(m <= n); - assert(vstatus.size() == n); - assert(lp.A.m == m); - assert(lp.A.n == n); - assert(lp.objective.size() == n); - assert(lp.lower.size() == n); - assert(lp.upper.size() == n); - assert(lp.rhs.size() == m); std::vector basic_list(m); std::vector nonbasic_list; std::vector superbasic_list; @@ -2259,6 +2278,8 @@ dual::status_t dual_phase2_with_basis_update(i_t phase, std::vector vstatus_old = vstatus; std::vector z_old = z; + phase2::bound_info(lp, settings); + std::vector c_basic(m); for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; @@ -3008,6 +3029,28 @@ template dual::status_t dual_phase2( int& iter, std::vector& steepest_edge_norms); +template dual::status_t dual_phase2_with_basis_update( + int phase, + int slack_basis, + double start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + int& iter, + std::vector& steepest_edge_norms); + +template dual::status_t factorize_basis( + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + double start_time); #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/phase2.hpp b/cpp/src/dual_simplex/phase2.hpp index 0b50453113..bd1a803897 100644 --- a/cpp/src/dual_simplex/phase2.hpp +++ b/cpp/src/dual_simplex/phase2.hpp @@ -56,6 +56,15 @@ static std::string status_to_string(status_t status) } } // namespace dual +template +dual::status_t factorize_basis(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + f_t start_time); + template dual::status_t dual_phase2(i_t phase, i_t slack_basis, @@ -80,4 +89,5 @@ dual::status_t dual_phase2_with_basis_update(i_t phase, lp_solution_t& sol, i_t& iter, std::vector& delta_y_steepest_edge); + } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index dadb60cc6b..4da9644741 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -116,6 +116,35 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original lp_solution_t& original_solution, std::vector& vstatus, std::vector& edge_norms) +{ + const i_t m = original_lp.num_rows; + const i_t n = original_lp.num_cols; + assert(m <= n); + std::vector basic_list(m); + std::vector nonbasic_list; + basis_update_mpf_t ft(m, settings.refactor_frequency); + return solve_linear_program_with_basis_update(original_lp, + start_time, + settings, + original_solution, + ft, + basic_list, + nonbasic_list, + vstatus, + edge_norms); +} + +template +lp_status_t solve_linear_program_with_basis_update( + const lp_problem_t& original_lp, + const f_t start_time, + const simplex_solver_settings_t& settings, + lp_solution_t& original_solution, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + std::vector& edge_norms) { lp_status_t lp_status = lp_status_t::UNSET; lp_problem_t presolved_lp(original_lp.handle_ptr, 1, 1, 1); @@ -138,21 +167,21 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original column_scaling(presolved_lp, settings, lp, column_scales); assert(presolved_lp.num_cols == lp.num_cols); lp_problem_t phase1_problem(original_lp.handle_ptr, 1, 1, 1); - std::vector phase1_vstatus; + f_t phase1_obj = -inf; create_phase1_problem(lp, phase1_problem); assert(phase1_problem.num_cols == presolved_lp.num_cols); // Set the vstatus for the phase1 problem based on a slack basis - phase1_vstatus.resize(phase1_problem.num_cols); - std::fill(phase1_vstatus.begin(), phase1_vstatus.end(), variable_status_t::NONBASIC_LOWER); + vstatus.resize(phase1_problem.num_cols); + std::fill(vstatus.begin(), vstatus.end(), variable_status_t::NONBASIC_LOWER); i_t num_basic = 0; for (i_t j = phase1_problem.num_cols - 1; j >= 0; --j) { const i_t col_start = phase1_problem.A.col_start[j]; const i_t col_end = phase1_problem.A.col_start[j + 1]; const i_t nz = col_end - col_start; if (nz == 1 && std::abs(phase1_problem.A.x[col_start]) == 1.0) { - phase1_vstatus[j] = variable_status_t::BASIC; + vstatus[j] = variable_status_t::BASIC; num_basic++; } if (num_basic == phase1_problem.num_rows) { break; } @@ -160,9 +189,28 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original assert(num_basic == phase1_problem.num_rows); i_t iter = 0; lp_solution_t phase1_solution(phase1_problem.num_rows, phase1_problem.num_cols); - std::vector junk; - dual::status_t phase1_status = dual_phase2( - 1, 1, start_time, phase1_problem, settings, phase1_vstatus, phase1_solution, iter, junk); + + dual::status_t LU_status = + factorize_basis(lp, settings, vstatus, ft, basic_list, nonbasic_list, start_time); + if (LU_status == dual::status_t::NUMERICAL) { + settings.log.printf("Failed in factorizing the basis\n"); + return lp_status_t::NUMERICAL_ISSUES; + } + if (LU_status == dual::status_t::TIME_LIMIT) { return lp_status_t::TIME_LIMIT; } + + edge_norms.clear(); + dual::status_t phase1_status = dual_phase2_with_basis_update(1, + 1, + start_time, + phase1_problem, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + phase1_solution, + iter, + edge_norms); if (phase1_status == dual::status_t::NUMERICAL || phase1_status == dual::status_t::DUAL_UNBOUNDED) { settings.log.printf("Failed in Phase 1\n"); @@ -177,27 +225,62 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original lp_solution_t solution(lp.num_rows, lp.num_cols); assert(lp.num_cols == phase1_problem.num_cols); assert(solution.x.size() == lp.num_cols); - vstatus = phase1_vstatus; + edge_norms.clear(); - dual::status_t status = dual_phase2( - 2, iter == 0 ? 1 : 0, start_time, lp, settings, vstatus, solution, iter, edge_norms); + dual::status_t status = dual_phase2_with_basis_update(2, + iter == 0 ? 1 : 0, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + solution, + iter, + edge_norms); + if (status == dual::status_t::NUMERICAL) { // Became dual infeasible. Try phase 1 again - phase1_vstatus = vstatus; settings.log.printf("Running Phase 1 again\n"); - junk.clear(); - dual_phase2(1, - 0, - start_time, - phase1_problem, - settings, - phase1_vstatus, - phase1_solution, - iter, - edge_norms); - vstatus = phase1_vstatus; + + dual::status_t LU_status = + factorize_basis(lp, settings, vstatus, ft, basic_list, nonbasic_list, start_time); + + if (LU_status == dual::status_t::NUMERICAL) { + settings.log.printf("Failed in factorizing the basis\n"); + return lp_status_t::NUMERICAL_ISSUES; + } + + if (LU_status == dual::status_t::TIME_LIMIT) { return lp_status_t::TIME_LIMIT; } + edge_norms.clear(); - status = dual_phase2(2, 0, start_time, lp, settings, vstatus, solution, iter, edge_norms); + dual_phase2_with_basis_update(1, + 0, + start_time, + phase1_problem, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + phase1_solution, + iter, + edge_norms); + + edge_norms.clear(); + status = dual_phase2_with_basis_update(2, + 0, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + solution, + iter, + edge_norms); } constexpr bool primal_cleanup = false; if (status == dual::status_t::OPTIMAL && primal_cleanup) { @@ -602,6 +685,17 @@ template lp_status_t solve_linear_program_advanced( std::vector& vstatus, std::vector& edge_norms); +template lp_status_t solve_linear_program_with_basis_update( + const lp_problem_t& original_lp, + const double start_time, + const simplex_solver_settings_t& settings, + lp_solution_t& original_solution, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + std::vector& edge_norms); + template lp_status_t solve_linear_program_with_barrier( const user_problem_t& user_problem, const simplex_solver_settings_t& settings, diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index a54acf6975..65418f5f22 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -56,6 +57,20 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original std::vector& vstatus, std::vector& edge_norms); +// Solve the LP using dual simplex and keep the `basis_update_mpf_t` +// for future use. +template +lp_status_t solve_linear_program_with_basis_update( + const lp_problem_t& original_lp, + const f_t start_time, + const simplex_solver_settings_t& settings, + lp_solution_t& original_solution, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + std::vector& edge_norms); + template lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, From 6ec4806c4364b344090d22e4b69ebbbbc7af2950 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 13 Oct 2025 11:54:46 +0200 Subject: [PATCH 06/86] child nodes can reuse the basis factorization from the parent --- cpp/src/dual_simplex/branch_and_bound.cpp | 172 ++++++++++++++++++---- cpp/src/dual_simplex/branch_and_bound.hpp | 16 +- cpp/src/dual_simplex/mip_node.hpp | 2 +- 3 files changed, 154 insertions(+), 36 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 2ce3ee0b4e..62c0da7cbf 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -555,12 +555,15 @@ template node_status_t branch_and_bound_t::solve_node(search_tree_t& search_tree, mip_node_t* node_ptr, lp_problem_t& leaf_problem, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, const csc_matrix_t& Arow, - f_t upper_bound, - logger_t& log, - char thread_type) + char thread_type, + logger_t& log) { - f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; + const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; + const f_t upper_bound = get_upper_bound(); lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); std::vector& leaf_vstatus = node_ptr->vstatus; @@ -590,20 +593,31 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; - lp_status = dual_phase2(2, - 0, - lp_start_time, - leaf_problem, - lp_settings, - leaf_vstatus, - leaf_solution, - node_iter, - leaf_edge_norms); + lp_status = dual_phase2_with_basis_update(2, + 0, + lp_start_time, + leaf_problem, + lp_settings, + leaf_vstatus, + ft, + basic_list, + nonbasic_list, + leaf_solution, + node_iter, + leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); - lp_status_t second_status = solve_linear_program_advanced( - leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); + lp_status_t second_status = solve_linear_program_with_basis_update(leaf_problem, + lp_start_time, + lp_settings, + leaf_solution, + ft, + basic_list, + nonbasic_list, + leaf_vstatus, + leaf_edge_norms); + lp_status = convert_lp_status_to_dual_status(second_status); } @@ -615,7 +629,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& // Node was infeasible. Do not branch node_ptr->lower_bound = inf; search_tree.graphviz_node(log, node_ptr, "infeasible", 0.0); - search_tree.update_tree(node_ptr, node_status_t::INFEASIBLE); + search_tree.update(node_ptr, node_status_t::INFEASIBLE); return node_status_t::INFEASIBLE; } else if (lp_status == dual::status_t::CUTOFF) { @@ -623,7 +637,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& node_ptr->lower_bound = upper_bound; f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); search_tree.graphviz_node(log, node_ptr, "cut off", leaf_objective); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); return node_status_t::FATHOMED; } else if (lp_status == dual::status_t::OPTIMAL) { @@ -641,13 +655,15 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& // Found a integer feasible solution add_feasible_solution(leaf_objective, leaf_solution.x, node_ptr->depth, thread_type); search_tree.graphviz_node(log, node_ptr, "integer feasible", leaf_objective); - search_tree.update_tree(node_ptr, node_status_t::INTEGER_FEASIBLE); + search_tree.update(node_ptr, node_status_t::INTEGER_FEASIBLE); return node_status_t::INTEGER_FEASIBLE; } else if (leaf_objective <= upper_bound + abs_fathom_tol) { + logger_t pc_log = log; + pc_log.log = false; + // Choose fractional variable to branch on - const i_t branch_var = - pc_.variable_selection(leaf_fractional, leaf_solution.x, lp_settings.log); + const i_t branch_var = pc_.variable_selection(leaf_fractional, leaf_solution.x, pc_log); assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( @@ -657,12 +673,12 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& } else { search_tree.graphviz_node(log, node_ptr, "fathomed", leaf_objective); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); return node_status_t::FATHOMED; } } else if (lp_status == dual::status_t::TIME_LIMIT) { search_tree.graphviz_node(log, node_ptr, "timeout", 0.0); - search_tree.update_tree(node_ptr, node_status_t::TIME_LIMIT); + search_tree.update(node_ptr, node_status_t::TIME_LIMIT); return node_status_t::TIME_LIMIT; } else { @@ -678,11 +694,50 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& } search_tree.graphviz_node(log, node_ptr, "numerical", 0.0); - search_tree.update_tree(node_ptr, node_status_t::NUMERICAL); + search_tree.update(node_ptr, node_status_t::NUMERICAL); return node_status_t::NUMERICAL; } } +template +dual::status_t branch_and_bound_t::refactorize_basis(search_tree_t& search_tree, + mip_node_t* node, + lp_problem_t& leaf_problem, + basis_update_mpf_t& ft, + std::vector& basic, + std::vector& nonbasic, + char thread_type) +{ + std::vector& vstatus = node->vstatus; + auto status = factorize_basis(leaf_problem, settings_, vstatus, ft, basic, nonbasic, tic()); + + if (status == dual::status_t::TIME_LIMIT) { + search_tree.graphviz_node(settings_.log, node, "timeout", 0.0); + search_tree.update(node, node_status_t::TIME_LIMIT); + status_ = mip_exploration_status_t::TIME_LIMIT; + return status; + + } else if (status == dual::status_t::NUMERICAL) { + lower_bound_ceiling_.fetch_min(node->lower_bound); + + if (thread_type == 'B') { + settings_.log.printf( + "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " + "to " + "%+10.6e.\n", + status, + node->node_id, + compute_user_objective(original_lp_, lower_bound_ceiling_.load())); + } + + search_tree.graphviz_node(settings_.log, node, "numerical", 0.0); + search_tree.update(node, node_status_t::NUMERICAL); + return status; + } + + return dual::status_t::UNSET; +} + template void branch_and_bound_t::exploration_ramp_up(search_tree_t* search_tree, mip_node_t* node, @@ -707,7 +762,7 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree->graphviz_node(settings_.log, node, "cutoff", node->lower_bound); - search_tree->update_tree(node, node_status_t::FATHOMED); + search_tree->update(node, node_status_t::FATHOMED); return; } @@ -744,12 +799,28 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* return; } + const i_t m = leaf_problem.num_rows; + basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + std::vector basic_list(m); + std::vector nonbasic_list; + // Set the correct bounds for the leaf problem leaf_problem.lower = original_lp_.lower; leaf_problem.upper = original_lp_.upper; - node_status_t node_status = - solve_node(*search_tree, node, leaf_problem, Arow, upper_bound, settings_.log, 'B'); + auto status = refactorize_basis( + *search_tree, node, leaf_problem, basis_update, basic_list, nonbasic_list, 'B'); + if (status != dual::status_t::UNSET) { return; } + + node_status_t node_status = solve_node(*search_tree, + node, + leaf_problem, + basis_update, + basic_list, + nonbasic_list, + Arow, + 'B', + settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -784,6 +855,12 @@ void branch_and_bound_t::explore_subtree(i_t task_id, lp_problem_t& leaf_problem, const csc_matrix_t& Arow) { + const i_t m = leaf_problem.num_rows; + bool recompute = true; + basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + std::vector basic_list(m); + std::vector nonbasic_list; + std::deque*> stack; stack.push_front(start_node); @@ -812,7 +889,8 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); + recompute = true; continue; } @@ -850,8 +928,23 @@ void branch_and_bound_t::explore_subtree(i_t task_id, leaf_problem.lower = original_lp_.lower; leaf_problem.upper = original_lp_.upper; - node_status_t node_status = - solve_node(search_tree, node_ptr, leaf_problem, Arow, upper_bound, settings_.log, 'B'); + if (recompute) { + auto status = refactorize_basis( + search_tree, node_ptr, leaf_problem, basis_update, basic_list, nonbasic_list, 'B'); + if (status != dual::status_t::UNSET) { continue; } + } + + node_status_t node_status = solve_node(search_tree, + node_ptr, + leaf_problem, + basis_update, + basic_list, + nonbasic_list, + Arow, + 'B', + settings_.log); + + recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -922,7 +1015,7 @@ void branch_and_bound_t::best_first_thread(i_t id, // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); - search_tree.update_tree(node_ptr, node_status_t::FATHOMED); + search_tree.update(node_ptr, node_status_t::FATHOMED); active_subtrees_--; continue; } @@ -967,6 +1060,13 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr if (start_node.has_value()) { if (get_upper_bound() < start_node->node.lower_bound) { continue; } + bool recompute = true; + const i_t m = leaf_problem.num_rows; + + basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + std::vector basic_list(m); + std::vector nonbasic_list; + search_tree_t subtree(std::move(start_node->node)); std::deque*> stack; stack.push_front(&subtree.root); @@ -978,6 +1078,7 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node_ptr->lower_bound); if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { + recompute = true; continue; } @@ -987,8 +1088,14 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr leaf_problem.lower = start_node->lp_lower; leaf_problem.upper = start_node->lp_upper; - node_status_t node_status = - solve_node(subtree, node_ptr, leaf_problem, Arow, upper_bound, log, 'D'); + if (recompute) { + auto status = refactorize_basis( + subtree, node_ptr, leaf_problem, basis_update, basic_list, nonbasic_list, 'B'); + if (status != dual::status_t::UNSET) { continue; } + } + + node_status_t node_status = solve_node( + subtree, node_ptr, leaf_problem, basis_update, basic_list, nonbasic_list, Arow, 'D', log); if (node_status == node_status_t::TIME_LIMIT) { return; @@ -1052,6 +1159,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, stats_.start_time, lp_settings, root_relax_soln_, root_vstatus_, edge_norms_); stats_.total_lp_iters = root_relax_soln_.iterations; stats_.total_lp_solve_time = toc(stats_.start_time); + if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); // FIXME: rarely dual simplex detects infeasible whereas it is feasible. diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 23fb9eb7f8..8b338f5bd6 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -256,10 +256,20 @@ class branch_and_bound_t { node_status_t solve_node(search_tree_t& search_tree, mip_node_t* node_ptr, lp_problem_t& leaf_problem, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, const csc_matrix_t& Arow, - f_t upper_bound, - logger_t& log, - char thread_type); + char thread_type, + logger_t& log); + + dual::status_t refactorize_basis(search_tree_t& search_tree, + mip_node_t* node, + lp_problem_t& leaf_problem, + basis_update_mpf_t& ft, + std::vector& basic, + std::vector& nonbasic, + char thread_type); // Sort the children based on the Martin's criteria. std::pair*, mip_node_t*> child_selection( diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index a3a34eb81d..1402dc088d 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -270,7 +270,7 @@ class search_tree_t { search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} - void update_tree(mip_node_t* node_ptr, node_status_t status) + void update(mip_node_t* node_ptr, node_status_t status) { mutex.lock(); std::vector*> stack; From 95de283fe790de3789516cc5832be541a6502351 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 13 Oct 2025 13:07:34 +0200 Subject: [PATCH 07/86] revert refactor --- cpp/src/dual_simplex/branch_and_bound.cpp | 61 +++++++---------------- cpp/src/dual_simplex/branch_and_bound.hpp | 24 +++------ 2 files changed, 26 insertions(+), 59 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 7f70503b6f..edfd75fa87 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include #include @@ -553,19 +552,24 @@ branch_and_bound_t::child_selection(mip_node_t* node_ptr) } template -dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* node_ptr, - lp_problem_t& leaf_problem, - lp_solution_t& leaf_solution, - const csc_matrix_t& Arow, - const std::vector& bounds_changed, - logger_t& log) +node_status_t branch_and_bound_t::solve_node(search_tree_t& search_tree, + mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + const std::vector& bounds_changed, + const csc_matrix_t& Arow, + char thread_type, + logger_t& log) { + const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; + const f_t upper_bound = get_upper_bound(); + + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); std::vector& leaf_vstatus = node_ptr->vstatus; assert(leaf_vstatus.size() == leaf_problem.num_cols); simplex_solver_settings_t lp_settings = settings_; lp_settings.set_log(false); - lp_settings.cut_off = get_upper_bound() + settings_.dual_tol; + lp_settings.cut_off = upper_bound + settings_.dual_tol; lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); @@ -602,22 +606,6 @@ dual::status_t branch_and_bound_t::solve_node_lp(mip_node_t* stats_.total_lp_iters += node_iter; } - return lp_status; -} - -template -node_status_t branch_and_bound_t::update_tree( - search_tree_t& search_tree, - mip_node_t* node_ptr, - const lp_problem_t& leaf_problem, - const lp_solution_t& leaf_solution, - dual::status_t lp_status, - char thread_type, - logger_t& log) -{ - f_t upper_bound = get_upper_bound(); - f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; - if (lp_status == dual::status_t::DUAL_UNBOUNDED) { // Node was infeasible. Do not branch node_ptr->lower_bound = inf; @@ -658,9 +646,9 @@ node_status_t branch_and_bound_t::update_tree( // Choose fractional variable to branch on const i_t branch_var = pc_.variable_selection(leaf_fractional, leaf_solution.x, pc_log); - assert(node_ptr->vstatus.size() == leaf_problem.num_cols); + assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( - node_ptr, branch_var, leaf_solution.x[branch_var], node_ptr->vstatus, original_lp_, log); + node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, original_lp_, log); node_ptr->status = node_status_t::HAS_CHILDREN; return node_status_t::HAS_CHILDREN; @@ -753,21 +741,16 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* return; } - const i_t m = leaf_problem.num_rows; const i_t n = leaf_problem.num_cols; std::vector bounds_changed(n, false); - lp_solution_t leaf_solution(m, n); // Set the correct bounds for the leaf problem leaf_problem.lower = original_lp_.lower; leaf_problem.upper = original_lp_.upper; node->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); - dual::status_t lp_status = - solve_node_lp(node, leaf_problem, leaf_solution, Arow, bounds_changed, settings_.log); - node_status_t node_status = - update_tree(*search_tree, node, leaf_problem, leaf_solution, lp_status, 'B', settings_.log); + solve_node(*search_tree, node, leaf_problem, bounds_changed, Arow, 'B', settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -884,10 +867,8 @@ void branch_and_bound_t::explore_subtree(i_t task_id, node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); } - dual::status_t lp_status = - solve_node_lp(node_ptr, leaf_problem, leaf_solution, Arow, bounds_changed, settings_.log); - node_status_t node_status = update_tree( - search_tree, node_ptr, leaf_problem, leaf_solution, lp_status, 'B', settings_.log); + node_status_t node_status = + solve_node(search_tree, node_ptr, leaf_problem, bounds_changed, Arow, 'B', settings_.log); recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { @@ -993,7 +974,6 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr logger_t log; log.log = false; - const i_t m = leaf_problem.num_rows; const i_t n = leaf_problem.num_cols; while (status_ == mip_exploration_status_t::RUNNING && @@ -1009,7 +989,6 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr bool recompute = true; std::vector bounds_changed(n); - lp_solution_t leaf_solution(m, n); search_tree_t subtree(std::move(start_node->node)); std::deque*> stack; stack.push_front(&subtree.root); @@ -1039,12 +1018,8 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); } - dual::status_t lp_status = - solve_node_lp(node_ptr, leaf_problem, leaf_solution, Arow, bounds_changed, log); - node_status_t node_status = - update_tree(subtree, node_ptr, leaf_problem, leaf_solution, lp_status, 'D', log); - recompute = node_status != node_status_t::HAS_CHILDREN; + solve_node(subtree, node_ptr, leaf_problem, bounds_changed, Arow, 'D', log); if (node_status == node_status_t::TIME_LIMIT) { return; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index bea4001448..4ff7cfbde9 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -252,22 +252,14 @@ class branch_and_bound_t { // a deep dive into the subtree determined by the node. void diving_thread(lp_problem_t& leaf_problem, const csc_matrix_t& Arow); - // Solve the LP relaxation of a leaf node. - dual::status_t solve_node_lp(mip_node_t* node_ptr, - lp_problem_t& leaf_problem, - lp_solution_t& leaf_solution, - const csc_matrix_t& Arow, - const std::vector& bounds_changed, - logger_t& log); - - // Update the tree after solving the LP relaxation. - node_status_t update_tree(search_tree_t& search_tree, - mip_node_t* node_ptr, - const lp_problem_t& leaf_problem, - const lp_solution_t& leaf_solution, - dual::status_t lp_status, - char thread_type, - logger_t& log); + // Solve the LP relaxation of a leaf node and update the tree. + node_status_t solve_node(search_tree_t& search_tree, + mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + const std::vector& bounds_changed, + const csc_matrix_t& Arow, + char thread_type, + logger_t& log); // Sort the children based on the Martin's criteria. std::pair*, mip_node_t*> child_selection( From f44b134ad1e1bbbbe9e24122b266254919e5f6d4 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 13 Oct 2025 14:35:48 +0200 Subject: [PATCH 08/86] fix incorrect bounds when two nodes shares the same branch variable --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- cpp/src/dual_simplex/mip_node.hpp | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index edfd75fa87..0fd03a10a6 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -648,7 +648,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( - node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, original_lp_, log); + node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, leaf_problem, log); node_ptr->status = node_status_t::HAS_CHILDREN; return node_status_t::HAS_CHILDREN; diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 0eb9142acf..3f2a3e4325 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -90,21 +90,27 @@ class mip_node_t { update_variable_bound(lower, upper, bounds_changed); mip_node_t* parent_ptr = parent; - while (parent_ptr != nullptr) { - if (parent_ptr->node_id == 0) { break; } + while (parent_ptr != nullptr && parent_ptr->node_id != 0) { parent_ptr->update_variable_bound(lower, upper, bounds_changed); parent_ptr = parent_ptr->parent; } } + // Here we assume that we are traversing from the deepest node to the + // root of the tree void update_variable_bound(std::vector& lower, std::vector& upper, std::vector& bounds_changed) const { - // Apply the bounds at the current node assert(branch_var >= 0); assert(lower.size() > branch_var); assert(upper.size() > branch_var); + + // If the bounds have already been updated on another node, + // skip this node as it contains a less tight bounds. + if (bounds_changed[branch_var]) { return; } + + // Apply the bounds at the current node lower[branch_var] = branch_var_lower; upper[branch_var] = branch_var_upper; bounds_changed[branch_var] = true; @@ -206,9 +212,8 @@ class mip_node_t { } // This method creates a copy of the current node - // with its parent set to `nullptr`, `node_id = 0` - // and `depth = 0` such that it is the root - // of a separated tree. + // with its parent set to `nullptr` and `depth = 0`. + // This detaches the node from the tree. mip_node_t detach_copy() const { mip_node_t copy(lower_bound, vstatus); From 1cedfd6d8b9154030a29d0a086d0e4e58a93b3c8 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 13 Oct 2025 17:31:02 +0200 Subject: [PATCH 09/86] small refactor --- cpp/src/dual_simplex/phase2.cpp | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index b8becf3c85..37d4294b1c 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2209,23 +2209,13 @@ dual::status_t dual_phase2(i_t phase, std::vector& delta_y_steepest_edge) { const i_t m = lp.num_rows; - const i_t n = lp.num_cols; std::vector basic_list(m); std::vector nonbasic_list; - std::vector superbasic_list; - - phase2::bound_info(lp, settings); - get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); - assert(superbasic_list.size() == 0); - assert(nonbasic_list.size() == n - m); - basis_update_mpf_t ft(m, settings.refactor_frequency); - if (ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { - return dual::status_t::NUMERICAL; - } + auto status = factorize_basis(lp, settings, vstatus, ft, basic_list, nonbasic_list, start_time); + if (status != dual::status_t::UNSET) { return status; } - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } return dual_phase2_with_basis_update(phase, slack_basis, start_time, From 1ca6055fe6bff6b1f7d14fe2715499b02845566e Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 14 Oct 2025 12:08:48 +0200 Subject: [PATCH 10/86] fix incorrect thread type in diving --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 62c0da7cbf..ee674b44c9 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1090,7 +1090,7 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr if (recompute) { auto status = refactorize_basis( - subtree, node_ptr, leaf_problem, basis_update, basic_list, nonbasic_list, 'B'); + subtree, node_ptr, leaf_problem, basis_update, basic_list, nonbasic_list, 'D'); if (status != dual::status_t::UNSET) { continue; } } From bfeb6608a43bdbf9d5b9f6ba7b26cecc4e2e029a Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 14 Oct 2025 18:55:36 +0200 Subject: [PATCH 11/86] constraints_changed is only set for branched variables. refactor bound_strenghtening to persist some variables between calls. --- cpp/src/dual_simplex/CMakeLists.txt | 1 + cpp/src/dual_simplex/branch_and_bound.cpp | 100 ++++---- cpp/src/dual_simplex/branch_and_bound.hpp | 19 +- cpp/src/dual_simplex/node_presolve.cpp | 293 ++++++++++++++++++++++ cpp/src/dual_simplex/node_presolve.hpp | 49 ++++ cpp/src/dual_simplex/presolve.cpp | 274 +------------------- cpp/src/dual_simplex/presolve.hpp | 9 - 7 files changed, 415 insertions(+), 330 deletions(-) create mode 100644 cpp/src/dual_simplex/node_presolve.cpp create mode 100644 cpp/src/dual_simplex/node_presolve.hpp diff --git a/cpp/src/dual_simplex/CMakeLists.txt b/cpp/src/dual_simplex/CMakeLists.txt index c091ebac4f..c52121a631 100644 --- a/cpp/src/dual_simplex/CMakeLists.txt +++ b/cpp/src/dual_simplex/CMakeLists.txt @@ -27,6 +27,7 @@ set(DUAL_SIMPLEX_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/phase1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/phase2.cpp ${CMAKE_CURRENT_SOURCE_DIR}/presolve.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/node_presolve.cpp ${CMAKE_CURRENT_SOURCE_DIR}/primal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pseudo_costs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/right_looking_lu.cpp diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 0fd03a10a6..694aa3be03 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -555,8 +555,7 @@ template node_status_t branch_and_bound_t::solve_node(search_tree_t& search_tree, mip_node_t* node_ptr, lp_problem_t& leaf_problem, - const std::vector& bounds_changed, - const csc_matrix_t& Arow, + node_presolve_t& presolve, char thread_type, logger_t& log) { @@ -575,8 +574,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& // in B&B we only have equality constraints, leave it empty for default std::vector row_sense; - bool feasible = - bound_strengthening(row_sense, lp_settings, leaf_problem, Arow, var_types_, bounds_changed); + bool feasible = presolve.bound_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; @@ -680,6 +678,29 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& } } +template +void branch_and_bound_t::set_variable_bounds(mip_node_t* node, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed, + const std::vector& root_lower, + const std::vector& root_upper, + bool recompute) +{ + // Reset the bound_changed markers + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + + // Recompute the bounds + if (recompute) { + lower = root_lower; + upper = root_upper; + node->get_variable_bounds(lower, upper, bounds_changed); + + } else { + node->update_variable_bound(lower, upper, bounds_changed); + } +} + template void branch_and_bound_t::exploration_ramp_up(search_tree_t* search_tree, mip_node_t* node, @@ -741,16 +762,20 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* return; } - const i_t n = leaf_problem.num_cols; - std::vector bounds_changed(n, false); + std::vector row_sense; + node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); // Set the correct bounds for the leaf problem - leaf_problem.lower = original_lp_.lower; - leaf_problem.upper = original_lp_.upper; - node->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); + set_variable_bounds(node, + leaf_problem.lower, + leaf_problem.upper, + presolve.bounds_changed, + original_lp_.lower, + original_lp_.upper, + true); node_status_t node_status = - solve_node(*search_tree, node, leaf_problem, bounds_changed, Arow, 'B', settings_.log); + solve_node(*search_tree, node, leaf_problem, presolve, 'B', settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -785,11 +810,10 @@ void branch_and_bound_t::explore_subtree(i_t task_id, lp_problem_t& leaf_problem, const csc_matrix_t& Arow) { - const i_t m = leaf_problem.num_rows; - const i_t n = leaf_problem.num_cols; bool recompute = true; - std::vector bounds_changed(n); - lp_solution_t leaf_solution(m, n); + + std::vector row_sense; + node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); std::deque*> stack; stack.push_front(start_node); @@ -854,21 +878,17 @@ void branch_and_bound_t::explore_subtree(i_t task_id, return; } - // Reset the bound_changed markers - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - - // Recompute the bounds - if (recompute) { - leaf_problem.lower = original_lp_.lower; - leaf_problem.upper = original_lp_.upper; - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); - - } else { - node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); - } + // Set the correct bounds for the leaf problem + set_variable_bounds(node_ptr, + leaf_problem.lower, + leaf_problem.upper, + presolve.bounds_changed, + original_lp_.lower, + original_lp_.upper, + recompute); node_status_t node_status = - solve_node(search_tree, node_ptr, leaf_problem, bounds_changed, Arow, 'B', settings_.log); + solve_node(search_tree, node_ptr, leaf_problem, presolve, 'B', settings_.log); recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { @@ -974,7 +994,8 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr logger_t log; log.log = false; - const i_t n = leaf_problem.num_cols; + std::vector row_sense; + node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { @@ -988,7 +1009,6 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr if (get_upper_bound() < start_node->node.lower_bound) { continue; } bool recompute = true; - std::vector bounds_changed(n); search_tree_t subtree(std::move(start_node->node)); std::deque*> stack; stack.push_front(&subtree.root); @@ -1006,20 +1026,16 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr if (toc(stats_.start_time) > settings_.time_limit) { return; } - // Reset the bound_changed markers - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - - // Recompute the bounds - if (recompute) { - leaf_problem.lower = start_node->lp_lower; - leaf_problem.upper = start_node->lp_upper; - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); - } else { - node_ptr->update_variable_bound(leaf_problem.lower, leaf_problem.upper, bounds_changed); - } + // Set the correct bounds for the leaf problem + set_variable_bounds(node_ptr, + leaf_problem.lower, + leaf_problem.upper, + presolve.bounds_changed, + start_node->lower, + start_node->upper, + recompute); - node_status_t node_status = - solve_node(subtree, node_ptr, leaf_problem, bounds_changed, Arow, 'D', log); + node_status_t node_status = solve_node(subtree, node_ptr, leaf_problem, presolve, 'D', log); if (node_status == node_status_t::TIME_LIMIT) { return; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 4ff7cfbde9..4f9847d500 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -19,8 +19,8 @@ #include #include +#include #include -#include #include #include #include @@ -58,13 +58,13 @@ void upper_bound_callback(f_t upper_bound); template struct diving_root_t { mip_node_t node; - std::vector lp_lower; - std::vector lp_upper; + std::vector lower; + std::vector upper; diving_root_t(mip_node_t&& node, const std::vector& lower, const std::vector& upper) - : node(std::move(node)), lp_upper(upper), lp_lower(lower) + : node(std::move(node)), upper(upper), lower(lower) { } @@ -226,6 +226,14 @@ class branch_and_bound_t { // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); + void set_variable_bounds(mip_node_t* node, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed, + const std::vector& root_lower, + const std::vector& root_upper, + bool recompute); + // Ramp-up phase of the solver, where we greedily expand the tree until // there is enough unexplored nodes. This is done recursively using OpenMP tasks. void exploration_ramp_up(search_tree_t* search_tree, @@ -256,8 +264,7 @@ class branch_and_bound_t { node_status_t solve_node(search_tree_t& search_tree, mip_node_t* node_ptr, lp_problem_t& leaf_problem, - const std::vector& bounds_changed, - const csc_matrix_t& Arow, + node_presolve_t& presolve, char thread_type, logger_t& log); diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp new file mode 100644 index 0000000000..829ef5f0e1 --- /dev/null +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -0,0 +1,293 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +static inline f_t update_lb(f_t curr_lb, f_t coeff, f_t delta_min_act, f_t delta_max_act) +{ + auto comp_bnd = (coeff < 0.) ? delta_min_act / coeff : delta_max_act / coeff; + return std::max(curr_lb, comp_bnd); +} + +template +static inline f_t update_ub(f_t curr_ub, f_t coeff, f_t delta_min_act, f_t delta_max_act) +{ + auto comp_bnd = (coeff < 0.) ? delta_max_act / coeff : delta_min_act / coeff; + return std::min(curr_ub, comp_bnd); +} + +template +static inline bool check_infeasibility(f_t min_a, f_t max_a, f_t cnst_lb, f_t cnst_ub, f_t eps) +{ + return (min_a > cnst_ub + eps) || (max_a < cnst_lb - eps); +} + +#define DEBUG_BOUND_STRENGTHENING 0 + +template +void print_bounds_stats(const std::vector& lower, + const std::vector& upper, + const simplex_solver_settings_t& settings, + const std::string msg) +{ +#if DEBUG_BOUND_STRENGTHENING + f_t lb_norm = 0.0; + f_t ub_norm = 0.0; + + i_t sz = lower.size(); + for (i_t i = 0; i < sz; ++i) { + if (std::isfinite(lower[i])) { lb_norm += abs(lower[i]); } + if (std::isfinite(upper[i])) { ub_norm += abs(upper[i]); } + } + settings.log.printf("%s :: lb norm %e, ub norm %e\n", msg.c_str(), lb_norm, ub_norm); +#endif +} + +template +node_presolve_t::node_presolve_t(const lp_problem_t& problem, + const std::vector& row_sense, + const csc_matrix_t& Arow, + const std::vector& var_types) + : bounds_changed(problem.num_cols, false), + problem(problem), + Arow(Arow), + var_types(var_types), + delta_min_activity(problem.num_rows), + delta_max_activity(problem.num_rows), + constraint_lb(problem.num_rows), + constraint_ub(problem.num_rows) +{ + const bool is_row_sense_empty = row_sense.empty(); + if (is_row_sense_empty) { + std::copy(problem.rhs.begin(), problem.rhs.end(), constraint_lb.begin()); + std::copy(problem.rhs.begin(), problem.rhs.end(), constraint_ub.begin()); + } else { + // Set the constraint bounds + for (i_t i = 0; i < problem.num_rows; ++i) { + if (row_sense[i] == 'E') { + constraint_lb[i] = problem.rhs[i]; + constraint_ub[i] = problem.rhs[i]; + } else if (row_sense[i] == 'L') { + constraint_ub[i] = problem.rhs[i]; + constraint_lb[i] = -inf; + } else { + constraint_lb[i] = problem.rhs[i]; + constraint_ub[i] = inf; + } + } + } +} + +template +bool node_presolve_t::bound_strengthening( + std::vector& lower_bounds, + std::vector& upper_bounds, + const simplex_solver_settings_t& settings) +{ + const i_t m = problem.num_rows; + const i_t n = problem.num_cols; + + std::vector constraint_changed(m, false); + std::vector variable_changed(n, false); + std::vector constraint_changed_next(m, false); + + for (i_t i = 0; i < bounds_changed.size(); ++i) { + if (bounds_changed[i]) { + const i_t row_start = problem.A.col_start[i]; + const i_t row_end = problem.A.col_start[i + 1]; + for (i_t p = row_start; p < row_end; ++p) { + const i_t j = problem.A.i[p]; + constraint_changed[j] = true; + } + } + } + + std::vector lower = lower_bounds; + std::vector upper = upper_bounds; + print_bounds_stats(lower, upper, settings, "Initial bounds"); + + i_t iter = 0; + const i_t iter_limit = 10; + while (iter < iter_limit) { + for (i_t i = 0; i < m; ++i) { + if (!constraint_changed[i]) { continue; } + const i_t row_start = Arow.col_start[i]; + const i_t row_end = Arow.col_start[i + 1]; + + f_t min_a = 0.0; + f_t max_a = 0.0; + for (i_t p = row_start; p < row_end; ++p) { + const i_t j = Arow.i[p]; + const f_t a_ij = Arow.x[p]; + + variable_changed[j] = true; + if (a_ij > 0) { + min_a += a_ij * lower[j]; + max_a += a_ij * upper[j]; + } else if (a_ij < 0) { + min_a += a_ij * upper[j]; + max_a += a_ij * lower[j]; + } + if (upper[j] == inf && a_ij > 0) { max_a = inf; } + if (lower[j] == -inf && a_ij < 0) { max_a = inf; } + + if (lower[j] == -inf && a_ij > 0) { min_a = -inf; } + if (upper[j] == inf && a_ij < 0) { min_a = -inf; } + } + + f_t cnst_lb = constraint_lb[i]; + f_t cnst_ub = constraint_ub[i]; + bool is_infeasible = + check_infeasibility(min_a, max_a, cnst_lb, cnst_ub, settings.primal_tol); + if (is_infeasible) { + settings.log.printf( + "Iter:: %d, Infeasible constraint %d, cnst_lb %e, cnst_ub %e, min_a %e, max_a %e\n", + iter, + i, + cnst_lb, + cnst_ub, + min_a, + max_a); + return false; + } + + delta_min_activity[i] = cnst_ub - min_a; + delta_max_activity[i] = cnst_lb - max_a; + } + + i_t num_bounds_changed = 0; + + for (i_t k = 0; k < n; ++k) { + if (!variable_changed[k]) { continue; } + f_t old_lb = lower[k]; + f_t old_ub = upper[k]; + + f_t new_lb = old_lb; + f_t new_ub = old_ub; + + const i_t row_start = problem.A.col_start[k]; + const i_t row_end = problem.A.col_start[k + 1]; + for (i_t p = row_start; p < row_end; ++p) { + const i_t i = problem.A.i[p]; + + if (!constraint_changed[i]) { continue; } + const f_t a_ik = problem.A.x[p]; + + f_t delta_min_act = delta_min_activity[i]; + f_t delta_max_act = delta_max_activity[i]; + + delta_min_act += (a_ik < 0) ? a_ik * old_ub : a_ik * old_lb; + delta_max_act += (a_ik > 0) ? a_ik * old_ub : a_ik * old_lb; + + new_lb = std::max(new_lb, update_lb(old_lb, a_ik, delta_min_act, delta_max_act)); + new_ub = std::min(new_ub, update_ub(old_ub, a_ik, delta_min_act, delta_max_act)); + } + + // Integer rounding + if (!var_types.empty() && + (var_types[k] == variable_type_t::INTEGER || var_types[k] == variable_type_t::BINARY)) { + new_lb = std::ceil(new_lb - settings.integer_tol); + new_ub = std::floor(new_ub + settings.integer_tol); + } + + bool lb_updated = abs(new_lb - old_lb) > 1e3 * settings.primal_tol; + bool ub_updated = abs(new_ub - old_ub) > 1e3 * settings.primal_tol; + + new_lb = std::max(new_lb, lower_bounds[k]); + new_ub = std::min(new_ub, upper_bounds[k]); + + if (new_lb > new_ub + 1e-6) { + settings.log.printf( + "Iter:: %d, Infeasible variable after update %d, %e > %e\n", iter, k, new_lb, new_ub); + return false; + } + if (new_lb != old_lb || new_ub != old_ub) { + for (i_t p = row_start; p < row_end; ++p) { + const i_t i = problem.A.i[p]; + constraint_changed_next[i] = true; + } + } + + lower[k] = std::min(new_lb, new_ub); + upper[k] = std::max(new_lb, new_ub); + + bool bounds_changed = lb_updated || ub_updated; + if (bounds_changed) { num_bounds_changed++; } + } + + if (num_bounds_changed == 0) { break; } + + std::swap(constraint_changed, constraint_changed_next); + std::fill(constraint_changed_next.begin(), constraint_changed_next.end(), false); + std::fill(variable_changed.begin(), variable_changed.end(), false); + + iter++; + } + + // settings.log.printf("Total strengthened variables %d\n", total_strengthened_variables); + +#if DEBUG_BOUND_STRENGTHENING + f_t lb_change = 0.0; + f_t ub_change = 0.0; + int num_lb_changed = 0; + int num_ub_changed = 0; + + for (i_t i = 0; i < n; ++i) { + if (lower[i] > problem.lower[i] + settings.primal_tol || + (!std::isfinite(problem.lower[i]) && std::isfinite(lower[i]))) { + num_lb_changed++; + lb_change += + std::isfinite(problem.lower[i]) + ? (lower[i] - problem.lower[i]) / (1e-6 + std::max(abs(lower[i]), abs(problem.lower[i]))) + : 1.0; + } + if (upper[i] < problem.upper[i] - settings.primal_tol || + (!std::isfinite(problem.upper[i]) && std::isfinite(upper[i]))) { + num_ub_changed++; + ub_change += + std::isfinite(problem.upper[i]) + ? (problem.upper[i] - upper[i]) / (1e-6 + std::max(abs(problem.upper[i]), abs(upper[i]))) + : 1.0; + } + } + + if (num_lb_changed > 0 || num_ub_changed > 0) { + settings.log.printf( + "lb change %e, ub change %e, num lb changed %d, num ub changed %d, iter %d\n", + 100 * lb_change / std::max(1, num_lb_changed), + 100 * ub_change / std::max(1, num_ub_changed), + num_lb_changed, + num_ub_changed, + iter); + } + print_bounds_stats(lower, upper, settings, "Final bounds"); +#endif + + lower_bounds = lower; + upper_bounds = upper; + + return true; +} + +#ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE +template class node_presolve_t; +#endif + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/node_presolve.hpp b/cpp/src/dual_simplex/node_presolve.hpp new file mode 100644 index 0000000000..54a3c6c341 --- /dev/null +++ b/cpp/src/dual_simplex/node_presolve.hpp @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +class node_presolve_t { + public: + // For pure LP bounds strengthening, var_types should be defaulted (i.e. left empty) + node_presolve_t(const lp_problem_t& problem, + const std::vector& row_sense, + const csc_matrix_t& Arow, + const std::vector& var_types); + + bool bound_strengthening(std::vector& lower_bounds, + std::vector& upper_bounds, + const simplex_solver_settings_t& settings); + + std::vector bounds_changed; + + private: + const lp_problem_t& problem; + const csc_matrix_t& Arow; + const std::vector& var_types; + + std::vector delta_min_activity; + std::vector delta_max_activity; + std::vector constraint_lb; + std::vector constraint_ub; +}; +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 8d80337c74..a84d64b369 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -28,272 +28,6 @@ namespace cuopt::linear_programming::dual_simplex { -template -static inline f_t update_lb(f_t curr_lb, f_t coeff, f_t delta_min_act, f_t delta_max_act) -{ - auto comp_bnd = (coeff < 0.) ? delta_min_act / coeff : delta_max_act / coeff; - return std::max(curr_lb, comp_bnd); -} - -template -static inline f_t update_ub(f_t curr_ub, f_t coeff, f_t delta_min_act, f_t delta_max_act) -{ - auto comp_bnd = (coeff < 0.) ? delta_max_act / coeff : delta_min_act / coeff; - return std::min(curr_ub, comp_bnd); -} - -template -static inline bool check_infeasibility(f_t min_a, f_t max_a, f_t cnst_lb, f_t cnst_ub, f_t eps) -{ - return (min_a > cnst_ub + eps) || (max_a < cnst_lb - eps); -} - -#define DEBUG_BOUND_STRENGTHENING 0 - -template -void print_bounds_stats(const std::vector& lower, - const std::vector& upper, - const simplex_solver_settings_t& settings, - const std::string msg) -{ -#if DEBUG_BOUND_STRENGTHENING - f_t lb_norm = 0.0; - f_t ub_norm = 0.0; - - i_t sz = lower.size(); - for (i_t i = 0; i < sz; ++i) { - if (std::isfinite(lower[i])) { lb_norm += abs(lower[i]); } - if (std::isfinite(upper[i])) { ub_norm += abs(upper[i]); } - } - settings.log.printf("%s :: lb norm %e, ub norm %e\n", msg.c_str(), lb_norm, ub_norm); -#endif -} - -template -bool bound_strengthening(const std::vector& row_sense, - const simplex_solver_settings_t& settings, - lp_problem_t& problem, - const csc_matrix_t& Arow, - const std::vector& var_types, - const std::vector& bounds_changed) -{ - const i_t m = problem.num_rows; - const i_t n = problem.num_cols; - - std::vector delta_min_activity(m); - std::vector delta_max_activity(m); - std::vector constraint_lb(m); - std::vector constraint_ub(m); - - // FIXME:: Instead of initializing constraint_changed to true, we can only look - // at the constraints corresponding to branched variable in branch and bound - // This is because, the parent LP already checked for feasibility of the constraints - // without the branched variable bounds - std::vector constraint_changed(m, true); - std::vector variable_changed(n, false); - std::vector constraint_changed_next(m, false); - - if (false && !bounds_changed.empty()) { - std::fill(constraint_changed.begin(), constraint_changed.end(), false); - for (i_t i = 0; i < n; ++i) { - if (bounds_changed[i]) { - const i_t row_start = problem.A.col_start[i]; - const i_t row_end = problem.A.col_start[i + 1]; - for (i_t p = row_start; p < row_end; ++p) { - const i_t j = problem.A.i[p]; - constraint_changed[j] = true; - } - } - } - } - - const bool is_row_sense_empty = row_sense.empty(); - if (is_row_sense_empty) { - std::copy(problem.rhs.begin(), problem.rhs.end(), constraint_lb.begin()); - std::copy(problem.rhs.begin(), problem.rhs.end(), constraint_ub.begin()); - } else { - // Set the constraint bounds - for (i_t i = 0; i < m; ++i) { - if (row_sense[i] == 'E') { - constraint_lb[i] = problem.rhs[i]; - constraint_ub[i] = problem.rhs[i]; - } else if (row_sense[i] == 'L') { - constraint_ub[i] = problem.rhs[i]; - constraint_lb[i] = -inf; - } else { - constraint_lb[i] = problem.rhs[i]; - constraint_ub[i] = inf; - } - } - } - - std::vector lower = problem.lower; - std::vector upper = problem.upper; - print_bounds_stats(lower, upper, settings, "Initial bounds"); - - i_t iter = 0; - const i_t iter_limit = 10; - while (iter < iter_limit) { - for (i_t i = 0; i < m; ++i) { - if (!constraint_changed[i]) { continue; } - const i_t row_start = Arow.col_start[i]; - const i_t row_end = Arow.col_start[i + 1]; - - f_t min_a = 0.0; - f_t max_a = 0.0; - for (i_t p = row_start; p < row_end; ++p) { - const i_t j = Arow.i[p]; - const f_t a_ij = Arow.x[p]; - - variable_changed[j] = true; - if (a_ij > 0) { - min_a += a_ij * lower[j]; - max_a += a_ij * upper[j]; - } else if (a_ij < 0) { - min_a += a_ij * upper[j]; - max_a += a_ij * lower[j]; - } - if (upper[j] == inf && a_ij > 0) { max_a = inf; } - if (lower[j] == -inf && a_ij < 0) { max_a = inf; } - - if (lower[j] == -inf && a_ij > 0) { min_a = -inf; } - if (upper[j] == inf && a_ij < 0) { min_a = -inf; } - } - - f_t cnst_lb = constraint_lb[i]; - f_t cnst_ub = constraint_ub[i]; - bool is_infeasible = - check_infeasibility(min_a, max_a, cnst_lb, cnst_ub, settings.primal_tol); - if (is_infeasible) { - settings.log.printf( - "Iter:: %d, Infeasible constraint %d, cnst_lb %e, cnst_ub %e, min_a %e, max_a %e\n", - iter, - i, - cnst_lb, - cnst_ub, - min_a, - max_a); - return false; - } - - delta_min_activity[i] = cnst_ub - min_a; - delta_max_activity[i] = cnst_lb - max_a; - } - - i_t num_bounds_changed = 0; - - for (i_t k = 0; k < n; ++k) { - if (!variable_changed[k]) { continue; } - f_t old_lb = lower[k]; - f_t old_ub = upper[k]; - - f_t new_lb = old_lb; - f_t new_ub = old_ub; - - const i_t row_start = problem.A.col_start[k]; - const i_t row_end = problem.A.col_start[k + 1]; - for (i_t p = row_start; p < row_end; ++p) { - const i_t i = problem.A.i[p]; - - if (!constraint_changed[i]) { continue; } - const f_t a_ik = problem.A.x[p]; - - f_t delta_min_act = delta_min_activity[i]; - f_t delta_max_act = delta_max_activity[i]; - - delta_min_act += (a_ik < 0) ? a_ik * old_ub : a_ik * old_lb; - delta_max_act += (a_ik > 0) ? a_ik * old_ub : a_ik * old_lb; - - new_lb = std::max(new_lb, update_lb(old_lb, a_ik, delta_min_act, delta_max_act)); - new_ub = std::min(new_ub, update_ub(old_ub, a_ik, delta_min_act, delta_max_act)); - } - - // Integer rounding - if (!var_types.empty() && - (var_types[k] == variable_type_t::INTEGER || var_types[k] == variable_type_t::BINARY)) { - new_lb = std::ceil(new_lb - settings.integer_tol); - new_ub = std::floor(new_ub + settings.integer_tol); - } - - bool lb_updated = abs(new_lb - old_lb) > 1e3 * settings.primal_tol; - bool ub_updated = abs(new_ub - old_ub) > 1e3 * settings.primal_tol; - - new_lb = std::max(new_lb, problem.lower[k]); - new_ub = std::min(new_ub, problem.upper[k]); - - if (new_lb > new_ub + 1e-6) { - settings.log.printf( - "Iter:: %d, Infeasible variable after update %d, %e > %e\n", iter, k, new_lb, new_ub); - return false; - } - if (new_lb != old_lb || new_ub != old_ub) { - for (i_t p = row_start; p < row_end; ++p) { - const i_t i = problem.A.i[p]; - constraint_changed_next[i] = true; - } - } - - lower[k] = std::min(new_lb, new_ub); - upper[k] = std::max(new_lb, new_ub); - - bool bounds_changed = lb_updated || ub_updated; - if (bounds_changed) { num_bounds_changed++; } - } - - if (num_bounds_changed == 0) { break; } - - std::swap(constraint_changed, constraint_changed_next); - std::fill(constraint_changed_next.begin(), constraint_changed_next.end(), false); - std::fill(variable_changed.begin(), variable_changed.end(), false); - - iter++; - } - - // settings.log.printf("Total strengthened variables %d\n", total_strengthened_variables); - -#if DEBUG_BOUND_STRENGTHENING - f_t lb_change = 0.0; - f_t ub_change = 0.0; - int num_lb_changed = 0; - int num_ub_changed = 0; - - for (i_t i = 0; i < n; ++i) { - if (lower[i] > problem.lower[i] + settings.primal_tol || - (!std::isfinite(problem.lower[i]) && std::isfinite(lower[i]))) { - num_lb_changed++; - lb_change += - std::isfinite(problem.lower[i]) - ? (lower[i] - problem.lower[i]) / (1e-6 + std::max(abs(lower[i]), abs(problem.lower[i]))) - : 1.0; - } - if (upper[i] < problem.upper[i] - settings.primal_tol || - (!std::isfinite(problem.upper[i]) && std::isfinite(upper[i]))) { - num_ub_changed++; - ub_change += - std::isfinite(problem.upper[i]) - ? (problem.upper[i] - upper[i]) / (1e-6 + std::max(abs(problem.upper[i]), abs(upper[i]))) - : 1.0; - } - } - - if (num_lb_changed > 0 || num_ub_changed > 0) { - settings.log.printf( - "lb change %e, ub change %e, num lb changed %d, num ub changed %d, iter %d\n", - 100 * lb_change / std::max(1, num_lb_changed), - 100 * ub_change / std::max(1, num_ub_changed), - num_lb_changed, - num_ub_changed, - iter); - } - print_bounds_stats(lower, upper, settings, "Final bounds"); -#endif - - problem.lower = lower; - problem.upper = upper; - - return true; -} - template i_t remove_empty_cols(lp_problem_t& problem, i_t& num_empty_cols, @@ -851,6 +585,7 @@ void convert_user_problem(const user_problem_t& user_problem, problem.A.transpose(Arow); bound_strengthening(row_sense, settings, problem, Arow); } + settings.log.debug( "equality rows %d less rows %d columns %d\n", equal_rows, less_rows, problem.num_cols); if (settings.barrier && settings.dualize != 0 && @@ -1608,13 +1343,6 @@ template void uncrush_solution(const presolve_info_t& std::vector& uncrushed_y, std::vector& uncrushed_z); -template bool bound_strengthening( - const std::vector& row_sense, - const simplex_solver_settings_t& settings, - lp_problem_t& problem, - const csc_matrix_t& Arow, - const std::vector& var_types, - const std::vector& bounds_changed); #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index bf0aab8997..538ca5dffe 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -190,13 +190,4 @@ void uncrush_solution(const presolve_info_t& presolve_info, std::vector& uncrushed_y, std::vector& uncrushed_z); -// For pure LP bounds strengthening, var_types should be defaulted (i.e. left empty) -template -bool bound_strengthening(const std::vector& row_sense, - const simplex_solver_settings_t& settings, - lp_problem_t& problem, - const csc_matrix_t& Arow, - const std::vector& var_types = {}, - const std::vector& bounds_changed = {}); - } // namespace cuopt::linear_programming::dual_simplex From b5ed956903b414312ffba9be179d4c88302b8bda Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 22 Oct 2025 11:27:17 +0200 Subject: [PATCH 12/86] small refactor --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 694aa3be03..58e124b7ea 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -572,8 +572,6 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); - // in B&B we only have equality constraints, leave it empty for default - std::vector row_sense; bool feasible = presolve.bound_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; From abeb2cf06d59149e4b78c3a3bea3f36d25950294 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 22 Oct 2025 13:11:38 +0200 Subject: [PATCH 13/86] fix log spacing --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 58e124b7ea..44a381b76c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -414,7 +414,7 @@ void branch_and_bound_t::repair_heuristic_solutions() std::string user_gap = user_mip_gap(obj, lower); settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + "H %+13.6e %+10.6e %s %9.2f\n", obj, lower, user_gap.c_str(), From 3237913698ab4887231876dd24e9526de1c45a34 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 22 Oct 2025 13:15:27 +0200 Subject: [PATCH 14/86] fix log spacing for repaired solutions --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 44a381b76c..1998e39bad 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -414,7 +414,7 @@ void branch_and_bound_t::repair_heuristic_solutions() std::string user_gap = user_mip_gap(obj, lower); settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + "H %+13.6e %+10.6e %s %9.2f\n", obj, lower, user_gap.c_str(), From 5768f50a681f85f803ce90a647eeb9b584094b7c Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 22 Oct 2025 14:32:01 +0200 Subject: [PATCH 15/86] fix incorrect matrix dimension in LU after LP presolve --- cpp/src/dual_simplex/basis_updates.cpp | 1 + cpp/src/dual_simplex/basis_updates.hpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 5efc86b580..e9cbfd3454 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2059,6 +2059,7 @@ int basis_update_mpf_t::factorize_basis( std::vector deficient; std::vector slacks_needed; + if (L0_.m != A.m) { resize(A.m); } if (dual_simplex::factorize_basis(A, settings, basic_list, diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index 3198ccd4b8..11f044d03f 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -291,6 +291,23 @@ class basis_update_mpf_t { return 0; } + void resize(i_t n) + { + L0_.resize(n, n, 1); + U0_.resize(n, n, 1); + row_permutation_.resize(n); + inverse_row_permutation_.resize(n); + S_.resize(n, 0, 0); + col_permutation_.resize(n); + inverse_col_permutation_.resize(n); + xi_workspace_.resize(2 * n, 0); + x_workspace_.resize(n, 0.0); + U0_transpose_.resize(1, 1, 1); + L0_transpose_.resize(1, 1, 1); + clear(); + reset_stats(); + } + f_t estimate_solution_density(f_t rhs_nz, f_t sum, i_t& num_calls, bool& use_hypersparse) const { num_calls++; From f9853b2ee72747a21ed388198373e437691bbf47 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 22 Oct 2025 18:16:32 +0200 Subject: [PATCH 16/86] initialize the presolver only once per thread --- cpp/src/dual_simplex/branch_and_bound.cpp | 21 ++++++++------------- cpp/src/dual_simplex/branch_and_bound.hpp | 6 +++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 1998e39bad..56592ab734 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -806,13 +806,9 @@ void branch_and_bound_t::explore_subtree(i_t task_id, search_tree_t& search_tree, mip_node_t* start_node, lp_problem_t& leaf_problem, - const csc_matrix_t& Arow) + node_presolve_t& presolve) { bool recompute = true; - - std::vector row_sense; - node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); - std::deque*> stack; stack.push_front(start_node); @@ -932,7 +928,7 @@ template void branch_and_bound_t::best_first_thread(i_t id, search_tree_t& search_tree, lp_problem_t& leaf_problem, - const csc_matrix_t& Arow) + node_presolve_t& presolve) { f_t lower_bound = -inf; f_t upper_bound = inf; @@ -964,7 +960,7 @@ void branch_and_bound_t::best_first_thread(i_t id, } // Best-first search with plunging - explore_subtree(id, search_tree, node_ptr, leaf_problem, Arow); + explore_subtree(id, search_tree, node_ptr, leaf_problem, presolve); active_subtrees_--; } @@ -987,14 +983,11 @@ void branch_and_bound_t::best_first_thread(i_t id, template void branch_and_bound_t::diving_thread(lp_problem_t& leaf_problem, - const csc_matrix_t& Arow) + node_presolve_t& presolve) { logger_t log; log.log = false; - std::vector row_sense; - node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); - while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { std::optional> start_node; @@ -1225,6 +1218,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut { // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; + std::vector row_sense; + node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); #pragma omp master { @@ -1247,12 +1242,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut (active_subtrees_ > 0 || get_heap_size() > 0)) { for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree, leaf_problem, Arow); + best_first_thread(i, search_tree, leaf_problem, presolve); } for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(leaf_problem, Arow); + diving_thread(leaf_problem, presolve); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 4f9847d500..79c8c6650b 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -247,18 +247,18 @@ class branch_and_bound_t { search_tree_t& search_tree, mip_node_t* start_node, lp_problem_t& leaf_problem, - const csc_matrix_t& Arow); + node_presolve_t& presolve); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. void best_first_thread(i_t id, search_tree_t& search_tree, lp_problem_t& leaf_problem, - const csc_matrix_t& Arow); + node_presolve_t& presolve); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(lp_problem_t& leaf_problem, const csc_matrix_t& Arow); + void diving_thread(lp_problem_t& leaf_problem, node_presolve_t& presolve); // Solve the LP relaxation of a leaf node and update the tree. node_status_t solve_node(search_tree_t& search_tree, From 0ed38a907cb247d6879637342f56eb413a5b9c7e Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 22 Oct 2025 13:51:20 -0700 Subject: [PATCH 17/86] Refactor, simplify interface, and reduce number of code changes --- cpp/src/dual_simplex/basis_updates.cpp | 42 ++++++++-------- cpp/src/dual_simplex/basis_updates.hpp | 10 ++-- cpp/src/dual_simplex/phase2.cpp | 63 +++++++++++++++++++----- cpp/src/dual_simplex/phase2.hpp | 1 + cpp/src/dual_simplex/solve.cpp | 68 +++++++------------------- 5 files changed, 98 insertions(+), 86 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index e9cbfd3454..b34ebcde8e 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2049,7 +2049,7 @@ void basis_update_mpf_t::multiply_lu(csc_matrix_t& out) cons } template -int basis_update_mpf_t::factorize_basis( +int basis_update_mpf_t::refactor_basis( const csc_matrix_t& A, const simplex_solver_settings_t& settings, std::vector& basic_list, @@ -2060,16 +2060,16 @@ int basis_update_mpf_t::factorize_basis( std::vector slacks_needed; if (L0_.m != A.m) { resize(A.m); } - if (dual_simplex::factorize_basis(A, - settings, - basic_list, - L0_, - U0_, - row_permutation_, - inverse_row_permutation_, - col_permutation_, - deficient, - slacks_needed) == -1) { + if (factorize_basis(A, + settings, + basic_list, + L0_, + U0_, + row_permutation_, + inverse_row_permutation_, + col_permutation_, + deficient, + slacks_needed) == -1) { settings.log.debug("Initial factorization failed\n"); basis_repair(A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); @@ -2089,16 +2089,16 @@ int basis_update_mpf_t::factorize_basis( } #endif - if (dual_simplex::factorize_basis(A, - settings, - basic_list, - L0_, - U0_, - row_permutation_, - inverse_row_permutation_, - col_permutation_, - deficient, - slacks_needed) == -1) { + if (factorize_basis(A, + settings, + basic_list, + L0_, + U0_, + row_permutation_, + inverse_row_permutation_, + col_permutation_, + deficient, + slacks_needed) == -1) { #ifdef CHECK_L_FACTOR if (L.check_matrix() == -1) { settings.log.printf("Bad L after basis repair\n"); } #endif diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index 11f044d03f..11e31732db 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -388,11 +388,11 @@ class basis_update_mpf_t { void multiply_lu(csc_matrix_t& out) const; // Compute L*U = A(p, basic_list) - int factorize_basis(const csc_matrix_t& A, - const simplex_solver_settings_t& settings, - std::vector& basic_list, - std::vector& nonbasic_list, - std::vector& vstatus); + int refactor_basis(const csc_matrix_t& A, + const simplex_solver_settings_t& settings, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus); private: void clear() diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 37d4294b1c..74caa44dd2 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2243,6 +2243,43 @@ dual::status_t dual_phase2_with_basis_update(i_t phase, lp_solution_t& sol, i_t& iter, std::vector& delta_y_steepest_edge) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + std::vector basic_list(m); + std::vector nonbasic_list; + std::vector superbasic_list; + basis_update_mpf_t ft(m, settings.refactor_frequency); + const bool initialize_basis = true; + return dual_phase2_with_basis_update(phase, + slack_basis, + initialize_basis, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + delta_y_steepest_edge); +} + +template +dual::status_t dual_phase2_with_basis_update(i_t phase, + i_t slack_basis, + bool initialize_basis, + f_t start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + i_t& iter, + std::vector& delta_y_steepest_edge) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -2269,6 +2306,18 @@ dual::status_t dual_phase2_with_basis_update(i_t phase, std::vector z_old = z; phase2::bound_info(lp, settings); + if (initialize_basis) { + std::vector superbasic_list; + get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); + assert(superbasic_list.size() == 0); + assert(nonbasic_list.size() == n - m); + + if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + return dual::status_t::NUMERICAL; + } + + if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + } std::vector c_basic(m); for (i_t k = 0; k < m; ++k) { @@ -2905,14 +2954,14 @@ dual::status_t dual_phase2_with_basis_update(i_t phase, #endif if (should_refactor) { bool should_recompute_x = false; - if (ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } i_t count = 0; i_t deficient_size; while ((deficient_size = - ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus)) > 0) { + ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus)) > 0) { settings.log.printf("Failed to repair basis. Iteration %d. %d deficient columns.\n", iter, static_cast(deficient_size)); @@ -3023,6 +3072,7 @@ template dual::status_t dual_phase2( template dual::status_t dual_phase2_with_basis_update( int phase, int slack_basis, + bool initialize_basis, double start_time, const lp_problem_t& lp, const simplex_solver_settings_t& settings, @@ -3033,15 +3083,6 @@ template dual::status_t dual_phase2_with_basis_update( lp_solution_t& sol, int& iter, std::vector& steepest_edge_norms); - -template dual::status_t factorize_basis( - const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - std::vector& vstatus, - basis_update_mpf_t& ft, - std::vector& basic_list, - std::vector& nonbasic_list, - double start_time); #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/phase2.hpp b/cpp/src/dual_simplex/phase2.hpp index bd1a803897..4719c9d5b4 100644 --- a/cpp/src/dual_simplex/phase2.hpp +++ b/cpp/src/dual_simplex/phase2.hpp @@ -79,6 +79,7 @@ dual::status_t dual_phase2(i_t phase, template dual::status_t dual_phase2_with_basis_update(i_t phase, i_t slack_basis, + bool initialize_basis, f_t start_time, const lp_problem_t& lp, const simplex_solver_settings_t& settings, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 4da9644741..1b3e2d258b 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -227,60 +227,30 @@ lp_status_t solve_linear_program_with_basis_update( assert(solution.x.size() == lp.num_cols); edge_norms.clear(); - dual::status_t status = dual_phase2_with_basis_update(2, - iter == 0 ? 1 : 0, - start_time, - lp, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - solution, - iter, - edge_norms); - + bool initialize_basis_update = true; + dual::status_t status = dual_phase2_with_basis_update( + 2, iter == 0 ? 1 : 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); if (status == dual::status_t::NUMERICAL) { // Became dual infeasible. Try phase 1 again settings.log.printf("Running Phase 1 again\n"); - - dual::status_t LU_status = - factorize_basis(lp, settings, vstatus, ft, basic_list, nonbasic_list, start_time); - - if (LU_status == dual::status_t::NUMERICAL) { - settings.log.printf("Failed in factorizing the basis\n"); - return lp_status_t::NUMERICAL_ISSUES; - } - - if (LU_status == dual::status_t::TIME_LIMIT) { return lp_status_t::TIME_LIMIT; } - - edge_norms.clear(); + junk.clear(); + initialize_basis_update = false; dual_phase2_with_basis_update(1, - 0, - start_time, - phase1_problem, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - phase1_solution, - iter, - edge_norms); - + 0, + initialize_basis_update, + start_time, + phase1_problem, + settings, + phase1_vstatus, + ft, + basic_list, + nonbasic_list, + phase1_solution, + iter, + edge_norms); + vstatus = phase1_vstatus; edge_norms.clear(); - status = dual_phase2_with_basis_update(2, - 0, - start_time, - lp, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - solution, - iter, - edge_norms); + status = dual_phase2_with_basis_update(2, 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); } constexpr bool primal_cleanup = false; if (status == dual::status_t::OPTIMAL && primal_cleanup) { From 166c44653ef4cc7943a82660c01be476403f9560 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 22 Oct 2025 16:18:16 -0700 Subject: [PATCH 18/86] Further removal; missing when cherry-picking previous commit --- cpp/src/dual_simplex/basis_solves.cpp | 8 +--- cpp/src/dual_simplex/phase2.cpp | 63 --------------------------- cpp/src/dual_simplex/phase2.hpp | 9 ---- cpp/src/dual_simplex/solve.cpp | 36 ++++----------- 4 files changed, 10 insertions(+), 106 deletions(-) diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index 91a2566b9b..c6f0ec4868 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -46,13 +46,6 @@ void get_basis_from_vstatus(i_t m, i_t n = vstatus.size(); i_t num_basic = 0; i_t num_non_basic = 0; - - assert(n >= m); - nonbasic_list.clear(); - superbasic_list.clear(); - nonbasic_list.reserve(n - m); - basis_list.resize(m); - for (i_t j = 0; j < n; ++j) { if (vstatus[j] == variable_status_t::BASIC) { basis_list[num_basic++] = j; @@ -68,6 +61,7 @@ void get_basis_from_vstatus(i_t m, superbasic_list.push_back(j); } } + i_t num_super_basic = superbasic_list.size(); assert(num_basic == m); } diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 74caa44dd2..3509fa1bee 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2169,33 +2169,6 @@ class phase2_timers_t { }; } // namespace phase2 -template -dual::status_t factorize_basis(const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - std::vector& vstatus, - basis_update_mpf_t& ft, - std::vector& basic_list, - std::vector& nonbasic_list, - f_t start_time) -{ - const i_t m = lp.num_rows; - const i_t n = lp.num_cols; - assert(m <= n); - assert(vstatus.size() == n); - - std::vector superbasic_list; - get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); - assert(superbasic_list.size() == 0); - assert(nonbasic_list.size() == n - m); - - if (ft.factorize_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { - return dual::status_t::NUMERICAL; - } - - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } - - return dual::status_t::UNSET; -} template dual::status_t dual_phase2(i_t phase, @@ -2207,42 +2180,6 @@ dual::status_t dual_phase2(i_t phase, lp_solution_t& sol, i_t& iter, std::vector& delta_y_steepest_edge) -{ - const i_t m = lp.num_rows; - std::vector basic_list(m); - std::vector nonbasic_list; - basis_update_mpf_t ft(m, settings.refactor_frequency); - - auto status = factorize_basis(lp, settings, vstatus, ft, basic_list, nonbasic_list, start_time); - if (status != dual::status_t::UNSET) { return status; } - - return dual_phase2_with_basis_update(phase, - slack_basis, - start_time, - lp, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - sol, - iter, - delta_y_steepest_edge); -} - -template -dual::status_t dual_phase2_with_basis_update(i_t phase, - i_t slack_basis, - f_t start_time, - const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - std::vector& vstatus, - basis_update_mpf_t& ft, - std::vector& basic_list, - std::vector& nonbasic_list, - lp_solution_t& sol, - i_t& iter, - std::vector& delta_y_steepest_edge) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; diff --git a/cpp/src/dual_simplex/phase2.hpp b/cpp/src/dual_simplex/phase2.hpp index 4719c9d5b4..a4f8066d08 100644 --- a/cpp/src/dual_simplex/phase2.hpp +++ b/cpp/src/dual_simplex/phase2.hpp @@ -56,15 +56,6 @@ static std::string status_to_string(status_t status) } } // namespace dual -template -dual::status_t factorize_basis(const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - std::vector& vstatus, - basis_update_mpf_t& ft, - std::vector& basic_list, - std::vector& nonbasic_list, - f_t start_time); - template dual::status_t dual_phase2(i_t phase, i_t slack_basis, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 1b3e2d258b..5850893bce 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -167,21 +167,21 @@ lp_status_t solve_linear_program_with_basis_update( column_scaling(presolved_lp, settings, lp, column_scales); assert(presolved_lp.num_cols == lp.num_cols); lp_problem_t phase1_problem(original_lp.handle_ptr, 1, 1, 1); - + std::vector phase1_vstatus; f_t phase1_obj = -inf; create_phase1_problem(lp, phase1_problem); assert(phase1_problem.num_cols == presolved_lp.num_cols); // Set the vstatus for the phase1 problem based on a slack basis - vstatus.resize(phase1_problem.num_cols); - std::fill(vstatus.begin(), vstatus.end(), variable_status_t::NONBASIC_LOWER); + phase1_vstatus.resize(phase1_problem.num_cols); + std::fill(phase1_vstatus.begin(), phase1_vstatus.end(), variable_status_t::NONBASIC_LOWER); i_t num_basic = 0; for (i_t j = phase1_problem.num_cols - 1; j >= 0; --j) { const i_t col_start = phase1_problem.A.col_start[j]; const i_t col_end = phase1_problem.A.col_start[j + 1]; const i_t nz = col_end - col_start; if (nz == 1 && std::abs(phase1_problem.A.x[col_start]) == 1.0) { - vstatus[j] = variable_status_t::BASIC; + phase1_vstatus[j] = variable_status_t::BASIC; num_basic++; } if (num_basic == phase1_problem.num_rows) { break; } @@ -189,28 +189,9 @@ lp_status_t solve_linear_program_with_basis_update( assert(num_basic == phase1_problem.num_rows); i_t iter = 0; lp_solution_t phase1_solution(phase1_problem.num_rows, phase1_problem.num_cols); - - dual::status_t LU_status = - factorize_basis(lp, settings, vstatus, ft, basic_list, nonbasic_list, start_time); - if (LU_status == dual::status_t::NUMERICAL) { - settings.log.printf("Failed in factorizing the basis\n"); - return lp_status_t::NUMERICAL_ISSUES; - } - if (LU_status == dual::status_t::TIME_LIMIT) { return lp_status_t::TIME_LIMIT; } - - edge_norms.clear(); - dual::status_t phase1_status = dual_phase2_with_basis_update(1, - 1, - start_time, - phase1_problem, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - phase1_solution, - iter, - edge_norms); + std::vector junk; + dual::status_t phase1_status = dual_phase2( + 1, 1, start_time, phase1_problem, settings, phase1_vstatus, phase1_solution, iter, junk); if (phase1_status == dual::status_t::NUMERICAL || phase1_status == dual::status_t::DUAL_UNBOUNDED) { settings.log.printf("Failed in Phase 1\n"); @@ -225,13 +206,14 @@ lp_status_t solve_linear_program_with_basis_update( lp_solution_t solution(lp.num_rows, lp.num_cols); assert(lp.num_cols == phase1_problem.num_cols); assert(solution.x.size() == lp.num_cols); - + vstatus = phase1_vstatus; edge_norms.clear(); bool initialize_basis_update = true; dual::status_t status = dual_phase2_with_basis_update( 2, iter == 0 ? 1 : 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); if (status == dual::status_t::NUMERICAL) { // Became dual infeasible. Try phase 1 again + phase1_vstatus = vstatus; settings.log.printf("Running Phase 1 again\n"); junk.clear(); initialize_basis_update = false; From 27884e46a1a1059cf8c9e146b9abf57f7eaded3b Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 22 Oct 2025 16:36:49 -0700 Subject: [PATCH 19/86] Rename functions. Address code-rabbit issues --- cpp/src/dual_simplex/basis_updates.cpp | 3 +- cpp/src/dual_simplex/basis_updates.hpp | 1 - cpp/src/dual_simplex/phase2.cpp | 54 +++++++++++++------------- cpp/src/dual_simplex/phase2.hpp | 26 ++++++------- cpp/src/dual_simplex/solve.cpp | 28 ++++++------- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index b34ebcde8e..2c1da68793 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2074,6 +2074,7 @@ int basis_update_mpf_t::refactor_basis( basis_repair(A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); #ifdef CHECK_BASIS_REPAIR + const i_t m = A.m; csc_matrix_t B(m, m, 0); form_b(A, basic_list, B); for (i_t k = 0; k < deficient.size(); ++k) { @@ -2100,7 +2101,7 @@ int basis_update_mpf_t::refactor_basis( deficient, slacks_needed) == -1) { #ifdef CHECK_L_FACTOR - if (L.check_matrix() == -1) { settings.log.printf("Bad L after basis repair\n"); } + if (L0_.check_matrix() == -1) { settings.log.printf("Bad L after basis repair\n"); } #endif return deficient.size(); } diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index 11e31732db..e13f4dddf4 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -284,7 +284,6 @@ class basis_update_mpf_t { i_t reset() { - inverse_permutation(row_permutation_, inverse_row_permutation_); clear(); compute_transposes(); reset_stats(); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 3509fa1bee..098c3b6e2b 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2188,35 +2188,35 @@ dual::status_t dual_phase2(i_t phase, std::vector superbasic_list; basis_update_mpf_t ft(m, settings.refactor_frequency); const bool initialize_basis = true; - return dual_phase2_with_basis_update(phase, - slack_basis, - initialize_basis, - start_time, - lp, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - sol, - iter, - delta_y_steepest_edge); + return dual_phase2_with_advanced_basis(phase, + slack_basis, + initialize_basis, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + delta_y_steepest_edge); } template -dual::status_t dual_phase2_with_basis_update(i_t phase, - i_t slack_basis, - bool initialize_basis, - f_t start_time, - const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - std::vector& vstatus, - basis_update_mpf_t& ft, - std::vector& basic_list, - std::vector& nonbasic_list, - lp_solution_t& sol, - i_t& iter, - std::vector& delta_y_steepest_edge) +dual::status_t dual_phase2_with_advanced_basis(i_t phase, + i_t slack_basis, + bool initialize_basis, + f_t start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + i_t& iter, + std::vector& delta_y_steepest_edge) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -3006,7 +3006,7 @@ template dual::status_t dual_phase2( int& iter, std::vector& steepest_edge_norms); -template dual::status_t dual_phase2_with_basis_update( +template dual::status_t dual_phase2_with_advanced_basis( int phase, int slack_basis, bool initialize_basis, diff --git a/cpp/src/dual_simplex/phase2.hpp b/cpp/src/dual_simplex/phase2.hpp index a4f8066d08..e237ca3724 100644 --- a/cpp/src/dual_simplex/phase2.hpp +++ b/cpp/src/dual_simplex/phase2.hpp @@ -68,18 +68,18 @@ dual::status_t dual_phase2(i_t phase, std::vector& steepest_edge_norms); template -dual::status_t dual_phase2_with_basis_update(i_t phase, - i_t slack_basis, - bool initialize_basis, - f_t start_time, - const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - std::vector& vstatus, - basis_update_mpf_t& ft, - std::vector& basic_list, - std::vector& nonbasic_list, - lp_solution_t& sol, - i_t& iter, - std::vector& delta_y_steepest_edge); +dual::status_t dual_phase2_with_advanced_basis(i_t phase, + i_t slack_basis, + bool initialize_basis, + f_t start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& ft, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + i_t& iter, + std::vector& delta_y_steepest_edge); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 5850893bce..d286b0dbf1 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -123,19 +123,19 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original std::vector basic_list(m); std::vector nonbasic_list; basis_update_mpf_t ft(m, settings.refactor_frequency); - return solve_linear_program_with_basis_update(original_lp, - start_time, - settings, - original_solution, - ft, - basic_list, - nonbasic_list, - vstatus, - edge_norms); + return solve_linear_program_with_advanced_basis(original_lp, + start_time, + settings, + original_solution, + ft, + basic_list, + nonbasic_list, + vstatus, + edge_norms); } template -lp_status_t solve_linear_program_with_basis_update( +lp_status_t solve_linear_program_with_advanced_basis( const lp_problem_t& original_lp, const f_t start_time, const simplex_solver_settings_t& settings, @@ -209,7 +209,7 @@ lp_status_t solve_linear_program_with_basis_update( vstatus = phase1_vstatus; edge_norms.clear(); bool initialize_basis_update = true; - dual::status_t status = dual_phase2_with_basis_update( + dual::status_t status = dual_phase2_with_advanced_basis( 2, iter == 0 ? 1 : 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); if (status == dual::status_t::NUMERICAL) { // Became dual infeasible. Try phase 1 again @@ -217,7 +217,7 @@ lp_status_t solve_linear_program_with_basis_update( settings.log.printf("Running Phase 1 again\n"); junk.clear(); initialize_basis_update = false; - dual_phase2_with_basis_update(1, + dual_phase2_with_advanced_basis(1, 0, initialize_basis_update, start_time, @@ -232,7 +232,7 @@ lp_status_t solve_linear_program_with_basis_update( edge_norms); vstatus = phase1_vstatus; edge_norms.clear(); - status = dual_phase2_with_basis_update(2, 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); + status = dual_phase2_with_advanced_basis(2, 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); } constexpr bool primal_cleanup = false; if (status == dual::status_t::OPTIMAL && primal_cleanup) { @@ -637,7 +637,7 @@ template lp_status_t solve_linear_program_advanced( std::vector& vstatus, std::vector& edge_norms); -template lp_status_t solve_linear_program_with_basis_update( +template lp_status_t solve_linear_program_with_advanced_basis( const lp_problem_t& original_lp, const double start_time, const simplex_solver_settings_t& settings, From deffe681242b3bfc5c44e9a146058bb776aa488f Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 22 Oct 2025 16:37:22 -0700 Subject: [PATCH 20/86] Style fixes --- cpp/src/dual_simplex/solve.cpp | 53 ++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index d286b0dbf1..ba6a050848 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -209,8 +209,19 @@ lp_status_t solve_linear_program_with_advanced_basis( vstatus = phase1_vstatus; edge_norms.clear(); bool initialize_basis_update = true; - dual::status_t status = dual_phase2_with_advanced_basis( - 2, iter == 0 ? 1 : 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); + dual::status_t status = dual_phase2_with_advanced_basis(2, + iter == 0 ? 1 : 0, + initialize_basis_update, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + solution, + iter, + edge_norms); if (status == dual::status_t::NUMERICAL) { // Became dual infeasible. Try phase 1 again phase1_vstatus = vstatus; @@ -218,21 +229,33 @@ lp_status_t solve_linear_program_with_advanced_basis( junk.clear(); initialize_basis_update = false; dual_phase2_with_advanced_basis(1, - 0, - initialize_basis_update, - start_time, - phase1_problem, - settings, - phase1_vstatus, - ft, - basic_list, - nonbasic_list, - phase1_solution, - iter, - edge_norms); + 0, + initialize_basis_update, + start_time, + phase1_problem, + settings, + phase1_vstatus, + ft, + basic_list, + nonbasic_list, + phase1_solution, + iter, + edge_norms); vstatus = phase1_vstatus; edge_norms.clear(); - status = dual_phase2_with_advanced_basis(2, 0, initialize_basis_update, start_time, lp, settings, vstatus, ft, basic_list, nonbasic_list, solution, iter, edge_norms); + status = dual_phase2_with_advanced_basis(2, + 0, + initialize_basis_update, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + solution, + iter, + edge_norms); } constexpr bool primal_cleanup = false; if (status == dual::status_t::OPTIMAL && primal_cleanup) { From f779cb71f01aea6096eb2100f891eb5f16316e18 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 22 Oct 2025 17:23:36 -0700 Subject: [PATCH 21/86] Remove confusing col_permutation_ from middle-product update --- cpp/src/dual_simplex/basis_updates.cpp | 9 +++++---- cpp/src/dual_simplex/basis_updates.hpp | 10 ---------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 2c1da68793..d2127cc4d8 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2060,6 +2060,7 @@ int basis_update_mpf_t::refactor_basis( std::vector slacks_needed; if (L0_.m != A.m) { resize(A.m); } + std::vector q; if (factorize_basis(A, settings, basic_list, @@ -2067,7 +2068,7 @@ int basis_update_mpf_t::refactor_basis( U0_, row_permutation_, inverse_row_permutation_, - col_permutation_, + q, deficient, slacks_needed) == -1) { settings.log.debug("Initial factorization failed\n"); @@ -2097,7 +2098,7 @@ int basis_update_mpf_t::refactor_basis( U0_, row_permutation_, inverse_row_permutation_, - col_permutation_, + q, deficient, slacks_needed) == -1) { #ifdef CHECK_L_FACTOR @@ -2108,8 +2109,8 @@ int basis_update_mpf_t::refactor_basis( settings.log.debug("Basis repaired\n"); } - assert(col_permutation_.size() == A.m); - reorder_basic_list(col_permutation_, basic_list); + assert(q.size() == A.m); + reorder_basic_list(q, basic_list); // We no longer need q after reordering the basic list reset(); return 0; } diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index e13f4dddf4..d92a528a7f 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -184,8 +184,6 @@ class basis_update_mpf_t { row_permutation_(n), inverse_row_permutation_(n), S_(n, 0, 0), - col_permutation_(n), - inverse_col_permutation_(n), xi_workspace_(2 * n, 0), x_workspace_(n, 0.0), U0_transpose_(1, 1, 1), @@ -214,8 +212,6 @@ class basis_update_mpf_t { row_permutation_(p), inverse_row_permutation_(p.size()), S_(Linit.m, 0, 0), - col_permutation_(Linit.m), - inverse_col_permutation_(Linit.m), xi_workspace_(2 * Linit.m, 0), x_workspace_(Linit.m, 0.0), U0_transpose_(1, 1, 1), @@ -297,8 +293,6 @@ class basis_update_mpf_t { row_permutation_.resize(n); inverse_row_permutation_.resize(n); S_.resize(n, 0, 0); - col_permutation_.resize(n); - inverse_col_permutation_.resize(n); xi_workspace_.resize(2 * n, 0); x_workspace_.resize(n, 0.0); U0_transpose_.resize(1, 1, 1); @@ -398,8 +392,6 @@ class basis_update_mpf_t { { pivot_indices_.clear(); pivot_indices_.reserve(L0_.m); - std::iota(col_permutation_.begin(), col_permutation_.end(), 0); - std::iota(inverse_col_permutation_.begin(), inverse_col_permutation_.end(), 0); S_.col_start.resize(refactor_frequency_ + 1); S_.col_start[0] = 0; S_.col_start[1] = 0; @@ -452,8 +444,6 @@ class basis_update_mpf_t { std::vector pivot_indices_; // indicies for rank-1 updates to L csc_matrix_t S_; // stores information about the rank-1 updates to L std::vector mu_values_; // stores information about the rank-1 updates to L - std::vector col_permutation_; // symmetric permuation q used in U(q, q) represents Q - std::vector inverse_col_permutation_; // inverse permutation represents Q' mutable std::vector xi_workspace_; mutable std::vector x_workspace_; mutable csc_matrix_t U0_transpose_; // Needed for sparse solves From f669f642a900d28a5c9c439b91d922eb6d1ed8bc Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 14:20:04 +0200 Subject: [PATCH 22/86] removed unused variable --- cpp/src/dual_simplex/solve.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index ba6a050848..b6f84a9dc8 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -189,9 +189,9 @@ lp_status_t solve_linear_program_with_advanced_basis( assert(num_basic == phase1_problem.num_rows); i_t iter = 0; lp_solution_t phase1_solution(phase1_problem.num_rows, phase1_problem.num_cols); - std::vector junk; + edge_norms.clear(); dual::status_t phase1_status = dual_phase2( - 1, 1, start_time, phase1_problem, settings, phase1_vstatus, phase1_solution, iter, junk); + 1, 1, start_time, phase1_problem, settings, phase1_vstatus, phase1_solution, iter, edge_norms); if (phase1_status == dual::status_t::NUMERICAL || phase1_status == dual::status_t::DUAL_UNBOUNDED) { settings.log.printf("Failed in Phase 1\n"); @@ -226,7 +226,7 @@ lp_status_t solve_linear_program_with_advanced_basis( // Became dual infeasible. Try phase 1 again phase1_vstatus = vstatus; settings.log.printf("Running Phase 1 again\n"); - junk.clear(); + edge_norms.clear(); initialize_basis_update = false; dual_phase2_with_advanced_basis(1, 0, From 8a06d3ea0fce2c443d37dc4c0cc02188bf757490 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 14:33:12 +0200 Subject: [PATCH 23/86] added missing function declaration --- cpp/src/dual_simplex/solve.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 65418f5f22..53146eda96 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -60,7 +60,7 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original // Solve the LP using dual simplex and keep the `basis_update_mpf_t` // for future use. template -lp_status_t solve_linear_program_with_basis_update( +lp_status_t solve_linear_program_with_advanced_basis( const lp_problem_t& original_lp, const f_t start_time, const simplex_solver_settings_t& settings, From 6ccb6d9f1b205e675c09d0e6a81d7d9acf006b35 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 17:05:54 +0200 Subject: [PATCH 24/86] fixed race condition --- cpp/src/dual_simplex/branch_and_bound.cpp | 81 ++++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 20 +++--- cpp/src/dual_simplex/node_presolve.cpp | 32 ++++----- cpp/src/dual_simplex/node_presolve.hpp | 10 +-- 4 files changed, 74 insertions(+), 69 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 56592ab734..a1d59cc858 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -572,7 +572,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); - bool feasible = presolve.bound_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); + bool feasible = presolve.bound_strengthening(leaf_problem.lower, leaf_problem.upper); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; @@ -702,8 +702,6 @@ void branch_and_bound_t::set_variable_bounds(mip_node_t* nod template void branch_and_bound_t::exploration_ramp_up(search_tree_t* search_tree, mip_node_t* node, - lp_problem_t& leaf_problem, - const csc_matrix_t& Arow, i_t initial_heap_size) { if (status_ != mip_exploration_status_t::RUNNING) { return; } @@ -713,6 +711,10 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* // to repair the heuristic solution. repair_heuristic_solutions(); + i_t tid = omp_get_thread_num(); + auto presolve = thread_data_[tid].presolve; + auto leaf_problem = thread_data_[tid].leaf_problem; + f_t lower_bound = node->lower_bound; f_t upper_bound = get_upper_bound(); f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); @@ -760,20 +762,17 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* return; } - std::vector row_sense; - node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); - // Set the correct bounds for the leaf problem set_variable_bounds(node, - leaf_problem.lower, - leaf_problem.upper, - presolve.bounds_changed, + leaf_problem->lower, + leaf_problem->upper, + presolve->bounds_changed, original_lp_.lower, original_lp_.upper, true); node_status_t node_status = - solve_node(*search_tree, node, leaf_problem, presolve, 'B', settings_.log); + solve_node(*search_tree, node, *leaf_problem, *presolve, 'B', settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -785,11 +784,10 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase if (stats_.nodes_unexplored < initial_heap_size) { #pragma omp task - exploration_ramp_up( - search_tree, node->get_down_child(), leaf_problem, Arow, initial_heap_size); + exploration_ramp_up(search_tree, node->get_down_child(), initial_heap_size); #pragma omp task - exploration_ramp_up(search_tree, node->get_up_child(), leaf_problem, Arow, initial_heap_size); + exploration_ramp_up(search_tree, node->get_up_child(), initial_heap_size); } else { // We've generated enough nodes, push further nodes onto the heap @@ -804,14 +802,16 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* template void branch_and_bound_t::explore_subtree(i_t task_id, search_tree_t& search_tree, - mip_node_t* start_node, - lp_problem_t& leaf_problem, - node_presolve_t& presolve) + mip_node_t* start_node) { bool recompute = true; std::deque*> stack; stack.push_front(start_node); + i_t tid = omp_get_thread_num(); + auto presolve = thread_data_[tid].presolve; + auto leaf_problem = thread_data_[tid].leaf_problem; + while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { if (task_id == 0) { repair_heuristic_solutions(); } @@ -874,15 +874,15 @@ void branch_and_bound_t::explore_subtree(i_t task_id, // Set the correct bounds for the leaf problem set_variable_bounds(node_ptr, - leaf_problem.lower, - leaf_problem.upper, - presolve.bounds_changed, + leaf_problem->lower, + leaf_problem->upper, + presolve->bounds_changed, original_lp_.lower, original_lp_.upper, recompute); node_status_t node_status = - solve_node(search_tree, node_ptr, leaf_problem, presolve, 'B', settings_.log); + solve_node(search_tree, node_ptr, *leaf_problem, *presolve, 'B', settings_.log); recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { @@ -906,7 +906,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, // would be better if we discard the node instead. if (get_heap_size() > settings_.num_bfs_threads) { mutex_dive_queue_.lock(); - dive_queue_.emplace(node->detach_copy(), leaf_problem.lower, leaf_problem.upper); + dive_queue_.emplace(node->detach_copy(), leaf_problem->lower, leaf_problem->upper); mutex_dive_queue_.unlock(); } @@ -925,10 +925,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } template -void branch_and_bound_t::best_first_thread(i_t id, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - node_presolve_t& presolve) +void branch_and_bound_t::best_first_thread(i_t id, search_tree_t& search_tree) { f_t lower_bound = -inf; f_t upper_bound = inf; @@ -960,7 +957,7 @@ void branch_and_bound_t::best_first_thread(i_t id, } // Best-first search with plunging - explore_subtree(id, search_tree, node_ptr, leaf_problem, presolve); + explore_subtree(id, search_tree, node_ptr); active_subtrees_--; } @@ -982,12 +979,15 @@ void branch_and_bound_t::best_first_thread(i_t id, } template -void branch_and_bound_t::diving_thread(lp_problem_t& leaf_problem, - node_presolve_t& presolve) +void branch_and_bound_t::diving_thread() { logger_t log; log.log = false; + i_t tid = omp_get_thread_num(); + auto presolve = thread_data_[tid].presolve; + auto leaf_problem = thread_data_[tid].leaf_problem; + while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { std::optional> start_node; @@ -1019,14 +1019,15 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr // Set the correct bounds for the leaf problem set_variable_bounds(node_ptr, - leaf_problem.lower, - leaf_problem.upper, - presolve.bounds_changed, + leaf_problem->lower, + leaf_problem->upper, + presolve->bounds_changed, start_node->lower, start_node->upper, recompute); - node_status_t node_status = solve_node(subtree, node_ptr, leaf_problem, presolve, 'D', log); + node_status_t node_status = + solve_node(subtree, node_ptr, *leaf_problem, *presolve, 'D', log); if (node_status == node_status_t::TIME_LIMIT) { return; @@ -1046,7 +1047,7 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr mutex_dive_queue_.lock(); mip_node_t* new_node = stack.back(); stack.pop_back(); - dive_queue_.emplace(new_node->detach_copy(), leaf_problem.lower, leaf_problem.upper); + dive_queue_.emplace(new_node->detach_copy(), leaf_problem->lower, leaf_problem->upper); mutex_dive_queue_.unlock(); } } @@ -1213,13 +1214,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut min_diving_queue_size_ = 4 * settings_.num_diving_threads; status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; + thread_data_.resize(settings_.num_threads, {nullptr, nullptr}); #pragma omp parallel num_threads(settings_.num_threads) { // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_); + node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_, settings_); + + thread_data_[omp_get_thread_num()].presolve = &presolve; + thread_data_[omp_get_thread_num()].leaf_problem = &leaf_problem; #pragma omp master { @@ -1228,10 +1233,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t initial_size = 2 * settings_.num_threads; #pragma omp task - exploration_ramp_up(&search_tree, down_child, leaf_problem, Arow, initial_size); + exploration_ramp_up(&search_tree, down_child, initial_size); #pragma omp task - exploration_ramp_up(&search_tree, up_child, leaf_problem, Arow, initial_size); + exploration_ramp_up(&search_tree, up_child, initial_size); } #pragma omp barrier @@ -1242,12 +1247,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut (active_subtrees_ > 0 || get_heap_size() > 0)) { for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree, leaf_problem, presolve); + best_first_thread(i, search_tree); } for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(leaf_problem, presolve); + diving_thread(); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 79c8c6650b..f0d1b8b4b0 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -213,6 +213,13 @@ class branch_and_bound_t { // its blocks the progression of the lower bound. omp_atomic_t lower_bound_ceiling_; + // Stores the node presolver per thread. + struct thread_data_t { + node_presolve_t* presolve; + lp_problem_t* leaf_problem; + }; + std::vector thread_data_; + // Set the final solution. mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); @@ -238,27 +245,20 @@ class branch_and_bound_t { // there is enough unexplored nodes. This is done recursively using OpenMP tasks. void exploration_ramp_up(search_tree_t* search_tree, mip_node_t* node, - lp_problem_t& leaf_problem, - const csc_matrix_t& Arow, i_t initial_heap_size); // Explore the search tree using the best-first search with plunging strategy. void explore_subtree(i_t task_id, search_tree_t& search_tree, - mip_node_t* start_node, - lp_problem_t& leaf_problem, - node_presolve_t& presolve); + mip_node_t* start_node); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. - void best_first_thread(i_t id, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - node_presolve_t& presolve); + void best_first_thread(i_t id, search_tree_t& search_tree); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(lp_problem_t& leaf_problem, node_presolve_t& presolve); + void diving_thread(); // Solve the LP relaxation of a leaf node and update the tree. node_status_t solve_node(search_tree_t& search_tree, diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp index 829ef5f0e1..a8b773b914 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -64,11 +64,13 @@ template node_presolve_t::node_presolve_t(const lp_problem_t& problem, const std::vector& row_sense, const csc_matrix_t& Arow, - const std::vector& var_types) + const std::vector& var_types, + const simplex_solver_settings_t& settings) : bounds_changed(problem.num_cols, false), - problem(problem), + A(problem.A), Arow(Arow), var_types(var_types), + settings(settings), delta_min_activity(problem.num_rows), delta_max_activity(problem.num_rows), constraint_lb(problem.num_rows), @@ -96,13 +98,11 @@ node_presolve_t::node_presolve_t(const lp_problem_t& problem } template -bool node_presolve_t::bound_strengthening( - std::vector& lower_bounds, - std::vector& upper_bounds, - const simplex_solver_settings_t& settings) +bool node_presolve_t::bound_strengthening(std::vector& lower_bounds, + std::vector& upper_bounds) { - const i_t m = problem.num_rows; - const i_t n = problem.num_cols; + const i_t m = A.m; + const i_t n = A.n; std::vector constraint_changed(m, false); std::vector variable_changed(n, false); @@ -110,10 +110,10 @@ bool node_presolve_t::bound_strengthening( for (i_t i = 0; i < bounds_changed.size(); ++i) { if (bounds_changed[i]) { - const i_t row_start = problem.A.col_start[i]; - const i_t row_end = problem.A.col_start[i + 1]; + const i_t row_start = A.col_start[i]; + const i_t row_end = A.col_start[i + 1]; for (i_t p = row_start; p < row_end; ++p) { - const i_t j = problem.A.i[p]; + const i_t j = A.i[p]; constraint_changed[j] = true; } } @@ -182,13 +182,13 @@ bool node_presolve_t::bound_strengthening( f_t new_lb = old_lb; f_t new_ub = old_ub; - const i_t row_start = problem.A.col_start[k]; - const i_t row_end = problem.A.col_start[k + 1]; + const i_t row_start = A.col_start[k]; + const i_t row_end = A.col_start[k + 1]; for (i_t p = row_start; p < row_end; ++p) { - const i_t i = problem.A.i[p]; + const i_t i = A.i[p]; if (!constraint_changed[i]) { continue; } - const f_t a_ik = problem.A.x[p]; + const f_t a_ik = A.x[p]; f_t delta_min_act = delta_min_activity[i]; f_t delta_max_act = delta_max_activity[i]; @@ -220,7 +220,7 @@ bool node_presolve_t::bound_strengthening( } if (new_lb != old_lb || new_ub != old_ub) { for (i_t p = row_start; p < row_end; ++p) { - const i_t i = problem.A.i[p]; + const i_t i = A.i[p]; constraint_changed_next[i] = true; } } diff --git a/cpp/src/dual_simplex/node_presolve.hpp b/cpp/src/dual_simplex/node_presolve.hpp index 54a3c6c341..1ebcf750d8 100644 --- a/cpp/src/dual_simplex/node_presolve.hpp +++ b/cpp/src/dual_simplex/node_presolve.hpp @@ -28,18 +28,18 @@ class node_presolve_t { node_presolve_t(const lp_problem_t& problem, const std::vector& row_sense, const csc_matrix_t& Arow, - const std::vector& var_types); + const std::vector& var_types, + const simplex_solver_settings_t& settings); - bool bound_strengthening(std::vector& lower_bounds, - std::vector& upper_bounds, - const simplex_solver_settings_t& settings); + bool bound_strengthening(std::vector& lower_bounds, std::vector& upper_bounds); std::vector bounds_changed; private: - const lp_problem_t& problem; + const csc_matrix_t& A; const csc_matrix_t& Arow; const std::vector& var_types; + const simplex_solver_settings_t& settings; std::vector delta_min_activity; std::vector delta_max_activity; From 8f55932f527ba0295060dc0246ff31500b598c79 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 17:20:15 +0200 Subject: [PATCH 25/86] small refactor --- cpp/src/dual_simplex/branch_and_bound.cpp | 31 ++++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 4 +-- cpp/src/dual_simplex/node_presolve.cpp | 16 ++++++------ cpp/src/dual_simplex/node_presolve.hpp | 12 ++++----- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index a1d59cc858..1390b0091f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -555,7 +555,7 @@ template node_status_t branch_and_bound_t::solve_node(search_tree_t& search_tree, mip_node_t* node_ptr, lp_problem_t& leaf_problem, - node_presolve_t& presolve, + node_presolver_t& presolver, char thread_type, logger_t& log) { @@ -572,7 +572,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); - bool feasible = presolve.bound_strengthening(leaf_problem.lower, leaf_problem.upper); + bool feasible = presolver.bound_strengthening(leaf_problem.lower, leaf_problem.upper); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; @@ -712,7 +712,7 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* repair_heuristic_solutions(); i_t tid = omp_get_thread_num(); - auto presolve = thread_data_[tid].presolve; + auto presolver = thread_data_[tid].presolver; auto leaf_problem = thread_data_[tid].leaf_problem; f_t lower_bound = node->lower_bound; @@ -766,13 +766,13 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* set_variable_bounds(node, leaf_problem->lower, leaf_problem->upper, - presolve->bounds_changed, + presolver->bounds_changed, original_lp_.lower, original_lp_.upper, true); node_status_t node_status = - solve_node(*search_tree, node, *leaf_problem, *presolve, 'B', settings_.log); + solve_node(*search_tree, node, *leaf_problem, *presolver, 'B', settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -809,7 +809,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, stack.push_front(start_node); i_t tid = omp_get_thread_num(); - auto presolve = thread_data_[tid].presolve; + auto presolver = thread_data_[tid].presolver; auto leaf_problem = thread_data_[tid].leaf_problem; while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { @@ -876,13 +876,13 @@ void branch_and_bound_t::explore_subtree(i_t task_id, set_variable_bounds(node_ptr, leaf_problem->lower, leaf_problem->upper, - presolve->bounds_changed, + presolver->bounds_changed, original_lp_.lower, original_lp_.upper, recompute); node_status_t node_status = - solve_node(search_tree, node_ptr, *leaf_problem, *presolve, 'B', settings_.log); + solve_node(search_tree, node_ptr, *leaf_problem, *presolver, 'B', settings_.log); recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { @@ -985,7 +985,7 @@ void branch_and_bound_t::diving_thread() log.log = false; i_t tid = omp_get_thread_num(); - auto presolve = thread_data_[tid].presolve; + auto presolver = thread_data_[tid].presolver; auto leaf_problem = thread_data_[tid].leaf_problem; while (status_ == mip_exploration_status_t::RUNNING && @@ -1021,13 +1021,13 @@ void branch_and_bound_t::diving_thread() set_variable_bounds(node_ptr, leaf_problem->lower, leaf_problem->upper, - presolve->bounds_changed, + presolver->bounds_changed, start_node->lower, start_node->upper, recompute); node_status_t node_status = - solve_node(subtree, node_ptr, *leaf_problem, *presolve, 'D', log); + solve_node(subtree, node_ptr, *leaf_problem, *presolver, 'D', log); if (node_status == node_status_t::TIME_LIMIT) { return; @@ -1214,17 +1214,18 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut min_diving_queue_size_ = 4 * settings_.num_diving_threads; status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; - thread_data_.resize(settings_.num_threads, {nullptr, nullptr}); + thread_data_.resize(settings_.num_threads); #pragma omp parallel num_threads(settings_.num_threads) { // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - node_presolve_t presolve(leaf_problem, row_sense, Arow, var_types_, settings_); + node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_, settings_); - thread_data_[omp_get_thread_num()].presolve = &presolve; - thread_data_[omp_get_thread_num()].leaf_problem = &leaf_problem; + i_t tid = omp_get_thread_num(); + thread_data_[tid].presolver = &presolver; + thread_data_[tid].leaf_problem = &leaf_problem; #pragma omp master { diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index f0d1b8b4b0..36e7c17a8b 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -215,7 +215,7 @@ class branch_and_bound_t { // Stores the node presolver per thread. struct thread_data_t { - node_presolve_t* presolve; + node_presolver_t* presolver; lp_problem_t* leaf_problem; }; std::vector thread_data_; @@ -264,7 +264,7 @@ class branch_and_bound_t { node_status_t solve_node(search_tree_t& search_tree, mip_node_t* node_ptr, lp_problem_t& leaf_problem, - node_presolve_t& presolve, + node_presolver_t& presolve, char thread_type, logger_t& log); diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp index a8b773b914..c4eb5ff0fd 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -61,11 +61,11 @@ void print_bounds_stats(const std::vector& lower, } template -node_presolve_t::node_presolve_t(const lp_problem_t& problem, - const std::vector& row_sense, - const csc_matrix_t& Arow, - const std::vector& var_types, - const simplex_solver_settings_t& settings) +node_presolver_t::node_presolver_t(const lp_problem_t& problem, + const std::vector& row_sense, + const csc_matrix_t& Arow, + const std::vector& var_types, + const simplex_solver_settings_t& settings) : bounds_changed(problem.num_cols, false), A(problem.A), Arow(Arow), @@ -98,8 +98,8 @@ node_presolve_t::node_presolve_t(const lp_problem_t& problem } template -bool node_presolve_t::bound_strengthening(std::vector& lower_bounds, - std::vector& upper_bounds) +bool node_presolver_t::bound_strengthening(std::vector& lower_bounds, + std::vector& upper_bounds) { const i_t m = A.m; const i_t n = A.n; @@ -287,7 +287,7 @@ bool node_presolve_t::bound_strengthening(std::vector& lower_boun } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE -template class node_presolve_t; +template class node_presolver_t; #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/node_presolve.hpp b/cpp/src/dual_simplex/node_presolve.hpp index 1ebcf750d8..910088ce3d 100644 --- a/cpp/src/dual_simplex/node_presolve.hpp +++ b/cpp/src/dual_simplex/node_presolve.hpp @@ -22,14 +22,14 @@ namespace cuopt::linear_programming::dual_simplex { template -class node_presolve_t { +class node_presolver_t { public: // For pure LP bounds strengthening, var_types should be defaulted (i.e. left empty) - node_presolve_t(const lp_problem_t& problem, - const std::vector& row_sense, - const csc_matrix_t& Arow, - const std::vector& var_types, - const simplex_solver_settings_t& settings); + node_presolver_t(const lp_problem_t& problem, + const std::vector& row_sense, + const csc_matrix_t& Arow, + const std::vector& var_types, + const simplex_solver_settings_t& settings); bool bound_strengthening(std::vector& lower_bounds, std::vector& upper_bounds); From 7f337d4c14f3171b84dc822f3b004b34d51dce95 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 17:33:59 +0200 Subject: [PATCH 26/86] fixed missing template parameters --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 1390b0091f..dc7ee39675 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1214,12 +1214,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut min_diving_queue_size_ = 4 * settings_.num_diving_threads; status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; - thread_data_.resize(settings_.num_threads); + thread_data_.resize(settings_.num_threads, {nullptr, nullptr}); #pragma omp parallel num_threads(settings_.num_threads) { // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp_; + lp_problem_t leaf_problem = original_lp_; std::vector row_sense; node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_, settings_); From 06ca86d87f5db91788e49fd8c89aa8bd40612c43 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 19:08:09 +0200 Subject: [PATCH 27/86] fixed crash --- cpp/src/dual_simplex/branch_and_bound.cpp | 97 +++++++++++------------ cpp/src/dual_simplex/branch_and_bound.hpp | 28 +++---- cpp/src/dual_simplex/node_presolve.cpp | 10 +-- cpp/src/dual_simplex/node_presolve.hpp | 8 +- 4 files changed, 70 insertions(+), 73 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index dc7ee39675..dd8efd562c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -552,8 +552,8 @@ branch_and_bound_t::child_selection(mip_node_t* node_ptr) } template -node_status_t branch_and_bound_t::solve_node(search_tree_t& search_tree, - mip_node_t* node_ptr, +node_status_t branch_and_bound_t::solve_node(mip_node_t* node_ptr, + search_tree_t& search_tree, lp_problem_t& leaf_problem, node_presolver_t& presolver, char thread_type, @@ -572,7 +572,8 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); - bool feasible = presolver.bound_strengthening(leaf_problem.lower, leaf_problem.upper); + bool feasible = + presolver.bound_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; @@ -700,8 +701,9 @@ void branch_and_bound_t::set_variable_bounds(mip_node_t* nod } template -void branch_and_bound_t::exploration_ramp_up(search_tree_t* search_tree, - mip_node_t* node, +void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, + search_tree_t* search_tree, + const csc_matrix_t& Arow, i_t initial_heap_size) { if (status_ != mip_exploration_status_t::RUNNING) { return; } @@ -711,9 +713,10 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* // to repair the heuristic solution. repair_heuristic_solutions(); - i_t tid = omp_get_thread_num(); - auto presolver = thread_data_[tid].presolver; - auto leaf_problem = thread_data_[tid].leaf_problem; + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp_; + std::vector row_sense; + node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_); f_t lower_bound = node->lower_bound; f_t upper_bound = get_upper_bound(); @@ -764,15 +767,15 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* // Set the correct bounds for the leaf problem set_variable_bounds(node, - leaf_problem->lower, - leaf_problem->upper, - presolver->bounds_changed, + leaf_problem.lower, + leaf_problem.upper, + presolver.bounds_changed, original_lp_.lower, original_lp_.upper, true); node_status_t node_status = - solve_node(*search_tree, node, *leaf_problem, *presolver, 'B', settings_.log); + solve_node(node, *search_tree, leaf_problem, presolver, 'B', settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -784,10 +787,10 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase if (stats_.nodes_unexplored < initial_heap_size) { #pragma omp task - exploration_ramp_up(search_tree, node->get_down_child(), initial_heap_size); + exploration_ramp_up(node->get_down_child(), search_tree, Arow, initial_heap_size); #pragma omp task - exploration_ramp_up(search_tree, node->get_up_child(), initial_heap_size); + exploration_ramp_up(node->get_up_child(), search_tree, Arow, initial_heap_size); } else { // We've generated enough nodes, push further nodes onto the heap @@ -801,17 +804,15 @@ void branch_and_bound_t::exploration_ramp_up(search_tree_t* template void branch_and_bound_t::explore_subtree(i_t task_id, + mip_node_t* start_node, search_tree_t& search_tree, - mip_node_t* start_node) + lp_problem_t& leaf_problem, + node_presolver_t& presolver) { bool recompute = true; std::deque*> stack; stack.push_front(start_node); - i_t tid = omp_get_thread_num(); - auto presolver = thread_data_[tid].presolver; - auto leaf_problem = thread_data_[tid].leaf_problem; - while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { if (task_id == 0) { repair_heuristic_solutions(); } @@ -874,15 +875,15 @@ void branch_and_bound_t::explore_subtree(i_t task_id, // Set the correct bounds for the leaf problem set_variable_bounds(node_ptr, - leaf_problem->lower, - leaf_problem->upper, - presolver->bounds_changed, + leaf_problem.lower, + leaf_problem.upper, + presolver.bounds_changed, original_lp_.lower, original_lp_.upper, recompute); node_status_t node_status = - solve_node(search_tree, node_ptr, *leaf_problem, *presolver, 'B', settings_.log); + solve_node(node_ptr, search_tree, leaf_problem, presolver, 'B', settings_.log); recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { @@ -906,7 +907,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, // would be better if we discard the node instead. if (get_heap_size() > settings_.num_bfs_threads) { mutex_dive_queue_.lock(); - dive_queue_.emplace(node->detach_copy(), leaf_problem->lower, leaf_problem->upper); + dive_queue_.emplace(node->detach_copy(), leaf_problem.lower, leaf_problem.upper); mutex_dive_queue_.unlock(); } @@ -925,13 +926,20 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } template -void branch_and_bound_t::best_first_thread(i_t id, search_tree_t& search_tree) +void branch_and_bound_t::best_first_thread(i_t id, + search_tree_t& search_tree, + const csc_matrix_t& Arow) { f_t lower_bound = -inf; f_t upper_bound = inf; f_t abs_gap = inf; f_t rel_gap = inf; + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp_; + std::vector row_sense; + node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_); + while (status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_subtrees_ > 0 || get_heap_size() > 0)) { @@ -957,7 +965,7 @@ void branch_and_bound_t::best_first_thread(i_t id, search_tree_t::best_first_thread(i_t id, search_tree_t -void branch_and_bound_t::diving_thread() +void branch_and_bound_t::diving_thread(const csc_matrix_t& Arow) { logger_t log; log.log = false; - i_t tid = omp_get_thread_num(); - auto presolver = thread_data_[tid].presolver; - auto leaf_problem = thread_data_[tid].leaf_problem; + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp_; + std::vector row_sense; + node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_); while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { @@ -1019,15 +1028,15 @@ void branch_and_bound_t::diving_thread() // Set the correct bounds for the leaf problem set_variable_bounds(node_ptr, - leaf_problem->lower, - leaf_problem->upper, - presolver->bounds_changed, + leaf_problem.lower, + leaf_problem.upper, + presolver.bounds_changed, start_node->lower, start_node->upper, recompute); node_status_t node_status = - solve_node(subtree, node_ptr, *leaf_problem, *presolver, 'D', log); + solve_node(node_ptr, subtree, leaf_problem, presolver, 'D', log); if (node_status == node_status_t::TIME_LIMIT) { return; @@ -1047,7 +1056,7 @@ void branch_and_bound_t::diving_thread() mutex_dive_queue_.lock(); mip_node_t* new_node = stack.back(); stack.pop_back(); - dive_queue_.emplace(new_node->detach_copy(), leaf_problem->lower, leaf_problem->upper); + dive_queue_.emplace(new_node->detach_copy(), leaf_problem.lower, leaf_problem.upper); mutex_dive_queue_.unlock(); } } @@ -1214,19 +1223,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut min_diving_queue_size_ = 4 * settings_.num_diving_threads; status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; - thread_data_.resize(settings_.num_threads, {nullptr, nullptr}); #pragma omp parallel num_threads(settings_.num_threads) { - // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp_; - std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_, settings_); - - i_t tid = omp_get_thread_num(); - thread_data_[tid].presolver = &presolver; - thread_data_[tid].leaf_problem = &leaf_problem; - #pragma omp master { auto down_child = search_tree.root.get_down_child(); @@ -1234,10 +1233,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t initial_size = 2 * settings_.num_threads; #pragma omp task - exploration_ramp_up(&search_tree, down_child, initial_size); + exploration_ramp_up(down_child, &search_tree, Arow, initial_size); #pragma omp task - exploration_ramp_up(&search_tree, up_child, initial_size); + exploration_ramp_up(up_child, &search_tree, Arow, initial_size); } #pragma omp barrier @@ -1248,12 +1247,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut (active_subtrees_ > 0 || get_heap_size() > 0)) { for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree); + best_first_thread(i, search_tree, Arow); } for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(); + diving_thread(Arow); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 36e7c17a8b..d92a62dc91 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -213,13 +213,6 @@ class branch_and_bound_t { // its blocks the progression of the lower bound. omp_atomic_t lower_bound_ceiling_; - // Stores the node presolver per thread. - struct thread_data_t { - node_presolver_t* presolver; - lp_problem_t* leaf_problem; - }; - std::vector thread_data_; - // Set the final solution. mip_status_t set_final_solution(mip_solution_t& solution, f_t lower_bound); @@ -243,28 +236,33 @@ class branch_and_bound_t { // Ramp-up phase of the solver, where we greedily expand the tree until // there is enough unexplored nodes. This is done recursively using OpenMP tasks. - void exploration_ramp_up(search_tree_t* search_tree, - mip_node_t* node, + void exploration_ramp_up(mip_node_t* node, + search_tree_t* search_tree, + const csc_matrix_t& Arow, i_t initial_heap_size); // Explore the search tree using the best-first search with plunging strategy. void explore_subtree(i_t task_id, + mip_node_t* start_node, search_tree_t& search_tree, - mip_node_t* start_node); + lp_problem_t& leaf_problem, + node_presolver_t& presolver); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. - void best_first_thread(i_t id, search_tree_t& search_tree); + void best_first_thread(i_t id, + search_tree_t& search_tree, + const csc_matrix_t& Arow); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(); + void diving_thread(const csc_matrix_t& Arow); // Solve the LP relaxation of a leaf node and update the tree. - node_status_t solve_node(search_tree_t& search_tree, - mip_node_t* node_ptr, + node_status_t solve_node(mip_node_t* node_ptr, + search_tree_t& search_tree, lp_problem_t& leaf_problem, - node_presolver_t& presolve, + node_presolver_t& presolver, char thread_type, logger_t& log); diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp index c4eb5ff0fd..f05bfab5a5 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -64,13 +64,11 @@ template node_presolver_t::node_presolver_t(const lp_problem_t& problem, const std::vector& row_sense, const csc_matrix_t& Arow, - const std::vector& var_types, - const simplex_solver_settings_t& settings) + const std::vector& var_types) : bounds_changed(problem.num_cols, false), A(problem.A), Arow(Arow), var_types(var_types), - settings(settings), delta_min_activity(problem.num_rows), delta_max_activity(problem.num_rows), constraint_lb(problem.num_rows), @@ -98,8 +96,10 @@ node_presolver_t::node_presolver_t(const lp_problem_t& probl } template -bool node_presolver_t::bound_strengthening(std::vector& lower_bounds, - std::vector& upper_bounds) +bool node_presolver_t::bound_strengthening( + std::vector& lower_bounds, + std::vector& upper_bounds, + const simplex_solver_settings_t& settings) { const i_t m = A.m; const i_t n = A.n; diff --git a/cpp/src/dual_simplex/node_presolve.hpp b/cpp/src/dual_simplex/node_presolve.hpp index 910088ce3d..17483d4ff4 100644 --- a/cpp/src/dual_simplex/node_presolve.hpp +++ b/cpp/src/dual_simplex/node_presolve.hpp @@ -28,10 +28,11 @@ class node_presolver_t { node_presolver_t(const lp_problem_t& problem, const std::vector& row_sense, const csc_matrix_t& Arow, - const std::vector& var_types, - const simplex_solver_settings_t& settings); + const std::vector& var_types); - bool bound_strengthening(std::vector& lower_bounds, std::vector& upper_bounds); + bool bound_strengthening(std::vector& lower_bounds, + std::vector& upper_bounds, + const simplex_solver_settings_t& settings); std::vector bounds_changed; @@ -39,7 +40,6 @@ class node_presolver_t { const csc_matrix_t& A; const csc_matrix_t& Arow; const std::vector& var_types; - const simplex_solver_settings_t& settings; std::vector delta_min_activity; std::vector delta_max_activity; From b5fc52c1c760ce8cd87a5c9b2f0e9e232da4ad2c Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 19:24:47 +0200 Subject: [PATCH 28/86] added asserts --- cpp/src/dual_simplex/branch_and_bound.cpp | 7 +++++++ cpp/src/dual_simplex/branch_and_bound.hpp | 6 ++++-- cpp/src/dual_simplex/presolve.cpp | 12 ++---------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index dd8efd562c..88c218a1a9 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -686,6 +687,12 @@ void branch_and_bound_t::set_variable_bounds(mip_node_t* nod const std::vector& root_upper, bool recompute) { + assert(bounds_changed.size() == original_lp_.lower.size()); + assert(lower.size() == original_lp_.lower.size()); + assert(upper.size() == original_lp_.upper.size()); + assert(root_lower.size() == original_lp_.lower.size()); + assert(root_upper.size() == original_lp_.upper.size()); + // Reset the bound_changed markers std::fill(bounds_changed.begin(), bounds_changed.end(), false); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index d92a62dc91..8d43300739 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -52,6 +51,9 @@ enum class mip_exploration_status_t { COMPLETED = 5, // The solver finished exploring the tree }; +template +class node_presolver_t; + template void upper_bound_callback(f_t upper_bound); @@ -64,7 +66,7 @@ struct diving_root_t { diving_root_t(mip_node_t&& node, const std::vector& lower, const std::vector& upper) - : node(std::move(node)), upper(upper), lower(lower) + : node(std::move(node)), lower(lower), upper(upper) { } diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index a84d64b369..1682e7a9c4 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -576,15 +576,7 @@ void convert_user_problem(const user_problem_t& user_problem, convert_greater_to_less(user_problem, row_sense, problem, greater_rows, less_rows); } - // At this point the problem representation is in the form: A*x {<=, =} b - // This is the time to run bound strengthening - constexpr bool run_bound_strengthening = false; - if constexpr (run_bound_strengthening) { - settings.log.printf("Running bound strengthening\n"); - csc_matrix_t Arow(1, 1, 1); - problem.A.transpose(Arow); - bound_strengthening(row_sense, settings, problem, Arow); - } + // bounds strenghtning was moved to node_presolve.hpp settings.log.debug( "equality rows %d less rows %d columns %d\n", equal_rows, less_rows, problem.num_cols); @@ -605,7 +597,7 @@ void convert_user_problem(const user_problem_t& user_problem, } if (problem.upper[j] < inf) { num_upper_bounds++; - vars_with_upper_bounds.push_back(j); + vars_with_upper_bounds.push_back(j) } } From f7a38bcfa44e7484b6c5b5d2a2ebc9b1dd5667a3 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 23 Oct 2025 19:39:35 +0200 Subject: [PATCH 29/86] fix compilation error --- cpp/src/dual_simplex/presolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 1682e7a9c4..f2439471af 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -597,7 +597,7 @@ void convert_user_problem(const user_problem_t& user_problem, } if (problem.upper[j] < inf) { num_upper_bounds++; - vars_with_upper_bounds.push_back(j) + vars_with_upper_bounds.push_back(j); } } From 649c8caa3054c07de8053ca9d48deec0df53b047 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 24 Oct 2025 11:00:30 +0200 Subject: [PATCH 30/86] Update cpp/src/dual_simplex/mip_node.hpp Co-authored-by: Rajesh Gandham --- cpp/src/dual_simplex/mip_node.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 3f2a3e4325..439832bec3 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -98,7 +98,7 @@ class mip_node_t { // Here we assume that we are traversing from the deepest node to the // root of the tree - void update_variable_bound(std::vector& lower, + void update_branched_variable_bounds(std::vector& lower, std::vector& upper, std::vector& bounds_changed) const { From 4afb71d8e58d8b05a415a4d40b2e30501873beeb Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 11:18:57 +0200 Subject: [PATCH 31/86] addresses reviewer's comments --- cpp/src/dual_simplex/branch_and_bound.cpp | 60 ++++++++-------- cpp/src/dual_simplex/branch_and_bound.hpp | 84 ++++------------------ cpp/src/dual_simplex/diving_queue.hpp | 88 +++++++++++++++++++++++ cpp/src/dual_simplex/node_presolve.cpp | 13 ++-- cpp/src/dual_simplex/node_presolve.hpp | 8 ++- cpp/src/dual_simplex/presolve.cpp | 2 +- cpp/src/dual_simplex/presolve.hpp | 2 + 7 files changed, 151 insertions(+), 106 deletions(-) create mode 100644 cpp/src/dual_simplex/diving_queue.hpp diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 88c218a1a9..34604b0096 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -201,6 +201,15 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) } } +inline const char* thread_type_symbol(thread_type_t type) +{ + switch (type) { + case thread_type_t::EXPLORATION: return "B "; + case thread_type_t::DIVING: return "D "; + default: return "U"; + } +} + } // namespace template @@ -497,7 +506,7 @@ template void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - char thread_type) + thread_type_t thread_type) { bool send_solution = false; i_t nodes_explored = stats_.nodes_explored; @@ -510,8 +519,8 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - settings_.log.printf("%c%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - thread_type, + settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + thread_type_symbol(thread_type), nodes_explored, nodes_unexplored, obj, @@ -557,7 +566,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod search_tree_t& search_tree, lp_problem_t& leaf_problem, node_presolver_t& presolver, - char thread_type, + thread_type_t thread_type, logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; @@ -661,7 +670,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod return node_status_t::TIME_LIMIT; } else { - if (thread_type == 'B') { + if (thread_type == thread_type_t::EXPLORATION) { lower_bound_ceiling_.fetch_min(node_ptr->lower_bound); log.printf( "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " @@ -703,14 +712,13 @@ void branch_and_bound_t::set_variable_bounds(mip_node_t* nod node->get_variable_bounds(lower, upper, bounds_changed); } else { - node->update_variable_bound(lower, upper, bounds_changed); + node->update_branched_variable_bounds(lower, upper, bounds_changed); } } template void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, search_tree_t* search_tree, - const csc_matrix_t& Arow, i_t initial_heap_size) { if (status_ != mip_exploration_status_t::RUNNING) { return; } @@ -723,7 +731,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_); + node_presolver_t presolver(leaf_problem, row_sense, var_types_); f_t lower_bound = node->lower_bound; f_t upper_bound = get_upper_bound(); @@ -781,8 +789,8 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod original_lp_.upper, true); - node_status_t node_status = - solve_node(node, *search_tree, leaf_problem, presolver, 'B', settings_.log); + node_status_t node_status = solve_node( + node, *search_tree, leaf_problem, presolver, thread_type_t::EXPLORATION, settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -794,10 +802,10 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase if (stats_.nodes_unexplored < initial_heap_size) { #pragma omp task - exploration_ramp_up(node->get_down_child(), search_tree, Arow, initial_heap_size); + exploration_ramp_up(node->get_down_child(), search_tree, initial_heap_size); #pragma omp task - exploration_ramp_up(node->get_up_child(), search_tree, Arow, initial_heap_size); + exploration_ramp_up(node->get_up_child(), search_tree, initial_heap_size); } else { // We've generated enough nodes, push further nodes onto the heap @@ -889,8 +897,8 @@ void branch_and_bound_t::explore_subtree(i_t task_id, original_lp_.upper, recompute); - node_status_t node_status = - solve_node(node_ptr, search_tree, leaf_problem, presolver, 'B', settings_.log); + node_status_t node_status = solve_node( + node_ptr, search_tree, leaf_problem, presolver, thread_type_t::EXPLORATION, settings_.log); recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { @@ -933,9 +941,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } template -void branch_and_bound_t::best_first_thread(i_t id, - search_tree_t& search_tree, - const csc_matrix_t& Arow) +void branch_and_bound_t::best_first_thread(i_t id, search_tree_t& search_tree) { f_t lower_bound = -inf; f_t upper_bound = inf; @@ -945,7 +951,7 @@ void branch_and_bound_t::best_first_thread(i_t id, // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_); + node_presolver_t presolver(leaf_problem, row_sense, var_types_); while (status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && @@ -994,7 +1000,7 @@ void branch_and_bound_t::best_first_thread(i_t id, } template -void branch_and_bound_t::diving_thread(const csc_matrix_t& Arow) +void branch_and_bound_t::diving_thread() { logger_t log; log.log = false; @@ -1002,7 +1008,7 @@ void branch_and_bound_t::diving_thread(const csc_matrix_t& A // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, Arow, var_types_); + node_presolver_t presolver(leaf_problem, row_sense, var_types_); while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { @@ -1043,7 +1049,7 @@ void branch_and_bound_t::diving_thread(const csc_matrix_t& A recompute); node_status_t node_status = - solve_node(node_ptr, subtree, leaf_problem, presolver, 'D', log); + solve_node(node_ptr, subtree, leaf_problem, presolver, thread_type_t::DIVING, log); if (node_status == node_status_t::TIME_LIMIT) { return; @@ -1219,9 +1225,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " "| Time |\n"); - csc_matrix_t Arow(1, 1, 1); - original_lp_.A.transpose(Arow); - stats_.nodes_explored = 0; stats_.nodes_unexplored = 2; stats_.nodes_since_last_log = 0; @@ -1230,6 +1233,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut min_diving_queue_size_ = 4 * settings_.num_diving_threads; status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; + original_lp_.A.transpose(original_lp_.Arow); #pragma omp parallel num_threads(settings_.num_threads) { @@ -1240,10 +1244,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t initial_size = 2 * settings_.num_threads; #pragma omp task - exploration_ramp_up(down_child, &search_tree, Arow, initial_size); + exploration_ramp_up(down_child, &search_tree, initial_size); #pragma omp task - exploration_ramp_up(up_child, &search_tree, Arow, initial_size); + exploration_ramp_up(up_child, &search_tree, initial_size); } #pragma omp barrier @@ -1254,12 +1258,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut (active_subtrees_ > 0 || get_heap_size() > 0)) { for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree, Arow); + best_first_thread(i, search_tree); } for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(Arow); + diving_thread(); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 8d43300739..0a1a556ec8 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -51,73 +52,21 @@ enum class mip_exploration_status_t { COMPLETED = 5, // The solver finished exploring the tree }; +// Indicate the search and variable selection algorithms used by the thread (See [1]). +// +// [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, +// Berlin, 2007. doi: 10.14279/depositonce-1634. +enum class thread_type_t { + EXPLORATION = 0, // Best-First + Plunging. Pseudocost branching + Martin's criteria. + DIVING = 1, +}; + template class node_presolver_t; template void upper_bound_callback(f_t upper_bound); -template -struct diving_root_t { - mip_node_t node; - std::vector lower; - std::vector upper; - - diving_root_t(mip_node_t&& node, - const std::vector& lower, - const std::vector& upper) - : node(std::move(node)), lower(lower), upper(upper) - { - } - - friend bool operator>(const diving_root_t& a, const diving_root_t& b) - { - return a.node.lower_bound > b.node.lower_bound; - } -}; - -// A min-heap for storing the starting nodes for the dives. -// This has a maximum size of 256, such that the container -// will discard the least promising node if the queue is full. -template -class dive_queue_t { - private: - std::vector> buffer; - static constexpr i_t max_size_ = 256; - - public: - dive_queue_t() { buffer.reserve(max_size_); } - - void push(diving_root_t&& node) - { - buffer.push_back(std::move(node)); - std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { buffer.pop_back(); } - } - - void emplace(mip_node_t&& node, - const std::vector& lower, - const std::vector& upper) - { - buffer.emplace_back(std::move(node), lower, upper); - std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { buffer.pop_back(); } - } - - diving_root_t pop() - { - std::pop_heap(buffer.begin(), buffer.end(), std::greater<>()); - diving_root_t node = std::move(buffer.back()); - buffer.pop_back(); - return node; - } - - i_t size() const { return buffer.size(); } - constexpr i_t max_size() const { return max_size_; } - const diving_root_t& top() const { return buffer.front(); } - void clear() { buffer.clear(); } -}; - template class branch_and_bound_t { public: @@ -205,7 +154,7 @@ class branch_and_bound_t { // Queue for storing the promising node for performing dives. omp_mutex_t mutex_dive_queue_; - dive_queue_t dive_queue_; + diving_queue_t dive_queue_; i_t min_diving_queue_size_; // Global status of the solver. @@ -223,7 +172,7 @@ class branch_and_bound_t { void add_feasible_solution(f_t leaf_objective, const std::vector& leaf_solution, i_t leaf_depth, - char thread_type); + thread_type_t thread_type); // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); @@ -240,7 +189,6 @@ class branch_and_bound_t { // there is enough unexplored nodes. This is done recursively using OpenMP tasks. void exploration_ramp_up(mip_node_t* node, search_tree_t* search_tree, - const csc_matrix_t& Arow, i_t initial_heap_size); // Explore the search tree using the best-first search with plunging strategy. @@ -252,20 +200,18 @@ class branch_and_bound_t { // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. - void best_first_thread(i_t id, - search_tree_t& search_tree, - const csc_matrix_t& Arow); + void best_first_thread(i_t id, search_tree_t& search_tree); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(const csc_matrix_t& Arow); + void diving_thread(); // Solve the LP relaxation of a leaf node and update the tree. node_status_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, node_presolver_t& presolver, - char thread_type, + thread_type_t thread_type, logger_t& log); // Sort the children based on the Martin's criteria. diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp new file mode 100644 index 0000000000..d86e5c79ca --- /dev/null +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +struct diving_root_t { + mip_node_t node; + std::vector lower; + std::vector upper; + + diving_root_t(mip_node_t&& node, + const std::vector& lower, + const std::vector& upper) + : node(std::move(node)), lower(lower), upper(upper) + { + } + + friend bool operator>(const diving_root_t& a, const diving_root_t& b) + { + return a.node.lower_bound > b.node.lower_bound; + } +}; + +// A min-heap for storing the starting nodes for the dives. +// This has a maximum size of 256, such that the container +// will discard the least promising node if the queue is full. +template +class diving_queue_t { + private: + std::vector> buffer; + static constexpr i_t max_size_ = 256; + + public: + diving_queue_t() { buffer.reserve(max_size_); } + + void push(diving_root_t&& node) + { + buffer.push_back(std::move(node)); + std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); + if (buffer.size() > max_size()) { buffer.pop_back(); } + } + + void emplace(mip_node_t&& node, + const std::vector& lower, + const std::vector& upper) + { + buffer.emplace_back(std::move(node), lower, upper); + std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); + if (buffer.size() > max_size()) { buffer.pop_back(); } + } + + diving_root_t pop() + { + std::pop_heap(buffer.begin(), buffer.end(), std::greater<>()); + diving_root_t node = std::move(buffer.back()); + buffer.pop_back(); + return node; + } + + i_t size() const { return buffer.size(); } + constexpr i_t max_size() const { return max_size_; } + const diving_root_t& top() const { return buffer.front(); } + void clear() { buffer.clear(); } +}; + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp index f05bfab5a5..a81cbad406 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -63,11 +63,10 @@ void print_bounds_stats(const std::vector& lower, template node_presolver_t::node_presolver_t(const lp_problem_t& problem, const std::vector& row_sense, - const csc_matrix_t& Arow, const std::vector& var_types) : bounds_changed(problem.num_cols, false), A(problem.A), - Arow(Arow), + Arow(problem.Arow), var_types(var_types), delta_min_activity(problem.num_rows), delta_max_activity(problem.num_rows), @@ -104,9 +103,9 @@ bool node_presolver_t::bound_strengthening( const i_t m = A.m; const i_t n = A.n; - std::vector constraint_changed(m, false); - std::vector variable_changed(n, false); - std::vector constraint_changed_next(m, false); + constraint_changed.assign(m, false); + variable_changed.assign(n, false); + constraint_changed_next.assign(m, false); for (i_t i = 0; i < bounds_changed.size(); ++i) { if (bounds_changed[i]) { @@ -119,8 +118,8 @@ bool node_presolver_t::bound_strengthening( } } - std::vector lower = lower_bounds; - std::vector upper = upper_bounds; + lower = lower_bounds; + upper = upper_bounds; print_bounds_stats(lower, upper, settings, "Initial bounds"); i_t iter = 0; diff --git a/cpp/src/dual_simplex/node_presolve.hpp b/cpp/src/dual_simplex/node_presolve.hpp index 17483d4ff4..1bb7fb899f 100644 --- a/cpp/src/dual_simplex/node_presolve.hpp +++ b/cpp/src/dual_simplex/node_presolve.hpp @@ -27,7 +27,6 @@ class node_presolver_t { // For pure LP bounds strengthening, var_types should be defaulted (i.e. left empty) node_presolver_t(const lp_problem_t& problem, const std::vector& row_sense, - const csc_matrix_t& Arow, const std::vector& var_types); bool bound_strengthening(std::vector& lower_bounds, @@ -41,6 +40,13 @@ class node_presolver_t { const csc_matrix_t& Arow; const std::vector& var_types; + std::vector constraint_changed; + std::vector variable_changed; + std::vector constraint_changed_next; + + std::vector lower; + std::vector upper; + std::vector delta_min_activity; std::vector delta_max_activity; std::vector constraint_lb; diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index f2439471af..29e63d8676 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -576,7 +576,7 @@ void convert_user_problem(const user_problem_t& user_problem, convert_greater_to_less(user_problem, row_sense, problem, greater_rows, less_rows); } - // bounds strenghtning was moved to node_presolve.hpp + // bounds strengthening was moved to node_presolve.hpp settings.log.debug( "equality rows %d less rows %d columns %d\n", equal_rows, less_rows, problem.num_cols); diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index 538ca5dffe..b0d7b0aa23 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -33,6 +33,7 @@ struct lp_problem_t { num_cols(n), objective(n), A(m, n, nz), + Arow(1, 1, 1), rhs(m), lower(n), upper(n), @@ -44,6 +45,7 @@ struct lp_problem_t { i_t num_cols; std::vector objective; csc_matrix_t A; + csc_matrix_t Arow; std::vector rhs; std::vector lower; std::vector upper; From 1c92230eb7d927c16bcad6795a3d26fb771a43e5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 11:29:00 +0200 Subject: [PATCH 32/86] fix style --- cpp/src/dual_simplex/mip_node.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 439832bec3..39544180fd 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -99,8 +99,8 @@ class mip_node_t { // Here we assume that we are traversing from the deepest node to the // root of the tree void update_branched_variable_bounds(std::vector& lower, - std::vector& upper, - std::vector& bounds_changed) const + std::vector& upper, + std::vector& bounds_changed) const { assert(branch_var >= 0); assert(lower.size() > branch_var); From bbd410b11ea2d503e82cf434a843f528f53989a9 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 12:04:08 +0200 Subject: [PATCH 33/86] small refactor. added missing recompute flag to diving. --- cpp/src/dual_simplex/branch_and_bound.cpp | 109 ++++++++++------------ cpp/src/dual_simplex/branch_and_bound.hpp | 11 +-- 2 files changed, 50 insertions(+), 70 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 34604b0096..61aebdacfb 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -567,6 +567,9 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod lp_problem_t& leaf_problem, node_presolver_t& presolver, thread_type_t thread_type, + bool recompute, + const std::vector& root_lower, + const std::vector& root_upper, logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; @@ -582,6 +585,20 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); + // Reset the bound_changed markers + std::fill(presolver.bounds_changed.begin(), presolver.bounds_changed.end(), false); + + // Set the correct bounds for the leaf problem + if (recompute) { + leaf_problem.lower = root_lower; + leaf_problem.upper = root_upper; + node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, presolver.bounds_changed); + + } else { + node_ptr->update_branched_variable_bounds( + leaf_problem.lower, leaf_problem.upper, presolver.bounds_changed); + } + bool feasible = presolver.bound_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); @@ -687,35 +704,6 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod } } -template -void branch_and_bound_t::set_variable_bounds(mip_node_t* node, - std::vector& lower, - std::vector& upper, - std::vector& bounds_changed, - const std::vector& root_lower, - const std::vector& root_upper, - bool recompute) -{ - assert(bounds_changed.size() == original_lp_.lower.size()); - assert(lower.size() == original_lp_.lower.size()); - assert(upper.size() == original_lp_.upper.size()); - assert(root_lower.size() == original_lp_.lower.size()); - assert(root_upper.size() == original_lp_.upper.size()); - - // Reset the bound_changed markers - std::fill(bounds_changed.begin(), bounds_changed.end(), false); - - // Recompute the bounds - if (recompute) { - lower = root_lower; - upper = root_upper; - node->get_variable_bounds(lower, upper, bounds_changed); - - } else { - node->update_branched_variable_bounds(lower, upper, bounds_changed); - } -} - template void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, search_tree_t* search_tree, @@ -780,17 +768,15 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod return; } - // Set the correct bounds for the leaf problem - set_variable_bounds(node, - leaf_problem.lower, - leaf_problem.upper, - presolver.bounds_changed, - original_lp_.lower, - original_lp_.upper, - true); - - node_status_t node_status = solve_node( - node, *search_tree, leaf_problem, presolver, thread_type_t::EXPLORATION, settings_.log); + node_status_t node_status = solve_node(node, + *search_tree, + leaf_problem, + presolver, + thread_type_t::EXPLORATION, + true, + original_lp_.lower, + original_lp_.upper, + settings_.log); if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -888,17 +874,16 @@ void branch_and_bound_t::explore_subtree(i_t task_id, return; } - // Set the correct bounds for the leaf problem - set_variable_bounds(node_ptr, - leaf_problem.lower, - leaf_problem.upper, - presolver.bounds_changed, - original_lp_.lower, - original_lp_.upper, - recompute); - - node_status_t node_status = solve_node( - node_ptr, search_tree, leaf_problem, presolver, thread_type_t::EXPLORATION, settings_.log); + node_status_t node_status = solve_node(node_ptr, + search_tree, + leaf_problem, + presolver, + thread_type_t::EXPLORATION, + recompute, + original_lp_.lower, + original_lp_.upper, + settings_.log); + recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { @@ -1039,17 +1024,17 @@ void branch_and_bound_t::diving_thread() if (toc(stats_.start_time) > settings_.time_limit) { return; } - // Set the correct bounds for the leaf problem - set_variable_bounds(node_ptr, - leaf_problem.lower, - leaf_problem.upper, - presolver.bounds_changed, - start_node->lower, - start_node->upper, - recompute); - - node_status_t node_status = - solve_node(node_ptr, subtree, leaf_problem, presolver, thread_type_t::DIVING, log); + node_status_t node_status = solve_node(node_ptr, + subtree, + leaf_problem, + presolver, + thread_type_t::DIVING, + recompute, + start_node->lower, + start_node->upper, + log); + + recompute = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { return; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 0a1a556ec8..fd4721b98c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -177,14 +177,6 @@ class branch_and_bound_t { // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); - void set_variable_bounds(mip_node_t* node, - std::vector& lower, - std::vector& upper, - std::vector& bounds_changed, - const std::vector& root_lower, - const std::vector& root_upper, - bool recompute); - // Ramp-up phase of the solver, where we greedily expand the tree until // there is enough unexplored nodes. This is done recursively using OpenMP tasks. void exploration_ramp_up(mip_node_t* node, @@ -212,6 +204,9 @@ class branch_and_bound_t { lp_problem_t& leaf_problem, node_presolver_t& presolver, thread_type_t thread_type, + bool recompute, + const std::vector& root_lower, + const std::vector& root_upper, logger_t& log); // Sort the children based on the Martin's criteria. From 8c420c4abd70f2546ff92f0ff06d37e2f07165c4 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 13:05:46 +0200 Subject: [PATCH 34/86] small fixes --- cpp/src/dual_simplex/mip_node.hpp | 2 +- cpp/src/dual_simplex/node_presolve.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 39544180fd..bdd97ad0b5 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -91,7 +91,7 @@ class mip_node_t { mip_node_t* parent_ptr = parent; while (parent_ptr != nullptr && parent_ptr->node_id != 0) { - parent_ptr->update_variable_bound(lower, upper, bounds_changed); + parent_ptr->update_branched_variable_bounds(lower, upper, bounds_changed); parent_ptr = parent_ptr->parent; } } diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp index a81cbad406..fb59c56932 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -17,6 +17,9 @@ #include +#include +#include + namespace cuopt::linear_programming::dual_simplex { template @@ -206,8 +209,8 @@ bool node_presolver_t::bound_strengthening( new_ub = std::floor(new_ub + settings.integer_tol); } - bool lb_updated = abs(new_lb - old_lb) > 1e3 * settings.primal_tol; - bool ub_updated = abs(new_ub - old_ub) > 1e3 * settings.primal_tol; + bool lb_updated = std::abs(new_lb - old_lb) > 1e3 * settings.primal_tol; + bool ub_updated = std::abs(new_ub - old_ub) > 1e3 * settings.primal_tol; new_lb = std::max(new_lb, lower_bounds[k]); new_ub = std::min(new_ub, upper_bounds[k]); From 5bd70e1811103b604fdc1f7a5c5963931f7ebe7d Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 13:11:10 +0200 Subject: [PATCH 35/86] small fix --- cpp/src/dual_simplex/mip_node.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index bdd97ad0b5..cfb491caf0 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -87,7 +87,7 @@ class mip_node_t { std::vector& upper, std::vector& bounds_changed) const { - update_variable_bound(lower, upper, bounds_changed); + update_branched_variable_bounds(lower, upper, bounds_changed); mip_node_t* parent_ptr = parent; while (parent_ptr != nullptr && parent_ptr->node_id != 0) { From 56747e5f2f3d30829c1fe2231b2c5586c7e888b6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 13:14:40 +0200 Subject: [PATCH 36/86] fix logs --- cpp/src/dual_simplex/branch_and_bound.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 61aebdacfb..d435e21718 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -204,8 +204,8 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) inline const char* thread_type_symbol(thread_type_t type) { switch (type) { - case thread_type_t::EXPLORATION: return "B "; - case thread_type_t::DIVING: return "D "; + case thread_type_t::EXPLORATION: return "B"; + case thread_type_t::DIVING: return "D"; default: return "U"; } } @@ -519,7 +519,7 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + settings_.log.printf("%c%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", thread_type_symbol(thread_type), nodes_explored, nodes_unexplored, From c7facc6e2e77be0a97ea19ba3b4822748d77671e Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 13:19:03 +0200 Subject: [PATCH 37/86] fixed log --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d435e21718..06e6389388 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -519,7 +519,7 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - settings_.log.printf("%c%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", thread_type_symbol(thread_type), nodes_explored, nodes_unexplored, From 28f55f0e9d7e4bc18244f428d47afd5c6eb38e88 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 13:33:39 +0200 Subject: [PATCH 38/86] fixed bugs after merge --- cpp/src/dual_simplex/branch_and_bound.cpp | 113 +++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 5 +- 2 files changed, 63 insertions(+), 55 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 33a06bd116..7ffc69ddf5 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -612,30 +612,31 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; - lp_status = dual_phase2_with_basis_update(2, - 0, - lp_start_time, - leaf_problem, - lp_settings, - leaf_vstatus, - ft, - basic_list, - nonbasic_list, - leaf_solution, - node_iter, - leaf_edge_norms); + lp_status = dual_phase2_with_advanced_basis(2, + 0, + recompute, + lp_start_time, + leaf_problem, + lp_settings, + leaf_vstatus, + ft, + basic_list, + nonbasic_list, + leaf_solution, + node_iter, + leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); - lp_status_t second_status = solve_linear_program_with_basis_update(leaf_problem, - lp_start_time, - lp_settings, - leaf_solution, - ft, - basic_list, - nonbasic_list, - leaf_vstatus, - leaf_edge_norms); + lp_status_t second_status = solve_linear_program_with_advanced_basis(leaf_problem, + lp_start_time, + lp_settings, + leaf_solution, + ft, + basic_list, + nonbasic_list, + leaf_vstatus, + leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } @@ -730,11 +731,6 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // to repair the heuristic solution. repair_heuristic_solutions(); - // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp_; - std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, var_types_); - f_t lower_bound = node->lower_bound; f_t upper_bound = get_upper_bound(); f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); @@ -782,6 +778,11 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod return; } + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp_; + std::vector row_sense; + node_presolver_t presolver(leaf_problem, row_sense, var_types_); + const i_t m = leaf_problem.num_rows; basis_update_mpf_t basis_update(m, settings_.refactor_frequency); std::vector basic_list(m); @@ -830,14 +831,12 @@ void branch_and_bound_t::explore_subtree(i_t task_id, mip_node_t* start_node, search_tree_t& search_tree, lp_problem_t& leaf_problem, - node_presolver_t& presolver) + node_presolver_t& presolver, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list) { - const i_t m = leaf_problem.num_rows; bool recompute = true; - basis_update_mpf_t basis_update(m, settings_.refactor_frequency); - std::vector basic_list(m); - std::vector nonbasic_list; - std::deque*> stack; stack.push_front(start_node); @@ -956,7 +955,8 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } template -void branch_and_bound_t::best_first_thread(i_t id, search_tree_t& search_tree) +void branch_and_bound_t::best_first_thread(i_t task_id, + search_tree_t& search_tree) { f_t lower_bound = -inf; f_t upper_bound = inf; @@ -968,32 +968,45 @@ void branch_and_bound_t::best_first_thread(i_t id, search_tree_t row_sense; node_presolver_t presolver(leaf_problem, row_sense, var_types_); + const i_t m = leaf_problem.num_rows; + basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + std::vector basic_list(m); + std::vector nonbasic_list; + while (status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_subtrees_ > 0 || get_heap_size() > 0)) { - mip_node_t* node_ptr = nullptr; + mip_node_t* start_node = nullptr; // If there any node left in the heap, we pop the top node and explore it. mutex_heap_.lock(); if (heap_.size() > 0) { - node_ptr = heap_.top(); + start_node = heap_.top(); heap_.pop(); active_subtrees_++; } mutex_heap_.unlock(); - if (node_ptr != nullptr) { - if (get_upper_bound() < node_ptr->lower_bound) { + if (start_node != nullptr) { + if (get_upper_bound() < start_node->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound - search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); - search_tree.update(node_ptr, node_status_t::FATHOMED); + search_tree.graphviz_node(settings_.log, start_node, "cutoff", start_node->lower_bound); + search_tree.update(start_node, node_status_t::FATHOMED); active_subtrees_--; continue; } // Best-first search with plunging - explore_subtree(id, node_ptr, search_tree, leaf_problem, presolver); + explore_subtree(task_id, + start_node, + search_tree, + leaf_problem, + presolver, + basis_update, + basic_list, + nonbasic_list); + active_subtrees_--; } @@ -1009,7 +1022,7 @@ void branch_and_bound_t::best_first_thread(i_t id, search_tree_t::diving_thread() std::vector row_sense; node_presolver_t presolver(leaf_problem, row_sense, var_types_); + const i_t m = leaf_problem.num_rows; + basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + std::vector basic_list(m); + std::vector nonbasic_list; + while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { std::optional> start_node; @@ -1037,19 +1055,6 @@ void branch_and_bound_t::diving_thread() if (get_upper_bound() < start_node->node.lower_bound) { continue; } bool recompute = true; - const i_t m = leaf_problem.num_rows; - - basis_update_mpf_t basis_update(m, settings_.refactor_frequency); - std::vector basic_list(m); - std::vector nonbasic_list; - - bool recompute = true; - const i_t m = leaf_problem.num_rows; - - basis_update_mpf_t basis_update(m, settings_.refactor_frequency); - std::vector basic_list(m); - std::vector nonbasic_list; - search_tree_t subtree(std::move(start_node->node)); std::deque*> stack; stack.push_front(&subtree.root); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index c88b4a109a..f8e31c0824 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -188,7 +188,10 @@ class branch_and_bound_t { mip_node_t* start_node, search_tree_t& search_tree, lp_problem_t& leaf_problem, - node_presolver_t& presolver); + node_presolver_t& presolver, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. From eae9ca52e6c4cbe49c891698c3a83a6c5a60d1db Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 14:23:32 +0200 Subject: [PATCH 39/86] fixed crash due to incorrect nonbasic list --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- cpp/src/dual_simplex/phase2.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 7ffc69ddf5..89c5fa86e6 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -627,7 +627,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.printf("Numerical issue node %d. 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(leaf_problem, lp_start_time, lp_settings, diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 098c3b6e2b..48d4ad1509 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2245,6 +2245,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, phase2::bound_info(lp, settings); if (initialize_basis) { std::vector superbasic_list; + nonbasic_list.clear(); get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); From 382cbe8b6d24a6cfd577019225353fd22e752666 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 16:33:15 +0200 Subject: [PATCH 40/86] fixed diving queue. added asserts. --- cpp/src/dual_simplex/diving_queue.hpp | 26 +++++++++++++++++++++++--- cpp/src/dual_simplex/mip_node.hpp | 1 + cpp/src/dual_simplex/node_presolve.cpp | 24 ++++++++++++------------ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp index d86e5c79ca..8c5d1884c4 100644 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -41,6 +41,11 @@ struct diving_root_t { { return a.node.lower_bound > b.node.lower_bound; } + + friend bool operator<(const diving_root_t& a, const diving_root_t& b) + { + return a.node.lower_bound > b.node.lower_bound; + } }; // A min-heap for storing the starting nodes for the dives. @@ -52,6 +57,14 @@ class diving_queue_t { std::vector> buffer; static constexpr i_t max_size_ = 256; + void discard_worst_node() + { + auto max_it = std::max_element(buffer.begin(), buffer.end(), std::less<>()); + std::swap(*max_it, buffer.back()); + buffer.pop_back(); + std::make_heap(buffer.begin(), buffer.end(), std::greater<>()); + } + public: diving_queue_t() { buffer.reserve(max_size_); } @@ -59,7 +72,7 @@ class diving_queue_t { { buffer.push_back(std::move(node)); std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { buffer.pop_back(); } + if (buffer.size() > max_size()) { discard_worst_node(); } } void emplace(mip_node_t&& node, @@ -68,11 +81,12 @@ class diving_queue_t { { buffer.emplace_back(std::move(node), lower, upper); std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { buffer.pop_back(); } + if (buffer.size() > max_size()) { discard_worst_node(); } } diving_root_t pop() { + assert(!buffer.empty() && "Cannot pop from an empty queue!"); std::pop_heap(buffer.begin(), buffer.end(), std::greater<>()); diving_root_t node = std::move(buffer.back()); buffer.pop_back(); @@ -81,7 +95,13 @@ class diving_queue_t { i_t size() const { return buffer.size(); } constexpr i_t max_size() const { return max_size_; } - const diving_root_t& top() const { return buffer.front(); } + + const diving_root_t& top() const + { + assert(!buffer.empty() && "Cannot get top from an empty queue!"); + return buffer.front(); + } + void clear() { buffer.clear(); } }; diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index cfb491caf0..d6c33ffbcf 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -105,6 +105,7 @@ class mip_node_t { assert(branch_var >= 0); assert(lower.size() > branch_var); assert(upper.size() > branch_var); + assert(bounds_changed.size() > branch_var); // If the bounds have already been updated on another node, // skip this node as it contains a less tight bounds. diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp index fb59c56932..65a76eeeb1 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -252,21 +252,21 @@ bool node_presolver_t::bound_strengthening( int num_ub_changed = 0; for (i_t i = 0; i < n; ++i) { - if (lower[i] > problem.lower[i] + settings.primal_tol || - (!std::isfinite(problem.lower[i]) && std::isfinite(lower[i]))) { + if (lower[i] > lower_bounds[i] + settings.primal_tol || + (!std::isfinite(lower_bounds[i]) && std::isfinite(lower[i]))) { num_lb_changed++; - lb_change += - std::isfinite(problem.lower[i]) - ? (lower[i] - problem.lower[i]) / (1e-6 + std::max(abs(lower[i]), abs(problem.lower[i]))) - : 1.0; + lb_change += std::isfinite(lower_bounds[i]) + ? (lower[i] - lower_bounds[i]) / + (1e-6 + std::max(std::abs(lower[i]), std::abs(lower_bounds[i]))) + : 1.0; } - if (upper[i] < problem.upper[i] - settings.primal_tol || - (!std::isfinite(problem.upper[i]) && std::isfinite(upper[i]))) { + if (upper[i] < upper_bounds[i] - settings.primal_tol || + (!std::isfinite(upper_bounds[i]) && std::isfinite(upper[i]))) { num_ub_changed++; - ub_change += - std::isfinite(problem.upper[i]) - ? (problem.upper[i] - upper[i]) / (1e-6 + std::max(abs(problem.upper[i]), abs(upper[i]))) - : 1.0; + ub_change += std::isfinite(upper_bounds[i]) + ? (upper_bounds[i] - upper[i]) / + (1e-6 + std::max(std::abs(upper_bounds[i]), std::abs(upper[i]))) + : 1.0; } } From 8c2e06e5ff377714d612e5232f64c937d785dcca Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 24 Oct 2025 16:44:15 +0200 Subject: [PATCH 41/86] fixed typo --- cpp/src/dual_simplex/diving_queue.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp index 8c5d1884c4..9db7ebfa78 100644 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -44,7 +44,7 @@ struct diving_root_t { friend bool operator<(const diving_root_t& a, const diving_root_t& b) { - return a.node.lower_bound > b.node.lower_bound; + return a.node.lower_bound < b.node.lower_bound; } }; From 12eb12d795df8c064b55cdbcdda2b8530b3a8f92 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 27 Oct 2025 09:56:13 +0100 Subject: [PATCH 42/86] minimize code changes --- cpp/src/dual_simplex/diving_queue.hpp | 26 +++----------------------- cpp/src/dual_simplex/mip_node.hpp | 1 - cpp/src/dual_simplex/node_presolve.cpp | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp index 9db7ebfa78..d86e5c79ca 100644 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -41,11 +41,6 @@ struct diving_root_t { { return a.node.lower_bound > b.node.lower_bound; } - - friend bool operator<(const diving_root_t& a, const diving_root_t& b) - { - return a.node.lower_bound < b.node.lower_bound; - } }; // A min-heap for storing the starting nodes for the dives. @@ -57,14 +52,6 @@ class diving_queue_t { std::vector> buffer; static constexpr i_t max_size_ = 256; - void discard_worst_node() - { - auto max_it = std::max_element(buffer.begin(), buffer.end(), std::less<>()); - std::swap(*max_it, buffer.back()); - buffer.pop_back(); - std::make_heap(buffer.begin(), buffer.end(), std::greater<>()); - } - public: diving_queue_t() { buffer.reserve(max_size_); } @@ -72,7 +59,7 @@ class diving_queue_t { { buffer.push_back(std::move(node)); std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { discard_worst_node(); } + if (buffer.size() > max_size()) { buffer.pop_back(); } } void emplace(mip_node_t&& node, @@ -81,12 +68,11 @@ class diving_queue_t { { buffer.emplace_back(std::move(node), lower, upper); std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { discard_worst_node(); } + if (buffer.size() > max_size()) { buffer.pop_back(); } } diving_root_t pop() { - assert(!buffer.empty() && "Cannot pop from an empty queue!"); std::pop_heap(buffer.begin(), buffer.end(), std::greater<>()); diving_root_t node = std::move(buffer.back()); buffer.pop_back(); @@ -95,13 +81,7 @@ class diving_queue_t { i_t size() const { return buffer.size(); } constexpr i_t max_size() const { return max_size_; } - - const diving_root_t& top() const - { - assert(!buffer.empty() && "Cannot get top from an empty queue!"); - return buffer.front(); - } - + const diving_root_t& top() const { return buffer.front(); } void clear() { buffer.clear(); } }; diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index d6c33ffbcf..cfb491caf0 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -105,7 +105,6 @@ class mip_node_t { assert(branch_var >= 0); assert(lower.size() > branch_var); assert(upper.size() > branch_var); - assert(bounds_changed.size() > branch_var); // If the bounds have already been updated on another node, // skip this node as it contains a less tight bounds. diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/node_presolve.cpp index 65a76eeeb1..fb59c56932 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/node_presolve.cpp @@ -252,21 +252,21 @@ bool node_presolver_t::bound_strengthening( int num_ub_changed = 0; for (i_t i = 0; i < n; ++i) { - if (lower[i] > lower_bounds[i] + settings.primal_tol || - (!std::isfinite(lower_bounds[i]) && std::isfinite(lower[i]))) { + if (lower[i] > problem.lower[i] + settings.primal_tol || + (!std::isfinite(problem.lower[i]) && std::isfinite(lower[i]))) { num_lb_changed++; - lb_change += std::isfinite(lower_bounds[i]) - ? (lower[i] - lower_bounds[i]) / - (1e-6 + std::max(std::abs(lower[i]), std::abs(lower_bounds[i]))) - : 1.0; + lb_change += + std::isfinite(problem.lower[i]) + ? (lower[i] - problem.lower[i]) / (1e-6 + std::max(abs(lower[i]), abs(problem.lower[i]))) + : 1.0; } - if (upper[i] < upper_bounds[i] - settings.primal_tol || - (!std::isfinite(upper_bounds[i]) && std::isfinite(upper[i]))) { + if (upper[i] < problem.upper[i] - settings.primal_tol || + (!std::isfinite(problem.upper[i]) && std::isfinite(upper[i]))) { num_ub_changed++; - ub_change += std::isfinite(upper_bounds[i]) - ? (upper_bounds[i] - upper[i]) / - (1e-6 + std::max(std::abs(upper_bounds[i]), std::abs(upper[i]))) - : 1.0; + ub_change += + std::isfinite(problem.upper[i]) + ? (problem.upper[i] - upper[i]) / (1e-6 + std::max(abs(problem.upper[i]), abs(upper[i]))) + : 1.0; } } From a1caad84462dc52aa681d1b5751d6b278e4df089 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 27 Oct 2025 10:23:52 +0100 Subject: [PATCH 43/86] reserve memory space for nonbasic list --- cpp/src/dual_simplex/phase2.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 48d4ad1509..995a6ed581 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2246,6 +2246,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, if (initialize_basis) { std::vector superbasic_list; nonbasic_list.clear(); + nonbasic_list.reserve(n - m); get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); From 8b294be5dd639f5b812f48fa2a1d37fc0b765fe3 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 27 Oct 2025 17:25:22 +0100 Subject: [PATCH 44/86] fixed starting bounds for diving nodes. replaced rounding direction with an enum. --- cpp/src/dual_simplex/branch_and_bound.cpp | 25 ++++++++---- cpp/src/dual_simplex/diving_queue.hpp | 18 ++++---- cpp/src/dual_simplex/mip_node.hpp | 50 ++++++++++++++--------- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 06e6389388..275d1c358d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -620,7 +620,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.printf("Numerical issue node %d. 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_advanced( leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); @@ -906,8 +906,12 @@ void branch_and_bound_t::explore_subtree(i_t task_id, // This lead to a SIGSEGV. Although, in this case, it // would be better if we discard the node instead. if (get_heap_size() > settings_.num_bfs_threads) { + std::vector lower = original_lp_.lower; + std::vector upper = original_lp_.upper; + node->get_variable_bounds(lower, upper, presolver.bounds_changed); + mutex_dive_queue_.lock(); - dive_queue_.emplace(node->detach_copy(), leaf_problem.lower, leaf_problem.upper); + dive_queue_.emplace(node->detach_copy(), std::move(lower), std::move(upper)); mutex_dive_queue_.unlock(); } @@ -1051,10 +1055,15 @@ void branch_and_bound_t::diving_thread() // lowest possible point and move to the queue, so it can // be picked by another thread. if (dive_queue_.size() < min_diving_queue_size_) { - mutex_dive_queue_.lock(); mip_node_t* new_node = stack.back(); stack.pop_back(); - dive_queue_.emplace(new_node->detach_copy(), leaf_problem.lower, leaf_problem.upper); + + std::vector lower = start_node->lower; + std::vector upper = start_node->upper; + new_node->get_variable_bounds(lower, upper, presolver.bounds_changed); + + mutex_dive_queue_.lock(); + dive_queue_.emplace(new_node->detach_copy(), std::move(lower), std::move(upper)); mutex_dive_queue_.unlock(); } } @@ -1200,11 +1209,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - settings_.log.printf( - "Exploring the B&B tree using %d best-first threads and %d diving threads (%d threads)\n", - settings_.num_bfs_threads, - settings_.num_diving_threads, - settings_.num_threads); + settings_.log.printf("Exploring the B&B tree using %d best-first threads and %d diving threads\n", + settings_.num_bfs_threads, + settings_.num_diving_threads); settings_.log.printf( " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp index d86e5c79ca..4b4610824c 100644 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -30,10 +30,8 @@ struct diving_root_t { std::vector lower; std::vector upper; - diving_root_t(mip_node_t&& node, - const std::vector& lower, - const std::vector& upper) - : node(std::move(node)), lower(lower), upper(upper) + diving_root_t(mip_node_t&& node, std::vector&& lower, std::vector&& upper) + : node(std::move(node)), lower(std::move(lower)), upper(std::move(upper)) { } @@ -50,7 +48,7 @@ template class diving_queue_t { private: std::vector> buffer; - static constexpr i_t max_size_ = 256; + static constexpr i_t max_size_ = INT16_MAX; public: diving_queue_t() { buffer.reserve(max_size_); } @@ -59,16 +57,14 @@ class diving_queue_t { { buffer.push_back(std::move(node)); std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { buffer.pop_back(); } + if (buffer.size() > max_size() - 1) { buffer.pop_back(); } } - void emplace(mip_node_t&& node, - const std::vector& lower, - const std::vector& upper) + void emplace(mip_node_t&& node, std::vector&& lower, std::vector&& upper) { - buffer.emplace_back(std::move(node), lower, upper); + buffer.emplace_back(std::move(node), std::move(lower), std::move(upper)); std::push_heap(buffer.begin(), buffer.end(), std::greater<>()); - if (buffer.size() > max_size()) { buffer.pop_back(); } + if (buffer.size() > max_size() - 1) { buffer.pop_back(); } } diving_root_t pop() diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index cfb491caf0..1c51c09d12 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -38,6 +38,8 @@ enum class node_status_t : int { TIME_LIMIT = 6 // Time out during the LP relaxation }; +enum class round_dir_t { NONE = -1, DOWN = 0, UP = 1 }; + bool inactive_status(node_status_t status); template @@ -50,7 +52,7 @@ class mip_node_t { parent(nullptr), node_id(0), branch_var(-1), - branch_dir(-1), + branch_dir(round_dir_t::NONE), vstatus(basis) { children[0] = nullptr; @@ -61,7 +63,7 @@ class mip_node_t { mip_node_t* parent_node, i_t node_num, i_t branch_variable, - i_t branch_direction, + round_dir_t branch_direction, f_t branch_var_value, const std::vector& basis) : status(node_status_t::ACTIVE), @@ -75,12 +77,12 @@ class mip_node_t { vstatus(basis) { - branch_var_lower = - branch_direction == 0 ? problem.lower[branch_var] : std::ceil(branch_var_value); - branch_var_upper = - branch_direction == 0 ? std::floor(branch_var_value) : problem.upper[branch_var]; - children[0] = nullptr; - children[1] = nullptr; + branch_var_lower = branch_direction == round_dir_t::DOWN ? problem.lower[branch_var] + : std::ceil(branch_var_value); + branch_var_upper = branch_direction == round_dir_t::DOWN ? std::floor(branch_var_value) + : problem.upper[branch_var]; + children[0] = nullptr; + children[1] = nullptr; } void get_variable_bounds(std::vector& lower, @@ -105,6 +107,7 @@ class mip_node_t { assert(branch_var >= 0); assert(lower.size() > branch_var); assert(upper.size() > branch_var); + assert(bounds_changed.size() > branch_var); // If the bounds have already been updated on another node, // skip this node as it contains a less tight bounds. @@ -231,7 +234,7 @@ class mip_node_t { i_t depth; i_t node_id; i_t branch_var; - i_t branch_dir; + round_dir_t branch_dir; f_t branch_var_lower; f_t branch_var_upper; f_t fractional_val; @@ -296,17 +299,26 @@ class search_tree_t { { i_t id = num_nodes.fetch_add(2); - // down child - auto down_child = std::make_unique>( - original_lp, parent_node, ++id, branch_var, 0, fractional_val, parent_vstatus); - - graphviz_edge(log, parent_node, down_child.get(), branch_var, 0, std::floor(fractional_val)); + auto down_child = std::make_unique>(original_lp, + parent_node, + ++id, + branch_var, + round_dir_t::DOWN, + fractional_val, + parent_vstatus); + + graphviz_edge(log, + parent_node, + down_child.get(), + branch_var, + round_dir_t::DOWN, + std::floor(fractional_val)); - // up child auto up_child = std::make_unique>( - original_lp, parent_node, ++id, branch_var, 1, fractional_val, parent_vstatus); + original_lp, parent_node, ++id, branch_var, round_dir_t::UP, fractional_val, parent_vstatus); - graphviz_edge(log, parent_node, up_child.get(), branch_var, 1, std::ceil(fractional_val)); + graphviz_edge( + log, parent_node, up_child.get(), branch_var, round_dir_t::UP, std::ceil(fractional_val)); assert(parent_vstatus.size() == original_lp.num_cols); parent_node->add_children(std::move(down_child), @@ -327,7 +339,7 @@ class search_tree_t { const mip_node_t* origin_ptr, const mip_node_t* dest_ptr, const i_t branch_var, - const i_t branch_dir, + round_dir_t branch_dir, const f_t bound) { if (write_graphviz) { @@ -335,7 +347,7 @@ class search_tree_t { origin_ptr->node_id, dest_ptr->node_id, branch_var, - branch_dir == 0 ? "<=" : ">=", + branch_dir == round_dir_t::DOWN ? "<=" : ">=", bound); } } From 27f9264eba1b86246a123fb43cd80c1e7795aa00 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 28 Oct 2025 10:35:13 +0100 Subject: [PATCH 45/86] fix missing enum --- cpp/src/dual_simplex/pseudo_costs.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 4bd9590e16..1a10dc73e2 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -210,10 +210,10 @@ void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_pt { mutex.lock(); const f_t change_in_obj = leaf_objective - node_ptr->lower_bound; - const f_t frac = node_ptr->branch_dir == 0 + const f_t frac = node_ptr->branch_dir == round_dir_t::DOWN ? node_ptr->fractional_val - std::floor(node_ptr->fractional_val) : std::ceil(node_ptr->fractional_val) - node_ptr->fractional_val; - if (node_ptr->branch_dir == 0) { + if (node_ptr->branch_dir == round_dir_t::DOWN) { pseudo_cost_sum_down[node_ptr->branch_var] += change_in_obj / frac; pseudo_cost_num_down[node_ptr->branch_var]++; } else { From bf142d5c9b2a5e94a01c8e57ab9eb6ff67f3abe9 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 28 Oct 2025 12:02:35 +0100 Subject: [PATCH 46/86] fixed typo --- cpp/src/dual_simplex/diving_queue.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp index 4b4610824c..57a8bacf19 100644 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include @@ -42,7 +43,7 @@ struct diving_root_t { }; // A min-heap for storing the starting nodes for the dives. -// This has a maximum size of 256, such that the container +// This has a maximum size of INT16_MAX, such that the container // will discard the least promising node if the queue is full. template class diving_queue_t { From b8e295da257dfec49a1e23217a939de5997ad7c6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 29 Oct 2025 13:03:13 +0100 Subject: [PATCH 47/86] adjusting refactor frequency --- 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 59e6dc7bbd..4f23389eca 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -72,7 +72,7 @@ struct simplex_solver_settings_t { ordering(-1), barrier_dual_initial_point(-1), crossover(false), - refactor_frequency(100), + refactor_frequency(50), iteration_log_frequency(1000), first_iteration_log(2), num_threads(omp_get_max_threads() - 1), From 8a00e9359d9532748d57722e4e52e58c436a9729 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 29 Oct 2025 16:37:29 +0100 Subject: [PATCH 48/86] removed debug leftover --- cpp/src/mip/problem/problem.cu | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 91836dd102..79b39912fc 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -1075,7 +1075,6 @@ void problem_t::set_implied_integers(const std::vector& implied_i { raft::common::nvtx::range fun_scope("set_implied_integers"); auto d_indices = cuopt::device_copy(implied_integer_indices, handle_ptr->get_stream()); - print("implied integer indices", d_indices); thrust::for_each(handle_ptr->get_thrust_policy(), d_indices.begin(), d_indices.end(), From 5b235541e410d385eefa1fe307cb120daf423f38 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 29 Oct 2025 16:39:54 +0100 Subject: [PATCH 49/86] fixed incorrect reporting of an infeasible solution due to timeout --- cpp/src/dual_simplex/branch_and_bound.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 89c5fa86e6..e65cfba813 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -485,11 +485,13 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t 0 && stats_.nodes_unexplored == 0 && upper_bound == inf) { - settings_.log.printf("Integer infeasible.\n"); - mip_status = mip_status_t::INFEASIBLE; - if (settings_.heuristic_preemption_callback != nullptr) { - settings_.heuristic_preemption_callback(); + if (status_ == mip_exploration_status_t::COMPLETED) { + if (stats_.nodes_explored > 0 && stats_.nodes_unexplored == 0 && upper_bound == inf) { + settings_.log.printf("Integer infeasible.\n"); + mip_status = mip_status_t::INFEASIBLE; + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); + } } } From 40b2bc9baa5c2c9687a3bf7014026469bfc3516d Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 30 Oct 2025 11:27:23 +0100 Subject: [PATCH 50/86] fix compilation issue for solve_MIP. removed debug leftover --- cpp/CMakeLists.txt | 6 ++++++ cpp/src/mip/problem/problem.cu | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 70c6ea0cc7..e4d581bcb7 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -466,6 +466,12 @@ if(BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY) OpenMP::OpenMP_CXX PRIVATE ) + + target_include_directories(solve_MIP + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" + ) + endif() option(BUILD_LP_BENCHMARKS "Build LP benchmarks" OFF) diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 91836dd102..79b39912fc 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -1075,7 +1075,6 @@ void problem_t::set_implied_integers(const std::vector& implied_i { raft::common::nvtx::range fun_scope("set_implied_integers"); auto d_indices = cuopt::device_copy(implied_integer_indices, handle_ptr->get_stream()); - print("implied integer indices", d_indices); thrust::for_each(handle_ptr->get_thrust_policy(), d_indices.begin(), d_indices.end(), From 28ad838c8540324be94253c07dde261d0ab30f32 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 30 Oct 2025 12:11:29 +0100 Subject: [PATCH 51/86] fixed incorrect infeasibility report --- cpp/src/dual_simplex/branch_and_bound.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index e65cfba813..c3c06ddc0a 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1292,17 +1292,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #pragma omp master { - if (status_ == mip_exploration_status_t::RUNNING && - (active_subtrees_ > 0 || get_heap_size() > 0)) { - for (i_t i = 0; i < settings_.num_bfs_threads; i++) { + for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree); - } + best_first_thread(i, search_tree); + } - for (i_t i = 0; i < settings_.num_diving_threads; i++) { + for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(); - } + diving_thread(); } } } From cc89861cf8e73bb6bf65f98d7d2d62524405bc13 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 31 Oct 2025 18:24:11 +0100 Subject: [PATCH 52/86] renamed files and classes --- cpp/src/dual_simplex/CMakeLists.txt | 2 +- ..._presolve.cpp => bounds_strengthening.cpp} | 22 +++---- ..._presolve.hpp => bounds_strengthening.hpp} | 17 +++--- cpp/src/dual_simplex/branch_and_bound.cpp | 57 ++++++++++--------- cpp/src/dual_simplex/branch_and_bound.hpp | 13 +++-- cpp/src/dual_simplex/presolve.hpp | 2 - 6 files changed, 61 insertions(+), 52 deletions(-) rename cpp/src/dual_simplex/{node_presolve.cpp => bounds_strengthening.cpp} (94%) rename cpp/src/dual_simplex/{node_presolve.hpp => bounds_strengthening.hpp} (73%) diff --git a/cpp/src/dual_simplex/CMakeLists.txt b/cpp/src/dual_simplex/CMakeLists.txt index c52121a631..fb063efbe7 100644 --- a/cpp/src/dual_simplex/CMakeLists.txt +++ b/cpp/src/dual_simplex/CMakeLists.txt @@ -27,7 +27,7 @@ set(DUAL_SIMPLEX_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/phase1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/phase2.cpp ${CMAKE_CURRENT_SOURCE_DIR}/presolve.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/node_presolve.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/bounds_strengthening.cpp ${CMAKE_CURRENT_SOURCE_DIR}/primal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pseudo_costs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/right_looking_lu.cpp diff --git a/cpp/src/dual_simplex/node_presolve.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp similarity index 94% rename from cpp/src/dual_simplex/node_presolve.cpp rename to cpp/src/dual_simplex/bounds_strengthening.cpp index fb59c56932..fc1adb771b 100644 --- a/cpp/src/dual_simplex/node_presolve.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -15,7 +15,7 @@ * limitations under the License. */ -#include +#include #include #include @@ -64,12 +64,14 @@ void print_bounds_stats(const std::vector& lower, } template -node_presolver_t::node_presolver_t(const lp_problem_t& problem, - const std::vector& row_sense, - const std::vector& var_types) +bounds_strengthening_t::bounds_strengthening_t( + const lp_problem_t& problem, + const csr_matrix_t& Arow, + const std::vector& row_sense, + const std::vector& var_types) : bounds_changed(problem.num_cols, false), A(problem.A), - Arow(problem.Arow), + Arow(Arow), var_types(var_types), delta_min_activity(problem.num_rows), delta_max_activity(problem.num_rows), @@ -98,7 +100,7 @@ node_presolver_t::node_presolver_t(const lp_problem_t& probl } template -bool node_presolver_t::bound_strengthening( +bool bounds_strengthening_t::bounds_strengthening( std::vector& lower_bounds, std::vector& upper_bounds, const simplex_solver_settings_t& settings) @@ -130,13 +132,13 @@ bool node_presolver_t::bound_strengthening( while (iter < iter_limit) { for (i_t i = 0; i < m; ++i) { if (!constraint_changed[i]) { continue; } - const i_t row_start = Arow.col_start[i]; - const i_t row_end = Arow.col_start[i + 1]; + const i_t row_start = Arow.row_start[i]; + const i_t row_end = Arow.row_start[i + 1]; f_t min_a = 0.0; f_t max_a = 0.0; for (i_t p = row_start; p < row_end; ++p) { - const i_t j = Arow.i[p]; + const i_t j = Arow.j[p]; const f_t a_ij = Arow.x[p]; variable_changed[j] = true; @@ -289,7 +291,7 @@ bool node_presolver_t::bound_strengthening( } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE -template class node_presolver_t; +template class bounds_strengthening_t; #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/node_presolve.hpp b/cpp/src/dual_simplex/bounds_strengthening.hpp similarity index 73% rename from cpp/src/dual_simplex/node_presolve.hpp rename to cpp/src/dual_simplex/bounds_strengthening.hpp index 1bb7fb899f..28df35d0dc 100644 --- a/cpp/src/dual_simplex/node_presolve.hpp +++ b/cpp/src/dual_simplex/bounds_strengthening.hpp @@ -22,22 +22,23 @@ namespace cuopt::linear_programming::dual_simplex { template -class node_presolver_t { +class bounds_strengthening_t { public: // For pure LP bounds strengthening, var_types should be defaulted (i.e. left empty) - node_presolver_t(const lp_problem_t& problem, - const std::vector& row_sense, - const std::vector& var_types); + bounds_strengthening_t(const lp_problem_t& problem, + const csr_matrix_t& Arow, + const std::vector& row_sense, + const std::vector& var_types); - bool bound_strengthening(std::vector& lower_bounds, - std::vector& upper_bounds, - const simplex_solver_settings_t& settings); + bool bounds_strengthening(std::vector& lower_bounds, + std::vector& upper_bounds, + const simplex_solver_settings_t& settings); std::vector bounds_changed; private: const csc_matrix_t& A; - const csc_matrix_t& Arow; + const csr_matrix_t& Arow; const std::vector& var_types; std::vector constraint_changed; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 275d1c358d..20a3e6a994 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -17,11 +17,11 @@ #include #include +#include #include #include #include #include -#include #include #include #include @@ -565,9 +565,9 @@ template node_status_t branch_and_bound_t::solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, - node_presolver_t& presolver, + bounds_strengthening_t& presolver, thread_type_t thread_type, - bool recompute, + bool recompute_bounds, const std::vector& root_lower, const std::vector& root_upper, logger_t& log) @@ -589,7 +589,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod std::fill(presolver.bounds_changed.begin(), presolver.bounds_changed.end(), false); // Set the correct bounds for the leaf problem - if (recompute) { + if (recompute_bounds) { leaf_problem.lower = root_lower; leaf_problem.upper = root_upper; node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, presolver.bounds_changed); @@ -600,7 +600,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod } bool feasible = - presolver.bound_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); + presolver.bounds_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; @@ -707,6 +707,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod template void branch_and_bound_t::exploration_ramp_up(mip_node_t* node, search_tree_t* search_tree, + const csr_matrix_t& Arow, i_t initial_heap_size) { if (status_ != mip_exploration_status_t::RUNNING) { return; } @@ -719,7 +720,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, var_types_); + bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); f_t lower_bound = node->lower_bound; f_t upper_bound = get_upper_bound(); @@ -788,10 +789,10 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase if (stats_.nodes_unexplored < initial_heap_size) { #pragma omp task - exploration_ramp_up(node->get_down_child(), search_tree, initial_heap_size); + exploration_ramp_up(node->get_down_child(), search_tree, Arow, initial_heap_size); #pragma omp task - exploration_ramp_up(node->get_up_child(), search_tree, initial_heap_size); + exploration_ramp_up(node->get_up_child(), search_tree, Arow, initial_heap_size); } else { // We've generated enough nodes, push further nodes onto the heap @@ -808,9 +809,9 @@ void branch_and_bound_t::explore_subtree(i_t task_id, mip_node_t* start_node, search_tree_t& search_tree, lp_problem_t& leaf_problem, - node_presolver_t& presolver) + bounds_strengthening_t& presolver) { - bool recompute = true; + bool recompute_bounds = true; std::deque*> stack; stack.push_front(start_node); @@ -840,7 +841,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); search_tree.update(node_ptr, node_status_t::FATHOMED); - recompute = true; + recompute_bounds = true; continue; } @@ -879,12 +880,12 @@ void branch_and_bound_t::explore_subtree(i_t task_id, leaf_problem, presolver, thread_type_t::EXPLORATION, - recompute, + recompute_bounds, original_lp_.lower, original_lp_.upper, settings_.log); - recompute = node_status != node_status_t::HAS_CHILDREN; + recompute_bounds = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { status_ = mip_exploration_status_t::TIME_LIMIT; @@ -930,7 +931,9 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } template -void branch_and_bound_t::best_first_thread(i_t id, search_tree_t& search_tree) +void branch_and_bound_t::best_first_thread(i_t id, + search_tree_t& search_tree, + const csr_matrix_t& Arow) { f_t lower_bound = -inf; f_t upper_bound = inf; @@ -940,7 +943,7 @@ void branch_and_bound_t::best_first_thread(i_t id, search_tree_t leaf_problem = original_lp_; std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, var_types_); + bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); while (status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && @@ -989,7 +992,7 @@ void branch_and_bound_t::best_first_thread(i_t id, search_tree_t -void branch_and_bound_t::diving_thread() +void branch_and_bound_t::diving_thread(const csr_matrix_t& Arow) { logger_t log; log.log = false; @@ -997,7 +1000,7 @@ void branch_and_bound_t::diving_thread() // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - node_presolver_t presolver(leaf_problem, row_sense, var_types_); + bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); while (status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { @@ -1010,7 +1013,7 @@ void branch_and_bound_t::diving_thread() if (start_node.has_value()) { if (get_upper_bound() < start_node->node.lower_bound) { continue; } - bool recompute = true; + bool recompute_bounds = true; search_tree_t subtree(std::move(start_node->node)); std::deque*> stack; stack.push_front(&subtree.root); @@ -1022,7 +1025,7 @@ void branch_and_bound_t::diving_thread() f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node_ptr->lower_bound); if (node_ptr->lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { - recompute = true; + recompute_bounds = true; continue; } @@ -1033,12 +1036,12 @@ void branch_and_bound_t::diving_thread() leaf_problem, presolver, thread_type_t::DIVING, - recompute, + recompute_bounds, start_node->lower, start_node->upper, log); - recompute = node_status != node_status_t::HAS_CHILDREN; + recompute_bounds = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::TIME_LIMIT) { return; @@ -1225,7 +1228,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut min_diving_queue_size_ = 4 * settings_.num_diving_threads; status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; - original_lp_.A.transpose(original_lp_.Arow); + + csr_matrix_t Arow(1, 1, 0); + original_lp_.A.to_compressed_row(Arow); #pragma omp parallel num_threads(settings_.num_threads) { @@ -1236,10 +1241,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t initial_size = 2 * settings_.num_threads; #pragma omp task - exploration_ramp_up(down_child, &search_tree, initial_size); + exploration_ramp_up(down_child, &search_tree, Arow, initial_size); #pragma omp task - exploration_ramp_up(up_child, &search_tree, initial_size); + exploration_ramp_up(up_child, &search_tree, Arow, initial_size); } #pragma omp barrier @@ -1250,12 +1255,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut (active_subtrees_ > 0 || get_heap_size() > 0)) { for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree); + best_first_thread(i, search_tree, Arow); } for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(); + diving_thread(Arow); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index fd4721b98c..00da2f96df 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -62,7 +62,7 @@ enum class thread_type_t { }; template -class node_presolver_t; +class bounds_strengthening_t; template void upper_bound_callback(f_t upper_bound); @@ -181,6 +181,7 @@ class branch_and_bound_t { // there is enough unexplored nodes. This is done recursively using OpenMP tasks. void exploration_ramp_up(mip_node_t* node, search_tree_t* search_tree, + const csr_matrix_t& Arow, i_t initial_heap_size); // Explore the search tree using the best-first search with plunging strategy. @@ -188,21 +189,23 @@ class branch_and_bound_t { mip_node_t* start_node, search_tree_t& search_tree, lp_problem_t& leaf_problem, - node_presolver_t& presolver); + bounds_strengthening_t& presolver); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. - void best_first_thread(i_t id, search_tree_t& search_tree); + void best_first_thread(i_t id, + search_tree_t& search_tree, + const csr_matrix_t& Arow); // Each diving thread pops the first node from the dive queue and then performs // a deep dive into the subtree determined by the node. - void diving_thread(); + void diving_thread(const csr_matrix_t& Arow); // Solve the LP relaxation of a leaf node and update the tree. node_status_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, - node_presolver_t& presolver, + bounds_strengthening_t& presolver, thread_type_t thread_type, bool recompute, const std::vector& root_lower, diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index b0d7b0aa23..538ca5dffe 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -33,7 +33,6 @@ struct lp_problem_t { num_cols(n), objective(n), A(m, n, nz), - Arow(1, 1, 1), rhs(m), lower(n), upper(n), @@ -45,7 +44,6 @@ struct lp_problem_t { i_t num_cols; std::vector objective; csc_matrix_t A; - csc_matrix_t Arow; std::vector rhs; std::vector lower; std::vector upper; From 874d45f31f68b5fa2b0bda159a6691b4aed3ce11 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 31 Oct 2025 18:37:19 +0100 Subject: [PATCH 53/86] fix incorrect report of infeasible solution --- cpp/src/dual_simplex/branch_and_bound.cpp | 53 +++++++++++------------ 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 20a3e6a994..b013920298 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -485,11 +485,13 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t 0 && stats_.nodes_unexplored == 0 && upper_bound == inf) { - settings_.log.printf("Integer infeasible.\n"); - mip_status = mip_status_t::INFEASIBLE; - if (settings_.heuristic_preemption_callback != nullptr) { - settings_.heuristic_preemption_callback(); + if (status_ == mip_exploration_status_t::COMPLETED) { + if (stats_.nodes_explored > 0 && stats_.nodes_unexplored == 0 && upper_bound == inf) { + settings_.log.printf("Integer infeasible.\n"); + mip_status = mip_status_t::INFEASIBLE; + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); + } } } @@ -717,11 +719,6 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod // to repair the heuristic solution. repair_heuristic_solutions(); - // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp_; - std::vector row_sense; - bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); - f_t lower_bound = node->lower_bound; f_t upper_bound = get_upper_bound(); f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); @@ -769,6 +766,11 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod return; } + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp_; + std::vector row_sense; + bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); + node_status_t node_status = solve_node(node, *search_tree, leaf_problem, @@ -931,7 +933,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } template -void branch_and_bound_t::best_first_thread(i_t id, +void branch_and_bound_t::best_first_thread(i_t task_id, search_tree_t& search_tree, const csr_matrix_t& Arow) { @@ -948,29 +950,29 @@ void branch_and_bound_t::best_first_thread(i_t id, while (status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_subtrees_ > 0 || get_heap_size() > 0)) { - mip_node_t* node_ptr = nullptr; + mip_node_t* start_node = nullptr; // If there any node left in the heap, we pop the top node and explore it. mutex_heap_.lock(); if (heap_.size() > 0) { - node_ptr = heap_.top(); + start_node = heap_.top(); heap_.pop(); active_subtrees_++; } mutex_heap_.unlock(); - if (node_ptr != nullptr) { - if (get_upper_bound() < node_ptr->lower_bound) { + if (start_node != nullptr) { + if (get_upper_bound() < start_node->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the // current upper bound - search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); - search_tree.update(node_ptr, node_status_t::FATHOMED); + search_tree.graphviz_node(settings_.log, start_node, "cutoff", start_node->lower_bound); + search_tree.update(start_node, node_status_t::FATHOMED); active_subtrees_--; continue; } // Best-first search with plunging - explore_subtree(id, node_ptr, search_tree, leaf_problem, presolver); + explore_subtree(task_id, start_node, search_tree, leaf_problem, presolver); active_subtrees_--; } @@ -986,7 +988,7 @@ void branch_and_bound_t::best_first_thread(i_t id, if (active_subtrees_ == 0) { status_ = mip_exploration_status_t::COMPLETED; } else { - local_lower_bounds_[id] = inf; + local_lower_bounds_[task_id] = inf; } } } @@ -1251,17 +1253,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #pragma omp master { - if (status_ == mip_exploration_status_t::RUNNING && - (active_subtrees_ > 0 || get_heap_size() > 0)) { - for (i_t i = 0; i < settings_.num_bfs_threads; i++) { + for (i_t i = 0; i < settings_.num_bfs_threads; i++) { #pragma omp task - best_first_thread(i, search_tree, Arow); - } + best_first_thread(i, search_tree, Arow); + } - for (i_t i = 0; i < settings_.num_diving_threads; i++) { + for (i_t i = 0; i < settings_.num_diving_threads; i++) { #pragma omp task - diving_thread(Arow); - } + diving_thread(Arow); } } } From 5bfda565b5574dacac02d734d256899341311093 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 31 Oct 2025 18:41:40 +0100 Subject: [PATCH 54/86] fixed typo --- cpp/src/dual_simplex/bounds_strengthening.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index fc1adb771b..9f92062e8f 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -15,7 +15,7 @@ * limitations under the License. */ -#include +#include #include #include From 04e3063035135bebfd3acdbbed97de219eabd6ca Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 31 Oct 2025 20:19:25 +0100 Subject: [PATCH 55/86] fixed typo --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b013920298..7247116aed 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -17,7 +17,7 @@ #include #include -#include +#include #include #include #include From 1df3b4c6b4f555b862603a47603f436c1ebc8d5d Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 3 Nov 2025 11:30:29 +0100 Subject: [PATCH 56/86] rename variable --- cpp/src/dual_simplex/mip_node.hpp | 47 ++++++++++++++++----------- cpp/src/dual_simplex/pseudo_costs.cpp | 4 +-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 1c51c09d12..b7d1b0a7d0 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -29,7 +29,7 @@ namespace cuopt::linear_programming::dual_simplex { enum class node_status_t : int { - ACTIVE = 0, // Node still in the tree + PENDING = 0, // Node still in the tree INTEGER_FEASIBLE = 1, // Node has an integer feasible solution INFEASIBLE = 2, // Node is infeasible FATHOMED = 3, // Node objective is greater than the upper bound @@ -38,7 +38,7 @@ enum class node_status_t : int { TIME_LIMIT = 6 // Time out during the LP relaxation }; -enum class round_dir_t { NONE = -1, DOWN = 0, UP = 1 }; +enum class rounding_direction_t { NONE = -1, DOWN = 0, UP = 1 }; bool inactive_status(node_status_t status); @@ -46,13 +46,13 @@ template class mip_node_t { public: mip_node_t(f_t root_lower_bound, const std::vector& basis) - : status(node_status_t::ACTIVE), + : status(node_status_t::PENDING), lower_bound(root_lower_bound), depth(0), parent(nullptr), node_id(0), branch_var(-1), - branch_dir(round_dir_t::NONE), + branch_dir(rounding_direction_t::NONE), vstatus(basis) { children[0] = nullptr; @@ -63,10 +63,10 @@ class mip_node_t { mip_node_t* parent_node, i_t node_num, i_t branch_variable, - round_dir_t branch_direction, + rounding_direction_t branch_direction, f_t branch_var_value, const std::vector& basis) - : status(node_status_t::ACTIVE), + : status(node_status_t::PENDING), lower_bound(parent_node->lower_bound), depth(parent_node->depth + 1), parent(parent_node), @@ -77,10 +77,10 @@ class mip_node_t { vstatus(basis) { - branch_var_lower = branch_direction == round_dir_t::DOWN ? problem.lower[branch_var] - : std::ceil(branch_var_value); - branch_var_upper = branch_direction == round_dir_t::DOWN ? std::floor(branch_var_value) - : problem.upper[branch_var]; + branch_var_lower = branch_direction == rounding_direction_t::DOWN ? problem.lower[branch_var] + : std::ceil(branch_var_value); + branch_var_upper = branch_direction == rounding_direction_t::DOWN ? std::floor(branch_var_value) + : problem.upper[branch_var]; children[0] = nullptr; children[1] = nullptr; } @@ -234,7 +234,7 @@ class mip_node_t { i_t depth; i_t node_id; i_t branch_var; - round_dir_t branch_dir; + rounding_direction_t branch_dir; f_t branch_var_lower; f_t branch_var_upper; f_t fractional_val; @@ -303,7 +303,7 @@ class search_tree_t { parent_node, ++id, branch_var, - round_dir_t::DOWN, + rounding_direction_t::DOWN, fractional_val, parent_vstatus); @@ -311,14 +311,23 @@ class search_tree_t { parent_node, down_child.get(), branch_var, - round_dir_t::DOWN, + rounding_direction_t::DOWN, std::floor(fractional_val)); - auto up_child = std::make_unique>( - original_lp, parent_node, ++id, branch_var, round_dir_t::UP, fractional_val, parent_vstatus); + auto up_child = std::make_unique>(original_lp, + parent_node, + ++id, + branch_var, + rounding_direction_t::UP, + fractional_val, + parent_vstatus); - graphviz_edge( - log, parent_node, up_child.get(), branch_var, round_dir_t::UP, std::ceil(fractional_val)); + graphviz_edge(log, + parent_node, + up_child.get(), + branch_var, + rounding_direction_t::UP, + std::ceil(fractional_val)); assert(parent_vstatus.size() == original_lp.num_cols); parent_node->add_children(std::move(down_child), @@ -339,7 +348,7 @@ class search_tree_t { const mip_node_t* origin_ptr, const mip_node_t* dest_ptr, const i_t branch_var, - round_dir_t branch_dir, + rounding_direction_t branch_dir, const f_t bound) { if (write_graphviz) { @@ -347,7 +356,7 @@ class search_tree_t { origin_ptr->node_id, dest_ptr->node_id, branch_var, - branch_dir == round_dir_t::DOWN ? "<=" : ">=", + branch_dir == rounding_direction_t::DOWN ? "<=" : ">=", bound); } } diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 1a10dc73e2..87c11e49e5 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -210,10 +210,10 @@ void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_pt { mutex.lock(); const f_t change_in_obj = leaf_objective - node_ptr->lower_bound; - const f_t frac = node_ptr->branch_dir == round_dir_t::DOWN + const f_t frac = node_ptr->branch_dir == rounding_direction_t::DOWN ? node_ptr->fractional_val - std::floor(node_ptr->fractional_val) : std::ceil(node_ptr->fractional_val) - node_ptr->fractional_val; - if (node_ptr->branch_dir == round_dir_t::DOWN) { + if (node_ptr->branch_dir == rounding_direction_t::DOWN) { pseudo_cost_sum_down[node_ptr->branch_var] += change_in_obj / frac; pseudo_cost_num_down[node_ptr->branch_var]++; } else { From d94220b74d66429cfaa5cc7930777b8b492d932d Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 3 Nov 2025 11:30:29 +0100 Subject: [PATCH 57/86] rename variable --- cpp/src/dual_simplex/mip_node.hpp | 47 ++++++++++++++++----------- cpp/src/dual_simplex/pseudo_costs.cpp | 4 +-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 1c51c09d12..b7d1b0a7d0 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -29,7 +29,7 @@ namespace cuopt::linear_programming::dual_simplex { enum class node_status_t : int { - ACTIVE = 0, // Node still in the tree + PENDING = 0, // Node still in the tree INTEGER_FEASIBLE = 1, // Node has an integer feasible solution INFEASIBLE = 2, // Node is infeasible FATHOMED = 3, // Node objective is greater than the upper bound @@ -38,7 +38,7 @@ enum class node_status_t : int { TIME_LIMIT = 6 // Time out during the LP relaxation }; -enum class round_dir_t { NONE = -1, DOWN = 0, UP = 1 }; +enum class rounding_direction_t { NONE = -1, DOWN = 0, UP = 1 }; bool inactive_status(node_status_t status); @@ -46,13 +46,13 @@ template class mip_node_t { public: mip_node_t(f_t root_lower_bound, const std::vector& basis) - : status(node_status_t::ACTIVE), + : status(node_status_t::PENDING), lower_bound(root_lower_bound), depth(0), parent(nullptr), node_id(0), branch_var(-1), - branch_dir(round_dir_t::NONE), + branch_dir(rounding_direction_t::NONE), vstatus(basis) { children[0] = nullptr; @@ -63,10 +63,10 @@ class mip_node_t { mip_node_t* parent_node, i_t node_num, i_t branch_variable, - round_dir_t branch_direction, + rounding_direction_t branch_direction, f_t branch_var_value, const std::vector& basis) - : status(node_status_t::ACTIVE), + : status(node_status_t::PENDING), lower_bound(parent_node->lower_bound), depth(parent_node->depth + 1), parent(parent_node), @@ -77,10 +77,10 @@ class mip_node_t { vstatus(basis) { - branch_var_lower = branch_direction == round_dir_t::DOWN ? problem.lower[branch_var] - : std::ceil(branch_var_value); - branch_var_upper = branch_direction == round_dir_t::DOWN ? std::floor(branch_var_value) - : problem.upper[branch_var]; + branch_var_lower = branch_direction == rounding_direction_t::DOWN ? problem.lower[branch_var] + : std::ceil(branch_var_value); + branch_var_upper = branch_direction == rounding_direction_t::DOWN ? std::floor(branch_var_value) + : problem.upper[branch_var]; children[0] = nullptr; children[1] = nullptr; } @@ -234,7 +234,7 @@ class mip_node_t { i_t depth; i_t node_id; i_t branch_var; - round_dir_t branch_dir; + rounding_direction_t branch_dir; f_t branch_var_lower; f_t branch_var_upper; f_t fractional_val; @@ -303,7 +303,7 @@ class search_tree_t { parent_node, ++id, branch_var, - round_dir_t::DOWN, + rounding_direction_t::DOWN, fractional_val, parent_vstatus); @@ -311,14 +311,23 @@ class search_tree_t { parent_node, down_child.get(), branch_var, - round_dir_t::DOWN, + rounding_direction_t::DOWN, std::floor(fractional_val)); - auto up_child = std::make_unique>( - original_lp, parent_node, ++id, branch_var, round_dir_t::UP, fractional_val, parent_vstatus); + auto up_child = std::make_unique>(original_lp, + parent_node, + ++id, + branch_var, + rounding_direction_t::UP, + fractional_val, + parent_vstatus); - graphviz_edge( - log, parent_node, up_child.get(), branch_var, round_dir_t::UP, std::ceil(fractional_val)); + graphviz_edge(log, + parent_node, + up_child.get(), + branch_var, + rounding_direction_t::UP, + std::ceil(fractional_val)); assert(parent_vstatus.size() == original_lp.num_cols); parent_node->add_children(std::move(down_child), @@ -339,7 +348,7 @@ class search_tree_t { const mip_node_t* origin_ptr, const mip_node_t* dest_ptr, const i_t branch_var, - round_dir_t branch_dir, + rounding_direction_t branch_dir, const f_t bound) { if (write_graphviz) { @@ -347,7 +356,7 @@ class search_tree_t { origin_ptr->node_id, dest_ptr->node_id, branch_var, - branch_dir == round_dir_t::DOWN ? "<=" : ">=", + branch_dir == rounding_direction_t::DOWN ? "<=" : ">=", bound); } } diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 1a10dc73e2..87c11e49e5 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -210,10 +210,10 @@ void pseudo_costs_t::update_pseudo_costs(mip_node_t* node_pt { mutex.lock(); const f_t change_in_obj = leaf_objective - node_ptr->lower_bound; - const f_t frac = node_ptr->branch_dir == round_dir_t::DOWN + const f_t frac = node_ptr->branch_dir == rounding_direction_t::DOWN ? node_ptr->fractional_val - std::floor(node_ptr->fractional_val) : std::ceil(node_ptr->fractional_val) - node_ptr->fractional_val; - if (node_ptr->branch_dir == round_dir_t::DOWN) { + if (node_ptr->branch_dir == rounding_direction_t::DOWN) { pseudo_cost_sum_down[node_ptr->branch_var] += change_in_obj / frac; pseudo_cost_num_down[node_ptr->branch_var]++; } else { From 2306ab8af386f25a8cd455bb2fd6618f17d6270b Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 6 Nov 2025 10:00:48 +0100 Subject: [PATCH 58/86] revert refactor frequency change --- 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 4f23389eca..59e6dc7bbd 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -72,7 +72,7 @@ struct simplex_solver_settings_t { ordering(-1), barrier_dual_initial_point(-1), crossover(false), - refactor_frequency(50), + refactor_frequency(100), iteration_log_frequency(1000), first_iteration_log(2), num_threads(omp_get_max_threads() - 1), From 5ec4472ce5d4515118fa9f20be3c10882ea03896 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 10 Nov 2025 11:10:21 +0100 Subject: [PATCH 59/86] addresses reviewer's comments --- cpp/src/dual_simplex/branch_and_bound.cpp | 395 ++++++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 36 +- cpp/src/dual_simplex/mip_node.hpp | 8 +- cpp/src/dual_simplex/presolve.cpp | 11 +- 4 files changed, 251 insertions(+), 199 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 7247116aed..c060ecd0f8 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -201,7 +201,7 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) } } -inline const char* thread_type_symbol(thread_type_t type) +inline const char* feasible_solution_symbol(thread_type_t type) { switch (type) { case thread_type_t::EXPLORATION: return "B"; @@ -210,6 +210,12 @@ inline const char* thread_type_symbol(thread_type_t type) } } +inline bool has_children(node_children_status_t status) +{ + return status == node_children_status_t::UP_CHILDREN_FIRST || + status == node_children_status_t::DOWN_CHILDREN_FIRST; +} + } // namespace template @@ -222,9 +228,9 @@ branch_and_bound_t::branch_and_bound_t( incumbent_(1), root_relax_soln_(1, 1), pc_(1), - status_(mip_exploration_status_t::UNSET) + solver_status_(mip_exploration_status_t::UNSET) { - stats_.start_time = tic(); + exploration_stats_.start_time = tic(); dualize_info_t dualize_info; convert_user_problem(original_problem_, settings_, original_lp_, new_slacks_, dualize_info); full_variable_types(original_problem_, original_lp_, var_types_); @@ -305,7 +311,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu mutex_upper_.unlock(); if (is_feasible) { - if (status_ == mip_exploration_status_t::RUNNING) { + if (solver_status_ == mip_exploration_status_t::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 gap = user_mip_gap(user_obj, user_lower); @@ -315,11 +321,11 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu user_obj, user_lower, gap.c_str(), - toc(stats_.start_time)); + 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(stats_.start_time)); + toc(exploration_stats_.start_time)); } } @@ -428,7 +434,7 @@ void branch_and_bound_t::repair_heuristic_solutions() obj, lower, user_gap.c_str(), - toc(stats_.start_time)); + toc(exploration_stats_.start_time)); if (settings_.solution_callback != nullptr) { std::vector original_x; @@ -449,12 +455,12 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t::set_final_solution(mip_solution_t::set_final_solution(mip_solution_t 0 && stats_.nodes_unexplored == 0 && upper_bound == inf) { + if (solver_status_ == mip_exploration_status_t::COMPLETED) { + if (exploration_stats_.nodes_explored > 0 && exploration_stats_.nodes_unexplored == 0 && + upper_bound == inf) { settings_.log.printf("Integer infeasible.\n"); mip_status = mip_status_t::INFEASIBLE; if (settings_.heuristic_preemption_callback != nullptr) { @@ -498,8 +506,8 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t::add_feasible_solution(f_t leaf_objective, thread_type_t thread_type) { bool send_solution = false; - i_t nodes_explored = stats_.nodes_explored; - i_t nodes_unexplored = stats_.nodes_unexplored; + i_t nodes_explored = exploration_stats_.nodes_explored; + i_t nodes_unexplored = exploration_stats_.nodes_unexplored; mutex_upper_.lock(); if (leaf_objective < upper_bound_) { @@ -521,16 +529,17 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, f_t lower_bound = get_lower_bound(); f_t obj = compute_user_objective(original_lp_, upper_bound_); f_t lower = compute_user_objective(original_lp_, lower_bound); - settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - thread_type_symbol(thread_type), - nodes_explored, - nodes_unexplored, - obj, - lower, - leaf_depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(stats_.start_time)); + settings_.log.printf( + "%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + feasible_solution_symbol(thread_type), + nodes_explored, + nodes_unexplored, + obj, + lower, + leaf_depth, + nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(obj, lower).c_str(), + toc(exploration_stats_.start_time)); send_solution = true; } @@ -544,8 +553,7 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, } template -std::pair*, mip_node_t*> -branch_and_bound_t::child_selection(mip_node_t* node_ptr) +rounding_direction_t branch_and_bound_t::child_selection(mip_node_t* node_ptr) { const i_t branch_var = node_ptr->get_down_child()->branch_var; const f_t branch_var_val = node_ptr->get_down_child()->fractional_val; @@ -556,23 +564,24 @@ branch_and_bound_t::child_selection(mip_node_t* node_ptr) constexpr f_t eps = 1e-6; if (down_dist < up_dist + eps) { - return std::make_pair(node_ptr->get_down_child(), node_ptr->get_up_child()); + return rounding_direction_t::DOWN; } else { - return std::make_pair(node_ptr->get_up_child(), node_ptr->get_down_child()); + return rounding_direction_t::UP; } } template -node_status_t branch_and_bound_t::solve_node(mip_node_t* node_ptr, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& presolver, - thread_type_t thread_type, - bool recompute_bounds, - const std::vector& root_lower, - const std::vector& root_upper, - logger_t& log) +node_children_status_t branch_and_bound_t::solve_node( + mip_node_t* node_ptr, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + thread_type_t thread_type, + bool recompute_bounds, + const std::vector& root_lower, + const std::vector& root_upper, + logger_t& log) { const f_t abs_fathom_tol = settings_.absolute_mip_gap_tol / 10; const f_t upper_bound = get_upper_bound(); @@ -585,24 +594,25 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod lp_settings.set_log(false); lp_settings.cut_off = upper_bound + settings_.dual_tol; lp_settings.inside_mip = 2; - lp_settings.time_limit = settings_.time_limit - toc(stats_.start_time); + lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); // Reset the bound_changed markers - std::fill(presolver.bounds_changed.begin(), presolver.bounds_changed.end(), false); + std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); // Set the correct bounds for the leaf problem if (recompute_bounds) { leaf_problem.lower = root_lower; leaf_problem.upper = root_upper; - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, presolver.bounds_changed); + node_ptr->get_variable_bounds( + leaf_problem.lower, leaf_problem.upper, node_presolver.bounds_changed); } else { node_ptr->update_branched_variable_bounds( - leaf_problem.lower, leaf_problem.upper, presolver.bounds_changed); + leaf_problem.lower, leaf_problem.upper, node_presolver.bounds_changed); } bool feasible = - presolver.bounds_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); + node_presolver.bounds_strengthening(leaf_problem.lower, leaf_problem.upper, lp_settings); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; @@ -622,14 +632,14 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod 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.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = solve_linear_program_advanced( leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } - stats_.total_lp_solve_time += toc(lp_start_time); - stats_.total_lp_iters += node_iter; + exploration_stats_.total_lp_solve_time += toc(lp_start_time); + exploration_stats_.total_lp_iters += node_iter; } if (lp_status == dual::status_t::DUAL_UNBOUNDED) { @@ -637,7 +647,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod node_ptr->lower_bound = inf; search_tree.graphviz_node(log, node_ptr, "infeasible", 0.0); search_tree.update(node_ptr, node_status_t::INFEASIBLE); - return node_status_t::INFEASIBLE; + return node_children_status_t::NO_CHILDREN; } else if (lp_status == dual::status_t::CUTOFF) { // Node was cut off. Do not branch @@ -645,7 +655,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); search_tree.graphviz_node(log, node_ptr, "cut off", leaf_objective); search_tree.update(node_ptr, node_status_t::FATHOMED); - return node_status_t::FATHOMED; + return node_children_status_t::NO_CHILDREN; } else if (lp_status == dual::status_t::OPTIMAL) { // LP was feasible @@ -663,7 +673,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod add_feasible_solution(leaf_objective, leaf_solution.x, node_ptr->depth, thread_type); search_tree.graphviz_node(log, node_ptr, "integer feasible", leaf_objective); search_tree.update(node_ptr, node_status_t::INTEGER_FEASIBLE); - return node_status_t::INTEGER_FEASIBLE; + return node_children_status_t::NO_CHILDREN; } else if (leaf_objective <= upper_bound + abs_fathom_tol) { logger_t pc_log = log; @@ -676,17 +686,23 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod search_tree.branch( node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, leaf_problem, log); node_ptr->status = node_status_t::HAS_CHILDREN; - return node_status_t::HAS_CHILDREN; + + rounding_direction_t round_dir = child_selection(node_ptr); + + if (round_dir == rounding_direction_t::UP) { + return node_children_status_t::UP_CHILDREN_FIRST; + } else { + return node_children_status_t::DOWN_CHILDREN_FIRST; + } } else { search_tree.graphviz_node(log, node_ptr, "fathomed", leaf_objective); search_tree.update(node_ptr, node_status_t::FATHOMED); - return node_status_t::FATHOMED; + return node_children_status_t::NO_CHILDREN; } } else if (lp_status == dual::status_t::TIME_LIMIT) { search_tree.graphviz_node(log, node_ptr, "timeout", 0.0); - search_tree.update(node_ptr, node_status_t::TIME_LIMIT); - return node_status_t::TIME_LIMIT; + return node_children_status_t::TIME_LIMIT; } else { if (thread_type == thread_type_t::EXPLORATION) { @@ -702,7 +718,7 @@ node_status_t branch_and_bound_t::solve_node(mip_node_t* nod search_tree.graphviz_node(log, node_ptr, "numerical", 0.0); search_tree.update(node_ptr, node_status_t::NUMERICAL); - return node_status_t::NUMERICAL; + return node_children_status_t::NUMERICAL; } } @@ -712,7 +728,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod const csr_matrix_t& Arow, i_t initial_heap_size) { - if (status_ != mip_exploration_status_t::RUNNING) { return; } + if (solver_status_ != mip_exploration_status_t::RUNNING) { return; } // Note that we do not know which thread will execute the // `exploration_ramp_up` task, so we allow to any thread @@ -723,9 +739,9 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod f_t upper_bound = get_upper_bound(); f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); f_t abs_gap = upper_bound - lower_bound; - i_t nodes_explored = (++stats_.nodes_explored); - i_t nodes_unexplored = (--stats_.nodes_unexplored); - stats_.nodes_since_last_log++; + i_t nodes_explored = (++exploration_stats_.nodes_explored); + i_t nodes_unexplored = (--exploration_stats_.nodes_unexplored); + exploration_stats_.nodes_since_last_log++; if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree->graphviz_node(settings_.log, node, "cutoff", node->lower_bound); @@ -733,63 +749,66 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod return; } - f_t now = toc(stats_.start_time); - f_t time_since_last_log = stats_.last_log == 0 ? 1.0 : toc(stats_.last_log); + f_t now = toc(exploration_stats_.start_time); + f_t time_since_last_log = + exploration_stats_.last_log == 0 ? 1.0 : toc(exploration_stats_.last_log); - if (((stats_.nodes_since_last_log >= 10 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && + if (((exploration_stats_.nodes_since_last_log >= 10 || + abs_gap < 10 * settings_.absolute_mip_gap_tol) && (time_since_last_log >= 1)) || (time_since_last_log > 30) || now > settings_.time_limit) { // Check if no new node was explored until now. If this is the case, // only the last thread should report the progress - if (stats_.nodes_explored.load() == nodes_explored) { - stats_.nodes_since_last_log = 0; - stats_.last_log = tic(); + if (exploration_stats_.nodes_explored.load() == nodes_explored) { + exploration_stats_.nodes_since_last_log = 0; + exploration_stats_.last_log = tic(); f_t obj = compute_user_objective(original_lp_, upper_bound); f_t user_lower = compute_user_objective(original_lp_, root_objective_); std::string gap_user = user_mip_gap(obj, user_lower); - settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node->depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - gap_user.c_str(), - now); + settings_.log.printf( + " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + obj, + user_lower, + node->depth, + nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, + gap_user.c_str(), + now); } } if (now > settings_.time_limit) { - status_ = mip_exploration_status_t::TIME_LIMIT; + solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); - - node_status_t node_status = solve_node(node, - *search_tree, - leaf_problem, - presolver, - thread_type_t::EXPLORATION, - true, - original_lp_.lower, - original_lp_.upper, - settings_.log); - - if (node_status == node_status_t::TIME_LIMIT) { - status_ = mip_exploration_status_t::TIME_LIMIT; + bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); + + node_children_status_t status = solve_node(node, + *search_tree, + leaf_problem, + node_presolver, + thread_type_t::EXPLORATION, + true, + original_lp_.lower, + original_lp_.upper, + settings_.log); + + if (status == node_children_status_t::TIME_LIMIT) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; - } else if (node_status == node_status_t::HAS_CHILDREN) { - stats_.nodes_unexplored += 2; + } else if (has_children(status)) { + exploration_stats_.nodes_unexplored += 2; // If we haven't generated enough nodes to keep the threads busy, continue the ramp up phase - if (stats_.nodes_unexplored < initial_heap_size) { + if (exploration_stats_.nodes_unexplored < initial_heap_size) { #pragma omp task exploration_ramp_up(node->get_down_child(), search_tree, Arow, initial_heap_size); @@ -811,13 +830,13 @@ void branch_and_bound_t::explore_subtree(i_t task_id, mip_node_t* start_node, search_tree_t& search_tree, lp_problem_t& leaf_problem, - bounds_strengthening_t& presolver) + bounds_strengthening_t& node_presolver) { bool recompute_bounds = true; std::deque*> stack; stack.push_front(start_node); - while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { + while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { if (task_id == 0) { repair_heuristic_solutions(); } mip_node_t* node_ptr = stack.front(); @@ -836,9 +855,9 @@ void branch_and_bound_t::explore_subtree(i_t task_id, // - The lower bound of the parent is lower or equal to its children assert(task_id < local_lower_bounds_.size()); local_lower_bounds_[task_id] = lower_bound; - i_t nodes_explored = (++stats_.nodes_explored); - i_t nodes_unexplored = (--stats_.nodes_unexplored); - stats_.nodes_since_last_log++; + i_t nodes_explored = (++exploration_stats_.nodes_explored); + i_t nodes_unexplored = (--exploration_stats_.nodes_unexplored); + exploration_stats_.nodes_since_last_log++; if (lower_bound > upper_bound || rel_gap < settings_.relative_mip_gap_tol) { search_tree.graphviz_node(settings_.log, node_ptr, "cutoff", node_ptr->lower_bound); @@ -847,53 +866,56 @@ void branch_and_bound_t::explore_subtree(i_t task_id, continue; } - f_t now = toc(stats_.start_time); + f_t now = toc(exploration_stats_.start_time); if (task_id == 0) { - f_t time_since_last_log = stats_.last_log == 0 ? 1.0 : toc(stats_.last_log); + f_t time_since_last_log = + exploration_stats_.last_log == 0 ? 1.0 : toc(exploration_stats_.last_log); - if (((stats_.nodes_since_last_log >= 1000 || abs_gap < 10 * settings_.absolute_mip_gap_tol) && + if (((exploration_stats_.nodes_since_last_log >= 1000 || + abs_gap < 10 * settings_.absolute_mip_gap_tol) && time_since_last_log >= 1) || (time_since_last_log > 30) || now > settings_.time_limit) { f_t obj = compute_user_objective(original_lp_, upper_bound); f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); std::string gap_user = user_mip_gap(obj, user_lower); - settings_.log.printf(" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - obj, - user_lower, - node_ptr->depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - gap_user.c_str(), - now); - stats_.last_log = tic(); - stats_.nodes_since_last_log = 0; + settings_.log.printf( + " %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + obj, + user_lower, + node_ptr->depth, + nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0, + gap_user.c_str(), + now); + exploration_stats_.last_log = tic(); + exploration_stats_.nodes_since_last_log = 0; } } if (now > settings_.time_limit) { - status_ = mip_exploration_status_t::TIME_LIMIT; + solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } - node_status_t node_status = solve_node(node_ptr, - search_tree, - leaf_problem, - presolver, - thread_type_t::EXPLORATION, - recompute_bounds, - original_lp_.lower, - original_lp_.upper, - settings_.log); + node_children_status_t status = solve_node(node_ptr, + search_tree, + leaf_problem, + node_presolver, + thread_type_t::EXPLORATION, + recompute_bounds, + original_lp_.lower, + original_lp_.upper, + settings_.log); - recompute_bounds = node_status != node_status_t::HAS_CHILDREN; + recompute_bounds = !has_children(status); - if (node_status == node_status_t::TIME_LIMIT) { - status_ = mip_exploration_status_t::TIME_LIMIT; + if (status == node_children_status_t::TIME_LIMIT) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; - } else if (node_status == node_status_t::HAS_CHILDREN) { + } else if (has_children(status)) { // The stack should only contain the children of the current parent. // If the stack size is greater than 0, // we pop the current node from the stack and place it in the global heap, @@ -911,7 +933,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (get_heap_size() > settings_.num_bfs_threads) { std::vector lower = original_lp_.lower; std::vector upper = original_lp_.upper; - node->get_variable_bounds(lower, upper, presolver.bounds_changed); + node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); mutex_dive_queue_.lock(); dive_queue_.emplace(node->detach_copy(), std::move(lower), std::move(upper)); @@ -923,11 +945,15 @@ void branch_and_bound_t::explore_subtree(i_t task_id, mutex_heap_.unlock(); } - stats_.nodes_unexplored += 2; + exploration_stats_.nodes_unexplored += 2; - auto [first, second] = child_selection(node_ptr); - stack.push_front(second); - stack.push_front(first); + if (status == node_children_status_t::UP_CHILDREN_FIRST) { + stack.push_front(node_ptr->get_down_child()); + stack.push_front(node_ptr->get_up_child()); + } else { + stack.push_front(node_ptr->get_up_child()); + stack.push_front(node_ptr->get_down_child()); + } } } } @@ -945,10 +971,10 @@ void branch_and_bound_t::best_first_thread(i_t task_id, // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); + bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); - while (status_ == mip_exploration_status_t::RUNNING && abs_gap > settings_.absolute_mip_gap_tol && - rel_gap > settings_.relative_mip_gap_tol && + while (solver_status_ == mip_exploration_status_t::RUNNING && + abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && (active_subtrees_ > 0 || get_heap_size() > 0)) { mip_node_t* start_node = nullptr; @@ -972,7 +998,7 @@ void branch_and_bound_t::best_first_thread(i_t task_id, } // Best-first search with plunging - explore_subtree(task_id, start_node, search_tree, leaf_problem, presolver); + explore_subtree(task_id, start_node, search_tree, leaf_problem, node_presolver); active_subtrees_--; } @@ -984,9 +1010,9 @@ void branch_and_bound_t::best_first_thread(i_t task_id, // Check if it is the last thread that exited the loop and no // timeout or numerical error has happen. - if (status_ == mip_exploration_status_t::RUNNING) { + if (solver_status_ == mip_exploration_status_t::RUNNING) { if (active_subtrees_ == 0) { - status_ = mip_exploration_status_t::COMPLETED; + solver_status_ = mip_exploration_status_t::COMPLETED; } else { local_lower_bounds_[task_id] = inf; } @@ -1002,9 +1028,9 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp_; std::vector row_sense; - bounds_strengthening_t presolver(leaf_problem, Arow, row_sense, var_types_); + bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); - while (status_ == mip_exploration_status_t::RUNNING && + while (solver_status_ == mip_exploration_status_t::RUNNING && (active_subtrees_ > 0 || get_heap_size() > 0)) { std::optional> start_node; @@ -1020,7 +1046,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A std::deque*> stack; stack.push_front(&subtree.root); - while (stack.size() > 0 && status_ == mip_exploration_status_t::RUNNING) { + while (stack.size() > 0 && solver_status_ == mip_exploration_status_t::RUNNING) { mip_node_t* node_ptr = stack.front(); stack.pop_front(); f_t upper_bound = get_upper_bound(); @@ -1031,27 +1057,32 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A continue; } - if (toc(stats_.start_time) > settings_.time_limit) { return; } + if (toc(exploration_stats_.start_time) > settings_.time_limit) { return; } - node_status_t node_status = solve_node(node_ptr, - subtree, - leaf_problem, - presolver, - thread_type_t::DIVING, - recompute_bounds, - start_node->lower, - start_node->upper, - log); + node_children_status_t status = solve_node(node_ptr, + subtree, + leaf_problem, + node_presolver, + thread_type_t::DIVING, + recompute_bounds, + start_node->lower, + start_node->upper, + log); - recompute_bounds = node_status != node_status_t::HAS_CHILDREN; + recompute_bounds = has_children(status); - if (node_status == node_status_t::TIME_LIMIT) { + if (status == node_children_status_t::TIME_LIMIT) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; - } else if (node_status == node_status_t::HAS_CHILDREN) { - auto [first, second] = child_selection(node_ptr); - stack.push_front(second); - stack.push_front(first); + } else if (has_children(status)) { + if (status == node_children_status_t::UP_CHILDREN_FIRST) { + stack.push_front(node_ptr->get_down_child()); + stack.push_front(node_ptr->get_up_child()); + } else { + stack.push_front(node_ptr->get_up_child()); + stack.push_front(node_ptr->get_down_child()); + } } if (stack.size() > 1) { @@ -1065,7 +1096,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A std::vector lower = start_node->lower; std::vector upper = start_node->upper; - new_node->get_variable_bounds(lower, upper, presolver.bounds_changed); + new_node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); mutex_dive_queue_.lock(); dive_queue_.emplace(new_node->detach_copy(), std::move(lower), std::move(upper)); @@ -1081,10 +1112,10 @@ template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { logger_t log; - log.log = false; - status_ = mip_exploration_status_t::UNSET; - stats_.nodes_unexplored = 0; - stats_.nodes_explored = 0; + log.log = false; + solver_status_ = mip_exploration_status_t::UNSET; + exploration_stats_.nodes_unexplored = 0; + exploration_stats_.nodes_explored = 0; if (guess_.size() != 0) { std::vector crushed_guess; @@ -1106,12 +1137,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.resize(original_lp_.num_rows, original_lp_.num_cols); settings_.log.printf("Solving LP root relaxation\n"); - simplex_solver_settings_t lp_settings = settings_; - lp_settings.inside_mip = 1; - lp_status_t root_status = solve_linear_program_advanced( - original_lp_, stats_.start_time, lp_settings, root_relax_soln_, root_vstatus_, edge_norms_); - stats_.total_lp_iters = root_relax_soln_.iterations; - stats_.total_lp_solve_time = toc(stats_.start_time); + simplex_solver_settings_t lp_settings = settings_; + lp_settings.inside_mip = 1; + lp_status_t root_status = solve_linear_program_advanced(original_lp_, + exploration_stats_.start_time, + lp_settings, + root_relax_soln_, + root_vstatus_, + edge_norms_); + exploration_stats_.total_lp_iters = root_relax_soln_.iterations; + exploration_stats_.total_lp_solve_time = toc(exploration_stats_.start_time); if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); @@ -1132,7 +1167,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::TIME_LIMIT) { - status_ = mip_exploration_status_t::TIME_LIMIT; + solver_status_ = mip_exploration_status_t::TIME_LIMIT; return set_final_solution(solution, -inf); } @@ -1174,7 +1209,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solution.simplex_iterations = root_relax_soln_.iterations; settings_.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", compute_user_objective(original_lp_, root_objective_), - toc(stats_.start_time)); + toc(exploration_stats_.start_time)); if (settings_.solution_callback != nullptr) { settings_.solution_callback(solution.x, solution.objective); @@ -1188,7 +1223,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut pc_.resize(original_lp_.num_cols); strong_branching(original_lp_, settings_, - stats_.start_time, + exploration_stats_.start_time, var_types_, root_relax_soln_.x, fractional, @@ -1197,8 +1232,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut edge_norms_, pc_); - if (toc(stats_.start_time) > settings_.time_limit) { - status_ = mip_exploration_status_t::TIME_LIMIT; + if (toc(exploration_stats_.start_time) > settings_.time_limit) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; return set_final_solution(solution, root_objective_); } @@ -1222,14 +1257,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut " | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap " "| Time |\n"); - stats_.nodes_explored = 0; - stats_.nodes_unexplored = 2; - stats_.nodes_since_last_log = 0; - stats_.last_log = tic(); - active_subtrees_ = 0; - min_diving_queue_size_ = 4 * settings_.num_diving_threads; - status_ = mip_exploration_status_t::RUNNING; - lower_bound_ceiling_ = inf; + exploration_stats_.nodes_explored = 0; + exploration_stats_.nodes_unexplored = 2; + exploration_stats_.nodes_since_last_log = 0; + exploration_stats_.last_log = tic(); + active_subtrees_ = 0; + min_diving_queue_size_ = 4 * settings_.num_diving_threads; + solver_status_ = mip_exploration_status_t::RUNNING; + lower_bound_ceiling_ = inf; csr_matrix_t Arow(1, 1, 0); original_lp_.A.to_compressed_row(Arow); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 00da2f96df..57ce4ed3d3 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -52,6 +52,15 @@ enum class mip_exploration_status_t { COMPLETED = 5, // The solver finished exploring the tree }; +enum class node_children_status_t { + NO_CHILDREN = 0, // The node does not produced children + UP_CHILDREN_FIRST = 1, // The up child should be explored first + DOWN_CHILDREN_FIRST = 2, // The down child should be explored first + TIME_LIMIT = 3, // The solver reached a time limit + ITERATION_LIMIT = 4, // The solver reached a iteration limit + NUMERICAL = 5 // The solver encounter a numerical error when solving the node +}; + // Indicate the search and variable selection algorithms used by the thread (See [1]). // // [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, @@ -130,7 +139,7 @@ class branch_and_bound_t { // This should only be used by the main thread f_t last_log = 0.0; omp_atomic_t nodes_since_last_log = 0; - } stats_; + } exploration_stats_; // Mutex for repair omp_mutex_t mutex_repair_; @@ -158,7 +167,7 @@ class branch_and_bound_t { i_t min_diving_queue_size_; // Global status of the solver. - omp_atomic_t status_; + omp_atomic_t solver_status_; // In case, a best-first thread encounters a numerical issue when solving a node, // its blocks the progression of the lower bound. @@ -189,7 +198,7 @@ class branch_and_bound_t { mip_node_t* start_node, search_tree_t& search_tree, lp_problem_t& leaf_problem, - bounds_strengthening_t& presolver); + bounds_strengthening_t& node_presolver); // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. @@ -202,19 +211,18 @@ class branch_and_bound_t { void diving_thread(const csr_matrix_t& Arow); // Solve the LP relaxation of a leaf node and update the tree. - node_status_t solve_node(mip_node_t* node_ptr, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& presolver, - thread_type_t thread_type, - bool recompute, - const std::vector& root_lower, - const std::vector& root_upper, - logger_t& log); + node_children_status_t solve_node(mip_node_t* node_ptr, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + thread_type_t thread_type, + bool recompute, + const std::vector& root_lower, + const std::vector& root_upper, + logger_t& log); // Sort the children based on the Martin's criteria. - std::pair*, mip_node_t*> child_selection( - mip_node_t* node_ptr); + rounding_direction_t child_selection(mip_node_t* node_ptr); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index b7d1b0a7d0..3b518b7755 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -34,11 +34,10 @@ enum class node_status_t : int { INFEASIBLE = 2, // Node is infeasible FATHOMED = 3, // Node objective is greater than the upper bound HAS_CHILDREN = 4, // Node has children to explore - NUMERICAL = 5, // Encountered numerical issue when solving the LP relaxation - TIME_LIMIT = 6 // Time out during the LP relaxation + NUMERICAL = 5 // Encountered numerical issue when solving the LP relaxation }; -enum class rounding_direction_t { NONE = -1, DOWN = 0, UP = 1 }; +enum class rounding_direction_t : int8_t { NONE = -1, DOWN = 0, UP = 1 }; bool inactive_status(node_status_t status); @@ -110,7 +109,8 @@ class mip_node_t { assert(bounds_changed.size() > branch_var); // If the bounds have already been updated on another node, - // skip this node as it contains a less tight bounds. + // skip this node as it contains looser bounds, since we + // are traversing up the tree toward the root if (bounds_changed[branch_var]) { return; } // Apply the bounds at the current node diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 29e63d8676..3620667d20 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -17,6 +17,7 @@ #include +#include #include #include #include @@ -576,7 +577,15 @@ void convert_user_problem(const user_problem_t& user_problem, convert_greater_to_less(user_problem, row_sense, problem, greater_rows, less_rows); } - // bounds strengthening was moved to node_presolve.hpp + constexpr bool run_bounds_strengthening = false; + if (run_bounds_strengthening) { + csr_matrix_t Arow(1, 1, 1); + problem.A.to_compressed_row(Arow); + + // Empty var_types means that all variables are continuous + bounds_strengthening_t bounds_strenghtening(problem, Arow, row_sense, {}); + bounds_strenghtening.bounds_strengthening(problem.lower, problem.upper, settings); + } settings.log.debug( "equality rows %d less rows %d columns %d\n", equal_rows, less_rows, problem.num_cols); From 015bb00a375e4cfec02cabeba7ee64f8e053bf61 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 10 Nov 2025 11:19:16 +0100 Subject: [PATCH 60/86] fixed incorrect stats update --- cpp/src/dual_simplex/branch_and_bound.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index c060ecd0f8..b8fce1f394 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -638,8 +638,10 @@ node_children_status_t branch_and_bound_t::solve_node( lp_status = convert_lp_status_to_dual_status(second_status); } - exploration_stats_.total_lp_solve_time += toc(lp_start_time); - exploration_stats_.total_lp_iters += node_iter; + if (thread_type == thread_type_t::EXPLORATION) { + exploration_stats_.total_lp_solve_time += toc(lp_start_time); + exploration_stats_.total_lp_iters += node_iter; + } } if (lp_status == dual::status_t::DUAL_UNBOUNDED) { @@ -1069,7 +1071,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A start_node->upper, log); - recompute_bounds = has_children(status); + recompute_bounds = !has_children(status); if (status == node_children_status_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; From 8b762e7d9f416415965dc26b1093979657cf46ff Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 10 Nov 2025 11:35:53 +0100 Subject: [PATCH 61/86] addressing reviewer's comments --- cpp/src/dual_simplex/branch_and_bound.cpp | 15 +++++++-------- cpp/src/dual_simplex/branch_and_bound.hpp | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 1fb958d8bf..5021d7f565 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -566,7 +566,7 @@ node_children_status_t branch_and_bound_t::solve_node( mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, - basis_update_mpf_t& ft, + basis_update_mpf_t& basis_update, std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, @@ -621,7 +621,7 @@ node_children_status_t branch_and_bound_t::solve_node( leaf_problem, lp_settings, leaf_vstatus, - ft, + basis_update, basic_list, nonbasic_list, leaf_solution, @@ -634,7 +634,7 @@ node_children_status_t branch_and_bound_t::solve_node( lp_start_time, lp_settings, leaf_solution, - ft, + basis_update, basic_list, nonbasic_list, leaf_vstatus, @@ -683,11 +683,9 @@ node_children_status_t branch_and_bound_t::solve_node( return node_children_status_t::NO_CHILDREN; } else if (leaf_objective <= upper_bound + abs_fathom_tol) { - logger_t pc_log = log; - pc_log.log = false; - // Choose fractional variable to branch on - const i_t branch_var = pc_.variable_selection(leaf_fractional, leaf_solution.x, pc_log); + const i_t branch_var = + pc_.variable_selection(leaf_fractional, leaf_solution.x, lp_settings.log); assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( @@ -1290,7 +1288,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - settings_.log.printf("Exploring the B&B tree using %d best-first threads and %d diving threads\n", + settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", + settings_.num_threads, settings_.num_bfs_threads, settings_.num_diving_threads); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 7e2c413a5f..f21b200421 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -207,7 +207,7 @@ class branch_and_bound_t { node_children_status_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, - basis_update_mpf_t& ft, + basis_update_mpf_t& basis_update, std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, From 4da2c9dd6b18a8eb6f98d6b8cb9fd39997a7aaa4 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 10 Nov 2025 11:39:33 +0100 Subject: [PATCH 62/86] variable renaming --- cpp/src/dual_simplex/branch_and_bound.cpp | 22 +++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 5021d7f565..ff273c5939 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -566,7 +566,7 @@ node_children_status_t branch_and_bound_t::solve_node( mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, - basis_update_mpf_t& basis_update, + basis_update_mpf_t& basis_factors, std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, @@ -621,7 +621,7 @@ node_children_status_t branch_and_bound_t::solve_node( leaf_problem, lp_settings, leaf_vstatus, - basis_update, + basis_factors, basic_list, nonbasic_list, leaf_solution, @@ -634,7 +634,7 @@ node_children_status_t branch_and_bound_t::solve_node( lp_start_time, lp_settings, leaf_solution, - basis_update, + basis_factors, basic_list, nonbasic_list, leaf_vstatus, @@ -796,14 +796,14 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); const i_t m = leaf_problem.num_rows; - basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); std::vector basic_list(m); std::vector nonbasic_list; node_children_status_t status = solve_node(node, *search_tree, leaf_problem, - basis_update, + basis_factors, basic_list, nonbasic_list, node_presolver, @@ -844,7 +844,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, search_tree_t& search_tree, lp_problem_t& leaf_problem, bounds_strengthening_t& node_presolver, - basis_update_mpf_t& basis_update, + basis_update_mpf_t& basis_factors, std::vector& basic_list, std::vector& nonbasic_list) { @@ -918,7 +918,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, node_children_status_t status = solve_node(node_ptr, search_tree, leaf_problem, - basis_update, + basis_factors, basic_list, nonbasic_list, node_presolver, @@ -993,7 +993,7 @@ void branch_and_bound_t::best_first_thread(i_t task_id, bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); const i_t m = leaf_problem.num_rows; - basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); std::vector basic_list(m); std::vector nonbasic_list; @@ -1027,7 +1027,7 @@ void branch_and_bound_t::best_first_thread(i_t task_id, search_tree, leaf_problem, node_presolver, - basis_update, + basis_factors, basic_list, nonbasic_list); @@ -1062,7 +1062,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); const i_t m = leaf_problem.num_rows; - basis_update_mpf_t basis_update(m, settings_.refactor_frequency); + basis_update_mpf_t basis_factors(m, settings_.refactor_frequency); std::vector basic_list(m); std::vector nonbasic_list; @@ -1098,7 +1098,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A node_children_status_t status = solve_node(node_ptr, subtree, leaf_problem, - basis_update, + basis_factors, basic_list, nonbasic_list, node_presolver, diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index f21b200421..321bded104 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -207,12 +207,12 @@ class branch_and_bound_t { node_children_status_t solve_node(mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, - basis_update_mpf_t& basis_update, + basis_update_mpf_t& basis_factors, std::vector& basic_list, std::vector& nonbasic_list, bounds_strengthening_t& node_presolver, thread_type_t thread_type, - bool recompute, + bool recompute_basis_and_bounds, const std::vector& root_lower, const std::vector& root_upper, logger_t& log); From ac199a32606189cc3acdd718f64143f81d3db046 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 10 Nov 2025 11:44:28 +0100 Subject: [PATCH 63/86] fix typo and styling --- cpp/src/dual_simplex/bounds_strengthening.cpp | 12 ------------ cpp/src/dual_simplex/bounds_strengthening.hpp | 12 ------------ cpp/src/dual_simplex/diving_queue.hpp | 12 ------------ cpp/src/dual_simplex/presolve.cpp | 4 ++-- 4 files changed, 2 insertions(+), 38 deletions(-) diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index 9f92062e8f..6a7d5ee62d 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -1,18 +1,6 @@ /* * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ #include diff --git a/cpp/src/dual_simplex/bounds_strengthening.hpp b/cpp/src/dual_simplex/bounds_strengthening.hpp index 28df35d0dc..fb237c7bb0 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.hpp +++ b/cpp/src/dual_simplex/bounds_strengthening.hpp @@ -1,18 +1,6 @@ /* * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ #pragma once diff --git a/cpp/src/dual_simplex/diving_queue.hpp b/cpp/src/dual_simplex/diving_queue.hpp index 57a8bacf19..91d0aa3309 100644 --- a/cpp/src/dual_simplex/diving_queue.hpp +++ b/cpp/src/dual_simplex/diving_queue.hpp @@ -1,18 +1,6 @@ /* * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ #pragma once diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index bac7d639be..fcc33a0e98 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -573,8 +573,8 @@ void convert_user_problem(const user_problem_t& user_problem, problem.A.to_compressed_row(Arow); // Empty var_types means that all variables are continuous - bounds_strengthening_t bounds_strenghtening(problem, Arow, row_sense, {}); - bounds_strenghtening.bounds_strengthening(problem.lower, problem.upper, settings); + bounds_strengthening_t strengthening(problem, Arow, row_sense, {}); + strengthening.bounds_strengthening(problem.lower, problem.upper, settings); } settings.log.debug( From 770adfb0fff3d6601bb948be6218961b74be9a69 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 10 Nov 2025 15:12:20 +0100 Subject: [PATCH 64/86] small refactor --- cpp/src/dual_simplex/bounds_strengthening.cpp | 4 +++- cpp/src/dual_simplex/bounds_strengthening.hpp | 4 +++- cpp/src/dual_simplex/branch_and_bound.cpp | 23 +++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 4 ++-- cpp/src/dual_simplex/mip_node.hpp | 5 ++-- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index 6a7d5ee62d..64e745d5f5 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -1,7 +1,9 @@ +/* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ +/* clang-format on */ #include diff --git a/cpp/src/dual_simplex/bounds_strengthening.hpp b/cpp/src/dual_simplex/bounds_strengthening.hpp index fb237c7bb0..8e38cde15c 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.hpp +++ b/cpp/src/dual_simplex/bounds_strengthening.hpp @@ -1,7 +1,9 @@ +/* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ +/* clang-format on */ #pragma once diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index aa60379edb..6b45053b8d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -668,11 +668,9 @@ node_children_status_t branch_and_bound_t::solve_node( return node_children_status_t::NO_CHILDREN; } else if (leaf_objective <= upper_bound + abs_fathom_tol) { - logger_t pc_log = log; - pc_log.log = false; - // Choose fractional variable to branch on - const i_t branch_var = pc_.variable_selection(leaf_fractional, leaf_solution.x, pc_log); + const i_t branch_var = + pc_.variable_selection(leaf_fractional, leaf_solution.x, lp_settings.log); assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( @@ -928,7 +926,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); mutex_dive_queue_.lock(); - dive_queue_.emplace(node->detach_copy(), std::move(lower), std::move(upper)); + diving_queue_.emplace(node->detach_copy(), std::move(lower), std::move(upper)); mutex_dive_queue_.unlock(); } @@ -1027,7 +1025,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A std::optional> start_node; mutex_dive_queue_.lock(); - if (dive_queue_.size() > 0) { start_node = dive_queue_.pop(); } + if (diving_queue_.size() > 0) { start_node = diving_queue_.pop(); } mutex_dive_queue_.unlock(); if (start_node.has_value()) { @@ -1082,7 +1080,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A // best first search, then we split the current subtree at the // lowest possible point and move to the queue, so it can // be picked by another thread. - if (dive_queue_.size() < min_diving_queue_size_) { + if (diving_queue_.size() < min_diving_queue_size_) { mip_node_t* new_node = stack.back(); stack.pop_back(); @@ -1091,7 +1089,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A new_node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); mutex_dive_queue_.lock(); - dive_queue_.emplace(new_node->detach_copy(), std::move(lower), std::move(upper)); + diving_queue_.emplace(new_node->detach_copy(), std::move(lower), std::move(upper)); mutex_dive_queue_.unlock(); } } @@ -1241,7 +1239,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_, log); - settings_.log.printf("Exploring the B&B tree using %d best-first threads and %d diving threads\n", + csr_matrix_t Arow(1, 1, 0); + original_lp_.A.to_compressed_row(Arow); + + settings_.log.printf("Exploring the B&B tree using %d threads (best-first = %d, diving = %d)\n", + settings_.num_threads, settings_.num_bfs_threads, settings_.num_diving_threads); @@ -1258,9 +1260,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solver_status_ = mip_exploration_status_t::RUNNING; lower_bound_ceiling_ = inf; - csr_matrix_t Arow(1, 1, 0); - original_lp_.A.to_compressed_row(Arow); - #pragma omp parallel num_threads(settings_.num_threads) { #pragma omp master diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 7d50478333..3c0ffa54e9 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -153,7 +153,7 @@ class branch_and_bound_t { // Queue for storing the promising node for performing dives. omp_mutex_t mutex_dive_queue_; - diving_queue_t dive_queue_; + diving_queue_t diving_queue_; i_t min_diving_queue_size_; // Global status of the solver. @@ -192,7 +192,7 @@ class branch_and_bound_t { // Each "main" thread pops a node from the global heap and then performs a plunge // (i.e., a shallow dive) into the subtree determined by the node. - void best_first_thread(i_t id, + void best_first_thread(i_t task_id, search_tree_t& search_tree, const csr_matrix_t& Arow); diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 7b93ae37cf..ea900a0c62 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -19,7 +19,7 @@ namespace cuopt::linear_programming::dual_simplex { enum class node_status_t : int { - PENDING = 0, // Node still in the tree + PENDING = 0, // Node is still in the tree, waiting to be solved INTEGER_FEASIBLE = 1, // Node has an integer feasible solution INFEASIBLE = 2, // Node is infeasible FATHOMED = 3, // Node objective is greater than the upper bound @@ -273,11 +273,10 @@ class search_tree_t { void update(mip_node_t* node_ptr, node_status_t status) { - mutex.lock(); + std::lock_guard lock(mutex); std::vector*> stack; node_ptr->set_status(status, stack); remove_fathomed_nodes(stack); - mutex.unlock(); } void branch(mip_node_t* parent_node, From 3317eaa928a7072e8314d28818eb3fe68baf024b Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 10 Nov 2025 15:40:50 +0100 Subject: [PATCH 65/86] fix missing initialization --- cpp/src/dual_simplex/presolve.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index fcc33a0e98..2cdb08f402 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -574,6 +574,7 @@ void convert_user_problem(const user_problem_t& user_problem, // Empty var_types means that all variables are continuous bounds_strengthening_t strengthening(problem, Arow, row_sense, {}); + std::fill(strengthening.bounds_changed.begin(), strengthening.bounds_changed.end(), true); strengthening.bounds_strengthening(problem.lower, problem.upper, settings); } From a0f8e13a2a4b3e8449242de624aebd7c38c812d9 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 11 Nov 2025 13:48:59 +0100 Subject: [PATCH 66/86] fixed bugs after merge --- cpp/src/dual_simplex/basis_updates.cpp | 4 ++-- cpp/src/dual_simplex/branch_and_bound.cpp | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 3e16411f47..2dceba2b39 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1864,7 +1864,7 @@ i_t basis_update_mpf_t::update(const std::vector& utilde, S_.x.data() + S_.col_start[S_start + 1], v_nz); - if (std::abs(mu) < 1e-13) { + if (std::abs(mu) < 1e-8) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } @@ -1928,7 +1928,7 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde S_.i.data() + S_.col_start[S_start + 1], S_.x.data() + S_.col_start[S_start + 1], S_.col_start[S_start + 2] - S_.col_start[S_start + 1]); - if (std::abs(mu) < 1e-13) { + if (std::abs(mu) < 1e-8) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b03229ce53..9c1e3c9d34 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -455,7 +455,7 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t::set_final_solution(mip_solution_t::explore_subtree(i_t task_id, solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } - if (stats_.nodes_explored >= settings_.node_limit) { - status_ = mip_exploration_status_t::NODE_LIMIT; + if (exploration_stats_.nodes_explored >= settings_.node_limit) { + solver_status_ = mip_exploration_status_t::NODE_LIMIT; return; } @@ -1124,11 +1125,11 @@ template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { logger_t log; - log.log = false; - log.log_prefix = settings_.log.log_prefix; - status_ = mip_exploration_status_t::UNSET; - stats_.nodes_unexplored = 0; - stats_.nodes_explored = 0; + log.log = false; + log.log_prefix = settings_.log.log_prefix; + solver_status_ = mip_exploration_status_t::UNSET; + exploration_stats_.nodes_unexplored = 0; + exploration_stats_.nodes_explored = 0; if (guess_.size() != 0) { std::vector crushed_guess; From 7077dbc4bfe09bb1776702acce9b4980ad8602fc Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 11 Nov 2025 14:13:16 +0100 Subject: [PATCH 67/86] revert parameters to their original values --- cpp/src/dual_simplex/basis_updates.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 2dceba2b39..3e16411f47 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1864,7 +1864,7 @@ i_t basis_update_mpf_t::update(const std::vector& utilde, S_.x.data() + S_.col_start[S_start + 1], v_nz); - if (std::abs(mu) < 1e-8) { + if (std::abs(mu) < 1e-13) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } @@ -1928,7 +1928,7 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde S_.i.data() + S_.col_start[S_start + 1], S_.x.data() + S_.col_start[S_start + 1], S_.col_start[S_start + 2] - S_.col_start[S_start + 1]); - if (std::abs(mu) < 1e-8) { + if (std::abs(mu) < 1e-13) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } From d9a45cf15216763a9a28e52f0a30ff2c34fbaf0c Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 11 Nov 2025 22:46:00 +0100 Subject: [PATCH 68/86] fixing crashes --- cpp/src/dual_simplex/bounds_strengthening.cpp | 25 +++++++++++-------- cpp/src/dual_simplex/bounds_strengthening.hpp | 4 --- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/cpp/src/dual_simplex/bounds_strengthening.cpp b/cpp/src/dual_simplex/bounds_strengthening.cpp index 64e745d5f5..f1bf52c1e3 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.cpp +++ b/cpp/src/dual_simplex/bounds_strengthening.cpp @@ -98,17 +98,20 @@ bool bounds_strengthening_t::bounds_strengthening( const i_t m = A.m; const i_t n = A.n; - constraint_changed.assign(m, false); - variable_changed.assign(n, false); - constraint_changed_next.assign(m, false); - - for (i_t i = 0; i < bounds_changed.size(); ++i) { - if (bounds_changed[i]) { - const i_t row_start = A.col_start[i]; - const i_t row_end = A.col_start[i + 1]; - for (i_t p = row_start; p < row_end; ++p) { - const i_t j = A.i[p]; - constraint_changed[j] = true; + std::vector constraint_changed(m, true); + std::vector variable_changed(n, false); + std::vector constraint_changed_next(m, false); + + if (!bounds_changed.empty()) { + std::fill(constraint_changed.begin(), constraint_changed.end(), false); + for (i_t i = 0; i < n; ++i) { + if (bounds_changed[i]) { + const i_t row_start = A.col_start[i]; + const i_t row_end = A.col_start[i + 1]; + for (i_t p = row_start; p < row_end; ++p) { + const i_t j = A.i[p]; + constraint_changed[j] = true; + } } } } diff --git a/cpp/src/dual_simplex/bounds_strengthening.hpp b/cpp/src/dual_simplex/bounds_strengthening.hpp index 8e38cde15c..e7e218b824 100644 --- a/cpp/src/dual_simplex/bounds_strengthening.hpp +++ b/cpp/src/dual_simplex/bounds_strengthening.hpp @@ -31,10 +31,6 @@ class bounds_strengthening_t { const csr_matrix_t& Arow; const std::vector& var_types; - std::vector constraint_changed; - std::vector variable_changed; - std::vector constraint_changed_next; - std::vector lower; std::vector upper; From 58c70acfe4cdc847c750b369f7937d65a32e5d9e Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 12 Nov 2025 14:45:42 +0100 Subject: [PATCH 69/86] disable RINS logs --- cpp/src/mip/diversity/lns/rins.cu | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 1efc971b21..1a79bdb053 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -244,6 +244,7 @@ void rins_t::run_rins() branch_and_bound_settings.num_threads = 2; branch_and_bound_settings.num_bfs_threads = 1; branch_and_bound_settings.num_diving_threads = 1; + branch_and_bound_settings.log.log = false; branch_and_bound_settings.log.log_prefix = "[RINS] "; branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( std::vector& solution, f_t objective) { From a881b70bd4f9704ef766fd738c7629b1e0fa5ec2 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 12 Nov 2025 14:51:15 +0100 Subject: [PATCH 70/86] tighten tolerance for refactoring --- cpp/src/dual_simplex/basis_updates.cpp | 4 ++-- cpp/src/dual_simplex/basis_updates.hpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 3e16411f47..e583691e9a 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1864,7 +1864,7 @@ i_t basis_update_mpf_t::update(const std::vector& utilde, S_.x.data() + S_.col_start[S_start + 1], v_nz); - if (std::abs(mu) < 1e-13) { + if (std::abs(mu) < mu_tolerance_) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } @@ -1928,7 +1928,7 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde S_.i.data() + S_.col_start[S_start + 1], S_.x.data() + S_.col_start[S_start + 1], S_.col_start[S_start + 2] - S_.col_start[S_start + 1]); - if (std::abs(mu) < 1e-13) { + if (std::abs(mu) < mu_tolerance_) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index 5fc0f99eba..ca0451b919 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -424,6 +424,7 @@ class basis_update_mpf_t { void l_multiply(std::vector& inout) const; void l_transpose_multiply(std::vector& inout) const; + static constexpr f_t mu_tolerance_ = 1e-10; i_t num_updates_; // Number of rank-1 updates to L0 i_t refactor_frequency_; // Average updates before refactoring mutable csc_matrix_t L0_; // Sparse lower triangular matrix from initial factorization From 4947fe17623a5ef04573de6086d2e09ab19a0c35 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 12 Nov 2025 15:53:21 +0100 Subject: [PATCH 71/86] further tighten the tolerance --- cpp/src/dual_simplex/basis_updates.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index ca0451b919..1449145dac 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -424,7 +424,7 @@ class basis_update_mpf_t { void l_multiply(std::vector& inout) const; void l_transpose_multiply(std::vector& inout) const; - static constexpr f_t mu_tolerance_ = 1e-10; + static constexpr f_t mu_tolerance_ = 1e-8; i_t num_updates_; // Number of rank-1 updates to L0 i_t refactor_frequency_; // Average updates before refactoring mutable csc_matrix_t L0_; // Sparse lower triangular matrix from initial factorization From 5e3cd26f7e5d3be893d146cab76c0482229b6b3e Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 14 Nov 2025 12:55:23 +0100 Subject: [PATCH 72/86] changed refactor tolerances. updated logger to support different modes. --- cpp/src/dual_simplex/basis_updates.cpp | 5 +++-- cpp/src/dual_simplex/basis_updates.hpp | 1 - cpp/src/dual_simplex/branch_and_bound.cpp | 20 ++++++++++++++++++++ cpp/src/dual_simplex/logger.hpp | 4 ++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index e583691e9a..fff33b8a58 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1864,7 +1864,7 @@ i_t basis_update_mpf_t::update(const std::vector& utilde, S_.x.data() + S_.col_start[S_start + 1], v_nz); - if (std::abs(mu) < mu_tolerance_) { + if (std::abs(mu) < 1E-8 || std::abs(mu) > 1E+8) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } @@ -1928,7 +1928,8 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde S_.i.data() + S_.col_start[S_start + 1], S_.x.data() + S_.col_start[S_start + 1], S_.col_start[S_start + 2] - S_.col_start[S_start + 1]); - if (std::abs(mu) < mu_tolerance_) { + + if (std::abs(mu) < 1E-8 || std::abs(mu) > 1E+8) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index c1c86a491f..078dfffeb3 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -424,7 +424,6 @@ class basis_update_mpf_t { void l_multiply(std::vector& inout) const; void l_transpose_multiply(std::vector& inout) const; - static constexpr f_t mu_tolerance_ = 1e-8; i_t num_updates_; // Number of rank-1 updates to L0 i_t refactor_frequency_; // Average updates before refactoring mutable csc_matrix_t L0_; // Sparse lower triangular matrix from initial factorization diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9c1e3c9d34..110b13265b 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -599,6 +599,26 @@ node_children_status_t branch_and_bound_t::solve_node( lp_settings.inside_mip = 2; lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); +#ifdef LOG_NODE_SIMPLEX + lp_settings.set_log(true); + std::stringstream ss; + ss << "simplex-" << std::this_thread::get_id() << ".log"; + std::string logname; + ss >> logname; + lp_settings.set_log_filename(logname); + lp_settings.log.enable_log_to_file("a+"); + lp_settings.log.log_to_console = false; + lp_settings.log.printf( + "%s node id = %d, branch var = %d, fractional val = %f, variable lower bound = %f, variable " + "upper bound = %f\n", + settings_.log.log_prefix.c_str(), + node_ptr->node_id, + node_ptr->branch_var, + node_ptr->fractional_val, + node_ptr->branch_var_lower, + node_ptr->branch_var_upper); +#endif + // Reset the bound_changed markers std::fill(node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 3b13665b1b..ac5e394f9e 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -30,10 +30,10 @@ class logger_t { { } - void enable_log_to_file() + void enable_log_to_file(std::string mode = "w") { if (log_file != nullptr) { std::fclose(log_file); } - log_file = std::fopen(log_filename.c_str(), "w"); + log_file = std::fopen(log_filename.c_str(), mode.c_str()); log_to_file = true; } From 74ef1ba6a69879fd17eb67ad8a19facc63eb6f14 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 12 Nov 2025 14:55:39 +0100 Subject: [PATCH 73/86] tighten tolerance for refactoring the basis. disabled RINS logs. --- cpp/src/dual_simplex/basis_updates.cpp | 5 +++-- cpp/src/mip/diversity/lns/rins.cu | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 3e16411f47..fff33b8a58 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1864,7 +1864,7 @@ i_t basis_update_mpf_t::update(const std::vector& utilde, S_.x.data() + S_.col_start[S_start + 1], v_nz); - if (std::abs(mu) < 1e-13) { + if (std::abs(mu) < 1E-8 || std::abs(mu) > 1E+8) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } @@ -1928,7 +1928,8 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde S_.i.data() + S_.col_start[S_start + 1], S_.x.data() + S_.col_start[S_start + 1], S_.col_start[S_start + 2] - S_.col_start[S_start + 1]); - if (std::abs(mu) < 1e-13) { + + if (std::abs(mu) < 1E-8 || std::abs(mu) > 1E+8) { // Force a refactor. Otherwise we will get numerical issues when dividing by mu. return 1; } diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 1efc971b21..1a79bdb053 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -244,6 +244,7 @@ void rins_t::run_rins() branch_and_bound_settings.num_threads = 2; branch_and_bound_settings.num_bfs_threads = 1; branch_and_bound_settings.num_diving_threads = 1; + branch_and_bound_settings.log.log = false; branch_and_bound_settings.log.log_prefix = "[RINS] "; branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( std::vector& solution, f_t objective) { From 669fdbec1e4021e96a4edb6f5a34f2787f23de8b Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 18 Nov 2025 11:18:49 +0000 Subject: [PATCH 74/86] add support for building with clang --- build.sh | 16 ++++++- ci/tsan_suppressions.txt | 6 +++ cpp/CMakeLists.txt | 19 +++++++- cpp/include/cuopt/error.hpp | 2 - .../utilities/internals.hpp | 1 + cpp/src/dual_simplex/branch_and_bound.cpp | 9 ++-- cpp/src/dual_simplex/branch_and_bound.hpp | 2 +- cpp/src/linear_programming/pdlp.cu | 2 +- .../utilities/cython_solve.cu | 3 +- cpp/src/linear_programming/utils.cuh | 6 +-- cpp/src/mip/diversity/lns/rins.cu | 4 +- cpp/src/mip/local_search/local_search.cu | 3 +- cpp/src/mip/presolve/gf2_presolve.hpp | 4 ++ cpp/src/mip/presolve/third_party_presolve.cpp | 4 ++ cpp/src/mip/utilities/cpu_worker_thread.cuh | 8 ++-- cpp/src/routing/crossovers/ox_recombiner.cuh | 4 +- cpp/src/utilities/omp_helpers.hpp | 48 +++++++++++-------- .../distance_engine/waypoint_matrix_test.cpp | 6 +-- .../c_api_tests/c_api_test.c | 1 + cpp/tests/routing/level0/l0_ges_test.cu | 4 +- .../level0/l0_objective_function_test.cu | 2 +- cpp/tests/routing/level0/l0_routing_test.cu | 2 +- .../routing/level0/l0_vehicle_order_match.cu | 2 +- .../routing/level0/l0_vehicle_types_test.cu | 2 +- 24 files changed, 106 insertions(+), 54 deletions(-) create mode 100644 ci/tsan_suppressions.txt diff --git a/build.sh b/build.sh index 2903684f6f..e040baa958 100755 --- a/build.sh +++ b/build.sh @@ -15,7 +15,7 @@ REPODIR=$(cd "$(dirname "$0")"; pwd) LIBCUOPT_BUILD_DIR=${LIBCUOPT_BUILD_DIR:=${REPODIR}/cpp/build} LIBMPS_PARSER_BUILD_DIR=${LIBMPS_PARSER_BUILD_DIR:=${REPODIR}/cpp/libmps_parser/build} -VALIDARGS="clean libcuopt libmps_parser cuopt_mps_parser cuopt cuopt_server cuopt_sh_client docs deb -a -b -g -fsanitize -v -l= --verbose-pdlp --build-lp-only --no-fetch-rapids --skip-c-python-adapters --skip-tests-build --skip-routing-build --skip-fatbin-write --host-lineinfo [--cmake-args=\\\"\\\"] [--cache-tool=] -n --allgpuarch --ci-only-arch --show_depr_warn -h --help" +VALIDARGS="clean libcuopt libmps_parser cuopt_mps_parser cuopt cuopt_server cuopt_sh_client docs deb -a -b -g -fsanitize -tsan -v -l= --verbose-pdlp --build-lp-only --no-fetch-rapids --skip-c-python-adapters --skip-tests-build --skip-routing-build --skip-fatbin-write --host-lineinfo [--cmake-args=\\\"\\\"] [--cache-tool=] -n --allgpuarch --ci-only-arch --show_depr_warn -h --help" HELP="$0 [ ...] [ ...] where is: clean - remove all existing build artifacts and configuration (start over) @@ -32,7 +32,8 @@ HELP="$0 [ ...] [ ...] -g - build for debug -a - Enable assertion (by default in debug mode) -b - Build with benchmark settings - -fsanitize - Build with sanitizer + -fsanitize - Build with AddressSanitizer and UndefinedBehaviorSanitizer + -tsan - Build with ThreadSanitizer (cannot be used with -fsanitize) -n - no install step --no-fetch-rapids - don't fetch rapids dependencies -l= - log level. Options are: TRACE | DEBUG | INFO | WARN | ERROR | CRITICAL | OFF. Default=INFO @@ -76,6 +77,7 @@ BUILD_ALL_GPU_ARCH=0 BUILD_CI_ONLY=0 BUILD_LP_ONLY=0 BUILD_SANITIZER=0 +BUILD_TSAN=0 SKIP_C_PYTHON_ADAPTERS=0 SKIP_TESTS_BUILD=0 SKIP_ROUTING_BUILD=0 @@ -230,6 +232,9 @@ fi if hasArg -fsanitize; then BUILD_SANITIZER=1 fi +if hasArg -tsan; then + BUILD_TSAN=1 +fi if hasArg --skip-c-python-adapters; then SKIP_C_PYTHON_ADAPTERS=1 fi @@ -298,6 +303,12 @@ if [ ${BUILD_LP_ONLY} -eq 1 ] && [ ${SKIP_C_PYTHON_ADAPTERS} -eq 0 ]; then exit 1 fi +if [ ${BUILD_SANITIZER} -eq 1 ] && [ ${BUILD_TSAN} -eq 1 ]; then + echo "ERROR: -fsanitize and -tsan cannot be used together" + echo "AddressSanitizer and ThreadSanitizer are mutually exclusive" + exit 1 +fi + if [ ${BUILD_ALL_GPU_ARCH} -eq 1 ]; then CUOPT_CMAKE_CUDA_ARCHITECTURES="RAPIDS" echo "Building for *ALL* supported GPU architectures..." @@ -344,6 +355,7 @@ if buildAll || hasArg libcuopt; then -DFETCH_RAPIDS=${FETCH_RAPIDS} \ -DBUILD_LP_ONLY=${BUILD_LP_ONLY} \ -DBUILD_SANITIZER=${BUILD_SANITIZER} \ + -DBUILD_TSAN=${BUILD_TSAN} \ -DSKIP_C_PYTHON_ADAPTERS=${SKIP_C_PYTHON_ADAPTERS} \ -DBUILD_TESTS=$((1 - ${SKIP_TESTS_BUILD})) \ -DSKIP_ROUTING_BUILD=${SKIP_ROUTING_BUILD} \ diff --git a/ci/tsan_suppressions.txt b/ci/tsan_suppressions.txt new file mode 100644 index 0000000000..b6f413e370 --- /dev/null +++ b/ci/tsan_suppressions.txt @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 + +# Ignore races in external header-only libraries +race:tbb +race:Papilo diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 4391c53df7..f770d80c81 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -81,9 +81,22 @@ endif(CMAKE_COMPILER_IS_GNUCXX) # 2. (Optional) To run with a debugger (gdb or cuda-gdb) use the additional ASAN option alloc_dealloc_mismatch=0 if(BUILD_SANITIZER) list(APPEND CUOPT_CXX_FLAGS -fsanitize=address,undefined -fno-omit-frame-pointer -g -Wno-error=maybe-uninitialized) + if(NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + list(APPEND CUOPT_CXX_FLAGS -Wno-error=maybe-uninitialized) + endif() add_link_options(-fsanitize=address,undefined) endif(BUILD_SANITIZER) +# To use ThreadSanitizer: +# 1. Build with clang and the -tsan flag +# 2. Run the binary with env var set: OMP_TOOL_LIBRARIES=/usr/lib/llvm-17/lib/libarcher.so ARCHER_OPTIONS='verbose=1' TSAN_OPTIONS='suppresions=ci/tsan_suppressions.txt:ignore_noninstrumented_modules=1:halt_on_error=1' +# Replace with local llvm install path. libarcher.so must be presetn +if(BUILD_TSAN) + message(STATUS "Building with ThreadSanitizer enabled") + list(APPEND CUOPT_CXX_FLAGS -fsanitize=thread -fno-omit-frame-pointer -g) + add_link_options(-fsanitize=thread) +endif(BUILD_TSAN) + if(DEFINE_ASSERT) add_definitions(-DASSERT_MODE) endif(DEFINE_ASSERT) @@ -117,7 +130,11 @@ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -static-global-template-stub=false") endif() list(APPEND CUOPT_CUDA_FLAGS -Werror=cross-execution-space-call -Wno-deprecated-declarations -Xcompiler=-Werror --default-stream=per-thread) -list(APPEND CUOPT_CUDA_FLAGS -Xcompiler=-Wall -Wno-error=non-template-friend) +if("${CMAKE_CUDA_HOST_COMPILER}" MATCHES "clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + list(APPEND CUOPT_CUDA_FLAGS -Xcompiler=-Wall) +else() + list(APPEND CUOPT_CUDA_FLAGS -Xcompiler=-Wall -Wno-error=non-template-friend) +endif() list(APPEND CUOPT_CUDA_FLAGS -Xfatbin=-compress-all) if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.9 AND CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 13.0) list(APPEND CUOPT_CUDA_FLAGS -Xfatbin=--compress-level=3) diff --git a/cpp/include/cuopt/error.hpp b/cpp/include/cuopt/error.hpp index b6086245db..a83413515e 100644 --- a/cpp/include/cuopt/error.hpp +++ b/cpp/include/cuopt/error.hpp @@ -33,8 +33,6 @@ enum class error_type_t { */ struct logic_error : public std::logic_error { - explicit logic_error() = default; - logic_error(const logic_error& exception) = default; // Move constructor diff --git a/cpp/include/cuopt/linear_programming/utilities/internals.hpp b/cpp/include/cuopt/linear_programming/utilities/internals.hpp index 84c96a7164..86e7246faf 100644 --- a/cpp/include/cuopt/linear_programming/utilities/internals.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/internals.hpp @@ -62,6 +62,7 @@ namespace linear_programming { class base_solution_t { public: + virtual ~base_solution_t() = default; virtual bool is_mip() const = 0; }; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9f207b6a6f..7a7b05b3bd 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -661,7 +661,9 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, original_lp_, log); + search_tree.mutex.lock(); node_ptr->status = node_status_t::HAS_CHILDREN; + search_tree.mutex.unlock(); return node_status_t::HAS_CHILDREN; } else { @@ -676,7 +678,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& } else { if (thread_type == 'B') { - lower_bound_ceiling_.fetch_min(node_ptr->lower_bound); + fetch_min(lower_bound_ceiling_, node_ptr->lower_bound); log.printf( "LP returned status %d on node %d. This indicates a numerical issue. The best bound is set " "to " @@ -1017,12 +1019,11 @@ void branch_and_bound_t::diving_thread(lp_problem_t& leaf_pr // best first search, then we split the current subtree at the // lowest possible point and move to the queue, so it can // be picked by another thread. - if (dive_queue_.size() < min_diving_queue_size_) { - mutex_dive_queue_.lock(); + if (std::lock_guard lock(mutex_dive_queue_); + dive_queue_.size() < min_diving_queue_size_) { mip_node_t* new_node = stack.back(); stack.pop_back(); dive_queue_.emplace(new_node->detach_copy(), leaf_problem.lower, leaf_problem.upper); - mutex_dive_queue_.unlock(); } } } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 5b304addd5..fdf29601b1 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -167,7 +167,7 @@ class branch_and_bound_t { omp_atomic_t total_lp_iters = 0; // This should only be used by the main thread - f_t last_log = 0.0; + omp_atomic_t last_log = 0.0; omp_atomic_t nodes_since_last_log = 0; } stats_; diff --git a/cpp/src/linear_programming/pdlp.cu b/cpp/src/linear_programming/pdlp.cu index 5d982bcc96..d78f1d1f48 100644 --- a/cpp/src/linear_programming/pdlp.cu +++ b/cpp/src/linear_programming/pdlp.cu @@ -1510,7 +1510,7 @@ void pdlp_solver_t::compute_initial_step_size() const auto& cusparse_view_ = pdhg_solver_.get_cusparse_view(); - int sing_iters = 0; + [[maybe_unused]] int sing_iters = 0; for (int i = 0; i < max_iterations; ++i) { ++sing_iters; // d_q = d_z diff --git a/cpp/src/linear_programming/utilities/cython_solve.cu b/cpp/src/linear_programming/utilities/cython_solve.cu index 111f3caf04..38968c0508 100644 --- a/cpp/src/linear_programming/utilities/cython_solve.cu +++ b/cpp/src/linear_programming/utilities/cython_solve.cu @@ -295,8 +295,7 @@ std::pair>, double> call_batch_solve( #pragma omp parallel for num_threads(max_thread) for (std::size_t i = 0; i < size; ++i) - list[i] = - std::move(call_solve(data_models[i], solver_settings, cudaStreamNonBlocking, is_batch_mode)); + list[i] = call_solve(data_models[i], solver_settings, cudaStreamNonBlocking, is_batch_mode); auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast(end - start_solver); diff --git a/cpp/src/linear_programming/utils.cuh b/cpp/src/linear_programming/utils.cuh index 0da5d25ceb..2333283f39 100644 --- a/cpp/src/linear_programming/utils.cuh +++ b/cpp/src/linear_programming/utils.cuh @@ -62,9 +62,9 @@ struct max_abs_value { template i_t conditional_major(uint64_t total_pdlp_iterations) { - uint64_t step = 10; - uint64_t threshold = 1000; - uint64_t iteration = 0; + uint64_t step = 10; + uint64_t threshold = 1000; + [[maybe_unused]] uint64_t iteration = 0; [[maybe_unused]] constexpr uint64_t max_u64 = std::numeric_limits::max(); diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 1efc971b21..73d6cbdf93 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -245,8 +245,8 @@ void rins_t::run_rins() branch_and_bound_settings.num_bfs_threads = 1; branch_and_bound_settings.num_diving_threads = 1; branch_and_bound_settings.log.log_prefix = "[RINS] "; - branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( - std::vector& solution, f_t objective) { + branch_and_bound_settings.solution_callback = [&rins_solution_queue](std::vector& solution, + f_t objective) { rins_solution_queue.push_back(solution); }; dual_simplex::branch_and_bound_t branch_and_bound(branch_and_bound_problem, diff --git a/cpp/src/mip/local_search/local_search.cu b/cpp/src/mip/local_search/local_search.cu index a8e06440aa..6d64f62ab1 100644 --- a/cpp/src/mip/local_search/local_search.cu +++ b/cpp/src/mip/local_search/local_search.cu @@ -80,8 +80,7 @@ void local_search_t::start_cpufj_scratch_threads(population_t 0); cpu_fj.fj_cpu->log_prefix = "******* scratch " + std::to_string(counter) + ": "; - cpu_fj.fj_cpu->improvement_callback = [this, &population, &cpu_fj]( - f_t obj, const std::vector& h_vec) { + cpu_fj.fj_cpu->improvement_callback = [&population](f_t obj, const std::vector& h_vec) { population.add_external_solution(h_vec, obj, solution_origin_t::CPUFJ); if (obj < local_search_best_obj) { CUOPT_LOG_TRACE("******* New local search best obj %g, best overall %g", diff --git a/cpp/src/mip/presolve/gf2_presolve.hpp b/cpp/src/mip/presolve/gf2_presolve.hpp index 19d4e7d813..53e79d2c93 100644 --- a/cpp/src/mip/presolve/gf2_presolve.hpp +++ b/cpp/src/mip/presolve/gf2_presolve.hpp @@ -7,13 +7,17 @@ #pragma once +#if !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstringop-overflow" // ignore boost error for pip wheel build +#endif #include #include #include #include +#if !defined(__clang__) #pragma GCC diagnostic pop +#endif namespace cuopt::linear_programming::detail { diff --git a/cpp/src/mip/presolve/third_party_presolve.cpp b/cpp/src/mip/presolve/third_party_presolve.cpp index 22827c6e28..f3faf3dd7d 100644 --- a/cpp/src/mip/presolve/third_party_presolve.cpp +++ b/cpp/src/mip/presolve/third_party_presolve.cpp @@ -14,11 +14,15 @@ #include +#if !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstringop-overflow" // ignore boost error for pip wheel build +#endif #include #include +#if !defined(__clang__) #pragma GCC diagnostic pop +#endif namespace cuopt::linear_programming::detail { diff --git a/cpp/src/mip/utilities/cpu_worker_thread.cuh b/cpp/src/mip/utilities/cpu_worker_thread.cuh index 0f1671c94a..8d0b6d71ee 100644 --- a/cpp/src/mip/utilities/cpu_worker_thread.cuh +++ b/cpp/src/mip/utilities/cpu_worker_thread.cuh @@ -60,6 +60,7 @@ template cpu_worker_thread_base_t::~cpu_worker_thread_base_t() { // Note: We don't call on_terminate() here since the derived object is already destroyed. + CUOPT_LOG_DEBUG("Destroying CPU worker thread"); join_worker(); } @@ -83,12 +84,14 @@ void cpu_worker_thread_base_t::cpu_worker_thread() std::lock_guard lock(cpu_mutex); cpu_thread_done = true; } + cpu_cv.notify_all(); } } template void cpu_worker_thread_base_t::request_termination() { + CUOPT_LOG_DEBUG("Requesting termination of CPU worker thread"); bool should_terminate = false; { std::lock_guard lock(cpu_mutex); @@ -131,9 +134,8 @@ void cpu_worker_thread_base_t::start_cpu_solver() template bool cpu_worker_thread_base_t::wait_for_cpu_solver() { - while (!cpu_thread_done && !cpu_thread_terminate) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + std::unique_lock lock(cpu_mutex); + cpu_cv.wait(lock, [this] { return cpu_thread_done || cpu_thread_terminate; }); return static_cast(this)->get_result(); } diff --git a/cpp/src/routing/crossovers/ox_recombiner.cuh b/cpp/src/routing/crossovers/ox_recombiner.cuh index 17823c28b8..7f965de2ff 100644 --- a/cpp/src/routing/crossovers/ox_recombiner.cuh +++ b/cpp/src/routing/crossovers/ox_recombiner.cuh @@ -336,7 +336,7 @@ struct OX { int i = routes_number; if (optimal_routes_search) { i = optimal_routes_number; } int end_index = offspring.size() - 1; - double cost_n, cost_p, total_delta = 0.; + [[maybe_unused]] double cost_n, cost_p, total_delta = 0.; std::vector>>> routes_to_add; std::vector tmp_route; @@ -530,7 +530,7 @@ struct OX { "Mismatch number of edges"); for (size_t j = 0; j < h_transpose_graph[i].size(); ++j) { auto [ref_edge, ref_weight, ref_veh] = h_transpose_graph[i][j]; - bool found = false; + [[maybe_unused]] bool found = false; for (int x = 0; x < tmp_transpose.row_sizes[i]; ++x) { auto edge = tmp_transpose.indices[transpose_offset + x]; auto veh = tmp_transpose.buckets[transpose_offset + x]; diff --git a/cpp/src/utilities/omp_helpers.hpp b/cpp/src/utilities/omp_helpers.hpp index 1ec95583ae..33eda66cba 100644 --- a/cpp/src/utilities/omp_helpers.hpp +++ b/cpp/src/utilities/omp_helpers.hpp @@ -91,38 +91,46 @@ class omp_atomic_t { T fetch_sub(T inc) { return fetch_add(-inc); } + private: + T val; + +#ifndef __NVCC__ + friend double fetch_min(omp_atomic_t& atomic_var, double other); + friend double fetch_max(omp_atomic_t& atomic_var, double other); +#endif +}; + // Atomic CAS are only supported in OpenMP v5.1 // (gcc 12+ or clang 14+), however, nvcc (or the host compiler) cannot // parse it correctly yet #ifndef __NVCC__ - T fetch_min(T other) - { - T old; +// 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 - { - old = val; - val = other < val ? other : val; - } - return old; + { + old = atomic_var.val; + if (other < atomic_var.val) { atomic_var.val = other; } } + return old; +} - T fetch_max(T other) - { - T old; +inline double fetch_max(omp_atomic_t& atomic_var, double other) +{ + double old; #pragma omp atomic compare capture - { - old = val; - val = other > val ? other : val; - } - return old; + { + old = atomic_var.val; + if (other > atomic_var.val) { atomic_var.val = other; } } + return old; +} #endif - private: - T val; -}; - #endif } // namespace cuopt diff --git a/cpp/tests/distance_engine/waypoint_matrix_test.cpp b/cpp/tests/distance_engine/waypoint_matrix_test.cpp index 80288bc6fa..1f9bacf0ec 100644 --- a/cpp/tests/distance_engine/waypoint_matrix_test.cpp +++ b/cpp/tests/distance_engine/waypoint_matrix_test.cpp @@ -44,7 +44,7 @@ class waypoint_matrix_waypoints_sequence_test_t this->expected_sequence_offsets = param.sequence_offsets; } - void TearDown() {} + void TearDown() override {} void test_compute_waypoint_sequence() { @@ -131,7 +131,7 @@ class waypoint_matrix_shortest_path_cost_t this->weights.data()); } - void TearDown() {} + void TearDown() override {} void test_compute_shortest_path_costs() { @@ -192,7 +192,7 @@ class waypoint_matrix_cost_matrix_test_t this->weights.data()); } - void TearDown() {} + void TearDown() override {} void test_compute_cost_matrix() { diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_test.c b/cpp/tests/linear_programming/c_api_tests/c_api_test.c index 25aef6d258..3b3032176b 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_test.c +++ b/cpp/tests/linear_programming/c_api_tests/c_api_test.c @@ -53,6 +53,7 @@ const char* termination_status_to_string(cuopt_int_t termination_status) case CUOPT_TERIMINATION_STATUS_FEASIBLE_FOUND: return "Feasible found"; } + return "Unknown"; } diff --git a/cpp/tests/routing/level0/l0_ges_test.cu b/cpp/tests/routing/level0/l0_ges_test.cu index 22373f7045..afd0a26276 100644 --- a/cpp/tests/routing/level0/l0_ges_test.cu +++ b/cpp/tests/routing/level0/l0_ges_test.cu @@ -55,7 +55,7 @@ class routing_ges_test_t : public ::testing::TestWithParam>, this->populate_device_vectors(); } - void TearDown() {} + void TearDown() override {} assignment_t solve(const cuopt::routing::data_model_view_t& data_model, const cuopt::routing::solver_settings_t& solver_settings, @@ -163,7 +163,7 @@ class simple_routes_ges_test_t : public ::testing::TestWithParampopulate_device_vectors(); } - void TearDown() {} + void TearDown() override {} assignment_t solve(const cuopt::routing::data_model_view_t& data_model, const cuopt::routing::solver_settings_t& solver_settings, diff --git a/cpp/tests/routing/level0/l0_objective_function_test.cu b/cpp/tests/routing/level0/l0_objective_function_test.cu index 9355750269..4491398497 100644 --- a/cpp/tests/routing/level0/l0_objective_function_test.cu +++ b/cpp/tests/routing/level0/l0_objective_function_test.cu @@ -25,7 +25,7 @@ template class objective_function_test_t : public base_test_t, public ::testing::TestWithParam> { public: - objective_function_test_t() : base_test_t(512, 5E-2, 0) {} + objective_function_test_t() : base_test_t(512, 0, 0) {} void SetUp() override { auto p = GetParam(); diff --git a/cpp/tests/routing/level0/l0_routing_test.cu b/cpp/tests/routing/level0/l0_routing_test.cu index 4d7bbad024..735b6e4bc1 100644 --- a/cpp/tests/routing/level0/l0_routing_test.cu +++ b/cpp/tests/routing/level0/l0_routing_test.cu @@ -320,7 +320,7 @@ class routing_retail_test_t : public base_test_t, this->populate_device_vectors(); } - void TearDown() {} + void TearDown() override {} void test_cvrptw() { diff --git a/cpp/tests/routing/level0/l0_vehicle_order_match.cu b/cpp/tests/routing/level0/l0_vehicle_order_match.cu index 6c4d40ab79..4b1b9fdd37 100644 --- a/cpp/tests/routing/level0/l0_vehicle_order_match.cu +++ b/cpp/tests/routing/level0/l0_vehicle_order_match.cu @@ -24,7 +24,7 @@ namespace test { template class vehicle_order_test_t : public base_test_t, public ::testing::TestWithParam { public: - vehicle_order_test_t() : base_test_t(512, 5E-2, 0) {} + vehicle_order_test_t() : base_test_t(512, 0, 0) {} void SetUp() override { this->not_matching_constraints_fraction = GetParam(); diff --git a/cpp/tests/routing/level0/l0_vehicle_types_test.cu b/cpp/tests/routing/level0/l0_vehicle_types_test.cu index f7f2476836..4e46d31d69 100644 --- a/cpp/tests/routing/level0/l0_vehicle_types_test.cu +++ b/cpp/tests/routing/level0/l0_vehicle_types_test.cu @@ -23,7 +23,7 @@ namespace test { template class vehicle_types_test_t : public base_test_t, public ::testing::Test { public: - vehicle_types_test_t() : base_test_t(512, 5E-2, 0) {} + vehicle_types_test_t() : base_test_t(512, 0, 0) {} void SetUp() override { this->n_locations = input_.n_locations; From 9975fdff946d06ad8032ed01c9f15de2510393aa Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 18 Nov 2025 12:34:27 +0000 Subject: [PATCH 75/86] remove debug calls --- cpp/src/mip/utilities/cpu_worker_thread.cuh | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/src/mip/utilities/cpu_worker_thread.cuh b/cpp/src/mip/utilities/cpu_worker_thread.cuh index 8d0b6d71ee..e437486cda 100644 --- a/cpp/src/mip/utilities/cpu_worker_thread.cuh +++ b/cpp/src/mip/utilities/cpu_worker_thread.cuh @@ -60,7 +60,6 @@ template cpu_worker_thread_base_t::~cpu_worker_thread_base_t() { // Note: We don't call on_terminate() here since the derived object is already destroyed. - CUOPT_LOG_DEBUG("Destroying CPU worker thread"); join_worker(); } @@ -91,7 +90,6 @@ void cpu_worker_thread_base_t::cpu_worker_thread() template void cpu_worker_thread_base_t::request_termination() { - CUOPT_LOG_DEBUG("Requesting termination of CPU worker thread"); bool should_terminate = false; { std::lock_guard lock(cpu_mutex); From f627a45030f6255c39308e2191b01855c858dc36 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 18 Nov 2025 14:30:11 +0100 Subject: [PATCH 76/86] replaced mutexes with call to update_tree method --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 7a7b05b3bd..653058470c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -661,9 +661,7 @@ node_status_t branch_and_bound_t::solve_node(search_tree_t& assert(leaf_vstatus.size() == leaf_problem.num_cols); search_tree.branch( node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, original_lp_, log); - search_tree.mutex.lock(); - node_ptr->status = node_status_t::HAS_CHILDREN; - search_tree.mutex.unlock(); + search_tree.update_tree(node_ptr, node_status_t::HAS_CHILDREN); return node_status_t::HAS_CHILDREN; } else { From 1bcc57f694a5a5dbc9543ef8bb2241870b7c2afa Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 19 Nov 2025 21:46:32 +0100 Subject: [PATCH 77/86] added missing workspace cleaning --- cpp/src/dual_simplex/basis_updates.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index 078dfffeb3..cea907074c 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -391,7 +391,11 @@ class basis_update_mpf_t { mu_values_.clear(); mu_values_.reserve(refactor_frequency_); num_updates_ = 0; + + std::fill(xi_workspace_.begin(), xi_workspace_.end(), 0); + std::fill(x_workspace_.begin(), x_workspace_.end(), 0.0); } + void grow_storage(i_t nz, i_t& S_start, i_t& S_nz); i_t index_map(i_t leaving) const; f_t u_diagonal(i_t j) const; From f687ce49cd54c1d1a99622e4cf2aef20c5f986c8 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 20 Nov 2025 10:03:55 +0100 Subject: [PATCH 78/86] small refactor --- cpp/src/dual_simplex/branch_and_bound.cpp | 90 +++++++++++------------ cpp/src/dual_simplex/branch_and_bound.hpp | 32 ++++---- cpp/src/dual_simplex/presolve.cpp | 2 + 3 files changed, 63 insertions(+), 61 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 110b13265b..eecafb14e4 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -200,10 +200,10 @@ inline const char* feasible_solution_symbol(thread_type_t type) } } -inline bool has_children(node_children_status_t status) +inline bool has_children(node_solve_info_t status) { - return status == node_children_status_t::UP_CHILDREN_FIRST || - status == node_children_status_t::DOWN_CHILDREN_FIRST; + return status == node_solve_info_t::UP_CHILD_FIRST || + status == node_solve_info_t::DOWN_CHILD_FIRST; } } // namespace @@ -575,7 +575,7 @@ rounding_direction_t branch_and_bound_t::child_selection(mip_node_t -node_children_status_t branch_and_bound_t::solve_node( +node_solve_info_t branch_and_bound_t::solve_node( mip_node_t* node_ptr, search_tree_t& search_tree, lp_problem_t& leaf_problem, @@ -672,7 +672,7 @@ node_children_status_t branch_and_bound_t::solve_node( node_ptr->lower_bound = inf; search_tree.graphviz_node(log, node_ptr, "infeasible", 0.0); search_tree.update(node_ptr, node_status_t::INFEASIBLE); - return node_children_status_t::NO_CHILDREN; + return node_solve_info_t::NO_CHILDREN; } else if (lp_status == dual::status_t::CUTOFF) { // Node was cut off. Do not branch @@ -680,7 +680,7 @@ node_children_status_t branch_and_bound_t::solve_node( f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); search_tree.graphviz_node(log, node_ptr, "cut off", leaf_objective); search_tree.update(node_ptr, node_status_t::FATHOMED); - return node_children_status_t::NO_CHILDREN; + return node_solve_info_t::NO_CHILDREN; } else if (lp_status == dual::status_t::OPTIMAL) { // LP was feasible @@ -704,7 +704,7 @@ node_children_status_t branch_and_bound_t::solve_node( add_feasible_solution(leaf_objective, leaf_solution.x, node_ptr->depth, thread_type); search_tree.graphviz_node(log, node_ptr, "integer feasible", leaf_objective); search_tree.update(node_ptr, node_status_t::INTEGER_FEASIBLE); - return node_children_status_t::NO_CHILDREN; + return node_solve_info_t::NO_CHILDREN; } else if (leaf_objective <= upper_bound + abs_fathom_tol) { // Choose fractional variable to branch on @@ -719,19 +719,19 @@ node_children_status_t branch_and_bound_t::solve_node( rounding_direction_t round_dir = child_selection(node_ptr); if (round_dir == rounding_direction_t::UP) { - return node_children_status_t::UP_CHILDREN_FIRST; + return node_solve_info_t::UP_CHILD_FIRST; } else { - return node_children_status_t::DOWN_CHILDREN_FIRST; + return node_solve_info_t::DOWN_CHILD_FIRST; } } else { search_tree.graphviz_node(log, node_ptr, "fathomed", leaf_objective); search_tree.update(node_ptr, node_status_t::FATHOMED); - return node_children_status_t::NO_CHILDREN; + return node_solve_info_t::NO_CHILDREN; } } else if (lp_status == dual::status_t::TIME_LIMIT) { search_tree.graphviz_node(log, node_ptr, "timeout", 0.0); - return node_children_status_t::TIME_LIMIT; + return node_solve_info_t::TIME_LIMIT; } else { if (thread_type == thread_type_t::EXPLORATION) { @@ -747,7 +747,7 @@ node_children_status_t branch_and_bound_t::solve_node( search_tree.graphviz_node(log, node_ptr, "numerical", 0.0); search_tree.update(node_ptr, node_status_t::NUMERICAL); - return node_children_status_t::NUMERICAL; + return node_solve_info_t::NUMERICAL; } } @@ -819,17 +819,17 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod std::vector row_sense; bounds_strengthening_t node_presolver(leaf_problem, Arow, row_sense, var_types_); - node_children_status_t status = solve_node(node, - *search_tree, - leaf_problem, - node_presolver, - thread_type_t::EXPLORATION, - true, - original_lp_.lower, - original_lp_.upper, - settings_.log); - - if (status == node_children_status_t::TIME_LIMIT) { + node_solve_info_t status = solve_node(node, + *search_tree, + leaf_problem, + node_presolver, + thread_type_t::EXPLORATION, + true, + original_lp_.lower, + original_lp_.upper, + settings_.log); + + if (status == node_solve_info_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; @@ -932,19 +932,19 @@ void branch_and_bound_t::explore_subtree(i_t task_id, return; } - node_children_status_t status = solve_node(node_ptr, - search_tree, - leaf_problem, - node_presolver, - thread_type_t::EXPLORATION, - recompute_bounds, - original_lp_.lower, - original_lp_.upper, - settings_.log); + node_solve_info_t status = solve_node(node_ptr, + search_tree, + leaf_problem, + node_presolver, + thread_type_t::EXPLORATION, + recompute_bounds, + original_lp_.lower, + original_lp_.upper, + settings_.log); recompute_bounds = !has_children(status); - if (status == node_children_status_t::TIME_LIMIT) { + if (status == node_solve_info_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; @@ -980,7 +980,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, exploration_stats_.nodes_unexplored += 2; - if (status == node_children_status_t::UP_CHILDREN_FIRST) { + if (status == node_solve_info_t::UP_CHILD_FIRST) { stack.push_front(node_ptr->get_down_child()); stack.push_front(node_ptr->get_up_child()); } else { @@ -1092,24 +1092,24 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A if (toc(exploration_stats_.start_time) > settings_.time_limit) { return; } - node_children_status_t status = solve_node(node_ptr, - subtree, - leaf_problem, - node_presolver, - thread_type_t::DIVING, - recompute_bounds, - start_node->lower, - start_node->upper, - log); + node_solve_info_t status = solve_node(node_ptr, + subtree, + leaf_problem, + node_presolver, + thread_type_t::DIVING, + recompute_bounds, + start_node->lower, + start_node->upper, + log); recompute_bounds = !has_children(status); - if (status == node_children_status_t::TIME_LIMIT) { + if (status == node_solve_info_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } else if (has_children(status)) { - if (status == node_children_status_t::UP_CHILDREN_FIRST) { + if (status == node_solve_info_t::UP_CHILD_FIRST) { stack.push_front(node_ptr->get_down_child()); stack.push_front(node_ptr->get_up_child()); } else { diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 3c0ffa54e9..6ad5b3d81d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -42,13 +42,13 @@ enum class mip_exploration_status_t { COMPLETED = 5, // The solver finished exploring the tree }; -enum class node_children_status_t { - NO_CHILDREN = 0, // The node does not produced children - UP_CHILDREN_FIRST = 1, // The up child should be explored first - DOWN_CHILDREN_FIRST = 2, // The down child should be explored first - TIME_LIMIT = 3, // The solver reached a time limit - ITERATION_LIMIT = 4, // The solver reached a iteration limit - NUMERICAL = 5 // The solver encounter a numerical error when solving the node +enum class node_solve_info_t { + NO_CHILDREN = 0, // The node does not produced children + UP_CHILD_FIRST = 1, // The up child should be explored first + DOWN_CHILD_FIRST = 2, // The down child should be explored first + TIME_LIMIT = 3, // The solver reached a time limit + ITERATION_LIMIT = 4, // The solver reached a iteration limit + NUMERICAL = 5 // The solver encounter a numerical error when solving the node }; // Indicate the search and variable selection algorithms used by the thread (See [1]). @@ -201,15 +201,15 @@ class branch_and_bound_t { void diving_thread(const csr_matrix_t& Arow); // Solve the LP relaxation of a leaf node and update the tree. - node_children_status_t solve_node(mip_node_t* node_ptr, - search_tree_t& search_tree, - lp_problem_t& leaf_problem, - bounds_strengthening_t& node_presolver, - thread_type_t thread_type, - bool recompute, - const std::vector& root_lower, - const std::vector& root_upper, - logger_t& log); + node_solve_info_t solve_node(mip_node_t* node_ptr, + search_tree_t& search_tree, + lp_problem_t& leaf_problem, + bounds_strengthening_t& node_presolver, + thread_type_t thread_type, + bool recompute, + const std::vector& root_lower, + const std::vector& root_upper, + logger_t& log); // Sort the children based on the Martin's criteria. rounding_direction_t child_selection(mip_node_t* node_ptr); diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 10e7d6358e..5cfe252085 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -572,6 +572,8 @@ void convert_user_problem(const user_problem_t& user_problem, csr_matrix_t Arow(1, 1, 1); problem.A.to_compressed_row(Arow); + settings.log.printf("Running bound strengthening\n"); + // Empty var_types means that all variables are continuous bounds_strengthening_t strengthening(problem, Arow, row_sense, {}); std::fill(strengthening.bounds_changed.begin(), strengthening.bounds_changed.end(), true); From 1048327221206c3b2def98c7eec11dcad4b15b5c Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 20 Nov 2025 14:21:35 +0100 Subject: [PATCH 79/86] fixed crash due to selecting the incorrect last column --- cpp/src/dual_simplex/basis_updates.cpp | 6 +++++- cpp/src/dual_simplex/sparse_matrix.cpp | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index fff33b8a58..a10c7ab42b 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1193,7 +1193,7 @@ i_t basis_update_mpf_t::scatter_into_workspace(const sparse_vector_t void basis_update_mpf_t::grow_storage(i_t nz, i_t& S_start, i_t& S_nz) { - const i_t last_S_col = num_updates_ * 2; + const i_t last_S_col = S_.n; const i_t new_last_S_col = last_S_col + 2; if (new_last_S_col >= S_.col_start.size()) { S_.col_start.resize(new_last_S_col + refactor_frequency_); @@ -1204,6 +1204,8 @@ void basis_update_mpf_t::grow_storage(i_t nz, i_t& S_start, i_t& S_nz) S_.x.resize(std::max(2 * S_nz, S_nz + nz)); } S_start = last_S_col; + assert(S_nz + nz <= S_.i.size()); + assert(S_nz + nz <= S_.x.size()); } template @@ -1915,6 +1917,8 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde i_t S_nz; grow_storage(nz + etilde.i.size(), S_start, S_nz); + if (nz + etilde.i.size() > S_.i.size()) {} + S_.append_column(nz, xi_workspace_.data() + m, x_workspace_.data()); // Gather etilde into a column of S diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index cdd45f720e..4e522d9daf 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -157,6 +157,8 @@ void csc_matrix_t::append_column(const std::vector& x) i_t nz = this->col_start[this->n]; for (i_t j = 0; j < xsz; ++j) { if (x[j] != 0.0) { + assert(nz < this->i.size()); + assert(nz < this->x.size()); this->i[nz] = j; this->x[nz] = x[j]; nz++; @@ -177,6 +179,8 @@ void csc_matrix_t::append_column(const sparse_vector_t& x) const i_t i = x.i[k]; const f_t x_val = x.x[k]; if (x_val != 0.0) { + assert(nz < this->i.size()); + assert(nz < this->x.size()); this->i[nz] = i; this->x[nz] = x_val; nz++; @@ -194,6 +198,8 @@ void csc_matrix_t::append_column(i_t x_nz, i_t* i, f_t* x) const i_t i_val = i[k]; const f_t x_val = x[i_val]; if (x_val != 0.0) { + assert(nz < this->i.size()); + assert(nz < this->x.size()); this->i[nz] = i_val; this->x[nz] = x_val; nz++; From 8b4546eb8d1a0c5720df5ee7c5a0e485c7f1d46b Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 20 Nov 2025 17:14:52 +0100 Subject: [PATCH 80/86] reverting fix --- cpp/src/dual_simplex/basis_updates.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index a10c7ab42b..2767da65de 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1193,7 +1193,8 @@ i_t basis_update_mpf_t::scatter_into_workspace(const sparse_vector_t void basis_update_mpf_t::grow_storage(i_t nz, i_t& S_start, i_t& S_nz) { - const i_t last_S_col = S_.n; + const i_t last_S_col = 2 * num_updates_; + assert(S_.n == last_S_col); const i_t new_last_S_col = last_S_col + 2; if (new_last_S_col >= S_.col_start.size()) { S_.col_start.resize(new_last_S_col + refactor_frequency_); From 3240f889645cf381b5572b8fc5e28c512ddadc52 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 20 Nov 2025 18:13:30 +0100 Subject: [PATCH 81/86] fixed missing vector if the matrix is rank deficient and there is no singleton --- cpp/src/dual_simplex/basis_solves.cpp | 15 ++++++++++++++- cpp/src/dual_simplex/basis_updates.cpp | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index d0f82967d4..db24f55a25 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -566,6 +566,20 @@ i_t factorize_basis(const csc_matrix_t& A, q.resize(m); f_t fact_start = tic(); rank = right_looking_lu(A, settings, medium_tol, basic_list, q, L, U, pinv); + inverse_permutation(pinv, p); + if (rank != m) { + // Get the rank deficient columns + deficient.clear(); + deficient.resize(m - rank); + for (i_t h = rank; h < m; ++h) { + deficient[h - rank] = q[h]; + } + // Get the slacks needed + slacks_needed.resize(m - rank); + for (i_t h = rank; h < m; ++h) { + slacks_needed[h - rank] = p[h]; + } + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Concurrent halt\n"); return -1; @@ -573,7 +587,6 @@ i_t factorize_basis(const csc_matrix_t& A, if (verbose) { printf("Right Lnz+Unz %d t %.3f\n", L.col_start[m] + U.col_start[m], toc(fact_start)); } - inverse_permutation(pinv, p); constexpr bool check_lu = false; if (check_lu) { csc_matrix_t C(m, m, 1); diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 2767da65de..931c9302f8 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2100,6 +2100,8 @@ int basis_update_mpf_t::refactor_basis( #ifdef CHECK_L_FACTOR if (L0_.check_matrix() == -1) { settings.log.printf("Bad L after basis repair\n"); } #endif + + assert(deficient.size() > 0); return deficient.size(); } settings.log.debug("Basis repaired\n"); From 8b879a379a268eb01e93c251758685fb20110ad6 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 21 Nov 2025 14:13:56 +0100 Subject: [PATCH 82/86] removed debug leftover --- cpp/src/dual_simplex/basis_updates.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 200d4b219b..6b79f3c862 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1918,8 +1918,6 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde i_t S_nz; grow_storage(nz + etilde.i.size(), S_start, S_nz); - if (nz + etilde.i.size() > S_.i.size()) {} - S_.append_column(nz, xi_workspace_.data() + m, x_workspace_.data()); // Gather etilde into a column of S From a8772a272a8be95a6051a5abf746eca3c6c2589f Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 24 Nov 2025 10:41:59 +0100 Subject: [PATCH 83/86] removing unrelated changes --- .coderabbit.yaml | 8 ++-- build.sh | 16 +------ ci/release/update-version.sh | 1 - ci/tsan_suppressions.txt | 6 --- .../all_cuda-129_arch-aarch64.yaml | 1 - .../all_cuda-129_arch-x86_64.yaml | 1 - .../all_cuda-130_arch-aarch64.yaml | 1 - .../all_cuda-130_arch-x86_64.yaml | 1 - conda/recipes/cuopt/recipe.yaml | 1 - cpp/CMakeLists.txt | 19 +------- cpp/include/cuopt/error.hpp | 2 + .../utilities/internals.hpp | 1 - cpp/src/linear_programming/pdlp.cu | 2 +- .../utilities/cython_solve.cu | 3 +- cpp/src/linear_programming/utils.cuh | 6 +-- cpp/src/mip/diversity/lns/rins.cu | 4 +- cpp/src/mip/local_search/local_search.cu | 3 +- cpp/src/mip/presolve/gf2_presolve.hpp | 4 -- cpp/src/mip/presolve/third_party_presolve.cpp | 4 -- cpp/src/mip/utilities/cpu_worker_thread.cuh | 6 +-- cpp/src/routing/crossovers/ox_recombiner.cuh | 4 +- cpp/src/utilities/omp_helpers.hpp | 48 ++++++++----------- .../distance_engine/waypoint_matrix_test.cpp | 6 +-- .../c_api_tests/c_api_test.c | 1 - cpp/tests/routing/level0/l0_ges_test.cu | 4 +- .../level0/l0_objective_function_test.cu | 2 +- cpp/tests/routing/level0/l0_routing_test.cu | 2 +- .../routing/level0/l0_vehicle_order_match.cu | 2 +- .../routing/level0/l0_vehicle_types_test.cu | 2 +- dependencies.yaml | 30 +----------- docs/cuopt/source/lp-milp-settings.rst | 10 ++-- python/cuopt/cuopt/routing/utils.py | 5 -- python/cuopt/pyproject.toml | 1 - 33 files changed, 58 insertions(+), 149 deletions(-) delete mode 100644 ci/tsan_suppressions.txt diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 855ebf1726..0361943b69 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -9,11 +9,9 @@ reviews: enabled: true drafts: false base_branches: - - main - - master - - develop - - "release/**" - - "hotfix/**" + - "^main$" + - "^release/.*" + - "^hotfix/.*" ignore_usernames: ["rapids-bot", "GPUtester", "nv-automation-bot", "copy-pr-bot"] tools: markdownlint: diff --git a/build.sh b/build.sh index e040baa958..2903684f6f 100755 --- a/build.sh +++ b/build.sh @@ -15,7 +15,7 @@ REPODIR=$(cd "$(dirname "$0")"; pwd) LIBCUOPT_BUILD_DIR=${LIBCUOPT_BUILD_DIR:=${REPODIR}/cpp/build} LIBMPS_PARSER_BUILD_DIR=${LIBMPS_PARSER_BUILD_DIR:=${REPODIR}/cpp/libmps_parser/build} -VALIDARGS="clean libcuopt libmps_parser cuopt_mps_parser cuopt cuopt_server cuopt_sh_client docs deb -a -b -g -fsanitize -tsan -v -l= --verbose-pdlp --build-lp-only --no-fetch-rapids --skip-c-python-adapters --skip-tests-build --skip-routing-build --skip-fatbin-write --host-lineinfo [--cmake-args=\\\"\\\"] [--cache-tool=] -n --allgpuarch --ci-only-arch --show_depr_warn -h --help" +VALIDARGS="clean libcuopt libmps_parser cuopt_mps_parser cuopt cuopt_server cuopt_sh_client docs deb -a -b -g -fsanitize -v -l= --verbose-pdlp --build-lp-only --no-fetch-rapids --skip-c-python-adapters --skip-tests-build --skip-routing-build --skip-fatbin-write --host-lineinfo [--cmake-args=\\\"\\\"] [--cache-tool=] -n --allgpuarch --ci-only-arch --show_depr_warn -h --help" HELP="$0 [ ...] [ ...] where is: clean - remove all existing build artifacts and configuration (start over) @@ -32,8 +32,7 @@ HELP="$0 [ ...] [ ...] -g - build for debug -a - Enable assertion (by default in debug mode) -b - Build with benchmark settings - -fsanitize - Build with AddressSanitizer and UndefinedBehaviorSanitizer - -tsan - Build with ThreadSanitizer (cannot be used with -fsanitize) + -fsanitize - Build with sanitizer -n - no install step --no-fetch-rapids - don't fetch rapids dependencies -l= - log level. Options are: TRACE | DEBUG | INFO | WARN | ERROR | CRITICAL | OFF. Default=INFO @@ -77,7 +76,6 @@ BUILD_ALL_GPU_ARCH=0 BUILD_CI_ONLY=0 BUILD_LP_ONLY=0 BUILD_SANITIZER=0 -BUILD_TSAN=0 SKIP_C_PYTHON_ADAPTERS=0 SKIP_TESTS_BUILD=0 SKIP_ROUTING_BUILD=0 @@ -232,9 +230,6 @@ fi if hasArg -fsanitize; then BUILD_SANITIZER=1 fi -if hasArg -tsan; then - BUILD_TSAN=1 -fi if hasArg --skip-c-python-adapters; then SKIP_C_PYTHON_ADAPTERS=1 fi @@ -303,12 +298,6 @@ if [ ${BUILD_LP_ONLY} -eq 1 ] && [ ${SKIP_C_PYTHON_ADAPTERS} -eq 0 ]; then exit 1 fi -if [ ${BUILD_SANITIZER} -eq 1 ] && [ ${BUILD_TSAN} -eq 1 ]; then - echo "ERROR: -fsanitize and -tsan cannot be used together" - echo "AddressSanitizer and ThreadSanitizer are mutually exclusive" - exit 1 -fi - if [ ${BUILD_ALL_GPU_ARCH} -eq 1 ]; then CUOPT_CMAKE_CUDA_ARCHITECTURES="RAPIDS" echo "Building for *ALL* supported GPU architectures..." @@ -355,7 +344,6 @@ if buildAll || hasArg libcuopt; then -DFETCH_RAPIDS=${FETCH_RAPIDS} \ -DBUILD_LP_ONLY=${BUILD_LP_ONLY} \ -DBUILD_SANITIZER=${BUILD_SANITIZER} \ - -DBUILD_TSAN=${BUILD_TSAN} \ -DSKIP_C_PYTHON_ADAPTERS=${SKIP_C_PYTHON_ADAPTERS} \ -DBUILD_TESTS=$((1 - ${SKIP_TESTS_BUILD})) \ -DSKIP_ROUTING_BUILD=${SKIP_ROUTING_BUILD} \ diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index ba38c383d2..c266c7f5aa 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -99,7 +99,6 @@ echo "${RAPIDS_BRANCH_NAME}" > RAPIDS_BRANCH DEPENDENCIES=( cudf - cuvs cuopt cuopt-mps-parser cuopt-server diff --git a/ci/tsan_suppressions.txt b/ci/tsan_suppressions.txt deleted file mode 100644 index b6f413e370..0000000000 --- a/ci/tsan_suppressions.txt +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. -# SPDX-License-Identifier: Apache-2.0 - -# Ignore races in external header-only libraries -race:tbb -race:Papilo diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index f87e9981d7..59d6b43c0d 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -21,7 +21,6 @@ dependencies: - cuda-version=12.9 - cudf==25.12.*,>=0.0.0a0 - cupy>=13.6.0 -- cuvs==25.12.*,>=0.0.0a0 - cxx-compiler - cython>=3.0.3 - doxygen=1.9.1 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 8105bf9aba..4aad50d006 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -21,7 +21,6 @@ dependencies: - cuda-version=12.9 - cudf==25.12.*,>=0.0.0a0 - cupy>=13.6.0 -- cuvs==25.12.*,>=0.0.0a0 - cxx-compiler - cython>=3.0.3 - doxygen=1.9.1 diff --git a/conda/environments/all_cuda-130_arch-aarch64.yaml b/conda/environments/all_cuda-130_arch-aarch64.yaml index bf626dd520..5adb8aeaef 100644 --- a/conda/environments/all_cuda-130_arch-aarch64.yaml +++ b/conda/environments/all_cuda-130_arch-aarch64.yaml @@ -21,7 +21,6 @@ dependencies: - cuda-version=13.0 - cudf==25.12.*,>=0.0.0a0 - cupy>=13.6.0 -- cuvs==25.12.*,>=0.0.0a0 - cxx-compiler - cython>=3.0.3 - doxygen=1.9.1 diff --git a/conda/environments/all_cuda-130_arch-x86_64.yaml b/conda/environments/all_cuda-130_arch-x86_64.yaml index 72691938c5..11c24d5890 100644 --- a/conda/environments/all_cuda-130_arch-x86_64.yaml +++ b/conda/environments/all_cuda-130_arch-x86_64.yaml @@ -21,7 +21,6 @@ dependencies: - cuda-version=13.0 - cudf==25.12.*,>=0.0.0a0 - cupy>=13.6.0 -- cuvs==25.12.*,>=0.0.0a0 - cxx-compiler - cython>=3.0.3 - doxygen=1.9.1 diff --git a/conda/recipes/cuopt/recipe.yaml b/conda/recipes/cuopt/recipe.yaml index 1c9df12707..229510f5df 100644 --- a/conda/recipes/cuopt/recipe.yaml +++ b/conda/recipes/cuopt/recipe.yaml @@ -74,7 +74,6 @@ requirements: - cudf =${{ minor_version }} - cuopt-mps-parser =${{ version }} - cupy >=13.6.0 - - cuvs =${{ minor_version }} - h5py - libcuopt =${{ version }} - numba >=0.60.0 diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index f770d80c81..4391c53df7 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -81,22 +81,9 @@ endif(CMAKE_COMPILER_IS_GNUCXX) # 2. (Optional) To run with a debugger (gdb or cuda-gdb) use the additional ASAN option alloc_dealloc_mismatch=0 if(BUILD_SANITIZER) list(APPEND CUOPT_CXX_FLAGS -fsanitize=address,undefined -fno-omit-frame-pointer -g -Wno-error=maybe-uninitialized) - if(NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - list(APPEND CUOPT_CXX_FLAGS -Wno-error=maybe-uninitialized) - endif() add_link_options(-fsanitize=address,undefined) endif(BUILD_SANITIZER) -# To use ThreadSanitizer: -# 1. Build with clang and the -tsan flag -# 2. Run the binary with env var set: OMP_TOOL_LIBRARIES=/usr/lib/llvm-17/lib/libarcher.so ARCHER_OPTIONS='verbose=1' TSAN_OPTIONS='suppresions=ci/tsan_suppressions.txt:ignore_noninstrumented_modules=1:halt_on_error=1' -# Replace with local llvm install path. libarcher.so must be presetn -if(BUILD_TSAN) - message(STATUS "Building with ThreadSanitizer enabled") - list(APPEND CUOPT_CXX_FLAGS -fsanitize=thread -fno-omit-frame-pointer -g) - add_link_options(-fsanitize=thread) -endif(BUILD_TSAN) - if(DEFINE_ASSERT) add_definitions(-DASSERT_MODE) endif(DEFINE_ASSERT) @@ -130,11 +117,7 @@ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -static-global-template-stub=false") endif() list(APPEND CUOPT_CUDA_FLAGS -Werror=cross-execution-space-call -Wno-deprecated-declarations -Xcompiler=-Werror --default-stream=per-thread) -if("${CMAKE_CUDA_HOST_COMPILER}" MATCHES "clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - list(APPEND CUOPT_CUDA_FLAGS -Xcompiler=-Wall) -else() - list(APPEND CUOPT_CUDA_FLAGS -Xcompiler=-Wall -Wno-error=non-template-friend) -endif() +list(APPEND CUOPT_CUDA_FLAGS -Xcompiler=-Wall -Wno-error=non-template-friend) list(APPEND CUOPT_CUDA_FLAGS -Xfatbin=-compress-all) if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.9 AND CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 13.0) list(APPEND CUOPT_CUDA_FLAGS -Xfatbin=--compress-level=3) diff --git a/cpp/include/cuopt/error.hpp b/cpp/include/cuopt/error.hpp index a83413515e..b6086245db 100644 --- a/cpp/include/cuopt/error.hpp +++ b/cpp/include/cuopt/error.hpp @@ -33,6 +33,8 @@ enum class error_type_t { */ struct logic_error : public std::logic_error { + explicit logic_error() = default; + logic_error(const logic_error& exception) = default; // Move constructor diff --git a/cpp/include/cuopt/linear_programming/utilities/internals.hpp b/cpp/include/cuopt/linear_programming/utilities/internals.hpp index 86e7246faf..84c96a7164 100644 --- a/cpp/include/cuopt/linear_programming/utilities/internals.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/internals.hpp @@ -62,7 +62,6 @@ namespace linear_programming { class base_solution_t { public: - virtual ~base_solution_t() = default; virtual bool is_mip() const = 0; }; diff --git a/cpp/src/linear_programming/pdlp.cu b/cpp/src/linear_programming/pdlp.cu index d78f1d1f48..5d982bcc96 100644 --- a/cpp/src/linear_programming/pdlp.cu +++ b/cpp/src/linear_programming/pdlp.cu @@ -1510,7 +1510,7 @@ void pdlp_solver_t::compute_initial_step_size() const auto& cusparse_view_ = pdhg_solver_.get_cusparse_view(); - [[maybe_unused]] int sing_iters = 0; + int sing_iters = 0; for (int i = 0; i < max_iterations; ++i) { ++sing_iters; // d_q = d_z diff --git a/cpp/src/linear_programming/utilities/cython_solve.cu b/cpp/src/linear_programming/utilities/cython_solve.cu index 38968c0508..111f3caf04 100644 --- a/cpp/src/linear_programming/utilities/cython_solve.cu +++ b/cpp/src/linear_programming/utilities/cython_solve.cu @@ -295,7 +295,8 @@ std::pair>, double> call_batch_solve( #pragma omp parallel for num_threads(max_thread) for (std::size_t i = 0; i < size; ++i) - list[i] = call_solve(data_models[i], solver_settings, cudaStreamNonBlocking, is_batch_mode); + list[i] = + std::move(call_solve(data_models[i], solver_settings, cudaStreamNonBlocking, is_batch_mode)); auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast(end - start_solver); diff --git a/cpp/src/linear_programming/utils.cuh b/cpp/src/linear_programming/utils.cuh index 2333283f39..0da5d25ceb 100644 --- a/cpp/src/linear_programming/utils.cuh +++ b/cpp/src/linear_programming/utils.cuh @@ -62,9 +62,9 @@ struct max_abs_value { template i_t conditional_major(uint64_t total_pdlp_iterations) { - uint64_t step = 10; - uint64_t threshold = 1000; - [[maybe_unused]] uint64_t iteration = 0; + uint64_t step = 10; + uint64_t threshold = 1000; + uint64_t iteration = 0; [[maybe_unused]] constexpr uint64_t max_u64 = std::numeric_limits::max(); diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 0d2da90cde..1a79bdb053 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -246,8 +246,8 @@ void rins_t::run_rins() branch_and_bound_settings.num_diving_threads = 1; branch_and_bound_settings.log.log = false; branch_and_bound_settings.log.log_prefix = "[RINS] "; - branch_and_bound_settings.solution_callback = [&rins_solution_queue](std::vector& solution, - f_t objective) { + branch_and_bound_settings.solution_callback = [this, &rins_solution_queue]( + std::vector& solution, f_t objective) { rins_solution_queue.push_back(solution); }; dual_simplex::branch_and_bound_t branch_and_bound(branch_and_bound_problem, diff --git a/cpp/src/mip/local_search/local_search.cu b/cpp/src/mip/local_search/local_search.cu index 6d64f62ab1..a8e06440aa 100644 --- a/cpp/src/mip/local_search/local_search.cu +++ b/cpp/src/mip/local_search/local_search.cu @@ -80,7 +80,8 @@ void local_search_t::start_cpufj_scratch_threads(population_t 0); cpu_fj.fj_cpu->log_prefix = "******* scratch " + std::to_string(counter) + ": "; - cpu_fj.fj_cpu->improvement_callback = [&population](f_t obj, const std::vector& h_vec) { + cpu_fj.fj_cpu->improvement_callback = [this, &population, &cpu_fj]( + f_t obj, const std::vector& h_vec) { population.add_external_solution(h_vec, obj, solution_origin_t::CPUFJ); if (obj < local_search_best_obj) { CUOPT_LOG_TRACE("******* New local search best obj %g, best overall %g", diff --git a/cpp/src/mip/presolve/gf2_presolve.hpp b/cpp/src/mip/presolve/gf2_presolve.hpp index 53e79d2c93..19d4e7d813 100644 --- a/cpp/src/mip/presolve/gf2_presolve.hpp +++ b/cpp/src/mip/presolve/gf2_presolve.hpp @@ -7,17 +7,13 @@ #pragma once -#if !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstringop-overflow" // ignore boost error for pip wheel build -#endif #include #include #include #include -#if !defined(__clang__) #pragma GCC diagnostic pop -#endif namespace cuopt::linear_programming::detail { diff --git a/cpp/src/mip/presolve/third_party_presolve.cpp b/cpp/src/mip/presolve/third_party_presolve.cpp index f3faf3dd7d..22827c6e28 100644 --- a/cpp/src/mip/presolve/third_party_presolve.cpp +++ b/cpp/src/mip/presolve/third_party_presolve.cpp @@ -14,15 +14,11 @@ #include -#if !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstringop-overflow" // ignore boost error for pip wheel build -#endif #include #include -#if !defined(__clang__) #pragma GCC diagnostic pop -#endif namespace cuopt::linear_programming::detail { diff --git a/cpp/src/mip/utilities/cpu_worker_thread.cuh b/cpp/src/mip/utilities/cpu_worker_thread.cuh index e437486cda..0f1671c94a 100644 --- a/cpp/src/mip/utilities/cpu_worker_thread.cuh +++ b/cpp/src/mip/utilities/cpu_worker_thread.cuh @@ -83,7 +83,6 @@ void cpu_worker_thread_base_t::cpu_worker_thread() std::lock_guard lock(cpu_mutex); cpu_thread_done = true; } - cpu_cv.notify_all(); } } @@ -132,8 +131,9 @@ void cpu_worker_thread_base_t::start_cpu_solver() template bool cpu_worker_thread_base_t::wait_for_cpu_solver() { - std::unique_lock lock(cpu_mutex); - cpu_cv.wait(lock, [this] { return cpu_thread_done || cpu_thread_terminate; }); + while (!cpu_thread_done && !cpu_thread_terminate) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } return static_cast(this)->get_result(); } diff --git a/cpp/src/routing/crossovers/ox_recombiner.cuh b/cpp/src/routing/crossovers/ox_recombiner.cuh index 7f965de2ff..17823c28b8 100644 --- a/cpp/src/routing/crossovers/ox_recombiner.cuh +++ b/cpp/src/routing/crossovers/ox_recombiner.cuh @@ -336,7 +336,7 @@ struct OX { int i = routes_number; if (optimal_routes_search) { i = optimal_routes_number; } int end_index = offspring.size() - 1; - [[maybe_unused]] double cost_n, cost_p, total_delta = 0.; + double cost_n, cost_p, total_delta = 0.; std::vector>>> routes_to_add; std::vector tmp_route; @@ -530,7 +530,7 @@ struct OX { "Mismatch number of edges"); for (size_t j = 0; j < h_transpose_graph[i].size(); ++j) { auto [ref_edge, ref_weight, ref_veh] = h_transpose_graph[i][j]; - [[maybe_unused]] bool found = false; + bool found = false; for (int x = 0; x < tmp_transpose.row_sizes[i]; ++x) { auto edge = tmp_transpose.indices[transpose_offset + x]; auto veh = tmp_transpose.buckets[transpose_offset + x]; diff --git a/cpp/src/utilities/omp_helpers.hpp b/cpp/src/utilities/omp_helpers.hpp index 33eda66cba..1ec95583ae 100644 --- a/cpp/src/utilities/omp_helpers.hpp +++ b/cpp/src/utilities/omp_helpers.hpp @@ -91,46 +91,38 @@ class omp_atomic_t { T fetch_sub(T inc) { return fetch_add(-inc); } - private: - T val; - -#ifndef __NVCC__ - friend double fetch_min(omp_atomic_t& atomic_var, double other); - friend double fetch_max(omp_atomic_t& atomic_var, double other); -#endif -}; - // Atomic CAS are only supported in OpenMP v5.1 // (gcc 12+ or clang 14+), however, nvcc (or the host compiler) cannot // parse it correctly yet #ifndef __NVCC__ -// 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; } + T old; +#pragma omp atomic compare capture + { + old = val; + val = other < val ? other : val; + } + return old; } - return old; -} -inline double fetch_max(omp_atomic_t& atomic_var, double 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; + val = other > val ? other : val; + } + return old; } - return old; -} #endif + private: + T val; +}; + #endif } // namespace cuopt diff --git a/cpp/tests/distance_engine/waypoint_matrix_test.cpp b/cpp/tests/distance_engine/waypoint_matrix_test.cpp index 1f9bacf0ec..80288bc6fa 100644 --- a/cpp/tests/distance_engine/waypoint_matrix_test.cpp +++ b/cpp/tests/distance_engine/waypoint_matrix_test.cpp @@ -44,7 +44,7 @@ class waypoint_matrix_waypoints_sequence_test_t this->expected_sequence_offsets = param.sequence_offsets; } - void TearDown() override {} + void TearDown() {} void test_compute_waypoint_sequence() { @@ -131,7 +131,7 @@ class waypoint_matrix_shortest_path_cost_t this->weights.data()); } - void TearDown() override {} + void TearDown() {} void test_compute_shortest_path_costs() { @@ -192,7 +192,7 @@ class waypoint_matrix_cost_matrix_test_t this->weights.data()); } - void TearDown() override {} + void TearDown() {} void test_compute_cost_matrix() { diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_test.c b/cpp/tests/linear_programming/c_api_tests/c_api_test.c index 3b3032176b..25aef6d258 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_test.c +++ b/cpp/tests/linear_programming/c_api_tests/c_api_test.c @@ -53,7 +53,6 @@ const char* termination_status_to_string(cuopt_int_t termination_status) case CUOPT_TERIMINATION_STATUS_FEASIBLE_FOUND: return "Feasible found"; } - return "Unknown"; } diff --git a/cpp/tests/routing/level0/l0_ges_test.cu b/cpp/tests/routing/level0/l0_ges_test.cu index afd0a26276..22373f7045 100644 --- a/cpp/tests/routing/level0/l0_ges_test.cu +++ b/cpp/tests/routing/level0/l0_ges_test.cu @@ -55,7 +55,7 @@ class routing_ges_test_t : public ::testing::TestWithParam>, this->populate_device_vectors(); } - void TearDown() override {} + void TearDown() {} assignment_t solve(const cuopt::routing::data_model_view_t& data_model, const cuopt::routing::solver_settings_t& solver_settings, @@ -163,7 +163,7 @@ class simple_routes_ges_test_t : public ::testing::TestWithParampopulate_device_vectors(); } - void TearDown() override {} + void TearDown() {} assignment_t solve(const cuopt::routing::data_model_view_t& data_model, const cuopt::routing::solver_settings_t& solver_settings, diff --git a/cpp/tests/routing/level0/l0_objective_function_test.cu b/cpp/tests/routing/level0/l0_objective_function_test.cu index 4491398497..9355750269 100644 --- a/cpp/tests/routing/level0/l0_objective_function_test.cu +++ b/cpp/tests/routing/level0/l0_objective_function_test.cu @@ -25,7 +25,7 @@ template class objective_function_test_t : public base_test_t, public ::testing::TestWithParam> { public: - objective_function_test_t() : base_test_t(512, 0, 0) {} + objective_function_test_t() : base_test_t(512, 5E-2, 0) {} void SetUp() override { auto p = GetParam(); diff --git a/cpp/tests/routing/level0/l0_routing_test.cu b/cpp/tests/routing/level0/l0_routing_test.cu index 735b6e4bc1..4d7bbad024 100644 --- a/cpp/tests/routing/level0/l0_routing_test.cu +++ b/cpp/tests/routing/level0/l0_routing_test.cu @@ -320,7 +320,7 @@ class routing_retail_test_t : public base_test_t, this->populate_device_vectors(); } - void TearDown() override {} + void TearDown() {} void test_cvrptw() { diff --git a/cpp/tests/routing/level0/l0_vehicle_order_match.cu b/cpp/tests/routing/level0/l0_vehicle_order_match.cu index 4b1b9fdd37..6c4d40ab79 100644 --- a/cpp/tests/routing/level0/l0_vehicle_order_match.cu +++ b/cpp/tests/routing/level0/l0_vehicle_order_match.cu @@ -24,7 +24,7 @@ namespace test { template class vehicle_order_test_t : public base_test_t, public ::testing::TestWithParam { public: - vehicle_order_test_t() : base_test_t(512, 0, 0) {} + vehicle_order_test_t() : base_test_t(512, 5E-2, 0) {} void SetUp() override { this->not_matching_constraints_fraction = GetParam(); diff --git a/cpp/tests/routing/level0/l0_vehicle_types_test.cu b/cpp/tests/routing/level0/l0_vehicle_types_test.cu index 4e46d31d69..f7f2476836 100644 --- a/cpp/tests/routing/level0/l0_vehicle_types_test.cu +++ b/cpp/tests/routing/level0/l0_vehicle_types_test.cu @@ -23,7 +23,7 @@ namespace test { template class vehicle_types_test_t : public base_test_t, public ::testing::Test { public: - vehicle_types_test_t() : base_test_t(512, 0, 0) {} + vehicle_types_test_t() : base_test_t(512, 5E-2, 0) {} void SetUp() override { this->n_locations = input_.n_locations; diff --git a/dependencies.yaml b/dependencies.yaml index 78e4f7628c..e4b49c9748 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Dependency list for https://github.com/rapidsai/dependency-file-generator @@ -32,7 +32,6 @@ files: - depends_on_rmm - depends_on_cupy - depends_on_cudf - - depends_on_cuvs - depends_on_pylibraft - depends_on_rapids_logger - rapids_build_backend @@ -181,7 +180,6 @@ files: - depends_on_mps_parser - depends_on_rmm - depends_on_cudf - - depends_on_cuvs - depends_on_pylibraft - depends_on_rapids_logger py_test_cuopt: @@ -620,32 +618,6 @@ dependencies: packages: - *cudf_unsuffixed - depends_on_cuvs: - common: - - output_types: conda - packages: - - &cuvs_unsuffixed cuvs==25.12.*,>=0.0.0a0 - - output_types: requirements - packages: - - --extra-index-url=https://pypi.nvidia.com - - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple - specific: - - output_types: [requirements, pyproject] - matrices: - - matrix: - cuda: "12.*" - cuda_suffixed: "true" - packages: - - cuvs-cu12==25.12.*,>=0.0.0a0 - - matrix: - cuda: "13.*" - cuda_suffixed: "true" - packages: - - cuvs-cu13==25.12.*,>=0.0.0a0 - - matrix: - packages: - - *cuvs_unsuffixed - depends_on_pylibraft: common: - output_types: conda diff --git a/docs/cuopt/source/lp-milp-settings.rst b/docs/cuopt/source/lp-milp-settings.rst index 2b4fdf3f9f..5113ff4828 100644 --- a/docs/cuopt/source/lp-milp-settings.rst +++ b/docs/cuopt/source/lp-milp-settings.rst @@ -100,7 +100,7 @@ Server Thin client users should use the :class:`cuopt_sh_client.SolverMethod` fo PDLP Solver Mode ^^^^^^^^^^^^^^^^ -``CUOPT_PDLP_MODE`` controls the mode under which PDLP should operate. The mode will change the way the +``CUOPT_PDLP_SOLVER_MODE`` controls the mode under which PDLP should operate. The mode will change the way the PDLP internally optimizes the problem. The mode choice can drastically impact how fast a specific problem will be solved. Users are encouraged to test different modes to see which one fits the best their problem. By default, the solver uses ``Stable3``, the best @@ -126,13 +126,13 @@ cuOpt will stop at the first limit (iteration or time) reached. or proves the problem is infeasible or unbounded. -Infeasiblity Detection -^^^^^^^^^^^^^^^^^^^^^^ +Infeasibility Detection +^^^^^^^^^^^^^^^^^^^^^^^^ ``CUOPT_INFEASIBILITY_DETECTION`` controls whether PDLP should detect infeasibility. Note that infeasibility detection in PDLP is not always accurate. Some problems detected as infeasible may converge under a different tolerance factor. Detecting infeasibility consumes both more runtime and memory. The added runtime is between 3% and 7%, -the added memory consumpution is between 10% and 20%. +the added memory consumption is between 10% and 20%. .. note:: by default PDLP will not detect infeasibility. Dual simplex will always detect infeasibility regardless of this setting. @@ -386,7 +386,7 @@ Relative Tolerance Integrality Tolerance ^^^^^^^^^^^^^^^^^^^^^ -``CUOPT_INTEGRALITY_TOLERANCE`` controls the MIP integrality tolerance. A variable is considered to be integral, if +``CUOPT_MIP_INTEGRALITY_TOLERANCE`` controls the MIP integrality tolerance. A variable is considered to be integral, if it is within the integrality tolerance of an integer. .. note:: the default value is ``1e-5``. diff --git a/python/cuopt/cuopt/routing/utils.py b/python/cuopt/cuopt/routing/utils.py index 1449180c75..51d56c1ae1 100644 --- a/python/cuopt/cuopt/routing/utils.py +++ b/python/cuopt/cuopt/routing/utils.py @@ -13,9 +13,6 @@ from cuopt import routing from cuopt.routing import utils_wrapper -# Enable once cupy 13.4.0 is available -# from cupyx.scipy.spatial import distance - def generate_dataset( locations=100, @@ -969,8 +966,6 @@ def create_from_yaml_file(file_path): return df, vehicle_capacity, vehicle_num -# This is temporary till cupy releases new version > 13.3.0 -# which has fix with cuvs for cdist def euclidean_distance(coord): coord = np.array(coord) n_coord = len(coord) diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 27a76337ce..f3f2dbbf8d 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -23,7 +23,6 @@ dependencies = [ "cudf==25.12.*,>=0.0.0a0", "cuopt-mps-parser==25.12.*,>=0.0.0a0", "cupy-cuda13x>=13.6.0", - "cuvs==25.12.*,>=0.0.0a0", "libcuopt==25.12.*,>=0.0.0a0", "numba-cuda>=0.19.1,<0.20.0a0", "numba>=0.60.0", From 354f4f73ee36b147d6fbac574493b227c1c9f73a Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 24 Nov 2025 12:15:54 +0100 Subject: [PATCH 84/86] fix compilation issue --- cpp/src/utilities/omp_helpers.hpp | 48 ++++++++++++++++++------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/cpp/src/utilities/omp_helpers.hpp b/cpp/src/utilities/omp_helpers.hpp index 1ec95583ae..33eda66cba 100644 --- a/cpp/src/utilities/omp_helpers.hpp +++ b/cpp/src/utilities/omp_helpers.hpp @@ -91,38 +91,46 @@ class omp_atomic_t { T fetch_sub(T inc) { return fetch_add(-inc); } + private: + T val; + +#ifndef __NVCC__ + friend double fetch_min(omp_atomic_t& atomic_var, double other); + friend double fetch_max(omp_atomic_t& atomic_var, double other); +#endif +}; + // Atomic CAS are only supported in OpenMP v5.1 // (gcc 12+ or clang 14+), however, nvcc (or the host compiler) cannot // parse it correctly yet #ifndef __NVCC__ - T fetch_min(T other) - { - T old; +// 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 - { - old = val; - val = other < val ? other : val; - } - return old; + { + old = atomic_var.val; + if (other < atomic_var.val) { atomic_var.val = other; } } + return old; +} - T fetch_max(T other) - { - T old; +inline double fetch_max(omp_atomic_t& atomic_var, double other) +{ + double old; #pragma omp atomic compare capture - { - old = val; - val = other > val ? other : val; - } - return old; + { + old = atomic_var.val; + if (other > atomic_var.val) { atomic_var.val = other; } } + return old; +} #endif - private: - T val; -}; - #endif } // namespace cuopt From e54fc9ec8eedeaeefc7acdc1454044a86ec7470e Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 24 Nov 2025 12:35:20 +0100 Subject: [PATCH 85/86] fix missing initialization of the bounds_changed variable --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 01665616f7..dd91ef2927 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -995,6 +995,8 @@ void branch_and_bound_t::explore_subtree(i_t task_id, if (get_heap_size() > settings_.num_bfs_threads) { std::vector lower = original_lp_.lower; std::vector upper = original_lp_.upper; + std::fill( + node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); mutex_dive_queue_.lock(); @@ -1179,6 +1181,8 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A std::vector lower = start_node->lower; std::vector upper = start_node->upper; + std::fill( + node_presolver.bounds_changed.begin(), node_presolver.bounds_changed.end(), false); new_node->get_variable_bounds(lower, upper, node_presolver.bounds_changed); diving_queue_.emplace(new_node->detach_copy(), std::move(lower), std::move(upper)); From 97a14dd388328cc5ade9df232ba0e6f1597893c7 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 24 Nov 2025 18:13:17 +0100 Subject: [PATCH 86/86] disabled column scaling --- cpp/src/dual_simplex/branch_and_bound.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index dd91ef2927..2595a935ac 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -598,9 +598,10 @@ node_solve_info_t branch_and_bound_t::solve_node( simplex_solver_settings_t lp_settings = settings_; lp_settings.set_log(false); - lp_settings.cut_off = upper_bound + settings_.dual_tol; - lp_settings.inside_mip = 2; - lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); + lp_settings.cut_off = upper_bound + settings_.dual_tol; + lp_settings.inside_mip = 2; + lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); + lp_settings.scale_columns = false; #ifdef LOG_NODE_SIMPLEX lp_settings.set_log(true);