Skip to content

Commit ae0a38a

Browse files
authored
GF2 presolve rank-deficiency fix (#1660)
## Issue Authors: - Alice Boucher (https://github.com/aliceb-nv) Approvers: - Akif ÇÖRDÜK (https://github.com/akifcorduk) - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) URL: #1660
1 parent 361d41f commit ae0a38a

7 files changed

Lines changed: 828 additions & 178 deletions

File tree

cpp/src/mip_heuristics/presolve/gf2_presolve.cpp

Lines changed: 146 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
#include "gf2_presolve.hpp"
99

1010
#include <mip_heuristics/mip_constants.hpp>
11+
#include <utilities/macros.cuh>
1112

1213
#include <cmath>
14+
#include <cstdint>
1315
#include <unordered_map>
1416

1517
#if GF2_PRESOLVE_DEBUG
@@ -33,55 +35,103 @@ static inline i_t positive_modulo(i_t i, i_t n)
3335
return (i % n + n) % n;
3436
}
3537

38+
static constexpr int GF2_WORD_BITS = 64;
39+
40+
static inline int gf2_nwords(int N) { return (N + GF2_WORD_BITS - 1) / GF2_WORD_BITS; }
41+
42+
static inline bool gf2_test_bit(const std::vector<uint64_t>& row, int col)
43+
{
44+
return (row[col / GF2_WORD_BITS] >> (col % GF2_WORD_BITS)) & uint64_t{1};
45+
}
46+
47+
static inline void gf2_set_bit(std::vector<uint64_t>& row, int col)
48+
{
49+
row[col / GF2_WORD_BITS] |= (uint64_t{1} << (col % GF2_WORD_BITS));
50+
}
51+
3652
// this is kind-of a stopgap implementation (as in practice MIPLIB2017 only contains a couple of GF2
3753
// problems and they're small) but cuDSS could be used for this since A is likely to be sparse and
3854
// low-bandwidth (i think?) unlikely to occur in real-world problems however. doubt it'd be worth
39-
// the effort trashes A and b, return true if solved
40-
static bool gf2_solve(std::vector<std::vector<int>>& A, std::vector<int>& b, std::vector<int>& x)
55+
// the effort
56+
gf2_status_t gf2_solve(std::vector<std::vector<uint64_t>>& A,
57+
int n_cols,
58+
std::vector<int>& b,
59+
std::vector<int>& x,
60+
std::vector<uint8_t>& determined)
4161
{
42-
int i, j, k;
43-
const int N = A.size();
44-
for (i = 0; i < N; i++) {
45-
// Find pivot
62+
const int m = (int)A.size();
63+
const int n = n_cols;
64+
const int nwords = gf2_nwords(n);
65+
cuopt_assert(m > 0, "");
66+
cuopt_assert(n >= 0, "");
67+
cuopt_assert((int)b.size() == m, "");
68+
cuopt_assert((int)A[0].size() == nwords, "");
69+
70+
// pivot_row_of_col[c] = row holding the pivot for column c, or -1 if free
71+
std::vector<int> pivot_row_of_col(n, -1);
72+
int next_pivot_row = 0;
73+
74+
for (int col = 0; col < n; col++) {
4675
int pivot = -1;
47-
for (j = i; j < N; j++) {
48-
if (A[j][i]) {
49-
pivot = j;
76+
for (int r = next_pivot_row; r < m; r++) {
77+
if (gf2_test_bit(A[r], col)) {
78+
pivot = r;
5079
break;
5180
}
5281
}
53-
if (pivot == -1) return false; // No solution
54-
55-
// Swap current row with pivot row if needed
56-
if (pivot != i) {
57-
for (k = 0; k < N; k++) {
58-
int temp = A[i][k];
59-
A[i][k] = A[pivot][k];
60-
A[pivot][k] = temp;
61-
}
62-
int temp = b[i];
63-
b[i] = b[pivot];
64-
b[pivot] = temp;
82+
if (pivot == -1) continue; // free column
83+
84+
if (pivot != next_pivot_row) {
85+
std::swap(A[next_pivot_row], A[pivot]);
86+
std::swap(b[next_pivot_row], b[pivot]);
6587
}
6688

67-
// Eliminate downwards
68-
for (j = i + 1; j < N; j++) {
69-
if (A[j][i]) {
70-
for (k = i; k < N; k++)
71-
A[j][k] ^= A[i][k];
72-
b[j] ^= b[i];
89+
// Eliminate column from all other rows (RREF)
90+
for (int r = 0; r < m; r++) {
91+
if (r != next_pivot_row && gf2_test_bit(A[r], col)) {
92+
for (int w = 0; w < nwords; w++)
93+
A[r][w] ^= A[next_pivot_row][w];
94+
b[r] ^= b[next_pivot_row];
7395
}
7496
}
97+
98+
pivot_row_of_col[col] = next_pivot_row;
99+
next_pivot_row++;
75100
}
76101

77-
// Back-substitution
78-
for (i = N - 1; i >= 0; i--) {
79-
x[i] = b[i];
80-
for (j = i + 1; j < N; j++)
81-
x[i] ^= (A[i][j] & x[j]);
82-
if (!A[i][i] && x[i]) return false; // No solution
102+
const int rank = next_pivot_row;
103+
for (int r = rank; r < m; r++) {
104+
for (int w = 0; w < nwords; w++) {
105+
cuopt_assert(A[r][w] == 0, "RREF unused row must be zero");
106+
}
107+
if (b[r]) return gf2_status_t::Infeasible;
83108
}
84-
return true; // Success
109+
110+
std::vector<uint64_t> free_mask(nwords, 0);
111+
for (int c = 0; c < n; c++) {
112+
if (pivot_row_of_col[c] == -1) gf2_set_bit(free_mask, c);
113+
}
114+
115+
determined.assign(n, 0);
116+
x.assign(n, 0);
117+
118+
for (int col = 0; col < n; col++) {
119+
int row = pivot_row_of_col[col];
120+
if (row == -1) continue; // free: x=0, determined=false
121+
122+
bool has_free_support = false;
123+
for (int w = 0; w < nwords; w++) {
124+
if (A[row][w] & free_mask[w]) {
125+
has_free_support = true;
126+
break;
127+
}
128+
}
129+
// Particular solution with free vars = 0: x[pivot] = b[row]
130+
x[col] = b[row];
131+
determined[col] = !has_free_support;
132+
}
133+
134+
return gf2_status_t::Feasible;
85135
}
86136

87137
template <typename f_t>
@@ -161,16 +211,20 @@ papilo::PresolveStatus GF2Presolve<f_t>::execute(const papilo::Problem<f_t>& pro
161211
if (key_var_idx != -1) { NOT_GF2("multiple key variables", var_idx); }
162212
key_var_idx = var_idx;
163213
key_var_coeff = coeff;
164-
gf2_key_vars.insert({var_idx, gf2_key_vars.size()});
165214
} else {
166215
// Binary variable
167216
constraint_bin_vars.push_back({var_idx, coeff});
168-
gf2_bin_vars.insert({var_idx, gf2_bin_vars.size()});
169217
}
170218
}
171219

172220
if (key_var_idx == -1) NOT_GF2("missing key variable");
173221

222+
// Commit to global maps only after the row is fully accepted
223+
gf2_key_vars.insert({(size_t)key_var_idx, gf2_key_vars.size()});
224+
for (auto [bin_var, _] : constraint_bin_vars) {
225+
gf2_bin_vars.insert({bin_var, gf2_bin_vars.size()});
226+
}
227+
174228
gf2_constraints.emplace_back((size_t)cstr_idx,
175229
std::move(constraint_bin_vars),
176230
std::pair<size_t, f_t>{key_var_idx, key_var_coeff},
@@ -183,12 +237,11 @@ papilo::PresolveStatus GF2Presolve<f_t>::execute(const papilo::Problem<f_t>& pro
183237
// If no GF2 constraints found, return unchanged
184238
if (gf2_constraints.empty()) { return papilo::PresolveStatus::kUnchanged; }
185239

186-
// Skip if that would cause computational explosion (O(n^3) with simple gaussian elimination)
187-
if (gf2_constraints.size() > 1000) { return papilo::PresolveStatus::kUnchanged; }
240+
// one unique key per GF2 row. #bins may differ from #rows.
241+
if (gf2_key_vars.size() != gf2_constraints.size()) { return papilo::PresolveStatus::kUnchanged; }
188242

189-
// Validate structure
190-
if (gf2_key_vars.size() != gf2_constraints.size() ||
191-
gf2_bin_vars.size() != gf2_constraints.size()) {
243+
// Skip if that would cause computational explosion (dense GE ~ O(m * n * min(m,n)))
244+
if (gf2_constraints.size() > 1000 || gf2_bin_vars.size() > 1000) {
192245
return papilo::PresolveStatus::kUnchanged;
193246
}
194247

@@ -198,40 +251,76 @@ papilo::PresolveStatus GF2Presolve<f_t>::execute(const papilo::Problem<f_t>& pro
198251
gf2_bin_vars_invmap.insert({gf2_idx, var_idx});
199252
}
200253

201-
// Build binary matrix
202-
// Could be a flat vector but. oh well. in practice N is small
203-
std::vector<std::vector<int>> A(gf2_constraints.size(),
204-
std::vector<int>(gf2_constraints.size(), 0));
205-
std::vector<int> b(gf2_constraints.size());
206-
for (size_t gf2_cstr_idx = 0; gf2_cstr_idx < gf2_constraints.size(); ++gf2_cstr_idx) {
254+
// Build binary matrix as packed uint64_t words
255+
const int m = (int)gf2_constraints.size();
256+
const int n = (int)gf2_bin_vars.size();
257+
const int nwords = gf2_nwords(n);
258+
std::vector<std::vector<uint64_t>> A(m, std::vector<uint64_t>(nwords, 0));
259+
std::vector<int> b(m);
260+
for (int gf2_cstr_idx = 0; gf2_cstr_idx < m; ++gf2_cstr_idx) {
207261
const auto& cons = gf2_constraints[gf2_cstr_idx];
208262
for (auto [bin_var, _] : cons.bin_vars) {
209-
A[gf2_cstr_idx][gf2_bin_vars[bin_var]] = 1;
263+
gf2_set_bit(A[gf2_cstr_idx], (int)gf2_bin_vars[bin_var]);
210264
}
211265
b[gf2_cstr_idx] = cons.rhs;
212266
}
213267

214-
std::vector<int> solution(gf2_constraints.size());
215-
bool feasible = gf2_solve(A, b, solution);
216-
if (!feasible) { return papilo::PresolveStatus::kInfeasible; }
268+
std::vector<int> solution(n);
269+
std::vector<uint8_t> determined(n);
270+
gf2_status_t gf2_status = gf2_solve(A, n, b, solution, determined);
271+
if (gf2_status == gf2_status_t::Infeasible) { return papilo::PresolveStatus::kInfeasible; }
217272

218273
std::unordered_map<size_t, f_t> fixings;
219-
// Fix binary variables
220-
for (size_t sol_idx = 0; sol_idx < gf2_constraints.size(); ++sol_idx) {
221-
fixings[gf2_bin_vars_invmap[sol_idx]] = solution[sol_idx];
274+
275+
// Fix only uniquely determined binaries
276+
for (int sol_idx = 0; sol_idx < n; ++sol_idx) {
277+
if (determined[sol_idx]) { fixings[gf2_bin_vars_invmap[sol_idx]] = solution[sol_idx]; }
222278
}
223279

224-
// Compute fixings for key variables by solving for the constraint
280+
// Fix key only when every binary in that constraint is uniquely determined
225281
for (const auto& cons : gf2_constraints) {
282+
bool all_bins_determined = true;
283+
for (auto [bin_var, _] : cons.bin_vars) {
284+
cuopt_assert(gf2_bin_vars.count(bin_var), "");
285+
if (!determined[gf2_bin_vars[bin_var]]) {
286+
all_bins_determined = false;
287+
break;
288+
}
289+
}
290+
if (!all_bins_determined) continue;
291+
226292
auto [key_var_idx, key_var_coeff] = cons.key_var;
227-
f_t constraint_rhs = lhs_values[cons.cstr_idx]; // equality constraint
293+
const f_t constraint_rhs = std::round(lhs_values[cons.cstr_idx]);
228294
f_t lhs = -constraint_rhs;
229295
for (auto [bin_var, coeff] : cons.bin_vars) {
296+
cuopt_assert(fixings.count(bin_var), "");
230297
lhs += fixings[bin_var] * coeff;
231298
}
232-
fixings[key_var_idx] = std::round(-lhs / key_var_coeff);
299+
const f_t key_val = std::round(-lhs / key_var_coeff);
300+
301+
// Residual must be exactly 0 after rounding (rejects half-integer / inconsistent carry)
302+
if (!num.isEq(lhs + key_val * key_var_coeff, f_t{0})) {
303+
return papilo::PresolveStatus::kInfeasible;
304+
}
305+
// Dual-role: same var already fixed as a GF(2) binary
306+
if (fixings.count(key_var_idx) && !num.isEq(fixings[key_var_idx], key_val)) {
307+
return papilo::PresolveStatus::kInfeasible;
308+
}
309+
if (!col_flags[key_var_idx].test(papilo::ColFlag::kLbInf) &&
310+
key_val < lower_bounds[key_var_idx] - integrality_tolerance) {
311+
return papilo::PresolveStatus::kInfeasible;
312+
}
313+
if (!col_flags[key_var_idx].test(papilo::ColFlag::kUbInf) &&
314+
key_val > upper_bounds[key_var_idx] + integrality_tolerance) {
315+
return papilo::PresolveStatus::kInfeasible;
316+
}
317+
318+
fixings[key_var_idx] = key_val;
233319
}
234320

321+
// necessary because Papilo asserts on empty TransactionGuard
322+
if (fixings.empty()) { return papilo::PresolveStatus::kUnchanged; }
323+
235324
papilo::PresolveStatus status = papilo::PresolveStatus::kUnchanged;
236325
papilo::TransactionGuard rg{reductions};
237326
for (const auto& [var_idx, fixing] : fixings) {

cpp/src/mip_heuristics/presolve/gf2_presolve.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,22 @@
2020
#pragma GCC diagnostic pop
2121
#endif
2222

23+
#include <cstdint>
24+
#include <vector>
25+
2326
namespace cuopt::mathematical_optimization::mip {
2427

28+
enum class gf2_status_t { Feasible, Infeasible };
29+
30+
// Solves A x = b over GF(2). A is m x n, each row packed into ceil(n/64) words (column c lives at
31+
// word c/64, bit c%64). Trashes A and b. On Feasible, x is the solution obtained by setting the
32+
// free variables to 0, and determined[c] is set iff x[c] is the same in every solution.
33+
gf2_status_t gf2_solve(std::vector<std::vector<uint64_t>>& A,
34+
int n_cols,
35+
std::vector<int>& b,
36+
std::vector<int>& x,
37+
std::vector<uint8_t>& determined);
38+
2539
template <typename f_t>
2640
class GF2Presolve : public papilo::PresolveMethod<f_t> {
2741
public:

0 commit comments

Comments
 (0)