From b880fe06119f7d04671be632aa4840a685a985d1 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Jul 2026 08:15:28 -0700 Subject: [PATCH 1/2] fix gf2 constraint addressing --- .../mip_heuristics/presolve/gf2_presolve.cpp | 7 +- cpp/tests/mip/CMakeLists.txt | 4 + cpp/tests/mip/gf2_presolve_test.cpp | 84 +++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 cpp/tests/mip/gf2_presolve_test.cpp diff --git a/cpp/src/mip_heuristics/presolve/gf2_presolve.cpp b/cpp/src/mip_heuristics/presolve/gf2_presolve.cpp index 19eabaf930..c984ef306c 100644 --- a/cpp/src/mip_heuristics/presolve/gf2_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/gf2_presolve.cpp @@ -203,11 +203,12 @@ papilo::PresolveStatus GF2Presolve::execute(const papilo::Problem& pro std::vector> A(gf2_constraints.size(), std::vector(gf2_constraints.size(), 0)); std::vector b(gf2_constraints.size()); - for (const auto& cons : gf2_constraints) { + for (size_t gf2_cstr_idx = 0; gf2_cstr_idx < gf2_constraints.size(); ++gf2_cstr_idx) { + const auto& cons = gf2_constraints[gf2_cstr_idx]; for (auto [bin_var, _] : cons.bin_vars) { - A[cons.cstr_idx][gf2_bin_vars[bin_var]] = 1; + A[gf2_cstr_idx][gf2_bin_vars[bin_var]] = 1; } - b[cons.cstr_idx] = cons.rhs; + b[gf2_cstr_idx] = cons.rhs; } std::vector solution(gf2_constraints.size()); diff --git a/cpp/tests/mip/CMakeLists.txt b/cpp/tests/mip/CMakeLists.txt index d533f09c2d..24bf392a36 100644 --- a/cpp/tests/mip/CMakeLists.txt +++ b/cpp/tests/mip/CMakeLists.txt @@ -42,6 +42,10 @@ ConfigureTest(EMPTY_FIXED_PROBLEMS_TEST ConfigureTest(PRESOLVE_TEST ${CMAKE_CURRENT_SOURCE_DIR}/presolve_test.cu LABELS numopt) +ConfigureTest(GF2_PRESOLVE_TEST + ${CMAKE_CURRENT_SOURCE_DIR}/gf2_presolve_test.cpp + ${CUOPT_TEST_DIR}/../src/mip_heuristics/presolve/gf2_presolve.cpp + LABELS numopt) # Disable for now # ConfigureTest(FEASIBILITY_JUMP_TEST # ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump_tests.cu diff --git a/cpp/tests/mip/gf2_presolve_test.cpp b/cpp/tests/mip/gf2_presolve_test.cpp new file mode 100644 index 0000000000..6fabeddf44 --- /dev/null +++ b/cpp/tests/mip/gf2_presolve_test.cpp @@ -0,0 +1,84 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace cuopt::mathematical_optimization::test { + +TEST(gf2_presolve, uses_compact_constraint_indices) +{ + constexpr int num_non_gf2_constraints = 128; + constexpr int num_rows = num_non_gf2_constraints + 2; + + std::vector> entries; + entries.reserve(2 * num_non_gf2_constraints + 5); + + // These duplicate fractional equalities are deliberately not GF2 constraints. The last two + // rows mirror the x + 2y = k structure in the enlight instances, so their raw row indices (128 + // and 129) differ from their compact GF2 ordinals (0 and 1). + for (int row = 0; row < num_non_gf2_constraints; ++row) { + entries.emplace_back(row, 0, 1.0); + entries.emplace_back(row, 1, 1.0); + } + entries.emplace_back(num_non_gf2_constraints, 2, 1.0); + entries.emplace_back(num_non_gf2_constraints, 3, 1.0); + entries.emplace_back(num_non_gf2_constraints, 4, 2.0); + entries.emplace_back(num_non_gf2_constraints + 1, 3, 1.0); + entries.emplace_back(num_non_gf2_constraints + 1, 5, 2.0); + + std::vector row_bounds(num_non_gf2_constraints, 0.5); + row_bounds.insert(row_bounds.end(), {1.0, 0.0}); + + papilo::ProblemBuilder builder; + builder.reserve(entries.size(), num_rows, 6); + builder.setNumRows(num_rows); + builder.setNumCols(6); + builder.setObjAll({0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + builder.setColLbAll({-1.0, -1.0, 0.0, 0.0, -1.0, -1.0}); + builder.setColUbAll({1.0, 1.0, 1.0, 1.0, 1.0, 1.0}); + builder.setColIntegralAll({0, 0, 1, 1, 1, 1}); + builder.setRowLhsAll(row_bounds); + builder.setRowRhsAll(std::move(row_bounds)); + builder.addEntryAll(std::move(entries)); + auto problem = builder.build(); + + papilo::Num num; + papilo::PresolveOptions options; + papilo::Statistics statistics; + papilo::Message message; + papilo::PostsolveStorage postsolve(problem, num, options); + papilo::ProblemUpdate problem_update( + problem, postsolve, statistics, options, num, message); + papilo::Reductions reductions; + double elapsed_time = 0.0; + papilo::Timer timer(elapsed_time); + int reason_of_infeasibility = 0; + + mip::GF2Presolve presolver; + auto status = + presolver.execute(problem, problem_update, num, reductions, timer, reason_of_infeasibility); + + EXPECT_EQ(status, papilo::PresolveStatus::kReduced); +} + +} // namespace cuopt::mathematical_optimization::test From bf007d75eadaa402c088c91c6f4c8ce314c97fe0 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Jul 2026 09:51:59 -0700 Subject: [PATCH 2/2] removed separate test file --- cpp/tests/mip/CMakeLists.txt | 4 -- cpp/tests/mip/gf2_presolve_test.cpp | 84 ---------------------------- cpp/tests/mip/presolve_test.cu | 87 +++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 88 deletions(-) delete mode 100644 cpp/tests/mip/gf2_presolve_test.cpp diff --git a/cpp/tests/mip/CMakeLists.txt b/cpp/tests/mip/CMakeLists.txt index 24bf392a36..d533f09c2d 100644 --- a/cpp/tests/mip/CMakeLists.txt +++ b/cpp/tests/mip/CMakeLists.txt @@ -42,10 +42,6 @@ ConfigureTest(EMPTY_FIXED_PROBLEMS_TEST ConfigureTest(PRESOLVE_TEST ${CMAKE_CURRENT_SOURCE_DIR}/presolve_test.cu LABELS numopt) -ConfigureTest(GF2_PRESOLVE_TEST - ${CMAKE_CURRENT_SOURCE_DIR}/gf2_presolve_test.cpp - ${CUOPT_TEST_DIR}/../src/mip_heuristics/presolve/gf2_presolve.cpp - LABELS numopt) # Disable for now # ConfigureTest(FEASIBILITY_JUMP_TEST # ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump_tests.cu diff --git a/cpp/tests/mip/gf2_presolve_test.cpp b/cpp/tests/mip/gf2_presolve_test.cpp deleted file mode 100644 index 6fabeddf44..0000000000 --- a/cpp/tests/mip/gf2_presolve_test.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace cuopt::mathematical_optimization::test { - -TEST(gf2_presolve, uses_compact_constraint_indices) -{ - constexpr int num_non_gf2_constraints = 128; - constexpr int num_rows = num_non_gf2_constraints + 2; - - std::vector> entries; - entries.reserve(2 * num_non_gf2_constraints + 5); - - // These duplicate fractional equalities are deliberately not GF2 constraints. The last two - // rows mirror the x + 2y = k structure in the enlight instances, so their raw row indices (128 - // and 129) differ from their compact GF2 ordinals (0 and 1). - for (int row = 0; row < num_non_gf2_constraints; ++row) { - entries.emplace_back(row, 0, 1.0); - entries.emplace_back(row, 1, 1.0); - } - entries.emplace_back(num_non_gf2_constraints, 2, 1.0); - entries.emplace_back(num_non_gf2_constraints, 3, 1.0); - entries.emplace_back(num_non_gf2_constraints, 4, 2.0); - entries.emplace_back(num_non_gf2_constraints + 1, 3, 1.0); - entries.emplace_back(num_non_gf2_constraints + 1, 5, 2.0); - - std::vector row_bounds(num_non_gf2_constraints, 0.5); - row_bounds.insert(row_bounds.end(), {1.0, 0.0}); - - papilo::ProblemBuilder builder; - builder.reserve(entries.size(), num_rows, 6); - builder.setNumRows(num_rows); - builder.setNumCols(6); - builder.setObjAll({0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); - builder.setColLbAll({-1.0, -1.0, 0.0, 0.0, -1.0, -1.0}); - builder.setColUbAll({1.0, 1.0, 1.0, 1.0, 1.0, 1.0}); - builder.setColIntegralAll({0, 0, 1, 1, 1, 1}); - builder.setRowLhsAll(row_bounds); - builder.setRowRhsAll(std::move(row_bounds)); - builder.addEntryAll(std::move(entries)); - auto problem = builder.build(); - - papilo::Num num; - papilo::PresolveOptions options; - papilo::Statistics statistics; - papilo::Message message; - papilo::PostsolveStorage postsolve(problem, num, options); - papilo::ProblemUpdate problem_update( - problem, postsolve, statistics, options, num, message); - papilo::Reductions reductions; - double elapsed_time = 0.0; - papilo::Timer timer(elapsed_time); - int reason_of_infeasibility = 0; - - mip::GF2Presolve presolver; - auto status = - presolver.execute(problem, problem_update, num, reductions, timer, reason_of_infeasibility); - - EXPECT_EQ(status, papilo::PresolveStatus::kReduced); -} - -} // namespace cuopt::mathematical_optimization::test diff --git a/cpp/tests/mip/presolve_test.cu b/cpp/tests/mip/presolve_test.cu index 5d82dcec0a..9f9b65d6e0 100644 --- a/cpp/tests/mip/presolve_test.cu +++ b/cpp/tests/mip/presolve_test.cu @@ -22,6 +22,8 @@ #include +#include +#include #include #include #include @@ -63,4 +65,89 @@ TEST(problem, find_implied_integers) ((int)mip::problem_t::var_flags_t::VAR_IMPLIED_INTEGER)); } +TEST(gf2_presolve, uses_compact_constraint_indices) +{ + constexpr int num_packing_vars = 128; + constexpr int num_gf2_vars = 6; + constexpr int num_key_vars = 6; + constexpr int num_packing_rows = 128; + constexpr int num_key_rows = 2 * num_key_vars; + constexpr int num_gf2_rows = 6; + constexpr int num_vars = num_packing_vars + num_gf2_vars + num_key_vars; + constexpr int num_rows = num_packing_rows + num_key_rows + num_gf2_rows; + constexpr int x_offset = num_packing_vars; + constexpr int y_offset = x_offset + num_gf2_vars; + + std::vector values; + std::vector indices; + std::vector offsets{0}; + std::vector constraint_lb(num_rows, 1.0); + std::vector constraint_ub(num_rows, 2.0); + + auto add_entry = [&](int column, double value) { + indices.push_back(column); + values.push_back(value); + }; + auto finish_row = [&] { offsets.push_back(static_cast(values.size())); }; + + // A normal binary MIP block keeps the GF2 rows at high raw row indices. + for (int row = 0; row < num_packing_rows; ++row) { + std::array columns{row, (row + 1) % num_packing_vars, (row + 2) % num_packing_vars}; + std::sort(columns.begin(), columns.end()); + for (int column : columns) { + add_entry(column, 1.0); + } + finish_row(); + } + + // Keep every GF2 key column non-singleton without forcing it. + for (int key = 0; key < num_key_vars; ++key) { + add_entry(3 * key, 1.0); + add_entry(3 * key + 1, 1.0); + add_entry(y_offset + key, 1.0); + finish_row(); + + add_entry(3 * key + 1, 1.0); + add_entry(3 * key + 2, 1.0); + add_entry(y_offset + key, 1.0); + finish_row(); + } + + // Over GF(2), this is J-I for even dimension 6, hence nonsingular. Three positive and two + // negative coefficients per row prevent ordinary bound propagation from fixing the key. + for (int row = 0; row < num_gf2_rows; ++row) { + int term = 0; + for (int col = 0; col < num_gf2_vars; ++col) { + if (col == row) { continue; } + add_entry(x_offset + col, term < 3 ? 1.0 : -1.0); + ++term; + } + add_entry(y_offset + row, 2.0); + finish_row(); + constraint_lb[num_packing_rows + num_key_rows + row] = 1.0; + constraint_ub[num_packing_rows + num_key_rows + row] = 1.0; + } + + const raft::handle_t handle_{}; + optimization_problem_t problem(&handle_); + std::vector objective(num_vars, 1.0); + std::vector variable_lb(num_vars, 0.0); + std::vector variable_ub(num_vars, 1.0); + std::vector variable_types(num_vars, var_t::INTEGER); + problem.set_csr_constraint_matrix( + values.data(), values.size(), indices.data(), indices.size(), offsets.data(), offsets.size()); + problem.set_objective_coefficients(objective.data(), objective.size()); + problem.set_variable_lower_bounds(variable_lb.data(), variable_lb.size()); + problem.set_variable_upper_bounds(variable_ub.data(), variable_ub.size()); + problem.set_variable_types(variable_types.data(), variable_types.size()); + problem.set_constraint_lower_bounds(constraint_lb.data(), constraint_lb.size()); + problem.set_constraint_upper_bounds(constraint_ub.data(), constraint_ub.size()); + + auto presolver = std::make_unique>(); + auto result = presolver->apply( + problem, problem_category_t::MIP, presolver_t::Papilo, false, 1e-6, 1e-12, 20, 1); + + EXPECT_EQ(result.status, mip::third_party_presolve_status_t::REDUCED); +} + } // namespace cuopt::mathematical_optimization::test