Skip to content

cuopt_mcp: add cuopt_health, package it like the other Python modules - #1819

Draft
ramakrishnap-nv wants to merge 20 commits into
NVIDIA:grpc-codegen-settings-descriptionsfrom
ramakrishnap-nv:feat/cuopt-mcp-server
Draft

cuopt_mcp: add cuopt_health, package it like the other Python modules#1819
ramakrishnap-nv wants to merge 20 commits into
NVIDIA:grpc-codegen-settings-descriptionsfrom
ramakrishnap-nv:feat/cuopt-mcp-server

Conversation

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

Stacked on #1701 (base is grpc-codegen-settings-descriptions, not main) so the diff here is only the MCP work — cuopt_mcp itself lands with that branch.

Three commits:

  • cuopt_health — no-arg tool reporting host, port, tls and reachability. Every other tool needs a model or a job_id, so the connection could previously only be checked by submitting work and reading the failure. The service has no health RPC, so reachability is probed with a status lookup for an impossible job id, which must return NOT_FOUND. The unreachable message now says to look for a running server before starting one, and cuopt_solve_milp carries the problem key list rather than pointing at cuopt_solve_lp (a caller can hold the MILP tool alone and cannot follow that pointer).
  • Packagingcuopt_mcp used plain setuptools, so depends_on_cuopt was emitted as a literal cuopt==26.10.*, which no CUDA-suffixed build publishes; pip install python/cuopt_mcp could not resolve. It now gets the same rapids-build-backend treatment as cuopt_server and builds as cuopt_mcp-cu13, plus a build.sh cuopt_mcp target.
  • --allow-reuseport — separable from the rest; happy to pull it out. gRPC enables SO_REUSEPORT by default, so a second cuopt_grpc_server on an already-served port binds silently and the kernel splits connections between two processes, each with its own workers and RMM pool. The existing Failed to bind path never fired.

Trying it

./build.sh cuopt_mcp                       # installs into the active env
cuopt_grpc_server --port 50051 &

Point an MCP client at cuopt-mcp with CUOPT_REMOTE_HOST / CUOPT_REMOTE_PORT, then call cuopt_health first. 63 tests pass locally (CUOPT_TEST_GRPC_PORT=50051 pytest python/cuopt_mcp/tests); the end-to-end fixture resolves cuopt-mcp from PATH, so make sure the intended env is first or it will silently exercise a different install.

Known gaps

bdice and others added 19 commits August 11, 2026 03:25
Contributes to rapidsai/build-planning#313.

Updates rapids-logger to version 0.3 and removes the prerelease fallback.

Depends on rapidsai/rapids-metadata#64.

Authors:
  - Bradley Dice (https://github.com/bdice)

Approvers:
  - Vyas Ramasubramani (https://github.com/vyasr)

URL: NVIDIA#1659
…A#1695)

Upgrade cmake version to include the PSLP objective offset fix

Authors:
  - Nicolas Blin (https://github.com/Kh4ster)

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

URL: NVIDIA#1695
NVIDIA#1698)

When the feasibility pump adds the objective cutting plane to the problem, `fj.solve()` resizes the constraint weights, but leave the new weights unintialized. This trips the assertion that checks for infinite (and a negative) weights in the GPU FJ. The solution is to set the new weights to `1.0`, which matches `copy_weights` and `cstr_weights` fixup in `do_fj_solve`. Note that `fj.solve()` bypasses `do_fj_solve`.

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

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

URL: NVIDIA#1698
Metric | Main | fix-papilo-feastol | Branch − main
-- | -- | -- | --
Feasible | 226.5 | 225.0 | −1.5
Optimal | 73.5 | 71.5 | −2.0
Average objective gap | 8.091% | 8.502% | +0.411 points
MIP gap, shifted geomean | 5.949% | 6.051% | +0.102 points
<0.1% objective gap | 132.0 | 135.5 | +3.5
Solutions failing cuOpt’s postsolve feasibility check | 2.5 | 2.0 | −0.5

I compared main against fix-papilo-feastol using two complete runs of all 240 MIPLIB instances. Each main/branch instance pair ran sequentially on the same H100, using the same physical node, GPU index, 28-thread CPU mask, and 600-second time limit.

Overall performance is similar. The branch finds ~3.5 more solutions within 0.1% of the best-known objective and reduces solutions failing our postsolve feasibility check `The solution is not feasible after post solve` from ~2.5 to ~2.0 per run.

The +0.411 average objective gap difference is driven almost entirely by one instance, `gfd-schedulen180f7d50m30k18`.
Main solves this instance consistently, while the branch sometimes solves it optimally and sometimes finds no incumbent.

I couldn't reproduce NVIDIA#1502 and LP should simply use PSLP presolve.

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

Approvers:
  - Nicolas Blin (https://github.com/Kh4ster)

URL: NVIDIA#1688
…1696)

## Summary

- `pr-test-summary`: removed from `pr-builder` needs and dropped `continue-on-error`; non-blocking via exclusion from the merge gate, labeled in the checks UI as non-blocking
- `multi-gpu-cpp-tests`: removed `continue-on-error`; now properly blocking through `pr-builder`
- `require-nvskills-ci`: labeled as non-blocking in the checks UI (already excluded from `pr-builder`)

## Testing
CI will validate on this PR.

## Docs
No documentation changes.

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

Approvers:
  - Bradley Dice (https://github.com/bdice)

URL: NVIDIA#1696
cuopt_solve_lp and cuopt_solve_milp took only a problem_path, so a caller
holding a model in memory had to serialise it to MPS first. For a small
solve that serialisation is the bulk of the work, and MPS carries a trap:
an integer column with no explicit bound entry silently defaults to [0, 1],
turning an ordinary model infeasible with no diagnostic.

Both tools now also accept `problem`, a JSON object of plain arrays, built
into a DataModel directly. Integrality travels as variable_types rather
than INTORG/INTEND markers, so bounds are left alone. submit() is unchanged
below the seam -- it took a model, not a path, all along. JSON submissions
write a names sidecar, so names_from=source labels a solution either way.

Three details that each cost a wrong answer while developing this:

- A finite -1e30 row bound makes the solver return a constraint-violating
  point reported as Optimal. Bounds at or beyond 1e30 are normalised to a
  true infinity, and null means unbounded, since JSON has no infinity
  literal. The underlying solver behaviour is worth fixing separately.
- COO cells naming the same entry are summed. Building a row incrementally
  emits duplicates naturally, and passing them through would leave the
  row's meaning dependent on how the backend treats repeated indices.
- The row count comes from the constraint bounds rather than the largest
  row index, so a trailing all-zero row survives and an out-of-range index
  is reported against the matrix instead of the bounds array.

Also fix nonzero_only, which used an exact != 0 test and so let first-order
numerical dust (around 1e-13, sometimes negative on a variable bounded
below by zero) through as though it were signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Trivial blank-line addition after SKILL.md frontmatter to trigger NVSkills CI — fetch updated skill card and benchmark for `cuopt-routing-api-python`.

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

Approvers:
  - Ishika Roy (https://github.com/Iroy30)

URL: NVIDIA#1676
Trivial blank-line addition after SKILL.md frontmatter to trigger NVSkills CI — fetch updated skill card and benchmark for `cuopt-user-rules`.

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

Approvers:
  - Ishika Roy (https://github.com/Iroy30)

URL: NVIDIA#1679
Adds two C API accessors so solver statistics are reachable from the C ABI, and closes NVIDIA#1202.

Split out of NVIDIA#1524 at @mlubin's request, so the C API can be reviewed without the Java diff attached. The Java bindings are the first consumer but nothing here refers to them.

### The gap

The C API reports the *outcome* of a solve — `cuOptGetTerminationStatus`, `cuOptGetObjectiveValue`, `cuOptGetMIPGap`, `cuOptGetSolutionBound`, `cuOptGetSolveTime`, and the primal/dual/reduced-cost arrays — but none of the diagnostics the C++ solution interfaces already carry. Of the eleven fields listed in NVIDIA#1202, **zero** are reachable today.

Python sidesteps this by binding to the C++ structs through Cython, so the C ABI is a second-class path and every non-Python binding hits the same wall.

Concretely, what these unlock:

- **Judging a non-optimal answer.** When a solve stops on a time limit, residuals and gap are the only way to distinguish a nearly-converged solution from a useless one.
- **Verifying a returned MIP solution.** The three violation magnitudes are what a caller checks before acting on a solution.
- **Knowing which algorithm solved it.** Under `CUOPT_METHOD_CONCURRENT` the caller otherwise cannot tell PDLP from dual simplex.
- **Performance work.** Iteration counts, nodes, simplex iterations, and presolve time.

### Why attributes rather than one getter per statistic

NVIDIA#1202 proposed eleven individual getters. This uses the attribute model instead, following review feedback from @chris-maes:

- it matches how problem data is already read (`cuOptGetProblemIntAttribute` and friends);
- a future statistic is a new constant rather than a new exported symbol, so existing callers need no relink;
- bindings that generate from `constants.h` — as the Java bindings already do for `CuOptConstants.java` — pick up new statistics with no hand-written code at all.

Structured data stays on dedicated functions, which is the existing convention: `cuOptGetConstraintMatrix` is a function because CSR is three parallel arrays, and the same will apply to the quadratic constraint rows in NVIDIA#1703. The rule is scalars and homogeneous arrays as attributes, ragged or multi-output as functions.

### Safety

Solution selectors are numbered in their own range (300+), so a problem selector passed to a solution accessor, or the reverse, is rejected rather than silently read. LP selectors require an LP solution and MIP selectors require a MIP solution, since the two come from different solvers; `CUOPT_ATTR_IS_MIP` on the originating problem says which set applies.

The values are read straight off `lp_solution_interface_t` / `mip_solution_interface_t`, so this is exposure only — no computation, no solve-time cost.

### Tests

`c_api.lp_solution_attributes` and `c_api.mip_solution_attributes` cover both solvers and the ways a caller can get it wrong: a float selector through the integer accessor and the reverse, the other solver's selectors, unknown selectors, and null arguments.

Float outputs are seeded with `NaN` rather than a numeric sentinel, since the solver cannot legitimately produce `NaN` — so an accessor that never writes its output is caught, where a numeric sentinel would be indistinguishable from a real result.

Verified locally: **69/69** `C_API_TEST` cases pass with the full LP/MIP/QP dataset.

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

Approvers:
  - Miles Lubin (https://github.com/mlubin)

URL: NVIDIA#1715
`get_lp_stats()` reported a spurious, large `l2_dual_residual` for QPs and SOCPs solved via the barrier path, even at a fully optimal solution. The final-solution dual residual computed in `iteration_data_t::to_solution()` (`cpp/src/barrier/barrier.cu`) omitted the `Q*x` quadratic term, computing only `‖Aᵀy + z − c‖` instead of `‖Aᵀy + z − c − Qx‖`.

Adds the missing `Q*x` term to the reported dual residual, mirroring the existing correct pattern used internally.

Also adds a regression test (`qp_diagonal_only_dual_residual`) that reuses the diagonal-Hessian QP problem from the linked issue and asserts `l2_dual_residual` is near zero at the known optimum.

## Issue
Closes NVIDIA#1598

Authors:
  - Yuwen Chen (https://github.com/yuwenchen95)

Approvers:
  - Miles Lubin (https://github.com/mlubin)
  - Chris Maes (https://github.com/chris-maes)

URL: NVIDIA#1718
…#1713)

Adds a new solver setting, `barrier_presolve_bound_free_variables`
(`CUOPT_BARRIER_PRESOLVE_BOUND_FREE_VARIABLES`), that controls whether barrier
presolve bounds free variables (`-1` automatic/current default behavior,
`0` disabled, `1` enabled). 

## Issue

Closes NVIDIA#1711

Authors:
  - Yuwen Chen (https://github.com/yuwenchen95)

Approvers:
  - Chris Maes (https://github.com/chris-maes)

URL: NVIDIA#1713
Prints the cumulative barrier setup time before the true barrier solve.

## Issue
Closes NVIDIA#1712

Authors:
  - Yuwen Chen (https://github.com/yuwenchen95)

Approvers:
  - Alice Boucher (https://github.com/aliceb-nv)
  - Chris Maes (https://github.com/chris-maes)

URL: NVIDIA#1714
NVIDIA#1668 added a new `pre-commit` hook that expects to be able to invoke `vale` (https://github.com/vale-cli/vale).

In CI, that tool's provided via the conda environment `pre-commit` runs in. Locally, I think it's unlikely that all contributors would have it installed (I don't).

This proposes making the `vale` check optional locally but requiring it in CI.

## Issue

N/A

Authors:
  - James Lamb (https://github.com/jameslamb)

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

URL: NVIDIA#1708
… it (NVIDIA#1723)

`subprocess.call` in the `cuopt_grpc_server` and `cuopt_cli` console-script wrappers leaves a Python parent that waits on the child but forwards no signals. Terminating the console script's pid kills only the wrapper — the gRPC server and its GPU workers survive, still bound to the listen port, and the shutdown path from NVIDIA#1603 that cancels jobs and reaps workers never runs. `execv` replaces the process image so signals reach the binary directly.

Verified against the packaged binary: before, `kill -TERM` on the wrapper left the server and worker orphaned with no shutdown lines logged; after, the full shutdown sequence runs and nothing survives.

Related: NVIDIA#1492 worked around the same orphaned-worker symptom in the test harness via `setpgid` + group-kill (test-only, by design). This addresses the root cause for the packaged install path.

No test added — `python/libcuopt` only has `test_cli.sh`, which covers CLI output rather than signal behaviour; happy to add coverage wherever you think it belongs.

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

Approvers:
  - Ishika Roy (https://github.com/Iroy30)

URL: NVIDIA#1723
Every other tool needs a model or a job_id, so a caller could only discover
the gRPC target by submitting work and reading the failure -- by which point
a model has already been built, and the error reads as "no server anywhere".
cuopt_health takes no arguments and reports host, port, tls and reachability.
The service exposes no health RPC, so reachability is probed with the
cheapest call that still needs a server to answer: a status lookup for a job
id that cannot exist, which must come back NOT_FOUND.

The unreachable message now says to check for a running server before
starting one. Told only to start one, a caller that already has a server up
elsewhere starts a second, and two servers can share a listen port.

cuopt_solve_milp carries the problem key list instead of pointing at
cuopt_solve_lp; under deferred tool loading a caller can hold the MILP tool
alone and cannot follow that pointer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
cuopt_mcp used plain setuptools with no rapids-build-backend, so
depends_on_cuopt was emitted literally as "cuopt==26.10.*" -- a name no
CUDA-suffixed build publishes, leaving `pip install python/cuopt_mcp`
unsatisfiable in any real environment. It now carries the same
rapids-build-backend treatment as cuopt_server and resolves cuopt-cu13.

disable-cuda is deliberately not set: it would drop the suffix from this
package's name and from that dependency alike, reinstating the unresolvable
requirement. The suffix is inherited from the dependency, not from anything
this pure-Python package compiles. A comment records that so it is not
re-derived.

Also adds cuopt_mcp as a build.sh target so it installs through the same
--no-build-isolation --no-deps path as its siblings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
gRPC enables SO_REUSEPORT by default, so a second cuopt_grpc_server started
against a port that is already served binds successfully and nothing reports
it. The kernel then splits connections between two processes, each with its
own workers and RMM pool, so a job submitted to one can be polled from the
other. The existing "Failed to bind" path never fired.

Off by default; --allow-reuseport restores the old behaviour for a
deliberate multi-process pool behind one port. Because SO_REUSEPORT must be
set on every socket sharing a port, joining a pool now takes an explicit
choice on both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

Adds a Testing section covering the source build, a health smoke test, the
unit and end-to-end suites, and driving the server from an MCP client.

Calls out two things that cost real debugging time: the end-to-end fixture
launches cuopt-mcp from PATH rather than from the interpreter running pytest,
so a shadowing environment is exercised silently and surfaces as an unrelated
ABI error; and an unreachable backend does not imply no server is running, so
check before starting a second one that can share the listen port.

Also lists cuopt_health in the tools table and notes the package is not
published yet, so the first instruction in Install does not simply fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants