Triton
Dynamo Triton integrates NVIDIA Triton Inference Server into Dynamo’s distributed runtime. Triton models — across its full backend ecosystem (TensorRT, ONNX Runtime, PyTorch, Python, DALI, …) — are served through Dynamo’s frontend over the KServe gRPC protocol, gaining Dynamo’s service discovery and routing.
The runtime is composed exactly like the vLLM / SGLang / TRT-LLM runtimes: the Dynamo wheels are installed on top of the upstream Triton release image (nvcr.io/nvidia/tritonserver:<tag>).
That image is the build’s RUNTIME_IMAGE, so the result is a single “Dynamo + Triton” artifact.
Feature Support Matrix
Core Dynamo Features
Known limitations
The KServe gRPC tensor path is still being completed. The Triton and KServe features below are not yet supported end-to-end through the Dynamo Frontend.
Discovery backend at scale.
The file discovery backend suits small repositories and the examples below for simple quickstarts.
A worker that registers a large repository (hundreds of models) in a single burst overflows the file watcher’s channels and can tear the frontend down before it reports ready.
Use the etcd discovery backend (--discovery-backend=etcd, with etcd running) for large repositories on baremetal. On Kubernetes, native k8s discovery will be used by default.
Container / driver matrix
The Triton release is pinned in container/context.yaml under triton.cuda13.4 (runtime_image_tag/base_image_tag), the same way the other framework runtimes pin their image.
Prerequisites
- An NVIDIA GPU with a driver matching the Triton release in the table above.
- Docker with the NVIDIA Container Toolkit.
Quick Start (prebuilt / release container)
Step 1 — build the release image (source-built Dynamo wheels on the latest Triton release):
The ai-dynamo wheels are built from source and installed on top of the Triton release image, so the running Dynamo matches this repository.
--network=host is required at build time to fetch their Python dependencies from PyPI.
Step 2 — run the container:
Add -v /path/to/models:/models to mount your own model repository.
Step 3 — start the frontend and worker (inside the container):
Mirroring the other Dynamo runtimes, run the frontend and the Triton worker as two processes that find each other over file-based discovery (no etcd/NATS required).
Triton serves tensor models, so the frontend exposes the KServe gRPC endpoint (--kserve-grpc-server) rather than the OpenAI HTTP API:
The worker exposes the full triton_runtime.Options surface as flags, named to match Triton’s tritonserver CLI — run python3 -m dynamo.triton --help for the complete list.
The in-process Triton server finds its backends via the image’s LD_LIBRARY_PATH and the worker’s default backend directory (/opt/tritonserver/backends), so no extra environment setup is needed.
Step 4 — send a request:
Tensor models use the KServe gRPC protocol, so requests go through the bundled gRPC client rather than curl.
Point the client at the frontend’s port — client.py defaults to 8787, while the frontend above listens on 8001, matching the tritonserver convention:
Worker configuration
The worker exposes the full triton_runtime.Options surface as CLI flags, named to match Triton’s tritonserver CLI.
Run python3 -m dynamo.triton --help for the complete list.
Common flags:
Environment variables
Adding your own models
-
Create a model directory in your repository:
-
Define the model config (
config.pbtxt): -
Launch the worker against your repository. With the default
NONEmode it serves every model in the repository (--backend-configmatches Triton’s syntax):
Serving /v1/classify (OpenAI Classify)
Start the worker with --task classify to expose a Triton classifier through the OpenAI-compatible POST /v1/classify endpoint on the Dynamo Frontend, instead of the KServe tensor path.
The frontend translates the classify JSON into a Dynamo tensor request, this worker forwards it to Triton as a single BYTES text input, and reads a single FP32 probability output back — no HuggingFace tokenizer or config.json fetch is required.
Expected Model Shape
The worker auto-detects the input/output tensor names from config.pbtxt.
Any Triton model — leaf plan or ensemble — that declares exactly one TYPE_STRING input and exactly one TYPE_FP32 output serves classify unchanged.
The STRING input must declare dims: [ 1 ] or dims: [ -1 ] (one string per request item); other layouts are rejected at startup.
An ensemble that runs tokenization inside Triton (Python-backend tokenizer plus a TensorRT plan wired through ensemble_scheduling) is the intended shape:
When the model has more than one BYTES input or FP32 output, disambiguate with --classify-input-name / --classify-output-name.
Class labels come from Triton’s own label_filename on the output when set; when absent, response label fields are null.
Launching the Worker and Issuing a Request
The response shape mirrors vLLM’s /classify:
Notes and Limits
- Text input only (
"input": "..."or"input": ["..."]); token-ID variants (Tokens/TokenBatch) return HTTP 400. - Pooling (
/v1/pooling) is not yet served by this worker; requests withencoding_formatset are rejected. use_activation,add_special_tokens,truncate_prompt_tokens, andtruncation_sideare rejected with HTTP 400. The Triton model plan owns tokenization and the classify head’s activation, so this worker can neither apply nor skip them; a silent no-op would let clients relying on those semantics get a different classification per backend.- To serve the same underlying Triton model on both
/v2/models/{name}/infer(tensor) and/v1/classify, run two worker processes against the same--model-repository— one with the default--task=tensorand one with--task=classify. - Model aliases (multiple served names for one card) are not honored on this path.
Serving /v1/embeddings (OpenAI Embeddings)
Start the worker with --task embeddings to expose a Triton embedding model through the OpenAI-compatible POST /v1/embeddings endpoint on the Dynamo Frontend, instead of the KServe tensor path.
The frontend translates the embeddings JSON into a Dynamo tensor request, this worker forwards it to Triton as a single BYTES text input, and reads a single FP32 vector output back, so no HuggingFace tokenizer or config.json fetch is required.
Expected Model Shape
The worker auto-detects the input/output tensor names from config.pbtxt.
Any Triton model (leaf plan or ensemble) that declares exactly one TYPE_STRING input and exactly one TYPE_FP32 output serves embeddings unchanged.
The STRING input must declare dims: [ 1 ] or dims: [ -1 ] (one string per request item); other layouts are rejected at startup.
An ensemble that runs tokenization inside Triton (Python-backend tokenizer plus a TensorRT plan wired through ensemble_scheduling) is the intended shape:
When the model has more than one BYTES input or FP32 output, disambiguate with --embeddings-input-name / --embeddings-output-name.
The output vector length is whatever the Triton model’s FP32 output declares; the worker does not reshape or truncate it.
Model Repository Layout
A single worker process serves one task.
--task embeddings registers every user-facing model in --model-repository as an embedding, so every model in the repo must be shaped STRING-in / FP32-out. A classifier or raw-tensor model mixed into the same repo makes startup fail with the mismatched model’s name in the error.
Ensemble dependency models (tokenizer, encoder, anything referenced from another model’s ensemble_scheduling.step[].model_name) are the exception: they stay loaded in Triton so their parent ensemble can call them, but they are not registered as /v1/embeddings endpoints, so an ensemble + its dependencies are the intended repo shape.
To serve multiple tasks from the same underlying models, run one worker process per task against repositories dedicated to that task.
Launching the Worker and Issuing a Request
The response shape mirrors OpenAI’s /v1/embeddings:
The worker emits every vector as base64-encoded little-endian FP32 bytes on the Dynamo wire; the frontend decodes to a float array at the HTTP boundary when the client omits encoding_format or sets it to "float", and forwards the base64 string unchanged when the client sets encoding_format: "base64".
Notes and Limits
- Text input only (
"input": "..."or"input": ["..."]); token-ID variants (Tokens/TokenBatch) return HTTP 400. dimensions(Matryoshka truncation),add_special_tokens, andtruncate_prompt_tokensare rejected with HTTP 400. The Triton model plan owns tokenization and the output dimension is fixed by the model; a silent no-op would let clients relying on those semantics get a different embedding per backend.encoding_formatonly accepts"float"(default) and"base64"; other values return HTTP 400.- To serve the same underlying Triton model on both
/v2/models/{name}/infer(tensor) and/v1/embeddings, run two worker processes against the same--model-repository: one with the default--task=tensorand one with--task=embeddings. - Model aliases (multiple served names for one card) are not honored on this path.
Configuring the Triton version
The Triton release is pinned by triton.cuda13.4.runtime_image_tag in container/context.yaml (default 26.09-py3), mirroring how the other framework runtimes pin their image.
The CUDA family is fixed by the Triton release, so --cuda-version is auto-derived; passing it explicitly is rejected.
To build into a different Triton release, override RUNTIME_IMAGE_TAG at build time (no context.yaml edit needed). Keep BASE_IMAGE_TAG on CUDA 13.1 (default 26.02-cuda13.1-devel-ubuntu24.04):
the Rust cudarc crate rejects other CUDA versions at build time, and the wheels load CUDA dynamically so they run on the 13.4 runtime image regardless:
To make a release the default, edit triton.cuda13.4 in container/context.yaml.
Metrics
Triton metric collection is enabled by default (--allow-metrics).
The worker serves Triton’s native nv_* metrics from Dynamo’s /metrics endpoint alongside the dynamo_* metrics; set DYN_SYSTEM_PORT to expose that endpoint:
The nv_* metrics are collected on each scrape of /metrics.
Pass --allow-metrics=false to stop collection and exclude them from the endpoint.
Troubleshooting
”Model not found” error
- Verify the model exists in
/models/<model_name>/. - Check that
config.pbtxtis valid. - Ensure the backend is available in the backend directory (
/opt/tritonserver/backends).
Worker fails to start
- Verify GPU is available:
nvidia-smi. - Confirm the model repository path is mounted into the container.
- Increase log verbosity:
--log-verbose=1(or higher for more detail).
How it works
The worker (components/src/dynamo/triton/main.py):
- Starts an in-process server object
- Loads all models from the model repository
- Reads each model’s configuration (
config.pbtxt) - Registers each model with the Dynamo runtime, keyed on
--task:tensor(default):ModelInput.Tensor/ModelType.TensorBased— Dynamo Frontend hosted KServe gRPC endpoint. Requests are translated to Triton inference requests and streamed back as Dynamo tensors.classify:ModelInput.Text/ModelType.Classify— Dynamo Frontend hostedPOST /v1/classify. Requests are translated to a single BYTES text input and the FP32 probability output is wrapped in the OpenAI classify response shape.embed:ModelInput.Text/ModelType.Embedding: Dynamo Frontend hostedPOST /v1/embeddings. Requests are translated to a single BYTES text input and the FP32 vector output is base64-encoded on the wire, with the frontend decoding to a float array at the HTTP boundary when the client asks for float.
- Each model is served on its own endpoint (
triton.<model_name>), so a single worker can serve multiple models routed by name.