feat(kernels): port standalone Kimi K3 kernels (#32890)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: hnyls2002 <lsyincs@gmail.com>
Co-authored-by: zhangxiaolei <zhangxiaolei.666@bytedance.com>
This commit is contained in:
Xiaoyu Zhang
2026-08-01 13:26:47 +08:00
committed by GitHub
co-authored by Claude Opus 5 hnyls2002 zhangxiaolei
parent a1344fad4e
commit fb207b72b0
84 changed files with 26142 additions and 157 deletions
@@ -0,0 +1,215 @@
"""Kimi-K3 fused KDA decode must match the existing unfused decode chain.
The fused kernel replaces:
causal_conv1d_update -> kda_packed_decode -> sigmoid-gated RMSNorm
This file covers the local head layouts used by Kimi-K3 TP8/TP16/TP32:
H = 12/6/3. The H=6 and H=3 cases are the branches added by the fixed-head
dispatch in ``kda_fused_decode.cuh``.
"""
import pytest
import torch
from sglang.kernels.ops.attention import kda_fused_decode
from sglang.kernels.ops.attention.fla.fused_norm_gate import rms_norm_gated
from sglang.kernels.ops.attention.fla.fused_recurrent import (
fused_recurrent_kda_packed_decode,
)
from sglang.kernels.ops.mamba.causal_conv1d_triton import causal_conv1d_update
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=8, stage="base-b-kernel-unit", runner_config="1-gpu-large")
_HEAD_DIM = 128
_CONV_STATE_W = 3
_SLOTS = 8
_BATCH = 4
def _randn(shape, dtype, generator, scale=1.0):
return (torch.randn(shape, device="cuda", generator=generator) * scale).to(dtype)
def _make_case(heads: int, seed: int):
generator = torch.Generator(device="cuda").manual_seed(seed)
seg = heads * _HEAD_DIM
conv_dim = 3 * seg
# Keep magnitudes moderate so fp32 state updates stay in a stable range.
mixed_qkv = _randn((_BATCH, conv_dim), torch.bfloat16, generator, scale=0.2)
a = _randn((_BATCH, seg), torch.bfloat16, generator, scale=0.2)
b = _randn((_BATCH, heads), torch.bfloat16, generator, scale=0.2)
onorm_g = _randn((_BATCH, seg), torch.bfloat16, generator, scale=0.2)
conv_states = _randn(
(_SLOTS, _CONV_STATE_W, conv_dim), torch.bfloat16, generator, scale=0.2
)
ssm_states = _randn(
(_SLOTS, heads, _HEAD_DIM, _HEAD_DIM), torch.float32, generator, scale=0.02
)
cache_indices = torch.arange(_BATCH, device="cuda", dtype=torch.int32)
conv_weights = _randn((conv_dim, 4), torch.float32, generator, scale=0.1)
conv_bias = _randn((conv_dim,), torch.float32, generator, scale=0.05)
a_log = _randn((heads,), torch.float32, generator, scale=0.1)
dt_bias = _randn((seg,), torch.float32, generator, scale=0.1)
onorm_weight = _randn((_HEAD_DIM,), torch.float32, generator, scale=0.1) + 1.0
return (
mixed_qkv,
a,
b,
onorm_g,
conv_states,
ssm_states,
cache_indices,
conv_weights,
conv_bias,
a_log,
dt_bias,
onorm_weight,
)
def _run_unfused_reference(
mixed_qkv,
a,
b,
onorm_g,
conv_states,
ssm_states,
cache_indices,
conv_weights,
conv_bias,
a_log,
dt_bias,
onorm_weight,
):
heads = ssm_states.shape[-3]
qkv = causal_conv1d_update(
mixed_qkv,
conv_states.transpose(-1, -2),
conv_weights,
conv_bias,
activation="silu",
conv_state_indices=cache_indices,
)
out = torch.empty(
(_BATCH, 1, heads, _HEAD_DIM), dtype=torch.bfloat16, device="cuda"
)
out, _ = fused_recurrent_kda_packed_decode(
qkv,
a,
b,
a_log,
dt_bias,
_HEAD_DIM**-0.5,
ssm_states,
out,
cache_indices,
use_qk_l2norm_in_kernel=True,
)
ref = rms_norm_gated(
out,
onorm_g.view(1, _BATCH, heads, _HEAD_DIM),
onorm_weight,
None,
activation="sigmoid",
eps=1e-6,
)
return ref.transpose(0, 1).contiguous()
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
@pytest.mark.parametrize(
"heads,tp_size",
[
pytest.param(3, 32, id="tp32_h3"),
pytest.param(6, 16, id="tp16_h6"),
pytest.param(12, 8, id="tp8_h12"),
],
)
def test_kda_fused_decode_matches_unfused_chain(heads: int, tp_size: int):
(
mixed_qkv,
a,
b,
onorm_g,
conv_states,
ssm_states,
cache_indices,
conv_weights,
conv_bias,
a_log,
dt_bias,
onorm_weight,
) = _make_case(heads=heads, seed=20260731 + tp_size)
conv_ref = conv_states.clone()
conv_fused = conv_states.clone()
state_ref = ssm_states.clone()
state_fused = ssm_states.clone()
w_q_t, w_k_t, w_v_t = [
weight.t().contiguous()
for weight in conv_weights.split(heads * _HEAD_DIM, dim=0)
]
assert kda_fused_decode.covered(
mixed_qkv,
a,
b,
conv_fused,
state_fused,
cache_indices,
onorm_g,
)
ref = _run_unfused_reference(
mixed_qkv.clone(),
a,
b,
onorm_g,
conv_ref,
state_ref,
cache_indices,
conv_weights,
conv_bias,
a_log,
dt_bias,
onorm_weight,
)
fused = kda_fused_decode.kda_fused_decode(
mixed_qkv.clone(),
a,
b,
conv_fused,
w_q_t,
w_k_t,
w_v_t,
conv_bias,
a_log,
dt_bias,
onorm_g,
onorm_weight,
state_fused,
cache_indices,
scale=_HEAD_DIM**-0.5,
onorm_eps=1e-6,
)
torch.cuda.synchronize()
# JIT log breadcrumb for PR/CI evidence that the fused fixed-head branch ran.
print(f"K3 fused KDA decode test used fused path: TP{tp_size}, H={heads}")
torch.testing.assert_close(fused, ref, rtol=2e-2, atol=2e-2)
torch.testing.assert_close(state_fused, state_ref, rtol=2e-2, atol=2e-2)
torch.testing.assert_close(conv_fused, conv_ref, rtol=0, atol=0)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__]))
@@ -0,0 +1,166 @@
import unittest
import torch
import torch.nn.functional as F
from sglang.kernels.ops.attention.fla.kda import chunk_kda
from sglang.kernels.ops.attention.linear.kda_nvidia_prefill import (
chunk_kda_fwd as nvidia_chunk_kda_fwd,
)
from sglang.kernels.ops.attention.linear.kda_ptx_prefill import (
chunk_kda_fwd as ptx_chunk_kda_fwd,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=180, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
register_cuda_ci(est_time=180, stage="base-c", runner_config="4-gpu-gb300")
def _inputs(seed, seq_len=128):
generator = torch.Generator(device="cuda").manual_seed(seed)
batch_size, num_heads, head_dim = 1, 2, 128
shape = (batch_size, seq_len, num_heads, head_dim)
q = torch.randn(shape, generator=generator, device="cuda", dtype=torch.bfloat16)
k = torch.randn(shape, generator=generator, device="cuda", dtype=torch.bfloat16)
v = (
0.1
* torch.randn(
shape,
generator=generator,
device="cuda",
dtype=torch.float32,
)
).to(torch.bfloat16)
gate = torch.randn(shape, generator=generator, device="cuda", dtype=torch.bfloat16)
beta_logits = torch.randn(
shape[:-1],
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
a_log = torch.randn(
num_heads, generator=generator, device="cuda", dtype=torch.float32
)
dt_bias = torch.randn(
num_heads * head_dim,
generator=generator,
device="cuda",
dtype=torch.float32,
)
state = torch.zeros(
batch_size,
num_heads,
head_dim,
head_dim,
device="cuda",
dtype=torch.float32,
)
return q, k, v, gate, beta_logits, a_log, dt_bias, state
def _reference(q, k, v, gate, beta, a_log, dt_bias, state, fused_qk_norm):
return chunk_kda(
q=q,
k=k,
v=v,
g=gate,
beta=beta,
scale=q.shape[-1] ** -0.5,
initial_state=state,
initial_state_indices=torch.arange(
q.shape[0], device="cuda", dtype=torch.int32
),
use_qk_l2norm_in_kernel=fused_qk_norm,
A_log=a_log,
dt_bias=dt_bias,
lower_bound=-5.0,
)
class TestKdaPrefill(CustomTestCase):
@torch.inference_mode()
def test_nvidia_prefill(self):
if not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] != 10:
self.skipTest("NVIDIA KDA prefill requires datacenter Blackwell")
q, k, v, gate, beta_logits, a_log, dt_bias, state = _inputs(0)
q = F.normalize(q.float(), dim=-1).to(torch.bfloat16)
k = F.normalize(k.float(), dim=-1).to(torch.bfloat16)
beta = torch.sigmoid(beta_logits.float()).to(torch.bfloat16)
actual, actual_state = nvidia_chunk_kda_fwd(
q=q,
k=k,
v=v,
g=gate,
beta=beta,
scale=q.shape[-1] ** -0.5,
initial_state=state.transpose(-1, -2).contiguous(),
output_final_state=True,
safe_gate=True,
lower_bound=-5.0,
use_gate_in_kernel=True,
A_log=a_log,
dt_bias=dt_bias,
)[:2]
expected = _reference(
q, k, v, gate, beta, a_log, dt_bias, state, fused_qk_norm=False
)
torch.testing.assert_close(
actual.float(), expected.float(), rtol=2e-2, atol=3e-2
)
torch.testing.assert_close(
actual_state.transpose(-1, -2),
state,
rtol=2e-2,
atol=3e-2,
)
@torch.inference_mode()
def test_ptx_prefill(self):
if not torch.cuda.is_available() or torch.cuda.get_device_capability() != (
10,
3,
):
self.skipTest("PTX KDA prefill requires GB300")
q, k, v, gate, beta_logits, a_log, dt_bias, state = _inputs(1)
actual, actual_state = ptx_chunk_kda_fwd(
q=q,
k=k,
v=v,
g=gate,
beta=beta_logits,
scale=q.shape[-1] ** -0.5,
initial_state=state.transpose(-1, -2).contiguous(),
output_final_state=True,
safe_gate=True,
lower_bound=-5.0,
use_gate_in_kernel=True,
A_log=a_log,
dt_bias=dt_bias,
use_qk_l2norm_in_kernel=True,
use_beta_sigmoid_in_kernel=True,
)[:2]
expected = _reference(
q,
k,
v,
gate,
torch.sigmoid(beta_logits.float()).to(torch.bfloat16),
a_log,
dt_bias,
state,
fused_qk_norm=True,
)
torch.testing.assert_close(
actual.float(), expected.float(), rtol=2e-2, atol=3e-2
)
torch.testing.assert_close(
actual_state.transpose(-1, -2),
state,
rtol=2e-2,
atol=3e-2,
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,359 @@
from __future__ import annotations
import atexit
import os
import pytest
import torch
import torch.distributed as dist
import sglang.srt.distributed.parallel_state as ps
from sglang.kernels.jit.utils import cache_once
from sglang.kernels.ops.communication.mp import register_comm_cleanup
from sglang.kernels.ops.kimi_k3 import (
all_reduce,
attn_res,
gemm_ag,
gemm_ar,
sp_collective,
)
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kernels.utils import multigpu_pytest_main
register_cuda_ci(est_time=240, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
register_cuda_ci(est_time=480, suite="nightly-8-gpu-b200", nightly=True)
_HIDDEN_SIZE = 7168
_GEMM_AR_K_TOTAL = 12288
_GEMM_AG_WORLD_SIZE = 8
_MB = 1024 * 1024
_SP_TUNING = sp_collective.Tuning(num_blocks=1, block_size=256)
def _device():
return torch.device("cuda", int(os.environ["LOCAL_RANK"]))
def _require_sm100():
if not torch.cuda.is_available() or torch.cuda.get_device_capability() < (10, 0):
pytest.skip("Kimi K3 collectives require SM100+")
@cache_once
def _init_world():
local_rank = int(os.environ["LOCAL_RANK"])
world_size = int(os.environ["WORLD_SIZE"])
torch.cuda.set_device(local_rank)
dist.init_process_group(backend="gloo")
ps._WORLD = coord = ps.init_world_group(
ranks=list(range(world_size)),
local_rank=local_rank,
backend="nccl",
)
atexit.register(dist.destroy_process_group)
cpu_group = coord.cpu_group
assert isinstance(cpu_group, dist.ProcessGroup)
nccl_group = dist.new_group(backend="nccl", device_id=_device())
return cpu_group, nccl_group
@cache_once
def _init_comm():
cpu_group, _ = _init_world()
comm = CustomAllReduceV2(
cpu_group,
_device(),
max_pull_size=4 * _MB,
max_push_size=4 * _MB,
)
if comm.disabled or comm.mc_base_ptr == 0:
raise RuntimeError("Kimi K3 collectives require multicast symmetric memory")
all_reduce.register_comm(comm.obj, pull_sem_mc_ptr=comm.pull_sem_mc_ptr)
sp_collective.register_comm(comm.obj, pull_sem_mc_ptr=comm.pull_sem_mc_ptr)
attn_res.register_comm(comm.obj, pull_sem_mc_ptr=comm.pull_sem_mc_ptr)
register_comm_cleanup(comm)
return comm
@cache_once
def _init_gemm_ar():
cpu_group, _ = _init_world()
world_size = dist.get_world_size()
gemm_ar.init(
world_size=world_size,
rank=dist.get_rank(),
group=cpu_group,
k=_GEMM_AR_K_TOTAL // world_size,
)
def _symmetric_tensor(shape):
from torch._C._distributed_c10d import _SymmetricMemory
cpu_group, _ = _init_world()
tensor = _SymmetricMemory.empty_strided_p2p(
shape,
torch.empty(shape).stride(),
torch.bfloat16,
_device(),
cpu_group.group_name,
)
handle = _SymmetricMemory.rendezvous(tensor)
rank = dist.get_rank()
multicast_ptr = (
int(handle.multicast_ptr) + tensor.data_ptr() - int(handle.buffer_ptrs[rank])
)
if multicast_ptr == 0:
raise RuntimeError("symmetric tensor has no multicast mapping")
return tensor, handle, multicast_ptr
@torch.inference_mode()
def test_all_reduce_push():
_require_sm100()
comm = _init_comm()
rank = dist.get_rank()
generator = torch.Generator().manual_seed(10 + rank)
x = torch.randint(
0,
16,
(_HIDDEN_SIZE,),
generator=generator,
dtype=torch.bfloat16,
).to(_device())
residual = (
torch.arange(_HIDDEN_SIZE, dtype=torch.int32, device=_device())
.remainder_(7)
.to(torch.bfloat16)
)
expected = x.clone()
_, nccl_group = _init_world()
dist.all_reduce(expected, group=nccl_group)
expected += residual
all_reduce.all_reduce_push_res(
comm.world_size,
x,
residual,
ws_mc_base=comm.mc_base_ptr,
)
torch.cuda.synchronize()
torch.testing.assert_close(x, expected, rtol=0, atol=0)
@torch.inference_mode()
def test_sequence_parallel_collectives():
_require_sm100()
comm = _init_comm()
rank, world_size = dist.get_rank(), comm.world_size
local_tokens = 2
generator = torch.Generator(device="cuda").manual_seed(20 + rank)
reduce_input = torch.randn(
world_size * local_tokens,
_HIDDEN_SIZE,
generator=generator,
device=_device(),
dtype=torch.bfloat16,
)
residual = torch.randn(
local_tokens,
_HIDDEN_SIZE,
generator=torch.Generator(device="cuda").manual_seed(21),
device=_device(),
dtype=torch.bfloat16,
)
expected_reduce = reduce_input.float()
_, nccl_group = _init_world()
dist.all_reduce(expected_reduce, group=nccl_group)
lo = rank * local_tokens
expected_reduce = (expected_reduce[lo : lo + local_tokens] + residual.float()).to(
torch.bfloat16
)
reduce_output = torch.empty_like(expected_reduce)
sp_collective.reduce_scatter_res(
world_size,
reduce_input,
reduce_output,
residual,
tuning=_SP_TUNING,
)
gather_input = torch.randn(
local_tokens,
_HIDDEN_SIZE,
generator=generator,
device=_device(),
dtype=torch.bfloat16,
)
expected_gather = torch.empty(
world_size * local_tokens,
_HIDDEN_SIZE,
device=_device(),
dtype=torch.bfloat16,
)
dist.all_gather_into_tensor(
expected_gather,
gather_input,
group=nccl_group,
)
gather_output = torch.empty_like(expected_gather)
sp_collective.all_gather(
world_size,
gather_input,
gather_output,
ws_mc_base=comm.mc_base_ptr,
tuning=_SP_TUNING,
)
torch.cuda.synchronize()
torch.testing.assert_close(reduce_output, expected_reduce, rtol=2e-2, atol=3e-2)
torch.testing.assert_close(gather_output, expected_gather, rtol=0, atol=0)
@torch.inference_mode()
def test_gemm_all_gather():
_require_sm100()
if int(os.environ["WORLD_SIZE"]) != _GEMM_AG_WORLD_SIZE:
pytest.skip("Kimi K3 gemm_ag is compiled for TP8")
comm = _init_comm()
generator = torch.Generator().manual_seed(30)
x = (
(torch.randn(1, gemm_ag.K, generator=generator) * 0.05)
.to(torch.bfloat16)
.to(_device())
)
weight = (
(torch.randn(gemm_ag.N, gemm_ag.K, generator=generator) * 0.05)
.to(torch.bfloat16)
.to(_device())
)
bias = torch.randn(1, gemm_ag.N, generator=generator).to(
device=_device(), dtype=torch.bfloat16
)
output = torch.empty(1, gemm_ag.N, device=_device(), dtype=torch.bfloat16)
expected = (x.float() @ weight.float().t() + bias.float()).to(torch.bfloat16)
gemm_ag.gemm_ag_up_proj(
comm.world_size,
x,
weight,
bias,
None,
output,
ws_mc_base=comm.mc_base_ptr,
)
torch.cuda.synchronize()
torch.testing.assert_close(output, expected, rtol=3e-2, atol=3e-2)
@torch.inference_mode()
def test_gemm_all_reduce():
_require_sm100()
_init_gemm_ar()
rank, world_size = dist.get_rank(), dist.get_world_size()
local_k = _GEMM_AR_K_TOTAL // world_size
generator = torch.Generator().manual_seed(40 + rank)
x = torch.randn(1, local_k, generator=generator).to(
device=_device(), dtype=torch.bfloat16
)
weight = torch.randn(gemm_ar.N, local_k, generator=generator).to(
device=_device(), dtype=torch.bfloat16
)
expected = (x.float() @ weight.float().t()).to(torch.bfloat16).float()
_, nccl_group = _init_world()
dist.all_reduce(expected, group=nccl_group)
output = gemm_ar.o_proj_gemm_ar(x, weight)
torch.cuda.synchronize()
bad = ((output.float() - expected).abs() > 0.05 + 0.02 * expected.abs()).sum()
assert bad.item() <= output.numel() / 1000
@torch.inference_mode()
def test_attention_residual_direct_all_gather():
_require_sm100()
comm = _init_comm()
rank, local_tokens, num_bank_rows = dist.get_rank(), 2, 3
generator = torch.Generator(device="cuda").manual_seed(50 + rank)
prefix = torch.randn(
local_tokens,
_HIDDEN_SIZE,
generator=generator,
device=_device(),
dtype=torch.bfloat16,
)
bank = torch.randn(
local_tokens,
num_bank_rows + 1,
_HIDDEN_SIZE,
generator=generator,
device=_device(),
dtype=torch.bfloat16,
)
combine_weight = torch.linspace(
-0.01, 0.01, _HIDDEN_SIZE, device=_device(), dtype=torch.bfloat16
)
output_weight = torch.linspace(
1.25, 0.75, _HIDDEN_SIZE, device=_device(), dtype=torch.bfloat16
)
local_reference = torch.empty_like(prefix)
attn_res.attn_res_fused_tma(
prefix,
bank.clone(),
combine_weight,
output_weight,
local_reference,
num_bank_rows,
1e-6,
)
full_reference = torch.empty(
comm.world_size * local_tokens,
_HIDDEN_SIZE,
device=_device(),
dtype=torch.bfloat16,
)
_, nccl_group = _init_world()
dist.all_gather_into_tensor(
full_reference,
local_reference,
group=nccl_group,
)
output, handle, multicast_ptr = _symmetric_tensor(tuple(full_reference.shape))
attn_res.attn_res_fused_direct_ag(
comm.world_size,
prefix,
bank,
combine_weight,
output_weight,
output,
num_bank_rows,
1e-6,
output_mc_ptr=multicast_ptr,
max_blocks=4,
)
torch.cuda.synchronize()
torch.testing.assert_close(output, full_reference, rtol=2e-2, atol=3e-2)
assert handle is not None
def _precompile(num_gpus):
for world_size in num_gpus:
all_reduce._jit_module(world_size)
sp_collective._jit_module(world_size)
gemm_ar._jit_module(_GEMM_AR_K_TOTAL // world_size, world_size)
if _GEMM_AG_WORLD_SIZE in num_gpus:
gemm_ag._jit_module()
attn_res._jit_fused_tma_module(4, 1, 200)
if __name__ == "__main__":
multigpu_pytest_main(
__name__,
__file__,
num_gpus=(4, 8),
pre_launch_fn=_precompile,
)
@@ -0,0 +1,453 @@
import unittest
import torch
from sglang.kernels.ops.attention.fla.kda_replayssm_spec_decode import (
commit_kda_replayssm_spec,
)
from sglang.kernels.ops.kimi_k3 import (
situ_and_mul,
situ_and_mul_masked_post_quant,
)
from sglang.kernels.ops.kimi_k3.attn_res import attn_res_fused_tma
from sglang.kernels.ops.kimi_k3.kda_decode_mtp import (
fused_kda_decode_mtp_dspark,
)
from sglang.kernels.ops.kimi_k3.mla_output_gate import (
covered,
kimi_k3_mla_output_gate,
)
from sglang.kernels.ops.moe.moe_front import (
NUM_EXPERTS,
TOPK,
fused_front,
)
from sglang.kernels.ops.moe.moe_fused_gate import moe_fused_gate
from sglang.srt.utils import get_device_sm
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
_HIDDEN_SIZE = 7168
_GROUP_SIZE = 128
_BETA = 4.0
_LINEAR_BETA = 25.0
def _situ_reference(gate_up):
gate, up = gate_up.chunk(2, dim=-1)
gate = gate.float()
up = up.float()
return (
_BETA
* torch.tanh(gate / _BETA)
* torch.sigmoid(gate)
* _LINEAR_BETA
* torch.tanh(up / _LINEAR_BETA)
)
def _unpack_ue8m0_scales(packed, num_groups):
num_experts, groups_per_word, num_tokens = packed.shape
exponents = packed.contiguous().view(torch.uint8)
exponents = exponents.view(num_experts, groups_per_word, num_tokens, 4)
exponents = exponents.permute(0, 2, 1, 3).reshape(
num_experts, num_tokens, num_groups
)
return torch.exp2(exponents.float() - 127.0)
class TestKimiK3ComputeKernels(CustomTestCase):
@classmethod
def setUpClass(cls):
if not torch.cuda.is_available():
raise unittest.SkipTest("CUDA is not available")
if get_device_sm() < 100:
raise unittest.SkipTest("Kimi K3 compute kernels require SM100a+")
def test_attn_residual_and_prefix_write(self):
generator = torch.Generator(device="cuda").manual_seed(0)
def randn(*shape):
return torch.randn(*shape, generator=generator, device="cuda")
num_tokens, num_bank_rows, num_valid_bank_rows = 5, 8, 5
prefix = randn(num_tokens, _HIDDEN_SIZE).to(torch.bfloat16)
bank = randn(num_tokens, num_bank_rows, _HIDDEN_SIZE).to(torch.bfloat16)
combine_weight = (randn(_HIDDEN_SIZE) * _HIDDEN_SIZE**-0.5).to(torch.bfloat16)
output_weight = (1 + 0.1 * randn(_HIDDEN_SIZE)).to(torch.bfloat16)
output = torch.empty_like(prefix)
rows = torch.cat(
[
bank[:, :num_valid_bank_rows].float(),
prefix.unsqueeze(1).float(),
],
dim=1,
)
rms = torch.rsqrt(rows.square().mean(-1) + 1e-6)
scores = (rows * combine_weight.float()).sum(-1) * rms
mixed = (torch.softmax(scores, dim=-1).unsqueeze(-1) * rows).sum(1)
expected = (
mixed
* torch.rsqrt(mixed.square().mean(-1, keepdim=True) + 1e-6)
* output_weight.float()
)
attn_res_fused_tma(
prefix,
bank,
combine_weight,
output_weight,
output,
num_valid_bank_rows,
1e-6,
write_prefix=True,
)
torch.testing.assert_close(output.float(), expected, rtol=2e-2, atol=4e-2)
self.assertTrue(torch.equal(bank[:, num_valid_bank_rows], prefix))
def test_mla_output_gate(self):
generator = torch.Generator(device="cuda").manual_seed(1)
shape = (5, 12, 128)
x = torch.randn(shape, generator=generator, device="cuda", dtype=torch.bfloat16)
gate = torch.randn(
shape, generator=generator, device="cuda", dtype=torch.bfloat16
)
self.assertTrue(covered(x, gate))
expected = x * torch.sigmoid(gate).to(torch.bfloat16)
self.assertTrue(torch.equal(kimi_k3_mla_output_gate(x, gate), expected))
def test_situ_and_mul(self):
generator = torch.Generator(device="cuda").manual_seed(2)
hidden_size = 1024
storage = torch.randn(
(7, 2 * hidden_size + 16),
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
gate_up = storage[:, : 2 * hidden_size]
output = torch.empty(
(gate_up.shape[0], hidden_size),
device="cuda",
dtype=torch.bfloat16,
)
returned = situ_and_mul(gate_up, output, beta=_BETA, linear_beta=_LINEAR_BETA)
self.assertIs(returned, output)
torch.testing.assert_close(
returned.float(),
_situ_reference(gate_up).to(torch.bfloat16).float(),
rtol=2e-2,
atol=4e-2,
)
def test_situ_mul_quant(self):
torch.cuda.manual_seed_all(3)
num_experts, num_tokens, hidden_size, topk = 8, 32, 1024, 16
gate_up = (
torch.randn(
num_experts,
num_tokens,
2 * hidden_size,
device="cuda",
dtype=torch.float32,
)
* 2.0
).to(torch.bfloat16)
masked_m = torch.randint(
0,
num_tokens + 1,
(num_experts,),
device="cuda",
dtype=torch.int32,
)
masked_m[0] = 0
masked_m[-1] = num_tokens
output = torch.full(
(num_experts, num_tokens, hidden_size),
0x7F,
device="cuda",
dtype=torch.uint8,
).view(torch.float8_e4m3fn)
num_groups = hidden_size // _GROUP_SIZE
output_scale = torch.zeros(
(num_experts, num_groups // 4, num_tokens),
device="cuda",
dtype=torch.int32,
)
situ_and_mul_masked_post_quant(
input=gate_up,
output=output,
output_scale=output_scale,
quant_group_size=_GROUP_SIZE,
masked_m=masked_m,
beta=_BETA,
linear_beta=_LINEAR_BETA,
scale_ue8m0=True,
topk=topk,
transposed=True,
)
scales = _unpack_ue8m0_scales(output_scale, num_groups)
expanded_scales = scales.repeat_interleave(_GROUP_SIZE, dim=-1)
dequantized = output.float() * expanded_scales
expected = _situ_reference(gate_up)
error_bound = expanded_scales * 17.0
raw_output = output.view(torch.uint8)
for expert in range(num_experts):
valid_tokens = int(masked_m[expert].item())
self.assertTrue(
bool(
(
(
dequantized[expert, :valid_tokens]
- expected[expert, :valid_tokens]
).abs()
<= error_bound[expert, :valid_tokens]
).all()
)
)
self.assertTrue(bool((raw_output[expert, valid_tokens:] == 0x7F).all()))
def test_moe_front(self):
torch.manual_seed(4)
num_tokens, latent_dim = 1, 128
hidden = (
torch.randn(
num_tokens,
_HIDDEN_SIZE,
device="cuda",
dtype=torch.bfloat16,
)
/ 32
)
weight = (
torch.randn(
NUM_EXPERTS + latent_dim,
_HIDDEN_SIZE,
device="cuda",
dtype=torch.bfloat16,
)
/ 32
)
bias = torch.randn(NUM_EXPERTS, device="cuda")
weights, ids, routed = fused_front(
hidden,
weight,
bias,
latent_dim,
renormalize=True,
routed_scaling_factor=2.5,
apply_routed_scaling_factor_on_output=True,
)
merged = torch.mm(hidden, weight.t(), out_dtype=torch.float32)
ref_weights, ref_ids = moe_fused_gate(
merged[:, :NUM_EXPERTS],
bias,
topk=TOPK,
scoring_func="sigmoid",
renormalize=True,
routed_scaling_factor=2.5,
apply_routed_scaling_factor_on_output=True,
)
order = ids.argsort(dim=-1)
ref_order = ref_ids.argsort(dim=-1)
self.assertTrue(
torch.equal(
ids.gather(1, order),
ref_ids.to(torch.int32).gather(1, ref_order),
)
)
torch.testing.assert_close(
weights.gather(1, order),
ref_weights.gather(1, ref_order),
rtol=1e-6,
atol=0,
)
self.assertTrue(
torch.equal(
routed,
merged[:, NUM_EXPERTS:].to(torch.bfloat16),
)
)
def test_mtp_replayssm_ring(self):
num_requests, num_heads, num_spec, key_dim = 2, 2, 2, 128
num_tokens = num_requests * (1 + num_spec)
num_slots, ring_size, conv_width = num_requests + 2, 16, 4
def run(cache_ring):
torch.manual_seed(5)
x_q = torch.randn(
1,
num_tokens,
num_heads,
key_dim,
device="cuda",
dtype=torch.bfloat16,
)
x_k = torch.randn_like(x_q)
x_v = torch.randn_like(x_q)
gate = torch.randn_like(x_q)
beta = torch.randn(
1,
num_tokens,
num_heads,
device="cuda",
dtype=torch.bfloat16,
)
conv_weight = [
torch.randn(
num_heads * key_dim,
conv_width,
device="cuda",
)
* 0.1
for _ in range(3)
]
conv_state = [
torch.randn(
num_slots,
num_heads * key_dim,
conv_width - 1,
device="cuda",
dtype=torch.bfloat16,
)
for _ in range(3)
]
slots = torch.arange(1, num_requests + 1, device="cuda", dtype=torch.int32)
scratch = torch.arange(num_requests, device="cuda", dtype=torch.int32)
state = torch.randn(
num_slots,
num_heads,
key_dim,
key_dim,
device="cuda",
)
intermediate_conv = torch.zeros(
num_requests,
1 + num_spec,
num_heads * key_dim,
conv_width - 1,
device="cuda",
dtype=torch.bfloat16,
)
kwargs = dict(
x_q=x_q,
x_k=x_k,
x_v=x_v,
w_q=conv_weight[0],
w_k=conv_weight[1],
w_v=conv_weight[2],
cs_q=conv_state[0],
cs_k=conv_state[1],
cs_v=conv_state[2],
g=gate,
beta=beta,
A_log=torch.randn(num_heads, device="cuda"),
dt_bias=torch.randn(num_heads * key_dim, device="cuda"),
recurrent_state=state,
intermediate_state_indices=scratch,
intermediate_conv_q=intermediate_conv.clone(),
intermediate_conv_k=intermediate_conv.clone(),
intermediate_conv_v=intermediate_conv.clone(),
ssm_state_indices=slots,
cu_seqlens=torch.arange(
0,
num_tokens + 1,
1 + num_spec,
device="cuda",
dtype=torch.int32,
),
lower_bound=-5.0,
)
if not cache_ring:
intermediate = torch.zeros(
num_requests,
1 + num_spec,
num_heads,
key_dim,
key_dim,
device="cuda",
)
output = fused_kda_decode_mtp_dspark(
intermediate_ssm=intermediate,
**kwargs,
)
return output, intermediate, slots, scratch
raw_v = torch.zeros(
num_slots,
num_heads,
ring_size,
key_dim,
device="cuda",
dtype=torch.bfloat16,
)
raw_k = torch.zeros_like(raw_v)
ring_gate = torch.zeros(
num_slots,
num_heads,
ring_size,
key_dim,
device="cuda",
)
ring_beta = torch.zeros(
num_slots,
num_heads,
ring_size,
device="cuda",
)
output = fused_kda_decode_mtp_dspark(
intermediate_ssm=None,
replayssm_rawv=raw_v,
replayssm_rawk=raw_k,
replayssm_g=ring_gate,
replayssm_beta=ring_beta,
**kwargs,
)
return (
output,
state,
slots,
(raw_v, raw_k, ring_gate, ring_beta),
)
baseline, intermediate, slots, scratch = run(cache_ring=False)
ring_output, checkpoint, ring_slots, rings = run(cache_ring=True)
self.assertTrue(torch.equal(ring_output, baseline))
commit_kda_replayssm_spec(
checkpoint,
*rings,
ring_slots,
torch.full(
(num_requests,),
1 + num_spec,
device="cuda",
dtype=torch.int32,
),
max_cache_len=ring_size,
num_k_heads=num_heads,
use_qk_l2norm_in_kernel=True,
null_block_id=-1,
)
for request in range(num_requests):
expected = intermediate[scratch[request], num_spec]
actual = checkpoint[slots[request]]
relative_error = (
actual - expected
).abs().max() / expected.abs().max().clamp_min(1e-6)
self.assertLess(relative_error.item(), 2e-2)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,456 @@
"""Representative parity coverage for the lightweight Kimi-K3 prerequisites."""
import unittest
import torch
from sglang.kernels.ops.attention.concat_mla import concat_mla_absorb_q
from sglang.kernels.ops.attention.fla.fused_sigmoid_gating_recurrent import (
fused_sigmoid_gating_delta_rule_update,
)
from sglang.kernels.ops.attention.fla.kda_replayssm_spec_decode import (
commit_kda_replayssm_spec,
)
from sglang.kernels.ops.attention.set_mla_kv_concat_q import (
can_use_set_mla_kv_concat_q,
can_use_set_mla_kv_concat_q_fp8,
set_mla_kv_concat_q,
set_mla_kv_concat_q_fp8,
)
from sglang.kernels.ops.attention.utils import concat_mla_absorb_q_general
from sglang.kernels.ops.attention.vision_rope import (
apply_fused_qk_complex_rope,
)
from sglang.kernels.ops.elementwise import add3
from sglang.kernels.ops.gemm.tiny_gemm import (
tiny_k_gemm_bf16,
tiny_n_gemm_bf16,
)
from sglang.kernels.ops.kvcache.set_mla_kv_buffer import set_mla_kv_buffer
from sglang.kernels.ops.mm.process.image import (
_normalize_and_patchify_torch,
normalize_and_patchify,
)
from sglang.kernels.ops.moe import moe_route_quant_fused
from sglang.kernels.ops.moe.moe_route_radix import route_radix
from sglang.kernels.ops.moe.moe_topk_sum import moe_topk_sum
from sglang.kernels.ops.moe.pack_topk_ids import PackTopkIds
from sglang.kernels.ops.quantization.per_token_group_quant import (
per_token_group_quant,
)
from sglang.kernels.ops.sampling.top_p_renorm_triton import (
top_p_renorm_probs_triton,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="1-gpu-large")
NUM_EXPERTS = 896
TOPK = 16
NOPE_DIM = 512
ROPE_DIM = 64
MLA_DIM = NOPE_DIM + ROPE_DIM
MLA_PAGES = 256
def _route_oracle(
scores, bias, topk, renormalize, routed_scaling_factor, apply_scale, sorted
):
"""Pure-torch fp32 reference for route_radix.
Deliberately independent of moe_fused_gate: that entry dispatches back to
route_radix whenever scoring is sigmoid with no shared experts, no expert
groups and no softcapping (moe_fused_gate.py, the covered() fast path), which
is exactly the configuration under test.
Contract, from route_radix.cuh: bias participates in RANKING only and the
emitted weight stays bias-free; NaN is floored so it can never win; ties go to
the lower expert id; renormalize divides by the winners' sum (guarded to 1 when
that sum is non-positive) and only then is routed scaling applied; sorted=True
emits (biased desc, id asc) while sorted=False emits ascending expert id.
"""
s = torch.sigmoid(scores.float())
biased = s + bias.float()
biased = torch.where(torch.isnan(biased), torch.full_like(biased, -1e30), biased)
# stable + descending: equal biased values keep ascending-id order
ranked = torch.argsort(biased, dim=-1, descending=True, stable=True)[:, :topk]
w = s.gather(1, ranked)
total = w.sum(-1, keepdim=True)
norm = torch.where(total > 0, total, torch.ones_like(total))
if renormalize:
w = w / norm
if apply_scale:
w = w * routed_scaling_factor
if sorted:
return w, ranked.to(torch.int32)
by_id = ranked.argsort(dim=-1)
return w.gather(1, by_id), ranked.gather(1, by_id).to(torch.int32)
def _make_mla_inputs(batch_size, num_heads, seed):
generator = torch.Generator(device="cuda").manual_seed(seed)
def randn(*shape):
return (
torch.randn(*shape, generator=generator, device="cuda", dtype=torch.float32)
.mul(0.1)
.to(torch.bfloat16)
)
pool = randn(MLA_PAGES, MLA_DIM)
latent = randn(batch_size, MLA_DIM)
query = randn(batch_size, num_heads, MLA_DIM)
loc = torch.randperm(MLA_PAGES, generator=generator, device="cuda")[:batch_size].to(
torch.int64
)
return (
pool,
loc,
latent[:, :NOPE_DIM],
latent[:, NOPE_DIM:],
query[..., :NOPE_DIM],
query[..., NOPE_DIM:],
)
class TestKimiK3PrerequisiteOps(CustomTestCase):
def test_mla_scatter_concat_bf16_and_fp8(self):
batch_size, num_heads = 64, 8
pool, loc, k_nope, k_rope, q_nope, q_rope = _make_mla_inputs(
batch_size, num_heads, seed=0
)
if not can_use_set_mla_kv_concat_q(NOPE_DIM * 2, ROPE_DIM * 2):
self.skipTest("fused MLA scatter+concat requires SM90+")
pool_ref = pool.clone()
query = set_mla_kv_concat_q(pool, loc, k_nope, k_rope, q_nope, q_rope)
set_mla_kv_buffer(pool_ref, loc, k_nope, k_rope)
query_ref = concat_mla_absorb_q(q_nope, q_rope)
self.assertTrue(torch.equal(pool, pool_ref))
self.assertTrue(torch.equal(query, query_ref))
if not can_use_set_mla_kv_concat_q_fp8():
self.skipTest("fused FP8 MLA scatter+concat requires SM90+")
fp8_pool = torch.zeros(
MLA_PAGES, MLA_DIM, device="cuda", dtype=torch.float8_e4m3fn
)
fp8_ref = fp8_pool.clone()
fp8_query = set_mla_kv_concat_q_fp8(
fp8_pool, loc, k_nope, k_rope, q_nope, q_rope
)
row = torch.cat([k_nope, k_rope], dim=-1).to(torch.float8_e4m3fn)
fp8_ref[loc] = row
fp8_query_ref = concat_mla_absorb_q_general(q_nope, q_rope).to(
torch.float8_e4m3fn
)
self.assertTrue(
torch.equal(fp8_pool.view(torch.uint8), fp8_ref.view(torch.uint8))
)
self.assertTrue(
torch.equal(
fp8_query.view(torch.uint8),
fp8_query_ref.view(torch.uint8),
)
)
def test_replayssm_ring_fold(self):
batch_size, num_steps = 8, 4
num_value_heads, num_key_heads = 8, 2
key_dim = value_dim = 128
ring_size = 16
torch.manual_seed(6)
q = torch.randn(
batch_size,
num_steps,
num_key_heads,
key_dim,
device="cuda",
)
k = torch.randn_like(q)
v = torch.randn(
batch_size,
num_steps,
num_value_heads,
value_dim,
device="cuda",
)
a = torch.randn(
batch_size,
num_steps,
num_value_heads,
key_dim,
device="cuda",
)
b = torch.randn(batch_size, num_steps, num_value_heads, device="cuda")
a_log = torch.randn(num_value_heads, device="cuda")
dt_bias = torch.randn(num_value_heads, key_dim, device="cuda")
slots = torch.arange(1, batch_size + 1, device="cuda", dtype=torch.int32)
slots[-1] = -1
num_slots = batch_size + 1
state = torch.randn(
num_slots,
num_value_heads,
value_dim,
key_dim,
device="cuda",
)
intermediate = torch.zeros(
num_slots,
num_steps,
num_value_heads,
value_dim,
key_dim,
device="cuda",
)
raw_v = torch.zeros(
num_slots,
num_value_heads,
ring_size,
value_dim,
device="cuda",
)
raw_k = torch.zeros(
num_slots,
num_key_heads,
ring_size,
key_dim,
device="cuda",
)
gate = torch.zeros_like(raw_v)
beta = torch.zeros(
num_slots,
num_value_heads,
ring_size,
device="cuda",
)
fused_sigmoid_gating_delta_rule_update(
A_log=a_log,
a=a,
dt_bias=dt_bias,
softplus_beta=1.0,
softplus_threshold=20.0,
q=q,
k=k,
v=v,
b=b,
initial_state_source=state,
initial_state_indices=slots,
scale=key_dim**-0.5,
use_qk_l2norm_in_kernel=True,
is_kda=True,
lower_bound=-5.0,
disable_state_update=True,
intermediate_states_buffer=intermediate,
intermediate_state_indices=slots,
cache_steps=num_steps,
cache_ring=True,
replayssm_rawv=raw_v,
replayssm_rawk=raw_k,
replayssm_g=gate,
replayssm_beta=beta,
)
checkpoint = state.clone()
commit_kda_replayssm_spec(
checkpoint,
raw_v,
raw_k,
gate,
beta,
slots,
torch.full((batch_size,), num_steps, device="cuda", dtype=torch.int32),
max_cache_len=ring_size,
num_k_heads=num_key_heads,
use_qk_l2norm_in_kernel=True,
null_block_id=-1,
)
for slot in slots[:-1].tolist():
expected = intermediate[slot, num_steps - 1]
actual = checkpoint[slot]
relative_error = (
actual - expected
).abs().max() / expected.abs().max().clamp_min(1e-6)
self.assertLess(relative_error.item(), 1e-3)
def test_add3_bit_exact(self):
torch.manual_seed(0)
tensors = [
torch.randn(9, 112, device="cuda", dtype=torch.bfloat16) for _ in range(3)
]
actual = add3.add3(*tensors, prefetch_bc=True)
expected = (tensors[0] + tensors[1]) + tensors[2]
self.assertTrue(torch.equal(actual, expected))
def test_moe_auxiliary_kernels(self):
x = torch.randn(2, TOPK, 7168, device="cuda", dtype=torch.bfloat16)
out = torch.empty(2, 7168, device="cuda", dtype=torch.bfloat16)
self.assertIs(moe_topk_sum(x, out), out)
self.assertTrue(torch.equal(out, x.float().sum(1).to(torch.bfloat16)))
def test_moe_route_and_quant(self):
torch.manual_seed(1)
scores = torch.randn(8, NUM_EXPERTS, device="cuda", dtype=torch.bfloat16)
bias = torch.randn(NUM_EXPERTS, device="cuda", dtype=torch.float32)
args = (scores, bias, TOPK, True, 2.5, True)
weights, ids = route_radix(*args, sorted=True)
# Oracle, NOT moe_fused_gate: for this exact configuration (sigmoid, no
# shared experts, no expert groups, no softcapping) moe_fused_gate
# dispatches straight back to route_radix, so using it as the reference
# compares the kernel with itself and cannot see a selection, tie-break,
# NaN, renormalize or scaling error.
ref_weights, ref_ids = _route_oracle(*args, sorted=True)
self.assertTrue(torch.equal(ids, ref_ids))
# rtol is not 1e-6: the kernel computes sigmoid with __fdividef/__expf,
# whose last bits differ from torch's. The old self-comparison could
# afford atol=0; a real oracle cannot.
torch.testing.assert_close(weights, ref_weights, rtol=1e-5, atol=1e-6)
if not moe_route_quant_fused.available():
self.skipTest("fused route+quant kernel unavailable")
hidden = torch.randn(8, 3584, device="cuda", dtype=torch.bfloat16)
ref_weights, ref_ids = route_radix(*args, sorted=False)
ref_packed = PackTopkIds.execute(ref_ids, ref_weights)
ref_q, ref_scale = per_token_group_quant(
hidden, group_size=32, scale_ue8m0=True
)
actual = moe_route_quant_fused.route_quant_fused(
scores,
bias,
hidden,
TOPK,
renormalize=True,
routed_scaling_factor=2.5,
apply_scale=True,
)
weights, ids, packed, quantized, scale = actual
self.assertTrue(torch.equal(ids, ref_ids))
self.assertTrue(
torch.equal(weights.view(torch.int32), ref_weights.view(torch.int32))
)
self.assertTrue(torch.equal(packed, ref_packed))
self.assertTrue(
torch.equal(quantized.view(torch.uint8), ref_q.view(torch.uint8))
)
torch.testing.assert_close(scale, ref_scale, rtol=0, atol=0)
def test_route_radix_ties_and_nan(self):
"""The cases the self-comparison could not see.
Exact ties: many experts share one biased value, so the winner set is only
determined by the lowest-id rule. NaN: floored, so a NaN expert must never
be selected while enough finite ones exist. Both run with renormalize and
scaling on and off, since those are applied in a fixed order.
"""
bias = torch.zeros(NUM_EXPERTS, device="cuda", dtype=torch.float32)
tied = torch.full((4, NUM_EXPERTS), 0.25, device="cuda", dtype=torch.bfloat16)
# a handful of strict winners above the tied plateau, the rest exactly equal
tied[:, 300] = 2.0
tied[:, 7] = 2.0
tied[:, 800] = 1.5
nan_scores = torch.randn(4, NUM_EXPERTS, device="cuda", dtype=torch.bfloat16)
nan_scores[:, 100] = float("nan")
nan_scores[:, 500] = float("nan")
# make the NaN experts the ones that would otherwise win outright
nan_scores[:, 101] = 5.0
for name, scores in (("ties", tied), ("nan", nan_scores)):
for renormalize in (False, True):
for apply_scale in (False, True):
for sorted_ in (False, True):
args = (scores, bias, TOPK, renormalize, 2.5, apply_scale)
ids = route_radix(*args, sorted=sorted_)[1]
ref_ids = _route_oracle(*args, sorted=sorted_)[1]
tag = (
f"{name} renorm={renormalize} "
f"scale={apply_scale} sorted={sorted_}"
)
self.assertTrue(torch.equal(ids, ref_ids), msg=tag)
if name == "nan":
self.assertFalse(
bool(((ids == 100) | (ids == 500)).any()),
msg=f"{tag}: a NaN expert was selected",
)
def test_tiny_gemm_variants(self):
torch.manual_seed(2)
x = torch.randn(2, 7168, device="cuda", dtype=torch.bfloat16) / 8
weight = torch.randn(144, 7168, device="cuda", dtype=torch.bfloat16) / 8
actual = tiny_n_gemm_bf16(x, weight, out_dtype=torch.float32)
torch.testing.assert_close(
actual.double(), x.double() @ weight.double().t(), rtol=1e-3, atol=1e-3
)
x = torch.randn(7, 128, device="cuda", dtype=torch.bfloat16) / 4
weight = torch.randn(1536, 128, device="cuda", dtype=torch.bfloat16) / 4
actual = tiny_k_gemm_bf16(x, weight)
torch.testing.assert_close(
actual.double(), x.double() @ weight.double().t(), rtol=2e-2, atol=2e-2
)
def test_top_p_renorm(self):
torch.manual_seed(3)
probs = torch.randn(3, 1024, device="cuda").softmax(-1)
top_p = torch.tensor([0.5, 0.8, 0.95], device="cuda")
sorted_probs = probs.sort(-1).values
cutoff = torch.searchsorted(
sorted_probs.cumsum(-1), (1 - top_p).unsqueeze(1)
).squeeze(1)
cutoff.clamp_(max=probs.shape[1] - 1)
pivot = sorted_probs.gather(1, cutoff[:, None])
expected = torch.where(probs >= pivot, probs, 0)
expected /= expected.sum(-1, keepdim=True)
torch.testing.assert_close(
top_p_renorm_probs_triton(probs, top_p),
expected,
rtol=2e-6,
atol=1e-8,
)
def test_vision_rope(self):
torch.manual_seed(4)
qkv = torch.randn(480, 3, 12, 128, device="cuda", dtype=torch.bfloat16)
q, k, _ = qkv.unbind(1)
angles = torch.randn(480, 64, device="cuda")
freqs = torch.polar(torch.ones_like(angles), angles)
freqs_expanded = freqs.unsqueeze(-2)
def reference(x):
value = torch.view_as_complex(x.float().view(*x.shape[:-1], -1, 2))
return torch.view_as_real(value * freqs_expanded).flatten(-2).type_as(x)
actual_q, actual_k = apply_fused_qk_complex_rope(q, k, freqs)
atol = 2 * torch.finfo(torch.bfloat16).eps
torch.testing.assert_close(actual_q, reference(q), rtol=0, atol=atol)
torch.testing.assert_close(actual_k, reference(k), rtol=0, atol=atol)
def test_normalize_and_patchify(self):
torch.manual_seed(5)
image = torch.randn(2, 3, 17, 19, device="cuda")
scale = torch.randn(1, 3, 1, 1, device="cuda")
bias = torch.randn(1, 3, 1, 1, device="cuda")
args = (image, scale, bias, 4, 20, 20)
actual = normalize_and_patchify(
args[0],
args[1],
args[2],
patch_size=args[3],
padded_height=args[4],
padded_width=args[5],
)
expected = _normalize_and_patchify_torch(
args[0],
args[1],
args[2],
patch_size=args[3],
padded_height=args[4],
padded_width=args[5],
)
torch.testing.assert_close(actual, expected, rtol=1e-2, atol=1e-2)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,230 @@
"""NV KDA fused-decode kernel gate + slot addressing vs envelope-strided SSM
pools (CPU).
Derived property under test: the fully-fused KDA decode kernel
(``kda_fused_decode``) addresses the ssm/temporal state pool by the slot pitch
the pool actually reports (``ssm_states.stride(0)``), NOT the dense ``HV*V*K``
pitch. Under ``--enable-unified-memory`` / ``--enable-page-major-kv-layout`` the
per-layer temporal view is envelope-strided: one slot pitches across ALL layers
(56,171,520 B on K3), so a hardcoded ``slot*HV*V*K`` offset mis-addresses every
slot > 0 (the exact chunk_delta_h hardcoded-pitch bug pattern, GSM8K 0.17).
Two things are pinned here (both CPU-checkable without the CUDA kernel):
1. ``covered()`` ACCEPTS the envelope-strided view. Pre-fix the gate did
``ssm_states.view(-1, HV, V, K).is_contiguous()``, which is False on a
non-dense slot pitch, so decode silently dropped to the slower unfused
chain. Reverting to that ``.view(...)`` gate turns test (1) red. The gate
still REJECTS a view whose inner ``[HV, V, K]`` is non-contiguous (the
one contract the float4 state loads rely on).
2. The kernel's reconstructed slot-addressing formula
``base + slot*stride(0) + i_hv*V*K + v*K + k`` resolves to the exact same
storage element as torch's native ``ssm_states[slot, i_hv, v, k]`` on the
strided pool, while the pre-fix dense-pitch formula
``base + slot*(HV*V*K) + ...`` resolves ELSEWHERE for every slot > 0.
Hardcoding the dense pitch back into the ``.cuh`` turns test (2) red.
Runs on CPU: only the Python gate and the (dtype/stride-only) addressing
arithmetic execute — no CUDA kernel is launched.
python -m pytest test/registered/unit/mem_cache/test_kda_fused_decode_strided_state.py -v
"""
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=6, suite="base-a-test-cpu")
import unittest
import torch
from sglang.kernels.ops.attention.kda_fused_decode import (
_CONV_STATE_W,
covered,
)
from sglang.srt.mem_cache.layout.page_major import (
build_page_major_mamba_views,
mamba_entry_bytes,
)
_DEV = "cpu"
# The kernel is compiled for the K3 KDA decode regime; covered() enforces these
# supported local head counts. Multi-layer + several slots so the envelope slot
# pitch differs from the dense H*V*K pitch.
_KDA_HEADS = (3, 6, 12)
_V = 128
_K = 128
_LAYERS = 3
_LAYER_UNDER_TEST = 1
_SLOTS = 6
_CONV_SHAPES = ((3, 8),) # tiny bf16 conv region interleaves the temporal region
_CONV_DTYPE = torch.bfloat16
_TEMPORAL_DTYPE = torch.float32
def _seg(heads: int) -> int:
return heads * _V
def _conv_dim(heads: int) -> int:
return 3 * _seg(heads)
def _make_strided_temporal_view(heads: int):
"""Envelope-strided temporal (SSM) view as UnifiedMambaPool / the
page-major MambaPool serve it to the KDA backend: shape
``(num_layers, max_slots, H, V, K)`` with the slot pitch spanning ALL
layers' state, not H*V*K."""
entry = mamba_entry_bytes(
layer_num=_LAYERS,
conv_state_shapes=_CONV_SHAPES,
conv_dtype=_CONV_DTYPE,
temporal_state_shape=(heads, _V, _K),
temporal_dtype=_TEMPORAL_DTYPE,
)
raw = torch.zeros(_SLOTS * entry, dtype=torch.uint8, device=_DEV)
_conv_views, temporal = build_page_major_mamba_views(
raw,
layer_num=_LAYERS,
conv_state_shapes=_CONV_SHAPES,
conv_dtype=_CONV_DTYPE,
temporal_state_shape=(heads, _V, _K),
temporal_dtype=_TEMPORAL_DTYPE,
max_slots=_SLOTS,
)
return raw, temporal
def _make_covered_side_args(batch: int, heads: int):
"""The non-ssm covered() arguments, in the exact K3 shapes/dtypes so the
gate turns solely on the ssm_states view under test."""
bf16 = torch.bfloat16
seg = _seg(heads)
conv_dim = _conv_dim(heads)
mixed_qkv = torch.zeros((batch, conv_dim), dtype=bf16, device=_DEV)
a = torch.zeros((batch, seg), dtype=bf16, device=_DEV)
b = torch.zeros((batch, heads), dtype=bf16, device=_DEV)
onorm_g = torch.zeros((batch, seg), dtype=bf16, device=_DEV)
conv_states = torch.zeros(
(_SLOTS, _CONV_STATE_W, conv_dim), dtype=bf16, device=_DEV
)
cache_indices = torch.zeros((batch,), dtype=torch.int32, device=_DEV)
return mixed_qkv, a, b, conv_states, onorm_g, cache_indices
def _addressing_samples(heads: int):
return [
(0, 0, 0, 0),
(5, heads - 1, 127, 127),
(3, heads // 2, 64, 100),
(1, 0, 0, 1),
(2, min(heads - 1, 2), 3, 7),
]
class TestKdaFusedDecodeStridedState(unittest.TestCase):
def test_covered_accepts_envelope_strided_and_rejects_noncontiguous_inner(self):
for heads in _KDA_HEADS:
with self.subTest(kda_heads=heads):
_raw, temporal = _make_strided_temporal_view(heads)
ssm = temporal[_LAYER_UNDER_TEST] # what mamba2_layer_cache serves
# Precondition: the pool really is envelope-strided (else the
# property below would be vacuous — a dense pool passes the old
# gate too).
self.assertNotEqual(
ssm.stride(0),
heads * _V * _K,
"test setup no longer produces a strided pool",
)
# Inner [H, V, K] IS contiguous — the contract the kernel's
# float4 state loads rely on and all covered() must still require.
self.assertEqual(
(ssm.stride(-1), ssm.stride(-2), ssm.stride(-3)),
(1, _K, _V * _K),
)
(
mixed_qkv,
a,
b,
conv_states,
onorm_g,
cache_indices,
) = _make_covered_side_args(batch=2, heads=heads)
# (1) Accept the envelope-strided view — pre-fix
# .view(...).is_contiguous() would reject this and drop decode
# to the unfused chain.
self.assertTrue(
covered(mixed_qkv, a, b, conv_states, ssm, cache_indices, onorm_g),
"covered() rejected the envelope-strided ssm pool (fused decode "
"would silently fall back to the unfused chain)",
)
# Still reject a view whose inner dims are NOT contiguous: the
# kernel cannot float4-load a transposed [.., V, K] state.
ssm_bad = ssm.transpose(-1, -2) # stride(-1) == K, not 1
self.assertFalse(
covered(
mixed_qkv,
a,
b,
conv_states,
ssm_bad,
cache_indices,
onorm_g,
),
"covered() must reject a non-inner-contiguous ssm view",
)
def test_slot_formula_resolves_correct_element_and_dense_pitch_misaddresses(self):
for heads in _KDA_HEADS:
with self.subTest(kda_heads=heads):
raw, temporal = _make_strided_temporal_view(heads)
ssm = temporal[_LAYER_UNDER_TEST]
# Distinct value per storage element so an offset that lands
# elsewhere reads a provably different value.
raw_fp32 = raw.view(torch.float32)
raw_fp32.copy_(torch.arange(raw_fp32.numel(), dtype=torch.float32))
base = ssm.storage_offset()
# == state.stride(0) the wrapper passes the kernel.
slot_stride = ssm.stride(0)
dense_pitch = heads * _V * _K # the pre-fix hardcoded slot pitch
for slot, i_hv, v, k in _addressing_samples(heads):
intra = (
i_hv * (_V * _K) + v * _K + k
) # kernel's hardcoded intra-slot offset
kernel_off = base + slot * slot_stride + intra
# (2a) The kernel formula names exactly the element torch
# indexing names — proves slot*stride(0) + intra addresses
# the intended slot.
self.assertEqual(
raw_fp32[kernel_off].item(),
ssm[slot, i_hv, v, k].item(),
f"kernel slot formula mis-addressed "
f"(slot={slot}, i_hv={i_hv}, heads={heads})",
)
# (2b) The pre-fix dense-pitch formula lands on a DIFFERENT
# element (a different layer's envelope region) for every
# slot > 0.
dense_off = base + slot * dense_pitch + intra
if slot > 0:
self.assertNotEqual(
raw_fp32[dense_off].item(),
ssm[slot, i_hv, v, k].item(),
f"dense-pitch formula happened to match at "
f"slot={slot}, heads={heads}; the fix would not "
"be load-bearing",
)
if __name__ == "__main__":
unittest.main()