CPUFJ fastpath for binary instances + latency changes to find better solutions earlier - #1750
Open
aliceb-nv wants to merge 78 commits into
Open
CPUFJ fastpath for binary instances + latency changes to find better solutions earlier#1750aliceb-nv wants to merge 78 commits into
aliceb-nv wants to merge 78 commits into
Conversation
Contributor
Author
|
/ok to test de1a8bd |
CI Test Summary⏭️ All 5 test job(s) skipped. |
aliceb-nv
force-pushed
the
bhw-reduce
branch
2 times, most recently
from
August 21, 2026 09:35
5b2da95 to
2d1b0ff
Compare
aliceb-nv
force-pushed
the
cpufj-fastpath-isolated
branch
from
August 21, 2026 09:54
de1a8bd to
e93f63b
Compare
- 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
…1683) ## Issue Authors: - Alice Boucher (https://github.com/aliceb-nv) Approvers: - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) - Akif ÇÖRDÜK (https://github.com/akifcorduk) URL: #1683
## Issue Authors: - Alice Boucher (https://github.com/aliceb-nv) Approvers: - Akif ÇÖRDÜK (https://github.com/akifcorduk) URL: #1738
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
force-pushed
the
cpufj-fastpath-isolated
branch
from
August 25, 2026 13:52
e93f63b to
7eda27d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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