Admit MTP/EAGLE spec-decode steps and sliding-window layers into the Triton 3D flash-decoding path (B300, NVFP4) - #45450
Conversation
Upstream-viable subset (maintainer-approved tracks only): co-authored round-1 attention track (single physical track, two owners). OP-001: admit MTP/EAGLE spec-decode steps (query_len = 1+num_spec, e.g. 5) into the 3D flash-decoding path by replacing the hardcoded `max_seqlen_q > 1` 3D-admission test with `max_seqlen_q > decode_query_len`, and resizing the three 3D softmax-segment buffers' first dim from seq_threshold_3D to seq_threshold_3D*decode_query_len (with a fail-fast shape assert). OP-003: window-relative 3D segmentation (WINDOW_SEG_3D) for sliding-window layers — the NUM_SEGMENTS_PER_SEQ parallel-softmax segments tile only the sliding window's tile range instead of the full sequence, restoring ~11/16 active segments vs the window-blind 1/16 collapse at 27k context with a 1024 window; a new shared @triton.jit helper compute_window_tile_range / compute_window_segments keeps the mainloop early-return and reduce_segments mask from drifting. No env flag: guarded structurally — reduces to byte-identical original behavior when decode_query_len==1 (non-spec) and window_size[0]<0 (global) or use_mm_prefix. [always-on-guarded]. Signed-off-by: Jin Huang <jinhun@amazon.com>
c86d978 to
486808f
Compare
|
Hi @jinhuang12 — I opened #46724, which relaxes the same The two PRs target different, non-overlapping workloads:
We also independently converged on the same design choices: both are structurally guarded with no env flag, both reduce to byte-for-byte the original path for ordinary Because my branch only flips |
|
This pull request has merge conflicts that must be resolved before it can be |
Summary
This PR makes the Triton unified-attention 3D flash-decoding (split-KV) path usable for two cases that currently fall back to the slower 2D path: (1) speculative-decode verify steps, where each forward pass carries
query_len = 1 + num_speculative_tokens(e.g. 5) instead of 1, and (2) sliding-window attention layers at long context. It was developed and measured on NVIDIA B300 SXM6 (Blackwell,sm_103), TP=1, runningnvidia/gemma-4-31B-it-NVFP4with MTP speculative decoding (num_speculative_tokens=4). Base is vLLM v0.22.1 (commit0decac0d96c42b49572498019f0a0e3600f50398).The change is structurally guarded, not flag-gated: it reduces to byte-for-byte the original code for every non-speculative caller (
query_len == 1) on global/non-sliding layers, so the default decode path on every other model and arch is unchanged. There is no new env var to set.This branch is rebased onto current
main. The optimization commit was developed and measured on v0.22.1; rebasing ontomainrequired threading theUSE_CAUSAL/USE_PER_SEQ_CAUSALconstexprs added by #45163 through the new shared window-segmentation helpers (compute_window_tile_range/compute_window_segments) so the non-causal sliding-window path and the window-relative 3D segmentation stay consistent. The numbers below were measured on v0.22.1 and have not been re-validated onmain.Optimizations
max_seqlen_q > 1, which forced every spec-decode verify step (uniformquery_len = 1 + num_spec, e.g. 5) onto the 2D path. It is replaced withmax_seqlen_q > decode_query_len, wheredecode_query_lenis derived fromspeculative_config(None-guarded to 1 when there is no spec). The three 3D softmax-segment buffers' first dim is resized fromseq_threshold_3Dtoseq_threshold_3D * decode_query_len, with a fail-fast shape assert. Whendecode_query_len == 1the admission test is identical to the originalmax_seqlen_q > 1, so non-spec callers are untouched.WINDOW_SEG_3D) so theNUM_SEGMENTS_PER_SEQsegments tile only the window's live tile range. A shared@triton.jithelper (compute_window_tile_range/compute_window_segments) is used by both the mainloop early-return and thereduce_segmentsmask so the two kernels cannot drift apart. Sliding-window layers are admitted to 3D only under spec-decode (decode_query_len > 1 and window_size[0] >= 0); global layers (window_size[0] < 0) and themm_prefixpath are excluded.Fixed-batch latency
vllm bench latency—nvidia/gemma-4-31B-it-NVFP4, B300 SXM6, TP=1, NVFP4,--max-num-seqs 32, MTP spec-decode (num_speculative_tokens=4), output_len=150, CUDA graphs (FULL_AND_PIECEWISE) + torch.compile inductor, 5 iters per shape. A = base (vLLM v0.22.1, unpatched); B = this PR. Both arms run the same base commit0decac0d9with an identical workload; the only difference is the patched files. The grid sweeps input length × batch size in a single model load per arm. OTPS = output tokens/sec; TPOT = time per output token (both decode-path, from per-request metrics).The decode-path metrics (OTPS, TPOT) are derived from per-request timing and improve at every shape, scaling with context length — largest at input_len=27000, batch=1, where the unpatched path falls back to the 2D kernel for the entire spec-verify decode (decode time 1.999 s → 0.465 s). On end-to-end latency, only the two 27000-token shapes have a delta that clears twice the baseline's per-iteration standard deviation; the shorter-context E2E deltas sit inside that ±2σ band and are not distinguishable from run-to-run variance (at 8192/bs1, for example, the baseline per-iteration standard deviation is ~0.57 s, so the ±2σ band of ~1.14 s spans the 0.67 s mean delta). Prefill latency is identical between arms at every shape (e.g. 4.263 s vs 4.261 s at 27000/bs1), consistent with the change touching only the decode path. No shape regresses on any metric.
Correctness
GSM8K (greedy, full 1319-question split), B300, vLLM v0.22.1, same config as above:
Both changes are structurally guarded no-ops on the default path and bit-exact on the admitted spec-decode/sliding-window paths; the GSM8K delta is consistent with FP-associativity churn from the segmented-softmax reduction order rather than an algorithmic change.
After rebasing onto
main, the rebased kernel was re-checked with a differential numerics test (unified_attentionvs a dense PyTorch reference) over a 74-case matrix: 2D vs 3D flash-decoding, global vs sliding-window (64/128/256/1024), pure-decode vs spec-decode-verify (query_len 3/5), causal vs per-seq-causal (theUSE_PER_SEQ_CAUSALpath from #45163), MHA/GQA/MQA, head_size 128/256, and softcap on the window-relative 3D path. All 74 matched the reference (max abs error consistent with bf16 rounding). The window-relative 3D segmentation branch was confirmed to actually engage on the spec-decode + sliding-window cases (verified via thereduce_segmentslaunch). Scope of this re-check: L40S (sm_89), bf16, kernel-level vs reference only — it is not a B300/NVFP4 run and not an end-to-end GSM8K. The B300/NVFP4 GSM8K numbers above are from the pre-rebase v0.22.1 tree and have not been re-run onmain.Changes
3 files, +351/−35 (rebased onto
main), Python/Triton only — nocsrc/or C++/CUDA changes, so no recompilation is required. (The +20-line delta over the original v0.22.1 commit is theUSE_CAUSAL/USE_PER_SEQ_CAUSALthreading needed to integrate with #45163.)vllm/v1/attention/ops/triton_unified_attention.py):decode_query_lenkwarg + rewritten 3D-admission gate; window-relative segmentation (WINDOW_SEG_3D) in the mainloop andreduce_segments.vllm/v1/attention/ops/triton_attention_helpers.py):compute_window_tile_range/compute_window_segmentsused by both kernels to keep segmentation layout bit-identical.vllm/v1/attention/backends/triton_attn.py): derivedecode_query_lenfromspeculative_config(defaults to 1), resize the three 3D segment buffers toseq_threshold_3D * decode_query_len, plus a buffer-shape invariant assert.AI-assisted contribution
This change was produced by an automated process driving AMMO under human review. Each change was checked for correctness (a bit-exact comparison against the 2D reference, plus a GSM8K before/after) and benchmarked before/after for latency; all numbers above were measured on real B300 hardware under CUDA graphs and torch.compile. With the optimization reducing to a structural no-op for non-speculative, non-sliding callers, the default decode path is byte-for-byte identical to base.