Skip to content

Recursive RINS#1482

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

Recursive RINS#1482
nguidotti wants to merge 54 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. 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 |
----------------------------------------------------------------------

@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>
policy.graphviz(search_tree, node_ptr, "lower bound", leaf_obj);
policy.update_pseudo_costs(node_ptr, leaf_obj);
node_ptr->lower_bound = leaf_obj;
// If the objective is integral or must move in steps than

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.

We should get in the habit of keeping refactors like this separate from functional changing PR. It's easier to review PRs that keep the two separate. Refactors should have no functionality changes.

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 needed to move this logic to a separated function to avoid code duplication: it is applied in both the tree_update() and rins()

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
bool stop = halt_callback_(user_obj, user_lower);
if (stop) {
node_concurrent_halt_ = 1;
solver_status_ = mip_status_t::HALT;

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.

When is HALT triggerred?

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.

Add a comment above

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
break;
}

dual_status_t lp_status = solve_node_lp(&node, rins_worker, rins_stats, log);

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.

Is this solving the root node of the sub-MIP? If so a comment would be helpful to guide the reader.

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
scope_guard cpufj_guard([&]() { root_fj_cpu_worker.stop(); });
root_fj_cpu_worker.improvement_callback =
[this](f_t obj, const std::vector<f_t>& assignment, double work_units) {
std::vector<f_t> user_assignment;

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.

Is this just a refactor? Or are there functional changes here?

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.

Just refactor. We are using the CPU FJ in a few places now, so I move all the logic to a single method set_solution_from_cpu_fj

Comment thread cpp/src/branch_and_bound/branch_and_bound.hpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.hpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.hpp
// original-space in the mip_solver_context_t), but does NOT imply incumbent_.has_incumbent.
omp_atomic_t<f_t> upper_bound_;

// Callback for halting the solver. This passes the current upper and lower bound of the solver

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.

Under what conditions is the solver halted?

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.

Add a description

std::atomic<int> root_concurrent_halt_{0};
std::atomic<int> node_concurrent_halt_{0};
bool is_root_solution_set{false};
bool root_warm_start_{false};

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.

We should be careful about the use of the word "warm start" in MIP. What is meant here? That an initial integer feasible solution is available? If so, we usually call that a "MIP start".

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.

renamed the variable

Comment thread cpp/src/branch_and_bound/branch_and_bound.hpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.hpp
// Feb. 07, 2019, _arXiv_: arXiv:1902.02615. doi:
// [10.48550/arXiv.1902.02615](https://doi.org/10.48550/arXiv.1902.02615).
enum search_strategy_t : int {
enum worker_type_t : int {

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 the name change?

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.

Since I added the HEURISTICS and SUBMIP, search_strategy_t seems a little bit off

Comment thread cpp/src/branch_and_bound/constants.hpp
// minimize c^T x
// subject to A x = b
// l <= x <= u
// and express it as a user_problem in range (row-bounded) form

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.

Is it necessary to convert the problem into ranged form? Can't you give Papilo the problem above. That is, Papilo should not require ranges. You can just set the l_row = u_row = b.


// LP/MIP only: no quadratic objective and no second-order cones.
user_problem.cone_var_start = 0;
user_problem.second_order_cone_dims.clear();

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.

Don't you need to set the quadratic objective terms to zero?


namespace cuopt::mathematical_optimization::simplex {

struct submip_settings_t {

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.

Are all these settings necessary? Which ones are you considering exposing to the user?

Comment thread cpp/src/dual_simplex/simplex_solver_settings.hpp
// or -1 to draw from the global cuopt::seed_generator (the historical behavior).
// In deterministic mode the caller MUST pass an explicit seed, otherwise the underlying
// seed_generator::get_seed() racing with concurrent callers breaks reproducibility.
void from_simplex_lp(const simplex::lp_problem_t<i_t, f_t>& problem,

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.

It's a bit strange to construct a fj_cpu_worker_t from_simplex_lp. Maybe this function could have a better name.

const std::vector<char>& row_sense = problem.row_sense;
const std::vector<f_t>& rhs = problem.rhs;

// Range rows carry an extra width and are listed separately; mark them so the row-bound

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.

My hunch is that this is not necessary. Just pass the linear_program_t representation directly to Papilo.

case papilo::PostsolveStatus::kOk: CUOPT_LOG_DEBUG("Post-solve status: succeeded"); break;
case papilo::PostsolveStatus::kFailed:
CUOPT_LOG_INFO(
CUOPT_LOG_DEBUG(

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.

Ideally logging changes like this are done as a separate PR. Not mixed in with large functional changes.

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 silence this log line since the sub-MIP was constantly triggering the kFailed line, which polluted the logs. Let me re-enable and see if this is still the case after the latest changes.

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

One of the biggest concerns is the switching between Ax = b, l <= x <= u, and lb <= Ax <= ub. My hunch is that this is unnecessary and just adds overhead. Could you try giving Papilo the form b <= A*x <= b, l <= x <= u and see if it accepts that. If so, you can eliminate a lot of complexity from the code.

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.

[FEA] Recursive RINS

5 participants