Skip to content

Block Bounded Value Elimination presolving pass for binary problems - #1683

Open
aliceb-nv wants to merge 34 commits into
mainfrom
bnatt-presolve
Open

Block Bounded Value Elimination presolving pass for binary problems#1683
aliceb-nv wants to merge 34 commits into
mainfrom
bnatt-presolve

Conversation

@aliceb-nv

@aliceb-nv aliceb-nv commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This PR adds a binary reduction presolving pass inspired by SAT-related bounded variable elimination work such as "Effective Preprocessing in SAT through Variable and Clause Elimination" (SAT 2005).

Block BVE eliminates a small set of non-objective binary variables (the interior) by projecting the constraints they appear in onto the other binary variables of those same rows (the boundary), which stay in the model.
For every assignment of the boundary it checks if the interior can be set to satisfy each row of the block.

The assignments admitting no such setting are everything the block still forces on the rest of the
model, so these can be turned into no-good rows while eliminating the interior columns altogether. Reductions requiring new no-good clauses are only committed if they would result in fewer total rows, to avoid excessive growth.

Candidates come from the probing cache. Each non-objective binary carrying at least one edge becomes a seed, tried in order of increasing row count, grown one variable at a time: among the eligible implication neighbors of the current interior, it absorbs whichever leaves the smallest boundary, and only while the boundary strictly shrinks.

Each grown interior is staged into a block and dropped if it breaks preset caps. Disjoint candidates are batched and enumerated on the GPU, yielding a feasibility bit and witness interior per boundary pattern, and the derived clauses are checked against that table and checked for growth.

Results are as follows on MIPLIB2017. Most reductions occur on mostly-binary combinatorial problems like the bnatt class, the piperout class, and the cryptanalysis classes.

We now find +1 feasible/optimal: bnatt400 The BKS is found in ~5s, and optimality proven after B&B in ~2min.

  Instance                     Relative row reduction    Relative column reduction
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━
   bnatt500                                    40.532%                      46.000%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   bnatt400                                    40.720%                      45.167%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   piperout-27                                 19.922%                      10.507%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   piperout-08                                 14.991%                       7.020%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   cryptanalysiskb128n5obj14                    7.480%                       9.175%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   cryptanalysiskb128n5obj16                    7.480%                       9.169%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   neos-950242                                  2.875%                       2.500%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   physiciansched3-3                            2.128%                       3.874%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   neos8                                        1.243%                       1.240%
  ───────────────────────────  ────────────────────────  ───────────────────────────
   roll3000                                     1.133%                       1.201%

Description

Issue

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
    • NA
  • Documentation
    • The documentation is up to date with these changes
    • Added new documentation
    • NA

@aliceb-nv aliceb-nv added this to the 26.10 milestone Aug 6, 2026
@aliceb-nv
aliceb-nv requested review from a team as code owners August 6, 2026 14:46
@aliceb-nv aliceb-nv added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels Aug 6, 2026
@aliceb-nv
aliceb-nv requested review from Bubullzz and hlinsen August 6, 2026 14:46
@copy-pr-bot

copy-pr-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5cc9f1cd-b68a-42f3-a82c-eac74d042e67

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc1e57 and acac6b5.

📒 Files selected for processing (1)
  • ci/validate_wheel.sh

📝 Walkthrough

Walkthrough

Changes

Block-BVE presolve

Layer / File(s) Summary
Contracts and model preparation
cpp/include/cuopt/..., cpp/src/mip_heuristics/presolve/block_bve.cuh, cpp/src/utilities/integer_scaling.hpp, cpp/src/mip_heuristics/problem/*
Added block-BVE settings and interfaces, integer-scaling utilities, objective-offset initialization, and host CSR constraint replacement.
Block-BVE projection and reduction
cpp/src/mip_heuristics/presolve/block_bve.cu
Implemented block discovery, exact GPU projection, implication extraction, validated reductions, postsolve records, and variable compaction.
Probing identity and reconstruction
cpp/src/mip_heuristics/presolve/probing_cache.*, cpp/src/mip_heuristics/problem/presolve_data.*, cpp/src/mip_heuristics/solve.cu
Switched probing identity to original variable IDs, merged block forcings, and unified affine and block-BVE postsolve replay.
Presolve orchestration and export
cpp/src/mip_heuristics/diversity/*, cpp/src/io/mps_writer.cpp, cpp/src/mip_heuristics/CMakeLists.txt
Added timed probing, repeated block-BVE rounds, fixing and bound propagation, optional MPS export, build registration, and objective diagnostics.
Validation and development guidance
cpp/tests/mip/block_bve_test.cu, cpp/tests/internal/CMakeLists.txt, skills/cuopt-developer/*, ci/validate_wheel.sh
Added projection, reconstruction, equivalence, integration, and benchmark tests, plus CUDA, MIP testing, and wheel-size updates.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: hlinsen, ramakrishnap-nv, bubullzz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the new Block Bounded Value Elimination presolving pass for binary problems.
Description check ✅ Passed The description directly explains the Block BVE implementation, algorithm, scope, testing results, and reported MIPLIB improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bnatt-presolve

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (3)
cpp/src/mip_heuristics/presolve/block_bve.cu (1)

1321-1321: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider preserving the row names of the surviving rows.

The call passes {} for the names, so set_constraints_from_host_csr clears row_names for the whole model. The rows that block-BVE keeps are unchanged, and their names are still available in problem.row_names before the call. After the call, mps_writer_t::write and the log messages fall back to generated names such as R12, which makes an exported reduced model hard to compare with the input.

Filter the existing names for the kept rows, and generate names for the appended clause rows.

♻️ Proposed name mapping
+  std::vector<std::string> new_names;
+  if (problem.row_names.size() == (size_t)n_rows) {
+    new_names.reserve(n_rows + plan.added_rows.size());
+    for (i_t r = 0; r < n_rows; ++r)
+      if (!removed[r]) new_names.push_back(problem.row_names[r]);
+    for (size_t c = 0; c < plan.added_rows.size(); ++c)
+      new_names.push_back("bve_nogood_" + std::to_string(c));
+  }
   work_units += double(new_var.size()) + double(new_clb.size());
-  problem.set_constraints_from_host_csr(new_off, new_var, new_coef, new_clb, new_cub, {});
+  problem.set_constraints_from_host_csr(new_off, new_var, new_coef, new_clb, new_cub, new_names);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/mip_heuristics/presolve/block_bve.cu` at line 1321, Preserve row
names when rebuilding constraints in the block-BVE flow: derive names for the
surviving original rows from problem.row_names using the same kept-row mapping
as new_off/new_var/new_coef, append generated names for newly added clause rows,
and pass the resulting names instead of {} to set_constraints_from_host_csr.
Ensure the name list matches the rebuilt constraint count and preserves existing
names for unchanged rows.
cpp/src/mip_heuristics/presolve/probing_cache.cu (1)

194-201: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the cache-presence test.

Line 196 reads if (!bound_presolve.probing_cache.probing_cache.count(var_original) > 0). This parses as (!count(...)) > 0, which is true only when the count is zero. The result matches the intent, but the expression is misleading for a reader. Use an explicit form.

♻️ Proposed clarification
-    if (!bound_presolve.probing_cache.probing_cache.count(var_original) > 0) {
+    if (bound_presolve.probing_cache.probing_cache.count(var_original) == 0) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/mip_heuristics/presolve/probing_cache.cu` around lines 194 - 201,
Clarify the cache-presence condition in the probing-cache insertion logic by
replacing the negated count comparison with an explicit zero-count check. Update
the condition surrounding probing_cache.count(var_original); keep the existing
insertion and update branches unchanged.
cpp/src/mip_heuristics/problem/presolve_data.cu (1)

143-160: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add asserts for the interior width and the witness validity.

Two invariants of the BlockBve branch are unchecked:

  1. w is a uint32_t, so rec.bve.interior.size() must not exceed 32. If BVE_MAX_INTERIOR is ever raised above 32, the loop silently reconstructs zeros for the extra positions. The existing assert covers only the boundary width.
  2. rec.bve.witness[pattern] is only written for boundary patterns the projection found feasible. For an infeasible pattern the slot keeps its atomicMin initializer, and the reconstruction writes all ones. The added no-good clauses make that unreachable for a feasible reduced assignment, so an assert is the right way to record the invariant.
🛡️ Proposed asserts
       case reconstruction_kind_t::BlockBve: {
         cuopt_assert(rec.bve.witness.size() == (size_t{1} << rec.bve.boundary.size()),
                      "block witness size mismatch");
+        cuopt_assert(rec.bve.interior.size() <= 32,
+                     "block interior wider than the witness bit width");
         uint32_t pattern = 0;
         for (size_t j = 0; j < rec.bve.boundary.size(); ++j) {
           cuopt_assert(rec.bve.boundary[j] < (i_t)h_assignment.size(),
                        "block boundary out of bounds");
           const int bit = (h_assignment[rec.bve.boundary[j]] > static_cast<f_t>(0.5)) ? 1 : 0;
           pattern |= (static_cast<uint32_t>(bit) << j);
         }
         const uint32_t w = rec.bve.witness[pattern];
+        cuopt_assert(w != std::numeric_limits<uint32_t>::max(),
+                     "boundary pattern has no feasible interior witness");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/mip_heuristics/problem/presolve_data.cu` around lines 143 - 160, In
the BlockBve reconstruction branch, add an assertion that
rec.bve.interior.size() does not exceed 32 before shifting bits from the
uint32_t witness. After selecting rec.bve.witness[pattern], assert that the
witness is not the atomicMin sentinel representing an infeasible boundary
pattern, then preserve the existing interior reconstruction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/math_optimization/solver_settings.cu`:
- Around line 214-216: Update the block-BVE loop guard to require
run_probing_cache, adding !run_probing_cache to the existing condition so
block_bve_presolve is skipped when probing is disabled. Preserve the current BVE
behavior when the probing cache is enabled.

In `@cpp/src/mip_heuristics/diversity/diversity_manager.cu`:
- Around line 364-373: Guard the update_variable_bounds call in the surrounding
modification function so it runs only when var_indices is non-empty, matching
apply_modification_queue_to_problem. Keep n_applied assigned from
var_indices.size() and preserve the existing return behavior.
- Around line 481-494: Update the GPU-export block in solve_mip around
problem_to_mps_data_model and mps_writer_t::write to avoid calling exit from
library code; return through the normal solver path and propagate export success
or failure to the caller. Check the result of writer.write and report failures
instead of treating them as successful completion. Sanitize instance_name before
forming mps_path so model-provided path separators cannot escape the working
directory.

In `@cpp/src/mip_heuristics/presolve/block_bve.cu`:
- Around line 1111-1135: Make the deterministic-order fixes in
cpp/src/mip_heuristics/presolve/block_bve.cu at lines 1111-1135 and 716-717:
sort each adjacency list in the helper that builds and returns out, and have
grow_seed_interior iterate a sorted copy of cands_w for stable boundary-size
tie-breaking. In the per-bin work estimate at lines 716-717, accumulate
operation counts as an integer and convert to double only once after summing, so
the result is independent of bins iteration order.
- Around line 1015-1027: Update the growth loop around grow_seed_interior to use
taskloop when block_bve_presolve is called from an existing OpenMP team,
avoiding nested teams, while retaining a parallel-for fallback for callers
outside any team. Preserve the current per-seed growth_done handling, result
assignment, and interior moves in both execution paths.

In `@cpp/src/mip_heuristics/problem/problem.cu`:
- Around line 2113-2128: Update the empty-state assignment near nnz
initialization to match op_problem_cstr_body: mark the problem empty whenever
the constraint matrix has no entries, including n_constraints == 0 with
remaining variables. Preserve the existing nnz calculation and validation
checks, and anchor the change to the empty assignment in the constructor
handling variables_in and coefficients_in.
- Line 208: Update the initialization of objective_offset in the affected
problem reconstruction flow so that when no_deep_copy is false it uses the same
reconstructed presolve_data source as the corresponding presolve_data field,
rather than problem_.presolve_data.objective_offset. Keep objective_offset
consistent with the rebuilt data after presolve.

In `@cpp/tests/mip/block_bve_test.cu`:
- Around line 797-801: In the test block that reconstructs recon_obj, assert
that m_obj.size() equals full.size() before iterating. Then iterate over the
full objective vector without the mismatched-size guard, preserving the existing
EXPECT_NEAR comparison.
- Around line 666-678: Make the size guard in brute_force_binary fatal by
replacing EXPECT_LE(nv, 24) with ASSERT_LE so oversized reduced models return
before the exponential loop. Because ASSERT_* requires a void-returning
function, update brute_force_binary and its callers to use an out-parameter for
bve_bf_t while preserving the existing result behavior.

In `@skills/cuopt-developer/SKILL.md`:
- Line 174: Re-run NVSkills validation for the updated skill set and refresh the
signature commit so it covers the current SKILL.md and related skill files.
Update the workflow around the skill package validation/signing step to
regenerate skill.oms.sig after the latest push, and keep the resulting signature
commit in the branch history as the expected artifact.

---

Nitpick comments:
In `@cpp/src/mip_heuristics/presolve/block_bve.cu`:
- Line 1321: Preserve row names when rebuilding constraints in the block-BVE
flow: derive names for the surviving original rows from problem.row_names using
the same kept-row mapping as new_off/new_var/new_coef, append generated names
for newly added clause rows, and pass the resulting names instead of {} to
set_constraints_from_host_csr. Ensure the name list matches the rebuilt
constraint count and preserves existing names for unchanged rows.

In `@cpp/src/mip_heuristics/presolve/probing_cache.cu`:
- Around line 194-201: Clarify the cache-presence condition in the probing-cache
insertion logic by replacing the negated count comparison with an explicit
zero-count check. Update the condition surrounding
probing_cache.count(var_original); keep the existing insertion and update
branches unchanged.

In `@cpp/src/mip_heuristics/problem/presolve_data.cu`:
- Around line 143-160: In the BlockBve reconstruction branch, add an assertion
that rec.bve.interior.size() does not exceed 32 before shifting bits from the
uint32_t witness. After selecting rec.bve.witness[pattern], assert that the
witness is not the atomicMin sentinel representing an infeasible boundary
pattern, then preserve the existing interior reconstruction.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6f1d63c3-3198-40af-ad58-d8487e872367

📥 Commits

Reviewing files that changed from the base of the PR and between 07dddec and f770241.

⛔ Files ignored due to path filters (12)
  • datasets/mip/block_bve/and_used.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/aux_with_obj.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/chain_or.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/heavy_reduce.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/infeasible.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/mixed.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/neq_used.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/or_used.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/random_a.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/random_b.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/random_c.mps is excluded by !**/*.mps
  • datasets/mip/block_bve/two_gadgets.mps is excluded by !**/*.mps
📒 Files selected for processing (21)
  • cpp/include/cuopt/mathematical_optimization/constants.h
  • cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp
  • cpp/src/io/mps_writer.cpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip_heuristics/CMakeLists.txt
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/mip_heuristics/diversity/population.cu
  • cpp/src/mip_heuristics/presolve/block_bve.cu
  • cpp/src/mip_heuristics/presolve/block_bve.cuh
  • cpp/src/mip_heuristics/presolve/probing_cache.cu
  • cpp/src/mip_heuristics/presolve/probing_cache.cuh
  • cpp/src/mip_heuristics/problem/presolve_data.cu
  • cpp/src/mip_heuristics/problem/presolve_data.cuh
  • cpp/src/mip_heuristics/problem/problem.cu
  • cpp/src/mip_heuristics/problem/problem.cuh
  • cpp/src/mip_heuristics/solve.cu
  • cpp/src/utilities/integer_scaling.hpp
  • cpp/tests/internal/CMakeLists.txt
  • cpp/tests/mip/block_bve_test.cu
  • skills/cuopt-developer/SKILL.md
  • skills/cuopt-developer/references/conventions.md

Comment thread cpp/src/math_optimization/solver_settings.cu
Comment thread cpp/src/mip_heuristics/diversity/diversity_manager.cu
Comment thread cpp/src/mip_heuristics/diversity/diversity_manager.cu
Comment thread cpp/src/mip_heuristics/presolve/block_bve.cu Outdated
Comment thread cpp/src/mip_heuristics/presolve/block_bve.cu
Comment thread cpp/src/mip_heuristics/problem/problem.cu
Comment thread cpp/src/mip_heuristics/problem/problem.cu
Comment thread cpp/tests/mip/block_bve_test.cu
Comment thread cpp/tests/mip/block_bve_test.cu Outdated
Comment thread skills/cuopt-developer/SKILL.md
@aliceb-nv

Copy link
Copy Markdown
Contributor Author

/ok to test 0abe89c

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

CI Test Summary

✅ All 31 test job(s) passed.

@aliceb-nv

Copy link
Copy Markdown
Contributor Author

/ok to test 4dc1e57

@aliceb-nv

Copy link
Copy Markdown
Contributor Author

/ok to test acac6b5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant