Dynamo Deployment#
Run an NVIDIA Dynamo frontend with Cosmos3 Reasoner workers on Kubernetes. This
example uses a pinned Dynamo frontend image and a Cosmos3 NIM worker image.
It launches python3 -m dynamo.frontend for Chat Completions, with
round-robin routing and session affinity. It starts
two Nano FP8 workers, each on one GPU, with persistent video sessions disabled.
For those sessions, use Dynamo With Streaming.
For required worker environment variables, discovery, and proxy settings, refer to Dynamo Configuration, including running NIM as a Dynamo worker.
The manifests use the tested operator’s nvidia.com/v1alpha1 API and
spec.services schema (componentType, extraPodSpec.mainContainer, and
service-level resources). They cannot be applied unchanged to an operator
that serves only the v1beta1 spec.components schema. Helm is not needed,
but the Dynamo operator is still required.
Replace values in <angle-brackets> with your own values before applying.
The workers and model-download.yaml use the same NIM
image, nvcr.io/nim/nvidia/cosmos3:2.1.0. The regular frontend stays pinned
to nvcr.io/nvidia/ai-dynamo/dynamo-frontend-nightly:20260906-c70f1c7.
The tested NIM image bundled ai-dynamo==1.5.0.dev20260902, with the operator’s
canonical runtimeVersionOverride: "1.5.0". Verify compatibility among the
frontend, worker image, and operator when changing versions.
This setup is validated for Chat Completions. Validate streaming sessions, Responses, and the other cookbook clients separately on your deployment.
Prerequisites#
Before you deploy, make sure you have the following:
A Kubernetes cluster with NVIDIA GPU support and a compatible Dynamo operator/platform serving the
v1alpha1DynamoGraphDeploymentschema above. Check withkubectl get crd dynamographdeployments.nvidia.com -o yamlandkubectl explain dynamographdeployment.spec.services --api-version=nvidia.com/v1alpha1. The operator must provide Kubernetes discovery permissions and inject worker system-port health probes (/liveand/healthon port 9090).Capacity for two workers: each requests one GPU compatible with the image’s Nano FP8 profile, 32 CPUs, and 96 GiB of pod memory (128 GiB limit). The frontend requests another 4 CPUs and 8 GiB (limits: 8 CPUs and 16 GiB). These are deployment resource settings, not profile memory floors. Verify each GPU and effective host/container RAM against the Support Matrix. Do not add memory across GPUs.
A ReadWriteMany (RWX) storage class and a 200 GiB model cache writable by the image’s user with group 1000. All workers and the download Job must be able to mount it. Workers must be able to download model artifacts if needed.
Capacity for the download Job before workers start: one compatible GPU, 4 CPUs and 16 GiB requested (limits: 8 CPUs and 32 GiB). This Job runs image-specific profile selection and downloads artifacts without serving. Its memory limit is not evidence that a serving worker fits that limit.
kubectl, cURL, Python 3, and an exportedNGC_API_KEYwith image and model access. Refer to Network and NGC Access.
Both services and the download Job tolerate the nvidia.com/gpu taint and the
kubernetes.io/arch=arm64:NoSchedule taint. They leave node architecture unset,
allowing placement on compatible AMD64 or ARM64 nodes. Tolerations permit
scheduling, but they do not select an architecture or establish image/GPU
compatibility. Choose an architecture supported by your image and GPU profile,
and add matching nodeSelector/tolerations to both services and the download
Job if your cluster uses other constraints. Use your own node labels to
constrain placement on mixed-GPU clusters.
The example pins Nano FP8 to match the tested deployment. Keep the image,
NIM_MODEL_VARIANT, NIM_PRECISION, and NIM_TAGS_SELECTOR aligned in the Job
and workers when choosing another profile. Confirm image support during profile
selection. For unified-memory hosts, follow the
Reasoner memory-share guidance
in the Job and workers before starting either. Do not assume these
discrete-GPU pod settings apply unchanged.
To match the tested configuration, workers set NIM_ENABLE_WARMUP=0 and disable
text, video, and SigLIP guardrails explicitly. Review those policy choices
before use. To enable the checks, remove those guardrail overrides or set them
to 1. Disabled warmup means Ready does not establish representative inference
has completed. Do not copy a cold-start duration as an expected startup time.
Prepare the Namespace and Cache#
Run commands from .agents/skills/cookbooks/cosmos3/nim/dynamo, with the intended Kubernetes
context selected. Set your namespace (also export it in terminals used below).
Create it if needed, then create the two secrets referenced by the manifests.
Add namespace labels only if required by your cluster policy, using your own values:
export NAMESPACE="<your-namespace>"
kubectl create namespace "$NAMESPACE"
kubectl -n "$NAMESPACE" create secret docker-registry ngc-registry-secret \
--docker-server=nvcr.io --docker-username='$oauthtoken' \
--docker-password="$NGC_API_KEY"
kubectl -n "$NAMESPACE" create secret generic ngc-api-secret \
--from-literal=NGC_API_KEY="$NGC_API_KEY"
Reuse these resources if they already exist. In
model-cache.yaml, replace
<your-rwx-storage-class> with your cluster’s RWX storage class, then apply it:
kubectl -n "$NAMESPACE" apply -f model-cache.yaml
If you already have a suitable cache PVC, skip its creation.
For the v1alpha1 graph manifests, also replace spec.pvcs[0].name and
ReasonerWorker.volumeMounts[0].name with that claim name, and keep
create: false. Set the download Job’s
volumes[].persistentVolumeClaim.claimName to the same claim. The cache
mounts at /opt/nim/.cache without a subdirectory. A storage class using
WaitForFirstConsumer can leave the PVC Pending until the download Job or
workers are scheduled.
Download Model Artifacts#
Keep the image and profile selectors in
model-download.yaml matched to the workers, then run:
kubectl -n "$NAMESPACE" apply -f model-download.yaml
kubectl -n "$NAMESPACE" logs -f job/cosmos3-reasoner-model-download
kubectl -n "$NAMESPACE" wait --for=condition=Complete \
job/cosmos3-reasoner-model-download --timeout=3600s
The Job first runs profile_selection.startup, sources the selected profile,
and invokes the image’s nimutils.download_models helper. Selection success
establishes candidate-profile compatibility. Serving still requires a cold
start and representative requests on every participating device. The one-hour
wait is an operator wait ceiling, not an expected download duration. Inspect
Job status and logs if it fails or times out. Do not deploy workers until it
completes. The Job reserves one GPU while running and releases it on
completion. Completed Job objects expire after one day, but cached artifacts
remain on the PVC.
Deploy#
kubectl -n "$NAMESPACE" apply -f reasoner.yaml
kubectl -n "$NAMESPACE" get pods \
-l nvidia.com/dynamo-graph-deployment-name=cosmos3-reasoner -w
Check the Deployment#
Wait for the frontend and worker pods to become Ready, then stop the watch
with Ctrl+C. Cold startup still includes model loading and can download any
missing artifacts.
The worker deliberately leaves system-port probes and DYN_SYSTEM_PORT to the
operator. Do not replace them with NIM HTTP liveness probes. Inspect the
generated pods to confirm the operator injected port 9090 and its health
probes. The operator creates the frontend Service. Forward it in a separate
terminal:
kubectl -n "$NAMESPACE" port-forward service/cosmos3-reasoner-frontend 8000:8000
Check registration and discover the served model:
export NIM_URL=http://localhost:8000
curl -fsS "$NIM_URL/health" | python3 -m json.tool
curl -fsS "$NIM_URL/v1/models" | python3 -m json.tool
Before sending requests, confirm /health lists a distinct generate
instance for each worker in this deployment and /v1/models is nonempty.
/health can also list other deployments’ workers. Match the instance’s
namespace and transport address to your worker pods. Frontend health alone
does not establish worker readiness. Workers expose NIM’s
/v1/health/ready, /v1/metadata, and /v1/metrics on their own port 8000.
To inspect those endpoints, port-forward a worker pod to a different local
port.
Check each worker’s readiness and match selectedModelProfileId from metadata
to /v1/manifest, using inspect_profile.py.
Send a Request#
The frontend serves /v1/chat/completions. Discover the model ID rather than
assuming it matches the Kubernetes resource name:
MODEL=$(curl -fsS "$NIM_URL/v1/models" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["data"][0]["id"])')
curl -fsS "$NIM_URL/v1/chat/completions" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"Describe how a robot can avoid obstacles.\"}],\"max_tokens\":128}"
Refer to Reasoning for image/video request formats. The
frontend uses Dynamo’s API, including OpenAI response_format. The worker’s
NIM proxy handles NIM-specific aliases. The frontend does not expose NIM
metadata. The Python Reasoner and Responses examples require a worker NIM URL.
Refer to Run the Cookbook Clients. Verify the selected profile through the worker’s /v1/manifest. For
persistent video sessions, use the client’s explicit
Dynamo mode, which
uses only shared frontend endpoints and does not require worker metadata.
The Responses path needs separate validation. For a frontend build that supports stateless Responses creation, try the discovered model:
curl -fsS "$NIM_URL/v1/responses" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$MODEL\",\"input\":\"Describe how a robot can avoid obstacles.\",\"store\":false,\"max_output_tokens\":128}"
Successful creation with store=false does not imply support for persisted
retrieval, cancellation, background responses, or previous_response_id.
The worker NIM proxy also forwards Responses creation unless
NIM_DISABLE_RESPONSES_ROUTE=true. That setting does not disable the shared
frontend’s own route. Check the selected frontend build and operator settings
when a route is absent.
Run the Cookbook Clients#
inspect_profile.py, reasoner.py, and reasoner_responses.py use the worker’s
NIM metadata and model endpoints. Use these commands with either deployment
variant after confirming the selected image exposes the required metadata.
Forward the worker’s NIM port in a separate terminal:
kubectl -n "$NAMESPACE" port-forward pod/<reasoner-worker-pod> 8001:8000
From .agents/skills/cookbooks/cosmos3/nim, run the clients in the
pinned client environment:
curl -fsS http://localhost:8001/v1/health/ready
NIM_URL=http://localhost:8001 uv run python examples/inspect_profile.py
NIM_URL=http://localhost:8001 uv run python examples/reasoner.py --case image_caption
NIM_URL=http://localhost:8001 uv run python examples/reasoner_responses.py
These per-command assignments leave the shared frontend’s NIM_URL unchanged.
Use --case all to run the full Reasoner catalog. The
Dynamo streaming client
uses the shared frontend directly and needs no worker port-forward.
Both deployment manifests set the worker container’s DYN_NAMESPACE_PREFIX
from its nvidia.com/dynamo-namespace pod label. The private frontend inherits
this setting, so model discovery includes the operator’s suffixed worker
namespaces. Prefix matching takes precedence over the exact DYN_NAMESPACE
filter and covers all worker pools under that deployment prefix.
Check /v1/models on the interface receiving inference. A worker’s NIM
readiness check can succeed while its private frontend’s model list is empty.
In that case, check that DYN_NAMESPACE_PREFIX resolves to the deployment’s
namespace prefix in the worker container. A Responses
“Model not found” error does not mean the Responses route is absent. Successful
requests through the shared frontend do not validate the worker proxy’s routes.
Scale or Stop#
To add workers, set spec.services.ReasonerWorker.replicas to <worker-count> in
reasoner.yaml and apply it again. Each
replica needs the listed resources and access to the shared cache. Changing
the model or GPU topology also requires matching NIM_MODEL_VARIANT,
NIM_PRECISION, NIM_TAGS_SELECTOR, and GPU requests/limits to the selected
image’s profile. Update the download Job selectors too if you need artifacts
for a different profile. This example uses fixed replicas. Autoscaling is not
enabled.
Set spec.services.ReasonerWorker.replicas to 0 to release the GPUs while
retaining the frontend and cache. Restore a positive count and wait for
readiness to resume inference.
To stop serving, delete only the graph deployment:
kubectl -n "$NAMESPACE" delete -f reasoner.yaml
This removes the frontend and workers while retaining the externally managed
cache PVC (create: false) and secrets for reuse. Run it only when you intend
to stop serving. It interrupts active requests. Stop the port-forward with
Ctrl+C.