From e13da4070d64df5240b68a02fa18a56b0f7d239e Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Fri, 21 Aug 2026 09:23:35 -0700 Subject: [PATCH 01/12] Improve duplicate cut detection Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 233 ++++++++++++++++++++++--------------- cpp/tests/mip/cuts_test.cu | 31 +++++ 2 files changed, 170 insertions(+), 94 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 7acd7dee0a..ea59dc628e 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include @@ -1141,6 +1143,23 @@ inline uint64_t hash64_with_seed(uint64_t value, uint64_t seed) return splitmix64_mix(value ^ (seed * 0xbf58476d1ce4e5b9ULL + 0x9e3779b97f4a7c15ULL)); } +struct duplicate_cut_signature_t { + uint64_t support; + uint64_t coefficients; + + bool operator==(const duplicate_cut_signature_t& other) const + { + return support == other.support && coefficients == other.coefficients; + } +}; + +struct duplicate_cut_signature_hash_t { + size_t operator()(const duplicate_cut_signature_t& signature) const + { + return splitmix64_mix(signature.support ^ splitmix64_mix(signature.coefficients)); + } +}; + } // namespace template @@ -1227,113 +1246,139 @@ f_t cut_pool_t::cut_orthogonality(i_t i, i_t j) template void cut_pool_t::check_for_duplicate_cuts() { - // Algorithm from Finding Duplicate Rows in a Linear Programming Model - // by J. A. Tomlin and J.S. Welch - // Operations Research Letters Volume 5, Number 1, June 1986 - std::vector divisors(cut_storage_.m, 0.0); - std::vector sets(cut_storage_.m, 0); + const i_t m = cut_storage_.m; - csc_matrix_t cut_storage_csc(0, 0, 1); - cut_storage_.to_compressed_col(cut_storage_csc); - i_t n = cut_storage_csc.n; - i_t m = cut_storage_csc.m; + constexpr f_t coefficient_bucket_width = 1e-8; + constexpr f_t duplicate_tolerance = 1e-10; + const i_t no_group = -1; - const i_t sentinel = std::numeric_limits::max(); + struct duplicate_group_t { + i_t representative; + i_t strongest; + i_t next; + }; - i_t new_set = 1; - i_t remaining_potential_duplicates = cut_storage_.m; - for (i_t j = 0; j < n; j++) { - i_t r0 = -1; - i_t new_rows = 0; - i_t new_set_0 = new_set; - new_set++; - const i_t col_start = cut_storage_csc.col_start[j]; - const i_t col_end = cut_storage_csc.col_start[j + 1]; - for (i_t p = col_start; p < col_end; p++) { - const i_t r = cut_storage_csc.i[p]; - const f_t a_rj = cut_storage_csc.x[p]; - const f_t f_r = divisors[r]; - if (sets[r] == 0) { - r0 = r; // To enable use to find this new set later - sets[r] = new_set_0; - divisors[r] = a_rj; - new_rows++; - } else if (sets[r] < new_set_0) { - // Look over indices a_ij with i > r - for (i_t q = p + 1; q < col_end; q++) { - const i_t i = cut_storage_csc.i[q]; - const f_t a_ij = cut_storage_csc.x[q]; - if (sets[i] == sets[r]) { - // These two rows are currently in the same set - // Check to see if the coefficients still match - const f_t f_i = divisors[i]; - const f_t val = (a_rj / f_r) * (f_i / a_ij); - const f_t epsilon = 1e-10; - if ((val >= 1.0 - epsilon && val <= 1.0 + epsilon)) { - sets[r] = new_set; - sets[i] = new_set; - } - } - } - if (sets[r] >= new_set_0) { // This is only true if a match was found inside the above loop - new_set++; - } else { - sets[r] = sentinel; - remaining_potential_duplicates--; - if (remaining_potential_duplicates == 0) { break; } + std::vector divisors(m, 0.0); + std::vector groups; + groups.reserve(m); + std::unordered_map buckets; + buckets.reserve(m); + + auto coefficients_match = [&](f_t a, f_t divisor_a, f_t b, f_t divisor_b) { + const f_t ratio = (a / divisor_a) * (divisor_b / b); + return ratio >= 1.0 - duplicate_tolerance && ratio <= 1.0 + duplicate_tolerance; + }; + + auto rows_are_duplicates = [&](i_t first, i_t second) { + const i_t first_start = cut_storage_.row_start[first]; + const i_t first_end = cut_storage_.row_start[first + 1]; + const i_t second_start = cut_storage_.row_start[second]; + const i_t second_end = cut_storage_.row_start[second + 1]; + const i_t row_length = first_end - first_start; + if (row_length != second_end - second_start) { return false; } + + const f_t first_divisor = divisors[first]; + const f_t second_divisor = divisors[second]; + if ((first_divisor > 0.0) != (second_divisor > 0.0)) { return false; } + + bool same_order = true; + for (i_t k = 0; k < row_length; k++) { + if (cut_storage_.j[first_start + k] != cut_storage_.j[second_start + k]) { + same_order = false; + break; + } + } + if (same_order) { + for (i_t k = 0; k < row_length; k++) { + if (!coefficients_match(cut_storage_.x[first_start + k], + first_divisor, + cut_storage_.x[second_start + k], + second_divisor)) { + return false; } } + return true; } - if (remaining_potential_duplicates == 0) { break; } - if (new_rows == 1) { - sets[r0] = sentinel; - remaining_potential_duplicates--; - if (remaining_potential_duplicates == 0) { break; } + + std::vector first_order(row_length); + std::vector second_order(row_length); + std::iota(first_order.begin(), first_order.end(), first_start); + std::iota(second_order.begin(), second_order.end(), second_start); + const auto column_less = [&](i_t left, i_t right) { + return cut_storage_.j[left] < cut_storage_.j[right]; + }; + std::sort(first_order.begin(), first_order.end(), column_less); + std::sort(second_order.begin(), second_order.end(), column_less); + for (i_t k = 0; k < row_length; k++) { + const i_t first_position = first_order[k]; + const i_t second_position = second_order[k]; + if (cut_storage_.j[first_position] != cut_storage_.j[second_position] || + !coefficients_match(cut_storage_.x[first_position], + first_divisor, + cut_storage_.x[second_position], + second_divisor)) { + return false; + } } - } + return true; + }; - // The cuts are stored in the form: sum_j d_ij x_j >= rhs_i - // We now look for cuts that are duplicates of each other and remove them std::vector cuts_to_remove(m, 0); i_t num_cuts_to_remove = 0; for (i_t r = 0; r < m; r++) { - const i_t set_r = sets[r]; - if (set_r > 0 && set_r < sentinel && cuts_to_remove[r] == 0) { - // This cut has a duplicate - for (i_t i = r + 1; i < m; i++) { - if (sets[i] == set_r) { - const f_t f_r = divisors[r]; - const f_t f_i = divisors[i]; - const f_t theta_r = rhs_storage_[r] / f_r; - const f_t theta_i = rhs_storage_[i] / f_i; - if (f_r > 0 && f_i > 0) { - // We have sum_j d_rj / f_r x_j >= rhs_r / f_r = theta_r - // and sum_j d_ij / f_i x_j >= rhs_i / f_i = theta_i - if (theta_r <= theta_i) { - // Cut i is either the same or stronger than cut r - if (cuts_to_remove[r] == 0) { num_cuts_to_remove++; } - cuts_to_remove[r] = 1; // Remove row r - } else { - // theta_r > theta_i, so cut r is stricly stronger than cut i - if (cuts_to_remove[i] == 0) { num_cuts_to_remove++; } - cuts_to_remove[i] = 1; // Remove row i - } - } else if (f_r < 0 && f_i < 0) { - // We have sum_j d_rj / f_r x_j <= rhs_r / f_r = theta_r - // and sum_j d_ij / f_i x_j <= rhs_i / f_i = theta_i - if (theta_r >= theta_i) { - // Cut i is either the same or stronger than cut r - if (cuts_to_remove[r] == 0) { num_cuts_to_remove++; } - cuts_to_remove[r] = 1; // Remove row r - } else { - // theta_r < theta_i, so cut r is strictly stronger than cut i - if (cuts_to_remove[i] == 0) { num_cuts_to_remove++; } - cuts_to_remove[i] = 1; // Remove row i - } - } - } + const i_t row_start = cut_storage_.row_start[r]; + const i_t row_end = cut_storage_.row_start[r + 1]; + i_t pivot = row_start; + for (i_t p = row_start + 1; p < row_end; p++) { + const f_t pivot_abs = std::abs(cut_storage_.x[pivot]); + const f_t value_abs = std::abs(cut_storage_.x[p]); + if (value_abs > pivot_abs || + (value_abs == pivot_abs && cut_storage_.j[p] < cut_storage_.j[pivot])) { + pivot = p; + } + } + const f_t divisor = cut_storage_.x[pivot]; + divisors[r] = divisor; + + uint64_t support_hash = splitmix64_mix(static_cast(row_end - row_start)); + uint64_t coefficient_hash = + splitmix64_mix(static_cast(row_end - row_start) ^ 0x6a09e667f3bcc909ULL); + for (i_t p = row_start; p < row_end; p++) { + const uint64_t column_hash = + splitmix64_mix(static_cast(cut_storage_.j[p]) + 0x9e3779b97f4a7c15ULL); + const int64_t quantized = static_cast( + std::llround((cut_storage_.x[p] / divisor) / coefficient_bucket_width)); + support_hash += column_hash; + coefficient_hash += splitmix64_mix(column_hash ^ static_cast(quantized)); + } + const duplicate_cut_signature_t signature{support_hash, coefficient_hash}; + + auto bucket = buckets.emplace(signature, no_group).first; + i_t matching_group = no_group; + for (i_t group = bucket->second; group != no_group; group = groups[group].next) { + if (rows_are_duplicates(groups[group].representative, r)) { + matching_group = group; + break; } } + if (matching_group == no_group) { + groups.push_back({r, r, bucket->second}); + bucket->second = static_cast(groups.size() - 1); + continue; + } + + const i_t strongest = groups[matching_group].strongest; + const f_t strongest_theta = rhs_storage_[strongest] / divisors[strongest]; + const f_t row_theta = rhs_storage_[r] / divisor; + const bool row_is_stronger = + divisor > 0.0 ? row_theta >= strongest_theta : row_theta <= strongest_theta; + if (row_is_stronger) { + cuts_to_remove[strongest] = 1; + groups[matching_group].strongest = r; + } else { + cuts_to_remove[r] = 1; + } + num_cuts_to_remove++; } if (num_cuts_to_remove > 0) { diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 5af0754e3d..ce25c6066f 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -981,6 +981,37 @@ TEST(cuts, test_duplicate_cuts_detection) cut_pool.add_cut(mip::cut_type_t::MIXED_INTEGER_GOMORY, cut8); cut_pool.check_for_duplicate_cuts(); + EXPECT_EQ(cut_pool.pool_size(), 5); +} + +TEST(cuts, duplicate_cuts_support_unordered_and_strongest_retained) +{ + simplex::simplex_solver_settings_t settings; + mip::cut_pool_t cut_pool(2, settings); + + mip::inequality_t weaker; + weaker.push_back(0, 1.0); + weaker.push_back(1, 2.0); + weaker.rhs = 1.0; + cut_pool.add_cut(mip::cut_type_t::KNAPSACK, weaker); + + mip::inequality_t stronger; + stronger.push_back(1, 4.0); + stronger.push_back(0, 2.0); + stronger.rhs = 4.0; + cut_pool.add_cut(mip::cut_type_t::FLOW_COVER, stronger); + + cut_pool.check_for_duplicate_cuts(); + EXPECT_EQ(cut_pool.pool_size(), 1); + EXPECT_EQ(cut_pool.count_violated_cuts({1.5, 0.0}), 1); + + mip::inequality_t opposite; + opposite.push_back(0, -1.0); + opposite.push_back(1, -2.0); + opposite.rhs = -2.0; + cut_pool.add_cut(mip::cut_type_t::KNAPSACK, opposite); + cut_pool.check_for_duplicate_cuts(); + EXPECT_EQ(cut_pool.pool_size(), 2); } TEST(cuts, clique_phase1_smoke_conflict_graph_edges) From 4c9bde8036765e5bfa69b09b9d1255d311db7609 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Fri, 21 Aug 2026 15:14:43 -0700 Subject: [PATCH 02/12] Remove duplicate cut unit test Signed-off-by: Hugo Linsenmaier --- cpp/tests/mip/cuts_test.cu | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index ce25c6066f..5af0754e3d 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -981,37 +981,6 @@ TEST(cuts, test_duplicate_cuts_detection) cut_pool.add_cut(mip::cut_type_t::MIXED_INTEGER_GOMORY, cut8); cut_pool.check_for_duplicate_cuts(); - EXPECT_EQ(cut_pool.pool_size(), 5); -} - -TEST(cuts, duplicate_cuts_support_unordered_and_strongest_retained) -{ - simplex::simplex_solver_settings_t settings; - mip::cut_pool_t cut_pool(2, settings); - - mip::inequality_t weaker; - weaker.push_back(0, 1.0); - weaker.push_back(1, 2.0); - weaker.rhs = 1.0; - cut_pool.add_cut(mip::cut_type_t::KNAPSACK, weaker); - - mip::inequality_t stronger; - stronger.push_back(1, 4.0); - stronger.push_back(0, 2.0); - stronger.rhs = 4.0; - cut_pool.add_cut(mip::cut_type_t::FLOW_COVER, stronger); - - cut_pool.check_for_duplicate_cuts(); - EXPECT_EQ(cut_pool.pool_size(), 1); - EXPECT_EQ(cut_pool.count_violated_cuts({1.5, 0.0}), 1); - - mip::inequality_t opposite; - opposite.push_back(0, -1.0); - opposite.push_back(1, -2.0); - opposite.rhs = -2.0; - cut_pool.add_cut(mip::cut_type_t::KNAPSACK, opposite); - cut_pool.check_for_duplicate_cuts(); - EXPECT_EQ(cut_pool.pool_size(), 2); } TEST(cuts, clique_phase1_smoke_conflict_graph_edges) From 828670f8672bdc683ca04d33242cf1f77dc332ec Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Fri, 21 Aug 2026 15:16:12 -0700 Subject: [PATCH 03/12] Use support-only duplicate cut hashing Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index ea59dc628e..bf017a6d0f 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1143,23 +1143,6 @@ inline uint64_t hash64_with_seed(uint64_t value, uint64_t seed) return splitmix64_mix(value ^ (seed * 0xbf58476d1ce4e5b9ULL + 0x9e3779b97f4a7c15ULL)); } -struct duplicate_cut_signature_t { - uint64_t support; - uint64_t coefficients; - - bool operator==(const duplicate_cut_signature_t& other) const - { - return support == other.support && coefficients == other.coefficients; - } -}; - -struct duplicate_cut_signature_hash_t { - size_t operator()(const duplicate_cut_signature_t& signature) const - { - return splitmix64_mix(signature.support ^ splitmix64_mix(signature.coefficients)); - } -}; - } // namespace template @@ -1248,9 +1231,8 @@ void cut_pool_t::check_for_duplicate_cuts() { const i_t m = cut_storage_.m; - constexpr f_t coefficient_bucket_width = 1e-8; - constexpr f_t duplicate_tolerance = 1e-10; - const i_t no_group = -1; + constexpr f_t duplicate_tolerance = 1e-10; + const i_t no_group = -1; struct duplicate_group_t { i_t representative; @@ -1261,7 +1243,7 @@ void cut_pool_t::check_for_duplicate_cuts() std::vector divisors(m, 0.0); std::vector groups; groups.reserve(m); - std::unordered_map buckets; + std::unordered_map buckets; buckets.reserve(m); auto coefficients_match = [&](f_t a, f_t divisor_a, f_t b, f_t divisor_b) { @@ -1341,19 +1323,13 @@ void cut_pool_t::check_for_duplicate_cuts() divisors[r] = divisor; uint64_t support_hash = splitmix64_mix(static_cast(row_end - row_start)); - uint64_t coefficient_hash = - splitmix64_mix(static_cast(row_end - row_start) ^ 0x6a09e667f3bcc909ULL); for (i_t p = row_start; p < row_end; p++) { const uint64_t column_hash = splitmix64_mix(static_cast(cut_storage_.j[p]) + 0x9e3779b97f4a7c15ULL); - const int64_t quantized = static_cast( - std::llround((cut_storage_.x[p] / divisor) / coefficient_bucket_width)); support_hash += column_hash; - coefficient_hash += splitmix64_mix(column_hash ^ static_cast(quantized)); } - const duplicate_cut_signature_t signature{support_hash, coefficient_hash}; - auto bucket = buckets.emplace(signature, no_group).first; + auto bucket = buckets.emplace(support_hash, no_group).first; i_t matching_group = no_group; for (i_t group = bucket->second; group != no_group; group = groups[group].next) { if (rows_are_duplicates(groups[group].representative, r)) { From 1f2e0279442e5477b86f42688cb61ed0f64d7e9d Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 24 Aug 2026 18:53:16 -0700 Subject: [PATCH 04/12] Preserve duplicate cut behavior with indexed lookup --- cpp/src/cuts/cuts.cpp | 215 ++++++++++++++++++++---------------------- 1 file changed, 103 insertions(+), 112 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index bf017a6d0f..ec32afb824 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1232,131 +1232,122 @@ void cut_pool_t::check_for_duplicate_cuts() const i_t m = cut_storage_.m; constexpr f_t duplicate_tolerance = 1e-10; - const i_t no_group = -1; - - struct duplicate_group_t { - i_t representative; - i_t strongest; - i_t next; - }; - std::vector divisors(m, 0.0); - std::vector groups; - groups.reserve(m); - std::unordered_map buckets; - buckets.reserve(m); - - auto coefficients_match = [&](f_t a, f_t divisor_a, f_t b, f_t divisor_b) { - const f_t ratio = (a / divisor_a) * (divisor_b / b); - return ratio >= 1.0 - duplicate_tolerance && ratio <= 1.0 + duplicate_tolerance; - }; - - auto rows_are_duplicates = [&](i_t first, i_t second) { - const i_t first_start = cut_storage_.row_start[first]; - const i_t first_end = cut_storage_.row_start[first + 1]; - const i_t second_start = cut_storage_.row_start[second]; - const i_t second_end = cut_storage_.row_start[second + 1]; - const i_t row_length = first_end - first_start; - if (row_length != second_end - second_start) { return false; } - - const f_t first_divisor = divisors[first]; - const f_t second_divisor = divisors[second]; - if ((first_divisor > 0.0) != (second_divisor > 0.0)) { return false; } - - bool same_order = true; - for (i_t k = 0; k < row_length; k++) { - if (cut_storage_.j[first_start + k] != cut_storage_.j[second_start + k]) { - same_order = false; - break; - } + std::vector sets(m, 0); + csc_matrix_t cut_storage_csc(0, 0, 1); + cut_storage_.to_compressed_col(cut_storage_csc); + const i_t n = cut_storage_csc.n; + const i_t sentinel = std::numeric_limits::max(); + + // Algorithm from Finding Duplicate Rows in a Linear Programming Model + // by J. A. Tomlin and J.S. Welch + // Operations Research Letters Volume 5, Number 1, June 1986. + // + // Preserve the legacy partition refinement and row-ordered deletion semantics, but index + // entries by their current set. This avoids scanning unrelated later entries without changing + // the first matching partner or the resulting removal mask. + i_t new_set = 1; + i_t remaining_potential_duplicates = m; + for (i_t j = 0; j < n; j++) { + i_t r0 = -1; + i_t new_rows = 0; + i_t new_set_0 = new_set; + new_set++; + const i_t col_start = cut_storage_csc.col_start[j]; + const i_t col_end = cut_storage_csc.col_start[j + 1]; + + std::unordered_map> positions_by_set; + positions_by_set.reserve(col_end - col_start); + for (i_t p = col_start; p < col_end; p++) { + const i_t set = sets[cut_storage_csc.i[p]]; + if (set > 0 && set < new_set_0) { positions_by_set[set].push_back(p); } } - if (same_order) { - for (i_t k = 0; k < row_length; k++) { - if (!coefficients_match(cut_storage_.x[first_start + k], - first_divisor, - cut_storage_.x[second_start + k], - second_divisor)) { - return false; + + for (i_t p = col_start; p < col_end; p++) { + const i_t r = cut_storage_csc.i[p]; + const f_t a_rj = cut_storage_csc.x[p]; + const f_t f_r = divisors[r]; + if (sets[r] == 0) { + r0 = r; + sets[r] = new_set_0; + divisors[r] = a_rj; + new_rows++; + } else if (sets[r] < new_set_0) { + const i_t old_set = sets[r]; + bool matched = false; + const auto set_positions = positions_by_set.find(old_set); + if (set_positions != positions_by_set.end()) { + auto q_position = std::upper_bound( + set_positions->second.begin(), set_positions->second.end(), p); + for (; q_position != set_positions->second.end(); ++q_position) { + const i_t q = *q_position; + const i_t i = cut_storage_csc.i[q]; + const f_t a_ij = cut_storage_csc.x[q]; + if (sets[i] != old_set) { continue; } + const f_t f_i = divisors[i]; + const f_t val = (a_rj / f_r) * (f_i / a_ij); + if (val >= 1.0 - duplicate_tolerance && + val <= 1.0 + duplicate_tolerance) { + sets[r] = new_set; + sets[i] = new_set; + matched = true; + break; + } + } + } + if (matched) { + new_set++; + } else { + sets[r] = sentinel; + remaining_potential_duplicates--; + if (remaining_potential_duplicates == 0) { break; } } } - return true; } - - std::vector first_order(row_length); - std::vector second_order(row_length); - std::iota(first_order.begin(), first_order.end(), first_start); - std::iota(second_order.begin(), second_order.end(), second_start); - const auto column_less = [&](i_t left, i_t right) { - return cut_storage_.j[left] < cut_storage_.j[right]; - }; - std::sort(first_order.begin(), first_order.end(), column_less); - std::sort(second_order.begin(), second_order.end(), column_less); - for (i_t k = 0; k < row_length; k++) { - const i_t first_position = first_order[k]; - const i_t second_position = second_order[k]; - if (cut_storage_.j[first_position] != cut_storage_.j[second_position] || - !coefficients_match(cut_storage_.x[first_position], - first_divisor, - cut_storage_.x[second_position], - second_divisor)) { - return false; - } + if (remaining_potential_duplicates == 0) { break; } + if (new_rows == 1) { + sets[r0] = sentinel; + remaining_potential_duplicates--; + if (remaining_potential_duplicates == 0) { break; } } - return true; - }; + } - std::vector cuts_to_remove(m, 0); - i_t num_cuts_to_remove = 0; + std::unordered_map> rows_by_set; + rows_by_set.reserve(m); for (i_t r = 0; r < m; r++) { - const i_t row_start = cut_storage_.row_start[r]; - const i_t row_end = cut_storage_.row_start[r + 1]; - i_t pivot = row_start; - for (i_t p = row_start + 1; p < row_end; p++) { - const f_t pivot_abs = std::abs(cut_storage_.x[pivot]); - const f_t value_abs = std::abs(cut_storage_.x[p]); - if (value_abs > pivot_abs || - (value_abs == pivot_abs && cut_storage_.j[p] < cut_storage_.j[pivot])) { - pivot = p; - } - } - const f_t divisor = cut_storage_.x[pivot]; - divisors[r] = divisor; - - uint64_t support_hash = splitmix64_mix(static_cast(row_end - row_start)); - for (i_t p = row_start; p < row_end; p++) { - const uint64_t column_hash = - splitmix64_mix(static_cast(cut_storage_.j[p]) + 0x9e3779b97f4a7c15ULL); - support_hash += column_hash; - } + if (sets[r] > 0 && sets[r] < sentinel) { rows_by_set[sets[r]].push_back(r); } + } - auto bucket = buckets.emplace(support_hash, no_group).first; - i_t matching_group = no_group; - for (i_t group = bucket->second; group != no_group; group = groups[group].next) { - if (rows_are_duplicates(groups[group].representative, r)) { - matching_group = group; - break; + std::vector cuts_to_remove(m, 0); + for (i_t r = 0; r < m; r++) { + const i_t set_r = sets[r]; + if (set_r <= 0 || set_r >= sentinel || cuts_to_remove[r] != 0) { continue; } + const auto& members = rows_by_set.at(set_r); + auto member = std::upper_bound(members.begin(), members.end(), r); + for (; member != members.end(); ++member) { + const i_t i = *member; + const f_t f_r = divisors[r]; + const f_t f_i = divisors[i]; + const f_t theta_r = rhs_storage_[r] / f_r; + const f_t theta_i = rhs_storage_[i] / f_i; + if (f_r > 0.0 && f_i > 0.0) { + if (theta_r <= theta_i) { + cuts_to_remove[r] = 1; + } else { + cuts_to_remove[i] = 1; + } + } else if (f_r < 0.0 && f_i < 0.0) { + if (theta_r >= theta_i) { + cuts_to_remove[r] = 1; + } else { + cuts_to_remove[i] = 1; + } } } - if (matching_group == no_group) { - groups.push_back({r, r, bucket->second}); - bucket->second = static_cast(groups.size() - 1); - continue; - } - - const i_t strongest = groups[matching_group].strongest; - const f_t strongest_theta = rhs_storage_[strongest] / divisors[strongest]; - const f_t row_theta = rhs_storage_[r] / divisor; - const bool row_is_stronger = - divisor > 0.0 ? row_theta >= strongest_theta : row_theta <= strongest_theta; - if (row_is_stronger) { - cuts_to_remove[strongest] = 1; - groups[matching_group].strongest = r; - } else { - cuts_to_remove[r] = 1; - } - num_cuts_to_remove++; } + const i_t num_cuts_to_remove = + std::accumulate(cuts_to_remove.begin(), cuts_to_remove.end(), i_t{0}); if (num_cuts_to_remove > 0) { settings_.log.debug("Removing %d duplicate cuts\n", num_cuts_to_remove); csr_matrix_t new_cut_storage(0, 0, 0); From f6c0dcf6deafc9f73617571b0ee923424017ed25 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 27 Aug 2026 21:16:19 -0700 Subject: [PATCH 05/12] Restore duplicate cut algorithm comments Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index ec32afb824..0b9e9b6515 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1273,12 +1273,12 @@ void cut_pool_t::check_for_duplicate_cuts() divisors[r] = a_rj; new_rows++; } else if (sets[r] < new_set_0) { - const i_t old_set = sets[r]; - bool matched = false; + const i_t old_set = sets[r]; + bool matched = false; const auto set_positions = positions_by_set.find(old_set); if (set_positions != positions_by_set.end()) { - auto q_position = std::upper_bound( - set_positions->second.begin(), set_positions->second.end(), p); + auto q_position = + std::upper_bound(set_positions->second.begin(), set_positions->second.end(), p); for (; q_position != set_positions->second.end(); ++q_position) { const i_t q = *q_position; const i_t i = cut_storage_csc.i[q]; @@ -1286,8 +1286,7 @@ void cut_pool_t::check_for_duplicate_cuts() if (sets[i] != old_set) { continue; } const f_t f_i = divisors[i]; const f_t val = (a_rj / f_r) * (f_i / a_ij); - if (val >= 1.0 - duplicate_tolerance && - val <= 1.0 + duplicate_tolerance) { + if (val >= 1.0 - duplicate_tolerance && val <= 1.0 + duplicate_tolerance) { sets[r] = new_set; sets[i] = new_set; matched = true; @@ -1312,6 +1311,8 @@ void cut_pool_t::check_for_duplicate_cuts() } } + // The cuts are stored in the form: sum_j d_ij x_j >= rhs_i. + // We now look for cuts that are duplicates of each other and remove them. std::unordered_map> rows_by_set; rows_by_set.reserve(m); for (i_t r = 0; r < m; r++) { @@ -1322,6 +1323,8 @@ void cut_pool_t::check_for_duplicate_cuts() for (i_t r = 0; r < m; r++) { const i_t set_r = sets[r]; if (set_r <= 0 || set_r >= sentinel || cuts_to_remove[r] != 0) { continue; } + // This cut has a duplicate. The set members are in row order, preserving the legacy + // strongest-cut selection order without scanning unrelated rows. const auto& members = rows_by_set.at(set_r); auto member = std::upper_bound(members.begin(), members.end(), r); for (; member != members.end(); ++member) { @@ -1331,16 +1334,25 @@ void cut_pool_t::check_for_duplicate_cuts() const f_t theta_r = rhs_storage_[r] / f_r; const f_t theta_i = rhs_storage_[i] / f_i; if (f_r > 0.0 && f_i > 0.0) { + // We have sum_j d_rj / f_r x_j >= rhs_r / f_r = theta_r + // and sum_j d_ij / f_i x_j >= rhs_i / f_i = theta_i. if (theta_r <= theta_i) { - cuts_to_remove[r] = 1; + // Cut i is either the same or stronger than cut r. + cuts_to_remove[r] = 1; // Remove row r. } else { - cuts_to_remove[i] = 1; + // theta_r > theta_i, so cut r is strictly stronger than cut i. + cuts_to_remove[i] = 1; // Remove row i. } } else if (f_r < 0.0 && f_i < 0.0) { + // Dividing by a negative divisor reverses the inequality: + // sum_j d_rj / f_r x_j <= rhs_r / f_r = theta_r + // and sum_j d_ij / f_i x_j <= rhs_i / f_i = theta_i. if (theta_r >= theta_i) { - cuts_to_remove[r] = 1; + // Cut i is either the same or stronger than cut r. + cuts_to_remove[r] = 1; // Remove row r. } else { - cuts_to_remove[i] = 1; + // theta_r < theta_i, so cut r is strictly stronger than cut i. + cuts_to_remove[i] = 1; // Remove row i. } } } From 4ae4a3aa2d123fbf7e879d10571e4d1f9d62a3b8 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Thu, 27 Aug 2026 22:50:20 -0700 Subject: [PATCH 06/12] Add duplicate cut regression assertion Signed-off-by: Hugo Linsenmaier --- cpp/tests/mip/cuts_test.cu | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 5af0754e3d..4932fbcf41 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -981,6 +981,7 @@ TEST(cuts, test_duplicate_cuts_detection) cut_pool.add_cut(mip::cut_type_t::MIXED_INTEGER_GOMORY, cut8); cut_pool.check_for_duplicate_cuts(); + EXPECT_EQ(cut_pool.pool_size(), 5); } TEST(cuts, clique_phase1_smoke_conflict_graph_edges) From 161da202eab95bb0de237d980f31598fa8c8b8bf Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Mon, 31 Aug 2026 17:55:03 -0700 Subject: [PATCH 07/12] Use compressed storage for duplicate cut groups Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 62 +++++++++++++++++---------------------- cpp/src/cuts/cuts.hpp | 67 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 36 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 0b9e9b6515..8ff99a46e0 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1243,11 +1243,12 @@ void cut_pool_t::check_for_duplicate_cuts() // by J. A. Tomlin and J.S. Welch // Operations Research Letters Volume 5, Number 1, June 1986. // - // Preserve the legacy partition refinement and row-ordered deletion semantics, but index - // entries by their current set. This avoids scanning unrelated later entries without changing - // the first matching partner or the resulting removal mask. + // Preserve the legacy partition refinement and row-ordered deletion semantics, but group + // entries by their current set in compressed storage. This avoids scanning unrelated later + // entries without changing the first matching partner or the resulting removal mask. i_t new_set = 1; i_t remaining_potential_duplicates = m; + compressed_set_groups_t set_groups; for (i_t j = 0; j < n; j++) { i_t r0 = -1; i_t new_rows = 0; @@ -1256,12 +1257,8 @@ void cut_pool_t::check_for_duplicate_cuts() const i_t col_start = cut_storage_csc.col_start[j]; const i_t col_end = cut_storage_csc.col_start[j + 1]; - std::unordered_map> positions_by_set; - positions_by_set.reserve(col_end - col_start); - for (i_t p = col_start; p < col_end; p++) { - const i_t set = sets[cut_storage_csc.i[p]]; - if (set > 0 && set < new_set_0) { positions_by_set[set].push_back(p); } - } + set_groups.build( + col_start, col_end, new_set_0, [&](i_t p) { return sets[cut_storage_csc.i[p]]; }); for (i_t p = col_start; p < col_end; p++) { const i_t r = cut_storage_csc.i[p]; @@ -1273,25 +1270,22 @@ void cut_pool_t::check_for_duplicate_cuts() divisors[r] = a_rj; new_rows++; } else if (sets[r] < new_set_0) { - const i_t old_set = sets[r]; - bool matched = false; - const auto set_positions = positions_by_set.find(old_set); - if (set_positions != positions_by_set.end()) { - auto q_position = - std::upper_bound(set_positions->second.begin(), set_positions->second.end(), p); - for (; q_position != set_positions->second.end(); ++q_position) { - const i_t q = *q_position; - const i_t i = cut_storage_csc.i[q]; - const f_t a_ij = cut_storage_csc.x[q]; - if (sets[i] != old_set) { continue; } - const f_t f_i = divisors[i]; - const f_t val = (a_rj / f_r) * (f_i / a_ij); - if (val >= 1.0 - duplicate_tolerance && val <= 1.0 + duplicate_tolerance) { - sets[r] = new_set; - sets[i] = new_set; - matched = true; - break; - } + const i_t old_set = sets[r]; + bool matched = false; + const auto set_entries = set_groups.entries_in_set(old_set); + auto q_position = std::upper_bound(set_entries.begin(), set_entries.end(), p); + for (; q_position != set_entries.end(); ++q_position) { + const i_t q = *q_position; + const i_t i = cut_storage_csc.i[q]; + const f_t a_ij = cut_storage_csc.x[q]; + if (sets[i] != old_set) { continue; } + const f_t f_i = divisors[i]; + const f_t val = (a_rj / f_r) * (f_i / a_ij); + if (val >= 1.0 - duplicate_tolerance && val <= 1.0 + duplicate_tolerance) { + sets[r] = new_set; + sets[i] = new_set; + matched = true; + break; } } if (matched) { @@ -1313,11 +1307,7 @@ void cut_pool_t::check_for_duplicate_cuts() // The cuts are stored in the form: sum_j d_ij x_j >= rhs_i. // We now look for cuts that are duplicates of each other and remove them. - std::unordered_map> rows_by_set; - rows_by_set.reserve(m); - for (i_t r = 0; r < m; r++) { - if (sets[r] > 0 && sets[r] < sentinel) { rows_by_set[sets[r]].push_back(r); } - } + set_groups.build(0, m, new_set, [&](i_t r) { return sets[r]; }); std::vector cuts_to_remove(m, 0); for (i_t r = 0; r < m; r++) { @@ -1325,9 +1315,9 @@ void cut_pool_t::check_for_duplicate_cuts() if (set_r <= 0 || set_r >= sentinel || cuts_to_remove[r] != 0) { continue; } // This cut has a duplicate. The set members are in row order, preserving the legacy // strongest-cut selection order without scanning unrelated rows. - const auto& members = rows_by_set.at(set_r); - auto member = std::upper_bound(members.begin(), members.end(), r); - for (; member != members.end(); ++member) { + const auto set_entries = set_groups.entries_in_set(set_r); + auto member = std::upper_bound(set_entries.begin(), set_entries.end(), r); + for (; member != set_entries.end(); ++member) { const i_t i = *member; const f_t f_r = divisors[r]; const f_t f_i = divisors[i]; diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index fcb6080178..c0303b4cc8 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -19,12 +19,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace cuopt::mathematical_optimization::mip { @@ -303,6 +305,71 @@ std::vector> find_mod2_row_combinations_for_test( double max_work_estimate, double* work_estimate); +// Groups integer entries by sparse set id using prefix offsets and one flat member array. +// Repeated builds reuse the allocated storage. Entry order within each set is preserved. +template +class compressed_set_groups_t { + public: + // Groups entries in [first, last) whose set ids are in [1, set_id_limit). + template + void build(i_t first, i_t last, i_t set_id_limit, get_set_id_t get_set_id) + { + for (const i_t set_id : active_set_ids_) { + bucket_by_set_[set_id] = -1; + } + active_set_ids_.clear(); + set_counts_.clear(); + if (bucket_by_set_.size() < static_cast(set_id_limit)) { + bucket_by_set_.resize(set_id_limit, -1); + } + + for (i_t entry = first; entry < last; entry++) { + const i_t set_id = get_set_id(entry); + if (set_id > 0 && set_id < set_id_limit) { + i_t& bucket = bucket_by_set_[set_id]; + if (bucket < 0) { + bucket = static_cast(active_set_ids_.size()); + active_set_ids_.push_back(set_id); + set_counts_.push_back(0); + } + set_counts_[bucket]++; + } + } + + set_starts_.resize(set_counts_.size() + 1); + set_starts_[0] = 0; + std::inclusive_scan(set_counts_.begin(), set_counts_.end(), set_starts_.begin() + 1); + entries_.resize(set_starts_.back()); + next_entry_in_set_ = set_starts_; + for (i_t entry = first; entry < last; entry++) { + const i_t set_id = get_set_id(entry); + if (set_id > 0 && set_id < set_id_limit) { + const i_t bucket = bucket_by_set_[set_id]; + entries_[next_entry_in_set_[bucket]] = entry; + next_entry_in_set_[bucket]++; + } + } + } + + std::span entries_in_set(i_t set_id) const + { + if (set_id <= 0 || set_id >= static_cast(bucket_by_set_.size())) { return {}; } + const i_t bucket = bucket_by_set_[set_id]; + if (bucket < 0) { return {}; } + const i_t begin = set_starts_[bucket]; + const i_t end = set_starts_[bucket + 1]; + return {entries_.data() + begin, static_cast(end - begin)}; + } + + private: + std::vector bucket_by_set_; + std::vector active_set_ids_; + std::vector set_counts_; + std::vector set_starts_; + std::vector next_entry_in_set_; + std::vector entries_; +}; + template class cut_pool_t { public: From 915d4d8d49bb1566903cdb86e2529973449d2fc3 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Tue, 1 Sep 2026 16:51:59 -0700 Subject: [PATCH 08/12] Avoid binary search in duplicate cut groups Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 14 ++++---------- cpp/src/cuts/cuts.hpp | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 8ff99a46e0..43cd31d1b8 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1270,12 +1270,9 @@ void cut_pool_t::check_for_duplicate_cuts() divisors[r] = a_rj; new_rows++; } else if (sets[r] < new_set_0) { - const i_t old_set = sets[r]; - bool matched = false; - const auto set_entries = set_groups.entries_in_set(old_set); - auto q_position = std::upper_bound(set_entries.begin(), set_entries.end(), p); - for (; q_position != set_entries.end(); ++q_position) { - const i_t q = *q_position; + const i_t old_set = sets[r]; + bool matched = false; + for (const i_t q : set_groups.entries_after(old_set, p)) { const i_t i = cut_storage_csc.i[q]; const f_t a_ij = cut_storage_csc.x[q]; if (sets[i] != old_set) { continue; } @@ -1315,10 +1312,7 @@ void cut_pool_t::check_for_duplicate_cuts() if (set_r <= 0 || set_r >= sentinel || cuts_to_remove[r] != 0) { continue; } // This cut has a duplicate. The set members are in row order, preserving the legacy // strongest-cut selection order without scanning unrelated rows. - const auto set_entries = set_groups.entries_in_set(set_r); - auto member = std::upper_bound(set_entries.begin(), set_entries.end(), r); - for (; member != set_entries.end(); ++member) { - const i_t i = *member; + for (const i_t i : set_groups.entries_after(set_r, r)) { const f_t f_r = divisors[r]; const f_t f_i = divisors[i]; const f_t theta_r = rhs_storage_[r] / f_r; diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index c0303b4cc8..3ffc0a441f 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -314,6 +314,7 @@ class compressed_set_groups_t { template void build(i_t first, i_t last, i_t set_id_limit, get_set_id_t get_set_id) { + first_entry_ = first; for (const i_t set_id : active_set_ids_) { bucket_by_set_[set_id] = -1; } @@ -340,34 +341,39 @@ class compressed_set_groups_t { set_starts_[0] = 0; std::inclusive_scan(set_counts_.begin(), set_counts_.end(), set_starts_.begin() + 1); entries_.resize(set_starts_.back()); + position_by_entry_.resize(last - first); next_entry_in_set_ = set_starts_; for (i_t entry = first; entry < last; entry++) { const i_t set_id = get_set_id(entry); if (set_id > 0 && set_id < set_id_limit) { - const i_t bucket = bucket_by_set_[set_id]; - entries_[next_entry_in_set_[bucket]] = entry; - next_entry_in_set_[bucket]++; + const i_t bucket = bucket_by_set_[set_id]; + const i_t position = next_entry_in_set_[bucket]++; + entries_[position] = entry; + position_by_entry_[entry - first] = position; } } } - std::span entries_in_set(i_t set_id) const + // Entry order within a set matches input order, so position + 1 is its first later entry. + std::span entries_after(i_t set_id, i_t entry) const { if (set_id <= 0 || set_id >= static_cast(bucket_by_set_.size())) { return {}; } const i_t bucket = bucket_by_set_[set_id]; if (bucket < 0) { return {}; } - const i_t begin = set_starts_[bucket]; - const i_t end = set_starts_[bucket + 1]; - return {entries_.data() + begin, static_cast(end - begin)}; + const i_t position = position_by_entry_[entry - first_entry_]; + const i_t end = set_starts_[bucket + 1]; + return {entries_.data() + position + 1, static_cast(end - position - 1)}; } private: + i_t first_entry_{0}; std::vector bucket_by_set_; std::vector active_set_ids_; std::vector set_counts_; std::vector set_starts_; std::vector next_entry_in_set_; std::vector entries_; + std::vector position_by_entry_; }; template From 9b015c85cd600e76f0931522645105a9df4ce912 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Tue, 1 Sep 2026 16:55:06 -0700 Subject: [PATCH 09/12] Document duplicate cut candidate scan Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 43cd31d1b8..c27c5c7a3f 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1272,6 +1272,7 @@ void cut_pool_t::check_for_duplicate_cuts() } else if (sets[r] < new_set_0) { const i_t old_set = sets[r]; bool matched = false; + // Loop over all indices a_ij with i > r where i is in the same set as r. for (const i_t q : set_groups.entries_after(old_set, p)) { const i_t i = cut_storage_csc.i[q]; const f_t a_ij = cut_storage_csc.x[q]; From 14724ecf9e08d83443e966d47951f072acdee54d Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Tue, 1 Sep 2026 16:57:59 -0700 Subject: [PATCH 10/12] Restore duplicate set tracking comment Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index c27c5c7a3f..85b869cd0a 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1265,7 +1265,7 @@ void cut_pool_t::check_for_duplicate_cuts() const f_t a_rj = cut_storage_csc.x[p]; const f_t f_r = divisors[r]; if (sets[r] == 0) { - r0 = r; + r0 = r; // To enable us to find this new set later. sets[r] = new_set_0; divisors[r] = a_rj; new_rows++; From e152be8ef5e5d18c54bc77e9e651b5a04438a94f Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Tue, 1 Sep 2026 17:07:44 -0700 Subject: [PATCH 11/12] Track duplicate cut removals inline Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 85b869cd0a..c05bee4fdb 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1308,6 +1308,7 @@ void cut_pool_t::check_for_duplicate_cuts() set_groups.build(0, m, new_set, [&](i_t r) { return sets[r]; }); std::vector cuts_to_remove(m, 0); + i_t num_cuts_to_remove = 0; for (i_t r = 0; r < m; r++) { const i_t set_r = sets[r]; if (set_r <= 0 || set_r >= sentinel || cuts_to_remove[r] != 0) { continue; } @@ -1323,9 +1324,11 @@ void cut_pool_t::check_for_duplicate_cuts() // and sum_j d_ij / f_i x_j >= rhs_i / f_i = theta_i. if (theta_r <= theta_i) { // Cut i is either the same or stronger than cut r. + if (cuts_to_remove[r] == 0) { num_cuts_to_remove++; } cuts_to_remove[r] = 1; // Remove row r. } else { // theta_r > theta_i, so cut r is strictly stronger than cut i. + if (cuts_to_remove[i] == 0) { num_cuts_to_remove++; } cuts_to_remove[i] = 1; // Remove row i. } } else if (f_r < 0.0 && f_i < 0.0) { @@ -1334,17 +1337,17 @@ void cut_pool_t::check_for_duplicate_cuts() // and sum_j d_ij / f_i x_j <= rhs_i / f_i = theta_i. if (theta_r >= theta_i) { // Cut i is either the same or stronger than cut r. + if (cuts_to_remove[r] == 0) { num_cuts_to_remove++; } cuts_to_remove[r] = 1; // Remove row r. } else { // theta_r < theta_i, so cut r is strictly stronger than cut i. + if (cuts_to_remove[i] == 0) { num_cuts_to_remove++; } cuts_to_remove[i] = 1; // Remove row i. } } } } - const i_t num_cuts_to_remove = - std::accumulate(cuts_to_remove.begin(), cuts_to_remove.end(), i_t{0}); if (num_cuts_to_remove > 0) { settings_.log.debug("Removing %d duplicate cuts\n", num_cuts_to_remove); csr_matrix_t new_cut_storage(0, 0, 0); From ad9a4186ef990c556473b420e906d9b58ac0fdc2 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Tue, 1 Sep 2026 17:33:34 -0700 Subject: [PATCH 12/12] Use direct indexing for duplicate cut groups Signed-off-by: Hugo Linsenmaier --- cpp/src/cuts/cuts.cpp | 5 ++-- cpp/src/cuts/cuts.hpp | 60 +++++++++++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index c05bee4fdb..e9f51666dc 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1257,8 +1257,7 @@ void cut_pool_t::check_for_duplicate_cuts() const i_t col_start = cut_storage_csc.col_start[j]; const i_t col_end = cut_storage_csc.col_start[j + 1]; - set_groups.build( - col_start, col_end, new_set_0, [&](i_t p) { return sets[cut_storage_csc.i[p]]; }); + set_groups.build_with_entry_indices(col_start, col_end, new_set_0, cut_storage_csc.i, sets); for (i_t p = col_start; p < col_end; p++) { const i_t r = cut_storage_csc.i[p]; @@ -1305,7 +1304,7 @@ void cut_pool_t::check_for_duplicate_cuts() // The cuts are stored in the form: sum_j d_ij x_j >= rhs_i. // We now look for cuts that are duplicates of each other and remove them. - set_groups.build(0, m, new_set, [&](i_t r) { return sets[r]; }); + set_groups.build(0, m, new_set, sets); std::vector cuts_to_remove(m, 0); i_t num_cuts_to_remove = 0; diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 3ffc0a441f..0f6d4d634b 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -311,8 +311,38 @@ template class compressed_set_groups_t { public: // Groups entries in [first, last) whose set ids are in [1, set_id_limit). - template - void build(i_t first, i_t last, i_t set_id_limit, get_set_id_t get_set_id) + void build(i_t first, i_t last, i_t set_id_limit, const std::vector& set_ids) + { + build_impl(first, last, set_id_limit, set_ids, nullptr); + } + + void build_with_entry_indices(i_t first, + i_t last, + i_t set_id_limit, + const std::vector& entry_indices, + const std::vector& set_ids) + { + build_impl(first, last, set_id_limit, set_ids, &entry_indices); + } + + // Entry order within a set matches input order, so position + 1 is its first later entry. + std::span entries_after(i_t set_id, i_t entry) const + { + if (set_id <= 0 || set_id >= static_cast(bucket_by_set_.size())) { return {}; } + const i_t bucket = bucket_by_set_[set_id]; + if (bucket < 0) { return {}; } + const i_t position = position_by_entry_[entry - first_entry_]; + const i_t end = set_starts_[bucket + 1]; + return {entries_.data() + position + 1, static_cast(end - position - 1)}; + } + + private: + template + void build_impl(i_t first, + i_t last, + i_t set_id_limit, + const std::vector& set_ids, + const std::vector* entry_indices) { first_entry_ = first; for (const i_t set_id : active_set_ids_) { @@ -325,7 +355,12 @@ class compressed_set_groups_t { } for (i_t entry = first; entry < last; entry++) { - const i_t set_id = get_set_id(entry); + i_t set_id; + if constexpr (use_entry_indices) { + set_id = set_ids[(*entry_indices)[entry]]; + } else { + set_id = set_ids[entry]; + } if (set_id > 0 && set_id < set_id_limit) { i_t& bucket = bucket_by_set_[set_id]; if (bucket < 0) { @@ -344,7 +379,12 @@ class compressed_set_groups_t { position_by_entry_.resize(last - first); next_entry_in_set_ = set_starts_; for (i_t entry = first; entry < last; entry++) { - const i_t set_id = get_set_id(entry); + i_t set_id; + if constexpr (use_entry_indices) { + set_id = set_ids[(*entry_indices)[entry]]; + } else { + set_id = set_ids[entry]; + } if (set_id > 0 && set_id < set_id_limit) { const i_t bucket = bucket_by_set_[set_id]; const i_t position = next_entry_in_set_[bucket]++; @@ -354,18 +394,6 @@ class compressed_set_groups_t { } } - // Entry order within a set matches input order, so position + 1 is its first later entry. - std::span entries_after(i_t set_id, i_t entry) const - { - if (set_id <= 0 || set_id >= static_cast(bucket_by_set_.size())) { return {}; } - const i_t bucket = bucket_by_set_[set_id]; - if (bucket < 0) { return {}; } - const i_t position = position_by_entry_[entry - first_entry_]; - const i_t end = set_starts_[bucket + 1]; - return {entries_.data() + position + 1, static_cast(end - position - 1)}; - } - - private: i_t first_entry_{0}; std::vector bucket_by_set_; std::vector active_set_ids_;