Skip to content

Recursive RINS#1482

Open
nguidotti wants to merge 48 commits into
NVIDIA:mainfrom
nguidotti:recursive-submip
Open

Recursive RINS#1482
nguidotti wants to merge 48 commits into
NVIDIA:mainfrom
nguidotti:recursive-submip

Conversation

@nguidotti

@nguidotti nguidotti commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

This PR implements the recursive RINS heuristics similar to HiGHS. More specifically, it applies a dive-like procedure to create a RINS sub-MIP, presolved it with Papilo and then passes to the cut-and-branch procedure. It also launch a CPU FJ worker over the sub-MIP that runs in parallel to cut-and-branch.

Since the current implementation, does not have an unified problem representation, this PR also adds:

  • A routine for converting a lp_problem_t (standard form) to user_problem_t (range form).
  • Allow Papilo to be applied to an user_problem_t instead of optimization_problem_t.
  • Adds an upper bound callback for retrieving the upper bound of the main solver (since it may change if another worker find a better incumbent).

Additionally, the number of worker per diving heuristic is no longer tied to the total number of workers, which allows the solver to use all diving heuristics in low thread counts (we rotate between the different heuristics for each diving worker launch).

Results

MIPLIB2017, 10min, GH200

================================================================================
 main-2026-07-03 (1) vs recursive-submip (2)
================================================================================

------------------------------------------------------------------------------------------------------------------------------
|                                        |       Run 1        |       Run 2        |     Abs. Diff.     |   Rel. Diff. (%)   |
------------------------------------------------------------------------------------------------------------------------------
| Imported                                                 240                  240                   +0                 --- |
| Feasible                                                 227                  228                   +1                 --- |
| Optimal                                                   86                   91                   +5                 --- |
| Solutions with <0.1% primal gap                          138                  151                  +13                 --- |
| Nodes explored (mean)                              1.326e+07            1.296e+07           -3.057e+05               -2.31 |
| Nodes explored (shifted geomean)                   1.262e+04            1.239e+04                 -230               -1.82 |
| Relative MIP gap (mean)                               0.2777               0.2726             -0.00512               -1.84 |
| Relative MIP gap (shifted geomean)                   0.09089              0.08901            -0.001883               -2.07 |
| Solve time (mean)                                      422.6                435.4               +12.79               +3.03 |
| Solve time (shifted geomean)                           196.9                226.6               +29.66               +15.1 |
| Primal gap (mean)                                      9.969                 9.29               -0.679               -6.81 |
| Primal gap (shifted geomean)                          0.4584               0.3491              -0.1093               -23.8 |
| Primal integral (mean)                                 25.02                22.97               -2.051                -8.2 |
| Primal integral (shifted geomean)                      2.849                2.421              -0.4275                 -15 |
------------------------------------------------------------------------------------------------------------------------------


----------------------------------------------------------------------
|             Name             |     status 1     |     status 2     |
----------------------------------------------------------------------
| neos-1171737                           feasible            optimal |
| radiationm40-10-02                     feasible            optimal |
| rail01                                  timeout           feasible |
| ran14x18-disj-8                        feasible            optimal |
| triptim1                               feasible            optimal |
| unitcal_7                              feasible            optimal |
----------------------------------------------------------------------

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

@nguidotti nguidotti added this to the 26.08 milestone Jun 26, 2026
@nguidotti nguidotti added non-breaking Introduces a non-breaking change improvement Improves an existing functionality mip labels Jun 26, 2026
@copy-pr-bot

copy-pr-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
nguidotti added 23 commits June 29, 2026 12:56
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…bmip (and restarted) problems. Papilo can now be applied to an user_problem_t inplace.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
# Conflicts:
#	cpp/src/mip_heuristics/diversity/lns/rins.cu
#	cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…e old warm start code.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…/root change diving procedure when the RINS neighbourhood is not large enough

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…ation count for the root relaxation (it should reflect the number of simplex iterations).

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…. this enable the use of diving heuristics for RINS.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…f the neighbourhood is too loose or it already found an improving solution.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
@nguidotti nguidotti requested review from aliceb-nv, chris-maes and rg20 and removed request for Bubullzz and Kh4ster July 6, 2026 08:56

@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.

🧹 Nitpick comments (1)
cpp/src/mip_heuristics/presolve/third_party_presolve.cpp (1)

1300-1307: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

get_coeff linear scan makes reduced-cost replay quadratic per row.

When the (row, col) key is absent from coeff_current (the common case), get_coeff scans the entire row in A. Callers iterate p over that same row and call get_coeff(row, A_indices[p]) (Lines 1354-1357 and 1441-1443), so each row costs O(row_nnz²). Since A_values[p] is already the coefficient for A_indices[p], only map-overridden entries need a lookup — pass the known value and consult the map only for overrides to keep the replay linear in nnz.

♻️ Illustrative change at the call sites
-      for (i_t p = A_offsets[i]; p < A_offsets[i + 1]; ++p) {
-        z[A_indices[p]] += y[i] * get_coeff(i, A_indices[p]);
-      }
+      for (i_t p = A_offsets[i]; p < A_offsets[i + 1]; ++p) {
+        const i_t col = A_indices[p];
+        auto it       = coeff_current.find(coeff_key(i, col));
+        const f_t a   = (it != coeff_current.end()) ? it->second : A_values[p];
+        z[col] += y[i] * a;
+      }
🤖 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/third_party_presolve.cpp` around lines 1300 -
1307, The get_coeff helper currently does a linear scan of A for every miss in
coeff_current, which makes the reduced-cost replay quadratic when called inside
the row iteration loops. Update the logic around get_coeff and its call sites in
the replay path so the caller passes the already-known A_values[p] for
A_indices[p], and only consult coeff_current for overridden entries instead of
rescanning the row. Keep the fix localized to get_coeff and the loops that
invoke it so the replay stays linear in nnz.
🤖 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.

Nitpick comments:
In `@cpp/src/mip_heuristics/presolve/third_party_presolve.cpp`:
- Around line 1300-1307: The get_coeff helper currently does a linear scan of A
for every miss in coeff_current, which makes the reduced-cost replay quadratic
when called inside the row iteration loops. Update the logic around get_coeff
and its call sites in the replay path so the caller passes the already-known
A_values[p] for A_indices[p], and only consult coeff_current for overridden
entries instead of rescanning the row. Keep the fix localized to get_coeff and
the loops that invoke it so the replay stays linear in nnz.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: abae84ad-32f0-4d30-9d20-25a91ec0603b

📥 Commits

Reviewing files that changed from the base of the PR and between 869cc34 and 0a9f7ca.

📒 Files selected for processing (8)
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/branch_and_bound/constants.hpp
  • cpp/src/branch_and_bound/pseudo_costs.hpp
  • cpp/src/branch_and_bound/worker.hpp
  • cpp/src/mip_heuristics/presolve/third_party_presolve.cpp
  • cpp/tests/dual_simplex/unit_tests/solve.cpp
  • cpp/tests/linear_programming/unit_tests/presolve_test.cu
  • cpp/tests/mip/presolve_test.cu
🚧 Files skipped from review as they are similar to previous changes (7)
  • cpp/tests/dual_simplex/unit_tests/solve.cpp
  • cpp/src/branch_and_bound/constants.hpp
  • cpp/tests/mip/presolve_test.cu
  • cpp/src/branch_and_bound/pseudo_costs.hpp
  • cpp/tests/linear_programming/unit_tests/presolve_test.cu
  • cpp/src/branch_and_bound/worker.hpp
  • cpp/src/branch_and_bound/branch_and_bound.cpp

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
@nguidotti nguidotti changed the title Recursive subMIP Recursive RINS Jul 6, 2026
nguidotti added 8 commits July 8, 2026 13:37
…ilo to my fork with the fix, revert when the PR is merged)

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…han the main solve)

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…reads for ex9 test

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…let the degeneracy test to handle optimal as well

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>

@aliceb-nv aliceb-nv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks a lot for the hard work and amazing results Nicolas! Approving with a few nits; let's also get Chris' eyes on the dual simplex/B&B parts

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp Outdated
Comment on lines 1672 to 1698
@@ -1645,6 +1698,12 @@ void branch_and_bound_t<i_t, f_t>::plunge_with(bfs_worker_t<i_t, f_t>* worker,
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's check with Chris that these logging changes are agreed upon

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp Outdated
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp Outdated
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp Outdated
Comment on lines +707 to +709
// Free row (both sides infinite).
problem.row_sense[r] = 'L';
problem.rhs[r] = std::numeric_limits<f_t>::infinity();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we handle free rows properly in simplex?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is a good question. Let me check, although @chris-maes or @rg20 may answer better than me.

Comment thread cpp/tests/dual_simplex/unit_tests/solve.cpp Outdated
Comment thread cpp/tests/mip/presolve_test.cu Outdated
Comment thread cpp/CMakeLists.txt
Comment on lines 253 to +262
FetchContent_Declare(
papilo
GIT_REPOSITORY "https://github.com/scipopt/papilo.git"
GIT_REPOSITORY "https://github.com/nguidotti/papilo.git"
# We would want to get the main branch. However, the main branch
# does not have some of the presolvers and settings that we need
# Mainly, probing and clique merging.
# This is the reason we are using the development branch
# from Oct 12, 2025. Once these changes are merged into the main branch,
#we can switch to the main branch.
GIT_TAG "741a2b9c8155b249d6df574d758b4d97d4417520"
GIT_TAG "0cfea20c5655249174dbd04f0fb7bd6b1f6e9a0c"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we pin on your repo? Is it because of a required upstream fix? If so, we probably should wait until it is merged before merging the PR instead of depending on a personal repo (or at least, track that with an issue)

@nguidotti nguidotti Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is only temporary. I will wait for scipopt/papilo#89 to merged before merging this PR

Comment on lines +748 to +749
const f_t t1 = b - c * simplex_problem.lower[s];
const f_t t2 = b - c * simplex_problem.upper[s];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The following was flagged by an analysis agent, do you believe it's legit?

At presolve.cpp:748-754, for a row with b = +inf and a standard [0, inf) slack:

  • t1 = b − c·l_s = inf − 0 = inf
  • t2 = b − c·u_s = inf − inf = NaN
  • std::min(t1, t2) and std::max(t1, t2) both discard the NaN operand and return inf, so lo == hi == +inf
  • the lo == hi branch fires → row recovered as sense 'E' with rhs = +inf — a vacuous constraint has become an unsatisfiable one.

The same collapse happens NaN-free if the row carries an artificial with bounds [0,0] (t1 = t2 = inf). Downstream, build_papilo_problem sets lhs = rhs = +inf with only the rhs infinity flag (third_party_presolve.cpp:304-315), so Papilo sees a^T x >= +inf and declares the sub-MIP INFEASIBLE. solve_submip records it via save_infeasible and moves on — every RINS/sub-MIP on such an instance silently reports infeasible, disabling the whole heuristic with no assert firing even in debug builds.

@nguidotti nguidotti Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I do not think b can be inf here since $Ax = b$, which means that a row of the matrix has infinity coefficient or one entry in the solution vector x is infinite (this will be a weird problem hahaha). @chris-maes is this correct?

nguidotti added 4 commits July 9, 2026 15:00
…her review comments.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
… solution

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds Sub-MIP/RINS branch-and-bound support, PaPILO-backed presolve crush/uncrush paths, an inverse simplex-to-user-problem conversion, simplex-iteration accounting updates, and related test and dataset changes.

Changes

Sub-MIP/RINS and Presolve Crush Support

Layer / File(s) Summary
Settings, status, and worker contracts
cpp/src/branch_and_bound/constants.hpp, cpp/src/branch_and_bound/deterministic_workers.hpp, cpp/src/branch_and_bound/worker.hpp, cpp/src/branch_and_bound/diving_heuristics.hpp, cpp/src/branch_and_bound/pseudo_costs.{hpp,cpp}, cpp/src/cuts/cuts.cpp, cpp/src/mip_heuristics/diversity/lns/rins.cu, cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh, cpp/src/dual_simplex/simplex_solver_settings.hpp
Adds mip_status_t, SUBMIP, submip_settings_t, submip_stats_t, renames sub_mip to inside_submip and total_lp_iters to total_simplex_iters, adds warm-start pseudo-cost initialization, and updates diving-worker selection/state.
Presolver wrapper and inverse conversion
cpp/src/dual_simplex/presolve.{cpp,hpp}, cpp/src/branch_and_bound/CMakeLists.txt, cpp/CMakeLists.txt, cpp/tests/dual_simplex/unit_tests/solve.cpp
Adds convert_simplex_problem to rebuild a range-form user_problem_t from simplex standard form, updates CMake wiring/formatting, and adds a round-trip conversion test.
PaPILO-backed crush/uncrush
cpp/src/mip_heuristics/presolve/third_party_presolve.{cpp,hpp}, cpp/tests/mip/presolve_test.cu
Adds build_papilo_problem/build_user_problem overloads, a sub-MIP apply(...) presolve path, crush_primal_solution/crush_primal_dual_solution, and a presolve test asserting full reduction.
Branch-and-bound Sub-MIP/RINS pipeline
cpp/src/branch_and_bound/branch_and_bound.{cpp,hpp}
Adds launch_submip_worker, solve_submip, rins, set_solution_from_submip, warm_start, apply_objective_step, external upper-bound callback support, and switches accounting to total_simplex_iters.
Diversity manager, early incumbents, and dataset updates
cpp/src/mip_heuristics/diversity/diversity_manager.cu, datasets/mip/download_miplib_test_dataset.sh
Crushes user-given initial solutions via PaPILO, reorders initial-solution insertion, updates Sub-MIP flags, and extends the MIPLIB instance list.

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

Suggested reviewers: kaatish, hlinsen, Iroy30, KyleFromNVIDIA

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: recursive RINS heuristics and sub-MIP handling.
Description check ✅ Passed The description accurately describes the PR’s RINS sub-MIP, Papilo presolve, upper-bound callback, and worker scheduling changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…tion.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…re calling cpu fj

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants