Skip to content

Triton on Ray: Ray Serve's gRPC proxy in front of Triton - #212

Merged
kondratyevd merged 9 commits into
mainfrom
sonic-ray-serve
Sep 7, 2026
Merged

Triton on Ray: Ray Serve's gRPC proxy in front of Triton#212
kondratyevd merged 9 commits into
mainfrom
sonic-ray-serve

Conversation

@kondratyevd

@kondratyevd kondratyevd commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What

Triton on Ray, with Ray Serve's gRPC proxy carrying Triton's protocol: every worker pod runs NVIDIA's Triton Inference Server, pointed at the models CMSSW ships on CVMFS (four --model-repository directories inside CMSSW_17_0_0_pre2, an explicit list of ten models, the cluster's read-only cvmfs claim), beside a Ray container; Serve's gRPC proxy is handed Triton's own generated servicer, so it accepts exactly Triton's RPCs, counts each one, load-balances it, and forwards it to the Triton in the chosen pod. Ray Serve sizes the deployment from those requests and the Ray autoscaler adds a GPU pod for each replica with nowhere to go. Triton does all of the inference.

clients ──▶ sonic-ray-serve (LoadBalancer, geddes-private-pool) :8001  Triton gRPC
              ▼  Serve gRPC proxy — counted, balanced, autoscaled
   worker pod × 1…4:  ray-worker (raylet `triton: 1`, proxy, forwarder replica)
                      triton     (1 GPU, 4 CPU, 16G, /cvmfs ro)    ◀ localhost:8001 (HTTP moved to 8100)
   head pod:          Serve controller + Ray autoscaler (no GPU, 0 CPUs for work)
  • All of Triton, none of ours. The ten CMSSW models (deepmet, deeptau_2018v2p5, six ParticleNet variants, particlenet_PT, unifiedparticletransformer_AK4_V01) straight from CVMFS — nothing uploaded, no model manager; a new CMSSW release is a path change in the values. Every backend, config.pbtxt semantics, dynamic batching, repository index — it is Triton. CMSSW's TritonClient and tritonclient.grpc point at sonic-ray-serve:8001 like at any Triton endpoint. The one RPC not forwarded is ModelStreamInfer (bidirectional; Serve's proxy carries unary and server-streaming only; CMSSW uses unary ModelInfer).
  • The glue is ~100 lines, apps/ray/sonic-ray/chart/files/sonic_ray/serve_app.py: a Serve deployment that, for every unary RPC of GRPCInferenceService (derived from the generated servicer at import), forwards the protobuf message to Triton on localhost and returns Triton's answer. It blocks on ServerReady before becoming ready and polls ServerLive as its health check. Nothing in it parses a request.
  • One replica per Triton pod. Each worker advertises a triton: 1 resource; each replica claims one. That pins a replica next to its Triton, leaves a pod without a replica idle (reclaimable), and makes a replica without a pod the pending request that grows the group. One setting, replicas: {min: 1, max: 4}, bounds Serve and the worker group alike (target_ongoing_requests: 16; the group's own floor is 0 since Serve's minimum keeps pods alive); the chart refuses to render if a pod has other than one GPU or if the grace period would cut Triton's drain short.
  • No custom image. Official Ray (2.52.0-py312-cpu, no Ray process touches a GPU) via the Docker Hub proxy cache, official Triton (26.04-py3, the tag supersonic runs). Triton's Python stubs (tritonclient==2.48.0, the last release whose protobuf-4 stubs match the Ray image, --no-deps) are pip-installed into an emptyDir on PYTHONPATH by an init container on every pod — Serve's proxies import the servicer outside any runtime_env, so replicas-only installation would not do.
  • Ports. Both containers share the pod's network namespace, so Triton's HTTP moves to 8100: Ray Serve's HTTP proxy holds 8000 and KubeRay probes it there, and Triton otherwise exits with Socket '0.0.0.0:8000' already in use. Triton's gRPC keeps 8001 (what the forwarder dials). Both are chart values, and the chart refuses to render if they collide with Ray's ports or disagree with triton.args.
  • No KEDA, no Prometheus in the loop. nv_* and ray_serve_* metrics are exposed for the AF Prometheus under release="sonic-ray".

Layout

path what
apps/ray/operator/ kuberay-operator 1.7.0, namespaced to cms
apps/ray/sonic-ray/chart/ RayService (Triton sidecar per worker, Serve gRPC proxy with Triton's servicer, pip init container), the ConfigMap carrying the forwarder, two metrics Services; refusals for values that cannot work
apps/ray/sonic-ray/values.yaml the AF release: Triton image/args/resources, the CVMFS model repositories and load list, the CMS GPU nodes, the private address pool
tests/sonic_ray/ source-level checks of the forwarder (every RPC but the stream, readiness gated on Triton, no request parsing)
tests/manifests/test_ray.py rendered chart: the Triton container is the one in the values, models from the read-only cvmfs claim with host-to-container propagation, one replica per pod, Serve ≤ worker bounds, stubs installed where the proxies run, chart refusals

Also: validate-manifests.sh now helm templates charts sourced from this repository (the RayService kind has no kubeconform schema), README badges for the two new components, mypy.ini scope.

Verified

  • pytest tests green; validate-manifests.sh green; ruff/mypy/prettier clean.
  • Not run on the cluster (no VPN from here). The first deploy has to confirm: the init container's pip from the GPU nodes, the CVMFS mount inside the Triton container (first load pulls the models over the network; the startup probe allows four minutes), Serve's gRPC proxy loading tritonclient's servicer against the image's grpcio 1.74 / protobuf 4.25, a CMSSW client end to end, autoscaling, and the LoadBalancer allocation. Nothing needs to be built first — merging is enough for Flux to reconcile it.

History of this PR

Three earlier shapes were tried and dropped in review: a custom Ray image with ONNX Runtime and a KServe v2 HTTP server; the same on the stock image; then plain-JSON endpoints. Each served fewer models or fewer clients than Triton already does. This shape keeps Triton and reduces our part to the forwarder.

Follow-ups (deliberately out of scope)

  1. HTTP through Serve, if HTTP clients turn up: a second forwarder for Triton's HTTP, or Serve's HTTP proxy passing bytes to Triton's port.
  2. A Grafana panel on ray_serve_* next to the SuperSONIC nv_* dashboards.
  3. Retuning target_ongoing_requests against real CMSSW load once it is measured.

🤖 Generated with Claude Code

A minimal Ray deployment for GPU inference that scales to more GPUs on
demand: Ray Serve + ONNX Runtime behind the KServe v2 HTTP API, on KubeRay,
beside the supersonic release and on its model repository claim.

One Serve deployment, SonicServer, loads every onnxruntime_onnx model of the
repository onto one GPU — a replica is a server, and a pod (one GPU) is a
replica. Ray Serve adds replicas from in-flight requests; the Ray autoscaler
adds a GPU pod for each replica with nowhere to run. No KEDA, no Prometheus
in the loop. The TensorFlow models (deepmet, deeptau) are listed UNAVAILABLE
with the reason rather than served.

The protocol includes the binary tensor extension, so tritonclient.http
works unchanged; gRPC (what CMSSW speaks) is not here yet.

- docker/sonic-ray: rayproject/ray 2.52.0 cu128 + onnxruntime-gpu 1.26.0
  (last CUDA 12 build, held by Renovate) + the sonic_ray package; built and
  published by ci.yml like the other aux images, build fails if the CUDA
  provider's libraries do not resolve on the image
- apps/ray: kuberay-operator 1.7.0 (namespaced), the sonic-ray chart
  (RayService + metrics Service) and the AF release; the chart refuses
  values that cannot work (Serve replicas > GPU pods, pods with != 1 GPU,
  grace period <= Serve drain)
- validate-manifests.sh renders charts sourced from this repository
- tests/sonic_ray (server, CPU onnxruntime, models built in-test) and
  tests/manifests/test_ray.py (rendered chart, parity with supersonic)

Not run on the cluster: GPU inference, autoscaling and the LoadBalancer
allocation are for the first deploy to confirm.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

kondratyevd and others added 3 commits September 5, 2026 13:28
…e_env

Dmitry's call: serving some of the CMS models without maintaining a Ray
image beats serving them all with one. So docker/sonic-ray is gone, along
with its CI matrix entry, image badge and Renovate pin.

The pods now run rayproject/ray:2.52.0-py312-cu128 as published (ray[all],
CUDA 12.8, cuDNN 9) through the geddes Docker Hub proxy cache. The sonic_ray
package lives in the chart's files/ and is rendered into a ConfigMap mounted
on PYTHONPATH on head and workers, its hash annotated onto both pod templates
so a code change rolls the cluster. onnxruntime-gpu==1.26.0 (the last CUDA 12
build) goes into the Serve application's runtime_env; Ray installs it once
per pod when the first replica starts there. The chart refuses to render
without the pin.

The price is a minute or two per scale-up and PyPI reachability from the GPU
nodes; the README says so. Tests follow the code to its new home and check
the ConfigMap, the mount, the image tag derived from ray.version, and the pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dmitry's call: start without the Triton protocol and add it later. That
removes the KServe v2 wire format (JSON + binary tensor extension) and the
config.pbtxt parser — about two thirds of the server.

What is left is models.py (find every <model>/<version>/model.onnx in the
repository, load it with ONNX Runtime, run it on named arrays, report the
tensors ORT itself declares) and serve_app.py (the Ray Serve deployment with
GET /healthz, GET /models, GET /models/{name}, POST /models/{name}). Inputs
are cast to the model's dtypes; a bad name, rank or shape is a 400 with the
reason; a directory without an ONNX model is listed under "skipped" with the
reason instead of 404'd.

No existing SONIC client speaks this yet — the README says so and names the
path back: KServe v2 HTTP first, then gRPC for CMSSW. The model allowlist
went with the rest; the chart, the tests and the docs follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dmitry's spec: Triton as the actual server, gRPC through Ray's plumbing so
Serve balances and autoscales it, and the least new code that gets there.

Every worker pod now runs supersonic's Triton (same image, arguments,
resources, model repository claim — the parity tests from the first attempt
return) beside a Ray container. Serve's gRPC proxy is handed Triton's own
generated servicer (tritonclient.grpc.service_pb2_grpc), so it accepts
exactly Triton's RPCs; our code is a ~100-line forwarder that passes each
unary RPC to the Triton on localhost and returns its answer, becomes ready
only when Triton answers ServerReady, and polls ServerLive as its health
check. ModelStreamInfer, the one bidirectional stream, is the only RPC not
carried. Nothing of ours parses a request; the ONNX Runtime server and its
tests are gone.

One replica per pod: workers advertise a `triton` resource, replicas claim
it. The serve Service exposes the gRPC proxy on Triton's conventional 8001,
so CMSSW clients point at it as they point at supersonic's Envoy.

Still no custom image: official Ray (CPU flavour — no Ray process touches a
GPU) and official Triton. tritonclient 2.48.0 — the last release whose
protobuf-4 stubs match the Ray image — is pip-installed --no-deps into an
emptyDir on PYTHONPATH by an init container on every pod, because Serve's
proxies import the servicer outside any runtime_env.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kondratyevd kondratyevd changed the title Serve the SuperSONIC model repository with Ray Serve Triton on Ray: Serve's gRPC proxy in front of SuperSONIC's Triton Sep 5, 2026
kondratyevd and others added 2 commits September 5, 2026 14:30
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server in every worker pod is NVIDIA's Triton Inference Server,
configured as in the AF's supersonic release; SuperSONIC is a deployment of
Triton, not an owner of it. Docs, comments and test docstrings said
"SuperSONIC's Triton" — they now say what is meant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kondratyevd kondratyevd changed the title Triton on Ray: Serve's gRPC proxy in front of SuperSONIC's Triton Triton on Ray: Ray Serve's gRPC proxy in front of Triton Sep 5, 2026
kondratyevd and others added 3 commits September 5, 2026 17:47
Chart, forwarder, values comments and tests no longer describe this release
in terms of the supersonic one. The parity tests that compared the Triton
block against apps/sonic/supersonic/values.yaml are gone with them — the
two releases are independent deployments of Triton, and one should not turn
the other's CI red. What remains is the CMS model repository claim name,
which is the existing PVC's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dmitry: no model manager uploads; the models CMSSW ships, as the existing
CVMFS-backed SONIC deployment serves them. The AF values now mount the
cluster's read-only cvmfs claim (HostToContainer, the repositories are autofs
mounts on the node) and point Triton at four --model-repository directories
inside CMSSW_17_0_0_pre2 with an explicit load list of ten models, the same
image, arguments and resources that deployment uses. The chart grew one
optional knob, modelRepository.mountPropagation. A new CMSSW release is a
path change in the values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…both

Dmitry asked why the values carried two min/max pairs. They were Serve's
(1–4 replicas) and the worker group's (0–4 pods), coupled one to one by the
triton resource and policed by a validation rule. Now `replicas: {min, max}`
is the only pair: it becomes Serve's autoscaling bounds and the group's
ceiling; the group starts with `min` pods and its own floor is always 0,
since a pod with a replica on it is never idle and Serve's minimum therefore
keeps pods alive. The cross-check rule and its test go away.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kondratyevd
kondratyevd merged commit af7db96 into main Sep 7, 2026
18 checks passed
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.

1 participant