Add NVIDIA cuOpt as a GPU-accelerated solver - #909
Open
mal84emma wants to merge 10 commits into
Open
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add test/test_cuopt.py with the cuOpt differential tests against live HiGHS (dual sign matrix, presolve maximisation, status mapping, options, refusals, device probe and fork safety) and test/test_cuopt_interrupt.py for the KeyboardInterrupt helper. Register cuOpt's time limit option and feature matrix rows, and skip the warmstart test for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trim test/test_cuopt.py to the tests that guard linopy's compensation code for cuOpt quirks that fail silently (always-minimise duals, 0.5*Q Hessian, pad-row dual slicing, empty-primal handling, MIQP pre-check) plus the process-safety machinery. All expectations are now baked analytic values (confirmed once against HiGHS 1.15.1) instead of live differential solves. Fold test_cuopt_interrupt.py into test_cuopt.py, add cuOpt cases to test_semi_continuous.py, document the remaining quirks in doc/gpu-acceleration.rst and condense the cuOpt comments and docstrings in solvers.py to the guidance future devs need. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes from two adversarial review passes over the cuOpt implementation: - Widen the pad row for constraint-free models to include 0 so a semi-continuous variable keeps its off state (was silently forced on, reported Optimal); guarded by a new constraint-free SC test. - Hand DataModel ownership to the solve worker via a box list so the worker's clear is deterministically the final decref for the solver-owned reference; docstrings reworded to the actual thread contract (solves and unpredictable-thread teardown, not all natives). - refresh() clears the CUDA probe's own cache; probe failures now log the exception and stderr so a broken install is distinguishable from a missing GPU. - Guard a short LP dual like the primal instead of an opaque IndexError; MIQP refusal names semi-continuous variables too. - Default io_api=None fallback logs at info, only an explicitly requested file io_api warns; docs updated to match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Guard cuOpt against silent int32 CSR truncation beyond 2**31-1 nonzeros - Trim cuOpt helper docstrings to state failures without inline evidence - Simplify square_equality_model to a fixed one-row fixture with baked values - Assert cuOpt's GPU_ACCELERATION capability in the feature matrix - Mark the repeated-solves test `slow` (intrinsic ~135 s GPU cost) and register the marker so it can be deselected during development - Drop the non-discriminating semi-continuous cuOpt test, loosen the unbounded-QP status pin to membership, and source the warmstart skip reason - Slim the GPU documentation Co-Authored-By: mal84emma <info@materialdifference.earth> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merging this PR will degrade performance by 19.95%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | test_to_lp[milp-n=50] |
2 MB | 2.7 MB | -24.48% |
| ❌ | Memory | test_to_lp[masked-n=100] |
2.1 MB | 2.7 MB | -22.43% |
| ❌ | Memory | test_to_lp[merge_balance-severity=0] |
2.6 MB | 3.2 MB | -17.78% |
| ❌ | Memory | test_to_lp[expression_arithmetic-n=250] |
40.3 MB | 47.3 MB | -14.76% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing mal84emma:feat/cuopt-solver (ff9383d) with master (570ff2e)
Footnotes
-
175 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
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.
Closes #515
Bringing Nvidia speed to linopy.
Why?
Changes proposed in this Pull Request
First-class support for NVIDIA cuOpt as a linopy solver: LP, MILP and convex QP on the GPU through cuOpt's direct (in-memory) API, with duals for LP/QP, MIP gap and dual bound for MILP, semi-continuous variables, and a
Model.to_cuopt()bridge. Installable withpip install "linopy[gpu]"(Linux, CUDA 12 driver ≥ 525.60.13, compute capability ≥ 7.0).Note
The following content was generated by AI (a supervised multi-agent run; every commit on this branch is marked with its authoring model).
Design points a reviewer will care about
io_apis transparently fall back to the direct build with one warning. The rationale (unverified solution-file parsing, an inverted Q convention between.lpand.mps) is documented at the code site.method=3(Barrier) is linopy's default, not cuOpt's own concurrent default, which segfaults on repeated solves of models ≳1300 variables (reproduced and documented). An escape hatch to PDLP (method=1) is documented for very large sparse LPs.0.5 · M.Q(cuOpt symmetrises internally); a committed deliberate-failure test proves the wrong convention changes the answer by 50% relative, so the guard cannot rot silently.NotImplementedErrorup front (cuOpt silently mis-solves it), warm start is refused (usable PDLP warm start needs three simultaneous non-default settings), reduced costs are not populated (upstream sign defect for maximised models with<=rows), SOS/indicator constraints are absent upstream. Each refusal carries a 1–3-line rationale at its enforcement site.available_solverson a GPU-less machine simply omits cuOpt, and the probe cannot poison lateros.fork()children.Performance
Indicative benchmark: LOPF of the PyPSA
scigrid-deexample network (585 buses, 1423 generators, 852 lines), its 24-hour day tiled to scale the LP; wall time ofm.solve(), solvers run sequentially on an otherwise idle Azure VM (8-core EPYC 7V12, Tesla T4 16 GB). HiGHS was giventhreads=8, parallel=on(measured: no gain over its defaults); cuOpt ran at this branch's defaults (direct API, barrier). Objectives agree to ≤8·10⁻⁴ relative at both solvers' default tolerances.cuOpt times are first-solve-in-process (cold): they include one-off CUDA context initialisation, which repeat solves in the same process avoid, running ~2 s faster. The gap widens with size (HiGHS scales ~n^1.9 here, cuOpt near-linearly). A T4 is the smallest supported datacenter GPU; newer hardware should do better.
Note
To be fair this is a bit of a trust-me-bro benchmark. HiGHS can solve much faster than its default config and even faster with better hardware (test was on a GPU optimized VM, though with not a very good GPU).
Regardless, cuOpt is fast and has been winning competitions.
Particularly now that cards with serious VRAM are available I think it could make solving large ESOMs more accessible without a commercial solver license.
Known upstream issue reviewers should weigh
cuOpt 26.08.00 can destabilise a long-lived host process after any in-process solve: its bundled OpenMP runtime intermittently corrupts glibc malloc metadata, crashing the process later in unrelated code. This was diagnosed to root cause (gdb backtrace into
_int_malloc, linopy exonerated by a source audit and by mitigation experiments) and reported with full evidence on NVIDIA/cuopt#1768. Every completed full test-suite run on this branch has a failure set byte-identical to master's baseline — correctness is unaffected — but roughly two thirds of full--run-gpusuite runs abort mid-run from this corruption. The feature is opt-in (install extra + explicit solver choice); whether to ship ahead of an upstream fix is a maintainer decision this PR intentionally surfaces rather than hides.Verification summary
minandmax.Solutiontime-limit path.pytest test/test_cuopt.py test/test_semi_continuous.py --run-gpu: 102 passed); CPU-only machines skip all of them; full CPU suite and every completed full GPU suite run are failure-set-identical to master's baseline (74 pre-existing failures/errors, all intest/remoteand unrelated areas).Implementation Approach
This PR was a bit of an experiment at getting a Claude agent system to one-shot new features. If you're interested in the approach I took you can read the agent docs and my thoughts on how it went here.
Checklist
AGENTS.md).doc.doc/release_notes.rstof the upcoming release is included.