AI inference servers#
Overview#
Build your application’s speech, vision, and reasoning capabilities using the shared inference services. Your workers call these services through model interfaces, allowing you to choose supported models and local or hosted endpoints as your application develops.
Available services#
Launch these inference and typed capability services alongside
services/device-io-hub/. Model HTTP stays inside xr-ai-models; workers must
not add vendor SDKs or hand-written HTTP clients.
Server |
Command |
Port |
Model |
Backend |
|---|---|---|---|---|
|
|
8100 |
Cosmos3 Nano Reasoner |
vLLM (pip or docker) |
|
|
8103 |
parakeet-tdt-0.6b-v3 |
NeMo ASR in-process |
|
|
8102 |
Nemotron 3 Diarization + Multitalker Parakeet |
NeMo streaming ASR |
|
|
8104 |
magpie_tts_multilingual_357m |
NeMo TTS in-process |
|
|
8105 |
kyutai/pocket-tts |
Pocket TTS in-process |
|
|
8107 |
NVIDIA-Nemotron-3-Nano-30B-A3B-{NVFP4,FP8} |
vLLM (pip or docker) |
|
|
8108 |
Nemotron-3-Nano-Omni-30B-A3B-Reasoning (NVFP4, FP8, or BF16, GPU-selected) |
vLLM (pip or docker) — multimodal (text + video) |
|
|
8120 |
Cloudflare Clef-Flash 9B |
Native SystemOne choice decisions |
|
|
8109 |
Nemotron-3-Embed-1B-BF16 |
vLLM (pip or docker) |
|
Operator-run container |
8000 (container); 8112 in example |
Nemotron OCR v2 multilingual |
Hugging Face in a local CUDA container |
|
|
8310 |
— |
Typed recorded-video capability |
|
|
8340 |
— |
Typed dense document retrieval capability |
For model configuration and the launch workflow, refer to Customizing model servers. For the orchestrator pattern, refer to Adding a new sample.
Launcher-managed local model artifacts land in the service’s model_cache directory, set per
YAML and resolved relative to the YAML file. Operators configure self-hosted
NIM caches for their optimized engines and weights; hosted endpoints keep no
weights in this repository. Every models/ tree is excluded from version
control. The model-servers profiles share models/ at the repository root;
the exact layout per launch style is below.
OCR serving#
The reusable services/ocr-server provides multilingual Nemotron OCR v2 in
an operator-managed container. No supplied sample launches or consumes OCR.
The Dockerfile builds an isolated Python 3.12, PyTorch 2.9.1, CUDA 13 runtime
with NVIDIA’s pinned Hugging Face source. At startup, the service downloads
v2_multilingual weights and keeps the model loaded across requests.
Docker, NVIDIA Container Toolkit, and a CUDA 13-compatible NVIDIA driver
are required; a host CUDA toolkit is not. The first build takes several minutes.
Refer to the
upstream installation instructions.
Set the build argument TORCH_CUDA_ARCH_LIST for the selected GPU: 8.9 for
Ada, 12.0 for Blackwell, or 12.1 for Spark. From the repository root,
build the image; this example targets Blackwell:
docker build --build-arg TORCH_CUDA_ARCH_LIST=12.0 \
-t xr-ai-nemotron-ocr:local services/ocr-server/ocr_server
Run it in the foreground on GPU 0, publishing the API only on localhost:
mkdir -p models
docker run --rm --name xr-ai-ocr-manual --gpus device=0 --ipc=host \
-e HF_TOKEN -e OCR_INFER_LENGTH=1024 \
-p 127.0.0.1:8112:8000 -v "$PWD/models:/models" \
xr-ai-nemotron-ocr:local
The operator owns GPU placement, container startup and shutdown, cache mounts, and readiness checks. After startup completes, check readiness in another terminal:
curl --fail http://localhost:8112/v1/health/ready
The service loads and warms the model before accepting requests. There is no
host ocr_server command or run_model_stack registration. Sample stack stop
commands do not stop this container, and no automatic reuse or replacement is
performed. Provision GPU capacity before running it alongside other services;
no GPU allocations are changed and coexistence is not qualified.
Repository GPU tests do not discover arbitrary operator-run containers. Stop
this container before running GPU tests that use the same device; the HTTP-only
OCR smoke test below can run against it without managing containers.
For NIM, deploy Nemotron OCR v2 independently and configure its endpoint in the consumer profile. The operator selects the multilingual variant and owns GPU placement, model downloads, caches, and container lifecycle. XR AI does not start an OCR NIM container or require a local HTTP proxy. Refer to Deploying samples with NIM endpoints for the endpoint example and validation.
Both backends expose POST /v1/ocr and readiness at
GET /v1/health/ready. Use the nemotron_ocr preset and typed SDK factory:
from pathlib import Path
from xr_ai_models import load_models_config_from_dict, make_ocr
config = load_models_config_from_dict({"ocr": {
"adapter": {"preset": "nemotron_ocr"},
"endpoint": {"base_url": "http://localhost:8112"},
"deployment": {"ownership": "external"},
}})
async with make_ocr(config, "ocr") as ocr:
regions = await ocr.read_text(Path("image.png"))
for region in regions:
print(region.text, region.confidence, region.polygon)
Applications reuse the endpoint; an OCR tool does not load its own GPU model. The client sends one inline PNG or JPEG and returns text, confidence, and normalized polygons. Remote image URLs are intentionally unsupported. Word, sentence, and paragraph grouping are available; recognition results need not be identical across backends.
Send an opaque image. The local Hugging Face server converts inputs to RGB without compositing transparency: opaque black text on a transparent black background becomes uniformly black, losing the visible text. Composite transparent PNGs onto an appropriate background before calling either backend. For dark text intended for a white background:
from PIL import Image
with Image.open("input.png") as source:
rgba = source.convert("RGBA")
opaque = Image.new("RGBA", rgba.size, "white")
opaque.alpha_composite(rgba)
opaque.convert("RGB").save("ocr-input.png")
Choose a contrasting background for light text. This preparation remains caller-owned; the service does not infer the intended background color.
The Hugging Face server clips finite bounding-box coordinates to the image’s
normalized [0, 1] bounds while preserving recognized text and confidence.
Non-finite model coordinates are an inference failure (HTTP 500), not valid
detections. The SDK continues to reject malformed responses from any endpoint.
The Hugging Face server accepts one image per request, at most 10 MiB of PNG
or JPEG file bytes after base64 decoding, and 20 million pixels after
full-resolution square padding:
max(width, height)² <= 20,000,000 (longest side at most 4,472 pixels).
This check runs before RGB conversion or inference because upstream pads the
image on CUDA before resizing; lowering infer_length does not reduce that
allocation. The padded FP16 RGB tensor alone is bounded to about 114 MiB;
this is not a bound on total inference memory. Requests that exceed the schema’s
data-URL string-length limit receive HTTP 422 before image decoding. Requests
that pass that limit but exceed the decoded image-file byte limit or padded
pixel limit receive HTTP 413. Both limits apply before model inference;
callers must reduce the encoded file size, downsample, or tile oversized inputs.
The server serializes inference and rejects overlapping requests with HTTP 503.
Its detector length is configured server-wide by OCR_INFER_LENGTH (640 or
1024, default 1024). NIM request limits and engine settings belong to the deployed version,
not this local server configuration. Neither integration adds experimental
text-dense cropping or sharpening; preprocessing remains caller-owned.
Local weights and the torchvision backbone are cached at /models/huggingface
and /models/torch inside the container. The example mounts the repository’s
models/ directory there; change the host mount to use another persistent
cache. The compiled CUDA extension stays in the Docker image.
Stop only the container you launched with docker stop xr-ai-ocr-manual.
The example’s --rm removes the stopped container, not the mounted cache.
Rebuild after source changes and rerun after configuration changes. Docker
rejects an occupied name or port rather than replacing an existing service.
Closing an SDK client stops neither the local service nor an externally deployed NIM.
To test transcription against a running endpoint, run from the repository root:
XR_AI_OCR_TEST_URL=http://localhost:8112 uv --config-file uv.toml run --project tests \
pytest tests/test_ocr_server.py::test_live_ocr_equipment_text
This opt-in HTTP test does not require a local GPU or Docker and never owns
containers. It skips when XR_AI_OCR_TEST_URL is unset; do not mark it gpu
or run it through container-management fixtures.
The smoke test checks transcription on a square image, not polygon placement.
CPU tests cover the response coordinate contract with mocked predictions;
they do not establish real-model normalization on non-square images.
Speaker-conditioned STT#
services/speaker-stt is a private local inference process. It shares
Nemotron 3 Diarization and Multitalker Parakeet model weights across sessions,
while keeping each session’s diarization history, enrollment and ASR caches
separate. In conversation and hybrid modes, a session enrolls one speaker with
the configured start phrase and
releases that identity with the stop phrase. With diagnostics disabled,
background voices do not reach the transcript as separate speakers after
enrollment: the decoder receives the mixed audio with owner and interference
masks and is conditioned on the owner. Optional diagnostics expose ignored
transcripts for display only; they cannot query the agent or change enrollment.
Before enrollment, each detected speaker has independent conditioned decoding
and phrase-matching state. Someone already talking does not reserve ownership:
another speaker can say the start phrase over that conversation and enroll when
their own utterance completes. Non-speech background noise does not impose a
silence gate. Each speaker’s existing inactivity interval ends their utterance;
there is no requirement that every speaker become quiet together.
All complete start phrases finalized in one accepted audio step are considered before ownership changes. One match enrolls its speaker; multiple matches in that step enroll nobody and require a fresh attempt. A phrase finalized later cannot take over an existing owner. This is a streaming decision boundary, not a guarantee of distinguishing all acoustically simultaneous requests. Speaker labels are session-local identities; actual attribution and interference suppression require qualification on the intended microphones.
Interaction mode is independent of the selected ASR backend. While closed,
wake_only and hybrid can authorize an attributed, wake-addressed one-off
without enrolling its speaker. A unique exact start takes priority in hybrid
mode; ambiguous same-step requests authorize nobody. A bare wake permits only
that recognized speaker’s next utterance whose onset precedes the configured
grace deadline. always_on accepts all recognized speakers without enrollment.
Candidate transcripts themselves are not query authorization or diagnostics.
The process follows the model-service HTTP pattern and exposes health and model identity endpoints. A private WebSocket carries mono signed 16-bit PCM at 16 kHz and returns enrollment, authorized one-turn, and transcription events. It serializes inference because model conditioning is mutable. Opening a session establishes the capture timestamp of its first sample. Subsequent audio requests form one ordered, contiguous PCM stream; the service derives all later timestamps from the number of samples it has accepted. The caller must close and reopen the session if its capture or transport layer drops audio, because that layer owns the information needed to detect the loss. Utterances forced to end at the configured duration limit cannot authorize a closed-state turn, enroll, or release a speaker; an enrolled owner’s forced segment is still transcribed. Start and stop phrases can span utterances within the conversation phrase window, retaining separate enrollment fragments for each speaker. A truncated candidate cannot enroll from the tail of the same continuing speech episode; that speaker must end its episode before making another attempt.
The private protocol makes that ownership explicit: the opening JSON message
supplies audio_origin_us once, and subsequent binary WebSocket messages
supply only PCM bytes. Each connection owns one session, and the caller waits
for the event response before sending the next audio message. Audio must be
serialized in capture order. The service neither reorders audio nor guesses
whether samples are missing from wall-clock arrival times.
Binary messages retain the existing limit of one second of audio (16,000
complete signed 16-bit samples, or 32,000 bytes). Invalid message types, JSON,
or incomplete PCM samples are protocol errors. The server’s standard transport
message-size and heartbeat defaults are retained; JSON controls do not inherit
an unrelated audio-duration limit.
The private identity is protocol 5, adding the explicit diagnostics setting to protocol 4’s interaction policy. Worker and speaker service must be restarted together after updating: earlier endpoints fail admission rather than silently selecting batch STT or accepting incompatible interaction settings.
Configure model IDs, device, precision, host, port and session capacity
in services/speaker-stt/speaker_stt.yaml. The default HTTP base URL is
http://127.0.0.1:8102. Keep the unauthenticated private streaming endpoint on
a loopback or trusted interface. Hardware profiles or CUDA_VISIBLE_DEVICES
select a GPU; the generic configuration uses the first visible GPU. From the
repository root, start it with:
uv --config-file uv.toml run --project services/speaker-stt speaker_stt
The service warms both streaming and final decoding before signaling ready. Reuse verifies the local listener’s managed-process ownership and resolved configuration before launching a child; a changed configuration requires stopping that process first. A matching listener still warming its models refuses another launch rather than allocating a duplicate GPU copy. Persistent reuse exits after signaling readiness; monitored reuse keeps its readiness proxy alive.
Each session owns one diarization cache and at most eight attributed ASR
continuations, allocated when their speaker identities appear. Features and model
weights are shared. The Multitalker Parakeet encoder uses 160 ms chunks. Native
encoder and predictor state continue through quiet intervals; utterance boundaries
clear only the text being delivered. Accumulated native transcript metadata is
replaced after each step while retaining its decoder state, so completed turns do
not grow the hypothesis history. Initial speaker admission replays at most the
encoder’s existing left-context window through that speaker’s masks. Always-on
and wake-only keep candidate decoding as their
steady state; hybrid uses it whenever no owner conversation is open. Every
candidate remains required, so a decoder failure resets that session rather
than silently ignoring a competing speaker. After enrollment only the owner’s
ASR continuation is retained with diagnostics disabled. Enabled diagnostics share
encoded features but add decoding work and separate continuations for non-owner
speakers. Every retained continuation advances through actual quiet audio; this
adds inference work compared with resetting each utterance. With the
default max_sessions: 16, up to 128 enrollment decoder caches can coexist;
GPU memory and latency at that bound have not been qualified. Candidate-decoding
failure revokes the session because dropping a candidate could conceal a phrase
tie. All cache allocation, inference, and release use one serialization boundary.
At a quiet endpoint, final text is decoded on an isolated copy with 320 ms of terminal silence. This model-specific padding allows delayed subword emissions to complete; it does not change the configured silence threshold or advance the live capture timeline. Forced utterance truncation does not add terminal silence. Tokens already delivered by finalization are reconciled against subsequent real decoding before new-turn text is exposed. If the real continuation revises that finalized suffix, the inference session fails and releases enrollment rather than assigning old words to a new turn. The padding is a bounded policy qualified with recorded and generated speech, not a universal recognition guarantee.
A connected session may pause without losing enrollment. Disconnect, transport
liveness failure, or inference failure releases it; max_sessions bounds
retained sessions. Cancelled model work finishes before its cache and
serialization ownership are released, including repeated cancellation.
The model environment is isolated from workers and the existing batch STT
service. Model weights are downloaded on first use and then reused from the
configured cache. As with the sibling NeMo services, existing HF_HOME and
NEMO_CACHE_DIR values take precedence over cache defaults.
The WebSocket session is not a public model API. It is reserved for the private typed voice integration; applications continue to use the public voice and model interfaces rather than connecting to this route directly.
Two HuggingFace cache roots#
The servers use two different HF_HOME values, so HuggingFace weights live in
two separate trees under the service’s resolved model_cache:
Consumer |
|
Hub cache |
|---|---|---|
vLLM-backed servers (pip and docker) |
|
|
STT and Magpie TTS (NeMo host processes) |
|
|
(The NeMo servers additionally cache non-HF artifacts under <model_cache>/nemo/.)
For host processes (pip-mode vLLM, STT, Magpie TTS) these are defaults: an
HF_HOME or NEMO_CACHE_DIR already set in the environment takes priority.
Docker-mode vLLM always uses the YAML model_cache, which it mounts into the
container.
Pocket TTS defaults HF_HOME to <model_cache>/pocket/huggingface/ and
HF_XET_CACHE to <model_cache>/pocket/xet/.
model_cache itself is set per YAML and resolved relative to the YAML file.
Both the model-servers profiles and the standalone service YAMLs resolve it to
models/ at the repository root. For a manual hf download, set HF_HOME to
match the consumer:
# vLLM-served model, launched via a model-servers profile
# (model_cache resolves to models/ at the repository root):
HF_HOME=models hf download nvidia/Cosmos3-Nano
# NeMo STT server launched from its standalone YAML:
HF_HOME=models/huggingface hf download nvidia/parakeet-tdt-0.6b-v3
Starting without network access#
The following model services look up their models in the Hugging Face cache before contacting the Hub, and load cached models from their local paths:
Service |
Cached artifact |
Loaded through |
|---|---|---|
Docker-mode vLLM servers |
Snapshot of |
|
STT and Magpie TTS |
|
NeMo |
Pocket TTS |
Weights, tokenizer, and voice state named by the selected |
A cache-local copy of that language configuration |
Clef |
Snapshot at |
Native release loader with the snapshot directory |
The listed services use cached artifacts without refreshing them from the Hub. Only a cache miss follows the online download path. Pocket TTS accepts its ungated weights when the gated voice-cloning weights are not cached. Pip-mode vLLM passes the model ID to vLLM unchanged, so it can still contact the Hub.
Offline startup supports a previously working cache. A local snapshot lookup
does not establish completeness or integrity. Interrupted or damaged caches
may require an online re-download; automatic partial-cache repair is not
provided. The underlying loader may fail or apply defaults for missing files.
To recover, re-download the affected model and revision with hf download <model-id> --revision <revision> --force-download, using the same HF_HOME
or --cache-dir as the service, then start the service again while online.
A cache hit is never refreshed. A model without a pinned revision keeps the
main commit it was downloaded at; update it with hf download, as shown
above.
After a successful online start, the listed services can reuse their working
model cache without network access. This assumes their runtime dependencies
and Docker images are already installed. To forbid downloads, set
HF_HUB_OFFLINE=1. The host services inherit it, and the vLLM wrapper forwards
it into its containers:
HF_HUB_OFFLINE=1 uv run --project agent-samples/simple-vlm-example/model-servers simple_vlm_example_model_servers
In this mode, a service whose model is not cached fails at startup with an
error that names the model, its revision, and the cache directory. Like the
other forwarded Hub settings, changing HF_HUB_OFFLINE recreates persistent
vLLM containers.
Migrating model caches from ai-services/#
The service-directory rename does not move ignored model weights. Existing
installations may still hold VLM, STT, and LLM weights in
ai-services/models/, and TTS weights in ai-services/tts/models/; all
current service profiles resolve to the repository-root models/ directory.
Stop model services, then merge the caches from the repository root:
mkdir -p models
if [ -d ai-services/models ]; then
cp -a -l -n ai-services/models/. models/
fi
if [ -d ai-services/tts/models ]; then
cp -a -l -n ai-services/tts/models/. models/
fi
The -l option avoids duplicating large weight files and requires both paths
to use the same filesystem. Omit -l when copying across filesystems and
verify free space first. Keep the old directories until the relocated services
start successfully without network access. Recreate project environments with
uv sync; do not copy .venv directories across the move.
Calling these from a worker#
Workers do not hand-roll httpx clients against these endpoints. They
depend on xr-ai-models,
load a per-sample model profile, and construct service clients via
make_llm, make_vlm, make_stt, make_tts, make_embedding, and
make_decision. The SDK encapsulates the
OpenAI-compatible wire format and the per-model quirks (reasoning-field
aliasing, chat_template_kwargs, served-model-name strings) so callers
never branch on backend.
from xr_ai_models import load_models_config, make_llm, ChatMessage
config = load_models_config("yaml/models.json")
async with make_llm(config, "llm") as llm:
resp = await llm.chat(
[ChatMessage(role="user", content="hello")],
max_tokens=128,
enable_thinking=True,
)
print(resp.content, resp.reasoning)
A consumer model profile specifies adapter behavior and endpoint connectivity:
{
"models": {
"llm": {
"category": "llm",
"adapter": {"preset": "nemotron_omni"},
"endpoint": {"base_url": "http://localhost:8108"}
}
}
}
The worker-side xr-ai-models loader accepts JSON or YAML, including flat
legacy entries and direct role mappings. A profile shared with the launcher
must use the wrapped nested .json contract; non-.json profiles are
rejected before deployment metadata is read. Full protocol surface, the preset
table, and the profile contract are in
xr-ai-models.
Hosting models on NVIDIA NIM#
Develop with the local shared model servers and deploy the same workers against
independently operated NIM endpoints. The model profile selects a protocol
adapter and an endpoint; samples continue to use the typed xr_ai_models
interfaces. There is no XR AI NIM container launcher or local HTTP proxy.
Chat and vision use openai_compat with native model IDs. Speech uses
riva_grpc, which normalizes Riva audio inside the SDK. Asymmetric embedding
endpoints use the existing adapter.default_extras.input_type field; the SDK
translates query and passage labels without changing the RAG service.
Self-hosted NIM endpoints#
Operators own container lifecycle, GPU placement, optimized-engine caching,
health exposure, and endpoint authorization. Consumer profiles use external
ownership. The local model-servers launcher remains a development convenience.
Refer to Deploying samples with NIM endpoints for a complete profile, sample
configuration paths, smoke checks, and deployment qualification.
Model-server persistence#
The persistent vLLM-backed servers (vlm_server,
nemotron3_nano_llm_server, nemotron_omni_llm_server, embedding_server)
survive agent restarts by design: the sample’s convenience launcher starts
them and returns after readiness, leaving weights warm. Use the sample’s model command with --stop
for the selected local deployment. Startup does not sweep services omitted
from its configuration. Independently deployed NIMs follow their operator’s
lifecycle. At startup a wrapper
that finds a different persistent xr-ai container holding its port (found
by the xr-ai-vllm.port=<port> label) stops and removes it before launching
its own. Each persistent wrapper script checks its
health endpoint before spawning vLLM:
Already running with a matching launch fingerprint → touch the ready file immediately, then idle. Stack is ready in seconds; no model reload.
Matching container still starting → attach to its lifecycle and keep waiting for
/healthinstead of issuing a conflicting seconddocker run.Already running with changed or legacy configuration → stop, remove, and recreate the repository-owned container from the current YAML.
Healthy endpoint without the expected running container → fail without stopping the unowned listener.
Stopped Docker container with matching launch fingerprint → restart it, wait for
/health, then touch the ready file.Stopped Docker container with changed or legacy configuration → remove and recreate it from the current YAML before waiting for
/health.Not running → spawn vLLM normally, wait for
/health, then touch the ready file.
In pip mode, vLLM is spawned with start_new_session=True so the launcher’s
killpg() does not reach it on shutdown. In docker mode, Docker owns the
container while the foreground docker run client uses its own session.
Either way vLLM keeps running after the orchestrator exits.
The STT server follows the same pattern without Docker: stt_server spawns
its persistent process with start_new_session=True, reuses a healthy server
that survived a previous stack run, and is stopped by the sample’s model
command with --stop when selected in that stack.
Most shipped model-server profiles that include voice select speaker ASR;
procedure-capture-guide selects batch STT for narration without speaker
enrollment. When selected, the speaker STT launcher waits for model loading,
decoder warmup, and HTTP readiness before reporting ready, then reuses a warm
instance with matching configuration. Configuration changes require stopping
and restarting the service. --stop stops only the sample’s explicitly
selected server list, using its configured ports and the existing
process-ownership guards. The speaker service listens on port 8102 and supports
streaming audio over a private WebSocket.
Native NIM speech uses independently deployed Riva endpoints rather than this
local model stack. Refer to Deploying samples with NIM endpoints and
xr-ai-voice for worker backend selection and enrollment.
Pocket TTS uses the launcher’s persistent process group directly. Its bootstrap
reuses an existing healthy listener. In a monitored stack, the reuse invocation
remains alive as a health proxy so the launcher can detect service loss. A
persistent-only launcher that uses exit_after_ready=True explicitly permits
that proxy to exit after readiness, so repeated model-servers starts do not
leave idle wrappers behind. When no server exists, the bootstrap replaces
itself with the foreground Uvicorn and Pocket TTS process.
Docker containers carry a fingerprint of their image, GPU assignment, model cache, environment, bootstrap packages, complete vLLM command, and a versioned launcher-controlled Docker contract. This prevents a failed container created by one sample profile—or by older launcher behavior—from being restarted later with stale memory limits, entrypoint, setup commands, or model arguments.
Stopping the persisted servers, from the repo root:
uv run --project agent-samples/simple-vlm-example/model-servers simple_vlm_example_model_servers --stop
Cleanup locates labelled Docker containers before inspecting ports, then
stops them with docker stop (escalating to docker kill after 20 s).
Locally persisted processes (pip-mode vLLM and Pocket TTS) must carry the
XR_AI_VLLM_MANAGED and XR_AI_VLLM_PORT ownership markers before cleanup
sends SIGTERM or SIGKILL. Pocket TTS records the dedicated process group
created by the launcher without leaving that group. Cleanup signals the
complete group only after verifying both the listener marker and, when
separate, the launcher’s group-leader marker, so inference descendants cannot
survive as orphans. Launcher abort-time escalation can reach Pocket TTS before
it signals readiness; once its ready file exists when cleanup begins, the
launcher preserves it. Explicit stop falls back to PID-only cleanup if group
ownership cannot be verified; other local servers always retain PID-only cleanup. Unknown listeners and
failed inspection abort cleanup without sending a signal, and
the sample’s model command with --stop exits nonzero if any target could not be stopped. Absent
servers are silently skipped.
Stop targets are only the servers listed by that sample’s convenience launcher,
with ports read from the service defaults and selected sample GPU overrides.
Use the same explicit --gpu-profile NAME on start and stop. Stop does not
require model-download credentials.
Before switching samples, updating the checkout, or editing server settings, stop the current stack using its current checkout and profile. Then start the new stack. The convenience launchers do not track previous configurations or provide isolation between samples using the same ports. Cleanup can stop a recognized service used by another sample or manual invocation. Refer to Customizing model servers for the complete development flow.
Choosing the vLLM runtime (pip vs Docker)#
All four vLLM-backed servers (vlm_server,
nemotron3_nano_llm_server, nemotron_omni_llm_server, embedding_server) accept a
vllm_backend: key in their YAML to pick how vLLM is hosted:
|
Runtime |
Code fallback |
Shipped standalone YAMLs |
Use when |
|---|---|---|---|---|
|
|
yes |
no |
Developing the wrapper in its local environment or using a custom pip installation. |
|
|
no |
yes |
Running the configured vLLM container used by the checked-in configurations. |
Both modes honor identical configuration keys — same model, same port, same vLLM
flags. Docker-only lifecycle settings such as vllm_image, extra_pip, and
spark_uma are accepted and ignored in pip mode. The dispatcher lives in
utils/xr-ai-vllm/. Switching is one YAML edit:
# vlm-server (Cosmos3)
vllm_backend: docker
vllm_image: nvcr.io/nvidia/vllm:26.10.pre1-py3
vllm_image: defaults to nvcr.io/nvidia/vllm:26.10.pre1-py3 for all wrappers.
The Python environments also pin vLLM 0.31.0. vLLM 0.31.0 is the upstream fixed
release for CVE-2026-114050 and CVE-2026-114055. Keep per-request multimodal
processor overrides disabled; server-level processor configuration remains
available.
This NVIDIA NGC preview uses CUDA 13.4 and reports vLLM
0.31.0+88641915 on Linux x86_64 and aarch64. The checked-in Cosmos3 and
Nemotron model configurations select this image. Qualify the preview on the
target GPU and model workload before deploying it. Nemotron Omni uses its
native Mamba and causal-convolution implementations, so the shipped
configuration does not compile or install mamba-ssm or causal-conv1d.
Override the image to pin another tag, an internal mirror, or a custom build.
The preview tag is mutable. The observed multi-architecture manifest digest is
sha256:cb38bbefdbc3e9fe045f931dd72cbd58cd48985c7e09916bcc6c15c10f39b929.
For reproducible deployments, append @sha256:<digest> to the image reference
in each service configuration and qualify that exact manifest.
Important
When upgrading an existing checkout, stop the persistent model stack before starting it with the new image:
uv run --project agent-samples/simple-vlm-example/model-servers simple_vlm_example_model_servers --stop
docker pull nvcr.io/nvidia/vllm:26.10.pre1-py3
The next launch recreates stale managed containers when their image or command
fingerprint differs. After the new stack starts successfully, reclaim disk from
an old image with docker image rm <old-vllm-image>. Keep the shared model cache;
the updated stack reuses compatible weights and downloads any missing artifacts.
docker mode — prerequisites#
Docker Engine accessible from the launch session (
docker psmust succeed withoutsudo). Refer to Docker access and NVIDIA runtime for group setup.NVIDIA Container Toolkit installed, with its
nvidiaruntime registered in Docker. Refer to Docker access and NVIDIA runtime for the runtime check, configuration steps, and GPU smoke test.NGC pull access, when the configured
vllm_imageis restricted or requires authentication onnvcr.io. The wrapper auto-runsdocker login nvcr.ioifNGC_API_KEYis in the environment (loaded byload_credentials()from~/.config/xr-ai/credentials.jsonper Credentials). Otherwise, log in manually once:docker login nvcr.io -u '$oauthtoken' -p $NGC_API_KEY
Existing ~/.docker/config.json entries take priority and are not overwritten.
docker mode — runtime details#
Container is launched with
--network host --ipc host --runtime nvidia(forwardingNVIDIA_VISIBLE_DEVICES), and/bin/bashoverrides the image entrypoint so setup installs run beforevllm serve.Failed stopped containers are recreated because Docker cannot change their recorded entrypoint or command.
The host
model_cacheis bind-mounted at the same path inside the container andHF_HOMEis set to it, so weights cached by pip mode are reused by docker mode and vice versa.Before
vllm servestarts, the container resolves the model snapshot from the mounted cache, as described in Starting without network access. Only when the snapshot must be downloaded does the wrapper verify that the image’s Hub exposes its Xet download path and hashf-xet>=1.1.2,<2.0.0. It repairs a missing or incompatiblehf-xetwheel and checks the complete integration again; an unavailable accelerator therefore fails startup instead of silently falling back to plain HTTPS. The download completes and is synced to disk before vLLM initializes CUDA.HF_XET_HIGH_PERFORMANCE=1is the default in pip and docker modes. Setting it to0disables high-performance tuning, not Xet itself. SetHF_HUB_DISABLE_XET=1to disable Xet. To use legacyhf_transferwith a compatible Hub version, install thehf_transferpackage and set bothHF_HUB_DISABLE_XET=1andHF_HUB_ENABLE_HF_TRANSFER=1; Xet-backed files continue to use Xet when it remains enabled. These values are preserved, forwarded, and included in the container fingerprint, so changing one recreates a persistent container.
The selected NGC preview reports this vLLM version in its image metadata:
Image |
vLLM |
|---|---|
|
0.31.0+88641915 |
Container name is deterministic per service:
xr-ai-vllm-vlm-server,xr-ai-vllm-nemotron3-nano-llm-server,xr-ai-vllm-nemotron-omni-llm-server, andxr-ai-vllm-embedding-server.Persistence parity: all four vLLM-backed wrappers launch their Docker processes in separate sessions, so they survive launcher shutdowns like their pip-mode
start_new_session=Truecounterparts.
Cleanup#
the sample’s model command with --stop works for both modes within the selected managed launch
plan. Cleanup locates labelled Docker containers before inspecting ports, then
stops them with docker stop (escalating to docker kill after 20 s).
Pip-mode processes carry an xr-ai-vllm ownership marker; unknown listeners
and failed inspection abort cleanup without sending a signal.
Pip-mode vLLM processes started before the ownership markers were introduced cannot be identified safely. After upgrading, stop each unmarked process manually once; subsequent launches include the markers and support managed cleanup.
Per-server notes#
vlm-server defaults to the Cosmos3 Nano Reasoner. Hugging Face publishes Reasoner and Generator weights in the unified
nvidia/Cosmos3-Nanocheckpoint. The requiredCosmos3ForConditionalGenerationarchitecture override selects vLLM’s native Reasoner loader: despite its generic class name, it maps only the understanding tower and vision encoder and drops the Generator weights. The Generator requires vLLM’s separate--omnipath, which xr-ai intentionally does not enable. The checkpoint’s official chat template emits the assistant answer directly and has noenable_thinkingbranch or<think>delimiters, so the client preset needs no reasoning-field mapping. Cosmos-Reason1 remains available by pairingmodel: nvidia/Cosmos-Reason1-7Bwith thecosmos_vlmclient preset. Hosting backend is selectable per YAML — refer to Choosing the vLLM runtime above. When servingnvidia/Cosmos3-Nanowith the default Docker image on SM12 GPUs, including RTX Blackwell and GB10, the service selects Triton attention for both the decoder and vision encoder. Measurements on NGC 26.09 showed lower cold image latency with Triton: the x86 image fell back to FlashAttention-2 PTX compilation, while the ARM image included native Blackwell kernels. This conservative default is retained for the NGC 26.10 preview pending GPU qualification; those kernel properties and latency results have not been reverified on the preview. Other GPUs, custom images, and pip mode retain vLLM’s backend selection. The optionalattention_backendandmm_encoder_attn_backendYAML keys override each component independently; explicitautorestores vLLM’s selection. Cold starts still include model loading and backend compilation. A healthy model HTTP endpoint is not a guarantee that sample inference warmup has completed.nemotron3-nano-llm is a thin wrapper around
vllm serveforNVIDIA-Nemotron-3-Nano-30B-A3B-{NVFP4,FP8}(auto-selected by GPU compute capability). vLLM handles tool calling (qwen3_coderparser), reasoning extraction (nano_v3parser — auto-fetched intomodel_cache), and FlashInfer FP4 MoE kernels.model_blackwellselects the NVFP4 checkpoint on SM100+;model_adaselects FP8 on earlier supported GPUs. Parsed reasoning is returned in thereasoningfield. Thenemotron3_nanoclient preset disables thinking by default so short calls retain an answer token budget; passenable_thinking=Trueon a call to opt in. Native FP4 requires a Blackwell-class GPU such as B200 or RTX PRO 6000; FP8 is used on Ada, Hopper, and Ampere.enforce_eager: trueby default to avoid the silent 3–8 min CUDA graph and FlashInfer autotune on cold start. Hosting backend is selectable per YAML (refer to Choosing the vLLM runtime). The model card recommendstemperature=1.0andtop_p=1.0for reasoning, andtemperature=0.6andtop_p=0.95for tool calling. It identifies the model as ready for commercial use under the NVIDIA Nemotron Open Model License.nemotron-omni-llm is a vLLM-backed multimodal LLM serving
Nemotron-3-Nano-Omni-30B-A3B-Reasoning(text + video input) at port 8108. The YAML auto-selects between three model variants by detected GPU compute capability: NVFP4 on Blackwell (SM100+), FP8 on Ada, Hopper, and Ampere, BF16 forced viause_bf16: truefor highest quality at the largest VRAM cost. Same OpenAI-compatible HTTP contract as the other LLM servers — swap the port to swap backends. Hosting backend is selectable per YAML (refer to Choosing the vLLM runtime); persists across stack restarts in both pip and docker modes.stt-server loads parakeet-tdt-0.6b-v3 via NeMo ASR in-process. The
languageandtemperatureform fields are accepted but ignored; recognition uses the loaded model’s language behavior. Setstartup_timeout_sto a positive finite number to override the 600-second cold-start budget.magpie-tts loads magpie_tts_multilingual_357m via NeMo TTS in-process.
pocket-tts loads the compact
kyutai/pocket-ttsmodel on the configuredcpu,cuda, or automatically selected device. The checked-in deployment profiles use CUDA and warm up the model before reporting ready. Its native PCM streaming path emits audio during generation; non-streaming WAV requests remain supported. Model loading and synthesis run outside the asyncio event loop. The defaultbill_boerstvoice derives from a CC0 Voice-Zero recording and is the only voice accepted by this release. The service logs whether it loaded the gated voice-cloning weights or the ungated fallback.To stream, send
POST /v1/audio/speechwithresponse_formatset topcmandstreamset totrue. The response usesaudio/pcm; thex-audio-sample-rateandx-audio-channelsheaders describe the signed 16-bit interleaved samples. Streaming with another response format returns HTTP 400.embedding-server serves
nvidia/Nemotron-3-Embed-1B-BF16through/v1/embeddingsunder the served nameembed. It emits 2048-dimensional vectors. Retrieval inputs usequery:for queries andpassage:for documents, as specified in the model card. The standalone configuration reserves 20% of a GPU and uses Docker; the tea sample applies its own memory budget and indexes all 2048 dimensions. Its Spark profile selects Triton attention for GB10 compatibility; other profiles leave attention-backend selection to vLLM. Refer to Release migration for the model upgrade steps.rag-service is a typed dense document-retrieval capability. Point
documents_dirat an application-owned tree andmodels_configat a profile with anembeddingrole. It chunks and embeds supported documents at startup, caches its index, and returns matches abovemin_score. Start the embedding service first, RAG second, and the consuming worker last. The cache includes document content, indexing settings, the resolved embedding-role hash, the server-reportedrootfrom the matching/v1/modelsentry, and optionalcache_key. Unrelated model-role edits do not invalidate the cache. The reported identity is best-effort: this repository’s Docker launcher serves a resolved snapshot path, while pip serves the Hugging Face repository ID. Switching backends can rebuild the cache, and changing a Hub revision under the same pip model ID requires a newcache_key. A served alias such asembeddoes not identify weights. Providers omittingrootcause documents to be re-embedded on every startup without persistent caching;cache_keycannot opt back in. Failed metadata requests or invalid responses abort startup. The manual key supplements the role hash and reported identity. Every replica must remain on the same weights throughout indexing and serving; health probes check availability, not model consistency. Restart RAG whenever the embedding model changes. Refer to Release migration for upgrading existing indexes and servers.video-memory-service owns recorded chunk queries, NVDEC, and PNG output behind typed msgpack over ZMQ on port 8310. Set
recordings_dirin its YAML to enable recorded-video operations; the path must match the hub’svideo_recording.out_dir. Latest video and sampling windows end at the newest recorded timestamp and require only a duration. Historical frame, video, and sampling requests share an absolutestart_us; video windows add a duration. Sampling also accepts a hard total frame budget, decodes each selected chunk once, skips unavailable or corrupt chunks when other frames remain, and can bound exported PNG dimensions. The sampled timestamps are estimates interpolated from chunk metadata. The selection budget may be up to 256, but the shipped Cosmos VLM accepts no more than four selected images per inference request. Current frames stay with the caller’s hub client. Whenrecordings_diris empty, participant discovery returns an empty list and recorded-media operations returnrecording_disabled.Ports are configurable — avoid conflicts with LiveKit (7880–7882) and hub (8080, 8090).
Standalone service YAMLs live beside services for direct single-service launches; they are not sample configuration. Sample convenience stacks start from those service YAMLs and apply overrides under
agent-samples/<sample>/model-servers/yaml/<gpu-profile>.yaml. Workers consume the resulting endpoints through their models JSON.