[Kernel] Reclassify kernel tests by ops group + move helpers out of the package (RFC #29630) (#32128)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Xiaoyu Zhang
2026-07-23 12:18:27 +08:00
committed by GitHub
co-authored by Claude Opus 4.8
parent a2935ce329
commit 2d1a7be8c4
205 changed files with 204 additions and 173 deletions
@@ -0,0 +1,170 @@
import itertools
import sys
import pytest
import torch
from sglang.kernels.ops.quantization.awq_dequantize import (
awq_dequantize as jit_awq_dequantize,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=9, stage="base-b-kernel-unit", runner_config="1-gpu-large")
try:
from sgl_kernel import awq_dequantize as aot_awq_dequantize
AOT_AVAILABLE = True
except ImportError:
AOT_AVAILABLE = False
def reverse_awq_order(t: torch.Tensor):
bits = 4
AWQ_REVERSE_ORDER = [0, 4, 1, 5, 2, 6, 3, 7]
reverse_order_tensor = torch.arange(
t.shape[-1],
dtype=torch.int32,
device=t.device,
)
reverse_order_tensor = reverse_order_tensor.view(-1, 32 // bits)
reverse_order_tensor = reverse_order_tensor[:, AWQ_REVERSE_ORDER]
reverse_order_tensor = reverse_order_tensor.view(-1)
t = t[:, reverse_order_tensor] & 0xF
return t
# qweights - [R , C // 8], int32
# scales - [R // G, C ], float16
# zeros - [R // G, C // 8], int32
def awq_dequantize_torch(
qweight: torch.Tensor, scales: torch.Tensor, qzeros: torch.Tensor, group_size: int
) -> torch.Tensor:
if group_size == -1:
group_size = qweight.shape[0]
bits = 4
shifts = torch.arange(0, 32, bits, device=qzeros.device)
iweights = torch.bitwise_right_shift(qweight[:, :, None], shifts[None, None, :]).to(
torch.int8
)
iweights = iweights.view(iweights.shape[0], -1)
zeros = torch.bitwise_right_shift(qzeros[:, :, None], shifts[None, None, :]).to(
torch.int8
)
zeros = zeros.view(qzeros.shape[0], -1)
zeros = reverse_awq_order(zeros)
iweights = reverse_awq_order(iweights)
iweights = torch.bitwise_and(iweights, (2**bits) - 1)
zeros = torch.bitwise_and(zeros, (2**bits) - 1)
scales = scales.repeat_interleave(group_size, dim=0)
zeros = zeros.repeat_interleave(group_size, dim=0)
return (iweights - zeros) * scales
@pytest.mark.parametrize(
"qweight_row,qweight_col,is_bf16_act",
list(
itertools.product(
[128, 256, 512, 1024, 3584],
[16, 32, 64, 128, 448],
[True, False],
)
),
)
def test_awq_dequantize_jit_vs_torch(
qweight_row: int, qweight_col: int, is_bf16_act: bool
):
device = torch.device("cuda")
qweight = torch.randint(
0,
torch.iinfo(torch.int32).max,
(qweight_row, qweight_col),
dtype=torch.int32,
device=device,
)
group_size = qweight_row
scales_row = qweight_row // group_size
scales_col = qweight_col * 8
if is_bf16_act:
scales = torch.rand(scales_row, scales_col, dtype=torch.bfloat16, device=device)
else:
scales = torch.rand(scales_row, scales_col, dtype=torch.float16, device=device)
qzeros = torch.randint(
0,
torch.iinfo(torch.int32).max,
(scales_row, qweight_col),
dtype=torch.int32,
device=device,
)
# Run both implementations
torch_out = awq_dequantize_torch(qweight, scales, qzeros, group_size)
jit_out = jit_awq_dequantize(qweight, scales, qzeros)
# Compare results (approximate due to different computation paths)
torch.testing.assert_close(
torch_out.to(torch.float32), jit_out.to(torch.float32), rtol=1e-3, atol=1e-5
)
@pytest.mark.parametrize(
"qweight_row,qweight_col,is_bf16_act",
list(
itertools.product(
[128, 256, 512, 1024, 3584],
[16, 32, 64, 128, 448],
[True, False],
)
),
)
def test_awq_dequantize_jit_vs_aot(
qweight_row: int, qweight_col: int, is_bf16_act: bool
):
if not AOT_AVAILABLE:
pytest.skip("sgl_kernel AOT not available")
device = torch.device("cuda")
qweight = torch.randint(
0,
torch.iinfo(torch.int32).max,
(qweight_row, qweight_col),
dtype=torch.int32,
device=device,
)
group_size = qweight_row
scales_row = qweight_row // group_size
scales_col = qweight_col * 8
if is_bf16_act:
scales = torch.rand(scales_row, scales_col, dtype=torch.bfloat16, device=device)
else:
scales = torch.rand(scales_row, scales_col, dtype=torch.float16, device=device)
qzeros = torch.randint(
0,
torch.iinfo(torch.int32).max,
(scales_row, qweight_col),
dtype=torch.int32,
device=device,
)
# Run both implementations
aot_out = aot_awq_dequantize(qweight, scales, qzeros)
jit_out = jit_awq_dequantize(qweight, scales, qzeros)
# Bitwise equality
torch.testing.assert_close(jit_out, aot_out, rtol=0, atol=0)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,124 @@
import sys
import numpy as np
import pytest
import torch
from sgl_kernel.scalar_type import scalar_types
from sglang.kernels.ops.quantization.awq_marlin_repack import (
awq_marlin_moe_repack as jit_awq_marlin_moe_repack,
)
from sglang.srt.layers.quantization.utils import pack_cols, quantize_weights
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
def _has_aot_awq_marlin_moe_repack() -> bool:
return hasattr(torch.ops.sgl_kernel, "awq_marlin_moe_repack") and hasattr(
torch.ops.sgl_kernel.awq_marlin_moe_repack, "default"
)
AOT_AVAILABLE = _has_aot_awq_marlin_moe_repack()
def awq_pack(
q_w: torch.Tensor,
num_bits: int,
size_k: int,
size_n: int,
):
assert q_w.shape == (size_k, size_n)
if num_bits == 4:
interleave = np.array([0, 2, 4, 6, 1, 3, 5, 7])
elif num_bits == 8:
interleave = np.array([0, 2, 1, 3])
else:
raise Exception("num_bits must be 4 or 8, got {}".format(num_bits))
q_w = q_w.reshape((-1, len(interleave)))[:, interleave].ravel()
q_w = q_w.reshape((-1, size_n)).contiguous()
return pack_cols(q_w, num_bits, size_k, size_n)
@pytest.mark.parametrize("num_bits", [4])
@pytest.mark.parametrize("num_experts", [2, 4, 8])
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2), (4, 4)])
@pytest.mark.parametrize("group_size", [16, 32])
def test_awq_marlin_moe_repack_jit_vs_aot(
num_bits, num_experts, k_tiles, n_tiles, group_size
):
if not AOT_AVAILABLE:
pytest.skip("sgl_kernel AOT not available")
tile_k, tile_n = 16, 64
size_k = k_tiles * tile_k
size_n = n_tiles * tile_n
pack_factor = 32 // num_bits
# Create per-expert AWQ-packed weights
b_q_weight = torch.empty(
(num_experts, size_k, size_n // pack_factor),
dtype=torch.int32,
device="cuda",
)
for e in range(num_experts):
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
w_ref, q_w, s, zp = quantize_weights(
b_weight, scalar_types.uint4, group_size, zero_points=True
)
b_q_weight[e] = awq_pack(q_w, num_bits, size_k, size_n)
perm = torch.empty((num_experts, 0), dtype=torch.int32, device="cuda")
out_jit = jit_awq_marlin_moe_repack(b_q_weight, perm, size_k, size_n, num_bits)
out_aot = torch.ops.sgl_kernel.awq_marlin_moe_repack.default(
b_q_weight, perm, size_k, size_n, num_bits
)
torch.cuda.synchronize()
# Bitwise equality
torch.testing.assert_close(out_jit, out_aot, rtol=0, atol=0)
@pytest.mark.parametrize("num_bits", [4])
@pytest.mark.parametrize("num_experts", [2, 4])
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2)])
@pytest.mark.parametrize("group_size", [16, 32])
def test_awq_marlin_moe_repack_shape(
num_bits, num_experts, k_tiles, n_tiles, group_size
):
tile_k, tile_n = 16, 64
size_k = k_tiles * tile_k
size_n = n_tiles * tile_n
pack_factor = 32 // num_bits
# Create per-expert AWQ-packed weights
b_q_weight = torch.empty(
(num_experts, size_k, size_n // pack_factor),
dtype=torch.int32,
device="cuda",
)
for e in range(num_experts):
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
w_ref, q_w, s, zp = quantize_weights(
b_weight, scalar_types.uint4, group_size, zero_points=True
)
b_q_weight[e] = awq_pack(q_w, num_bits, size_k, size_n)
perm = torch.empty((num_experts, 0), dtype=torch.int32, device="cuda")
out = jit_awq_marlin_moe_repack(b_q_weight, perm, size_k, size_n, num_bits)
torch.cuda.synchronize()
assert out.is_cuda and out.dtype == torch.int32
expected_shape = (num_experts, size_k // 16, size_n * (num_bits // 2))
assert list(out.shape) == list(expected_shape)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,110 @@
import sys
import numpy as np
import pytest
import torch
from sgl_kernel.scalar_type import scalar_types
from sglang.kernels.ops.quantization.awq_marlin_repack import (
awq_marlin_repack as jit_awq_marlin_repack,
)
from sglang.srt.layers.quantization.utils import pack_cols, quantize_weights
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_marlin_utils import get_weight_perm, marlin_weights
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
def _has_aot_awq_marlin_repack() -> bool:
return hasattr(torch.ops.sgl_kernel, "awq_marlin_repack") and hasattr(
torch.ops.sgl_kernel.awq_marlin_repack, "default"
)
AOT_AVAILABLE = _has_aot_awq_marlin_repack()
def awq_pack(
q_w: torch.Tensor,
num_bits: int,
size_k: int,
size_n: int,
):
assert q_w.shape == (size_k, size_n)
if num_bits == 4:
interleave = np.array([0, 2, 4, 6, 1, 3, 5, 7])
elif num_bits == 8:
interleave = np.array([0, 2, 1, 3])
else:
raise Exception("num_bits must be 4 or 8, got {}".format(num_bits))
q_w = q_w.reshape((-1, len(interleave)))[:, interleave].ravel()
q_w = q_w.reshape((-1, size_n)).contiguous()
return pack_cols(q_w, num_bits, size_k, size_n)
@pytest.mark.parametrize("num_bits", [4, 8])
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2), (4, 4)])
@pytest.mark.parametrize("group_size", [16, 32])
def test_awq_marlin_repack_jit_vs_aot(num_bits, k_tiles, n_tiles, group_size):
if not AOT_AVAILABLE:
pytest.skip("sgl_kernel AOT not available")
tile_k, tile_n = 16, 64
size_k = k_tiles * tile_k
size_n = n_tiles * tile_n
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
w_ref, q_w, s, zp = quantize_weights(
b_weight, scalar_types.uint4, group_size, zero_points=True
)
q_w_awq = awq_pack(q_w, num_bits, size_k, size_n)
out_jit = jit_awq_marlin_repack(q_w_awq, size_k, size_n, num_bits)
out_aot = torch.ops.sgl_kernel.awq_marlin_repack.default(
q_w_awq, size_k, size_n, num_bits
)
torch.cuda.synchronize()
# Bitwise equality
torch.testing.assert_close(out_jit, out_aot, rtol=0, atol=0)
@pytest.mark.parametrize("num_bits", [4, 8])
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2)])
@pytest.mark.parametrize("group_size", [16, 32])
def test_awq_marlin_repack_correct(num_bits, k_tiles, n_tiles, group_size):
tile_k, tile_n = 16, 64
size_k = k_tiles * tile_k
size_n = n_tiles * tile_n
pack_factor = 32 // num_bits
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
w_ref, q_w, s, zp = quantize_weights(
b_weight, scalar_types.uint4, group_size, zero_points=True
)
q_w_awq = awq_pack(q_w, num_bits, size_k, size_n)
weight_perm = get_weight_perm(num_bits)
q_w_marlin = marlin_weights(q_w, size_k, size_n, num_bits, weight_perm)
out_gpu = jit_awq_marlin_repack(q_w_awq, size_k, size_n, num_bits)
assert out_gpu.is_cuda and out_gpu.dtype == torch.int32
expected_cols = size_n * tile_k // pack_factor
assert list(out_gpu.shape) == [size_k // tile_k, expected_cols]
torch.cuda.synchronize()
torch.testing.assert_close(out_gpu, q_w_marlin)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,191 @@
import sys
from types import SimpleNamespace
import pytest
import torch
from sgl_kernel.scalar_type import scalar_types
from sglang.kernels.ops.quantization.gptq_marlin import gptq_marlin_gemm
from sglang.srt.layers.quantization.marlin_utils import (
check_marlin_supported,
marlin_make_workspace,
)
from sglang.srt.layers.quantization.marlin_utils_fp4 import (
apply_fp4_marlin_linear,
nvfp4_marlin_process_global_scale,
prepare_nvfp4_layer_for_marlin,
)
from sglang.srt.utils.common import is_sm80_supported, is_sm90_supported
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_marlin_utils import (
awq_marlin_quantize,
make_nvfp4_weight_and_ref,
marlin_quantize,
)
register_cuda_ci(est_time=13, stage="base-b-kernel-unit", runner_config="1-gpu-large")
MNK_FACTORS = [
(1, 1, 1),
(1, 4, 8),
(13, 17, 67),
(257, 13, 11),
]
@pytest.mark.parametrize("k_chunk", [128])
@pytest.mark.parametrize("n_chunk", [64, 256])
@pytest.mark.parametrize("quant_type", [scalar_types.uint4, scalar_types.uint4b8])
@pytest.mark.parametrize("group_size", [-1, 128])
@pytest.mark.parametrize("mnk_factors", MNK_FACTORS)
@pytest.mark.parametrize("act_order", [False, True])
def test_gptq_marlin_gemm(
k_chunk,
n_chunk,
quant_type,
group_size,
mnk_factors,
act_order,
):
m_factor, n_factor, k_factor = mnk_factors
has_zp = quant_type in [scalar_types.uint4, scalar_types.uint8]
size_m = m_factor
size_k = k_chunk * k_factor
size_n = n_chunk * n_factor
if act_order:
if group_size == -1:
return
if group_size == size_k:
return
if has_zp:
return
if size_k % group_size != 0:
return
a_input = torch.randn((size_m, size_k), dtype=torch.float16, device="cuda")
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
if has_zp:
w_ref, marlin_q_w, marlin_s, marlin_zp = awq_marlin_quantize(
b_weight, quant_type, group_size
)
g_idx = None
sort_indices = None
marlin_s2 = None
else:
w_ref, marlin_q_w, marlin_s, g_idx, sort_indices, _ = marlin_quantize(
b_weight, quant_type, group_size, act_order
)
marlin_zp = None
marlin_s2 = None
workspace = marlin_make_workspace(w_ref.device)
output = gptq_marlin_gemm(
a_input,
None,
marlin_q_w,
marlin_s,
marlin_s2,
marlin_zp,
g_idx,
sort_indices,
workspace,
quant_type,
a_input.shape[0],
b_weight.shape[1],
a_input.shape[1],
is_k_full=True,
use_atomic_add=False,
use_fp32_reduce=False,
is_zp_float=False,
)
output_ref = torch.matmul(a_input, w_ref)
torch.cuda.synchronize()
# JIT kernel should produce approximately correct results vs torch.matmul
max_diff = torch.mean(torch.abs(output - output_ref)) / torch.mean(
torch.abs(output_ref)
)
assert max_diff < 0.04
@pytest.mark.skipif(
not (is_sm80_supported() or is_sm90_supported()),
reason="NVFP4 Marlin fallback tests require CUDA SM8X/SM9X",
)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_nvfp4_marlin_support_and_scale_transforms_sm80_sm90(dtype):
major, minor = torch.cuda.get_device_capability()
capability = major * 10 + minor
assert check_marlin_supported(
scalar_types.float4_e2m1f,
group_size=16,
has_zp=False,
device_capability=capability,
)
global_scale = torch.tensor(1.0, dtype=dtype, device="cuda")
actual_global_scale = nvfp4_marlin_process_global_scale(global_scale)
assert actual_global_scale.is_cuda
assert actual_global_scale.ndim == 1
assert actual_global_scale.numel() == 1
if dtype == torch.float16:
assert actual_global_scale.item() == 128.0
else:
assert actual_global_scale.item() == 2.0**119
@pytest.mark.skipif(
not (is_sm80_supported() or is_sm90_supported()),
reason="NVFP4 Marlin dense numeric test requires CUDA SM80, SM86, or SM90",
)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_nvfp4_marlin_dense_matches_dequant_reference(dtype):
torch.manual_seed(0)
size_m = 17
size_k = 256
size_n = 192
group_size = 16
a_input = torch.randn((size_m, size_k), dtype=dtype, device="cuda") / 10
fp4_weight, scales, global_scale, weight_ref = make_nvfp4_weight_and_ref(
size_n, size_k, dtype, group_size=group_size
)
layer = torch.nn.Module()
layer.quant_config = SimpleNamespace(group_size=group_size)
layer.output_size_per_partition = size_n
layer.input_size_per_partition = size_k
layer.params_dtype = dtype
layer.weight = torch.nn.Parameter(fp4_weight, requires_grad=False)
layer.weight_scale = torch.nn.Parameter(scales, requires_grad=False)
layer.weight_global_scale = torch.nn.Parameter(
global_scale.reshape(1), requires_grad=False
)
prepare_nvfp4_layer_for_marlin(layer)
output = apply_fp4_marlin_linear(
a_input,
layer.weight,
layer.weight_scale,
layer.weight_global_scale,
layer.workspace,
size_n,
size_k,
use_fp32_reduce=True,
)
output_ref = torch.matmul(a_input, weight_ref.T)
torch.cuda.synchronize()
torch.testing.assert_close(output, output_ref, rtol=0.04, atol=0.04)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,95 @@
import sys
import pytest
import torch
from sgl_kernel.scalar_type import scalar_types
from sglang.kernels.ops.quantization.gptq_marlin_repack import gptq_marlin_repack
from sglang.srt.layers.quantization.utils import (
gptq_quantize_weights,
pack_rows,
sort_weights,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_marlin_utils import get_weight_perm, marlin_weights
register_cuda_ci(est_time=16, stage="base-b-kernel-unit", runner_config="1-gpu-large")
MARLIN_K_CHUNKS = [128]
MARLIN_N_CHUNKS = [64, 256]
MNK_FACTORS = [
(1, 1, 1),
(1, 4, 8),
(1, 7, 5),
(13, 17, 67),
(26, 37, 13),
(67, 13, 11),
(257, 13, 11),
(658, 13, 11),
]
@pytest.mark.parametrize("k_chunk", MARLIN_K_CHUNKS)
@pytest.mark.parametrize("n_chunk", MARLIN_N_CHUNKS)
@pytest.mark.parametrize("quant_type", [scalar_types.uint4b8])
@pytest.mark.parametrize("group_size", [-1, 32, 64, 128])
@pytest.mark.parametrize("act_order", [False, True])
@pytest.mark.parametrize("mnk_factors", MNK_FACTORS)
def test_gptq_marlin_repack(
k_chunk, n_chunk, quant_type, group_size, act_order, mnk_factors
):
m_factor, n_factor, k_factor = mnk_factors
size_k = k_chunk * k_factor
size_n = n_chunk * n_factor
# Filter act_order
if act_order:
if group_size == -1:
return
if group_size == size_k:
return
# Normalize group_size
if group_size == -1:
group_size = size_k
assert group_size <= size_k
if size_k % group_size != 0:
pytest.skip("size_k must be divisible by group_size")
# Create input
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
# Quantize (and apply act_order if provided)
w_ref, q_w, s, g_idx, rand_perm = gptq_quantize_weights(
b_weight, quant_type, group_size, act_order
)
q_w_gptq = pack_rows(q_w, quant_type.size_bits, size_k, size_n)
# For act_order, sort the "weights" and "g_idx" so that group ids are
# increasing
sort_indices = torch.empty(0, dtype=torch.int, device=b_weight.device)
if act_order:
q_w, g_idx, sort_indices = sort_weights(q_w, g_idx)
marlin_layout_perm = get_weight_perm(quant_type.size_bits)
q_w_marlin_ref = marlin_weights(
q_w, size_k, size_n, quant_type.size_bits, marlin_layout_perm
)
# Run JIT repack kernel
jit_output = gptq_marlin_repack(
q_w_gptq, sort_indices, size_k, size_n, quant_type.size_bits
)
torch.cuda.synchronize()
# JIT should match the reference (computed from CPU marlin_weights)
torch.testing.assert_close(jit_output, q_w_marlin_ref)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,152 @@
import random
import sys
import pytest
import torch
from sglang.kernels.ops.quantization.mxfp8 import (
es_sm100_mxfp8_blockscaled_grouped_quant,
es_sm100_mxfp8_blockscaled_moe_grouped_gemm,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=5, stage="base-b-kernel-unit", runner_config="1-gpu-large")
def align(val: int, alignment: int = 128) -> int:
return int((val + alignment - 1) // alignment * alignment)
# Copy from: https://github.com/deepseek-ai/DeepGEMM/blob/main/deep_gemm/utils.py
def calc_diff(x, y):
x, y = x.double(), y.double()
denominator = (x * x + y * y).sum()
sim = 2 * (x * y).sum() / denominator
return 1 - sim
def is_sm100_supported(device=None) -> bool:
return (torch.cuda.get_device_capability(device)[0] == 10) and (
torch.version.cuda >= "12.8"
)
@pytest.mark.skipif(
not is_sm100_supported(),
reason="test_mxfp8_moe at jit kernen is only supported on sm100",
)
@pytest.mark.parametrize("num_experts", [8, 16, 32, 64])
@pytest.mark.parametrize("out_dtype", [torch.half, torch.bfloat16])
def test_es_sm100_mxfp8_blockscaled_grouped_mm(num_experts, out_dtype):
device = "cuda"
alignment = 128
n_g = random.randint(1, 64) * alignment
k_g = random.randint(1, 64) * alignment
expert_offset = 0
expert_offsets = []
aux_expert_offset = 0
aux_expert_offsets = []
a_blockscale_offset = 0
a_blockscale_offsets = []
b_blockscale_offset = 0
b_blockscale_offsets = []
a_list = []
b_list = []
ref_d_list = []
tokens_per_expert = []
for g in range(num_experts):
m_g = random.randint(1, 512)
tokens_per_expert.append(m_g)
expert_offsets.append(expert_offset)
expert_offset += m_g
aux_expert_offsets.append(aux_expert_offset)
aux_expert_offset += n_g
a_blockscale_offsets.append(a_blockscale_offset)
a_blockscale_offset += align(m_g, 128)
b_blockscale_offsets.append(b_blockscale_offset)
b_blockscale_offset += n_g # n_g already align to 128
a = torch.normal(
0.0, std=1.0, size=(m_g, k_g), device=device, dtype=out_dtype
) # (M, K):(K, 1)
b = torch.normal(
0.0, std=1.0, size=(n_g, k_g), device=device, dtype=out_dtype
) # (N, K):(K, 1)
a_list.append(a)
b_list.append(b)
ref_d = a @ b.T
ref_d_list.append(ref_d)
a = torch.concat(a_list, dim=0)
b = torch.concat(b_list, dim=0)
_expert_offsets = torch.tensor(expert_offsets).to(device=device, dtype=torch.int32)
_aux_expert_offsets = torch.tensor(aux_expert_offsets).to(
device=device, dtype=torch.int32
)
_a_blockscale_offsets = torch.tensor(a_blockscale_offsets).to(
device=device, dtype=torch.int32
)
_b_blockscale_offsets = torch.tensor(b_blockscale_offsets).to(
device=device, dtype=torch.int32
)
a_quant = torch.zeros_like(a, dtype=torch.float8_e4m3fn, device=device)
a_scale_factor = torch.zeros(
(a_blockscale_offset, k_g // 32), dtype=torch.uint8, device=device
)
b_quant = torch.zeros_like(b, dtype=torch.float8_e4m3fn, device=device)
b_scale_factor = torch.zeros(
(num_experts * n_g, k_g // 32), dtype=torch.uint8, device=device
)
tokens_per_expert = torch.tensor(tokens_per_expert).to(
device=device, dtype=torch.int32
)
workspace = torch.empty((1024, 1024, 1024), dtype=torch.uint8, device=device)
es_sm100_mxfp8_blockscaled_grouped_quant(
a,
tokens_per_expert,
_expert_offsets,
_a_blockscale_offsets,
a_quant,
a_scale_factor,
)
es_sm100_mxfp8_blockscaled_grouped_quant(
b,
torch.ones_like(tokens_per_expert) * n_g,
_aux_expert_offsets,
_b_blockscale_offsets,
b_quant,
b_scale_factor,
)
b_quant = b_quant.view(num_experts, n_g, k_g)
b_scale_factor = b_scale_factor.view(num_experts, n_g, k_g // 32)
d = es_sm100_mxfp8_blockscaled_moe_grouped_gemm(
b_quant,
a_quant,
b_scale_factor,
a_scale_factor,
_expert_offsets,
_a_blockscale_offsets,
tokens_per_expert,
workspace,
a.dtype,
)
for g in range(num_experts):
baseline = ref_d_list[g]
actual = d[expert_offsets[g] : (expert_offsets[g] + tokens_per_expert[g])]
diff = calc_diff(actual, baseline)
assert diff < 0.001
print(
f"m_g={baseline.shape[0]} n_g={n_g} k_g={k_g} num_experts={num_experts}, out_dtype={out_dtype}, diff={diff:.5f}: OK"
)
if __name__ == "__main__":
sys.exit(pytest.main([__file__]))
@@ -0,0 +1,103 @@
import itertools
import sys
from typing import Optional, Tuple
import pytest
import torch
from sglang.kernels.jit.utils import get_ci_test_range
from sglang.kernels.ops.quantization._jit_per_tensor_quant_fp8 import (
per_tensor_quant_fp8,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=16, stage="base-b-kernel-unit", runner_config="1-gpu-large")
# Nightly is not redundant here: it sets SGLANG_JIT_KERNEL_RUN_FULL_TESTS=1 to expand get_ci_test_range sweeps.
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
try:
from sglang.srt.utils import is_hip
_is_hip = is_hip()
except ImportError:
_is_hip = False
fp8_type_ = torch.float8_e4m3fnuz if _is_hip else torch.float8_e4m3fn
def sglang_scaled_fp8_quant(
input: torch.Tensor,
scale: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
fp8_type_: torch.dtype = torch.float8_e4m3fn
output = torch.empty_like(input, device=input.device, dtype=fp8_type_)
is_static = True
if scale is None:
scale = torch.zeros(1, device=input.device, dtype=torch.float32)
is_static = False
per_tensor_quant_fp8(input, output, scale, is_static)
return output, scale
def torch_scaled_fp8_quant(tensor, inv_scale):
finfo = torch.finfo(torch.float8_e4m3fn)
scale = inv_scale.reciprocal()
qweight = (tensor.to(torch.float32) * scale).clamp(min=finfo.min, max=finfo.max)
qweight = qweight.to(torch.float8_e4m3fn)
return qweight
PER_TENSOR_QUANT_CASES = get_ci_test_range(
list(
itertools.product(
[38, 39, 128, 256, 512, 1392],
[512, 1536, 2048, 4096, 7168],
)
),
[(38, 7168), (39, 1536), (128, 512), (512, 4096), (1392, 1536)],
)
@pytest.mark.parametrize("num_tokens,hidden_dim", PER_TENSOR_QUANT_CASES)
def test_jit_per_tensor_quant_compare_implementations(
num_tokens: int,
hidden_dim: int,
):
device = torch.device("cuda")
x = torch.rand((num_tokens, hidden_dim), dtype=torch.float16, device=device)
sglang_out, sglang_scale = sglang_scaled_fp8_quant(x)
torch_out = torch_scaled_fp8_quant(x, sglang_scale)
torch.testing.assert_close(
sglang_out.float(), torch_out.float(), rtol=1e-3, atol=1e-3
)
@pytest.mark.parametrize("shape", [(4, 8, 64), (2, 16, 128), (19260817, 1, 1)])
def test_jit_per_tensor_quant_supports_3d(shape):
device = torch.device("cuda")
x = torch.rand(shape, dtype=torch.bfloat16, device=device)
out = torch.empty_like(x, device=x.device, dtype=fp8_type_)
scale = torch.zeros(1, device=x.device, dtype=torch.float32)
per_tensor_quant_fp8(x, out, scale, is_static=False)
x_2d = x.flatten(0, -2)
out_ref_2d = torch_scaled_fp8_quant(x_2d, scale)
out_ref = out_ref_2d.reshape(shape)
torch.testing.assert_close(out.float(), out_ref.float(), rtol=1e-3, atol=1e-3)
scale = torch.rand(1, dtype=torch.float32, device=device)
sglang_out, _ = sglang_scaled_fp8_quant(x, scale)
torch_out = torch_scaled_fp8_quant(x, scale)
torch.testing.assert_close(
sglang_out.float(), torch_out.float(), rtol=1e-3, atol=1e-3
)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,475 @@
"""Correctness tests for the trait-driven per_token_group_quant JIT kernel.
The reference is computed in pure PyTorch (the quantization math itself), NOT by
calling the v2 / minimax kernels -- those are being deprecated, so the tests
must outlive them.
Two guard strengths, chosen by what the kernel's numerics can actually pin:
- UE8M0 paths: the quant multiplier is an exact power of two (a bit shift, no
division), so codes and packed exponent bytes are compared BIT-EXACT
against the torch reference. These are the production paths (DeepGEMM dense,
EP-MoE), so this is where bit-exactness matters.
- fp32 / int8 scale paths: the kernel divides under ``--use_fast_math`` (fast
reciprocal), so codes are not bit-reproducible from an exact torch divide.
Those tests pin the exactly-reproducible parts -- the stored scale (a single
multiply) -- and the dequant round-trip error, which is what downstream
actually consumes.
"""
import itertools
import pytest
import torch
from sglang.kernels.jit.utils import get_ci_test_range
from sglang.kernels.ops.quantization._jit_per_token_group_quant import (
per_token_group_quant,
)
from sglang.kernels.ops.quantization.fp8_kernel import (
create_per_token_group_quant_fp8_output_scale,
fp8_dtype,
fp8_max,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=90, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=90, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
G = 128
FMAX = float(fp8_max) # 448 for e4m3
I8_MAX, I8_MIN = 127.0, -128.0
EPS = 1e-10
# --------------------------------------------------------------------------- #
# Pure-torch references (match the kernel's expression order).
# --------------------------------------------------------------------------- #
def _group_amax(x: torch.Tensor, gs: int) -> torch.Tensor:
"""Per-group absmax over the last dim, floored at EPS. Returns [..., ng]."""
xf = x.float().unflatten(-1, (-1, gs))
return xf.abs().amax(-1).clamp_min(EPS)
def _quantize(x: torch.Tensor, gs: int, quant_scale: torch.Tensor, out_dtype, lo, hi):
xf = x.float().unflatten(-1, (-1, gs))
q = (xf * quant_scale.unsqueeze(-1)).clamp(lo, hi).to(out_dtype)
return q.flatten(-2)
def ref_fp8_fp32_scale(x, gs):
"""fp8 codes + fp32 stored scale (scale = amax / FMAX, a single multiply)."""
amax = _group_amax(x, gs)
scale_inv = amax * (1.0 / FMAX)
q = _quantize(x, gs, FMAX / amax, fp8_dtype, -FMAX, FMAX)
return q, scale_inv
def ref_int8(x, gs):
amax = _group_amax(x, gs)
scale_inv = amax * (1.0 / I8_MAX)
q = _quantize(x, gs, I8_MAX / amax, torch.int8, I8_MIN, I8_MAX)
return q, scale_inv
def ref_fp8_ue8m0(x, gs):
"""fp8 codes + UE8M0 exponent bytes [..., ng]. The multiplier 2^-e is exact
in fp32, so codes are bit-reproducible (unlike the fp32-scale path)."""
amax = _group_amax(x, gs)
raw = (amax / FMAX).contiguous()
bits = raw.view(torch.int32)
exp = ((bits >> 23) & 0xFF) + ((bits & 0x7FFFFF) != 0).to(
torch.int32
) # ceil to ue8m0
quant_scale = ((127 + 127 - exp) << 23).view(torch.float32) # 2^(127 - (exp-127))
q = _quantize(x, gs, quant_scale, fp8_dtype, -FMAX, FMAX)
return q, exp.to(torch.uint8)
def _decode_packed_exp(s_int32: torch.Tensor, ng: int) -> torch.Tensor:
"""Decode an int32 packed-UE8M0 scale (logical [..., ceil(ng/4)]) to the
[..., ng] exponent grid, independent of the physical (row/col-major) layout:
exponent[..., g] = byte (g % 4) of int32[..., g // 4]."""
g = torch.arange(ng, device=s_int32.device)
col = s_int32.index_select(-1, g // 4)
return ((col >> (8 * (g % 4))) & 0xFF).to(torch.uint8)
def _dequant_rel_err(q, scale_inv, x, gs) -> float:
deq = (q.float().unflatten(-1, (-1, gs)) * scale_inv.unsqueeze(-1)).flatten(-2)
return ((x.float() - deq).abs() / (x.float().abs() + 1e-6)).mean().item()
def _packed_exp_to_dequant_scale(x_s, ng) -> torch.Tensor:
"""Decode a packed-UE8M0 scale buffer to the fp32 dequant scale 2^(e-127)."""
exp = _decode_packed_exp(x_s, ng).to(torch.int32)
return torch.exp2(exp.float() - 127.0)
def _alloc_scale(x_shape, *, column_major, scale_ue8m0):
s = create_per_token_group_quant_fp8_output_scale(
x_shape=x_shape,
device="cuda",
group_size=G,
column_major_scales=column_major,
scale_tma_aligned=column_major,
scale_ue8m0=scale_ue8m0,
)
s.zero_()
return s
# --------------------------------------------------------------------------- #
# UE8M0 paths: bit-exact vs the torch reference.
# --------------------------------------------------------------------------- #
# hidden 768 (Qwen3-30B-A3B moe_intermediate: 6 groups) exercises the non-4
# aligned col-packed tail; 128 is a single group.
UE8M0_CASES = get_ci_test_range(
list(
itertools.product(
[torch.bfloat16, torch.float16],
[1, 7, 38, 333],
[128, 768, 2048, 7168],
)
),
[
(torch.bfloat16, 1, 128),
(torch.bfloat16, 38, 768),
(torch.bfloat16, 333, 7168),
(torch.float16, 7, 2048),
],
)
@pytest.mark.parametrize("dtype,num_tokens,hidden", UE8M0_CASES)
def test_ue8m0_bitexact(dtype, num_tokens, hidden):
"""Col-major packed UE8M0: fp8 codes and decoded exponent bytes are
bit-exact with the torch reference (exact pow-2 multiplier). Covers the
aligned and non-4-aligned (hidden=768) pack-tail layouts."""
torch.manual_seed(hidden * 10 + num_tokens)
x = torch.randn(num_tokens, hidden, device="cuda", dtype=dtype)
q_ref, exp_ref = ref_fp8_ue8m0(x, G)
x_q = torch.zeros_like(x, dtype=fp8_dtype)
x_s = _alloc_scale((num_tokens, hidden), column_major=True, scale_ue8m0=True)
per_token_group_quant(x, x_q, x_s, G, scale_ue8m0=True)
torch.cuda.synchronize()
assert torch.equal(x_q.view(torch.int8), q_ref.view(torch.int8)), "codes differ"
exp = _decode_packed_exp(x_s, hidden // G)
assert torch.equal(exp, exp_ref), "exponent bytes differ"
@pytest.mark.parametrize("group_size", get_ci_test_range([16, 32, 64, 128], [16, 64]))
def test_ue8m0_group_sizes(group_size):
"""Group size is a template axis (v2 dispatched a runtime switch). Each size
maps a group onto a different subwarp lane count; codes/exponents must stay
bit-exact -- a wrong lane span would fold the wrong elements into absmax."""
torch.manual_seed(group_size)
num_tokens, hidden = 9, 4096
x = torch.randn(num_tokens, hidden, device="cuda", dtype=torch.bfloat16)
q_ref, exp_ref = ref_fp8_ue8m0(x, group_size)
x_q = torch.zeros_like(x, dtype=fp8_dtype)
x_s = create_per_token_group_quant_fp8_output_scale(
x_shape=(num_tokens, hidden),
device="cuda",
group_size=group_size,
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
)
x_s.zero_()
per_token_group_quant(x, x_q, x_s, group_size, scale_ue8m0=True)
torch.cuda.synchronize()
assert torch.equal(x_q.view(torch.int8), q_ref.view(torch.int8)), "codes differ"
exp = _decode_packed_exp(x_s, hidden // group_size)
assert torch.equal(exp, exp_ref), "exponent bytes differ"
# hidden 4096 -> 32 groups (aligned); 768 -> 6 groups (6 % 4 = 2, unaligned:
# the last int32 holds 2 real exponent bytes + 2 zero-padded tail bytes).
@pytest.mark.parametrize("hidden", [4096, 768])
def test_ue8m0_row_packed_bitexact(hidden):
"""Row-major packed UE8M0 (int32 [T, ceil(G/4)] contiguous, the minimax
layout): bit-exact vs the torch reference. The unaligned hidden exercises
the row-major pack-tail zeroing (fill_unaligned)."""
torch.manual_seed(hidden)
num_tokens = 17
x = torch.randn(num_tokens, hidden, device="cuda", dtype=torch.bfloat16)
q_ref, exp_ref = ref_fp8_ue8m0(x, G)
x_q = torch.zeros_like(x, dtype=fp8_dtype)
x_s = torch.zeros(
num_tokens, (hidden // G + 3) // 4, device="cuda", dtype=torch.int32
)
per_token_group_quant(x, x_q, x_s, G, scale_ue8m0=True)
torch.cuda.synchronize()
assert torch.equal(x_q.view(torch.int8), q_ref.view(torch.int8)), "codes differ"
exp = _decode_packed_exp(x_s, hidden // G)
assert torch.equal(exp, exp_ref), "exponent bytes differ"
# unaligned tail bytes of the last int32 must be zero-padded, not garbage.
ng = hidden // G
if ng % 4:
last_bytes = x_s[:, -1].contiguous().view(torch.uint8).view(num_tokens, 4)
assert torch.all(last_bytes[:, ng % 4 :] == 0), "pack-tail bytes not zeroed"
# --------------------------------------------------------------------------- #
# fp32 / int8 scale paths: exact stored scale + dequant round-trip (the codes
# are not bit-reproducible under fast-math division).
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("hidden", [4096, 768])
@pytest.mark.parametrize("column_major", [False, True])
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
def test_fp32_scale(dtype, column_major, hidden):
"""fp32 scale (row-major contiguous / col-major TMA view): the stored scale
is amax/FMAX (a single multiply, bit-exact) and dequant round-trips within
fp8 error.
hidden=768 (6 groups, ng % 4 != 0) is a bug regression: the host check
used to apply the ue8m0 pack-tail alignment requirement to fp32 scales,
which have no packing, and rejected this shape outright."""
torch.manual_seed(int(column_major) + 2 * (dtype == torch.float16))
num_tokens = 128
x = torch.randn(num_tokens, hidden, device="cuda", dtype=dtype)
_, scale_ref = ref_fp8_fp32_scale(x, G)
x_q = torch.zeros_like(x, dtype=fp8_dtype)
x_s = _alloc_scale(
(num_tokens, hidden), column_major=column_major, scale_ue8m0=False
)
per_token_group_quant(x, x_q, x_s, G)
torch.cuda.synchronize()
torch.testing.assert_close(x_s, scale_ref, rtol=0, atol=0)
assert _dequant_rel_err(x_q, x_s, x, G) < 0.05
@pytest.mark.parametrize("column_major", [False, True])
def test_int8_scale(column_major):
"""int8 output (row-major / col-major fp32 scale): exact stored scale
(amax/127) + dequant round-trip. Pins the multiply-by-inverse family (v1
divided by the scale and differed by a ULP) without depending on v2."""
torch.manual_seed(2 + int(column_major))
num_tokens, hidden = 33, 4096
x = torch.randn(num_tokens, hidden, device="cuda", dtype=torch.bfloat16)
_, scale_ref = ref_int8(x, G)
x_q = torch.zeros(num_tokens, hidden, device="cuda", dtype=torch.int8)
x_s = _alloc_scale(
(num_tokens, hidden), column_major=column_major, scale_ue8m0=False
)
per_token_group_quant(x, x_q, x_s, G)
torch.cuda.synchronize()
torch.testing.assert_close(x_s, scale_ref, rtol=0, atol=0)
# int8 group-quant is coarser than fp8 (127 vs 448 levels), so its mean
# relative round-trip error on randn data sits a little above the fp8 0.05.
assert _dequant_rel_err(x_q, x_s, x, G) < 0.08
def test_group_size_256_roundtrip():
"""Group 256 (32 lanes on H100, 16 on Blackwell) is above v2's old cap of
128. Pin the derived property: the fp32 scale equals absmax/FMAX and dequant
round-trips. A mis-mapped wide subwarp would fold the wrong elements into
the group absmax and move the scale."""
torch.manual_seed(256)
num_tokens, hidden, gs = 9, 4096, 256
x = torch.randn(num_tokens, hidden, device="cuda", dtype=torch.bfloat16)
_, scale_ref = ref_fp8_fp32_scale(x, gs)
x_q = torch.zeros_like(x, dtype=fp8_dtype)
x_s = torch.zeros(num_tokens, hidden // gs, device="cuda", dtype=torch.float32)
per_token_group_quant(x, x_q, x_s, gs)
torch.cuda.synchronize()
torch.testing.assert_close(x_s, scale_ref, rtol=0, atol=0)
assert _dequant_rel_err(x_q, x_s, x, gs) < 0.05
# --------------------------------------------------------------------------- #
# Fused silu+mul.
# --------------------------------------------------------------------------- #
def _ref_silu_mul(x, hidden):
"""silu in fp32, round to the input dtype, multiply in the input dtype --
matching the kernel's fused path exactly."""
gate, up = x[..., :hidden], x[..., hidden:]
return torch.nn.functional.silu(gate.float()).to(x.dtype) * up
@pytest.mark.parametrize("column_major", [True, False])
@pytest.mark.parametrize("scale_ue8m0", [True, False])
def test_fused_silu(scale_ue8m0, column_major):
"""fuse_silu_and_mul quantizes ``silu(x[..., :h]) * x[..., h:]`` (SGLang's
SiluAndMul: first half is the gated half). Covered across all four scale
layouts so the fused [gate | up] input layout is pinned everywhere.
The kernel's silu uses the fast ``__tanhf`` intrinsic on Blackwell, which
is not bit-reproducible from torch's sigmoid-based silu, so this is a
property test: dequant the kernel output through its own scale and check it
round-trips to the torch activation within fp8 error. (The quant math is
pinned bit-exact by the non-fused ue8m0 tests; a wrong gate/up split or
offset would move the round-trip well past tolerance.)"""
torch.manual_seed(int(scale_ue8m0) * 2 + int(column_major))
num_tokens, hidden = 37, 4096
x = torch.randn(num_tokens, hidden * 2, device="cuda", dtype=torch.bfloat16)
act = _ref_silu_mul(x, hidden)
x_q = torch.zeros(num_tokens, hidden, device="cuda", dtype=fp8_dtype)
if scale_ue8m0 and not column_major:
x_s = torch.zeros(
num_tokens, hidden // G // 4, device="cuda", dtype=torch.int32
)
else:
x_s = _alloc_scale(
(num_tokens, hidden), column_major=column_major, scale_ue8m0=scale_ue8m0
)
per_token_group_quant(
x, x_q, x_s, G, scale_ue8m0=scale_ue8m0, fuse_silu_and_mul=True
)
torch.cuda.synchronize()
deq_scale = _packed_exp_to_dequant_scale(x_s, hidden // G) if scale_ue8m0 else x_s
assert _dequant_rel_err(x_q, deq_scale, act, G) < 0.05
# --------------------------------------------------------------------------- #
# Masked EP-MoE schedule.
# --------------------------------------------------------------------------- #
MASKED_CASES = get_ci_test_range(
list(itertools.product([2, 5], [2048, 4096], [128, 384])),
[(2, 2048, 128), (5, 4096, 384)],
)
@pytest.mark.parametrize("masked_m_dtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("expected_m", [None, 4])
@pytest.mark.parametrize("num_experts,hidden,tokens_pad", MASKED_CASES)
def test_masked(num_experts, hidden, tokens_pad, expected_m, masked_m_dtype):
"""Masked EP-MoE schedule (col-packed ue8m0, plain quant -- no silu, so the
quant is bit-reproducible): rows < masked_m[e] are bit-exact vs the torch
reference; rows >= masked_m[e] stay zero (untouched). Fusion numerics are
covered by test_fused_silu; here the schedule is what's under test.
masked_m is accepted as int32 or int64 (the latter read as its low word),
so both dtypes are exercised.
expected_m=4 shrinks the grid's token axis far below masked_m, so the
grid-stride token loop must still cover every valid token -- guards the
host-hint-only contract (a wrong hint can never drop tokens)."""
torch.manual_seed(num_experts * 1000 + hidden + tokens_pad)
x = torch.randn(
num_experts, tokens_pad, hidden, device="cuda", dtype=torch.bfloat16
)
masked_m = torch.randint(
0, tokens_pad + 1, (num_experts,), device="cuda", dtype=masked_m_dtype
)
out_shape = (num_experts, tokens_pad, hidden)
x_q = torch.zeros(out_shape, device="cuda", dtype=fp8_dtype)
x_s = _alloc_scale(out_shape, column_major=True, scale_ue8m0=True)
per_token_group_quant(
x, x_q, x_s, G, scale_ue8m0=True, masked_m=masked_m, expected_m=expected_m
)
torch.cuda.synchronize()
q_ref, exp_ref = ref_fp8_ue8m0(x, G)
exp = _decode_packed_exp(x_s, hidden // G)
for e in range(num_experts):
m = int(masked_m[e])
assert torch.equal(
x_q[e, :m].view(torch.int8), q_ref[e, :m].view(torch.int8)
), "written codes differ"
assert torch.equal(exp[e, :m], exp_ref[e, :m]), "written exponents differ"
assert torch.all(x_q[e, m:].view(torch.int8) == 0), "padding codes touched"
def _as_int32(t: torch.Tensor) -> torch.Tensor:
return t.view(torch.int32) if t.dtype == torch.int32 else t
# (out_dtype, column_major_scales, scale_ue8m0); ue8m0 implies fp8 output.
AUTO_ALLOC_CASES = [
(torch.float8_e4m3fn, True, True),
(torch.float8_e4m3fn, False, True),
(torch.float8_e4m3fn, True, False),
(torch.float8_e4m3fn, False, False),
(torch.int8, True, False),
(torch.int8, False, False),
]
def test_masked_fused():
"""The production EP-MoE path: masked schedule + fuse_silu_and_mul +
col-packed ue8m0. silu is not bit-reproducible, so check the written rows
round-trip to the torch activation and padding rows stay zero."""
torch.manual_seed(7)
num_experts, tokens_pad, hidden = 3, 256, 2048
x = torch.randn(
num_experts, tokens_pad, hidden * 2, device="cuda", dtype=torch.bfloat16
)
masked_m = torch.randint(
0, tokens_pad + 1, (num_experts,), device="cuda", dtype=torch.int32
)
out_shape = (num_experts, tokens_pad, hidden)
x_q = torch.zeros(out_shape, device="cuda", dtype=fp8_dtype)
x_s = _alloc_scale(out_shape, column_major=True, scale_ue8m0=True)
per_token_group_quant(
x, x_q, x_s, G, scale_ue8m0=True, fuse_silu_and_mul=True, masked_m=masked_m
)
torch.cuda.synchronize()
act = _ref_silu_mul(x, hidden)
deq_scale = _packed_exp_to_dequant_scale(x_s, hidden // G)
for e in range(num_experts):
m = int(masked_m[e])
if m > 0:
assert _dequant_rel_err(x_q[e, :m], deq_scale[e, :m], act[e, :m], G) < 0.05
assert torch.all(x_q[e, m:].view(torch.int8) == 0), "padding touched"
@pytest.mark.parametrize("out_dtype,column_major_scales,scale_ue8m0", AUTO_ALLOC_CASES)
def test_auto_allocation(out_dtype, column_major_scales, scale_ue8m0):
"""Omitting output_q/output_s allocates them per out_dtype / major mode /
scale format and returns (q, s). The auto-allocated run must be bit-
identical to quantizing into caller-supplied buffers of the same layout --
guards that _allocate_outputs picks the layout the kernel decodes."""
torch.manual_seed(int(column_major_scales) * 2 + int(scale_ue8m0))
num_tokens, hidden = 38, 2048 # 16 groups, %4 == 0 for row-packed ue8m0
x = torch.randn(num_tokens, hidden, device="cuda", dtype=torch.bfloat16)
q_buf = torch.zeros(num_tokens, hidden, device="cuda", dtype=out_dtype)
if scale_ue8m0 and not column_major_scales:
s_buf = torch.zeros(
num_tokens, hidden // G // 4, device="cuda", dtype=torch.int32
)
else:
s_buf = _alloc_scale(
(num_tokens, hidden),
column_major=column_major_scales,
scale_ue8m0=scale_ue8m0,
)
per_token_group_quant(x, q_buf, s_buf, G, scale_ue8m0=scale_ue8m0)
q_auto, s_auto = per_token_group_quant(
x,
group_size=G,
scale_ue8m0=scale_ue8m0,
column_major_scales=column_major_scales,
out_dtype=out_dtype,
)
torch.cuda.synchronize()
assert q_auto.dtype == out_dtype and q_auto.shape == x.shape
assert s_auto.dtype == s_buf.dtype and s_auto.shape == s_buf.shape
assert torch.equal(q_auto.view(torch.int8), q_buf.view(torch.int8)), "codes differ"
assert torch.equal(_as_int32(s_auto), _as_int32(s_buf)), "scales differ"
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,191 @@
import itertools
import pytest
import torch
from sglang.kernels.jit.utils import get_ci_test_range
from sglang.kernels.ops.quantization._jit_per_token_group_quant_8bit_v2 import (
per_token_group_quant_8bit_v2,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=90, stage="base-b-kernel-unit", runner_config="1-gpu-large")
try:
from sgl_kernel import sgl_per_token_group_quant_8bit # AOT v2 reference op
except ImportError:
sgl_per_token_group_quant_8bit = None
if sgl_per_token_group_quant_8bit is None and not torch.cuda.is_available():
pytest.skip("sgl_kernel AOT reference op is unavailable", allow_module_level=True)
if sgl_per_token_group_quant_8bit is None:
raise ImportError("sgl_kernel AOT reference op is unavailable")
from sglang.kernels.ops.quantization.fp8_kernel import ( # noqa: E402
create_per_token_group_quant_fp8_output_scale,
fp8_dtype,
fp8_max,
fp8_min,
)
G = 128
def _alloc(x_shape, scale_ue8m0):
"""Pre-allocated (zeroed) output_q + output_s for a given input/output shape.
Zeroing makes the unwritten (padding / aligned) regions compare equal."""
x_q = torch.zeros(x_shape, device="cuda", dtype=fp8_dtype)
x_s = create_per_token_group_quant_fp8_output_scale(
x_shape=x_shape,
device="cuda",
group_size=G,
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=scale_ue8m0,
)
x_s.zero_()
return x_q, x_s
V2_QUANT_CASES = get_ci_test_range(
list(
itertools.product(
[torch.bfloat16, torch.float16],
[1, 7, 38, 64, 333],
[128, 2048, 4096, 7168],
[False, True],
[False, True],
)
),
[
(torch.bfloat16, 1, 128, False, False),
(torch.bfloat16, 17, 1536, False, False),
(torch.bfloat16, 38, 7168, False, True),
(torch.bfloat16, 38, 4096, False, True),
(torch.bfloat16, 64, 4096, True, True),
(torch.float16, 333, 2048, True, False),
],
)
@pytest.mark.parametrize(
"dtype,num_tokens,hidden,fuse_silu_and_mul,scale_ue8m0", V2_QUANT_CASES
)
def test_v2_jit_matches_aot(dtype, num_tokens, hidden, fuse_silu_and_mul, scale_ue8m0):
"""JIT v2 must be bit-exact with the AOT v2 across vanilla/silu and float/ue8m0
scales (NaiveScheduler)."""
torch.manual_seed(
hidden + num_tokens + int(fuse_silu_and_mul) + 7 * int(scale_ue8m0)
)
in_hidden = hidden * (2 if fuse_silu_and_mul else 1)
x = torch.randn(num_tokens, in_hidden, device="cuda", dtype=dtype)
out_shape = (num_tokens, hidden)
q_ref, s_ref = _alloc(out_shape, scale_ue8m0)
sgl_per_token_group_quant_8bit(
x,
q_ref,
s_ref,
G,
1e-10,
float(fp8_min),
float(fp8_max),
scale_ue8m0,
fuse_silu_and_mul,
None,
enable_v2=True,
)
x_q, x_s = _alloc(out_shape, scale_ue8m0)
per_token_group_quant_8bit_v2(
x,
x_q,
x_s,
G,
1e-10,
float(fp8_min),
float(fp8_max),
scale_ue8m0=scale_ue8m0,
fuse_silu_and_mul=fuse_silu_and_mul,
)
torch.cuda.synchronize()
assert torch.equal(x_q.view(torch.int8), q_ref.view(torch.int8)), "fp8 codes differ"
assert torch.equal(x_s, s_ref), "scales differ"
# NOTE: "row-major + scale_ue8m0=True" names two different formats:
# 1. packed int32 [T, ceil(G/4)] (4 exponent bytes per int32) -- supported by
# the JIT per_token_group_quant kernel and pinned bit-exact in test_per_token_group_quant
# (test_v3_ue8m0_row_packed_bitexact);
# 2. fp32 [T, G] storing power-of-two VALUES (the deep_gemm.fp8_einsum
# format) -- v2-only. No srt caller requests it (production ties
# scale_ue8m0 and column_major_scales to the same DEEPGEMM_SCALE_UE8M0
# flag), so the srt entry `sglang_per_token_group_quant_fp8`, which now
# routes to the JIT kernel, rejects it loudly instead of allocating an fp32
# buffer the kernel cannot fill. The v2 JIT kernel itself still implements it and is
# covered by test_v2_jit_matches_aot above.
# Masked (EP-MoE) path: the v2 op only has a masked scheduler for the
# column-major + ue8m0 + fused-silu+mul + masked combination. Input is 3D
# [num_experts, tokens_padded, hidden*2]; only tokens < masked_m[e] are processed
# (padding left untouched → zeros in both). Compare JIT vs AOT bit-exact.
MASKED_V2_CASES = get_ci_test_range(
# MaskedLayoutScheduler requires hidden / group_size to be divisible by 16.
list(itertools.product([2, 5], [2048, 4096, 8192], [128, 384])),
[(2, 2048, 128), (5, 4096, 384), (2, 8192, 128)],
)
@pytest.mark.parametrize("num_experts,hidden,tokens_pad", MASKED_V2_CASES)
def test_v2_jit_masked_matches_aot(num_experts, hidden, tokens_pad):
torch.manual_seed(num_experts * 1000 + hidden + tokens_pad)
x = torch.randn(
num_experts, tokens_pad, hidden * 2, device="cuda", dtype=torch.bfloat16
)
masked_m = torch.randint(
0, tokens_pad + 1, (num_experts,), device="cuda", dtype=torch.int32
)
out_shape = (num_experts, tokens_pad, hidden)
q_ref, s_ref = _alloc(out_shape, scale_ue8m0=True)
sgl_per_token_group_quant_8bit(
x,
q_ref,
s_ref,
G,
1e-10,
float(fp8_min),
float(fp8_max),
True,
True,
masked_m,
enable_v2=True,
)
x_q, x_s = _alloc(out_shape, scale_ue8m0=True)
per_token_group_quant_8bit_v2(
x,
x_q,
x_s,
G,
1e-10,
float(fp8_min),
float(fp8_max),
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_m=masked_m,
)
torch.cuda.synchronize()
assert torch.equal(
x_q.view(torch.int8), q_ref.view(torch.int8)
), "masked fp8 differ"
assert torch.equal(x_s, s_ref), "masked scales differ"
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))