Skip to content

[Perf][GDN] Share GDN metadata across cache groups for native MTP of any depth - #699

Merged
yangzhuxinyzx merged 2 commits into
1CatAI:mainfrom
Leonccaa:pr/sm70-mtp-shared-gdn-any-depth
Sep 26, 2026
Merged

yangzhuxinyzx merged 2 commits into
1CatAI:mainfrom
Leonccaa:pr/sm70-mtp-shared-gdn-any-depth

Conversation

@Leonccaa

Copy link
Copy Markdown
Contributor

Purpose

Follow-up to #684 (see this comment). #684 made native MTP4 classify each decode batch once and build every GDN cache group's state rows in one fused launch. Before that, metadata was built once per group per step, at about 0.65 ms of host time per group on V100.

Nothing in that path depends on the draft depth except num_spec_state_tokens. The shared request metadata, the fused state rows (width num_spec + 1) and the sequence-length align column are all computed per row. Only the two gates required num_speculative_tokens == 4, so MTP1–MTP3 deployments, including our MTP3 Qwen3.8 serving stack, still build per group.

This PR:

  • Accepts native MTP at any depth in the two gates (gdn_attn.py, mamba_hybrid.py). The VLLM_SM70_MTP4_SHARED_GDN_METADATA / VLLM_SM70_MTP4_FUSED_GDN_METADATA switches and their defaults are unchanged and still roll the paths back; the names keep their historical prefix, and comments and log lines now say MTP.
  • Fixes the fused-metadata shadow oracle (VLLM_SM70_DFLASH2_GDN_METADATA_SHADOW, debug-only), in a separate commit. The fused kernel writes a step's rows only up to num_actual_tokens, but the oracle required every entry of the shared spec_sequence_masks buffer after the live rows to be False, including rows no graph replay of this step reads. That buffer came from torch.empty, so the oracle passed or failed depending on leftover device memory; widening the tests to more depths made that visible. The mask is now allocated zeroed and the oracle checks only the padded rows the step writes and replays. Replay behavior is unchanged.

Test Plan

  • pytest tests/v1/attention/test_gdn_metadata_builder.py tests/v1/spec_decode/test_dflash2.py on V100 (SM70). test_mtp_shared_gdn_metadata_matches_legacy and test_mtp_fused_gdn_group_metadata_matches_legacy_replay now cover MTP1 through MTP4 against separate per-group builds. The fused test runs every verifier query length at each depth, in both none and align cache modes, with the shadow oracle on. Run twice to check stability.
  • End-to-end evidence for MTP3 comes from our serving stack (4x V100 TP4, AWQ g32, E4M3 KV, MTP3 greedy, prefix caching with align, grouped CPU offload, FULL_AND_PIECEWISE graphs), where we ran an equivalent implementation (details in the [Perf] Share Flash-Next decode fast paths with MTP4 by default #684 comment).

Test Result

Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

🤖 Generated with Claude Code

Leon added 2 commits September 26, 2026 06:03
…racle

The shared fused-metadata buffers are sized for the largest graph batch,
and the fused kernel writes a step's rows only up to num_actual_tokens.
The shadow oracle (VLLM_SM70_DFLASH2_GDN_METADATA_SHADOW) nevertheless
required every entry of spec_sequence_masks after the live speculative
rows to be False, including rows past num_actual_tokens that no graph
replay of this step reads. Those rows were allocated with torch.empty, so
the oracle failed or passed depending on leftover device memory.

Allocate the mask zeroed, and check only the padded rows this step writes
and replays. Replay behavior is unchanged; the oracle is debug-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Signed-off-by: Leon <tomsel1010.us2@gmail.com>
…any depth

1CatAI#684 made native MTP4 classify each decode batch once and build every GDN
cache group's state rows in one fused launch, instead of one metadata
build per group per step. Nothing in that path depends on the draft
depth except num_spec_state_tokens: the shared request metadata, the
fused state rows (width num_spec + 1) and the sequence-length align
column are all written per row. Only the gates required
num_speculative_tokens == 4.

Accept native MTP at any depth. The VLLM_SM70_MTP4_* switches and their
defaults are unchanged and still roll the paths back; the names keep
their historical prefix.

The shared-metadata and fused-replay tests now run MTP1 through MTP4,
each fused case at every verifier query length and in both cache modes,
against separate per-group builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Signed-off-by: Leon <tomsel1010.us2@gmail.com>
@yangzhuxinyzx
yangzhuxinyzx merged commit b034648 into 1CatAI:main Sep 26, 2026
4 checks passed
Leonccaa pushed a commit to Leonccaa/1Cat-vLLM that referenced this pull request Sep 26, 2026
Under DCP2 each rank holds half of a target QSA layer's main K/V. The
shared CSA+linear pool sizes a block so that one physical page holds one
recurrent (GDN) state, so keeping a whole state-sized page per sharded
layer doubled the global block span (3,200 tokens instead of DCP1's 1,600
with E4M3 KV). The replicated selector, the draft and every request's
recurrent-state blocks paid for that doubled span, short requests fit
worse than on DCP1, and a prefix shorter than 3,200 tokens could never be
reused.

A block now spans cache_config.block_size global tokens at every DCP
size: a sharded target layer holds block_size // dcp slots per rank, and
the allocator packs sharded owners whose pages cannot hold a state, in
layer order, as few per physical page as hold one (two at DCP2). A packed
page interleaves its members one kernel block at a time, so each member
is a strided view of the shared tensor and every kernel that honours the
block stride (the cache write and the QSA sparse attention) works
unchanged.

- qsa_dcp_block_geometry returns span = block_size; the model asks the
  platform to align the block size to kernel block x dcp so the
  rank-local share stays kernel aligned (an lcm with any user block size).
- The allocator groups owners (_pack_csa_linear_main_kv), places
  recurrent states per physical owner, and emits one KVCacheTensor per
  physical page with packed_members.
- The v2 GPU runner lays out packed members as interleaved strided views;
  the v1 runner and pipeline parallelism reject packed configs.
- The offload worker registers a packed page once, by its first member,
  covering every member's kernel blocks of that pool block.
- The replicated draft expands its table from the target's rank-local
  page.

Fewer physical owners per block (7 instead of 13) give six GDN state
groups instead of three. Each group builds its metadata every decode step
unless native MTP shares that metadata across groups (1CatAI#684 for MTP4,
1CatAI#699 for any depth).

TP4 V100, MTP3, CUDA graphs, GPU KV tokens (DCP1 -> DCP2):
- E4M3 KV (E4M3 MTP lane and grouped CPU offload applied): block 1,600
  on both; 32K/C2 396,336 -> 493,244 (the previous 3,200-token span gave
  439,296); 262,144/C4/GMU 0.96 775,096 -> 1,178,337 (1,142,744); repeated
  2K/8K prefix TTFT 834/818 ms before this change, now 270/256 ms (DCP1
  226/235).
- FP16 KV: block 800 on both; 32K/C2 284,341 -> 404,706; 262,144/C4/GMU
  0.96 433,401 -> 718,015.
- Accuracy gate identical to DCP1 (tokens and MTP acceptance) with both
  KV dtypes; offload eviction+recovery, mixed prefill/decode and 5x30K
  admission regressions pass with DCP1-identical outputs.

Unit tests cover the geometry, allocator packing and capacity projection,
interleaved views (NHD/HND), offload registration, and a V100 test that
writes packed members through the real cache writer and reads them with
QSA sparse attention bit-identically to unpacked caches (FP16 and E4M3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Signed-off-by: Leon <tomsel1010.us2@gmail.com>
Leonccaa pushed a commit to Leonccaa/1Cat-vLLM that referenced this pull request Sep 26, 2026
Describe target-only DCP2 for Qwen4Exp QSA on TP4 V100: cache ownership,
the packed layout and its geometry, the attention path, the related changes
(1CatAI#664 for E4M3 KV with MTP; 1CatAI#617/1CatAI#598 for grouped CPU offload, whose DCP
pieces this series adds; 1CatAI#684/1CatAI#699 for GDN metadata), the validation gates
and results, and the known limits.

Assisted-by: OpenAI Codex
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Signed-off-by: Leon <tomsel1010.us2@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants