Skip to content

Zero Half(Odd-Cycle) cuts#1428

Open
akifcorduk wants to merge 58 commits into
NVIDIA:mainfrom
akifcorduk:zero_half
Open

Zero Half(Odd-Cycle) cuts#1428
akifcorduk wants to merge 58 commits into
NVIDIA:mainfrom
akifcorduk:zero_half

Conversation

@akifcorduk

@akifcorduk akifcorduk commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This branch adds zero-half (odd-cycle / odd-wheel) cuts to the set of cuts. The cut generator runs on the same fractional conflict-graph subgraph that clique cuts use: once per cut pass, prepare_fractional_sub_cg() builds the subgraph then Dijkstra finds violated odd cycles on the auxillarry conflict graph (edge weights summing below 0.5), optionally extends them to odd-wheels by adding fully conflicting variables(similar logic to clique extension). Clique cut was refactored to share that subgraph and the same greedy extension helpers, and implied-bound cuts were moved earlier in the pass so the background clique-table thread has more time to finish before we join it.

A-> this PR
B-> main

Gap Closed Metrics:

Top 10 A>B (A-B), gap_closed_pct

instance A B A-B
brazil3 54.242 12.879 41.364
neos-957323 50.141 13.489 36.652
roll3000 71.977 61.487 10.491
supportcase7 62.035 51.657 10.379
nursesched-medium-hint03 36.597 33.262 3.336
atlanta-ip 17.689 14.890 2.799
sorrell3 3.704 1.961 1.743
neos-873061 62.916 61.324 1.592
physiciansched6-2 1.673 0.157 1.516
neos-3656078-kumeu 31.977 30.583 1.394

Top 10 B>A (B-A), gap_closed_pct

instance A B B-A
rail01 52.512 62.944 10.432
neos-2746589-doon 49.619 58.182 8.563
50v-10 65.878 72.200 6.323
neos-662469 63.135 66.714 3.580
physiciansched3-3 33.387 36.218 2.831
ns1830653 25.524 28.122 2.598
irp 73.107 75.094 1.987
s100 29.239 30.497 1.258
seymour1 24.850 25.933 1.083
qap10 10.571 11.460 0.889

Average and shifted geomean gap closed

A avg=29.648 sgm=8.1513 (shift=1)
B avg=28.924 sgm=7.7679 (shift=1)

Benchmark Results

A Batch 1
total_optimal: 79, avg_mip_gap: 0.3173, geomean_mip_gap: 0.1947, n_low_error: 125
A Batch 2
total_optimal: 77, avg_mip_gap: 0.3175, geomean_mip_gap: 0.1972, n_low_error: 124
B Batch 1
total_optimal: 75, avg_mip_gap: 0.3419, geomean_mip_gap: 0.2062, n_low_error: 119
B Batch2
total_optimal: 75, avg_mip_gap: 0.3126, geomean_mip_gap: 0.1930, n_low_error: 118

On average +3 optimal solutions, -0.4% geomean MIP gap reductions.

Brings in upstream's unified OpenMP threading model (PR NVIDIA#1099) and other
fixes (NVIDIA#1206 concurrent LP exception cleanup, NVIDIA#1214/NVIDIA#1216 destruction/
capture fixes) while preserving local work on the cut and clique stack.

Conflict resolution highlights:
- Drop std::future/std::async clique flow; adopt upstream's omp task +
  omp_atomic_t<bool> signal_extend pattern.
- Drop modify_problem parameter from find_initial_cliques (we already
  removed the code that consumed it); adapt the omp-task call site in
  branch_and_bound::solve accordingly.
- Take upstream's [this, &population] capture for the root-LP CPUFJ
  improvement callback; the new omp taskwait-before-destruction guarantee
  makes the prior context-lifetime fix unnecessary.
- Take upstream's do_cut_pass refactor of the per-pass LP resolve loop;
  move our per-pass root_lp_with_cuts publish into do_cut_pass so the
  benchmark metric is still updated on early exits.
- Keep our out-of-line omp_mutex_t definitions in omp_helpers.cpp; the
  enhanced omp_atomic_t with std::memory_order is taken from upstream.
@akifcorduk akifcorduk requested review from a team as code owners June 12, 2026 12:40
@copy-pr-bot

copy-pr-bot Bot commented Jun 12, 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.

@akifcorduk akifcorduk requested a review from nguidotti June 12, 2026 12:40
@akifcorduk akifcorduk changed the title Zero Half ) Zero Half(Odd-Cycle) cuts Jun 12, 2026
@coderabbitai

coderabbitai Bot commented Jun 12, 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: c7e6d527-64d1-4cd5-84fe-31cbc8ff5ea7

📥 Commits

Reviewing files that changed from the base of the PR and between cfb5f4b and 3edceae.

📒 Files selected for processing (2)
  • cpp/src/cuts/cuts.hpp
  • cpp/tests/mip/cuts_test.cu
💤 Files with no reviewable changes (1)
  • cpp/src/cuts/cuts.hpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/tests/mip/cuts_test.cu

📝 Walkthrough

Walkthrough

Adds a configurable zero-half cut family: new solver settings, cut-type enum/name, fractional conflict-subgraph caching, Dijkstra-based odd-cycle extraction and odd-wheel lifting, integration into the cut pass and B&B wiring, and unit + end-to-end tests.

Changes

Zero-half cut generation and integration

Layer / File(s) Summary
Settings plumbing and solver wiring
cpp/include/cuopt/linear_programming/mip/solver_settings.hpp, cpp/src/dual_simplex/simplex_solver_settings.hpp, cpp/src/mip_heuristics/solver.cu
New zero_half_cuts field added to MIP and simplex solver settings (default -1), forwarded into branch-and-bound construction; minor comment removal and required include insertion.
Cut type contracts and fractional subgraph
cpp/src/cuts/cuts.hpp
Inserted ZERO_HALF enum and name, declared find_violated_odd_cycles_for_test, added fractional_conflict_subgraph_t and new cut_generation_t private members (prepare_fractional_sub_cg, generate_zero_half_cuts, sub_cg_).
Shared clique-growth helpers and debug infra
cpp/src/cuts/cuts.cpp
Added logger include and debug macros; refactored clique-anchor selection, reduced-cost ordering, and greedy clique growth under work/time budgets reused by clique and zero-half lifting.
Zero-half separator: odd-cycle extraction and wheel lifting
cpp/src/cuts/cuts.cpp
Implemented bipartite Dijkstra odd-cycle search, path-to-cycle mapping with validations, build_zero_half_cut with coefficient handling and status returns, and odd-wheel lifting using shared clique helpers; added test helper for enumerating violated odd cycles.
Fractional subgraph preparation and cut-pass orchestration
cpp/src/cuts/cuts.cpp
Added prepare_fractional_sub_cg to build/cache fractional CG once per pass; generate_cuts now calls preparation before clique/zero-half separation; clique separator and zero-half generator consume sub_cg_.
Clique detection scheduling and sub-MIP controls
cpp/src/branch_and_bound/branch_and_bound.cpp, cpp/src/mip_heuristics/diversity/lns/rins.cu, cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
Initial clique-detection task triggered when clique or zero-half cuts are enabled; RINS and sub-MIP heuristics explicitly disable zero-half cuts.
Zero-half cut tests and helper updates
cpp/tests/mip/cuts_test.cu
Adds pentagon model builder and canonicalization helper; unit tests for odd-cycle discovery (5-cycle, 7-cycle), non-detection cases (even cycles, integer-feasible, triangles), overlapping/disjoint cases, and an end-to-end MIP tightening test; updates cut-disable helpers to control zero_half_cuts.
Documentation and API cleanup
skills/cuopt-developer/references/contributing.md, cpp/src/utilities/omp_helpers.hpp
Added "Resolving Merge Conflicts" guidance to contributor docs and removed explicit deleted copy-constructor declaration from omp_mutex_t.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Suggested labels

mip

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.98% 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 'Zero Half(Odd-Cycle) cuts' clearly and concisely summarizes the main change: adding zero-half (odd-cycle) cuts to the MIP solver's cut set.
Description check ✅ Passed The description comprehensively explains the zero-half cuts feature, implementation approach, refactoring details, and provides benchmark results demonstrating the improvement.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cpp/src/mip_heuristics/diversity/diversity_manager.cu (1)

523-603: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten the root-relaxation handoff contract.

root_lp_usable is only a buffer-shape check, but it is also being used as the gate for set_root_relaxation_solution_callback(...). That means B&B can still receive payloads this function does not actually treat as a valid root relaxation: PrimalInfeasible/DualInfeasible still satisfy root_lp_usable, and the callback is populated from lp_result.get_primal_solution() before Line 600 clamps PDLP bound violations. The downstream solve_root_relaxation() path consumes that callback as a crossover candidate, so this can hand crossover an infeasibility certificate or an out-of-bounds primal vector instead of a sanitized relaxation iterate.

Use a separate root_lp_handoffable gate for the callback, clamp/sanitize before marshaling host_primal, and only hand off statuses that the crossover path is documented to accept. citeturn0search0

🤖 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/diversity/diversity_manager.cu` around lines 523 -
603, The root-relaxation handoff must be gated by a new root_lp_handoffable flag
and the primal must be sanitized before marshaling: compute root_lp_handoffable
(start from root_lp_usable) and only set it true for termination statuses the
crossover expects (e.g., pdlp_termination_status_t::Optimal, and optionally
pdlp_termination_status_t::TimeLimit only if an LP feasibility check passes);
run clamp_within_var_bounds(lp_optimal_solution, ...) and any
lp_sol.compute_feasibility()/lp_sol.get_feasible() checks before copying/prior
to calling set_root_relaxation_solution_callback; use
lp_result.get_termination_status(),
lp_result.get_primal_solution()/get_dual_solution()/get_reduced_cost(),
clamp_within_var_bounds, and set_root_relaxation_solution_callback to locate
where to implement the flag, the sanitize step, and the tightened conditional.
cpp/src/mip_heuristics/solver.cu (1)

430-455: ⚠️ Potential issue | 🟡 Minor

Update clique-table lifecycle comment (dead publish callback)

In cpp/src/mip_heuristics/solver.cu the lifecycle text claims B&B async-builds a clique_table and publishes it into context.problem_ptr->clique_table via set_clique_publish_callback, but the entire callback block is commented out and there are no real (non-comment) publish_clique_table / ensure_clique_data readers using problem_ptr->clique_table. B&B instead owns/builds clique_table_ internally (find_initial_cliques(..., &clique_table_, ...)) and passes it to cut_generation, so the “publish-to-heuristics” description should be removed or rewritten to match the actual ownership/consumption path.

🤖 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/solver.cu` around lines 430 - 455, The comment about
an async B&B publish of a clique_table is outdated: remove or rewrite the
paragraph describing
set_clique_publish_callback/publish_clique_table/ensure_clique_data since the
callback is commented out and B&B actually owns/builds clique_table_ (via
find_initial_cliques(..., &clique_table_, ...)) and passes it directly into
cut_generation; also delete the commented-out callback block referencing
set_clique_publish_callback to avoid misleading future readers and update the
text to state that branch_and_bound_t maintains clique_table_ internally and
supplies it to cut_generation rather than publishing it to
context.problem_ptr->clique_table.
🧹 Nitpick comments (2)
benchmarks/linear_programming/cuopt/miplib2017_optima.hpp (1)

73-73: ⚡ Quick win

Rename getter functions to follow snake_case convention.

kBenchmarkOptima() and kBenchmarkInfeasible() use kCamelCase naming, but the coding guidelines require snake_case for all functions and methods. Consider renaming to benchmark_optima() and benchmark_infeasible() (or similar) to match project conventions.

♻️ Proposed naming fix
-inline const std::unordered_map<std::string, double>& kBenchmarkOptima()
+inline const std::unordered_map<std::string, double>& benchmark_optima()
 {
   static const std::unordered_map<std::string, double> kOptima = {
-inline const std::unordered_set<std::string>& kBenchmarkInfeasible()
+inline const std::unordered_set<std::string>& benchmark_infeasible()
 {
   static const std::unordered_set<std::string> kInfeas = {

Then update the call sites on lines 333 and 341:

-  const auto& m = kBenchmarkOptima();
+  const auto& m = benchmark_optima();
-  return kBenchmarkInfeasible().count(normalize_instance_name(filename)) != 0;
+  return benchmark_infeasible().count(normalize_instance_name(filename)) != 0;

Also applies to: 317-317

🤖 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 `@benchmarks/linear_programming/cuopt/miplib2017_optima.hpp` at line 73, The
function names kBenchmarkOptima() and kBenchmarkInfeasible() violate the
project's snake_case convention; rename these functions to benchmark_optima()
and benchmark_infeasible() (and update their declarations and definitions
accordingly) and update all call sites that reference kBenchmarkOptima and
kBenchmarkInfeasible to the new names; ensure the return types and const/ref
qualifiers remain unchanged and run a build to catch any remaining references.

Source: Coding guidelines

cpp/include/cuopt/linear_programming/mip/solver_settings.hpp (1)

132-132: ⚡ Quick win

Document the new zero_half_cuts parameter.

The new parameter lacks a comment explaining its meaning, valid values, and behavioral implications. While this is consistent with other cut-setting members (lines 127–131, 133–134), documenting it would improve usability. From the internal contract (cpp/src/dual_simplex/simplex_solver_settings.hpp:83–184), the semantics are: -1 = automatic, 0 = disabled, >0 = enabled. Additionally, enabling zero-half cuts (!= 0) triggers async clique-table preparation when thread count is sufficient (cpp/src/branch_and_bound/branch_and_bound.cpp:2451–2472), similar to clique_cuts.

Consider adding a brief inline comment or a Doxygen block above the cut-settings group documenting the shared -1/0/>0 convention and any async-preparation behavior users should be aware of.

🤖 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/include/cuopt/linear_programming/mip/solver_settings.hpp` at line 132,
Add a Doxygen-style comment for the cut-settings group (or immediately above the
member) explaining the semantics of zero_half_cuts: zero_half_cuts uses the same
convention as other cut settings where -1 = automatic, 0 = disabled, >0 =
enabled (positive values control aggressiveness/limit), and note that enabling
zero-half cuts (value != 0) triggers asynchronous clique-table preparation when
thread counts are sufficient (same behavior as clique_cuts); reference the
member name zero_half_cuts and the related clique_cuts setting in the comment so
maintainers can locate the linked behavior.
🤖 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 2654-2663: The lambda publish_cut_generation_time is defined but
never invoked, so clamping and the force-time-limit logic are skipped; replace
the direct assignments that set
settings_.benchmark_info_ptr->cut_generation_time_sec using raw
toc(cut_generation_start_time) (including occurrences around the nearby blocks
you noted) with calls to publish_cut_generation_time(), and where the logic
should honor the time_limit use publish_cut_generation_time(true). Ensure calls
occur after computing cut_generation_start_time and before any early returns so
the lambda (which uses toc, settings_.benchmark_info_ptr, and
settings_.time_limit) performs the clamping and publishes the final double
value.

In `@cpp/src/cuts/cuts.cpp`:
- Around line 1077-1119: The code currently uses the already_used vector in the
main source loop (for (int s = 0; s < num_local; ++s)) and marks vertices as
used after pushing a cycle (setting already_used[v] = 1), which causes later
sources to be skipped if they share vertices with an earlier found cycle; to fix
this, remove the already_used gating (the if (already_used[s]) continue;) and
the marking loop that sets already_used[v] = 1 so that dijkstra_odd_cycle and
the helper will return all violated odd cycles including overlapping ones; if
the disjoint-only behavior is intended instead, rename already_used to something
explicit and add documentation/comments around dijkstra_odd_cycle and the result
collection to make that contract clear.
- Around line 3222-3233: The loop is pushing raw xstar[j] and 1-xstar[j] into
sub_cg_ even when xstar is slightly outside [0,1]; clamp these cached literal
weights to [0,1] (or skip/report if they exceed primal_tol) before pushing into
sub_cg_. Concretely, in the block that computes const f_t xj = xstar[j] inside
the for-loop over j (and after the integer_tol check), compute a clamped value
(e.g., xj_clamped = clamp(xj, 0.0, 1.0)) and use that for
sub_cg_.weights.push_back(...) and for 1 - xj_clamped; if xj is outside [0 -
primal_tol, 1 + primal_tol], treat it as an error/skip per project policy
instead of caching the raw value.

In `@cpp/src/cuts/cuts.hpp`:
- Around line 360-363: Update the setter set_clique_cousin_minhash_k(i_t v) to
validate the input and reject non-positive values: if v <= 0, throw a
descriptive std::invalid_argument (or use your project's preferred error type)
indicating "clique_cousin_minhash_k must be > 0"; otherwise assign
clique_cousin_minhash_k_ = v. This enforces the contract at the boundary and
prevents downstream empty-sketch/out-of-bounds issues.

---

Outside diff comments:
In `@cpp/src/mip_heuristics/diversity/diversity_manager.cu`:
- Around line 523-603: The root-relaxation handoff must be gated by a new
root_lp_handoffable flag and the primal must be sanitized before marshaling:
compute root_lp_handoffable (start from root_lp_usable) and only set it true for
termination statuses the crossover expects (e.g.,
pdlp_termination_status_t::Optimal, and optionally
pdlp_termination_status_t::TimeLimit only if an LP feasibility check passes);
run clamp_within_var_bounds(lp_optimal_solution, ...) and any
lp_sol.compute_feasibility()/lp_sol.get_feasible() checks before copying/prior
to calling set_root_relaxation_solution_callback; use
lp_result.get_termination_status(),
lp_result.get_primal_solution()/get_dual_solution()/get_reduced_cost(),
clamp_within_var_bounds, and set_root_relaxation_solution_callback to locate
where to implement the flag, the sanitize step, and the tightened conditional.

In `@cpp/src/mip_heuristics/solver.cu`:
- Around line 430-455: The comment about an async B&B publish of a clique_table
is outdated: remove or rewrite the paragraph describing
set_clique_publish_callback/publish_clique_table/ensure_clique_data since the
callback is commented out and B&B actually owns/builds clique_table_ (via
find_initial_cliques(..., &clique_table_, ...)) and passes it directly into
cut_generation; also delete the commented-out callback block referencing
set_clique_publish_callback to avoid misleading future readers and update the
text to state that branch_and_bound_t maintains clique_table_ internally and
supplies it to cut_generation rather than publishing it to
context.problem_ptr->clique_table.

---

Nitpick comments:
In `@benchmarks/linear_programming/cuopt/miplib2017_optima.hpp`:
- Line 73: The function names kBenchmarkOptima() and kBenchmarkInfeasible()
violate the project's snake_case convention; rename these functions to
benchmark_optima() and benchmark_infeasible() (and update their declarations and
definitions accordingly) and update all call sites that reference
kBenchmarkOptima and kBenchmarkInfeasible to the new names; ensure the return
types and const/ref qualifiers remain unchanged and run a build to catch any
remaining references.

In `@cpp/include/cuopt/linear_programming/mip/solver_settings.hpp`:
- Line 132: Add a Doxygen-style comment for the cut-settings group (or
immediately above the member) explaining the semantics of zero_half_cuts:
zero_half_cuts uses the same convention as other cut settings where -1 =
automatic, 0 = disabled, >0 = enabled (positive values control
aggressiveness/limit), and note that enabling zero-half cuts (value != 0)
triggers asynchronous clique-table preparation when thread counts are sufficient
(same behavior as clique_cuts); reference the member name zero_half_cuts and the
related clique_cuts setting in the comment so maintainers can locate the linked
behavior.
🪄 Autofix (Beta)

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: aaaebcac-3cc5-4505-8de2-edaa79f781e5

📥 Commits

Reviewing files that changed from the base of the PR and between 1138845 and 44a0cfa.

📒 Files selected for processing (14)
  • benchmarks/linear_programming/cuopt/miplib2017_optima.hpp
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/cuts/cuts.cpp
  • cpp/src/cuts/cuts.hpp
  • cpp/src/dual_simplex/simplex_solver_settings.hpp
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/mip_heuristics/diversity/lns/rins.cu
  • cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
  • cpp/src/mip_heuristics/solver.cu
  • cpp/src/utilities/manual_cuda_graph.cuh
  • cpp/src/utilities/omp_helpers.hpp
  • cpp/tests/mip/cuts_test.cu
  • skills/cuopt-developer/references/contributing.md
💤 Files with no reviewable changes (1)
  • cpp/src/utilities/omp_helpers.hpp

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp Outdated
Comment thread cpp/src/cuts/cuts.cpp Outdated
Comment thread cpp/src/cuts/cuts.cpp
Comment thread cpp/src/cuts/cuts.hpp Outdated
@akifcorduk akifcorduk added improvement Improves an existing functionality non-breaking Introduces a non-breaking change labels Jun 12, 2026
@akifcorduk akifcorduk added this to the 26.08 milestone Jun 12, 2026
@akifcorduk

Copy link
Copy Markdown
Contributor Author

/ok to test cfb5f4b

@akifcorduk

Copy link
Copy Markdown
Contributor Author

/ok to test 3edceae

@akifcorduk akifcorduk requested review from chris-maes and removed request for mlubin June 12, 2026 14:46
@rgsl888prabhu

Copy link
Copy Markdown
Collaborator

/nvskills-ci

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.

2 participants