Part U · Where it falls over › Multi-node KV offloading

Module 8·Part U — Where it falls over·18 min

Multi-node KV offloading

Five backends behind one vLLM connector interface — native, LMCache, Mooncake, 3FS, NIXL — three storage tiers, and the constraint nobody documents: which of them work with which parallelism, and what a recurrent state does to all of it.

The core mental model

KV offloading extends your effective cache beyond HBM by pushing cold blocks down a hierarchy — GPU HBM, then CPU DRAM (up to roughly 250 GB per GPU), then shared storage measured in terabytes and visible across the cluster. The economics are straightforward: llm-d reports that loading cached blocks is “typically faster than recomputing them — up to 16× faster for long prompts”, so if you can find a block anywhere cheaper than a prefill, fetching it wins. For K3 specifically, Module 3’s measured 7.95 GB per-GPU pool makes this more than an optimisation:

colder, larger, furtherGPU HBM~8 GB measured — rank scopeCPU DRAMup to ~250 GB — node scopeshared storageTB+ — cluster scope, cross-replica reuse
The tiers a block can fall through, and how far each is findable. HBM is per rank, DRAM per node, shared storage across the cluster — and only the last makes a block computed by one replica reusable by another, which is the capability that changes system design rather than tuning it. Loading a cached block beats recomputing it by up to 16× on long prompts.

vLLM exposes this through a connector interface with five named backendsnative, lmcache, mooncake, 3fs and nixl — currently configured through a JSON string to --kv-transfer-config, with an open RFC proposing simpler top-level flags (--kv-offloading-size, --kv-offloading-backend). They are not interchangeable. native is vLLM’s built-in OffloadingConnector dispatching to a CPU or shared-storage tier — simplest, fewest dependencies, single-instance scope. LMCache is the one llm-d’s deployment guides formally cover with explicit recipes. Mooncake is a distributed store providing hash-based prefix caching across instances, so a block cached by one replica can be found by another — the genuinely multi-node capability. NIXL is transport rather than storage, built on UCX/libfabric/EFA, and is what both engines use to move state between disaggregated prefill and decode workers. KVBM is NVIDIA Dynamo’s equivalent, with its own three-layer design over NIXL transport.

Your instinct that some options only work with specific parallelisms is right in substance, and the honest finding is that the documentation is thin on exactly this point. The vLLM RFC does not specify parallelism compatibility per backend; llm-d’s page does not discuss TP/PP/EP/DCP restrictions at all. What is stated: Mooncake requires all participating instances to share the same tensor_parallel_size, which makes sense — a KV block is laid out per TP rank, so a block written by a TP8 instance is not meaningful to a TP16 one. Treat that as the shape of the general constraint rather than the only instance of it, and verify on your own configuration, because a cache that silently never hits looks exactly like a cache that is working.

What the reports actually measured

The backends behind vLLM’s connector interface:

BackendKindScopeNotes
nativebuilt-in OffloadingConnectorsingle instanceCPU tier or shared-storage tier
lmcachecache engineinstance + remotethe one llm-d formally documents
mooncakedistributed storecross-instancehash-based prefix caching, block-hash dedup
3fsstorage backendcluster
nixltransport, not storagecross-nodeUCX / libfabric / EFA; used for disaggregation
KVBM (Dynamo)offloading systemCPU + diskthree layers over NIXL

Storage tiers, per llm-d:

TierCapacityScope
GPU HBM7.95 GB/GPU measured on K3/H200rank
CPU DRAMup to ~250 GB per GPUnode
Shared storageTB+cluster

Reported effects:

ClaimFigure
Loading cached blocks vs recomputingup to 16× faster for long prompts
Contiguous memory layout (vLLM ≥ 0.12.0)4–5× CPU transfer throughput

Documented constraints — note how short this list is:

ConstraintStated by
All Mooncake instances must share the same tensor_parallel_sizeMooncake vLLM integration docs
Multi-node EP/DP requires RDMA or NVLinkvLLM K3 post
Per-backend TP/PP/EP/DCP compatibilitynot documented in the RFC or llm-d’s page

And the K3-specific complication, from Modules 2 and 5:

StateOffloadable how
MLA paged KV (24 layers)per page, like any KV
KDA recurrent state (69 layers)whole object only; needs snapshot/copy-on-write/donate
vLLM’s disaggregation view“token-level MLA cache” and “request-level KDA state”

Critical thinking

Which backend should you actually pick, and what does each buy?

Pick by scope — how far you need a cached block to be findable — because that is the axis the backends actually differ on.

native — one instance, CPU or shared storage. The right default. No extra dependency, no extra process, no new failure mode, and it captures the biggest win available: a block that fell out of HBM is in DRAM instead of gone. If your traffic has temporal locality within a replica — a user’s conversation returning to the same replica — this is most of the benefit for none of the operational cost. Start here.

LMCache — richer engine, formally supported by llm-d. The distinction worth knowing is that third-party engines “own their own indexing, memory management, and storage” rather than borrowing vLLM’s. That buys smarter eviction, better layout and more tiers; it costs you a second system to understand and operate. llm-d covering it with explicit recipes matters more than it sounds — the difference between a documented path and “follows the same pattern” is measured in weeks.

Mooncake — the one that is genuinely multi-node. Hash-based prefix caching across instances with block-hash deduplication means a block computed by replica A is findable by replica B. That is the capability that changes system design rather than tuning it, and it is the answer to your question about multi-node strategies. It matters most when a shared prefix is used by many users across many replicas — a common system prompt, a shared document, a codebase context — because without it every replica prefills that prefix independently and caches it independently, which is N× the work and N× the memory for one piece of content.

NIXL — not really in this list. It is transport. You use it because you are disaggregating (Module 9), not because you are offloading, and both engines already use it for K3’s prefill→decode handoff. Do not compare it against the others; you may well use it and one of them.

The decision, compactly:

IfUse
Single replica, want cheap capacitynative
Want a mature engine with documented recipesLMCache
Many replicas share prefixesMooncake (or LMCache + Mooncake store)
Disaggregating prefill and decodeNIXL, alongside the above
On NVIDIA DynamoKVBM

And the thing to check before any of them: whether --enable-prefix-caching is on at all. vLLM leaves it off by default (Module 4). Offloading extends a cache you are not using otherwise.

Why would a backend care about parallelism, and how do you find out whether yours does?

Because a KV block is not an abstract object — it is a specific memory layout produced by a specific sharding of the model — and a consumer that shards differently cannot interpret it.

The mechanism. Under tensor parallelism, attention heads are split across ranks, so rank ii holds the KV for its own heads only. A “block” for token range [a,b)[a,b) on a TP8 instance contains one eighth of the heads. A TP16 instance expects one sixteenth, arranged differently. Byte-for-byte the cached block is meaningless to it. Hence Mooncake’s stated requirement that all instances share the same tensor_parallel_size — it is not a limitation so much as a statement of what a shared cache can mean.

Which other parallelisms should worry you:

  • Pipeline parallelism partitions by layer, so a given rank holds KV for only its layers. A cache entry is therefore per-(rank, layer-range), and two deployments with different PP degrees partition differently. SGLang uses PP8 for prefill on K3 (Module 5), so this is live.
  • DCP shards MLA KV by token position. A block cached under DCP8 is one eighth of the positions; under DCP4 it is one quarter. Different DCP degree, incompatible cache.
  • Expert parallelism does not affect KV layout — experts hold weights, not cache — so EP degree should be free. This is the one that does not matter.
  • Quantized KV is a silent one: a cache written with FP8 KV is not readable by an instance configured for BF16 KV, and nothing about the parallelism configuration tells you that.

How to find out, since the documentation will not tell you:

  1. Read the connector’s key derivation. Whatever goes into the block hash defines what must match. If TP rank, layer range or dtype are in the key, mismatched instances simply miss rather than corrupt — which is the safe design and, I would hope, what these implement.
  2. Test the negative case explicitly. Bring up two instances with different TP degrees pointed at one store, send the same prefix twice, and check whether the second hits. A cache that never hits is invisible in correctness terms and shows up only as unexplained cost.
  3. Monitor hit rate as a first-class metric, not a debug counter. It is the only signal that distinguishes “offloading configured” from “offloading working”, and the failure is silent.
  4. Keep the fleet homogeneous. The simplest way to satisfy an undocumented compatibility requirement is not to vary the thing it might depend on. Given how thin the documentation is, I would treat parallelism configuration as part of the cache’s schema and version it accordingly.

What does a KDA recurrent state do to an offloading design?

It breaks the three properties every offloading system is built on, and the workarounds are the ones Module 2 already established for prefix caching — which is a hint that they are the general answer for hybrid models.

The three broken properties:

  • Page granularity. KV offloading moves blocks: you can evict half a sequence’s KV and keep the rest, fetch a prefix without the suffix, and dedupe at block-hash granularity. A KDA state is one object for the whole request. There is no “half a recurrent state”, so it is offloaded whole or not at all, and it cannot be deduplicated against anything.
  • Immutability. A KV block, once written, never changes, which is what makes it safe to share across requests and safe to hold in a remote store while a request runs. A KDA state is overwritten every token, so anything you offloaded is stale the moment the request produces another token. Offloading a live request’s KDA state is meaningless; only checkpoints are offloadable.
  • Position addressability. A cache lookup asks “do you have the state for this token prefix?” For MLA that is a hash of the tokens. For KDA there is no state for an arbitrary prefix — only for the positions where a checkpoint was taken.

What that forces, and both engines converge on it:

  • Only checkpoints are cacheable. vLLM’s interval checkpointing (e.g. every 32 K tokens, plus automatic prompt-end retention) defines the set of KDA states that exist, and therefore the set that could ever be stored or transferred. A prefix that does not land on one replays from the checkpoint below.
  • The transfer object is two things. vLLM’s NIXL connector treats a shared page as “token-level MLA cache and request-level KDA state, including convolution and recurrent state”. Any offloading design has to move both, at two granularities, and a partial hit means two lengths to reconcile — which is exactly what vLLM describes comparing.
  • Copy-on-write is mandatory, not an optimisation. SGLang lists copy-on-write, snapshot and donate as the operations added to make a mutable state safely shareable. Fetching a checkpoint from a remote store and continuing from it is a branch, and branches copy.

The upside, which is real: because only 24 of 93 layers have per-token KV, the paged portion of K3’s cache is roughly a quarter of what a uniform full-attention model would produce. So there is less to offload per token, and offloading is correspondingly less critical for context length than it would otherwise be. Where it still matters enormously is cross-request prefix reuse — the shared system prompt, the shared document — because that is a capacity-independent win, and it is exactly what Mooncake’s cross-instance store is for.

Is offloading actually worth it for K3, or is DCP the better lever?

They solve different problems and the honest answer is that DCP is the stronger lever for context length while offloading is the stronger lever for reuse across requests — so which you need depends on whether your prefixes are shared.

DCP’s case. SGLang’s reported 7.9× logical KV capacity with DCP8 is a large, direct increase in how much context fits, achieved by sharding rather than by moving data anywhere slower. There is no fetch latency, no cache miss, no consistency question: the state is in HBM, just distributed. For “few users, enormous contexts” it is close to strictly better than offloading. Its limits are that it costs an all-to-all per layer, it does not help the KDA state, and — critically — it does not help request count, because per-request overheads are untouched.

Offloading’s case. It attacks something DCP cannot: a block computed for request A being reused by request B, possibly on a different replica, possibly hours later. That is not a capacity multiplier, it is a work eliminator, and llm-d’s “up to 16× faster than recomputing for long prompts” is the relevant figure. For workloads where many users share a large prefix — a system prompt, a document, a repository — this can dominate everything else, because the alternative is every replica prefilling the same 50 K tokens independently.

How to tell which you need, which is a question about your traffic rather than your hardware:

Your workloadBetter lever
Few users, very long unique contextsDCP
Many users, large shared prefixesOffloading (Mooncake, cross-instance)
Many users, short unique contextsNeither — you are running-request-cap bound (Module 7)
Conversations returning to one replicanative CPU offload, cheaply

And they compose. Nothing prevents running DCP for capacity and a cross-instance store for reuse; they operate on different axes. The reason to be deliberate is operational cost — a distributed KV store is a stateful system with its own failure modes, capacity planning and consistency behaviour, added to a deployment that already takes 13 minutes to restart.

The measurement that should drive the decision is prefix cache hit rate on your real traffic with native offloading enabled and prefix caching on. If it is already high, a distributed store adds little; if it is low because requests scatter across replicas, that is precisely the gap Mooncake fills, and you will see it as a hit-rate jump rather than as a latency improvement.

Self-check

Name vLLM's five offloading backends and what distinguishes each.

native — built-in OffloadingConnector to a CPU or shared-storage tier, single-instance scope, no extra dependency. lmcache — a full cache engine owning its own indexing and storage, and the one llm-d formally documents with recipes. mooncake — a distributed store with hash-based prefix caching and block-hash dedup across instances, the genuinely multi-node option. 3fs — a storage backend. nixltransport, not storage (UCX/libfabric/EFA), used for disaggregated prefill→decode transfer rather than for offloading. Configured today via JSON to --kv-transfer-config, with an RFC proposing --kv-offloading-backend.

Why would an offloading backend depend on parallelism, and what is actually documented?

Because a KV block is a specific memory layout produced by a specific sharding: under TP8 a block holds one eighth of the heads, which is meaningless to a TP16 instance. PP partitions by layer and DCP by token position, so both also change what a block is; EP does not, since experts hold weights not cache; and KV dtype is a silent one. Documented: Mooncake requires all instances to share tensor_parallel_size, and vLLM requires RDMA/NVLink for multi-node EP/DP. Not documented: per-backend TP/PP/EP/DCP compatibility, in either the vLLM RFC or llm-d’s page. So test the negative case explicitly, monitor hit rate as a first-class metric, and keep the fleet homogeneous.

What three properties does a KDA state break, and what does that force?

Page granularity (one object per request — no half a recurrent state, nothing to dedupe), immutability (overwritten every token, so an offloaded copy is stale immediately), and position addressability (no state exists for an arbitrary prefix). Forces: only checkpoints are cacheable or transferable, so the checkpoint interval defines the offloadable set; the transfer object is two things at two granularities — vLLM’s NIXL connector moves “token-level MLA cache and request-level KDA state” and reconciles two hit lengths; and copy-on-write becomes mandatory, since continuing from a fetched checkpoint is a branch.

DCP or offloading — which lever, and on what basis?

Different axes. DCP (7.9× logical KV with DCP8) is a capacity multiplier with no fetch latency or miss — better for few users with very long unique contexts — but costs an all-to-all per layer, excludes KDA, and does nothing for request count. Offloading is a work eliminator: cross-request, cross-replica reuse of already-computed blocks, “up to 16× faster than recomputing” for long prompts — better for many users sharing large prefixes. They compose. The measurement that decides it is prefix cache hit rate on real traffic with native offload and prefix caching enabled: low hit rate because requests scatter across replicas is exactly the gap a cross-instance store fills.

What is the mitigating upside of K3's hybrid attention for offloading?

Only 24 of 93 layers produce per-token paged KV, so the offloadable paged portion is roughly a quarter of a uniform full-attention model’s — less to move per token, and offloading matters correspondingly less as a context-length lever. Where it still matters fully is cross-request prefix reuse, which is capacity-independent and is precisely what a cross-instance store like Mooncake exists for.