Skip to content

Root heuristics - #1700

Open
nguidotti wants to merge 13 commits into
mainfrom
root-heuristics
Open

Root heuristics#1700
nguidotti wants to merge 13 commits into
mainfrom
root-heuristics

Conversation

@nguidotti

Copy link
Copy Markdown
Contributor
  • Refactored the root heuristics (more specifically, the CPU FJ launches at the root node) to be more modular, while allowing to use more workers.
  • Extended recursive RINS to run on the root node
  • Fixed incorrect objective passed to the set_solution_from_submip (the objective was on the submip space, which is not comparable with the solution space of the B&B).

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

…rom submip.

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>
@nguidotti nguidotti added this to the 26.10 milestone Aug 11, 2026
@nguidotti nguidotti self-assigned this Aug 11, 2026
@nguidotti
nguidotti requested a review from a team as a code owner August 11, 2026 13:46
@nguidotti nguidotti added the non-breaking Introduces a non-breaking change label Aug 11, 2026
@nguidotti nguidotti added improvement Improves an existing functionality mip labels Aug 11, 2026
@nguidotti
nguidotti requested a review from aliceb-nv August 11, 2026 13:46
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

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

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: 4d2012fb-d823-4053-a3ab-11a646eef838

📥 Commits

Reviewing files that changed from the base of the PR and between c1b78cc and 71aaf13.

📒 Files selected for processing (5)
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/branch_and_bound/constants.hpp
  • cpp/src/dual_simplex/solve.cpp
  • cpp/src/dual_simplex/solve.hpp
  • cpp/src/mip_heuristics/root_heuristics.hpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/mip_heuristics/root_heuristics.hpp

📝 Walkthrough

Walkthrough

The PR adds DFS backtracking control, objective conversion, optional postsolve validation, and halt tracking. It refactors RINS and sub-MIP execution around prepared workers and adds persistent root-heuristic orchestration across root cut passes.

Changes

Root heuristics and sub-MIP execution

Layer / File(s) Summary
Execution contracts and objective conversion
cpp/include/cuopt/..., cpp/src/branch_and_bound/branch_and_bound.hpp, cpp/src/branch_and_bound/constants.hpp, cpp/src/dual_simplex/..., cpp/src/mip_heuristics/presolve/...
Interfaces now carry LP, variable-type, root-heuristic, and postsolve context. Solver-objective conversion, search-strategy naming, and the dfs_max_backtrack parameter are added.
RINS and sub-MIP processing
cpp/src/branch_and_bound/branch_and_bound.cpp
RINS and sub-MIP execution use prepared worker state, incumbent snapshots, LP-aware solution conversion, leaf objectives, root-heuristic behavior, and halt state.
Persistent root worker lifecycle
cpp/src/mip_heuristics/root_heuristics.hpp, cpp/src/mip_heuristics/feasibility_jump/*, cpp/src/branch_and_bound/worker.hpp, cpp/src/branch_and_bound/branch_and_bound.cpp
Persistent CPU-FJ and diving workers are created, counted, stopped, and relaunched around root cut passes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • NVIDIA/cuopt#1699: Directly related changes cover the same sub-MIP, RINS, root-heuristic, objective-conversion, worker, and presolve code paths.

Suggested reviewers: chris-maes, akifcorduk, aliceb-nv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 identifies the main change: refactoring and extending root heuristics.
Description check ✅ Passed The description accurately covers root CPU FJ refactoring, recursive RINS support, and the objective correction.
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 root-heuristics

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: 2

🧹 Nitpick comments (3)
cpp/src/branch_and_bound/branch_and_bound.cpp (1)

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

Document the declaration-order dependency between root_worker_count and root_heuristics.

The root heuristic tasks decrement *worker_count when they finish. root_heuristics is destroyed before root_worker_count because it is declared later, and its element destructors call stop(), which blocks on taskwait until those tasks complete. The ordering is correct as written.

If a future change moves root_heuristics above root_worker_count, the counter is destroyed while tasks can still decrement it, which is a use-after-free across threads. Add a short comment so the ordering is not reversed by accident.

♻️ Proposed comment
+  // Declaration order matters: `root_heuristics` must outlive nothing and be destroyed
+  // FIRST, because its destructors taskwait for the workers that decrement
+  // `root_worker_count`. Do not move `root_worker_count` below `root_heuristics`.
   omp_atomic_t<i_t> root_worker_count = 0;
   std::list<root_heuristics_t<i_t, f_t>> root_heuristics;
   launch_root_heuristics(original_lp_, root_relax_soln_.x, root_heuristics, &root_worker_count);
🤖 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/branch_and_bound/branch_and_bound.cpp` around lines 3549 - 3551, Add
a concise lifetime-ordering comment immediately before the declarations of
root_worker_count and root_heuristics, stating that root_worker_count must be
declared first because root_heuristics destruction waits for heuristic tasks
that decrement the counter. Preserve the current declaration order.
cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu (1)

1817-1829: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the worker-count decrement exception safe.

The counter is incremented before the task is created and decremented at the end of the task body. If cpufj_solve exits by an exception, the decrement is skipped and worker_count stays high for the rest of the solve. launch_root_heuristics then refuses to start new root heuristics once the leaked count reaches settings_.num_threads - 1, so root heuristics stop permanently for that run.

The same pattern exists in the RINS task in cpp/src/branch_and_bound/branch_and_bound.cpp at lines 2846-2852, and that path is more likely to throw: rins calls solve_submip, which runs a nested branch_and_bound_t::solve() containing cuopt_expects checks.

Use a small RAII guard so the decrement always runs.

♻️ Proposed RAII guard
 void fj_cpu_worker_t<i_t, f_t>::run_async(f_t time_limit,
                                           double work_unit_limit,
                                           omp_atomic_t<i_t>* worker_count)
 {
   if (!fj_cpu) return;
 
   if (worker_count) ++(*worker_count);
 `#pragma` omp task shared(fj_cpu) firstprivate(time_limit, work_unit_limit, worker_count) \
   priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : *fj_cpu)
   {
-    cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit);
-    if (worker_count) --(*worker_count);
+    struct count_guard_t {
+      omp_atomic_t<i_t>* c;
+      ~count_guard_t() { if (c) --(*c); }
+    } guard{worker_count};
+    cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit);
   }
 }
🤖 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/feasibility_jump/fj_cpu.cu` around lines 1817 - 1829,
Make the worker-count decrement exception-safe in fj_cpu_worker_t::run_async and
the analogous RINS task in branch_and_bound_t. Add a small RAII guard
immediately after incrementing the counter, capturing worker_count and
decrementing it on scope exit, then remove the manual task-body decrement while
preserving the null-pointer check.
cpp/src/mip_heuristics/root_heuristics.hpp (1)

29-37: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Signal both workers before waiting, and document the sibling-task requirement.

Two points about stop():

  1. fj_cpu_worker_.stop() blocks on its own taskwait before submip_worker_->halt is set. The RINS task therefore keeps running for the full duration of the CPU FJ join. Set both stop signals first, then wait. This shortens teardown without changing behavior.

  2. #pragma omp taskwait depend(in : *worker) only joins the RINS task if that task is a sibling, because OpenMP task dependences apply within one task region. The matching #pragma omp task ... depend(out : *worker) is created in branch_and_bound_t::launch_root_heuristics. Both call sites currently run directly in solve(), so the relationship holds. If a root_heuristics_t is ever destroyed from inside another task, this taskwait does not join the RINS task and submip_worker_ is freed while the task still uses it. State the requirement in a comment so the invariant is visible to callers.

♻️ Proposed reordering and comment
+  // Requirement: destroy this object from the same OpenMP task region that called
+  // `branch_and_bound_t::launch_root_heuristics`. The `taskwait depend(...)` below only
+  // joins the RINS task when the two are sibling tasks.
   void stop()
   {
-    fj_cpu_worker_.stop();
+    // Signal both workers first so they wind down in parallel, then join.
+    if (submip_worker_) { submip_worker_->halt = true; }
+    fj_cpu_worker_.stop();
     if (submip_worker_) {
-      submip_worker_->halt              = true;
       diving_worker_t<i_t, f_t>* worker = submip_worker_.get();
 `#pragma` omp taskwait depend(in : *worker)
     }
   }
🤖 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/root_heuristics.hpp` around lines 29 - 37, Update
root_heuristics_t::stop() to signal submip_worker_->halt before calling
fj_cpu_worker_.stop(), then retain the existing taskwait. Add a comment
documenting that the taskwait joins the RINS task only when stop() is called
from the same task region as the sibling task launched by
branch_and_bound_t::launch_root_heuristics, so callers must preserve that
requirement.
🤖 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/branch_and_bound/branch_and_bound.cpp`:
- Around line 2759-2765: Replace the mutable var_types_ member with the captured
var_types snapshot in submip_fj_cpu_worker.create_worker at
cpp/src/branch_and_bound/branch_and_bound.cpp:2759-2765. Update solve_submip at
cpp/src/branch_and_bound/branch_and_bound.cpp:2286-2290 to accept a const
variable-type vector reference, pass rins’s var_types snapshot at its call site
around line 2779, and use that parameter in convert_lp_to_user_problem.
- Around line 2828-2829: Update the root heuristic lifecycle around
root_heuristics_t and the RINS/CPU FJ task completion paths to track completion
independently for both tasks, rather than relying on is_active. Remove each
heuristic entry only after both RINS and CPU FJ have finished, including
asynchronous CPU FJ cases where fj_cpu remains non-null, while preserving access
needed by still-running tasks.

---

Nitpick comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 3549-3551: Add a concise lifetime-ordering comment immediately
before the declarations of root_worker_count and root_heuristics, stating that
root_worker_count must be declared first because root_heuristics destruction
waits for heuristic tasks that decrement the counter. Preserve the current
declaration order.

In `@cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu`:
- Around line 1817-1829: Make the worker-count decrement exception-safe in
fj_cpu_worker_t::run_async and the analogous RINS task in branch_and_bound_t.
Add a small RAII guard immediately after incrementing the counter, capturing
worker_count and decrementing it on scope exit, then remove the manual task-body
decrement while preserving the null-pointer check.

In `@cpp/src/mip_heuristics/root_heuristics.hpp`:
- Around line 29-37: Update root_heuristics_t::stop() to signal
submip_worker_->halt before calling fj_cpu_worker_.stop(), then retain the
existing taskwait. Add a comment documenting that the taskwait joins the RINS
task only when stop() is called from the same task region as the sibling task
launched by branch_and_bound_t::launch_root_heuristics, so callers must preserve
that requirement.
🪄 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: c81605c6-069a-4e91-88a2-143578b6b897

📥 Commits

Reviewing files that changed from the base of the PR and between f0ecadc and 3d2f04f.

📒 Files selected for processing (12)
  • cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/branch_and_bound/branch_and_bound.hpp
  • cpp/src/branch_and_bound/worker.hpp
  • cpp/src/dual_simplex/solve.cpp
  • cpp/src/dual_simplex/solve.hpp
  • cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu
  • cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu
  • cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh
  • cpp/src/mip_heuristics/presolve/third_party_presolve.cpp
  • cpp/src/mip_heuristics/presolve/third_party_presolve.hpp
  • cpp/src/mip_heuristics/root_heuristics.hpp

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp Outdated
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

CI Test Summary

✅ All 31 test job(s) passed.

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.

LGTM, minor nits, thanks Nicolas! As always, let's get Chris' eyes on this before the merge

Comment on lines +2778 to +2780
#ifdef DEBUG_SUBMIP
settings_.log.print_format("{} Running a quick DFS for the submip!", log_prefix);
#endif

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.

Any reason why we're not using debug_format here? Reducing #ifdef clutter is good for readability IMO

@nguidotti nguidotti Aug 12, 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.

This is mostly to enable/disable the logs from sub-MIP quickly. With log set to debug, things can get pretty chaotic hahah
(maybe we could have a way to enable/disable the debug logs from each part of the solver separately)

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.

Maybe not for this PR, but perhaps we could later add a scope/type argument to the debug logs to allow for more fine-grained filtering :)

@nguidotti nguidotti Aug 12, 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.

Wrapped the submip logs calls with a macro

Comment on lines +2254 to +2258
#ifdef DEBUG_SUBMIP
submip_settings.log.log = true;
#else
submip_settings.log.log = false;
#endif

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.

Seems like leftover debug code right? If possible let's ensure such code is out of major function bodies

@nguidotti nguidotti Aug 12, 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.

Same as above. Without the DEBUG_SUBMIP, the sub-MIP logs should be silent.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…t heuristics before B&B tree exploration.

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

@akifcorduk akifcorduk 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 Nicolas, just a few nitpicks.

case search_strategy_t::FARKAS_DIVING: return 'F';
case search_strategy_t::VECTOR_LENGTH_DIVING: return 'V';
default: return 'U';
case search_strategy_t::RINS: return '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.

Why S and not R?

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 used S for SUBMIP, but I guess R is also valid

} else {
#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) firstprivate(worker, sol)
rins(worker, sol);
#pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \

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.

Shouldn't we check if there are enough threads available at this point?

@nguidotti nguidotti Aug 13, 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.

This is controlled by the submip worker pool. If no worker is available, then we exit early (see the if some lines prior). Although we may need a better way to control the total number of threads, each level of the sub-MIP recursion can use more than one thread


// If we already exhausted all threads for the root heuristics, stop workers for the
// oldest set of heuristics launched. Leave 2 threads for the cut passes and the clique
// table generation. Add the number of workers that will be launched (1 submip worker +

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.

Clique table generation only happens in the first cut pass. So later it is freed. It complicates the logic, but in case you want to use that thread too :)

mutex_upper_.unlock();

if (settings_.inside_submip) {
// LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy

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 comment still necessary?

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.

Iirc this is because LLVM has an upstream bug where they don't honor the firstprivate copy constructor in their GCC OpenMP compat layer. It caused issues a few weeks back because some wheel builds had to move to LLVM's openmp lib due to the GCC openmp in their environment being outdated

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.

Should we switching to LLVM's openmp lib completely? Or even use clang?

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.

Probably a P2 task for much later :) Although it may be worth benchmarking GOMP vs llvmomp

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