[AMD][Spec] Accelerate Qwen3.5 verification with grouped-head shared KV (#34517)

Co-authored-by: chuyeh <chuyeh@users.noreply.github.com>
Co-authored-by: Thomas Wang <1am9trash@gmail.com>
This commit is contained in:
chuyeh
2026-08-15 00:15:52 -07:00
committed by GitHub
co-authored by chuyeh Thomas Wang
parent 5c9ee86d90
commit 6cbfa791d6
4 changed files with 314 additions and 41 deletions
@@ -0,0 +1,235 @@
"""Correctness tests for grouped-head target-verify attention."""
import unittest
from types import SimpleNamespace
from unittest.mock import patch
import torch
from sglang.kernels.ops.attention.extend_attention import extend_attention_fwd
from sglang.kernels.ops.attention.verify_mla import verify_shared_kv_fwd
from sglang.srt.layers.attention.triton_backend import (
_should_use_verify_shared_kv,
)
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.test_utils import CustomTestCase
register_amd_ci(est_time=30, suite="stage-b-test-1-gpu-small-amd-mi35x")
BF16_ATOL = 2e-2
BF16_RTOL = 1e-2
FP8_ATOL = 8e-2
FP8_RTOL = 2e-2
def _build_inputs(
prefix_lens,
l_ext,
h_q,
head_dim,
v_head_dim,
cache_dtype=torch.bfloat16,
):
device = "cuda"
dtype = torch.bfloat16
generator = torch.Generator(device=device).manual_seed(0)
prefix_lens_t = torch.tensor(prefix_lens, dtype=torch.int32, device=device)
total_prefix = sum(prefix_lens)
batch_size = len(prefix_lens)
num_extend_tokens = batch_size * l_ext
def randn(*shape):
return torch.randn(*shape, dtype=dtype, device=device, generator=generator)
q = randn(num_extend_tokens, h_q, head_dim)
k = randn(num_extend_tokens, 1, head_dim)
v = randn(num_extend_tokens, 1, v_head_dim)
k_buffer = randn(total_prefix, 1, head_dim).to(cache_dtype)
v_buffer = randn(total_prefix, 1, v_head_dim).to(cache_dtype)
qo_indptr = torch.arange(
0, num_extend_tokens + 1, l_ext, dtype=torch.int32, device=device
)
kv_indptr = torch.zeros(batch_size + 1, dtype=torch.int32, device=device)
kv_indptr[1:] = torch.cumsum(prefix_lens_t, dim=0)
kv_indices = torch.arange(total_prefix, dtype=torch.int64, device=device)
return q, k, v, k_buffer, v_buffer, qo_indptr, kv_indptr, kv_indices
@unittest.skipIf(not torch.cuda.is_available(), "GPU required")
class TestVerifySharedKV(CustomTestCase):
def _run_parity(
self,
head_dim,
v_head_dim,
h_q=4,
cache_dtype=torch.bfloat16,
k_scale=1.0,
v_scale=1.0,
l_ext=4,
atol=BF16_ATOL,
rtol=BF16_RTOL,
):
inputs = _build_inputs(
prefix_lens=[512, 2048],
l_ext=l_ext,
h_q=h_q,
head_dim=head_dim,
v_head_dim=v_head_dim,
cache_dtype=cache_dtype,
)
q, k, v, k_buffer, v_buffer, qo_indptr, kv_indptr, kv_indices = inputs
output_shape = (q.shape[0], q.shape[1], v_head_dim)
reference = torch.empty(output_shape, dtype=q.dtype, device=q.device)
actual = torch.empty_like(reference)
scale = head_dim**-0.5
extend_attention_fwd(
q,
k,
v,
reference,
k_buffer,
v_buffer,
qo_indptr,
kv_indptr,
kv_indices,
None,
True,
None,
l_ext,
k_scale,
v_scale,
sm_scale=scale,
)
ran = verify_shared_kv_fwd(
q,
k,
v,
actual,
k_buffer,
v_buffer,
qo_indptr,
kv_indptr,
kv_indices,
None,
True,
None,
l_ext,
k_scale,
v_scale,
sm_scale=scale,
max_bs=len(kv_indptr) - 1,
)
self.assertTrue(ran)
torch.testing.assert_close(actual, reference, atol=atol, rtol=rtol)
def test_qwen3_5_tp_shapes(self):
# Qwen3.5 has 32 global query heads. TP8, TP4, and InferenceX's TP2
# configurations expose 4, 8, and 16 local query heads respectively,
# all sharing one TP-local KV head.
for h_q in (4, 8, 16):
with self.subTest(h_q=h_q):
self._run_parity(head_dim=256, v_head_dim=256, h_q=h_q)
def test_qwen3_5_short_verify_widths(self):
for l_ext in (1, 2, 3):
with self.subTest(l_ext=l_ext):
self._run_parity(head_dim=256, v_head_dim=256, l_ext=l_ext)
def test_qwen3_5_fp8_kv_cache(self):
self._run_parity(
head_dim=256,
v_head_dim=256,
h_q=8,
cache_dtype=torch.float8_e4m3fn,
k_scale=0.5,
v_scale=0.25,
atol=FP8_ATOL,
rtol=FP8_RTOL,
)
def test_kimi_k3_absorbed_mla_shape(self):
self._run_parity(head_dim=576, v_head_dim=512)
def test_rejects_multiple_local_kv_heads(self):
inputs = list(
_build_inputs(
prefix_lens=[512],
l_ext=4,
h_q=4,
head_dim=256,
v_head_dim=256,
)
)
for index in (1, 2, 3, 4):
inputs[index] = inputs[index].expand(-1, 2, -1).contiguous()
q, k, v, k_buffer, v_buffer, qo_indptr, kv_indptr, kv_indices = inputs
output = torch.empty_like(q)
self.assertFalse(
verify_shared_kv_fwd(
q,
k,
v,
output,
k_buffer,
v_buffer,
qo_indptr,
kv_indptr,
kv_indices,
None,
True,
None,
4,
1.0,
1.0,
)
)
@patch(
"sglang.srt.layers.attention.triton_backend.is_gfx95_supported",
return_value=True,
)
@patch("sglang.srt.layers.attention.triton_backend.get_parallel")
def test_backend_dispatch_gate(self, get_parallel_mock, _is_gfx95_mock):
get_parallel_mock.return_value = SimpleNamespace(
attn_tp_size=8, attn_dcp_size=1
)
def model_config(architecture, local_kv_heads=1):
return SimpleNamespace(
hf_config=SimpleNamespace(architectures=[architecture]),
get_num_kv_heads=lambda _tp, _dcp: local_kv_heads,
)
qwen = model_config("Qwen3_5MoeForCausalLM")
self.assertTrue(_should_use_verify_shared_kv(qwen, 1, False, True))
self.assertFalse(_should_use_verify_shared_kv(qwen, 2, False, True))
self.assertFalse(_should_use_verify_shared_kv(qwen, 1, False, False))
self.assertFalse(
_should_use_verify_shared_kv(
model_config("Qwen3_5MoeForCausalLM", local_kv_heads=2),
1,
False,
True,
)
)
self.assertFalse(
_should_use_verify_shared_kv(
model_config("LlamaForCausalLM"), 1, False, True
)
)
self.assertTrue(
_should_use_verify_shared_kv(
model_config("KimiK3ForConditionalGeneration"), 1, True, False
)
)
with patch(
"sglang.srt.layers.attention.triton_backend.is_gfx95_supported",
return_value=False,
):
self.assertFalse(_should_use_verify_shared_kv(qwen, 1, False, True))
if __name__ == "__main__":
unittest.main()