Skip to content

CPUFJ fastpath for binary instances + latency changes to find better solutions earlier - #1750

Open
aliceb-nv wants to merge 78 commits into
bhw-reducefrom
cpufj-fastpath-isolated
Open

CPUFJ fastpath for binary instances + latency changes to find better solutions earlier#1750
aliceb-nv wants to merge 78 commits into
bhw-reducefrom
cpufj-fastpath-isolated

Conversation

@aliceb-nv

Copy link
Copy Markdown
Contributor

This PR adds a new SIMD-tuned fastpath for CPUFJ for the case of binary-only instances with certain restrictions. It also incorporates multiple changes to help find feasible solutions earlier in the solve.

Benchmark results pending. Initial results show the SGM primal integral going down from 0.030 to 0.026.

Description

Issue

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

@aliceb-nv aliceb-nv added this to the 26.10 milestone Aug 19, 2026
@aliceb-nv
aliceb-nv requested review from a team as code owners August 19, 2026 17:56
@aliceb-nv
aliceb-nv requested review from tmckayus and removed request for a team August 19, 2026 17:56
@aliceb-nv aliceb-nv added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels Aug 19, 2026
@copy-pr-bot

copy-pr-bot Bot commented Aug 19, 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.

@aliceb-nv

Copy link
Copy Markdown
Contributor Author

/ok to test de1a8bd

@github-actions

Copy link
Copy Markdown

CI Test Summary

⏭️ All 5 test job(s) skipped.

@aliceb-nv
aliceb-nv force-pushed the bhw-reduce branch 2 times, most recently from 5b2da95 to 2d1b0ff Compare August 21, 2026 09:35
@aliceb-nv
aliceb-nv force-pushed the cpufj-fastpath-isolated branch from de1a8bd to e93f63b Compare August 21, 2026 09:54
akifcorduk and others added 17 commits August 21, 2026 11:43
- Add general mod-2 zero-half separation. The separator finds combinations of constraint rows whose odd coefficients cancel modulo 2 while their aggregated right-hand side remains odd. Halving and rounding this aggregate yields a valid cut.
- Use sparse GF(2) elimination to find these row dependencies, then transform, lift, score, and add violated candidates to the cut pool.
- Propagate B&B timeouts to active node solves and improve work unit checks for some cuts.
- Update PaPILO pin for the coefficient-capacity guard fix.



On H100, two runs each:
Main vs mod-2
**Feasible** | 224.0 | 226.5 | +2.5
**Average error gap** | 12.285 | 11.655 | -0.630
**Optimal instances** | 76.5 | 77.5 | +1.0
**Root gap closed average** | 29.7518% | 31.0948% | +1.3429%
**Root gap closed shifted geomean** (+1) | 10.4875% | 11.1626% | +0.6750%
MIP gap unchanged, due to some cut passes taking longer.

Authors:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)

Approvers:
  - Alice Boucher (https://github.com/aliceb-nv)
  - Nicolas L. Guidotti (https://github.com/nguidotti)
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)

URL: #1726
Authors:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)

Approvers:
  - Hugo Linsenmaier (https://github.com/hlinsen)

URL: #1757
Fixes #1756 — `test_batch_solve_varying_sizes` aborted with a device-side `block_copy` assert on CUDA 13.3.

`rmm::device_uvector::set_element_async` does not synchronize; the host source must stay valid until the stream is synced (`device_uvector.hpp:200-207`). Four sites in `solution.cu` copied a route `view_t` from a stack local that died each loop iteration, two of them without ever syncing. Since the whole `view_t` is copied, a dead source corrupts the span size and the pointers together — hence both the assert and the `cudaErrorIllegalAddress` seen in CI.

Each site now stages its host sources in a vector reserved up front, so the staging outlives the copy.

Also removes the CUDA 13.3 skip, since the test passes again.

Note: only pull-request builds set `DEFINE_ASSERT`, so nightly and released builds were doing this out-of-bounds copy silently.

Authors:
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)

Approvers:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)
  - Trevor McKay (https://github.com/tmckayus)

URL: #1759
Revert changes to the PCG random number generator introduced in PR #1608. Note that these generators are carefully constructed and small changes can affect the quality of the random number generated. Added a skill to avoid agents to modify this file.

Authors:
  - Nicolas L. Guidotti (https://github.com/nguidotti)

Approvers:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)
  - Chris Maes (https://github.com/chris-maes)

URL: #1762
…1769)

<!--

Thank you for contributing to cuOpt :)

Here are some guidelines to help the review process go smoothly.

Many thanks in advance for your cooperation!

Note: The pull request title will be included in the CHANGELOG.
-->


## Description

Several standalone C API getters (`cuOptGetNumVariables`,
`cuOptGetObjectiveSense`, `cuOptGetObjectiveOffset`,
`cuOptGetObjectiveCoefficients`, `cuOptGetNumNonZeros`) duplicated logic
that already exists in `cuOptGetProblemIntAttribute` /
`cuOptGetProblemFloatAttribute` / `cuOptGetProblemFloatArrayAttribute` —
same null checks, same single-field access, maintained in two places.

Rewrote each as a thin wrapper delegating to the corresponding attribute
call, so there's a single source of truth per attribute and the
implementations can't drift apart. Behavior is unchanged: the
delegated-to
functions already perform the same null/argument validation, so error
codes and return values are identical to before.

`cuOptGetNumConstraints` had the same duplication and was already
cleaned
up the same way in #1761, along with other bug fixs.

## Issue

## Checklist

- [ ] I am familiar with the [Contributing
Guidelines](https://github.com/NVIDIA/cuopt/blob/HEAD/CONTRIBUTING.md).
- Testing
   - [ ] New or existing tests cover these changes
   - [ ] Added tests
   - [ ] Created an issue to follow-up
   - [x] NA
- Documentation
   - [ ] The documentation is up to date with these changes
   - [ ] Added new documentation
   - [x] NA

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>
<!--

Thank you for contributing to cuOpt :)

Here are some guidelines to help the review process go smoothly.

Many thanks in advance for your cooperation!

Note: The pull request title will be included in the CHANGELOG.
-->


## Description

`cuOptGetDualSolution`/`cuOptGetReducedCosts` wrote more elements than
`cuOptGetNumConstraints` documented whenever the problem had quadratic
constraints, overrunning a caller-sized buffer. Root cause: quadratic
constraints are internally reformulated into second-order cones for the
barrier solver, which grows the row count beyond the original
constraint count, but the returned dual vector was never resized back
down before being copied into the caller's buffer — and dual recovery
for QCQP isn't supported yet regardless.

Fix: dual solution / reduced cost access for QCQP now returns an error
instead of writing invalid or wrong-sized data, consistently across all
three surfaces:
- **C API**: `cuOptGetDualSolution`/`cuOptGetReducedCosts` return
  `CUOPT_INVALID_ARGUMENT` (matching the existing MIP-solution pattern).
- **Python**: `Solution.get_dual_solution()`/`get_reduced_cost()` raise
  `AttributeError` only when explicitly called — the solve itself still
  succeeds normally.
- **gRPC**: `dual_solution`/`reduced_cost` are simply omitted from the
  response payload (same mechanism already used for other optional
  fields), so a QCQP solve still succeeds end-to-end.

In all cases, a normal solve (primal solution, objective, termination
status, etc.) is unaffected for QCQP problems — only explicit
dual/reduced-cost access is gated.

Also fixes the underlying ambiguity flagged in the issue discussion:
`cuOptGetNumConstraints` / `CUOPT_ATTR_NUM_CONSTRAINTS` previously
reported linear constraints only; they now report linear + quadratic
constraints combined. Added a new `CUOPT_ATTR_NUM_QUADRATIC_CONSTRAINTS`
attribute so callers can still recover the quadratic-only count.

## Issue

closes #1751

## Checklist

- [x] I am familiar with the [Contributing
Guidelines](https://github.com/NVIDIA/cuopt/blob/HEAD/CONTRIBUTING.md).
- Testing
   - [ ] New or existing tests cover these changes
   - [x] Added tests
   - [ ] Created an issue to follow-up
   - [ ] NA
- Documentation
   - [ ] The documentation is up to date with these changes
   - [x] Added new documentation
   - [ ] NA

---------

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>
26.08 is released and no further 26.08 releases are planned, so the nightly pipeline no longer needs to build it.

Leaves `main`, which is 26.10 (`VERSION` = `26.10.00`).

`release/26.10` is not added here because it has not been cut yet — the workflow resolves each matrix entry through the branches API before triggering, so an entry for a branch that does not exist would fail every night:

```bash
export SHA=$(gh api -q '.commit.sha' "repos/nvidia/cuopt/branches/${CUOPT_BRANCH}")
```

It should be added once `release/26.10` exists.

Authors:
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)

Approvers:
  - Trevor McKay (https://github.com/tmckayus)

URL: #1777
…1717)

`seed_generator::seed_` was a single process-wide counter. The two solvers seed it from unrelated inputs:

```
cpp/src/routing/problem/problem.cu:80    set_seed(num_requests, num_orders, num_orders)   // problem geometry
cpp/src/mip_heuristics/solve.cu:374      if (settings.seed >= 0) set_seed(settings.seed)  // user settings
```

Sharing one counter means whichever solver runs last overwrites the other's seed, so solving a VRP and then a MIP in the same process silently discards the user's `settings.seed`.

## Change

The seed now belongs to the solver that uses it. `seed_generator_t` is an instance held by `routing::problem_t` and `mip::problem_t`, each seeded from its own settings, and the process-wide `seed_generator` is removed.

Routing gains `set_seed` / `get_seed` on `solver_settings_t`, following `mip_solver_settings_t` where `-1` means "derive it", so behaviour is unchanged when the user does not set one. Routing previously had no seed control at all despite being the component that overwrote the shared counter.

All 45 call sites now draw from the owning problem — 13 in routing, 32 in the MIP heuristics. Two sites cannot reach a problem and take the seed explicitly rather than keeping a global:

- `ejection_pool_t::random_shuffle(seed)` — the pool has no route back to a problem
- the feasibility jump host-LP path falls back to the simplex settings' `random_seed`, which it already receives

Seeds are handed out from a `thread_local` counter, rebased from the owning solver's base seed whenever that base changes, following the mechanism in Alice's earlier determinism work (`3e214a86`). A thread therefore walks the same sequence from a given base regardless of how workers interleave, so which seed a work item receives does not depend on scheduling — the property that matters for reproducibility across synchronisation points. The base itself lives on the problem, so the two concerns are separate: the base stops the two solvers overwriting each other's seed, the thread-local counter stops ordering from mattering.

This also resolves the `// TODO: should be thread local?` that sat on the class.

Because `base_seed_` is a plain `int64_t`, the class needs no atomic, no `mutable`, and no hand-written copy or assignment operators, and `problem_t` keeps its implicit copy and move.

One inherited caveat, documented in the header: the thread-local state is a function-local static shared by all generators on a thread, rebased on a *change* of base. Two solvers configured with the same base seed and used from one thread continue a single sequence rather than each restarting.

## On the test

`determinism_test.cu` called `seed_generator::set_seed(seed)` before each of three solves *even though it already set `settings.seed`* — a workaround for the global persisting between solves. Those three lines are gone; the test relies on `settings.seed` alone.

## Testing

Clean build (CUDA 13.3, gcc 14.3) and `ctest`. `DeterministicBBTest` passes all four cases, including `reproducible_high_contention`, which is where a change in seed assignment under concurrent solves would surface.

## Follow-ups

Python bindings for the routing seed, and routing-over-gRPC after #1597, which owns the routing entries in `field_registry.yaml`.

## History

The class arrived in [rapidsai/cuopt#1270](rapidsai/cuopt#1270) as a routing-local helper, where a static was a reasonable choice — it replaced `clock64()` seeding and was meant to be reachable from any kernel without plumbing. It became shared in [rapidsai/cuopt#2417](rapidsai/cuopt#2417), which moved it from `routing/utilities` to `src/utilities` and is described purely as a file move; the "accessible throughout the code" premise was not revisited once a second solver used it.

Authors:
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)

Approvers:
  - Bradley Dice (https://github.com/bdice)
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)

URL: #1717
…sion mismatch) (#1780)

## Summary
Fixes two independent issues causing the `build-images` workflow to fail (e.g. https://github.com/NVIDIA/cuopt/actions/runs/32692816239):

- **`ci/docker/Dockerfile`**: `add-apt-repository ppa:deadsnakes/ppa` calls Launchpad's API to fetch the PPA's signing key, which is subject to transient failures (observed `HTTP 500 GPGKeyTemporarilyNotFoundError`). Wrapped it in a 5-attempt retry loop with backoff so a transient Launchpad hiccup doesn't fail the whole image build.
- **`.github/workflows/build_test_publish_images.yaml`**: `compute-cuopt-ver` computed `CUOPT_VER` with plain `rapids-generate-version`, but `ci/build_wheel.sh` has published nightly wheels with a `.post<datetime>` suffix since #1661 (`dbaf523f`). The two were out of sync, so nightly image builds pinned a version (e.g. `26.10.0a43`) that was never actually published — only `.post`-suffixed builds exist on the index — causing `pip install` to fail with "No matching distribution found". Now sets `RAPIDS_VERSION_SUFFIX=".post${RAPIDS_DATETIME_STRING}"` before calling `rapids-generate-version`, mirroring `ci/build_wheel.sh`. `RAPIDS_VERSION_SUFFIX` is a no-op on release builds (confirmed in `rapids-generate-version`'s source), so release tagging/versioning is unaffected.

## Test plan
- [ ] Trigger `build-images` workflow (nightly build) and confirm the `python-env` stage installs Python without hitting the deadsnakes PPA error
- [ ] Confirm `pip install cuopt-server-cu13==<CUOPT_VER>` resolves to a real published nightly wheel
- [ ] Confirm release-build version computation (`IMAGE_TAG_PREFIX`) is unchanged for tagged releases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Authors:
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)

Approvers:
  - Trevor McKay (https://github.com/tmckayus)

URL: #1780
- 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).

MIPLIB 2017:
GH200, 10min

```
================================================================================
 main-2026-08-13 (1) vs root-heuristics (2)
================================================================================

------------------------------------------------------------------------------------------------------------------------------
|                                        |       Run 1        |       Run 2        |     Abs. Diff.     |   Rel. Diff. (%)   |
------------------------------------------------------------------------------------------------------------------------------
| Imported                                                 240                  240                   +0                 --- |
| Feasible                                                 226                  225                   -1                 --- |
| Optimal                                                   87                   87                   +0                 --- |
| Solutions with <0.1% primal gap                          145                  143                   -2                 --- |
| Nodes explored (mean)                              1.306e+07            1.322e+07            +1.63e+05               +1.25 |
| Nodes explored (shifted geomean)                   1.113e+04                 8201                -2930               -26.3 |
| Relative MIP gap (mean)                               0.2626               0.2695            +0.006939               +2.64 |
| Relative MIP gap (shifted geomean)                   0.08603               0.0867           +0.0006709               +0.78 |
| Solve time (mean)                                      412.7                415.6               +2.922              +0.708 |
| Solve time (shifted geomean)                           182.3                179.6               -2.652               -1.46 |
| Primal gap (mean)                                      9.867                10.28              +0.4097               +4.15 |
| Primal gap (shifted geomean)                          0.3855               0.4062             +0.02071               +5.37 |
| Primal integral (mean)                                    22                22.11              +0.1057              +0.481 |
| Primal integral (shifted geomean)                      2.274                2.074              -0.1999               -8.79 |
------------------------------------------------------------------------------------------------------------------------------


----------------------------------------------------------------------
|             Name             |     status 1     |     status 2     |
----------------------------------------------------------------------
| app1-2                                  optimal           feasible |
| cbs-cta                                feasible            optimal |
| cmflsp50-24-8-8                         optimal           feasible |
| csched007                               optimal           feasible |
| map16715-04                            feasible            optimal |
| neos-1171737                           feasible            optimal |
| neos-5104907-jarama                    feasible            timeout |
----------------------------------------------------------------------
```

Authors:
  - Nicolas L. Guidotti (https://github.com/nguidotti)

Approvers:
  - Alice Boucher (https://github.com/aliceb-nv)
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)
  - Chris Maes (https://github.com/chris-maes)

URL: #1700
This PR implements RENS (*Relaxation Enforced Neighborhood Search*, see [1, 2]) heuristic. RENS construct and then solve the following sub-MIP:

```math
\begin{align} & z^* = \min \, \vec{c}^T \vec{x} \\ & \text{subject to} & \mathbf{A} \vec{x} = \vec{b} \\ & & l_i \leq x_i \leq u_i, \forall i \in \mathcal{N} \setminus \mathcal{I}  \\ & & \lfloor \check{x}_j \rfloor \leq x_i \leq \lceil \check{x}_j \rceil, \forall i \in \mathcal{I}  \\ & & \vec{x} \in \mathbb{R}^n \\  & & x_j \in \mathbb{Z}, \forall j \in \mathcal{I} \end{align}
```
where $\mathcal{N}$ is the set of all variables and $\mathcal{I}$ is the set of integer values. In essence, all integer variables with integral values are fixed, while the ones with fractional values are restricted to be rounded up or down. 

This re-use the same infrastructure as RINS, i.e., it passes through presolve and can recurse. It requires some refactoring of the sub-MIP code to support these additional features.

Closes #1584.

## Results
MIPLIB2017, GH200, 10min

```
================================================================================
 root-heuristics-1 (1) vs rens-1 (2)
================================================================================

------------------------------------------------------------------------------------------------------------------------------
|                                        |       Run 1        |       Run 2        |     Abs. Diff.     |   Rel. Diff. (%)   |
------------------------------------------------------------------------------------------------------------------------------
| Imported                                                 240                  240                   +0                 --- |
| Feasible                                                 227                  227                   +0                 --- |
| Optimal                                                   87                   92                   +5                 --- |
| Solutions with <0.1% primal gap                          145                  144                   -1                 --- |
| Nodes explored (mean)                              1.366e+07            1.309e+07           -5.731e+05               -4.19 |
| Nodes explored (shifted geomean)                        8947                 5351                -3596               -40.2 |
| Relative MIP gap (mean)                               0.2709                0.248             -0.02291               -8.45 |
| Relative MIP gap (shifted geomean)                   0.08762                0.087           -0.0006245              -0.713 |
| Solve time (mean)                                        419                405.6               -13.35               -3.19 |
| Solve time (shifted geomean)                           189.4                  176               -13.38               -7.06 |
| Primal gap (mean)                                      9.237                9.634              +0.3963               +4.29 |
| Primal gap (shifted geomean)                          0.3752               0.3633             -0.01198               -3.19 |
| Primal integral (mean)                                 20.14                20.08             -0.06726              -0.334 |
| Primal integral (shifted geomean)                      2.071                1.978             -0.09335               -4.51 |
------------------------------------------------------------------------------------------------------------------------------


----------------------------------------------------------------------
|             Name             |     status 1     |     status 2     |
----------------------------------------------------------------------
| app1-2                                 feasible            optimal |
| cbs-cta                                feasible            optimal |
| glass-sc                               feasible            optimal |
| mzzv11                                 feasible            optimal |
| n2seq36q                               feasible            optimal |
| neos-5093327-huahum                     optimal           feasible |
| triptim1                               feasible            optimal |
----------------------------------------------------------------------

================================================================================
 root-heuristics-2 (1) vs rens-2 (2)
================================================================================

------------------------------------------------------------------------------------------------------------------------------
|                                        |       Run 1        |       Run 2        |     Abs. Diff.     |   Rel. Diff. (%)   |
------------------------------------------------------------------------------------------------------------------------------
| Imported                                                 240                  240                   +0                 --- |
| Feasible                                                 225                  228                   +3                 --- |
| Optimal                                                   87                   91                   +4                 --- |
| Solutions with <0.1% primal gap                          143                  145                   +2                 --- |
| Nodes explored (mean)                              1.322e+07            1.306e+07           -1.595e+05               -1.21 |
| Nodes explored (shifted geomean)                        8201                 6479                -1722                 -21 |
| Relative MIP gap (mean)                               0.2695               0.2752            +0.005638               +2.09 |
| Relative MIP gap (shifted geomean)                    0.0867              0.08533            -0.001362               -1.57 |
| Solve time (mean)                                      415.6                407.4               -8.174               -1.97 |
| Solve time (shifted geomean)                           179.6                175.6               -4.007               -2.23 |
| Primal gap (mean)                                      10.28                9.136               -1.141               -11.1 |
| Primal gap (shifted geomean)                          0.4062               0.3614             -0.04481                 -11 |
| Primal integral (mean)                                 22.11                19.93               -2.177               -9.85 |
| Primal integral (shifted geomean)                      2.074                1.961              -0.1133               -5.46 |
------------------------------------------------------------------------------------------------------------------------------


----------------------------------------------------------------------
|             Name             |     status 1     |     status 2     |
----------------------------------------------------------------------
| app1-2                                 feasible            optimal |
| cmflsp50-24-8-8                        feasible            optimal |
| glass-sc                               feasible            optimal |
| neos-3216931-puriri                     timeout           feasible |
| neos-5104907-jarama                     timeout           feasible |
| peg-solitaire-a3                        timeout            optimal |
----------------------------------------------------------------------

```


## References
[1] T. Berthold, “RENS: The optimal rounding,” Math. Prog. Comp., vol. 6, no. 1, pp. 33–54, Mar. 2014, doi: 10.1007/s12532-013-0060-9.
[2] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, Berlin, 2007. doi: 10.14279/depositonce-1634.

Authors:
  - Nicolas L. Guidotti (https://github.com/nguidotti)

Approvers:
  - Alice Boucher (https://github.com/aliceb-nv)
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)

URL: #1719
We were checking the tolerance for integrality of rows with relative tolerance, this changes it to absolute tolerance.

Authors:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)

Approvers:
  - Nicolas L. Guidotti (https://github.com/nguidotti)

URL: #1770
@aliceb-nv
aliceb-nv force-pushed the cpufj-fastpath-isolated branch from e93f63b to 7eda27d Compare August 25, 2026 13:52
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.

5 participants