Skip to content

fix(qwen4-exp): register the QSA FP8 scale planes for KV offload - #2655

Merged
zhuyuhua-v merged 12 commits into
lingzha/qwen38-lmcachefrom
lingzha/qwen38-lmcache-fp8-kv
Nov 4, 2026
Merged

zhuyuhua-v merged 12 commits into
lingzha/qwen38-lmcachefrom
lingzha/qwen38-lmcache-fp8-kv

Conversation

@ZLkanyo009

@ZLkanyo009 ZLkanyo009 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

With FP8 QSA caches and in-process lmcache_offload, a prefix fetched back from the CPU tier decodes under the wrong scales. The dense codec moves exactly what build_kv_cache_tensor registers on KVCacheTensor, and the FP8 K/V scale planes and the compressed-index scale plane were not registered. The quantized rows came back, but the scales stayed whatever the reused block held. Nothing is logged: on the GSM8K pass served from CPU, accuracy fell to 0.5432 and MTP acceptance to 0.06. #2636 therefore refuses this combination at startup. This PR registers the planes and lifts that refusal.

Base: #2636 (lingzha/qwen38-lmcache). The FP8 QSA caches themselves (#2652) are on main.

Changes

  • Qwen4ExpMetadataBuilder.build_kv_cache_tensor looks each arena field up once, with the row space's suffix, and hands the same views to bind_caches and to KVCacheTensor. It now registers k_scale / v_scale, and appends index_compressed_scale to extra_planes. The dense codec reads extra_planes after its existing tails, so BF16 byte layouts do not change.
  • qwen4_exp_attn._check_kv_transfer no longer refuses lmcache_offload with quantized QSA caches.
  • recipes/Qwen3.8-Flash-Next.md: drops the note asking for BF16 caches with lmcache_offload.
  • Tests, in tests/test_qwen4_exp_offload.py:
    • [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636's refusal test now checks that the quantized combinations are accepted (test_qwen38_runs_in_process_offload_of_quantized_caches).
    • New test_every_quantized_qsa_layer_hands_the_codec_the_rows_it_binds covers the four quantized cache formats, using two target layers plus an MTP draft layer with rope positions. Each registered tensor must be its own row space's row of the right field, in stored order, and the set must equal what the layer binds. All four cases fail when k_scale is left unregistered.
    • The BF16 case stays covered by [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636's test_a_bf16_target_gives_the_mtp_draft_layer_its_own_rows.

Stored layout and older tiers

PAGE_LAYOUT_VERSION is unchanged, for these reasons:

A version bump would invalidate every model's persisted PAGE objects.

Not changed

  • KVCacheTensor and DenseKVByteCodec are unchanged; PAGE_LAYOUT_VERSION too (above).
  • main's build_kv_cache_tensor docstring still says "three caches"; it is out of date but left alone.
  • qsa_caches_quantized (now one caller) and the leaves local stay as in [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636.
  • tests/test_qwen4_exp_offload.py is gated on AITER, so the CI CPU runner skips it. The module under test imports AITER at module level, so the new test has to live there; it runs in the GPU unit tests.

Validation (f0bea2199; GSM8K on de10df694 and 4aff317a5, see below)

  • black / ruff clean.
  • CPU suite: 10625 passed; the 3 inductor g++ tests that fail on main fail too. Two engram host-path tests, which this PR does not touch, failed once and pass on rerun.
  • GPU unit tests (test_qwen4_exp_offload.py, model_ops/test_qsa_quantized_cache.py, test_aiter_flydsl_gdn.py, test_qwen4_exp_mtp.py, test_lmcache_offload_config.py, test_lmcache_offload_connector.py, MI355X): 558 passed.
  • GSM8K, amd/Qwen3.8-Flash-Next-Quark-MXFP4, TP1, MTP 3, block 64, in-process lmcache_offload (320 GiB CPU pool), questions 0-439, state interval 256. Pass 1 stores. Pass 2 resends the same prompts after HBM was flushed, so it is served from CPU. de10df694 registers the same views the same way as this head. 4aff317a5 differs only in looking each field up twice, and in [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636's comment, line-order and default-argument changes.
Tree KV / index cache Pass 1 Pass 2 (from CPU) MTP acceptance, pass 2 State images from CPU
#2636 before the refusal, without this fix fp8 / bf16 0.9795 0.5432 0.063 198 / 440
this PR (de10df694) fp8 / bf16 0.9841 0.9750 0.725 430 / 440
this PR (de10df694) fp8 / fp8 0.9886 0.9818 0.724 430 / 440
this PR (4aff317a5) fp8 / bf16 0.9818 0.9750 0.721 427 / 440

ZLkanyo009 and others added 4 commits October 20, 2026 08:28
Decode-sized QSA index scoring (the target's MTP verify and the draft, at
most 256 rows) ran on the CUDA-core kernel, which reads every request's
compressed keys once per query row. The new Triton MFMA kernel stacks 4 rows
x 4 index heads into one M = 16 operand, so a request's keys are read once
per verify and meet every head in a single dot; the per-head ReLU is
applied before the heads are summed back out of M.

On Qwen3.8-Flash-Next (MI355X, TP1, MTP 3) the target's decode scoring
drops from 1130 to 320 us a step and the draft's from 147 to 85 us, -6.2%
of decode kernel time. ATOM_QSA_STACKED_DECODE_SCORER=0 keeps the old
kernel.

Co-authored-by: Cursor <cursoragent@cursor.com>
Qwen QSA refused anything but a BF16 KV cache and ignored
--index-cache-dtype. Serve --kv_cache_dtype fp8 and --index-cache-dtype
{bf16,fp8,fp4} (unset follows the KV dtype, as Config resolves it):

- K/V: E4M3 with one FP32 scale per (token, KV head) = row amax / FP8 max,
  written by a new Triton kernel (K and V in one launch). Dynamic scales
  need no calibration and never saturate. The sparse GQA kernel reads
  E4M3 tiles, multiplies the key scale into its score column and folds the
  value scale into the probability before PV; FP8 tiles have their own
  tuned block config.
- Compressed index keys: FP8 with a per-row FP32 scale, or MXFP4 (packed
  E2M1 pairs, one E8M0 exponent per 32 dims, rounded up so a group never
  clips). Every scorer -- the stacked decode one, the CUDA-core one and the
  prefill MFMA one -- reads them; FP4 decodes with bit operations. The raw
  index keys stay BF16: they are mean-pooled before normalization and a
  group can straddle a prefill chunk.
- The MTP draft layer keeps BF16 caches in a `qsa_draft` row space of its
  own (ATOM_QSA_DRAFT_KV_BF16, default 1): an FP8 draft KV lowers the real
  acceptance length by ~3.5%, a BF16 one not at all.
- `_qsa_fields` declares the scale planes, so the arena sizes the pool by
  bytes; the memory profile runs on caches of the served format. Plugin
  hosts keep binding their own BF16 caches.

On Qwen3.8-Flash-Next (MI355X, TP1, MTP 3) FP8 KV fits 1.64-1.67x the
tokens, cuts prefill sparse GQA by 17%, and leaves GSM8K and the MTP
acceptance length unchanged. With --block-size 64 and a BF16 index cache
the prefill scorer stays on FlyDSL.

Co-authored-by: Cursor <cursoragent@cursor.com>
… into lingzha/qwen38-lmcache-fp8-kv

Co-authored-by: Cursor <cursoragent@cursor.com>
build_kv_cache_tensor bound the FP8 K/V scale planes (and the quantized
compressed-index scale) to the attention module but registered k_scale /
v_scale as None, so lmcache_offload's DenseKVByteCodec stored and restored
the FP8 rows without their scales. A prefix fetched back from CPU then
decoded under whatever scales the reused blocks held: GSM8K pass 2 dropped
from 0.98 to 0.50, every CPU-restored request returning empty or running to
max_tokens. MTP draft rows also registered the target's index fields at the
draft row.

Register every paged field the arena declares, with the row space's own
suffix, and move index_compressed_scale as a new codec tail. BF16 caches
register the same tensors as before.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

No conflict with the updated base (lingzha/qwen38-lmcache at 484dc30), so I haven't pushed anything to this branch. 484dc30e touches entry_arena.py and qwen4_exp_attn.py (reset_entries handles the 1-byte FP8 fields this PR registers), so I retested a local tree: this PR merged with 484dc30, plus #2606 for MTP weights. Not pushed.

Verified (MI355X, rocm/atom-dev:latest + CI aiter wheel):

  • CPU unit tests: same 3 failures as main (test_graph_marker x2, test_sparse_indexer_side_effect_op), 9267 passed.
  • GPU: test_qwen4_exp_offload.py, test_entry_arena.py, test_state_checkpoint.py, test_block_manager.py, test_scheduler.py, test_qsa_quantized_cache.py, test_qsa_stacked_scorer.py and test_qwen4_exp_mtp.py: 698 passed.
  • FP8 KV with in-process lmcache_offload, using l2's split method: GSM8K questions 0-439, --kv_cache_dtype fp8 --index-cache-dtype bf16, interval 256, spec 0. Pass 1 (store) scored 0.9818 and pass 2 (retrieve from CPU) 0.9795. Pass 2 took 479,936 tokens from the CPU tier, with offload hit 0.448 and 426 state-tier joints. Before the fix, pass 2 scored 0.5000.

Note that the tree tested here does not include #2635. With #2635 merged, offload retrieval regresses; details are in #2636.

@HaonanWang98 HaonanWang98 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Reviewed the scale-plane registration and suffix-aware target/draft bindings at a1a3abf. The registered fields match the arena bindings, and the codec includes the compressed-index scale plane. No blocking issues found in this fix.

@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

@HaonanWang98 @valarLip A heads-up on how this PR relates to #2636.

valarLip's review of #2636 today flagged two issues in build_kv_cache_tensor on the in-process lmcache_offload path:

Both are what this PR fixes: it registers every paged field the arena declares, including the FP8 K/V scale planes and index_compressed_scale, each with its row space's suffix (target vs MTP draft). The fix stays here rather than being folded into #2636, so this PR's own change (a1a3abf7, +66 / −7) is unchanged and HaonanWang98's approval still covers it.

Plan: merge #2636 first, then retarget this PR to main, merge main into it once (the conflict resolution is already rehearsed and tested: GSM8K with FP8 KV + offload 0.985, and the CPU-restore pass 0.9795 vs 0.5682 without the fix), and merge it right after. The "conflicting" state shown now comes from #2636's branch having merged main; it goes away with that retarget. The replies on #2636 point to this PR as well.

ZLkanyo009 added a commit that referenced this pull request Oct 29, 2026
…#2655

The in-process codec copies only what build_kv_cache_tensor hands it: no
k_scale / v_scale / index_compressed_scale, and the target's index rows
for a BF16 draft layer. With FP8 KV (or an FP8/FP4 index cache, including
one that follows --kv-cache-dtype) a CPU hit restores quantized rows under
the scales of the blocks' previous owners; GSM8K pass 2 fell to 0.54 with
no error.

check_qwen38_kv_transfer now refuses lmcache_offload with any quantized
QSA cache, pointing at bf16 caches or lmcache_mp, which publishes every
arena field and stays allowed. #2655 carries the scale planes
and the draft rows and removes this check.

Addresses review 5698904199 (must change 1).

Co-authored-by: Cursor <cursoragent@cursor.com>
ZLkanyo009 added a commit that referenced this pull request Oct 29, 2026
*_draft rows exist only under quantized target caches with
ATOM_QSA_DRAFT_KV_BF16 (QSAAttention.qsa_row_space). lmcache_offload now
refuses that combination until #2655, which fixes the row lookup in
build_kv_cache_tensor. Pin the case that stays reachable: with BF16
caches the MTP layer is one more row of the target's fields, and the
KVCacheTensor the in-process codec copies names exactly the rows the
layer binds.

Addresses review 5698904199 (must change 2).

Co-authored-by: Cursor <cursoragent@cursor.com>
ZLkanyo009 added a commit that referenced this pull request Oct 29, 2026
Add k_scale/v_scale, index_compressed_scale and the *_draft copies, and
note that lmcache_offload refuses quantized QSA caches until #2655.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

@valar-cop review this pr

@valar-cop valar-cop left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but layout version and the duplicated field list should be fixed..

any e2e with fp8 index cache or mtp? old tier upgrade run?

ci green.

getattr(kvt, "index_raw", None),
getattr(kvt, "index_compressed", None),
getattr(kvt, "rope_positions", None),
getattr(kvt, "index_compressed_scale", None),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytes_per_block changed but namespace didn't, old tier chunks will hit 'too small'.. bump PAGE_LAYOUT_VERSION or fold segment sizes into build_page_namespace?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@valar-cop Not bumping it, and here is why:

A PAGE_LAYOUT_VERSION bump would invalidate every model's persisted PAGE objects. Details are in the PR comment (de10df694).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, checked all four. _qsa_fields only adds the scale planes when qsa_cache_row_layout returns a scale tail, so bf16 chunks keep their bytes; the quantized cases were refused at startup in the base, and main's qwen digest differs through qsa_page_segments (atom/kv_transfer/offload/config.py:386). keeping v3 is fine, resolved on my side..

arena.view(f.name).numel() * arena.view(f.name).element_size() // blocks
for f in fields
)
assert codec.bytes_per_block == per_block

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only sum checked, a swapped k_scale/v_scale would still pass.. assert each field data_ptr and per-segment sizes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@valar-cop Agreed: a swapped or wrong-row scale passed the sum. This file is back to its base. The coverage now lives in tests/test_qwen4_exp_offload.py::test_every_qsa_layer_hands_the_codec_the_rows_it_binds, which covers five cache formats and two target layers plus the MTP draft layer, with rope positions. For each layer, every registered tensor must be that layer's own row of the right field, compared by data_ptr in stored order, and the set must equal what the layer binds. Registering k_scale from v_scale fails 4 of the 5 cases, and every quantized case fails on the base (de10df694).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checked, resolved on my side: test_qsa_quantized_cache.py is back to base, and test_every_qsa_layer_hands_the_codec_the_rows_it_binds does compare each field's own row by data_ptr in stored order, plus the bound set at tests/test_qwen4_exp_offload.py:501. the four quantized cases fail on c90317566.

rope_positions=(
arena.view("rope_positions")[row] if self.cache_rope_positions else None
),
k_scale=optional("k_scale"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same field list twice, that drift is what caused this.. build the views once and use for both?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@valar-cop Done in de10df694. build_kv_cache_tensor now looks each field up once, and bind_caches and KVCacheTensor get the same views.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, looks right. one lookup at atom/model_ops/attentions/qwen4_exp_attn.py:572 now feeds both bind_caches and KVCacheTensor, and the rope gate follows the arena field (_qsa_fields only declares it when cache_rope_positions). resolved, thanks.

ZLkanyo009 and others added 4 commits November 2, 2026 04:53
…mcache-fp8-kv

#2652 (the QSA FP8 caches this branch carried as a pre-merge copy) is on
main now, and #2636 carries main. Conflicts take #2636's side; the scale
plane registration is reapplied on its new structure in the next commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
build_kv_cache_tensor bound the FP8 K/V scale planes and the quantized
compressed-index scale to the attention module but did not register them,
so the in-process codec stored and restored FP8 rows without their
scales: GSM8K pass 2 fell from 0.98 to 0.54. Register k_scale / v_scale
and append index_compressed_scale to `extra_planes`, all through the row
space's own suffix (MTP draft rows registered the target's index rows),
and drop #2636's refusal of lmcache_offload with quantized QSA caches.

Co-authored-by: Cursor <cursoragent@cursor.com>
… lookup

Look each field up once and hand the same views to bind_caches and
KVCacheTensor. Fold the codec coverage into the existing draft-row test:
every QSA layer, five cache-format cases, each registered tensor matched by
data_ptr against its own row space and row in the stored order.

Co-authored-by: Cursor <cursoragent@cursor.com>
…mcache-fp8-kv

#2636 moved the QSA rules into qwen4_exp/qsa_cache_rules.py; this
branch's changes to them (no lmcache_offload refusal for quantized
caches, qsa_caches_quantized folded into qsa_draft_rows) move with them.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

Whole-PR cleanup, pushed as de10df694. It merges #2636's new head (c90317566) and reworks this PR's own change after an independent quality review. The description is rewritten to match.

Size. The diff GitHub showed before was 12 files, +1619/-111. Most of that was a pre-merge copy of #2652, which is now on main and in #2636. This PR's own fix was 4 files, +66/-7. Against the new base it is now 4 files, +87/-113, net -26. It removes more than it adds, because #2636's startup refusal and its tests go away.

before (own fix a1a3abf79) after
source code +8 / -7 +27 / -26
source comments +5 +2 / -3
test code +41 +50 / -69
test comments +4 +4 / -3
docs 0 +2 / -3

What was merged or removed.

  • Duplicate field lookup: build_kv_cache_tensor looked the same fields up twice, once for bind_caches and once for KVCacheTensor. That drift is how the draft rows and the scale planes went missing. It now looks them up once and hands both sides the same views.
  • New KVCacheTensor field: index_compressed_scale is no longer a dedicated field and codec line. It is appended to [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636's generic extra_planes, so atom/config.py and kv_byte_codec.py are no longer touched here.
  • Duplicate test: the standalone codec test in test_qsa_quantized_cache.py is gone (that file is back to its base). Its cases are folded into the existing draft-row test, which now compares every field by data_ptr.
  • [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636's startup refusal of lmcache_offload with quantized QSA caches is removed, together with its two dtype-gate tests. The single-caller helper qsa_caches_quantized is folded into qsa_draft_rows.
  • Recipe: the "use BF16 caches with lmcache_offload until fix(qwen4-exp): register the QSA FP8 scale planes for KV offload #2655" note is removed.

@valar-cop, your three inline comments:

  1. kv_byte_codec.py:147, old tier chunks hitting "too small": not bumping PAGE_LAYOUT_VERSION.
  2. test_qsa_quantized_cache.py:150, only the sum checked: agreed, a swapped k_scale / v_scale passed.
    • test_every_qsa_layer_hands_the_codec_the_rows_it_binds now checks, for every layer (two target layers and the MTP draft layer, with rope positions) and five cache formats, that each registered tensor is that layer's own row of the right field, by data_ptr, in stored order. It also checks that the set equals what the layer binds.
    • Registering k_scale from v_scale fails 4 of the 5 cases. Every quantized case fails on the base.
  3. qwen4_exp_attn.py:662, the same field list twice: done, one lookup for both (see above).

Validation (de10df694):

  • lint clean.
  • CPU suite: 10650 passed; the 3 failures are the inductor g++ tests that fail on main too.
  • GPU unit tests: 192 passed.
  • GSM8K, TP1, MTP 3, in-process lmcache_offload, T1 split. Pass 2 is served from CPU.
    • Before this fix (old [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636 head, fp8 KV / bf16 index): 0.9795 / 0.5432, MTP acceptance 0.06.
    • This PR, fp8 / bf16: 0.9841 / 0.9750, acceptance 0.725, 430 of 440 state images from CPU.
    • This PR, fp8 / fp8, which also moves index_compressed_scale: 0.9886 / 0.9818, acceptance 0.724, 430 of 440.

@HaonanWang98 your approval predates this rework; could you take another look? cc @valarLip

@valar-cop

Copy link
Copy Markdown

…mcache-fp8-kv

#2636 folded qwen4_exp/qsa_cache_rules.py back into qwen4_exp_attn.py;
this branch's rule changes (no lmcache_offload refusal for quantized
caches, qsa_caches_quantized folded into qsa_draft_rows) follow them.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

Merged the new #2636 head (027591b7e), which folds the QSA cache rules back into qwen4_exp_attn.py per the maintainers' preference. The only conflict was the rules block, and this PR's change now lives in qwen4_exp_attn._check_kv_transfer with no behaviour change. Against the base the diff is still 3 files (+87/-111). CPU suite: 10630 passed (same 3 inductor g++ failures as main); GPU unit tests on MI355X: 541 passed. Description updated.

ZLkanyo009 and others added 3 commits November 2, 2026 10:14
Drop the refusal of quantized QSA caches under in-process offload and
hand the codec the k/v scale rows and the index_compressed scale plane
through extra_planes, with a test that every quantized layer gives the
codec the rows it binds.

Co-authored-by: Cursor <cursoragent@cursor.com>
Bind the layer and describe its KVCacheTensor from one set of views, as
the earlier review asked, keep the recipe's P/D line as it was, and drop
_check_kv_transfer from the readers of the QSA field rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ZLkanyo009

Copy link
Copy Markdown
Contributor Author

Update 23a3aec8d..f0bea2199: merged #2636's new head (32848851f), and cut this PR's own diff down to the scale planes and lifting the refusal.

Size vs #2636. Before: 3 files, +87/−111. After: 3 files, +94/−31. The change now adds lines instead of rewriting #2636's.

Per file.

Not changed. KVCacheTensor, DenseKVByteCodec and PAGE_LAYOUT_VERSION are unchanged. main's stale "three caches" docstring is left as is. The new test lives in the AITER-gated GPU test file, because the module under test imports AITER at module level.

Validation.

  • black / ruff clean.
  • CPU suite: 10625 passed. The 3 inductor g++ failures from main also failed. So did 2 engram host-path tests (tests/model_ops/engram/test_host_path.py), which this PR does not touch: they pass on rerun, and they passed in [Qwen 3.8] LMCache offload for QSA + GDN + PLE state #2636's full run on the same engram code.
  • GPU unit tests, MI355X, 6 files: 558 passed. With k_scale left unregistered, all 4 quantized cases of the new test fail.
  • GSM8K (TP1, MTP 3, FP8 KV, BF16 index cache, in-process lmcache_offload, 440 questions, pass 2 served from CPU): 0.9841 / 0.9750 on de10df694 (same view code as this head). 0.9818 / 0.9750 on 4aff317a5, which differs only in looking each field up twice. Before the fix, pass 2 scored 0.5432.

Pending. CI is running. #2636 lands first. The description is updated to match this head.

@yhl-amd cc @valarLip

yifehuan added a commit that referenced this pull request Nov 3, 2026
resident_grid_refusals takes the KV connectors a model has run beside its
resident kernels (default none, so Kimi-K3 and the DSA indexer are unchanged).
Qwen Mono passes ``lmcache_offload``: its stores and loads are bounded
gather / scatter kernels and host copies on the connector's own pack / copy
streams, ordered by events (the pack stream waits on the forward's producer
event), with no persistent kernel and nothing that waits on the model, so they
can share or delay CUs but not hold one. Other connectors stay refused.
GDNPools also checks that both pools are dense, as the kernels address them.

Checked on one MI355X, on this branch merged with #2655 (#2636's Qwen offload),
Zhang Ling's A line with OFFLOAD=1, a 3,613-block GPU pool and 16 sequences:
32 12K-token prompts pushed the early prefixes to the CPU tier, then three
4000-token Mono decodes ran while 16 of them came back (195,584 KV tokens and
16 GDN state images loaded from the CPU tier, stores throughout). There were 0
errors and 0 timeouts; the decode event gap was 7.5 / 84 / 89 ms
(median / p99 / max), against 9.4 / 84 / 87 ms on Native. GSM8K 200 with
real MTP and offload: 0.985, acceptance 0.72.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zhuyuhua-v
zhuyuhua-v merged commit 97af4f1 into lingzha/qwen38-lmcache Nov 4, 2026
16 checks passed
@zhuyuhua-v
zhuyuhua-v deleted the lingzha/qwen38-lmcache-fp8-kv branch November 4, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants