Verified fix for silent KV-cache persistence corruption with hybrid Mamba/GDN + full-attention models in LMCache (0.5.3 through current dev) when running vLLM ≥ 0.26.
Patch: patches/lmcache-053-hybrid-fa.patch
· sha256 ec14d5012bef7a86e1ed90c6ada8ee1cbf27a7fe7b921a5ff8f83364efaa7de9 (rev 2)
Deep technical narrative: EXPLAINER.md
· Upstream issue: LMCache#4701
2026-08-25: LMCache maintainers opened #4731 against
dev, carrying our proposed fix (byte-identical over the fix region) and our regression tests, closing #4701. It is not merged, and no released version contains it — the patch here remains the fix for released versions. See Relationship to upstream.
Symptom. With a hybrid model (Mamba/GDN recurrent layers plus a few full-attention layers), LMCache's persistent (disk-tier) store→restart→restore cycle silently persists only 1 of N kernel pages of the full-attention KV per chunk. The disk tier faithfully stores ~4% of the attention cache with no error; restores after an engine restart report high hit rates (the Mamba group restores fine) but return wrong continuations.
Root cause. Hybrid models force vLLM to pick a unified block size N larger
than the attention kernel page size, so each logical KV block of the
full-attention group spans N / page_size kernel pages — the group is
subpaged. Upstream commit 65c2ae81 ("[Core][MP] Support Mamba/GDN hybrid
models (Qwen3.5) (#3613)") added _SubpagedAttentionViewEdit in
lmcache/integration/vllm/kv_cache_group_edits.py to re-view exactly this
layout at registration time — but the edit only pattern-matches the old
5-dim KV tensor layout. vLLM ≥ 0.26 registers full-attention KV as
4-dim K/V-packed (FlashInfer in our runs), so no edit rule matches, the
raw kernel-page geometry flows into kv_layer_groups' compression math, and
the FA group is misclassified as slot-compressed ("one kernel page holds the
whole block").
Only the first kernel page of each chunk is stored and restored.
On 0.5.3 a second, independent bug compounds the failure: paging a multi-page
FA object through the single-page GPU staging buffer mutates the shared
MemoryObj.meta.address between in-flight async copies, corrupting the host
buffer (~96% zeros, no exception). Current dev has independently hardened this
path (size-mismatch ValueError, immutable staging-copy descriptors), so dev
fails loudly instead of corrupting — but the feature remains broken until the
layout matching is fixed.
kv_cache_group_edits.py is byte-identical across all three:
| LMCache version | Affected on vLLM ≥ 0.26? | Notes |
|---|---|---|
| 0.5.3 (released) | Yes | Layout bug and async staging corruption — silent wrong answers |
| 0.5.4 (released) | Yes | Layout bug; staging path hardened |
dev @ 1a430626 (2026-08-21) | Yes | Layout bug; fails loudly rather than corrupting silently |
The intended upstream fix never activates on vLLM ≥ 0.26, so as of 2026-08-21 no LMCache version supports disk-tier persistence for this model class on current vLLM without this patch.
LMCacheMPConnector), vLLM 0.27.1tokens_per_block=1600,
slots_per_block=64 → 25:1 subpaging--chunk-size 1600 --separate-object-groups,
L1 CPU + L2 local-disk (NVMe)The bug and the fix are geometry-driven, not configuration-driven: both
trigger on tokens_per_block > slots_per_block in an uncompressed
(kv_size == 2) group — i.e., on the model class, not on tensor-parallel
size, model size, or quantization.
Four files inside LMCache 0.5.3 — no changes to vLLM (vLLM was correct throughout):
lmcache/integration/vllm/lmcache_mp_connector.pylmcache/v1/kv_layer_groups.pylmcache/v1/multiprocess/modules/lmcache_driven_transfer.pylmcache/v1/platform/base/cache_context.pyWhat it changes:
tokens_per_block // slots_per_block at runtime in the transfer
path (verified at 25:1 and 17:1), sizes the reserved host object for all pages,
and expands every logical destination L → L*ratio … L*ratio + ratio-1 on
both store and retrieve.data_ptr = base + k*page_bytes) so the shared base object's metadata is
never mutated while copies are in flight. One helper serves both D2H
(store) and H2D (restore).cd <venv>/lib/python3.12/site-packages
patch -p1 --dry-run < lmcache-053-hybrid-fa.patch # must be clean
patch -p1 < lmcache-053-hybrid-fa.patch
# restart the LMCache server and the vLLM engine
Base: pristine lmcache==0.5.3 wheel. Verify the download:
shasum -a 256 lmcache-053-hybrid-fa.patch
# ec14d5012bef7a86e1ed90c6ada8ee1cbf27a7fe7b921a5ff8f83364efaa7de9
| Rev | sha256 | Change |
|---|---|---|
| 1 | 491b8faa… | Initial public release; contained diagnostic instrumentation |
| 2 | ec14d5012… | Identical transfer-path logic; removes diagnostic instrumentation and one unreachable branch |
A restore system that is sometimes right is worse than none. Every restore
must pass a buried-key exact-answer gate: a deterministic long context
containing a unique K-<24 hex> sentence buried mid-context is stored, then
the vLLM engine is fully killed and restarted (in the TP=1 run, the
LMCache server too), the context is restored from the disk tier only, and
the model is asked to reproduce the key. Pass requires all four: exact key
character-for-character, external hit ≥ 97%, wall time well below cold
prefill, engine healthy afterwards. This is deliberately stricter than
same-process cache-reset validation with score-level comparison — which
cannot catch this class of failure.
| Stage | What it proves | Result |
|---|---|---|
| 5k / 13k / 26k round trips across full restart | Correctness scales with length | Exact keys; hits 97.0 / 98.8 / 98.7% |
| End-to-end wall vs. cold | Speedup grows with length | 2.13/2.31/3.60 s vs 5.62/13.08/27.41 s → 2.6× / 5.7× / 7.6× |
| TTFT vs. cold | The prefill the restore actually replaces | 1.00/1.58/2.72 s vs 4.95/12.62/26.97 s → 5.0× / 8.0× / 9.9× |
| Warm re-ask (same session) | Restore is idempotent | Exact key |
| Cold-namespace control (never stored) | No false hits, no leakage | 0.00% hit, clean full prefill |
| Two independent repeat cycles (fresh namespaces) | Repeatability | Exact keys, 98.8% |
| TP=1 cross-check (17:1 geometry, engine and cache server restarted, L2-only) | TP-agnostic, geometry-driven | Exact key, 95.4% hit, 0.72 s vs 1.53 s cold |
Result: 12/12 PASS. The figures above were measured on rev 2
(sha256 ec14d5012…) on 2026-08-22, applied to a pristine lmcache==0.5.3
install. Per-chunk/per-rank md5 of FA host objects identical between store
and restore.
Rev 1 (2026-08-21) passed the same 12/12 suite with the same exact keys and the same hit rates (97.0 / 98.8 / 98.7%). Rev 2 is faster in both arms — including the cold-prefill baseline, which the restore path does not touch — so the difference is machine-level run-to-run variation plus the removal of rev 1's diagnostic instrumentation, not a change in restore behaviour. Rev 1 reference for comparison: restore 2.84/4.11/6.99 s vs cold 6.91/16.29/33.92 s.
Reported upstream 2026-08-22: LMCache#4701 — root cause, repro steps and these results, with the proposed fix attached as a reference patch.
Upstream's intended fix for the layout bug (_SubpagedAttentionViewEdit,
commit 65c2ae81, contained in released 0.5.3 and 0.5.4 and unchanged
through dev) never activates on vLLM ≥ 0.26 because it only matches the
old 5-dim KV layout. Rather than asking upstream to adopt this wheel-level
patch verbatim, we proposed fixing it the way 65c2ae81 intended: extend the
edit-rule matching in kv_cache_group_edits.py to recognize vLLM ≥ 0.26's
4-dim K/V-packed layouts, so the existing registration-time re-view
activates. Our restart/exact-answer verification suite was offered as
regression-test methodology. Related upstream issue on the same code path:
LMCache#4247.
2026-08-25 — upstream picked it up. LMCache maintainer 982945902
opened LMCache#4731
("fix(vllm): support packed subpaged attention caches") against dev,
closing #4701 and crediting the reproducer, root-cause analysis and
reference patch. We did not open that PR.
The PR carries our proposal essentially unchanged. We diffed it: the fix
region — from _synthetic_packed_content_size through the _EDITS registry,
18708 chars — is byte-identical to the staged proposal in
upstream-proposal/ (sha256
cfbfb0bc8ae8e4de… on both sides), including the _logical_block_ratio
shared-validation refactor, the byte-accounting matches() predicate, the
NHD/HND token-axis resolution via the kv_layout hint, the hard refusal on
an absent hint, and the design-doc paragraph. Our GPU-free regression test
file was taken too (214 lines). Upstream's only deltas are typing hygiene in
the test file (Any/cast for their mypy config) and unrelated dev drift
in an adjacent mamba_cache_mode check.
#4731 is open and unmerged, and it targets dev. No released LMCache
version contains the rule, so every claim and measurement above stands
unchanged.
That proposal is staged in
upstream-proposal/ as a git format-patch against
dev @ 1a430626, with a GPU-free unit test. We never submitted it as a PR
ourselves; upstream took it from the issue.
It is a different patch from the one above, not a port of it, and it is
unverified on hardware. The 0.5.3 patch attacks the transfer path and is
GPU-verified 12/12, twice; the proposal extends upstream's edit registry
instead — the right shape for upstream, but it has never run on any machine,
because we run a 0.5.3 wheel in production and have no dev-based deployment.
The hardware-verified artifact in this repo remains the 0.5.3 patch above.
Apache-2.0 (see LICENSE) — same license as LMCache, so the patch
is trivially adoptable upstream.
Maintained by 4rce.com Digital Technologies GmbH · project notes: https://stillpointlab.dev
8 commits
Verified fix for silent KV-cache persistence corruption with hybrid Mamba/GDN + full-attention models in LMCache (0.5.3 through current dev) when running vLLM ≥ 0.26.
Patch: patches/lmcache-053-hybrid-fa.patch
· sha256 ec14d5012bef7a86e1ed90c6ada8ee1cbf27a7fe7b921a5ff8f83364efaa7de9 (rev 2)
Deep technical narrative: EXPLAINER.md
· Upstream issue: LMCache#4701
2026-08-25: LMCache maintainers opened #4731 against
dev, carrying our proposed fix (byte-identical over the fix region) and our regression tests, closing #4701. It is not merged, and no released version contains it — the patch here remains the fix for released versions. See Relationship to upstream.
Symptom. With a hybrid model (Mamba/GDN recurrent layers plus a few full-attention layers), LMCache's persistent (disk-tier) store→restart→restore cycle silently persists only 1 of N kernel pages of the full-attention KV per chunk. The disk tier faithfully stores ~4% of the attention cache with no error; restores after an engine restart report high hit rates (the Mamba group restores fine) but return wrong continuations.
Root cause. Hybrid models force vLLM to pick a unified block size N larger
than the attention kernel page size, so each logical KV block of the
full-attention group spans N / page_size kernel pages — the group is
subpaged. Upstream commit 65c2ae81 ("[Core][MP] Support Mamba/GDN hybrid
models (Qwen3.5) (#3613)") added _SubpagedAttentionViewEdit in
lmcache/integration/vllm/kv_cache_group_edits.py to re-view exactly this
layout at registration time — but the edit only pattern-matches the old
5-dim KV tensor layout. vLLM ≥ 0.26 registers full-attention KV as
4-dim K/V-packed (FlashInfer in our runs), so no edit rule matches, the
raw kernel-page geometry flows into kv_layer_groups' compression math, and
the FA group is misclassified as slot-compressed ("one kernel page holds the
whole block").
Only the first kernel page of each chunk is stored and restored.
On 0.5.3 a second, independent bug compounds the failure: paging a multi-page
FA object through the single-page GPU staging buffer mutates the shared
MemoryObj.meta.address between in-flight async copies, corrupting the host
buffer (~96% zeros, no exception). Current dev has independently hardened this
path (size-mismatch ValueError, immutable staging-copy descriptors), so dev
fails loudly instead of corrupting — but the feature remains broken until the
layout matching is fixed.
kv_cache_group_edits.py is byte-identical across all three:
| LMCache version | Affected on vLLM ≥ 0.26? | Notes |
|---|---|---|
| 0.5.3 (released) | Yes | Layout bug and async staging corruption — silent wrong answers |
| 0.5.4 (released) | Yes | Layout bug; staging path hardened |
dev @ 1a430626 (2026-08-21) | Yes | Layout bug; fails loudly rather than corrupting silently |
The intended upstream fix never activates on vLLM ≥ 0.26, so as of 2026-08-21 no LMCache version supports disk-tier persistence for this model class on current vLLM without this patch.
LMCacheMPConnector), vLLM 0.27.1tokens_per_block=1600,
slots_per_block=64 → 25:1 subpaging--chunk-size 1600 --separate-object-groups,
L1 CPU + L2 local-disk (NVMe)The bug and the fix are geometry-driven, not configuration-driven: both
trigger on tokens_per_block > slots_per_block in an uncompressed
(kv_size == 2) group — i.e., on the model class, not on tensor-parallel
size, model size, or quantization.
Four files inside LMCache 0.5.3 — no changes to vLLM (vLLM was correct throughout):
lmcache/integration/vllm/lmcache_mp_connector.pylmcache/v1/kv_layer_groups.pylmcache/v1/multiprocess/modules/lmcache_driven_transfer.pylmcache/v1/platform/base/cache_context.pyWhat it changes:
tokens_per_block // slots_per_block at runtime in the transfer
path (verified at 25:1 and 17:1), sizes the reserved host object for all pages,
and expands every logical destination L → L*ratio … L*ratio + ratio-1 on
both store and retrieve.data_ptr = base + k*page_bytes) so the shared base object's metadata is
never mutated while copies are in flight. One helper serves both D2H
(store) and H2D (restore).cd <venv>/lib/python3.12/site-packages
patch -p1 --dry-run < lmcache-053-hybrid-fa.patch # must be clean
patch -p1 < lmcache-053-hybrid-fa.patch
# restart the LMCache server and the vLLM engine
Base: pristine lmcache==0.5.3 wheel. Verify the download:
shasum -a 256 lmcache-053-hybrid-fa.patch
# ec14d5012bef7a86e1ed90c6ada8ee1cbf27a7fe7b921a5ff8f83364efaa7de9
| Rev | sha256 | Change |
|---|---|---|
| 1 | 491b8faa… | Initial public release; contained diagnostic instrumentation |
| 2 | ec14d5012… | Identical transfer-path logic; removes diagnostic instrumentation and one unreachable branch |
A restore system that is sometimes right is worse than none. Every restore
must pass a buried-key exact-answer gate: a deterministic long context
containing a unique K-<24 hex> sentence buried mid-context is stored, then
the vLLM engine is fully killed and restarted (in the TP=1 run, the
LMCache server too), the context is restored from the disk tier only, and
the model is asked to reproduce the key. Pass requires all four: exact key
character-for-character, external hit ≥ 97%, wall time well below cold
prefill, engine healthy afterwards. This is deliberately stricter than
same-process cache-reset validation with score-level comparison — which
cannot catch this class of failure.
| Stage | What it proves | Result |
|---|---|---|
| 5k / 13k / 26k round trips across full restart | Correctness scales with length | Exact keys; hits 97.0 / 98.8 / 98.7% |
| End-to-end wall vs. cold | Speedup grows with length | 2.13/2.31/3.60 s vs 5.62/13.08/27.41 s → 2.6× / 5.7× / 7.6× |
| TTFT vs. cold | The prefill the restore actually replaces | 1.00/1.58/2.72 s vs 4.95/12.62/26.97 s → 5.0× / 8.0× / 9.9× |
| Warm re-ask (same session) | Restore is idempotent | Exact key |
| Cold-namespace control (never stored) | No false hits, no leakage | 0.00% hit, clean full prefill |
| Two independent repeat cycles (fresh namespaces) | Repeatability | Exact keys, 98.8% |
| TP=1 cross-check (17:1 geometry, engine and cache server restarted, L2-only) | TP-agnostic, geometry-driven | Exact key, 95.4% hit, 0.72 s vs 1.53 s cold |
Result: 12/12 PASS. The figures above were measured on rev 2
(sha256 ec14d5012…) on 2026-08-22, applied to a pristine lmcache==0.5.3
install. Per-chunk/per-rank md5 of FA host objects identical between store
and restore.
Rev 1 (2026-08-21) passed the same 12/12 suite with the same exact keys and the same hit rates (97.0 / 98.8 / 98.7%). Rev 2 is faster in both arms — including the cold-prefill baseline, which the restore path does not touch — so the difference is machine-level run-to-run variation plus the removal of rev 1's diagnostic instrumentation, not a change in restore behaviour. Rev 1 reference for comparison: restore 2.84/4.11/6.99 s vs cold 6.91/16.29/33.92 s.
Reported upstream 2026-08-22: LMCache#4701 — root cause, repro steps and these results, with the proposed fix attached as a reference patch.
Upstream's intended fix for the layout bug (_SubpagedAttentionViewEdit,
commit 65c2ae81, contained in released 0.5.3 and 0.5.4 and unchanged
through dev) never activates on vLLM ≥ 0.26 because it only matches the
old 5-dim KV layout. Rather than asking upstream to adopt this wheel-level
patch verbatim, we proposed fixing it the way 65c2ae81 intended: extend the
edit-rule matching in kv_cache_group_edits.py to recognize vLLM ≥ 0.26's
4-dim K/V-packed layouts, so the existing registration-time re-view
activates. Our restart/exact-answer verification suite was offered as
regression-test methodology. Related upstream issue on the same code path:
LMCache#4247.
2026-08-25 — upstream picked it up. LMCache maintainer 982945902
opened LMCache#4731
("fix(vllm): support packed subpaged attention caches") against dev,
closing #4701 and crediting the reproducer, root-cause analysis and
reference patch. We did not open that PR.
The PR carries our proposal essentially unchanged. We diffed it: the fix
region — from _synthetic_packed_content_size through the _EDITS registry,
18708 chars — is byte-identical to the staged proposal in
upstream-proposal/ (sha256
cfbfb0bc8ae8e4de… on both sides), including the _logical_block_ratio
shared-validation refactor, the byte-accounting matches() predicate, the
NHD/HND token-axis resolution via the kv_layout hint, the hard refusal on
an absent hint, and the design-doc paragraph. Our GPU-free regression test
file was taken too (214 lines). Upstream's only deltas are typing hygiene in
the test file (Any/cast for their mypy config) and unrelated dev drift
in an adjacent mamba_cache_mode check.
#4731 is open and unmerged, and it targets dev. No released LMCache
version contains the rule, so every claim and measurement above stands
unchanged.
That proposal is staged in
upstream-proposal/ as a git format-patch against
dev @ 1a430626, with a GPU-free unit test. We never submitted it as a PR
ourselves; upstream took it from the issue.
It is a different patch from the one above, not a port of it, and it is
unverified on hardware. The 0.5.3 patch attacks the transfer path and is
GPU-verified 12/12, twice; the proposal extends upstream's edit registry
instead — the right shape for upstream, but it has never run on any machine,
because we run a 0.5.3 wheel in production and have no dev-based deployment.
The hardware-verified artifact in this repo remains the 0.5.3 patch above.
Apache-2.0 (see LICENSE) — same license as LMCache, so the patch
is trivially adoptable upstream.
Maintained by 4rce.com Digital Technologies GmbH · project notes: https://stillpointlab.dev
8 commits