Skip to content

Zero Half(Odd-Cycle) cuts#1428

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

Zero Half(Odd-Cycle) cuts#1428
akifcorduk wants to merge 62 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. Overall 30 instances have zero-half cut in the benchmark set.

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

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
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/nvskills-ci

@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 Akif! Let's get Chris' eyes on this as well

One thought - maybe we could start thinking about splitting cuts.cpp into a file per cut family or something. It is becoming quite heavy :)

Comment thread cpp/src/cuts/cuts.cpp Outdated
Comment thread cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
Comment thread cpp/src/cuts/cuts.cpp Outdated
Comment on lines +3189 to +3192
if (clique_table_ == nullptr) {
if (signal_extend_) { signal_extend_->store(true, std::memory_order_release); }
#pragma omp taskwait depend(in : *signal_extend_)
}

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.

Isn't there a risk of race here? In "find_initial_cliques", clique_table_out is set to non-null before extend_cliques runs, so you could encounter a scenario where prepare_fractional_sub_cg reads clique_table_, sees it is non-null, and races on the clique table while it is being updated by extend_cliques, right?

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.

You are right. I think in practice it never happened because the other cut generation functions were taking some time and most of the time clique table was non-null. Now it depends on signal_extend being null. But there was a convoluted logic of local, shared out clique_table and that also complicated things, it was because we were using clique generation in presolve before and that included ownership transfer logic. Now that logic is also simplified. Thanks for catching it Alice!

@akifcorduk

Copy link
Copy Markdown
Contributor Author

/ok to test 0ea3102

@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

🤖 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/grpc/codegen/field_registry.yaml`:
- Around line 685-688: The gRPC path currently maps zero_half_cuts directly into
settings.zero_half_cuts without enforcing the local CUOPT_MIP_ZERO_HALF_CUTS
bounds. Add request-side validation in the generated protobuf-to-settings flow
(field_registry.yaml / generated_proto_to_mip_settings.inc path) so
zero_half_cuts is rejected unless it is within [-1, 1], or clamp it before
assigning to the MIP settings. Make sure the fix is applied where the field is
translated into the solver settings, not only in solver_settings.cu.

In `@cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu`:
- Around line 727-739: The publication order in clique_table.cu is unsafe
because clique_table_out is assigned in the clique_table extension path before
extend_cliques and fill_var_clique_maps finish mutating the same clique_table
object. Fix this by delaying the clique_table_out assignment until after all
mutations are complete, or by publishing a separate immutable snapshot from the
base table and extending a different object; use the clique_table_out
assignment, extend_cliques, and fill_var_clique_maps symbols to locate the
affected flow.
🪄 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: 93cd4392-45ad-481c-98b7-554bb5f60951

📥 Commits

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

⛔ Files ignored due to path filters (3)
  • cpp/src/grpc/codegen/generated/cuopt_remote_data.proto is excluded by !**/generated/**
  • cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc is excluded by !**/generated/**
  • cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc is excluded by !**/generated/**
📒 Files selected for processing (11)
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/cuts/cuts.cpp
  • cpp/src/grpc/codegen/field_registry.yaml
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu
  • cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh
  • docs/cuopt/source/cuopt-c/mip/mip-c-api.rst
  • docs/cuopt/source/mip-settings.rst
  • skills/cuopt-developer/references/contributing.md
  • skills/cuopt-developer/references/conventions.md
✅ Files skipped from review due to trivial changes (5)
  • cpp/include/cuopt/linear_programming/constants.h
  • skills/cuopt-developer/references/conventions.md
  • docs/cuopt/source/cuopt-c/mip/mip-c-api.rst
  • docs/cuopt/source/mip-settings.rst
  • skills/cuopt-developer/references/contributing.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/cuts/cuts.cpp

Comment thread cpp/src/grpc/codegen/field_registry.yaml
Comment thread cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu
@akifcorduk

Copy link
Copy Markdown
Contributor Author

/ok to test 758e045

Comment thread cpp/src/cuts/cuts.cpp
// aborts/terminates right after. Each channel below enables it through its own
// DEBUG_* flag and supplies its own prefix; when the flag is 0 the call expands
// to a no-op that still consumes its arguments.
#define CUTS_DEBUG_LOG(prefix, ...) \

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.

Nit: We have a settings.log.debug already. Could this be used instead of this macro?

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 seems strange to have cut specific debugging/logging.

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.

The setting.log.debug prints trace logs underneath because it was too verbose. I wanted to have some log in between which is CUTS_DEBUG_LOG. I can the trace logging to setting.log.trace so we can have finer control on the logs on another PR. I can convert this to settings.log.debug for now.

Comment thread cpp/src/cuts/cuts.cpp Outdated
f_t* work_estimate,
f_t max_work_estimate)
{
for (const auto candidate : candidates) {

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.

Nit: prefer an actual type here instead of auto. Using a type makes the code self-documenting.

Comment thread cpp/src/cuts/cuts.cpp Outdated
if (toc(start_time) >= time_limit) { return; }
bool add = true;
i_t checks = 0;
for (const auto v : selected) {

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.

Same here. Prefer actual type to auto

Comment thread cpp/src/cuts/cuts.cpp Outdated
scratch.ensure_size(static_cast<std::size_t>(total_idx));
++scratch.gen;
const std::uint64_t gen = scratch.gen;
auto& dist = scratch.dist;

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.

Prefer type to auto

Comment thread cpp/src/cuts/cuts.cpp Outdated
ZERO_HALF_DEBUG("dijkstra_odd_cycle work_limit hit pops=%lld", static_cast<long long>(pops));
return false;
}
for (const auto v_local : neigh) {

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.

Prefer type to auto

Comment thread cpp/src/cuts/cuts.cpp Outdated

std::unordered_set<i_t> seen_local;
seen_local.reserve(local_seq.size());
for (const auto lv : local_seq) {

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.

Prefer type to auto

Comment thread cpp/src/cuts/cuts.cpp Outdated
cycle_vertices.reserve(local_seq.size());
std::unordered_set<i_t> seen_var;
seen_var.reserve(local_seq.size());
for (const auto lv : local_seq) {

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.

Preferr type to auto

Comment thread cpp/src/cuts/cuts.cpp Outdated
std::unordered_set<i_t> cycle_members(cycle_vertices.begin(), cycle_vertices.end());
std::vector<i_t> candidates;
candidates.reserve(adj_set.size());
for (const auto candidate : adj_set) {

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.

Prefer type to auto

Comment thread cpp/src/cuts/cuts.cpp Outdated
Comment thread cpp/src/cuts/cuts.cpp Outdated
}

template <typename i_t, typename f_t>
void cut_generation_t<i_t, f_t>::prepare_fractional_sub_cg(

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.

Nit: The code would be easier to read if the CG acronym was written out in full. Does CG stand for cut graph?

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.

The acronym is conflict_graph. I can extend it.

Comment thread cpp/src/cuts/cuts.cpp Outdated
total_adj_entries += adj_set.size();
auto& adj = sub_cg_.adj_local[idx];
adj.reserve(adj_set.size());
for (const auto neighbor : adj_set) {

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.

Prefer type to auto

Comment thread cpp/src/cuts/cuts.cpp Outdated
{
std::unordered_set<i_t> adj_global;
adj_global.reserve(adj.size());
for (const auto neighbor : adj) {

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.

Prefer type to auto

Comment thread cpp/src/cuts/cuts.cpp
}
}

// Build the fractional conflict-graph subgraph once (resolving the async

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.

Ah it stands for conflict graph. My ignorance/confusion is a reason to write it out in full.

Comment thread cpp/src/cuts/cuts.cpp Outdated
added_per_var++;
// mark all CG vertices that participated so we do not re-derive the same
// cycle from a different source vertex
for (const auto v : cycle_vertices) {

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.

Prefer explicit type here instead of auto

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

Thanks for adding these cuts in @akifcorduk

LGTM. Very minor style comments. I did not review the math.

@akifcorduk

Copy link
Copy Markdown
Contributor Author

/ok to test b90197c

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.

4 participants