Skip to content

[Bugfix][Perf] Let speculative decode use the 3D split-KV attention path - #52879

Draft
guoriyue wants to merge 1 commit into
vllm-project:mainfrom
guoriyue:fix/triton-attn-3d-spec-decode
Draft

guoriyue wants to merge 1 commit into
vllm-project:mainfrom
guoriyue:fix/triton-attn-3d-spec-decode

Conversation

@guoriyue

Copy link
Copy Markdown
Contributor

Purpose

Fixes #48076. The 3D split-KV path is disabled whenever max_seqlen_q > 1, but the real constraint is that softmax_segm_* is allocated per sequence and indexed per query row, so this sizes the scratch per query row and gates on that width instead — letting speculative decode use split-KV rather than falling back to 2D.

Test Plan

pytest tests/kernels/attention/test_triton_unified_attention.py -v

Test Result

1584 passed with no new failures (the 290 use_td failures are pre-existing and reproduce identically on main). A guard-region probe forcing the 3D path at query_len=2 shows the current per-sequence sizing writing out of bounds — 32768 floats past the end at batch 33, 1048576 at batch 64 — and zero once the scratch is sized per query row. Kernel time improves 2.6x–8.9x, scaling with KV length (8.9x at KV=32768, batch 1), and without speculation max_q_len_3D == 1, so every gating decision and the scratch size are unchanged.

`unified_attention` disables the 3D (split-KV) path whenever
`max_seqlen_q > 1`. The comment attributes this to prefill, but the real
constraint is scratch capacity: the kernel and `reduce_segments` index
`softmax_segm_*` per *query row* --

    query_offset_0 = cur_batch_in_all_start_index + query_pos   # token index
    segm_output[query_offset_0, head, segm_idx, :]

-- while the builder allocates `seq_threshold_3D` rows, i.e. one per
*sequence*. The two coincide only at `query_len == 1`, which is why the
mismatch has been invisible.

Speculative decoding makes `max_seqlen_q = 1 + num_speculative_tokens`, so
it is swept onto the 2D path even though split-KV is exactly what a
long-KV, few-query-rows batch wants. Widening the gate alone is not safe:
with per-sequence scratch the kernel then writes past the end of all three
buffers. A guard-region probe on this tree, forcing the 3D path at
`query_len = 2`:

    batch=32  total_q=64   rows=64 | out-of-bounds writes:       0
    batch=33  total_q=66   rows=64 | out-of-bounds writes:   32768
    batch=64  total_q=128  rows=64 | out-of-bounds writes: 1048576

Size the scratch per query row instead, and gate on that width rather than
on `max_seqlen_q > 1`, with an exact capacity check so an oversized batch
degrades to 2D instead of corrupting memory. True prefill still takes the
2D path: its query length far exceeds the decode width, and split-KV would
not help it.

The width is derived from the speculative config directly rather than via
`_init_reorder_batch_threshold`, so this does not change the backend's
`reorder_batch_threshold` and therefore does not alter batch reordering.

Without speculation `max_q_len_3D == 1`, every gating decision is
unchanged, and the scratch keeps its current size.

Signed-off-by: Mingfei Guo <1800012773@pku.edu.cn>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Performance]: Triton attention drops long-context decode from 3D split-KV to 2D at batch ≥ ~12, ~doubling ITL

1 participant