πΎ LMCache KV-offload for Qwen3.6-27B β keep long sessions warm across turns & restarts (π£ incubating) #423
Replies: 8 comments 1 reply
|
Cross-rig β 2Γ RTX 3090 (TP=2, PCIe, no NVLink), 124 GB RAM, 260/280 π Env-passthrough bug
Warm-cache (cold baseline 34.2 s TTFT, ~40K-token prefix)
Decode (zero penalty confirmed)narr 81.5 / code 103.6 decode TPS (n=3, CV 3.4%/2.6%), TTFT ~115 ms, 22.3 GB/card β beats the #133 baseline (74/94), newer driver/BIOS Sizing (124 GB rig)L1=60 + L2 needs Gate results
|
Wow and omfg, you do not want to know how many (re)builds this has costed me. |
|
Thanks, this is exactly the cross-rig validation we needed. The env-passthrough bug is real, and I opened #454 for it: #454 Root cause matches your finding: the LMCache args are chosen inside the container via escaped #454:
Your numbers are useful as the cross-rig addendum: zero decode penalty confirmed, warm L1/L2 benefit confirmed, and the I agree we should keep this π£ for now: useful and fast, but the ~214K real fillable ceiling, 241K OOM, and soak VRAM-growth caveat are material enough that this should not be promoted on throughput alone. |
|
Results (all phases done) βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
gh returned Resource not accessible by personal access token β the Two ways forward β pick one:
The full reply is saved at scratchpad/post423.md and reproduced here for π΄ Cross-rig numbers β bare-metal dual 3090, real NVMe (L1=60) Re-ran the validation card on a bare-metal rig after the LMCACHE_* Rig: Ryzen 7700X Β· 124 GB RAM Β· 2Γ RTX 3090 PCIe Gen4 x8, no NVLink Β· L2 β‘ Cache behavior β 36,721-token session (controlled: full compose ββββββββββββββββββββββββββββββββββββββββββββββββββ¬βββββββββ¬βββββββββ Logs confirm tier isolation: warm-L1 = 46/46 retained keys (46 L1, 0 The full reply is saved at scratchpad/post423.md and reproduced here for copy-paste: π΄ Cross-rig numbers β bare-metal dual 3090, real NVMe (L1=60) Re-ran the validation card on a bare-metal rig after the LMCACHE_* env-passthrough fix landed (the knobs now actually reach the container β verified docker Rig: Ryzen 7700X Β· 124 GB RAM Β· 2Γ RTX 3090 PCIe Gen4 x8, no NVLink Β· L2 on /dev/nvme0n1p2 (NVMe) Β· driver 595 Β· LMCACHE_L1_GB=60 LMCACHE_L2=1, β‘ Cache behavior β 36,721-token session (controlled: full compose down/up between cold and warm-L2) βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββ¬ββββββββββ Logs confirm tier isolation: warm-L1 = 46/46 retained keys (46 L1, 0 L2); warm-L2 post-restart = 46/46 (0 L1, 46 L2). New finding on the L2 number: the 5.30 s warm-L2 breaks down as 405 ms disk prefetch + 37 ms hostβGPU + ~4.8 s recompute. On this NVMe the disk read is <10% β Decode β zero penalty (controlled A/B, toggle only the connector) Same image+config, removed only --kv-transfer-config. 3 warm + 5 measured. ββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββ Deltas sit inside the CV bands and flip sign (ON wins narrative, OFF wins code) β zero systematic decode penalty, confirmed. MTP accept ~83% both arms. VRAM β’ Capacity / sizing at L1=60
An aborted long prefill (client disconnect / timeout mid-prefill) leaves orphaned L2 data on disk but does not index it β a resend showed only 46/320 Setup notes for repro
Happy to re-run any specific scenario (e.g. a clean-restart L2 isolation at full 262K) if useful. Housekeeping: temp OFF compose removed; git status shows only your pre-existing changes; the ON container is up and lmcache-kv is 32 GB (this-model cache, |
Cross-rig numbers β bare-metal dual RTX 3090 (PCIe Gen4, no NVLink), 124 GB RAM, NVMe L2Re-ran the three tables on a second rig and the profile reproduces cleanly. Setup: Qwen3.6-27B fp8 ( First, a fix that mattered: the documented β Zero decode penalty β confirmedA/B toggling only
Within run-to-run CV; signs flip. No measurable decode cost. β‘ Cache behavior β cold / warm (controlled
|
| TTFT | speedup | |
|---|---|---|
| Cold prefill | 35.25 s | 1Γ |
| Warm β L1 (CPU RAM) | 0.51 s | 69Γ |
| Warm β L2 (disk, cross-restart) | 5.30 s | 6.7Γ |
β’ Sizing
L1=60 needs CLUB3090_MEM_LIMIT=110g + CLUB3090_SHM_SIZE=64gb; the shipped 70g default crash-loops at L1>~40 (pinned L1 + mmap'd fp8 weight pages both count against the cgroup β a TP worker OOMs as WorkerProc initialization failed, no OOMKilled flag). ~131 KB/token cache footprint. Restart via compose down/up / switch.sh, never docker restart (races GPU release β same worker-init crash).
Two findings worth adding to the thread
1. Warm-L2 is compute-bound, not disk-bound. Breaking down the 5.30 s warm-L2 load: ~405 ms disk prefetch + ~37 ms hostβGPU + ~4.8 s Mamba-state recompute (GDN hybrid reconstructs its recurrent state from the rehydrated KV). So a faster SSD barely moves warm-load β my NVMe β the original VM's ~1.9 GB/s here. L2 disk buys persistence/capacity, not speed; the speed win is L1.
2. An aborted prefill does NOT commit to LMCache. If the upstream client times out mid-prefill (e.g. a long ~255K session that cold-prefills >5 min, against a 300 s client limit), you get orphaned L2 chunks on disk but only the completed prefix is indexed (46/320 retained keys) β the session re-prefills from scratch next time. LMCache commits a session's blocks only on request completion. For long-ctx warm wins, don't let the client kill the first prefill.
Bonus: host RAM holds flat at the L1-pool size (~70 GiB at L1=60) across warm sessions β L1 is pre-pinned, sessions fill within it, no per-session host-RAM creep. And persistence across reboots works by putting LMCACHE_L2=1/LMCACHE_L1_GB=60/CLUB3090_MEM_LIMIT=110g/CLUB3090_SHM_SIZE=64gb in repo-root .env (switch.sh auto-loads it; the on-disk lmcache-kv/ survives restarts).
|
Clean re-post came through perfectly this time π β and the good news is everything here is already in the tree, credited to you:
So this is a clean across-the-board confirmation on a second bare-metal rig with real NVMe. Status stays π£ on the same caveats we agreed β the ~214K real fillable ceiling / 241K OOM and the soak VRAM-growth threshold β which are what keep it off the throughput-promoted list, not anything you found here. Thanks again for taking it end-to-end; this is exactly the contributor cycle the π£ tier exists for. π |
Follow-up (2026-07-05): native CPU KV offload landed in stock vLLM β and a caution on the warm-cache numbersTwo things worth sharing after re-visiting this on the current stack, in the same follow-the-data spirit as the original post. 1. Stock vLLM now does CPU KV offload natively β this slug is largely redundantvLLM v0.24.0 β the image we already run for That's LMCache's L1 (CPU-RAM) tier, first-party β no third-party image, no bundled-older-vLLM, no 38 GB pull, no mutable-tag digest pin. Those were the three things keeping So we're going to retire this slug in favour of a native-offload variant (validation pending). Zero decode penalty still holds either way (re-confirmed today, ~85 narr / ~105 code β current 2. A caution on the warm-cache multipliers aboveRe-testing surfaced a measurement subtlety our original card didn't account for, and it's worth flagging honestly. vLLM's built-in prefix cache ( Cleanly isolating LMCache's contribution requires forcing a GPU-cache miss (e.g. evicting the prefix past the GPU pool) β which we didn't do originally, and which turns out to be fiddly on a 262K-token pool. We're not going to publish a corrected multiplier, because getting an honest LMCache-specific number needs more careful isolation than we've been able to nail down, and the slug is being retired anyway. The safe takeaway: "reused long prefixes skip re-prefill" is real and valuable β but on a single node you get most of that from vLLM's native prefix caching (on by default), and the rest from the native CPU-offload connector above. Treat the earlier per-tier speedups as illustrative of prefix reuse, not as validated LMCache-vs-native deltas. NetFor the "keep long sessions warm" use case on a single node: native prefix caching (already on) + the native |
|
Addendum (2026-07-06) β crediting a use case I under-weighted above. Re-reading my follow-up, I lumped LMCache's L2 disk persistence in with "keeping sessions warm" and dismissed both together β but those are two different things. Keep active sessions warm β native prefix caching + Two honest reasons it doesn't reverse the retirement yet: (1) we never showed LMCache delivers it here β in our L1 test LMCache retrieved the KV but vLLM used 0% of it (no external hit, no TTFT benefit); the retrieved KV never reached the engine, most likely a Qwen3-Next GDN-hybrid KV-load issue we never root-caused β and we never tested L2 disk at all. So our only datapoint suggests the retrieved KV wouldn't load regardless of tier. (2) the dependency cost is unchanged β third-party image + bundled-older-vLLM + 38 GB pull + mutable-tag pin β heavy to carry for an unvalidated niche. So the retirement stands, but the door's explicitly open: we reopen if either (a) someone validates LMCache's L2 disk tier actually loads into the engine on Qwen3-Next β a real external hit + TTFT drop on a reloaded long prefix β or (b) vLLM's native offload grows a disk-persistence spec (it's CPU-RAM only today). If instant-reopen of long conversations is your use case and you're on hardware to test it, that L2-disk validation is exactly the missing datapoint β genuinely welcome it. π |
Uh oh!
There was an error while loading. Please reload this page.
π£ Incubating β opt-in, hidden from
switch.sh --list(--forceto launch). An LMCache tiered prefix-KV cache layered on our dual-max profile, for keeping many long sessions warm. Zero decode penalty (measured). Posted because the validation is interesting and the use case is real β cross-rig numbers welcome.We wired up
vllm/qwen-27b-dual-lmcacheβ dual-max Qwen3.6-27B (FP8 + int8-PTH KV + MTP n=3 @262K) with an LMCache tiered persistent prefix-KV cache (GPU β L1 CPU RAM β optional L2 disk). For workloads with many long-running sessions (50Kβ262K ctx), it reuses each session's cached prefix instead of re-prefilling β turning a ~40 s re-prefill into a sub-5 s warm load.Credit where it's due: this exists and is correct because of @efschu's persistence on #133. An early read of mine claimed LMCache "halves decode"; his cross-rig data said otherwise, and a controlled A/B proved him right. Thank you.
π΄ Results Card β dual RTX 3090 (TP=2), Qwen3.6-27B FP8 + int8-PTH KV + MTP n=3
β Serving β zero decode penalty
Controlled A/B (toggle only the connector, same image+config): identical within noise. The KV offload is async/overlapped β it does not cost decode. VRAM ~21.5 GB/card (LMCache adds ~0.7 GB).
β‘ Cache behavior β the actual win (36,807-token session, measured)
Cross-restart persistence confirmed β post-restart the log shows
0 L1 / 46 L2retained keys (the session rehydrated entirely from disk).β’ Capacity & sizing β cache β 131 KB/token measured (both tiers)
Per warm session: 50K β 6.5 GB Β· 128K β 17 GB Β· 262K β 33 GB (RAM or disk).
--l1-size-gb 30(default)--force(a too-large cache once OOM'd the host).fsadapter, off by default): survives restarts; unbounded β size disk per the table. No compression yet β LMCache's fp8 serde shape-errors on this hybrid KV (measured), and CacheGen for MP-mode is pending upstream, so provision for the full ~131 KB/token.β£ Takeaways
fsL2 adapter, notnixl_store(this image's NIXL backend is broken).Get it / run it
(hidden/incubating β
--force; the 38 GB image pulls on-demand). Tune by env, no compose edits:LMCACHE_L1_GB=30β CPU-RAM cache sizeLMCACHE_L2=1β enable disk persistence at a gitignored in-repo default path (zero config); orLMCACHE_L2_ADAPTER='{"type":"fs","base_path":"/your/ssd/lmcache-kv"}'to point at a faster/larger SSDWhat'd help
Cross-rig numbers β especially on faster interconnect / more VRAM (where the recipe should run without the 24 GB constraints we worked around), and L2 rehydrate on real NVMe.
Credits
All reactions