Conversation
ivanium
force-pushed
the
feat/dsv41-decoder-swa-bounded-replay
branch
4 times, most recently
from
September 14, 2026 06:14
edf39ee to
ac96ca1
Compare
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
ivanium
force-pushed
the
feat/dsv41-decoder-swa-bounded-replay
branch
3 times, most recently
from
September 14, 2026 09:28
2b75dcd to
516a1dd
Compare
ivanium
marked this pull request as ready for review
September 14, 2026 09:29
ivanium
requested review from
AndreasKaratzas,
DarkLight1337,
WoosukKwon,
njhill,
pavanimajety,
yewentao256 and
ywang96
as code owners
September 14, 2026 09:29
ivanium
requested review from
BoyuanFeng,
ProExpertProg,
alexm-redhat,
heheda12345,
youkaichao,
zhuohan123 and
zou3519
as code owners
September 14, 2026 09:29
ivanium
force-pushed
the
feat/dsv41-decoder-swa-bounded-replay
branch
from
September 14, 2026 17:00
e2038d9 to
2c8d5e7
Compare
ivanium
force-pushed
the
feat/dsv41-swa-bounded-replay
branch
from
September 15, 2026 00:41
c7ecd3a to
5f225e3
Compare
ivanium
requested review from
ApostaC,
hmellor,
mgoin,
orozery,
robertgshaw2-redhat,
tlrmchlsmth and
zyongye
as code owners
September 15, 2026 00:41
ivanium
force-pushed
the
feat/dsv41-swa-bounded-replay
branch
from
September 15, 2026 00:46
5f225e3 to
8609569
Compare
Layers past the last KV-source layer (21-39) own only their sliding-window KV, and decode reads just its trailing window. In prefill they now run on each request's last 128 tokens (its replay window): after layer 20 the batch is compacted to the replay windows, the mHC states are gathered, and the replay layers run under metadata built for them by private copies of the runner's builders. A trimmed request's replay_start rises to its replay window's start, so the window is floored there like a replayed prefix-cache hit. Results are scattered back so sampling and the drafter see full-batch rows. The top-k indices and candidate blocks that layer 20's indexer publishes for the layers after it are keyed by batch row, so they are realigned to the replay window as well. CUDA graphs: under a piecewise (breakable) capture the replay becomes an eager break of the model graph, so each replay re-plans from its own batch, and the replay layers replay their own breakable graphs keyed by the padded replay batch size, captured alongside the model graphs. Decode batches and full graphs never trim. The breakable capture therefore allows a nested capture while the outer one is paused in an eager break. The compacted slot mappings live in fixed buffers: the window KV insert is recorded in the graph and reads them by address. Part of --swa-bounded-replay (on by default): replay-layer window KV is unwritten outside each request's replay window, so a prefix-cache hit must replay the window, which is what the encoder-side replay does. Where the replay-layer batch cannot shrink per rank (sequence, data or prefill-context parallelism), or a drafter or Engram layer would read rows the replay layers skip, the decoder side stays off with a warning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
A DSpark drafter whose layers are all sliding-window attention can only ever read the last `window` context positions of a request, so projecting and inserting context KV for the rest of a long prefill chunk is wasted work. Under the target's decoder-side SWA bounded replay those rows also carry no real hidden state (the target never computed them), so restricting the precompute to the window removes the garbage KV they produced. It rides --swa-bounded-replay. Adaptive verification sizes the leading verification requests on the GPU, but those are never trimmed, so the row selection from the CPU query boundaries still holds. The DFlash context-KV precompute gains a row-selection hook that keeps every row by default; DSpark implements it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
The DFlash capture path builds the draft attention metadata from a dummy batch without the batch's replay_start, so a DeepSeek-V4.1 DSpark drafter, whose sliding-window caches replay like the target's, failed the SWA builder's assertion at graph capture. Pass the dummy batch's zeroed replay_start, as the runner's own capture and the drafter's per-step build already do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
ivanium
force-pushed
the
feat/dsv41-decoder-swa-bounded-replay
branch
from
September 15, 2026 00:49
2c8d5e7 to
9f4699a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Stacked on #56227 (base branch
feat/dsv41-swa-bounded-replay, so the diff shows only the two commits of this PR). It completes the DeepSeek-V4.1 SWA bounded replay design with its decoder side.Layers past the last KV-source layer (21-39) read the layer-20 compressed KV and indexer through kv_source and own only their 128-token sliding-window KV, of which decode reads just the trailing window. In prefill they now run on each request's last 128 tokens (its replay window) instead of the whole prompt: after layer 20 the batch is compacted to those rows, the mHC states are gathered, the replay layers run under attention metadata rebuilt for the compacted batch, and the results are scattered back so sampling and the drafter see full-batch rows. A trimmed request's
replay_startrises to its window start, so its window is floored there exactly like a replayed prefix-cache hit. The top-k indices and candidate blocks that layer 20's indexer publishes for the layers after it are keyed by batch row, so they are realigned to the compacted rows as well.The replay metadata is built by private clones of the runner's builders (
AttentionMetadataBuilder.clone): the runner's builders back the full batch's metadata with persistent buffers, and that metadata outlives the forward (the runner hands it to the speculator). The three DeepSeek-V4.1 metadata classes carrycommonandbuilderback-references for this, andCommonAttentionMetadata.replace_tokensproduces the copy over the compacted token set.CUDA graphs: DeepSeek-V4.1 uses breakable piecewise graphs by default, so the replay is an eager break of the model graph that re-plans from each step's batch, and the replay layers run their own breakable graphs keyed by the padded replay size, captured while the outer capture is paused. Decode batches and full graphs never trim. The compacted slot mappings live in fixed buffers because the window KV insert inside the replay graph reads them by address.
Per-step plan data (rows, query boundaries, window starts) is laid out on the CPU and read by the GPU through the runner's
UvaBufferPool, so no pinned memory is allocated and no H2D copy is issued per step. The new per-step GPU work is the gathers and scatters, onetorch.maximum, and the rebuilt replay-layer metadata.Second commit: the DSpark drafter's layers are all sliding-window attention, so it can only read the last
windowcontext positions of a request and its context-KV precompute now runs on those rows only (the DFlash precompute gains a row-selection hook that keeps every row by default). Under decoder trimming the other rows carry no real hidden state, so this also stops the drafter from inserting KV derived from them. It rides the same switch.Switch: part of
--swa-bounded-replay(on by default), since replay-layer window KV is unwritten outside each request's replay window and a prefix-cache hit must therefore replay the window, which is what the encoder side does. The decoder side stays off with one warning when the model cannot trim safely: a pipeline stage that holds the last KV-source layer but not every layer after it, sequence/data/prefill-context parallelism (the per-rank batch would shrink), an Engram layer after the cut, or a drafter without a sliding window, with a wider one than the target's, or with mixed layer types. Adaptive verification is supported: it splits only the leading verification requests on the GPU, which are never trimmed, so the row selection from the CPU query boundaries holds and the replay batch takes its boundaries from the devicequery_start_locminus the rows trimmed before them.Numerics: this changes cold-prefill outputs for every prompt longer than 128 tokens. Within a request's trailing window the replay layers' SWA attention is floored at the window start, so the first rows of the window attend fewer keys than in a full prefill, and the final token's replay-layer KV inputs come from those rows. The two-pass evaluation below covers both the cold path (pass 1) and the prefix-hit path (pass 2).
Known gaps: prompt logprobs of rows outside the trailing window are computed from scattered zeros, so requests that need
prompt_logprobsshould run with--no-swa-bounded-replayfor now. A DSpark draft config without asliding_windowfield turns the decoder side off with the warning above.Third commit: the DFlash graph-capture path built the draft attention metadata without the dummy batch's
replay_start, so a DSpark drafter (whose sliding-window caches replay like the target's) failed the SWA builder's assertion at capture under--swa-bounded-replay. It now passes the zeroedreplay_start, as the runner's own capture and the drafter's per-step build already do. This is a gap of #56227 with DSpark and could land there instead.Not a duplicate: no open PR trims the DeepSeek-V4.1 replay layers; #56227 is the encoder side this builds on.
Usage
Nothing to configure: with #56227, DeepSeek-V4.1 on model runner V2 replays on both sides by default.
--no-swa-bounded-replayturns both off.Test Plan
Accuracy:
deepseek-ai/DeepSeek-V4.1-Flashon TP4 GB200 withvllm serve, gsm8k (5-shot) run twice against the same server; pass 2 re-sends every prompt, so each request is a full-prompt prefix hit. Both arms on the same tree. Plus a local cold-prefill comparison, replay on vs off, on 12 prompts of 73 to 5415 tokens (the long ones span several prefill chunks), greedy, 32 output tokens; run once plain and once with the checkpoint's built-in DSpark drafter ({"method": "dspark", "num_speculative_tokens": 5, "enable_adaptive_verification": true}), which exercises the drafter's window-only context KV and adaptive verification's GPU-side query boundaries.Test Result
50 tests pass; pre-commit and mypy are clean.
--no-swa-bounded-replay)All four numbers are within run-to-run noise of each other. The plain cold-prefill comparison produced coherent, correct completions on both sides; 10 of 12 first tokens were identical (the two exceptions are a greedy near-tie and the replay-off side emitting EOS), and the multi-chunk prompts agreed on all 32 tokens. With the DSpark drafter and adaptive verification, all 12 first tokens were identical and every completion coherent and correct; 8 of 12 agreed on all 32 tokens, the rest diverged after a greedy near-tie.
🤖 Developed with Claude Code; all changes reviewed and tested by the author.