Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
"""Triton-kernel parity test for the page-aware decode / extend kernels.
|
|
|
|
Verifies that the modified decode / extend Triton kernels produce
|
|
bit-identical output when called against:
|
|
|
|
(a) the legacy 3-D ``[N, head, dim]`` KV view (PAGE_SIZE=1 default),
|
|
(b) the new 4-D ``[num_pages, page_size, head, dim]`` view with
|
|
``page_size=1`` (degenerate envelope — same physical bytes as (a)),
|
|
(c) the new 4-D view with ``page_size>1`` (layer-major), using the
|
|
same logical KV data but routed via page-aware address math.
|
|
|
|
Output for (a) vs (b) must be bit-identical at PAGE_SIZE=1 (the kernel
|
|
specializes to the legacy branch). Output for (c) must match a hand-
|
|
computed reference SDPA result (same logical attention; different byte
|
|
layout).
|
|
|
|
Skipped on CPU — Triton requires a GPU.
|
|
|
|
python -m pytest test/registered/unit/mem_cache/test_triton_kernel_layout.py -v
|
|
"""
|
|
|
|
import unittest
|
|
|
|
import torch
|
|
|
|
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
|
|
|
_HAS_CUDA = torch.cuda.is_available()
|
|
|
|
register_cuda_ci(est_time=30, stage="base-b", runner_config="1-gpu-small")
|
|
register_amd_ci(est_time=5, stage="stage-b", runner_config="1-gpu-small-amd")
|
|
|
|
|
|
@unittest.skipUnless(_HAS_CUDA, "Triton kernels require CUDA")
|
|
class TestTritonKernelLayoutParity(unittest.TestCase):
|
|
"""Decode + extend kernel parity across (3-D, 4-D ps=1, 4-D ps>1)."""
|
|
|
|
def _setup_decode_inputs(
|
|
self, bs=2, head_num=2, head_dim=8, num_slots=64, dtype=torch.float16
|
|
):
|
|
torch.manual_seed(0xC0FFEE)
|
|
# Logical KV: shape [num_slots, head_num, head_dim]
|
|
logical_kv_k = torch.randn(
|
|
num_slots, head_num, head_dim, dtype=dtype, device="cuda"
|
|
)
|
|
logical_kv_v = torch.randn(
|
|
num_slots, head_num, head_dim, dtype=dtype, device="cuda"
|
|
)
|
|
q = torch.randn(bs, head_num, head_dim, dtype=dtype, device="cuda")
|
|
# All requests use the first `seq_len` slots.
|
|
seq_len = 16
|
|
kv_indices_per_req = torch.arange(seq_len, dtype=torch.int64, device="cuda")
|
|
kv_indices = kv_indices_per_req.repeat(bs) # [bs * seq_len]
|
|
kv_indptr = torch.tensor(
|
|
[i * seq_len for i in range(bs + 1)], dtype=torch.int32, device="cuda"
|
|
)
|
|
return q, logical_kv_k, logical_kv_v, kv_indptr, kv_indices, seq_len
|
|
|
|
def _run_decode(self, q, k_buf, v_buf, kv_indptr, kv_indices, page_size):
|
|
from sglang.kernels.ops.attention.decode_attention import (
|
|
decode_attention_fwd,
|
|
)
|
|
|
|
bs, head_num, head_dim = q.shape
|
|
max_kv_splits = 4
|
|
attn_logits = torch.empty(
|
|
(bs, head_num, max_kv_splits, head_dim),
|
|
dtype=torch.float32,
|
|
device="cuda",
|
|
)
|
|
attn_lse = torch.empty(
|
|
(bs, head_num, max_kv_splits),
|
|
dtype=torch.float32,
|
|
device="cuda",
|
|
)
|
|
o = torch.empty_like(q)
|
|
num_kv_splits = torch.full(
|
|
(bs,), max_kv_splits, dtype=torch.int32, device="cuda"
|
|
)
|
|
decode_attention_fwd(
|
|
q,
|
|
k_buf,
|
|
v_buf,
|
|
o,
|
|
kv_indptr,
|
|
kv_indices,
|
|
attn_logits,
|
|
attn_lse,
|
|
num_kv_splits,
|
|
max_kv_splits,
|
|
sm_scale=1.0 / (head_dim**0.5),
|
|
k_scale=1.0,
|
|
v_scale=1.0,
|
|
logit_cap=0.0,
|
|
page_size=page_size,
|
|
)
|
|
return o
|
|
|
|
def test_decode_3d_vs_4d_ps1_byte_identical(self):
|
|
"""(a) vs (b): same physical bytes, different view shape.
|
|
Triton specializes PAGE_SIZE=1 to the legacy branch; output must
|
|
be bit-identical (modulo non-deterministic FP add ordering, which
|
|
we sidestep here since the kernels use deterministic reductions
|
|
for fixed input + grid)."""
|
|
q, k, v, kv_indptr, kv_indices, seq_len = self._setup_decode_inputs()
|
|
# (a) legacy 3-D view
|
|
o_3d = self._run_decode(q, k, v, kv_indptr, kv_indices, page_size=1)
|
|
# (b) 4-D view: reshape SAME physical bytes to (num_pages=N, 1, head, dim)
|
|
num_slots = k.shape[0]
|
|
k_4d = k.view(num_slots, 1, *k.shape[1:])
|
|
v_4d = v.view(num_slots, 1, *v.shape[1:])
|
|
o_4d_ps1 = self._run_decode(q, k_4d, v_4d, kv_indptr, kv_indices, page_size=1)
|
|
# bit-identical (same byte layout, same PAGE_SIZE specialization)
|
|
self.assertTrue(torch.equal(o_3d, o_4d_ps1))
|
|
|
|
def test_extend_3d_vs_4d_ps1_byte_identical(self):
|
|
"""Same parity check for extend kernel."""
|
|
from sglang.kernels.ops.attention.extend_attention import (
|
|
extend_attention_fwd,
|
|
)
|
|
|
|
torch.manual_seed(0xDEADBEEF)
|
|
# head_dim must be >= 16: the extend kernel's QK^T tl.dot requires the
|
|
# contraction dim K (= head_dim) >= 16 on modern GPU archs (Hopper+).
|
|
head_num, head_dim = 2, 32
|
|
num_slots = 32
|
|
dtype = torch.float16
|
|
bs = 2
|
|
prefix_len = 8
|
|
extend_len = 4
|
|
|
|
k_buffer = torch.randn(
|
|
num_slots, head_num, head_dim, dtype=dtype, device="cuda"
|
|
)
|
|
v_buffer = torch.randn(
|
|
num_slots, head_num, head_dim, dtype=dtype, device="cuda"
|
|
)
|
|
q_extend = torch.randn(
|
|
bs * extend_len, head_num, head_dim, dtype=dtype, device="cuda"
|
|
)
|
|
k_extend = torch.randn(
|
|
bs * extend_len, head_num, head_dim, dtype=dtype, device="cuda"
|
|
)
|
|
v_extend = torch.randn(
|
|
bs * extend_len, head_num, head_dim, dtype=dtype, device="cuda"
|
|
)
|
|
o = torch.empty_like(q_extend)
|
|
|
|
qo_indptr = torch.tensor(
|
|
[i * extend_len for i in range(bs + 1)], dtype=torch.int32, device="cuda"
|
|
)
|
|
kv_indptr = torch.tensor(
|
|
[i * prefix_len for i in range(bs + 1)], dtype=torch.int32, device="cuda"
|
|
)
|
|
kv_indices = torch.arange(prefix_len, dtype=torch.int64, device="cuda").repeat(
|
|
bs
|
|
)
|
|
|
|
def run(k_buf, v_buf, page_size):
|
|
o_out = torch.empty_like(q_extend)
|
|
extend_attention_fwd(
|
|
q_extend,
|
|
k_extend,
|
|
v_extend,
|
|
o_out,
|
|
k_buf,
|
|
v_buf,
|
|
qo_indptr,
|
|
kv_indptr,
|
|
kv_indices,
|
|
custom_mask=None,
|
|
is_causal=True,
|
|
mask_indptr=None,
|
|
max_len_extend=extend_len,
|
|
k_scale=1.0,
|
|
v_scale=1.0,
|
|
sm_scale=1.0 / (head_dim**0.5),
|
|
page_size=page_size,
|
|
)
|
|
return o_out
|
|
|
|
o_3d = run(k_buffer, v_buffer, page_size=1)
|
|
k_4d = k_buffer.view(num_slots, 1, *k_buffer.shape[1:])
|
|
v_4d = v_buffer.view(num_slots, 1, *v_buffer.shape[1:])
|
|
o_4d_ps1 = run(k_4d, v_4d, page_size=1)
|
|
self.assertTrue(torch.equal(o_3d, o_4d_ps1))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|