- 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 singleLMCacheEngine 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. Setnamespace: 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; thelmcache.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-workloadNIXL 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
OneLMCacheEngine 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 aCacheBlendEngine 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:
LMCacheEngine with spec.pd set (no enforceHandshakeCompat
needed on the decoder).
Helm values reference
Engine pd spec
Verification
Engine reconciled
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: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.yamlbefore the engine isRunning— thelmcache-engine-connectionConfigMap does not exist yet and the pods fail to start; wait forkubectl get lmcacheengineto showRunning - NIXL RDMA backend fails to initialize — standard overlay CNI does not populate RDMA GIDs inside pods;
hostNetwork: true+dnsPolicy: ClusterFirstWithHostNetis required for RDMA to work - Using the same
VLLM_NIXL_SIDE_CHANNEL_PORTfor prefiller and decoder — withhostNetwork: trueboth pods share the host IP so the ports conflict; use5557for prefiller and5558for decoder - Missing
IPC_LOCKcapability — NIXL needs to pin memory for RDMA operations; addcapabilities.add: ["IPC_LOCK"]to the container security context - Workload namespace not PSS-privileged — Pod Security rejects
hostIPC: trueandhostNetwork: true; label the namespacepod-security.kubernetes.io/enforce=privileged

