Files
sglang/test/registered/unit/mem_cache/test_triton_kernel_layout.py
T

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()