For deep references, see Install with Helm and
Install on OpenShift. This page distills both into
the shortest path to a working inference.
Prerequisites
- Kubernetes 1.28+ with at least one GPU node labeled
nvidia.com/gpu.present=true. - Helm 3.8+ and
kubectl(orocon OpenShift) configured against your cluster. - An access token from the Tensormesh team. Authenticate with
helm registry login ghcr.iobefore installing — see Helm install.
Step 1 — install the operator
Put your configuration in a values file rather than--set flags. Create
tensormesh-values.yaml:
tensormesh-values.yaml
Running 1/1:
| Pod name | What it is |
|---|---|
tensormesh-operator-<hash>-<id> | The operator (controller-manager Deployment). The two-segment suffix (<hash>-<id>) marks it as Deployment-owned. One per cluster. |
tensormesh-operator-default-engine-<id> | The engine (LMCache cache server, DaemonSet). One per GPU node. The operator creates it from the LMCacheEngine CR, so it appears a few seconds after the operator. |
...-test-... pod, that’s a leftover helm test pod — harmless; delete it with kubectl delete pod <name> -n tensormesh-operator.)
Step 2 — deploy vLLM connected to the engine
The chart creates a ConfigMap namedtensormesh-operator-default-engine-connection
containing the kv-transfer-config.json that tells vLLM how to reach the engine over
the MP connector. The Deployment below mounts that ConfigMap and passes it as
--kv-transfer-config.
If you already run your own vLLM Deployment, do not replace it with this demo manifest.
Use Modify an Existing Deployment for the
minimum patch set instead.
vllm-demo.yaml:
vllm-demo.yaml
Step 3 — fire two identical requests, see the cache work
The simplest possible “is LMCache working?” test is to send two identical long prompts. The first populates the cache; the second should reuse the stored KV blocks. Port-forward the service in one terminal:If port
8000 is already in use locally, forward to a different local port — e.g.
... 8001:8000 — and use that port in the requests below.PAYLOAD variable (rather than inlining $(jq …) inside a multi-line
curl) makes it safe to copy-paste and guarantees both calls send the identical
prompt:
Stored N tokens after call 1 and
Prefetch request completed ... prefix hits=N during call 2.
What a successful run looks like
What a successful run looks like
Sample output from a Qwen3-8B install on a single A100 40 GB, prompt repeated 30 times
for ~514 tokens:Call 2 was ~2.7× faster than call 1 (1.466 s → 0.548 s). The engine reports (~half the prompt was the shared prefix, so 49.8% is the expected hit ratio.)
2/2 prefix hits from L1, meaning the second request reused the KV blocks stored during the first.
vLLM’s own metrics line confirms the same number from the inference side:Step 4 — run the benchmark
For a more realistic measurement than two curls, usevllm bench serve to compare a cold
pass against a warm pass. Run two passes with the same --seed so the prompts repeat
and the second pass hits the cache.
Rather than installing the vLLM CLI locally, run it inside the cluster as a Job — the
vLLM image already ships the CLI, talks to the inference Service directly (no
port-forward), and a Job runs it once to completion and stops. The benchmark is a pure
HTTP client, so it needs no GPU.
Save as benchmark-job.yaml:
benchmark-job.yaml
Serving Benchmark Result blocks. A working cache shows pass 2 with
much lower TTFT (mean/p50/p99). The speedup scales with --random-input-len: with a fixed
--seed, both passes send the same 20 prompts, so each unique body is prefilled on the cold
pass and served from cache on the warm pass — bigger --random-input-len → bigger delta.
Example result from a working run with --num-prompts 20, --random-input-len 20480, and
--seed 32:
Keep
num_prompts × random-input-len of KV under engine.spec.l1.sizeGB (else pass 1’s
entries evict before pass 2 reads them — warm misses), and random-input-len ≤ --max-model-len (32768 in the Deployment above).ttlSecondsAfterFinished):
Why a
Job and not a Deployment? A Job runs its pod to completion and stops — the
right shape for a one-shot benchmark. A Deployment would restart the benchmark forever.Step 5 — observability
For the full OpenTelemetry stack — an OTel Collector that receives metrics and traces from the engine, a PrometheusServiceMonitor, and optional Tempo tracing — see the dedicated
Observability page. It covers the prerequisites, the
observability.* values, and how to verify telemetry is actually flowing.
A quick liveness check without the full stack — the operator writes status back to the
LMCacheEngine CR:
PHASE should be Ready and READY should equal DESIRED (one per GPU node).
Query the engine’s metrics
If you enabled observability (observability.enabled=true — see the
Observability page), the OTel Collector re-exposes the engine’s
metrics on port 8889 for Prometheus. After some traffic (Steps 3–4), port-forward and
grep for the lmcache_ series:
lmcache_mp_* gauges and counters:
lmcache_mp_* series confirms the engine is exporting metrics through the
Collector. Empty output means observability isn’t enabled yet, or no traffic has hit the
engine.
Port
8889 is the Collector’s Prometheus exporter (LMCache application metrics).
Don’t confuse it with 8888 on the …-monitoring service, which serves the Collector’s
own otelcol_* internals.Cleanup
The ordering principle: remove theLMCacheEngine CR before the CRD or the namespace,
while the operator is still alive to run its finalizer. helm uninstall does this for
you — its pre-delete hook deletes the CR first, then removes the operator.
Next steps
Install with Helm
Full chart reference, install modes, every tunable value.
Configuration
Every
values.yaml key, with example overlays.Observability
Metrics, dashboards, and performance tuning.
Troubleshooting
Pending pods, image pull, hung uninstall, ownership conflicts.
