[AMD] support qlen>1 for aiter gluon path for Kimi K3 (#37601)

This commit is contained in:
billishyahao
2026-09-06 19:25:58 -07:00
committed by GitHub
parent b9b39f222d
commit 1d5d85260c
4 changed files with 430 additions and 218 deletions
@@ -0,0 +1,321 @@
"""Unit tests for the aiter Gluon MLA path: h12 + FP8 routing, MTP (qlen>1)
shaping, and dispatch against the zero-pad ``mla_decode_fwd`` fallback.
Mocked throughout — no real aiter/Triton kernel is invoked.
"""
import unittest
from unittest import mock
import torch
from sglang.kernels.ops.quantization.fp8_kernel import fp8_dtype
from sglang.srt.environ import envs
from sglang.srt.layers.attention import aiter_mla_gluon as mod
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=15, suite="base-a-test-cpu")
_GLUON_FN = "sglang.srt.layers.attention.aiter_mla_gluon._gluon_fn"
def _fake_gluon_modules(*, cga_layout: bool):
"""sys.modules entries that make _gluon_fn() see a usable (or too-old) Triton.
``inspect.signature`` is called on ``gl.PaddedSharedLayout``, so that one has
to be a real callable rather than a Mock — the parameter list is exactly what
the Triton >= 3.7 probe reads.
"""
if cga_layout:
def padded_shared_layout(*, cga_layout=None):
pass
else:
def padded_shared_layout():
pass
gl = mock.MagicMock()
gl.PaddedSharedLayout = padded_shared_layout
# `import a.b.c as x` binds via getattr on the parent module, so the parents
# have to point at these exact objects -- a bare MagicMock parent would
# auto-create a different child and shadow them.
triton_gluon = mock.MagicMock(language=gl)
triton_experimental = mock.MagicMock(gluon=triton_gluon)
triton = mock.MagicMock(__version__="3.7.0", experimental=triton_experimental)
fake_fn = mock.Mock(name="mla_gluon")
aiter_mla = mock.MagicMock(mla_gluon=fake_fn)
aiter_gluon = mock.MagicMock(mla_gluon=aiter_mla)
aiter_triton = mock.MagicMock(gluon=aiter_gluon)
aiter_ops = mock.MagicMock(triton=aiter_triton)
aiter = mock.MagicMock(ops=aiter_ops)
return fake_fn, {
"triton": triton,
"triton.experimental": triton_experimental,
"triton.experimental.gluon": triton_gluon,
"triton.experimental.gluon.language": gl,
"aiter": aiter,
"aiter.ops": aiter_ops,
"aiter.ops.triton": aiter_triton,
"aiter.ops.triton.gluon": aiter_gluon,
"aiter.ops.triton.gluon.mla_gluon": aiter_mla,
}
class TestGluonAvailability(CustomTestCase):
"""_gluon_fn() resolves the kernel, or None with the reason logged once."""
def setUp(self):
mod._gluon_fn.cache_clear()
def tearDown(self):
mod._gluon_fn.cache_clear()
def test_none_when_env_disabled(self):
with envs.SGLANG_AITER_MLA_GLUON.override(False):
self.assertIsNone(mod._gluon_fn())
def test_none_when_import_fails(self):
# A None entry in sys.modules makes `import ...` raise ImportError.
with mock.patch.dict("sys.modules", {"aiter.ops.triton.gluon.mla_gluon": None}):
self.assertIsNone(mod._gluon_fn())
def test_none_when_triton_lacks_cga_layout(self):
_fn, modules = _fake_gluon_modules(cga_layout=False)
with mock.patch.dict("sys.modules", modules):
self.assertIsNone(mod._gluon_fn())
def test_returns_kernel_when_ready(self):
fake_fn, modules = _fake_gluon_modules(cga_layout=True)
with mock.patch.dict("sys.modules", modules):
self.assertIs(mod._gluon_fn(), fake_fn)
def test_result_is_cached(self):
fake_fn, modules = _fake_gluon_modules(cga_layout=True)
with mock.patch.dict("sys.modules", modules):
first = mod._gluon_fn()
# Second call must not re-probe: the fake modules are gone by now, so a
# re-probe would return None instead of the cached kernel.
self.assertIs(mod._gluon_fn(), first)
class TestPreferMlaGluonDecode(CustomTestCase):
"""Only the validated h12 + zero-pad + FP8 topology may route to Gluon."""
def _prefer(self, **kwargs):
args = dict(head_pad_mode="zero", num_head=12, kv_cache_dtype=fp8_dtype)
args.update(kwargs)
return mod.prefer_mla_gluon_decode(**args)
def test_true_for_h12_zero_pad_fp8(self):
with mock.patch(_GLUON_FN, return_value=mock.Mock()):
self.assertTrue(self._prefer())
def test_false_when_gluon_unavailable(self):
with mock.patch(_GLUON_FN, return_value=None):
self.assertFalse(self._prefer())
def test_false_for_other_head_counts(self):
with mock.patch(_GLUON_FN, return_value=mock.Mock()):
self.assertFalse(self._prefer(num_head=10))
self.assertFalse(self._prefer(num_head=16))
def test_false_for_non_zero_pad_topology(self):
with mock.patch(_GLUON_FN, return_value=mock.Mock()):
self.assertFalse(self._prefer(head_pad_mode="repeat"))
self.assertFalse(self._prefer(head_pad_mode="none"))
def test_false_for_non_fp8_kv(self):
with mock.patch(_GLUON_FN, return_value=mock.Mock()):
self.assertFalse(self._prefer(kv_cache_dtype=torch.bfloat16))
def _layer(num_head=12, qk_head_dim=576, v_head_dim=512):
layer = mock.Mock()
layer.tp_q_head_num = num_head
layer.qk_head_dim = qk_head_dim
layer.v_head_dim = v_head_dim
layer.scaling = 0.125
layer.logit_cap = 0.0
layer.layer_id = 0
return layer
class TestMlaGluonDecodeShapes(CustomTestCase):
"""Plain decode stays 3-D; target-verify (qlen>1) goes in as 4-D MTP."""
def _call(self, *, num_tokens, qlen):
layer = _layer()
q = torch.zeros(num_tokens, 12, 576, dtype=torch.bfloat16)
captured = {}
def fake_kernel(q_nope, q_pe, kv_c, o, *args, **kwargs):
captured["q_nope"] = q_nope.shape
captured["q_pe"] = q_pe.shape
captured["o"] = o.shape
with mock.patch(_GLUON_FN, return_value=fake_kernel):
out = mod.mla_gluon_decode(
q=q,
k_buffer=torch.zeros(64, 576, dtype=torch.bfloat16),
layer=layer,
kv_indices=torch.zeros(64, dtype=torch.int32),
kv_indptr=torch.zeros(5, dtype=torch.int32),
sm_scale=layer.scaling,
min_kv_seq_len=128,
qlen=qlen,
)
return out, captured
def test_plain_decode_uses_3d(self):
out, cap = self._call(num_tokens=4, qlen=1)
self.assertEqual(cap["q_nope"], torch.Size([4, 12, 512]))
self.assertEqual(cap["q_pe"], torch.Size([4, 12, 64]))
self.assertEqual(cap["o"], torch.Size([4, 12, 512]))
self.assertEqual(out.shape, torch.Size([4, 12, 512]))
def test_verify_uses_4d_mtp(self):
# 4 requests x 8 draft tokens, the DSPARK block-size-7 shape.
out, cap = self._call(num_tokens=32, qlen=8)
self.assertEqual(cap["q_nope"], torch.Size([4, 8, 12, 512]))
self.assertEqual(cap["q_pe"], torch.Size([4, 8, 12, 64]))
self.assertEqual(cap["o"], torch.Size([4, 8, 12, 512]))
# The caller's contract is the flat [num_tokens, H, v] layout.
self.assertEqual(out.shape, torch.Size([32, 12, 512]))
def test_mtp_views_do_not_copy(self):
"""The 4-D q must stay a view of the caller's tensor, not a copy."""
layer = _layer()
q = torch.zeros(32, 12, 576, dtype=torch.bfloat16)
seen = {}
def fake_kernel(q_nope, q_pe, *args, **kwargs):
seen["nope_ptr"] = q_nope.data_ptr()
seen["pe_ptr"] = q_pe.data_ptr()
with mock.patch(_GLUON_FN, return_value=fake_kernel):
mod.mla_gluon_decode(
q=q,
k_buffer=torch.zeros(64, 576, dtype=torch.bfloat16),
layer=layer,
kv_indices=torch.zeros(64, dtype=torch.int32),
kv_indptr=torch.zeros(5, dtype=torch.int32),
sm_scale=layer.scaling,
min_kv_seq_len=128,
qlen=8,
)
self.assertEqual(seen["nope_ptr"], q.data_ptr())
self.assertEqual(seen["pe_ptr"], q[..., 512:].data_ptr())
def test_returns_none_when_gluon_unavailable(self):
with mock.patch(_GLUON_FN, return_value=None):
out = mod.mla_gluon_decode(
q=torch.zeros(4, 12, 576, dtype=torch.bfloat16),
k_buffer=torch.zeros(64, 576, dtype=torch.bfloat16),
layer=_layer(),
kv_indices=torch.zeros(64, dtype=torch.int32),
kv_indptr=torch.zeros(5, dtype=torch.int32),
sm_scale=0.125,
min_kv_seq_len=128,
)
self.assertIsNone(out)
class TestForwardMlaDecodeDispatch(CustomTestCase):
"""_forward_mla_decode picks Gluon or the zero-pad ASM path, never both."""
def _make_backend(self, max_q_len=1):
from sglang.srt.layers.attention.aiter_backend import AiterAttnBackend
be = AiterAttnBackend.__new__(AiterAttnBackend)
be.num_head = 12
be.kv_cache_dtype = fp8_dtype
be.head_pad_mode = "zero"
be.num_head_padded = 16
be.forward_metadata = mock.Mock(
max_q_len=max_q_len,
kv_indices=torch.zeros(4, dtype=torch.int32),
kv_indptr=torch.tensor([0, 1, 2, 3, 4], dtype=torch.int32),
kv_last_page_len=torch.ones(4, dtype=torch.int32),
qo_indptr=torch.arange(5, dtype=torch.int32),
work_metadata=None,
work_indptr=None,
work_info_set=None,
reduce_indptr=None,
reduce_final_map=None,
reduce_partial_map=None,
num_kv_splits=None,
)
be.token_to_kv_pool = mock.Mock(
get_key_buffer=lambda _lid: torch.zeros(8, 576, dtype=fp8_dtype)
)
be._resolve_fp8_kv_scale_float = mock.Mock(return_value=1.0)
be._resolve_mla_gluon_min_kv_seq_len = mock.Mock(return_value=128)
be._mla_decode_fwd_with_head_pad = mock.Mock(
return_value=torch.zeros(4, 12, 512)
)
return be
@mock.patch(
"sglang.srt.layers.attention.aiter_backend.prefer_mla_gluon_decode",
return_value=False,
)
@mock.patch("sglang.srt.layers.attention.aiter_backend.mla_gluon_decode")
def test_uses_asm_when_gluon_not_preferred(self, mock_gluon, _prefer):
be = self._make_backend()
out = be._forward_mla_decode(
torch.zeros(4, 12, 576, dtype=torch.bfloat16),
_layer(),
mock.Mock(),
k_descale=1.0,
)
mock_gluon.assert_not_called()
be._mla_decode_fwd_with_head_pad.assert_called_once()
self.assertIs(out, be._mla_decode_fwd_with_head_pad.return_value)
@mock.patch(
"sglang.srt.layers.attention.aiter_backend.prefer_mla_gluon_decode",
return_value=True,
)
@mock.patch("sglang.srt.layers.attention.aiter_backend.mla_gluon_decode")
def test_uses_gluon_output_when_preferred(self, mock_gluon, _prefer):
gluon_out = torch.ones(4, 12, 512)
mock_gluon.return_value = gluon_out
be = self._make_backend()
out = be._forward_mla_decode(
torch.zeros(4, 12, 576, dtype=torch.bfloat16),
_layer(),
mock.Mock(),
k_descale=1.0,
)
mock_gluon.assert_called_once()
be._mla_decode_fwd_with_head_pad.assert_not_called()
self.assertIs(out, gluon_out)
@mock.patch(
"sglang.srt.layers.attention.aiter_backend.prefer_mla_gluon_decode",
return_value=True,
)
@mock.patch("sglang.srt.layers.attention.aiter_backend.mla_gluon_decode")
def test_passes_max_q_len_as_qlen(self, mock_gluon, _prefer):
"""Target-verify must reach the kernel as qlen, not be silently dropped:
the ASM fallback cannot serve this topology above qSeqLen 4 at all."""
mock_gluon.return_value = torch.ones(32, 12, 512)
be = self._make_backend(max_q_len=8)
be._forward_mla_decode(
torch.zeros(32, 12, 576, dtype=torch.bfloat16),
_layer(),
mock.Mock(),
k_descale=1.0,
)
self.assertEqual(mock_gluon.call_args.kwargs["qlen"], 8)
be._mla_decode_fwd_with_head_pad.assert_not_called()
if __name__ == "__main__":
unittest.main()