7.4 KiB
KV Cache Group Edits
Summary
lmcache/integration/vllm/kv_cache_group_edits.py is the single place where
vLLM KV cache groups are re-presented ("edited") before LMCache registration.
The connector calls apply_kv_cache_group_edits(kv_cache_config, kv_caches)
once in register_kv_caches, and the edited dict feeds both engine-group-info
creation (kv_cache_groups.py) and transfer registration.
LMCache derives each group's transfer metadata (block size, page layout,
dtype) from the registered tensors, and interprets store/retrieve block IDs in
vLLM's scheduler-side block-id space (kv_cache_spec.block_size units). The
edits exist to restore one invariant the raw tensors can violate:
The registered tensor's paging granularity must equal the block-id granularity.
Structure
Each case is one KVCacheGroupEdit rule in the module's _EDITS registry:
matches(spec, kv_cache) decides structurally — from the vLLM spec kind
(get_kv_cache_spec_kind, which also unwraps UniformTypeKVCacheSpecs) and
the registered tensor, never from model name or architecture — and
apply(spec, kv_cache) produces a view over the same storage. First matching
rule wins; unmatched layers pass through. Covering a new group kind means
adding one rule.
Model name/arch is deliberately not an input: the same architecture yields
different group structures depending on runtime decisions
(mamba_cache_mode, attention backend's kernel block size, TP), all of which
the config + tensors already resolve.
Edits
Both rules apply only to Mamba-hybrid models (the registry is only consulted
when kv_cache_config.has_mamba_layers).
1. Mamba state pages
A Mamba / linear-attention layer (e.g. Qwen3.5 GDN) registers [conv_state, ssm_state] — two tensors with different shapes and dtypes, laid out
contiguously in one padded page (conv | ssm | pad). The raw pair trips
format discovery (the SSM view starts mid-page). The edit reinterprets each
page as one bf16 tensor shaped (num_blocks, 2, block_size, 1, head_size)
over the same storage, where head_size is derived so the bytes fill the page
exactly.
2. Sub-paged full attention
vLLM unifies page sizes across hybrid groups by inflating the attention
logical block size (vllm/platforms/interface.py:_align_hybrid_block_size;
Qwen3.5-0.8B: 544), while the attention backend re-pages the physical tensor
at its own kernel block size (vllm/v1/worker/utils.py: prepare_kernel_block_sizes; FlashAttention on hybrids: 32). Logical block n
then occupies the k = logical/kernel contiguous kernel pages
n*k .. n*k+k-1 (vLLM expands the worker-side block table the same way,
BlockTable.map_to_kernel_blocks); the scheduler-side block IDs LMCache
receives stay logical.
Registering the raw kernel-paged tensor makes LMCache discover
block_size == kernel < logical, and _derive_compression_metadata
(lmcache/v1/kv_layer_groups.py) misclassifies the group as compressed:
only 1/k of each chunk's KV is transferred, addressed against the kernel
page space. The edit re-views the tensor as
(num_kernel_pages / k, 2, logical_block_size, 1, head_size) — a pure
view(), valid because k kernel pages tile each logical page's bytes
exactly (enforced; see Invariants).
Startup validation
validate_kv_cache_groups (called at connector init and again at
registration) rejects group specs the transfer path cannot serve correctly,
with one aggregated error: CrossAttentionSpec, and Mamba with
mamba_cache_mode != "align" (no reusable snapshots). Declared slot
compression (compress_ratio > 1 / tq_slot_size > 0, e.g. DeepSeek-V4) is
not rejected — those groups are served by the compression path in
lmcache/v1/kv_layer_groups.py and only skipped by the edits here. Note the
compression path still derives per-group ratios from the unified vLLM block
size; switching it to per-group block sizes is pending in a separate PR.
Reference: vLLM PR #42828 (Mooncake store HMA support) uses the same validate-and-reject-up-front pattern, and is the reference design for the deferred follow-ups (per-group store/load masks; manager-mirroring hit computation). Caveat for the latter: LMCache's lookup doubles as prefetch, so vLLM's lookup-first-then-trim flow does not map directly. See the module docstring for the full check-when list.
Non-edit: declared compression
Groups whose spec declares slot compression — MLAAttentionSpec. compress_ratio > 1 (DeepSeek-V4 slot packing, storage_block_size < block_size) or TQFullAttentionSpec.tq_slot_size > 0 — genuinely store fewer
physical slots than logical tokens. They must reach the compression path in
lmcache/v1/kv_layer_groups.py unedited. (DeepSeek-V3.2's fp8_ds_mla cache
packs bytes per slot, not slots per block: its specs keep
block_size == scheduler block size and compress_ratio == 1, so it never
needs an edit either.)
The sub-paged rule's matches excludes declared-compression specs by their
own fields (compress_ratio / tq_slot_size), and its apply additionally
verifies by byte accounting that k kernel pages tile the logical page's
bytes exactly — any undeclared packed layout fails with a loud ValueError
rather than being transferred wrongly.
The opaque-page contract
An edited view's dims are addressing metadata only (block id → byte range).
The named dims are not semantic: a Mamba view's "K plane" is conv/ssm
bytes, and a sub-paged attention view's "K plane" interleaves true K and V at
kernel-page granularity (true K is not contiguous across kernel pages, so no
logical-block view can have a pure-K plane). The synthetic head shape
(1, page_bytes / (2 * block_size * elem)) signals this deliberately.
Byte transport round-trips correctly because store and retrieve share the same bijective block-id → bytes mapping. Consequences:
- Valid: store/retrieve through the MP transfer path on the same engine configuration.
- Not valid for edited groups: content-aware processing (serde compression, blending, head resharding, layout conversion), and sharing cache entries across engines whose attention backends choose different kernel block sizes (the byte order inside a logical page is backend-dependent).
Invariants
- Edits are pure tensor views over the registered storage — never copies.
- A sub-paged view is only produced when
kernel_page_bytes * k == spec.page_size_bytes; any mismatch raisesValueError(fail loudly rather than silently transfer a compressed layout). - After edits, every registered tensor's block dim equals its group's
kv_cache_spec.block_size, so the server derivescompress_ratio == 1for these groups.
Code map
| Area | File |
|---|---|
| Edits (this doc) | lmcache/integration/vllm/kv_cache_group_edits.py |
| Caller | lmcache/integration/vllm/lmcache_mp_connector.py (register_kv_caches) |
| Compression-ratio derivation (downstream consumer) | lmcache/v1/kv_layer_groups.py |
| vLLM block-size inflation | vllm/platforms/interface.py (_align_hybrid_block_size) |
| vLLM kernel-page split + block-table expansion | vllm/v1/worker/utils.py, vllm/v1/worker/block_table.py |
| End-to-end test | .buildkite/k3_tests/multiprocess/scripts/run-single-test.sh (hma_lm_eval_qwen3_5) |
Testing is end-to-end only (the hma_lm_eval_qwen3_5 store-vs-retrieve gsm8k
check): the edit internals are expected to change as more group kinds are
covered, so tests pin the observable contract — faithful retrieve — rather
than view shapes.