Disaggregated Serving

Write a DynamoGraphDeployment that splits prefill and decode into independent worker pools
View as Markdown

Disaggregated serving separates the two phases of LLM inference into their own worker pools so you can scale, size, and place them independently:

PhaseWhat it doesScaling pressure
PrefillProcesses the prompt and produces the initial KV cache.Input length, prompt reuse, context size
DecodeGenerates output tokens using the KV cache.Concurrency, output length, active KV memory

In an aggregated deployment, one worker does both phases. In a disaggregated deployment, prefill and decode are separate pools on separate GPUs: Dynamo routes each request through prefill first, transfers the KV cache to the decode worker, then streams the response from decode.

Structurally, that difference is one worker versus two. The rest of the DynamoGraphDeployment (DGD) is the same:

1 services:
2 Frontend:
3 componentType: frontend
4 VLLMWorker:
5 componentType: worker
6 # one worker does both prefill and decode

This guide walks through writing the disaggregated spec end to end. If you have not authored a DGD before, read the Deploy with DGD guide first — this page assumes you know the overall shape and focuses on what disaggregation adds.

Should you disaggregate?

Disaggregation helps most when prefill and decode need different resource shapes:

  • long prompts or retrieval-heavy traffic make prefill expensive
  • long generations or high concurrency make decode the bottleneck
  • you want to scale prefill and decode replicas independently
  • large models need different parallelism for prompt processing and generation

It is not automatically better. For small models, short prompts, low concurrency, or clusters without a fast KV-transfer fabric, an aggregated deployment is simpler and often faster. Use Sizing with AIConfigurator to compare aggregated vs. disaggregated layouts before committing.

This guide uses a single-node, multi-GPU example — prefill and decode run on separate GPUs in the same node (for instance, a node with 8×H100 serving Qwen3-32B, one GPU per worker). On a single node the KV cache moves GPU-to-GPU over NVLink, so there is nothing extra to configure for the transport. Scaling prefill and decode across multiple nodes adds RDMA networking — see Enable multi-node transfer at the end.

Write the deployment

1

Check prerequisites

For the single-node example in this guide you need:

  1. A node with multiple GPUs — one per prefill and decode worker (this example uses 8×H100 with gpu: "1" per worker).
  2. ETCD and NATS deployed for Dynamo coordination.
  3. A HuggingFace token secret (hf-token-secret) if the model is gated.

Model caching (the PVC and HF_HOME wiring the workers mount) is configured separately — see Model caching. This guide omits those fields for clarity; without them the model downloads to the container’s default cache.

Scaling across multiple nodes additionally requires an RDMA-capable network and device plugin — covered in Enable multi-node transfer.

2

Start the spec: the frontend

Begin with the DGD skeleton — just a frontend. This is identical to an aggregated deployment; nothing about disaggregation changes it.

1apiVersion: nvidia.com/v1alpha1
2kind: DynamoGraphDeployment
3metadata:
4 name: dynamo-disagg
5 namespace: your-namespace
6spec:
7 backendFramework: vllm
8 services:
9 Frontend:
10 componentType: frontend
11 replicas: 1
12 extraPodSpec:
13 mainContainer:
14 image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:1.2.1
15 imagePullPolicy: IfNotPresent
3

Add the prefill worker

The prefill worker runs the prompt phase. Two things make it a prefill worker rather than a plain worker:

  • subComponentType: prefill tags the role for the operator and router.
  • --disaggregation-mode prefill tells the engine to stop after producing the KV cache instead of generating tokens.
1 VLLMPrefillWorker:
2 envFromSecret: hf-token-secret
3 componentType: worker
4 subComponentType: prefill
5 replicas: 3
6 resources:
7 limits:
8 gpu: "1"
9 sharedMemory:
10 size: 16Gi
11 extraPodSpec:
12 mainContainer:
13 image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:1.2.1
14 imagePullPolicy: IfNotPresent
15 command: ["python3", "-m", "dynamo.vllm"]
16 args:
17 - --model
18 - Qwen/Qwen3-32B
19 - --disaggregation-mode
20 - prefill

Qwen3-32B fits on a single H100, so each worker takes gpu: "1" with no tensor parallelism. replicas: 3 gives three prefill workers; size the count for your prompt load independently of decode. (sharedMemory is explained in the KV transfer step.)

4

Add the decode worker

The decode worker continues generation after prefill hands off the KV cache. It mirrors the prefill worker with subComponentType: decode and --disaggregation-mode decode, and its replica count is sized for concurrency rather than prompt load.

1 VLLMDecodeWorker:
2 envFromSecret: hf-token-secret
3 componentType: worker
4 subComponentType: decode
5 replicas: 4
6 resources:
7 limits:
8 gpu: "1"
9 sharedMemory:
10 size: 16Gi
11 extraPodSpec:
12 mainContainer:
13 image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:1.2.1
14 imagePullPolicy: IfNotPresent
15 command: ["python3", "-m", "dynamo.vllm"]
16 args:
17 - --model
18 - Qwen/Qwen3-32B
19 - --disaggregation-mode
20 - decode

Prefill and decode workers must pass the same --model (and, if you set them, the same dtype, block size, and KV layout). A mismatch means the KV cache prefill produces cannot be consumed by decode — you get transfer errors or silently corrupt output.

5

Enable KV transfer

Because prefill and decode run on different GPUs, the KV cache prefill produces has to be copied into the decode worker’s GPU memory before decode can generate. Dynamo’s transfer layer, NIXL, does this automatically. On a single node it selects CUDA IPC and moves the cache GPU-to-GPU over NVLink — no network, no RDMA, nothing to configure for the transport itself.

The one field you do set is shared memory, already in the worker specs above:

SettingWhy
sharedMemory.size: 16GiNIXL stages transfer metadata through the pod’s /dev/shm. The pod default (64 MB) is too small and NIXL fails to initialize. 16Gi is comfortable headroom, not a tightly-computed per-request budget — it does not consume that memory unless needed.

That is the entire KV-transfer configuration for a single node: two workers tagged prefill/decode, matching --model, and enough shared memory for NIXL.

6

Apply and verify the transfer path

Apply the assembled spec:

$kubectl apply -f dynamo-disagg.yaml -n your-namespace

Then confirm NIXL initialized its transfer backend — the key check for a disaggregated deployment. Grep a prefill worker’s logs:

$kubectl logs <prefill-worker-pod> | grep -i "NIXL"

You want to see NIXL bring up a backend, for example:

NIXL INFO Backend UCX was instantiated

If NIXL fails to initialize, the usual cause is sharedMemory.size left at the default. Symptoms of a broken transfer path include high TTFT despite free prefill capacity, decode workers sitting idle while prefill is busy, or disaggregated throughput falling below your aggregated baseline.

7

Enable multi-node transfer

Everything above keeps prefill and decode on one node, where the KV cache moves over NVLink. When a worker’s parallelism exceeds the GPUs on a node, or you want prefill and decode pools on separate machines, the KV cache travels over the network instead — and that path needs RDMA (InfiniBand or RoCE). Without it, transfers fall back to TCP and KV movement can dominate TTFT and throughput.

Multi-node adds RDMA fields to each worker (rdma/ib resource requests, the IPC_LOCK capability, and UCX_* transport env vars) plus an RDMA device plugin on the cluster. That setup is out of scope here — see the Disaggregated Communication Guide for the transport configuration and Multinode Deployments for spanning workers across machines.

Faster starting points

Typing the full spec from scratch is rarely the fastest path. Copy a validated template and adapt it:

Starting pointUse when
Dynamo RecipesA recipe matches your model, backend, and hardware. Best for validated baselines and perf.yaml benchmarks.
Backend disagg.yaml templatesYou want a working DGD to adapt. Each backend ships disagg.yaml, disagg_router.yaml, and disagg_planner.yaml under its deploy/ folder.
DGDRYou want Dynamo to generate a DGD from model, backend, hardware, and SLA intent.
Sizing with AIConfiguratorYou want to compare aggregated vs. disaggregated layouts and estimate prefill/decode sizing first.

Good recipe starting points:

Backend deployment examples with concrete worker flags:

BackendExamples
vLLMDeployment examplesdisagg.yaml, disagg_router.yaml, disagg_planner.yaml
TensorRT-LLMDeployment examples — disaggregated, router, and planner variants
SGLangDeployment examples — NIXL-based disaggregated serving

How disaggregation relates to KV-aware routing

These are separate features that pair well. Disaggregation splits prefill and decode. KV-aware routing chooses workers based on cache locality. Many production deployments use both, but you can reason about them independently.

For router behavior, see Router: Disaggregated Serving and KV Cache Aware Routing.

Before production

  • Confirm NIXL initialized on each worker (the log check above).
  • Confirm prefill and decode workers agree on model, dtype, block size, and KV layout.
  • Confirm pods have the required GPU and shared memory.
  • Confirm frontend/router flags match your routing strategy.
  • Run benchmarks inside the cluster, not through local port-forwarding.
  • If you scale across nodes, validate the RDMA transfer path — see Enable multi-node transfer.

Use Dynamo Benchmarking to compare aggregated and disaggregated configurations under the same workload.

Next steps

  1. Start from a matching Dynamo Recipe when one exists.
  2. Read the backend-specific disagg.yaml for your engine.
  3. Use Sizing with AIConfigurator or DGDR to choose prefill/decode sizing.
  4. Validate with Dynamo Benchmarking.