Skip to main content
BlogAI/ML

LMCache Multi-Process Mode: Shared KV Cache for MoE Serving

LMCache multi-process mode replaces per-rank KV offload with one shared host-memory pool for vLLM DP+EP serving. A close reading of the benchmark, config and limits.

Matthews Wong

Software Engineer

October 202611 min read
LMCache Multi-Process Mode: Shared KV Cache for MoE Serving

Key Takeaway

LMCache multi-process mode runs the KV cache as a separate server that every vLLM data-parallel rank shares, replacing eight isolated per-rank offload pools with one host-memory pool. In LMCache's single 8x H100 Qwen3-235B test, mean time-to-first-token fell from 3.98 s to 0.29 s, while the headline 10x is the p99 figure.

The number that made me open this post was in its title: a 10x improvement for mixture-of-experts inference. A figure that round usually hides a specific denominator, so I read the LMCache write-up, the multi-process documentation behind it and vLLM's data parallel guide side by side before forming an opinion.

I did not run this setup; an 8x H100 node is not on my desk. Everything below is a close reading of five primary sources, with the arithmetic redone and the caveats the headline leaves out. Each figure is attributed to the page I read it on, and anything those pages do not say is marked as unknown rather than guessed.

Why DP plus EP leaves each rank with its own KV cache

The baseline in the post is vLLM with data parallelism for attention and expert parallelism for the experts. vLLM's data parallel guide describes each DP rank as a separate core engine process that talks to the front end over ZMQ sockets, and states that each DP engine has an independent KV cache. Weights for attention are replicated per rank; the experts are sharded across them once expert parallelism is enabled.

That layout is good for throughput and bad for reuse. The LMCache post says each request is pinned to one DP rank, so a multi-turn conversation only benefits from a cached prefix if its next turn lands on the same rank that served the previous one. The vLLM guide says its internal load balancer uses each engine's running and waiting queues, that KV-cache-aware routing is not yet implemented, and that prefix caching gains depend on directing prompts sensibly without saying how.

The consequence is fragmentation. Even with CPU offload enabled, the post says caches are not shared between ranks, so overlapping prefixes seen on different ranks are recomputed. For chat, agents and RAG, where each turn extends the last, that is repeated prefill work you pay for in time-to-first-token. Note this is a claim about mechanism; the post reports no cache hit rates, so how often it happened in the test is not measured.

The baseline: in-process offload, one pool per rank

The in-process baseline uses vLLM's native offloading with 50 GB of host memory per rank. Across eight ranks that adds up to 400 GB, and the launch looks like this.

# Baseline from the LMCache post: 8-way DP, experts sharded by EP,
# and one isolated 50 GB host-memory offload pool PER RANK (8 x 50 = 400 GB).
VLLM_USE_FLASHINFER_MOE_FP8=0 VLLM_USE_DEEP_GEMM=1 \
vllm serve Qwen/Qwen3-235B-A22B-Instruct-2507-FP8 \
  --data-parallel-size 8 \
  --enable-expert-parallel \
  --gpu-memory-utilization 0.8 \
  --max-num-batched-tokens 1024 \
  --kv-offloading-size 50 \
  --disable-hybrid-kv-cache-manager \
  --max-model-len auto

# Each rank can only reuse prefixes that rank itself has seen. A follow-up turn
# routed to a different rank than its first turn finds nothing and recomputes.

Two details matter later. First, the post notes that the disable-hybrid-kv-cache-manager flag is required for hybrid-memory-allocator models with the native offloading connector. Second, the 400 GB total is the same number the multi-process run uses for its single pool, so the comparison gives both sides equal host memory. What differs is whether the memory is one pool or eight.

What multi-process mode changes architecturally

In multi-process mode LMCache stops being a library inside each vLLM process and becomes a standalone service. The documentation says engines connect to it through a ZMQ or gRPC request transport, and the README describes it as a daemon that runs separately from the inference engine. The comparison below is assembled from the post, the multi-process docs and the README.

ConcernIn-process offloadLMCache multi-process mode
Where the cache livesInside each vLLM rank, process-local and fragmentedOne server per node, one shared L1 host-memory pool
Cross-rank prefix reuseNone; each rank only sees its own historyAny rank can reuse what any other rank stored
Cache failureShares the engine process, so it shares its fateDocs state a cache failure does not take down the engine
Hashing and memory managementCompetes with vLLM threads in the same processRuns in the LMCache process, off vLLM's Python threads
Lower tiersHost memory offload only in the baselineL1 is DRAM; L2 backends such as local storage and remote connectors
ReachOne rankOne node today; peer-to-peer across nodes is roadmap

Inside the server, the docs describe a StorageManager that coordinates the L1 pool and optional L2 adapters, with controllers for storing to L2, evicting from L1 and prefetching L2 hits back into L1. Eviction policies listed are LRU, ARC, IsolatedLRU and noop. The post's run uses LRU. L1 defaults to pinned CPU DRAM, with Device-DAX and an NVMe slab through GPUDirect Storage as alternatives.

The configuration that produced the result

The post publishes both launch lines, which is what makes it checkable. The MP run starts a server with a 400 GB L1 pool and LRU eviction, then launches the same DP plus EP vLLM command with a KV connector instead of the offload flags.

# Terminal 1: the LMCache server owns ONE host-memory L1 pool for the node.
# 400 GB is the pool the post used on its 8x H100 box.
lmcache server --l1-size-gb 400 --eviction-policy LRU

# Terminal 2: the same vLLM DP+EP launch, with the connector instead of
# --kv-offloading-size. The post also drops --disable-hybrid-kv-cache-manager
# and does not say why, so treat that as a second variable.
VLLM_USE_FLASHINFER_MOE_FP8=0 VLLM_USE_DEEP_GEMM=1 \
vllm serve Qwen/Qwen3-235B-A22B-Instruct-2507-FP8 \
  --data-parallel-size 8 \
  --enable-expert-parallel \
  --gpu-memory-utilization 0.8 \
  --max-num-batched-tokens 1024 \
  --max-model-len auto \
  --kv-transfer-config '{"kv_connector":"LMCacheMPConnector","kv_role":"kv_both"}'

# The post's load generator: multi-round chat at 2.0 QPS for 120 seconds.
lmcache bench engine --engine-url http://localhost:8000 \
  --workload multi-round-chat --mrc-qps 2.0 --mrc-duration 120

Read the difference carefully. The MP command has no kv-offloading-size and no disable-hybrid-kv-cache-manager, and the post does not explain either omission. So the two runs differ in more than the connector, and the published delta cannot be attributed to shared memory alone. If you reproduce this, change one thing at a time.

Keep your own A/B honest: run the baseline and the MP launch with every other vLLM flag identical, including the hybrid KV cache manager setting, and report mean and p99 time-to-first-token from the same load generator.

Reading the benchmark: what 10x actually is

The test used Qwen3-235B-A22B-Instruct-2507-FP8 on one server with eight H100 80 GB GPUs, vLLM 0.18.1 and LMCache 0.4.3-dev, driven by a multi-round chat workload at 2.0 QPS for 120 seconds. These are the post's table values with the ratios recomputed.

MetricIn-processLMCache MPRatio
TTFT mean3.98 s0.29 sabout 13.7x lower
TTFT p9913.55 s1.30 sabout 10.4x lower
Decode mean9.81 tok/s37.47 tok/sabout 3.8x higher
Decode p9934.27 tok/s45.14 tok/sabout 1.3x higher

The headline 10x matches the p99 time-to-first-token ratio, not the mean. The mean ratio is larger, and the post's bar chart labels it 13.6x, close to my 13.7x from the rounded table values. The decode figures are smaller gains, and the post does not define whether decode speed is per request or aggregate, so I would not quote the 3.8x without that caveat.

The post credits higher cache hit rates, less redundant prefill and less fragmentation, but shows no hit-rate data to confirm the split. It is one model, one machine, one workload and one QPS level, with no repetitions or variance, on a development build of LMCache. The conversation count and prompt and output lengths are not stated. Treat it as a strong existence proof for the mechanism, not a forecast for your cluster.

The trade-offs and where it can bite

A shared cache service is one more moving part on the hot path of every request. From the documentation, these are the constraints I would plan around before adopting it.

  • Version lockstep: the docs say to upgrade the server and the workers together, because the typed inline protocol is not compatible with older versions.
  • Worker sizing: the max-workers setting defaults to 1, and max-gpu-workers should be at least the number of vLLM instances sharing the server.
  • Isolated IPC mode works only with the vLLM MP connector, needs the cuda-python package on both sides and is incompatible with expandable_segments:True and vLLM sleep mode.
  • Scope: sharing is per node. Peer-to-peer sharing and prefill-decode disaggregation for cluster-wide reuse are listed as planned, with no dates.

The benchmark used a development build, LMCache 0.4.3-dev, and the docs ask for the latest dev branch of LMCache. Pin exact versions of vLLM and LMCache together, test on a release build, and treat the server as a component whose version bumps are coordinated with your engines.

There is also a capacity question the post does not answer. One pool means eviction pressure from every rank lands on the same memory, and the docs say eviction triggers at a 0.8 watermark and frees 0.2 of allocated memory per cycle by default. If your working set is larger than L1, a shared pool can evict one rank's useful prefix to make room for another's, which is a behaviour to watch, not one the benchmark exercised.

Running it: ports, processes and a Kubernetes shape

The deployment page uses four ports. The transport port is the one the engine connector must agree with, so a mismatch between the server port and the lmcache.mp.port value in the connector config is the first thing to check when engines cannot find the cache.

PortPurpose per the deployment page
6555LMCache transport port, set by port on the server and lmcache.mp.port on the connector
8080HTTP server variant, with a healthcheck endpoint for probes
9090Prometheus metrics, enabled by default
8000vLLM API in the page's sample test
# Server as the deployment page runs it (Docker, one node, 60 GB L1).
# --ipc host is what lets the engine and server share GPU memory handles;
# --isolated-ipc plus lmcache.mp.isolated_ipc on both sides removes that need.
docker run --runtime nvidia --gpus all --network host --ipc host \
  lmcache/standalone:nightly \
  /opt/venv/bin/lmcache server --l1-size-gb 60 --eviction-policy LRU \
  --max-workers 4 --port 6555

# vLLM side: the connector must point at the same transport port.
--kv-transfer-config '{"kv_connector":"LMCacheMPConnector","kv_role":"kv_both","kv_connector_extra_config":{"lmcache.mp.port":6555}}'

# Kubernetes: the server is a DaemonSet (one per GPU node), vLLM a Deployment.
kubectl create namespace multi-process
kubectl apply -f examples/multi_process/lmcache-daemonset.yaml
kubectl apply -f examples/multi_process/vllm-deployment.yaml

On Kubernetes the page deploys the server as a DaemonSet so each GPU node runs one, with host networking and the transport bound to 127.0.0.1. The DaemonSet does not request GPUs, the vLLM pods keep them, and LMCache pods scheduled onto nodes without GPUs crash with CUDA initialisation errors. The page lists at least 4 GPUs per node and the NVIDIA GPU Operator as requirements for its example.

What to do on Monday

Do not start by installing it. Start by finding out whether fragmentation is your problem at all, then run a controlled comparison.

  1. Confirm the shape: you are running more than one DP rank and your traffic is multi-turn, agentic or RAG. With one rank there is nothing to share.
  2. Record a baseline on your own workload: mean and p99 time-to-first-token from lmcache bench engine or your own replay, with your current offload flags.
  3. Start one LMCache server per node with an L1 size taken from the host memory left after the OS and vLLM, then switch only the connector and offload flags.
  4. Set max-gpu-workers to at least the number of engines on the node, and scrape port 9090 so you can see eviction and hit behaviour rather than infer it.
  5. Pin versions together, rerun at more than one QPS with repetitions, and only then decide; the published result is one point.

If your routing layer already sends a conversation to the same rank every time, the shared pool buys you less than the post suggests, because the fragmentation it removes is the cost of misrouting. In that case fixing routing may be the cheaper first experiment.

The lesson I take from this post is that in DP plus EP serving, KV cache locality is a property of your deployment topology, not just of your cache policy. Moving the cache out of the rank and into a shared node-level service removes a real source of recomputation, and the 0.29 s mean is plausible for that reason, but the 10x headline is a p99 figure from one dev-build run. Measure your own workload before you believe either number.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is LMCache multi-process mode?

It runs LMCache as a standalone service instead of a library inside each vLLM process. Engines connect to it over a ZMQ or gRPC transport, and pods on the same node share one L1 cache. The docs say a cache failure does not take down the inference engine.

Why does data parallel serving fragment the KV cache?

Each data-parallel rank in vLLM is a separate engine with its own independent KV cache, and each request is pinned to one rank. A follow-up turn that lands on a different rank cannot reuse the prefix and recomputes it. CPU offload alone does not share cache between ranks.

Is the LMCache 10x improvement for MoE inference real?

It is a real measurement but a narrow one. The 10x matches p99 time-to-first-token, 13.55 s down to 1.30 s, on one model, one 8x H100 server and one workload on a development build. The mean TTFT ratio was about 13.7x, and the two launch configs also differed beyond the connector.

How do I start the LMCache server for vLLM?

Run lmcache server with an L1 size and an eviction policy, for example lmcache server --l1-size-gb 400 --eviction-policy LRU. Then launch vLLM with a kv-transfer-config that names LMCacheMPConnector with role kv_both. The connector port must match the server transport port.

Does LMCache multi-process mode share cache across nodes?

Not yet. Sharing today is node-level: multiple DP ranks and instances on one node share the host-memory L1 pool. Peer-to-peer sharing and prefill-decode disaggregation for cluster-wide reuse are listed as planned, without dates.