cuopt_mcp: add cuopt_health, package it like the other Python modules - #1819
Draft
ramakrishnap-nv wants to merge 20 commits into
Draft
cuopt_mcp: add cuopt_health, package it like the other Python modules#1819ramakrishnap-nv wants to merge 20 commits into
ramakrishnap-nv wants to merge 20 commits into
Conversation
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
…1707) ## Issue Authors: - Alice Boucher (https://github.com/aliceb-nv) Approvers: - Rajesh Gandham (https://github.com/rg20) URL: NVIDIA#1707
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>
|
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>
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.
Stacked on #1701 (base is
grpc-codegen-settings-descriptions, notmain) so the diff here is only the MCP work —cuopt_mcpitself lands with that branch.Three commits:
cuopt_health— no-arg tool reporting host, port, tls and reachability. Every other tool needs a model or ajob_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 returnNOT_FOUND. The unreachable message now says to look for a running server before starting one, andcuopt_solve_milpcarries the problem key list rather than pointing atcuopt_solve_lp(a caller can hold the MILP tool alone and cannot follow that pointer).cuopt_mcpused plain setuptools, sodepends_on_cuoptwas emitted as a literalcuopt==26.10.*, which no CUDA-suffixed build publishes;pip install python/cuopt_mcpcould not resolve. It now gets the same rapids-build-backend treatment ascuopt_serverand builds ascuopt_mcp-cu13, plus abuild.sh cuopt_mcptarget.--allow-reuseport— separable from the rest; happy to pull it out. gRPC enablesSO_REUSEPORTby default, so a secondcuopt_grpc_serveron an already-served port binds silently and the kernel splits connections between two processes, each with its own workers and RMM pool. The existingFailed to bindpath never fired.Trying it
Point an MCP client at
cuopt-mcpwithCUOPT_REMOTE_HOST/CUOPT_REMOTE_PORT, then callcuopt_healthfirst. 63 tests pass locally (CUOPT_TEST_GRPC_PORT=50051 pytest python/cuopt_mcp/tests); the end-to-end fixture resolvescuopt-mcpfromPATH, so make sure the intended env is first or it will silently exercise a different install.Known gaps
pr.yamljobs, so nothing runs these tests automatically. Deliberately deferred until [FEA] Decouple cuopt_mcp from the cuopt package so it ships as a pure-Python wheel #1755 settles, since decoupling would replace the CUDA-suffixed wheel matrix with a singlepy3-none-anywheel and change what needs wiring.cuopt-cu13and the CUDA runtime onto a machine that only needs a socket, which contradicts the "host needs no GPU" design inserver.py. That is [FEA] Decouple cuopt_mcp from the cuopt package so it ships as a pure-Python wheel #1755.