[Feature][Attention] Cover a whole spec-verify in one DiffKV split-KV program (#59085)

Signed-off-by: Giulio De Pasquale <git@depasquale.giugl.io>
This commit is contained in:
Giulio De Pasquale
2026-10-10 11:55:45 +08:00
committed by GitHub
parent 2d69083c4a
commit f1a3c60558
2 changed files with 136 additions and 4 deletions
@@ -2,6 +2,8 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for the Triton DiffKV unified-attention kernel."""
from unittest.mock import patch
import pytest
import torch
@@ -16,6 +18,7 @@ from vllm.v1.attention.backends.fa_utils import (
get_flash_attn_version,
is_flash_attn_varlen_func_available,
)
from vllm.v1.attention.ops import triton_unified_attention_diffkv as diffkv_module
from vllm.v1.attention.ops.triton_unified_attention_diffkv import (
unified_attention_diffkv,
)
@@ -258,3 +261,95 @@ def test_triton_unified_attn_diffkv_fp8_query(seq_lens, seq_threshold_3D):
seq_threshold_3D=seq_threshold_3D,
quant_query=True,
)
def _capture_diffkv_launch(
query_lens: list[int], max_seqlen_q: int, segm_first_dim: int
):
"""Run the launcher with both Triton kernels mocked; return grid/kwargs."""
num_query_heads, num_kv_heads = 8, 2
head_size_qk, head_size_v = 128, 128
num_seqs = len(query_lens)
num_tokens = sum(query_lens)
query = torch.zeros(
num_tokens, num_query_heads, head_size_qk, dtype=torch.bfloat16, device="cpu"
)
kv_cache = torch.zeros(
NUM_BLOCKS,
BLOCK_SIZES[0],
num_kv_heads,
head_size_qk + head_size_v,
dtype=torch.bfloat16,
device="cpu",
)
key_cache = kv_cache[..., :head_size_qk]
value_cache = kv_cache[..., head_size_qk:]
out = torch.zeros(
num_tokens, num_query_heads, head_size_v, dtype=torch.bfloat16, device="cpu"
)
cu_seqlens_q = torch.tensor(
[0] + query_lens, dtype=torch.int32, device="cpu"
).cumsum(dim=0, dtype=torch.int32)
seqused_k = torch.full((num_seqs,), 512, dtype=torch.int32, device="cpu")
block_tables = torch.zeros(num_seqs, 32, dtype=torch.int32, device="cpu")
segm_output, segm_max, segm_expsum = _alloc_segm_buffers(
segm_first_dim, num_query_heads, head_size_v
)
with (
patch.object(diffkv_module, "kernel_unified_attention_diffkv") as mock_kernel,
patch.object(diffkv_module, "kernel_reduce_segments_diffkv") as mock_reduce,
):
unified_attention_diffkv(
q=query,
k=key_cache,
v=value_cache,
out=out,
cu_seqlens_q=cu_seqlens_q,
seqused_k=seqused_k,
softmax_scale=head_size_qk**-0.5,
causal=True,
window_size=(-1, -1),
block_table=block_tables,
softcap=0,
max_seqlen_q=max_seqlen_q,
seq_threshold_3D=8,
num_par_softmax_segments=NUM_PAR_SOFTMAX_SEGMENTS,
softmax_segm_output=segm_output,
softmax_segm_max=segm_max,
softmax_segm_expsum=segm_expsum,
)
grid = mock_kernel.__getitem__.call_args.args[0]
kwargs = mock_kernel.__getitem__.return_value.call_args.kwargs
return grid, kwargs, mock_reduce.__getitem__.called
def test_spec_verify_block_m_grouping_and_launch_kwargs() -> None:
"""Pin program granularity for verify/decode without launching Triton."""
verify_grid, verify_kwargs, verify_reduce = _capture_diffkv_launch(
query_lens=[8, 8], max_seqlen_q=8, segm_first_dim=16
)
num_queries_per_kv = 4
expected_block_m = min(128, next_power_of_2(8 * num_queries_per_kv))
assert verify_grid == (4, 2, NUM_PAR_SOFTMAX_SEGMENTS)
assert verify_kwargs["IS_3D"] is True
assert verify_kwargs["BLOCK_M"] == expected_block_m
assert verify_kwargs["BLOCK_Q"] == expected_block_m // num_queries_per_kv
assert verify_kwargs["TILE_SIZE"] == 16
assert verify_kwargs["num_warps"] == 4
assert "num_stages" not in verify_kwargs
assert verify_reduce
decode_grid, decode_kwargs, decode_reduce = _capture_diffkv_launch(
query_lens=[1], max_seqlen_q=1, segm_first_dim=8
)
assert decode_grid == (1, 2, NUM_PAR_SOFTMAX_SEGMENTS)
assert decode_kwargs["IS_3D"] is True
assert decode_kwargs["BLOCK_M"] == 16
assert decode_kwargs["BLOCK_Q"] == 4
assert decode_kwargs["TILE_SIZE"] == 16
assert "num_warps" not in decode_kwargs
assert "num_stages" not in decode_kwargs
assert decode_reduce
@@ -46,6 +46,13 @@ logger = init_logger(__name__)
is_batch_invariant = envs.VLLM_BATCH_INVARIANT
# Whole-verify split-KV grouping: one program covers all q tokens of a
# spec-decode verify request (BLOCK_M = q_len * GQA group, capped at 128).
_SPEC_3D_MAX_Q = 16
_SPEC_3D_BLOCK_M = 128
_SPEC_3D_NUM_WARPS = 8
_SPEC_3D_TILE = 16
@triton.jit
def kernel_unified_attention_diffkv(
@@ -290,6 +297,9 @@ def kernel_unified_attention_diffkv(
acc,
mask=dim_mask_v[None, :] & query_mask_0[:, None] & query_mask_1[:, None],
)
# A row whose keys are all masked in this segment ends at M=0, L=0;
# -inf makes reduce_segments treat that segment as absent.
M = tl.where(L > 0.0, M, float("-inf"))
store_segm_reduce_scalars(
segm_max_ptr,
segm_expsum_ptr,
@@ -365,6 +375,8 @@ def kernel_reduce_segments_diffkv(
)
segm_max = tl.load(segm_max_ptr + segm_offset, mask=segm_mask, other=float("-inf"))
overall_max = tl.max(segm_max)
# Graph-capture dummy lengths can leave a query with no causal keys.
overall_max = tl.where(overall_max > float("-inf"), overall_max, 0.0)
segm_expsum = tl.load(segm_expsum_ptr + segm_offset, mask=segm_mask, other=0.0)
segm_expsum = segm_expsum * tl.exp(segm_max - overall_max)
@@ -439,29 +451,53 @@ def unified_attention_diffkv(
BLOCK_M = (
16 if num_queries_per_kv <= 16 else triton.next_power_of_2(num_queries_per_kv)
)
BLOCK_Q = BLOCK_M // num_queries_per_kv
total_num_q_blocks = q.shape[0] // BLOCK_Q + num_seqs
launch_kw: dict[str, int] = {}
sliding_window_val = 1 + window_size[0] if window_size[0] >= 0 else 0
# Decide between 2D and 3D launch. Mirrors the standard launcher:
# 3D requires preallocated softmax buffers, decode-only batches, and
# a small number of sequences (otherwise 2D already saturates the SM).
# Spec-decode verify batches (q_len <= _SPEC_3D_MAX_Q, full attention)
# also take 3D: their 2D grid is only q_blocks x kv_heads programs, each
# walking the whole context. Sliding-window layers stay 2D: their loop
# is already window-bounded.
spec_3d = (
1 < max_seqlen_q <= _SPEC_3D_MAX_Q
and sliding_window_val == 0
and softmax_segm_output is not None
and q.shape[0] <= softmax_segm_output.shape[0]
)
use_3d = not (
seq_threshold_3D is None
or num_par_softmax_segments is None
or softmax_segm_output is None
or softmax_segm_max is None
or softmax_segm_expsum is None
or max_seqlen_q > 1
or (max_seqlen_q > 1 and not spec_3d)
or num_seqs > seq_threshold_3D
or is_batch_invariant
)
spec_tile: int | None = None
if use_3d and spec_3d and _SPEC_3D_BLOCK_M > BLOCK_M:
spec_bm = min(
_SPEC_3D_BLOCK_M,
triton.next_power_of_2(max_seqlen_q * num_queries_per_kv),
)
if spec_bm > BLOCK_M and spec_bm % num_queries_per_kv == 0:
BLOCK_M = spec_bm
launch_kw["num_warps"] = _SPEC_3D_NUM_WARPS if BLOCK_M >= 128 else 4
spec_tile = _SPEC_3D_TILE
BLOCK_Q = BLOCK_M // num_queries_per_kv
total_num_q_blocks = q.shape[0] // BLOCK_Q + num_seqs
# Tile size: 32 for prefill-class kernels. Decode (small Q) prefers
# smaller tiles to expose more parallelism along the KV dim.
tile_size = 32 if not use_3d else (16 if q.element_size() >= 2 else 32)
if spec_tile is not None:
tile_size = spec_tile
grid: tuple[Any, ...]
if use_3d:
@@ -530,6 +566,7 @@ def unified_attention_diffkv(
BLOCK_M=BLOCK_M,
NUM_SEGMENTS_PER_SEQ=num_segments,
IS_3D=use_3d,
**launch_kw,
)
if use_3d: