Skip to main content
PD disaggregation splits the two phases of LLM inference — prefill (prompt processing, compute-bound) and decode (token generation, memory-bandwidth-bound) — onto separate vLLM instances. Each pool is sized and scheduled for its workload, and a router dispatches requests to the right pool. KV tensors produced by the prefiller are transferred directly to the decoder over NIXL (GPU-to-GPU via the NVIDIA Transfer Library), so the decoder never re-computes the prompt. PD disaggregation is most useful when:
  • TTFT degrades under heavy load because prefill and decode compete for the same GPU
  • you want to scale prefill and decode replicas independently to match traffic patterns
  • your cluster has GPU-to-GPU interconnect (NVLink, InfiniBand) that NIXL can exploit

How it works

A single LMCacheEngine DaemonSet serves both prefiller and decoder vLLM pods on every node. When spec.pd is set, the engine’s connection ConfigMap emits three kv-transfer-config keys: Each vLLM pod mounts the engine’s connection ConfigMap and reads the role-specific key directly via --kv-transfer-config "$(cat /etc/lmcache/kv-transfer-config-<role>.json)". No webhook is required for this approach. The router is a plain vllm-router Deployment that you deploy alongside your vLLM pods. The operator does not manage it — you configure the --prefill and --decode URLs directly in the Deployment args.

Prerequisites

Create a dedicated workload namespace for your vLLM pods and label it PSS-privileged (the pods use hostIPC: true and hostNetwork: true):

Install the chart

A single engine handles both prefiller and decoder vLLM pods. Set namespace: pd-workload so the engine lands in the same namespace as your vLLM pods.
my-values.yaml

Deploy vLLM and the router

Deploy your vLLM prefiller, decoder, and router together. Both vLLM Deployments opt in to the same engine; the lmcache.ai/pd-role annotation tells the webhook which kv-transfer-config key to inject.
vllm-pd.yaml
The ConfigMap lmcache-engine-connection is created automatically when the LMCacheEngine reaches Running state. Wait for the engine to be ready before applying vllm-pd.yaml: kubectl get lmcacheengine lmcache-engine -n pd-workload

NIXL RDMA networking

NIXL uses UCX for GPU-to-GPU KV transfer. UCX requires valid RDMA GIDs, which are derived from the host’s network interfaces. Under standard overlay CNI (each pod has its own network namespace) the GID table inside the pod is empty and UCX backend initialization fails. Workarounds: For the hostNetwork workaround, add these fields to both the prefiller and decoder pod specs and pre-set distinct NIXL ports:

Multiple models

One LMCacheEngine is shared across all models. Add a new router Deployment for each additional model, changing --prefill, --decode, --port, and the resource names:

CacheBlend PD

PD disaggregation also works with CacheBlend. Use a CacheBlendEngine for the prefiller role and a plain LMCacheEngine for the decoder. Because CacheBlend ships a modified vLLM build, set pd.enforceHandshakeCompat: false on the prefiller to disable strict NIXL version negotiation:
The decoder side uses a standard LMCacheEngine with spec.pd set (no enforceHandshakeCompat needed on the decoder).

Helm values reference

Engine pd spec

Verification

Engine reconciled

The engine should be Running and have a lmcache-engine-connection ConfigMap containing both prefiller and decoder keys. The ConfigMap is the gate the webhook reads — if it is missing, the pod starts without NIXL injection and PD transfers will not work.

Router running

Injection happened

After the vLLM pods start:
Expected output includes VLLM_NIXL_SIDE_CHANNEL_HOST and VLLM_NIXL_SIDE_CHANNEL_PORT.

Send a request

Common mistakes

  • Omitting namespace: <workload-ns> on the engine — the engine and its connection ConfigMap land in the operator namespace; the vLLM pods mount the ConfigMap by name, so make sure the engine is in the same namespace as the pods
  • Applying vllm-pd.yaml before the engine is Running — the lmcache-engine-connection ConfigMap does not exist yet and the pods fail to start; wait for kubectl get lmcacheengine to show Running
  • NIXL RDMA backend fails to initialize — standard overlay CNI does not populate RDMA GIDs inside pods; hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet is required for RDMA to work
  • Using the same VLLM_NIXL_SIDE_CHANNEL_PORT for prefiller and decoder — with hostNetwork: true both pods share the host IP so the ports conflict; use 5557 for prefiller and 5558 for decoder
  • Missing IPC_LOCK capability — NIXL needs to pin memory for RDMA operations; add capabilities.add: ["IPC_LOCK"] to the container security context
  • Workload namespace not PSS-privileged — Pod Security rejects hostIPC: true and hostNetwork: true; label the namespace pod-security.kubernetes.io/enforce=privileged