From e13da4070d64df5240b68a02fa18a56b0f7d239e Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Fri, 21 Aug 2026 09:23:35 -0700 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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)