A reproducible reference deployment of vLLM serving a model with LMCache as its KV-cache backend, with KV-cache events published over ZMQ. Use it to verify cache-aware behaviour end to end:
- a prefix-cache hit on a repeated long prompt prefix (lower latency, prefill skipped), and
- a live KV-cache event stream (
BlockStored/BlockRemoved/AllBlocksCleared) that a cache-aware router or controller can consume.
The manifests here are intentionally minimal and explicit so they can serve as a
starting template for your own automation (an operator, a Helm release, or plain
kubectl apply).
Two engine references live here. This page walks the vLLM path; the SGLang sibling (same LMCache backend + ZMQ event wire) is at
manifests/sglang-lmcache/with its own README.
You need an NVIDIA GPU for the full stack — vLLM loads weights on CUDA and LMCache offloads KV from GPU memory. See
GPU-RUNBOOK.mdfor how to size GPU memory and pick a card. If you only want to validate the event wiring and prefix-cache behaviour without a GPU, use the CPU-only path.
| Path | What |
|---|---|
VERSIONS.md |
Pinned images / models / chart. Read first. |
GPU-RUNBOOK.md |
GPU sizing (VRAM math), shape/card table, multi-card tensor-parallelism. |
kind/cluster.yaml |
Local kind cluster (NodePorts for the API + ZMQ; the ZMQ NodePort is used by the vLLM path — the SGLang manifest deliberately doesn't node-expose ZMQ). |
manifests/ |
GPU reference Deployment + Service. |
manifests/cpu-local/ |
CPU variant (no LMCache): prefix-cache hit + KV events. |
manifests/sglang-lmcache/ |
SGLang + LMCache reference (the second engine) — GPU; the hand-built template the (sglang, LMCache) adapter mirrors. See its README for the event-wire scope, validation split, and caveats. |
helm/values-reference.yaml |
Upstream vLLM Production-Stack chart path (alternative to the raw manifests). |
scripts/ |
ZMQ event subscriber, prefix-cache-hit test, synthetic publisher, tests. |
captures/ |
Where you save your event-stream sample and a cache-hit screenshot. |
brew install kind # or your platform's installer; kubectl + helm also required
kind --version # >= v0.23For the GPU path you additionally need an NVIDIA GPU host (or a managed GPU cluster), the NVIDIA Container Toolkit / device plugin, and — for gated models — a Hugging Face token.
Size the GPU first with
GPU-RUNBOOK.md. The 8B reference model fits on a single 24 GB card.
# 1. Cluster + GPU. On the GPU host, install the NVIDIA Container Toolkit and set
# the nvidia runtime as Docker's default FIRST, then create the cluster:
kind create cluster --name inference-cache-substrate --config kind/cluster.yaml
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm install nvdp nvdp/nvidia-device-plugin -n kube-system
kubectl get nodes -o json | jq '.items[].status.allocatable["nvidia.com/gpu"]' # expect "1"
# (Or use any managed GPU cluster that advertises nvidia.com/gpu — the manifests
# are identical; only the cluster differs.)
# 2. Pin the image to a real digest (the manifest ships a non-applyable placeholder
# on purpose — see VERSIONS.md), then create the HF token secret:
kubectl create namespace cache-substrate
kubectl -n cache-substrate create secret generic hf-token --from-literal=token="$HF_TOKEN"
# 3. Deploy.
kubectl apply -f manifests/namespace.yaml -f manifests/deployment.yaml -f manifests/service.yaml
kubectl -n cache-substrate rollout status deploy/vllm-lmcache-llama-8b --timeout=20m
# 4. Subscribe to the KV-cache event stream and save a sample.
pip install -r scripts/requirements.txt
python scripts/kv_events_subscriber.py --endpoint tcp://localhost:30557 \
--topic kv-events --max 200 --json | tee captures/kv-events-sample.jsonl
# 5. Demonstrate the prefix-cache hit (run while the subscriber is watching).
./scripts/prefix_cache_hit_test.sh # save the output to captures/- Prefix-cache hit: request 2 (same long prefix) is faster than request 1, and
vLLM's
prefix_cache_hitscounter increases. - Event stream: the subscriber prints
BlockStoredevents (with block hashes) during request 1; the saved sample contains metadata only — hashes and counts, never prompt text or token content.
helm/values-reference.yaml deploys the same stack
via the vLLM Production-Stack chart, if you prefer Helm over raw manifests. It
disables the chart's built-in router (this reference is about cache state and
events, not routing).
You can exercise the whole engine-config path without a GPU — both a prefix-cache hit and the KV-cache event stream. vLLM's v1 engine runs on CPU (vLLM >= ~0.21) and the event publisher works there too; it is just slower and has no LMCache offload. Uses a tiny model on vLLM's CPU build.
Verified (vLLM 0.21.0 CPU image, arm64): cold request ~31s, warm same-prefix request ~1.4s,
vllm:prefix_cache_hitsincremented, and realBlockStoredevents were captured over ZMQ with token content redacted. It needs enough RAM — see the memory note inmanifests/cpu-local/deployment.yaml.
Match the image to your host arch.
manifests/cpu-local/deployment.yamldefaults to the-arm64image tag. On x86_64 hosts, change it tovllm/vllm-openai-cpu:latest-x86_64first (the tags are arch-specific):sed -i 's/latest-arm64/latest-x86_64/' manifests/cpu-local/deployment.yaml.
kind create cluster --name inference-cache-substrate --config kind/cluster.yaml
kubectl apply -f manifests/namespace.yaml -f manifests/cpu-local/deployment.yaml
kubectl -n cache-substrate rollout status deploy/vllm-cpu-sanity --timeout=30m
pip install -r scripts/requirements.txt
python scripts/kv_events_subscriber.py --endpoint tcp://localhost:30557 --topic kv-events &
MODEL=Qwen/Qwen2.5-0.5B-Instruct ./scripts/prefix_cache_hit_test.shIf you can't pull the image or run a cluster, you can still confirm the event-decode + token-redaction path with the synthetic publisher — it emits vLLM-shaped frames, no image required:
pip install -r scripts/requirements.txt
python scripts/kv_events_synthetic_publisher.py --bind 'tcp://*:5557' &
python scripts/kv_events_subscriber.py --endpoint tcp://localhost:5557 --max 4
python scripts/test_kv_events.py # asserts token_ids never surfaces; token_count kepttest_kv_events.py is the regression check for the decode + token-redaction
logic. It is run manually (the repo's CI is Go-only and has no Python step), so
run it after changing the subscriber.
scripts/canary_c2_reconcile.sh is a GPU-free,
on-demand canary for the C2 reconciler: it brings up a kind cluster, runs the
controller, applies a legacy compatibility CacheBackend with
backendConfig.profile: cpu, and asserts
the controller stands up a healthy serving backend (Ready condition True, endpoint
published) and owner-ref garbage collection when the CR is deleted. It exercises
the reconciler against real pods — the gap the envtest unit tests can't cover.
An optional traffic block drives prefix traffic through the Service and asserts
an engine prefix-cache hit, but it is opt-in (SKIP_TRAFFIC=0) and requires a
separately wired engine — see the script for the port-forward target and
metric source.
docs/reference-stack/scripts/canary_c2_reconcile.shLike the full-chain canary it is on-demand, not a blocking gate: it needs
Docker + kind, pulls the vLLM CPU image, and wants ~10+ GiB of Docker VM RAM. The
cpu profile runs a GPU-free vLLM engine (prefix caching + KV events, no LMCache
offload); real LMCache offload still needs a GPU (the default gpu profile).
When the controller is installed in a cluster and the operator passes
--kvevent-subscriber-image=<ref> on the controller (the
subscriber-image make target emits the well-known dev tag; pin to a
digest in production), the pod-mutating webhook auto-attaches the
kvevent-subscriber as a sidecar to every engine pod whose labels match a
CacheBackend.spec.engineSelector and whose backend sets
spec.observation.modelID (or the deprecated backendConfig.model fallback).
The subscriber's identity flags (--replica-id,
--tenant-id, --model-id, --hash-scheme) are derived from the CR + pod
— no operator-supplied flags, no out-of-band kubectl port-forward + manual
binary launch on the demo path.
The default install ships with the flag unset and therefore does not
auto-attach: a nonexistent image would put the sidecar container into
ImagePullBackOff, which would keep the engine pod from going Ready and
turn the cache into a serving dependency. Operators opt in by passing the
image once they have one ready to ship.
The shape decision and rationale are in
docs/design/kvevent-subscriber-wiring.md;
the end-to-end auto-attach behaviour is gated by the webhook envtest
(internal/webhook/pod/envtest_integration_test.go), which boots a real
apiserver, installs the webhook, applies a CacheBackend, creates a labeled
engine pod, and asserts the persisted pod carries the kvevent-subscriber
container with flags derived from the CR. Run it locally with:
KUBEBUILDER_ASSETS=$(make test-env | tail -1) go test ./internal/webhook/pod/...scripts/canary_e2e.sh is a complementary GPU-free
canary that exercises the subscriber binary's data path end-to-end on the
host (no Kubernetes admission in the loop): CPU vLLM engine → kvevent- subscriber → policy server → index. It drives prefix traffic and asserts
both an engine prefix-cache hit and that the server index populated
(inferencecache_index_entries > 0). Builds the binaries, manages the engine
container, cleans up after itself, exits non-zero on failure.
Because this canary launches the engine in plain Docker (no K8s admission), the subscriber is hand-launched on purpose — the binary's wire protocol is what the test exercises. The in-cluster auto-attach path is covered by the envtest gate above.
The subscriber additionally scrapes the engine's Prometheus /metrics and emits
a per-replica ReplicaStats (cacheMemoryBytes, hitRate, pressure) on a
configurable tick (default --stats-interval=10s), so the policy server's
/snapshot.replicas[] (and the CacheIndex.status.replicas[] surface the
controller scrapes from it) populate alongside the prefix stream. Map
cacheMemoryBytes to the engine's KV cache by passing
--engine-cache-size-bytes (it is multiplied by the active *_cache_usage_perc
gauge); leave it 0 to publish cacheMemoryBytes=0 and let the other fields
populate normally.
If the engine serves more than one model on the same /metrics endpoint, pass
--engine-model-name=<served-model> to filter by vLLM's model_name label —
that label tracks the engine's identifier, which is independent of the cache
plane's --model-id index key. Leave it empty when the engine serves one model
and you want the scraper to consume every series unfiltered.
docs/reference-stack/scripts/canary_e2e.shSame on-demand profile as the reconciler canary (Docker, vLLM CPU image, ~10+ GiB Docker VM RAM). Run locally or wire into a scheduled/dispatch job.
kind delete cluster --name inference-cache-substrate