[Feature] Chunk-boundary states for gated-delta prefix caching - #4859
[Feature] Chunk-boundary states for gated-delta prefix caching#4859Tsundoku958 wants to merge 11 commits into
Conversation
…ary states Port the FLA chunked gated-delta-rule forward path to an in-repo Triton implementation (inference-only: no autograd, no backend dispatch, no CP). The inter-chunk state kernel already materializes the recurrent state at every chunk boundary; expose that tensor as ``chunk_states`` (the third return value) so downstream prefix-caching can checkpoint per chunk at no extra compute. decode keeps the existing TileLang fused-recurrent kernel. Five forward kernels are ported verbatim from FLA (cumsum, kkt+solve_tril, recompute_w_u, inter-chunk state, output) with the input_guard equivalent (device context + contiguous) reproduced at the public entry, required for the non-contiguous fused-QKV value view Qwen supplies. Autotune chunk-count bucketing: Add a discrete ``NT_BUCKET`` constexpr to all five forward kernels and put it in their autotune keys, instead of keying on the raw token/chunk count. Buckets (4/16/64/128/129) isolate short/mid/long-sequence best configs so a short-sequence first call no longer pollutes long-sequence latency (8192-token was ~24% slower when 64-token tuned first), while bounding cache entries. The bucket never enters math or pointer arithmetic. Metadata reuse via step_metadata: Wire gated-delta chunk_indices/offsets into the step_metadata architecture (GatedDeltaStepMetaUpdater now sets them on attn_metadata for the new plan path; the legacy fallback keeps update_chunked_gated_delta_rule_meta), so the metadata is built once per non-decode step and reused across all 36 gated-delta layers, replacing the FLA tensor-cache prewarm. Remove the now-dead prepare_chunked_gated_delta_rule FLA-prewarm helper and drop the per-layer cu_seqlens[].item() D2H sync checks from validation. Tests: Cover FLA numerical parity across lengths/states, fused-QKV non-contiguous views over multiple head/dim shapes (incl. K!=V and K=256), packed varlen, the TP4 production shape (T=4288), prefill->decode handoff, precomputed vs fallback metadata equivalence, once-per-step metadata construction, state-bank selective row writeback, invalid-input rejection, and autotune bucket structure/keys. Co-Authored-By: Claude <noreply@anthropic.com>
These notes/*.md are local design documents, not part of the repo. Co-Authored-By: Claude <noreply@anthropic.com>
test_fla_chunk_port.py was a one-off verification script used while porting the chunk gated-delta kernel; its coverage is fully subsumed by the dedicated pytest suite tests/pytorch/kernel/test_chunk_gated_delta_rule.py. Co-Authored-By: Claude <noreply@anthropic.com>
|
We have other models that require state cache, not all of them are GDN, could this feature support models with other state type? |
The lifecycle side is already state-type-agnostic; only the boundary-state producer is GDN-specific by necessity. This PR splits into two layers:
(a) True linear RNNs (KDA / GLA) — reusable, with only minor kernel changes. KDA's chunk forward (fla.ops.kda) is built on the same chunk_gated_delta_rule_fwd_h recurrence kernel gated-delta uses, and already exposes return_intermediate_states=True returning h = [B, NT, HV, V, K] — exactly the per-boundary state this PR materializes for GDN. So for KDA the producer is essentially a minor tweak of the existing kernel: the recurrence + output kernels and the restore invariant (resume from h[:, c] → run suffix → reproduces the terminal state) carry over directly; only the gate cumsum (KDA's carries Mamba-style A_log/dt_bias) and the WY/intra step need a per-type port. GLA is similar. Each such model is one (small) ported producer on the shared lifecycle. (b) Sliding-window / compressed-KV state (DeepSeek-V4's v4_window_kv / v4_compress_state) — not this mechanism. That state is an in-place overwritten ring buffer holding only the last ratio tokens: no accumulating recurrence, no boundary state to resume from, nothing to chunk-parallelize. It is KV, so it belongs on main's existing KV-cache prefix-hit path, not the SSM boundary-state lifecycle this PR adds. In short: shared, state-type-agnostic lifecycle + one (small) ported boundary-state producer per true-linear-RNN state type; this PR ships the GDN producer, the lifecycle wiring is a separate follow-up, and a KDA/GLA producer would be another such follow-up reusing the same lifecycle and kernel. |
|
Sliding window support in dsv4 is treated as a state, at least in engine level. The state cache and it's prefix caching is designed for models that require fixed size caches, not just mamba like ssm, it is model agnostic. Adding Marconi might requires updating to kernels/modeling that are technically not ssm. It would increase the maintainance pressure(you would not add kernel support for non-nvidia backend, right?). We need some time to evaluate weither we can accept this. |
You're right — this would increase maintenance pressure as you described, and I won't be implementing the kernel for other backends. To address this, when I implement the follow-up, I'll gate the feature behind a flag: the chunk-boundary checkpoint path runs only when the flag is on, and with it off everything falls back to main's existing single final-state checkpoint. The chunk kernel itself is also NVIDIA-only, so on non-NVIDIA backends the feature simply won't engage — no errors, no broken paths, just no multi-boundary gain. I'm hoping this opt-in approach mitigates the concern to some extent. |
Thanks for your contribution and we appreciate it a lot. The following instructions would make your pull request more healthy and more easily receiving feedbacks. If you do not understand some items, don't worry, just make the pull request and seek help from maintainers.
Motivation
Final goal: Marconi-style prefix caching for hybrid LLMs
Qwen3-Next / Qwen3.5 are hybrid (Attention + Recurrent) LLMs whose gated delta rule (linear attention) layer updates its recurrent state in place, so it cannot be sliced along the sequence or rolled back from the tail to represent an arbitrary prefix like KV cache. This forces SSM-state prefix reuse to require exact matches, while fine-grained checkpointing at every boundary floods the cache with low-hit-rate entries and thrashing. Marconi (arXiv:2411.19379, https://arxiv.org/html/2411.19379v1) is the system built for exactly this, with three core ideas:
chunk_statesmaps to this step);utility = recency + α·flop_efficiencyand evict the lowest first — since SSM state size is fixed regardless of sequence length, longer sequences save more FLOPs per byte and are worth keeping.This yields 4.5×–34.4× token hit-rate gains over fine-grained+LRU baselines on LMSys/ShareGPT/SWEBench. Our final goal is to land this mechanism on LMDeploy's gated-delta models; implementation details of each mechanism can be found in the paper.
This PR's role: filling the producer-side gap
mainalready ships the complete SSM prefix-cache consumer side (theblock_trie/checkpoint_lifecycle.pyreserve/publish/restore lifecycle,block_size=64naturally aligned to chunk boundaries,reserve_save(step=)already step-aware, and the long-context path already per-chunk). However, gated-delta prefill currently runs FLAchunk_gated_delta_rule(..., output_final_state=True)and only keeps the terminalfinal_state— every intermediate 64-chunk-boundary state is discarded. This is precisely the producer-side gap that the first idea above ("capture state at chunk boundaries") needs: without it, the existing checkpoint slots have nothing to fill at intermediate boundaries.This PR fills that producer side: the gated-delta chunk forward now emits, in one prefill pass, the recurrent state at every 64-chunk boundary (
chunk_states) together with the paired conv state (chunk_conv_states), corresponding to Marconi's first step of capturing state at chunk boundaries, as the foundation for later wiring intomain's lifecycle + Marconi admission/eviction. The local kernel is ported from the chunked gated-delta-rule forward of flash-linear-attention (FLA, https://github.com/fla-org/flash-linear-attention), and a restore-invariant test (chunk_states[:, c]→ run suffix → reproduces the terminal state) verifies the boundary states are exactly reusable.Modification
lmdeploy/pytorch/kernels/cuda/chunk_gated_delta_rule.py: a forward-only Triton chunk forward kernel chain ported from FLA that additionally materializes the recurrent state at every chunk boundary; the publicchunk_gated_delta_rulereturns(o, final_state, chunk_states), pluschunk_conv_states(theWconv-input tokens preceding each boundary). Includes localprepare_chunk_indices/offsets(replacing FLA's to avoid D2H sync) and autotune chunk-count bucketing.backends/cuda/gated_delta_rule.pymakes FLA optional, dispatches between FLA / local bychunk_indices, and returns a unified 3-tuple;backends/attention.py,backends/cuda/step_metadata.py,backends/cuda/op_backend.pyprepare chunk metadata once per step and reuse across layers;backends/causal_conv1d.py+backends/cuda/causal_conv1d.pyexposechunk_conv_statesthrough the backend abstraction (no direct CUDA import);nn/gated_delta.pythreads the metadata and unpacks the 3-tuple;models/qwen3_next.py,models/qwen3_5.pyadapt to the third return value (currently_chunk_statesas a placeholder; the convergence target removes it once the checkpoint bank is passed down).tests/pytorch/kernel/test_chunk_gated_delta_rule.py(56 passed) covers FLA numerical consistency (multiple shapes / packed varlen / production TP4 / non-contiguous fused-QKV view), prefill→decode handoff, prepare-metadata-once, and the Marconi restore invariant (one for recurrent, one for conv — restore at a boundary and run the suffix to reproduce the terminal state, with an independentF.conv1d/FLA reference);tests/pytorch/kernel/test_causal_conv1d.pygains a conv-state backend-dispatch equivalence test.BC-breaking (Optional)
No BC-breaking.
GatedDelta.__call__changes its return value from a 2-tuple to a 3-tuple; downstreamqwen3_next/qwen3_5are already adapted, and the interface only adds (the newchunk_statesis not consumed by downstream yet, purely placeholder).AttentionMetadatagains two optional fieldsgated_delta_chunk_indices/offsets, defaulting to None and not breaking existing construction paths.Use cases (Optional)
For enabling SSM prefix-cache reuse on gated-delta models such as Qwen3-Next / Qwen3.5 in follow-ups. This PR only provides the boundary-state producer and does not change existing inference behavior; wiring into
main's existing checkpoint lifecycle behind aCacheConfigflag is a follow-up PR.Future plan
This PR is only the producer side; the follow-ups wire
chunk_states/chunk_conv_statesintomain's existing lifecycle and progressively implement Marconi's admission/eviction, corresponding to the paper §4.1 (capturing states during prefill) and the admission/eviction policies:GatedDelta/the model layer (passed by reference likepast_key_value), write boundary states in place into the slots reserved byreserve_save(step=)viaindex_copy_, and return to the 2-tuple (removing the_chunk_statesplaceholder). Thechunk_states[:, c]here is exactly the per-boundary state the paper captures._prepare_prefill_cache_save'ssave_stepsto one-set-per-sequence, callingreserve_save(step=s)for each block-aligned step, so a normal prefill saves multiple chunk-boundary checkpoints at once.main):mainonly coversrecurrent_state; conv state must be checkpointed/restored at the same step or the restored layer is inconsistent.utility(n) = recency(n) + α·flop_efficiency(n)and evict the lowest first, withαtuned via offline grid search; exploit that SSM state is fixed-size so longer sequences save more FLOPs per byte.CacheConfigflag (e.g.--enable-marconi-prefix), shallow fork controlling storage/restore only, keeping the production path unified.References
@grimoire @lvhan028
[Feature] 当前qwen-next等SSM类模型prefix cache缓存命中率低 #4879