[JIT] Trait-driven per_token_group_quant: unify the quant kernel family (flat + masked) (#30924)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
DarkSharpness
2026-07-22 08:46:52 +08:00
committed by GitHub
co-authored by Claude Fable 5
parent 024639a372
commit 8bb0d8d005
17 changed files with 1720 additions and 1388 deletions
@@ -0,0 +1,76 @@
from sglang.jit_kernel.benchmark import marker
from sglang.jit_kernel.benchmark.utils import create_empty, create_random
# per_token_group_quant_8bit_v2 is DEPRECATED (no production call sites); the
# kernel is kept only as the perf baseline for this benchmark.
from sglang.jit_kernel.per_token_group_quant import per_token_group_quant
from sglang.jit_kernel.per_token_group_quant_8bit_v2 import (
per_token_group_quant_8bit_v2,
)
from sglang.kernels.ops.quantization.fp8_kernel import (
create_per_token_group_quant_fp8_output_scale,
fp8_dtype,
fp8_max,
fp8_min,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(
est_time=25, stage="base-b-kernel-benchmark", runner_config="1-gpu-large"
)
HIDDEN = 2048
LAYOUTS = {
"row_major_fp32": (False, False),
"col_major_fp32": (True, False),
"col_major_ue8m0": (True, True),
}
def _jit_v2(G, x, x_q, x_s, 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,
)
def _current(G, x, x_q, x_s, scale_ue8m0):
per_token_group_quant(x, x_q, x_s, G, scale_ue8m0=scale_ue8m0)
FN = {"jit_v2": _jit_v2, "current": _current}
@marker.parametrize("group_size", [32, 64, 128], ci_vals=[128])
@marker.parametrize("layout", list(LAYOUTS), ci_vals=["col_major_ue8m0"])
@marker.parametrize("num_tokens", [2**n for n in range(0, 14)], ci_vals=[1, 32, 2048])
@marker.benchmark("impl", ["jit_v2", "current"])
def benchmark(group_size: int, layout: str, num_tokens: int, impl: str):
column_major, scale_ue8m0 = LAYOUTS[layout]
x = create_random(num_tokens, HIDDEN)
x_q = create_empty(num_tokens, HIDDEN, 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=column_major,
scale_tma_aligned=column_major,
scale_ue8m0=scale_ue8m0,
)
return marker.do_bench(
FN[impl],
input_args=(group_size, x, x_q, x_s, scale_ue8m0),
graph_clone_args=(1,),
memory_args=(x,),
memory_output=(x_q, x_s),
)
if __name__ == "__main__":
benchmark.run()
@@ -1,311 +0,0 @@
import itertools
from typing import Any, Dict, List
import torch
import triton
from sgl_kernel.test_utils import create_per_token_group_quant_test_data
from sglang.jit_kernel.benchmark.utils import get_benchmark_range
from sglang.jit_kernel.per_token_group_quant_8bit import (
per_token_group_quant_8bit as sglang_per_token_group_quant_8bit,
)
from sglang.kernels.ops.quantization.fp8_kernel import (
create_per_token_group_quant_fp8_output_scale,
)
from sglang.kernels.ops.quantization.fp8_kernel import (
per_token_group_quant_8bit as triton_per_token_group_quant_8bit,
)
from sglang.srt.utils import is_hip
from sglang.srt.utils.bench_utils import bench_kineto
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.utils import is_in_ci
register_cuda_ci(
est_time=13, stage="base-b-kernel-benchmark", runner_config="1-gpu-large"
)
IS_CI = is_in_ci()
_is_hip = is_hip()
fp8_type_ = torch.float8_e4m3fnuz if _is_hip else torch.float8_e4m3fn
NUM_TESTS = 30 if IS_CI else 300
GROUP_SIZE_RANGE = [128]
DST_DTYPE_RANGE = [fp8_type_]
# ---- GEMM-like branch (num_ranks=None) ----
NUM_TOKENS_RANGE_GEMM = get_benchmark_range(
full_range=[1, 4, 16, 64, 256, 768, 2048, 8192, 16384],
ci_range=[768],
)
HIDDEN_DIM_RANGE_GEMM = [1536, 7168, 16384]
NUM_RANKS_RANGE_GEMM = [None]
FLAGS_GEMM_FULL: List[Dict[str, Any]] = [
dict(
column_major_scales=False,
scale_tma_aligned=False,
scale_ue8m0=False,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=False,
scale_ue8m0=False,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=False,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
]
FLAGS_GEMM_CI: List[Dict[str, Any]] = [
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
]
FLAGS_RANGE_GEMM = get_benchmark_range(
full_range=FLAGS_GEMM_FULL, ci_range=FLAGS_GEMM_CI
)
CONFIGS_GEMM = list(
itertools.product(
NUM_TOKENS_RANGE_GEMM,
HIDDEN_DIM_RANGE_GEMM,
GROUP_SIZE_RANGE,
NUM_RANKS_RANGE_GEMM,
DST_DTYPE_RANGE,
FLAGS_RANGE_GEMM,
)
)
# ---- MoE-like / multi-rank branch (hidden_dim=2048, num_ranks in {8,16,32,48}) ----
NUM_TOKENS_RANGE_MOE = get_benchmark_range(
full_range=[1 * 8, 4 * 8, 64 * 8, 256 * 8, 768 * 8],
ci_range=[768 * 8],
)
HIDDEN_DIM_RANGE_MOE = [2048]
NUM_RANKS_RANGE_MOE = get_benchmark_range(
full_range=[8, 16, 32, 48],
ci_range=[48],
)
FLAGS_MOE: List[Dict[str, Any]] = [
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode="balanced",
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode="imbalanced",
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode="extreme",
),
]
FLAGS_RANGE_MOE = get_benchmark_range(full_range=FLAGS_MOE, ci_range=FLAGS_MOE)
CONFIGS_MOE = list(
itertools.product(
NUM_TOKENS_RANGE_MOE,
HIDDEN_DIM_RANGE_MOE,
GROUP_SIZE_RANGE,
NUM_RANKS_RANGE_MOE,
DST_DTYPE_RANGE,
FLAGS_RANGE_MOE,
)
)
# ---- Final configs ----
CONFIGS = CONFIGS_GEMM + CONFIGS_MOE
LINE_VALS = ["triton", "aot_v2", "sglang"]
LINE_NAMES = ["Triton (Inaccurate)", "AOT v2 (sgl-kernel)", "JIT (this repo)"]
STYLES = [("blue", "-"), ("red", "-"), ("green", "-")]
def _flatten_to_2d(t: torch.Tensor) -> torch.Tensor:
"""Reshape a tensor with 3+ dims to 2D by merging all leading dims."""
if t.ndim <= 2:
return t
return t.reshape(-1, t.shape[-1])
def _make_sglang_bench_fn(
x: torch.Tensor,
group_size: int,
dst_dtype: torch.dtype,
flags: dict,
provider: str = "sglang",
):
"""
Adapter that pre-allocates output tensors and returns a zero-arg callable
matching the JIT kernel's signature.
The JIT kernel does not support fuse_silu_and_mul, so when enabled we
pre-compute silu+mul on the input. bench_kineto only times the kernel
matching the given name, so the pre-processing is not included.
The JIT kernel expects 2D tensors, so any higher-dimensional inputs
(e.g. from masked_layout_mode) are flattened to 2D.
"""
fuse_silu_and_mul = flags.get("fuse_silu_and_mul", False)
column_major_scales = flags.get("column_major_scales", False)
scale_tma_aligned = flags.get("scale_tma_aligned", False)
scale_ue8m0 = flags.get("scale_ue8m0", False)
# JIT kernel does not support fuse_silu_and_mul; pre-compute it
if fuse_silu_and_mul:
half = x.shape[-1] // 2
x_input = torch.nn.functional.silu(x[..., :half]) * x[..., half:]
else:
x_input = x
# JIT kernel expects 2D (num_tokens, hidden_dim); flatten if needed
x_input = _flatten_to_2d(x_input.contiguous())
out_shape = x_input.shape
output_q = torch.empty(out_shape, device=x.device, dtype=dst_dtype)
fp8_max = torch.finfo(dst_dtype).max
fp8_min = -fp8_max
output_s = create_per_token_group_quant_fp8_output_scale(
x_shape=out_shape,
device=x.device,
group_size=group_size,
column_major_scales=column_major_scales,
scale_tma_aligned=scale_tma_aligned,
scale_ue8m0=scale_ue8m0,
)
if provider == "aot_v2":
from sgl_kernel import sgl_per_token_group_quant_8bit as aot_quant
def _run():
aot_quant(
x_input,
output_q,
output_s,
group_size,
1e-10,
fp8_min,
fp8_max,
scale_ue8m0,
False, # fuse_silu_and_mul (already applied to x_input)
None, # masked_m (flattened to 2D)
enable_v2=True,
)
else:
def _run():
sglang_per_token_group_quant_8bit(
input=x_input,
output_q=output_q,
output_s=output_s,
group_size=group_size,
eps=1e-10,
fp8_min=fp8_min,
fp8_max=fp8_max,
scale_ue8m0=scale_ue8m0,
)
return _run
@triton.testing.perf_report(
triton.testing.Benchmark(
x_names=[
"num_tokens",
"hidden_dim",
"group_size",
"num_ranks",
"dst_dtype",
"flags",
],
x_vals=CONFIGS,
line_arg="provider",
line_vals=LINE_VALS,
# Triton has multi kernels and we only report the time for the core one
line_names=LINE_NAMES,
styles=STYLES,
ylabel="us",
plot_name="per-token-group-quant-8bit-performance",
args={},
)
)
def benchmark(
num_tokens, hidden_dim, group_size, num_ranks, dst_dtype, flags, provider
):
print(
f"Testing: {num_tokens=} {hidden_dim=} {group_size=} {num_ranks=} {dst_dtype=} {flags=} {provider=}"
)
x, masked_m = create_per_token_group_quant_test_data(
num_tokens=num_tokens, hidden_dim=hidden_dim, num_ranks=num_ranks, flags=flags
)
if provider == "triton":
fn = triton_per_token_group_quant_8bit
kernel_names = "_per_token_group_quant_8bit|_silu_and_mul_post_quant_kernel"
bench_fn = lambda: fn(
x=x,
masked_m=masked_m,
group_size=group_size,
dst_dtype=dst_dtype,
**{k: v for k, v in flags.items() if k not in ["masked_layout_mode"]},
)
elif provider in ("sglang", "aot_v2"):
kernel_names = "per_token_group_quant_8bit_kernel"
bench_fn = _make_sglang_bench_fn(
x=x,
group_size=group_size,
dst_dtype=dst_dtype,
flags=flags,
provider=provider,
)
else:
raise ValueError(f"Unknown provider: {provider}")
time_s = bench_kineto(bench_fn, kernel_names=kernel_names, num_tests=NUM_TESTS)
return time_s * 1e6
if __name__ == "__main__":
benchmark.run(print_data=True)
@@ -0,0 +1,121 @@
import math
import torch
from sglang.jit_kernel.benchmark import marker
from sglang.jit_kernel.benchmark.utils import create_empty, create_random
# per_token_group_quant_8bit_v2 is DEPRECATED (no production call sites); the
# kernel is kept only as the perf baseline for this benchmark.
from sglang.jit_kernel.per_token_group_quant import per_token_group_quant
from sglang.jit_kernel.per_token_group_quant_8bit_v2 import (
per_token_group_quant_8bit_v2,
)
from sglang.kernels.ops.quantization.fp8_kernel import (
create_per_token_group_quant_fp8_output_scale,
fp8_dtype,
fp8_max,
fp8_min,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(
est_time=25, stage="base-b-kernel-benchmark", runner_config="1-gpu-large"
)
# name -> (moe_intermediate_size, topk, num_experts, group_size)
MODELS = {
"deepseek_v4": (3072, 6, 384, 32), # DeepSeek-V4 Pro
"deepseek_v3": (2048, 8, 256, 128), # DeepSeek-V3/R1
"qwen3_235b": (1536, 8, 128, 128), # Qwen3-235B-A22B
}
def _jit_v2(G, x, x_q, x_s, masked_m, expected_m, fuse):
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=fuse,
masked_m=masked_m,
)
def _current(G, x, x_q, x_s, masked_m, expected_m, fuse):
per_token_group_quant(
x,
x_q,
x_s,
G,
scale_ue8m0=True,
fuse_silu_and_mul=fuse,
masked_m=masked_m,
expected_m=expected_m,
)
FN = {"jit_v2": _jit_v2, "current": _current}
@marker.parametrize("model", list(MODELS), ci_vals=["deepseek_v3"])
@marker.parametrize("num_gpus", [4, 8], ci_vals=[4])
@marker.parametrize("fuse_silu", [True, False], ci_vals=[False])
@marker.parametrize("balanced", [True, False], ci_vals=[True])
@marker.parametrize("num_tokens", [2**n for n in range(8)], ci_vals=[1, 128])
@marker.benchmark("impl", ["jit_v2", "current"], unit="us")
def benchmark(
model: str,
fuse_silu: bool,
num_gpus: int,
num_tokens: int,
balanced: bool,
impl: str,
) -> marker.BenchResult:
torch.cuda.random.manual_seed(42)
max_tokens = 128 # TODO: test other size
hidden_size, topk, num_experts, group_size = MODELS[model]
if num_experts % num_gpus != 0 or topk * num_gpus > num_experts:
marker.skip("Incompatible model configuration")
if impl == "jit_v2" and (hidden_size // group_size) % 16 != 0:
marker.skip("v2 masked requires num_groups % 16 == 0")
if num_tokens > max_tokens:
marker.skip("num_tokens exceeds max_tokens")
num_local_experts = num_experts // num_gpus
padded_tokens = max_tokens * num_gpus
expected_m = math.ceil(max_tokens * topk / num_local_experts)
in_hidden = hidden_size * (2 if fuse_silu else 1)
x = create_random(num_local_experts, padded_tokens, in_hidden)
x_q = create_empty(num_local_experts, padded_tokens, hidden_size, dtype=fp8_dtype)
x_s = create_per_token_group_quant_fp8_output_scale(
x_shape=(num_local_experts, padded_tokens, hidden_size),
device="cuda",
group_size=group_size,
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
)
if balanced: # simulation
topk_ids = torch.randint(0, num_local_experts, (num_tokens * topk,))
masked_m = torch.bincount(topk_ids, minlength=num_local_experts)
masked_m = masked_m.cuda().int()
else: # only the last few experts receive all tokens
masked_m = create_empty(num_local_experts, dtype=torch.int32)
masked_m[:-topk].zero_()
masked_m[-topk:].fill_(num_tokens)
return marker.do_bench(
FN[impl],
input_args=(group_size, x, x_q, x_s, masked_m, expected_m, fuse_silu),
graph_clone_args=(0,),
memory_args=(x[:topk, :num_tokens], masked_m),
memory_output=(x_q[:topk, :num_tokens], x_s[:topk, :num_tokens]),
)
if __name__ == "__main__":
benchmark.run()
@@ -0,0 +1,473 @@
"""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.jit_kernel.per_token_group_quant import per_token_group_quant
from sglang.jit_kernel.utils import get_ci_test_range
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"]))
@@ -1,334 +0,0 @@
import itertools
import sys
import pytest
import torch
from sglang.jit_kernel.per_token_group_quant_8bit import (
per_token_group_quant_8bit as sglang_per_token_group_quant_8bit,
)
from sglang.jit_kernel.utils import get_ci_test_range
from sglang.srt.utils import is_hip
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)
if not torch.cuda.is_available():
pytest.skip("CUDA required", allow_module_level=True)
from sgl_kernel import ( # noqa: E402
sgl_per_token_group_quant_8bit as aot_per_token_group_quant_8bit,
)
from sgl_kernel.test_utils import ( # noqa: E402
assert_all_close_or_tiny_diff,
create_per_token_group_quant_test_data,
)
from sglang.jit_kernel.per_token_group_quant_8bit import ( # noqa: E402
per_token_group_quant_8bit as jit_per_token_group_quant_8bit,
)
from sglang.kernels.ops.quantization.fp8_kernel import ( # noqa: E402
create_per_token_group_quant_fp8_output_scale,
)
from sglang.kernels.ops.quantization.fp8_kernel import ( # noqa: E402
per_token_group_quant_8bit as triton_per_token_group_quant_8bit,
)
_is_hip = is_hip()
fp8_type_ = torch.float8_e4m3fnuz if _is_hip else torch.float8_e4m3fn
BASE_FLAGS = [
dict(
column_major_scales=False,
scale_tma_aligned=False,
scale_ue8m0=False,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=False,
scale_ue8m0=False,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=False,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=False,
masked_layout_mode=None,
),
]
FUSED_FLAGS = [
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode=None,
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode="balanced",
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode="imbalanced",
),
dict(
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=True,
fuse_silu_and_mul=True,
masked_layout_mode="extreme",
),
]
configs = get_ci_test_range(
list(
itertools.product(
[1, 4, 16, 17, 38, 51, 64, 127, 128, 512, 1024, 4096, 8192],
[128, 256, 384, 512, 768, 1024, 1536, 1664, 2048, 4096, 7168, 16384],
[16, 32, 64, 128],
[None],
[fp8_type_],
BASE_FLAGS,
)
)
+ list(
itertools.product(
[1, 4, 1 * 8, 4 * 8, 64 * 8, 256 * 8, 768 * 8],
[2048],
[128],
[8, 16, 32, 48],
[fp8_type_],
FUSED_FLAGS,
)
),
[
(1, 128, 128, None, fp8_type_, BASE_FLAGS[0]),
(17, 1536, 128, None, fp8_type_, BASE_FLAGS[2]),
(38, 4096, 128, None, fp8_type_, BASE_FLAGS[2]),
(51, 4096, 128, None, fp8_type_, BASE_FLAGS[2]),
(512, 2048, 128, 8, fp8_type_, FUSED_FLAGS[0]),
(2048, 2048, 128, 16, fp8_type_, FUSED_FLAGS[1]),
],
)
@pytest.mark.parametrize(
"num_tokens, hidden_dim, group_size, num_ranks, dst_dtype, flags", configs
)
def test_per_token_group_quant_with_column_major(
num_tokens,
hidden_dim,
group_size,
num_ranks,
dst_dtype,
flags,
):
arch_major, _ = torch.cuda.get_device_capability(torch.cuda.current_device())
if flags["scale_ue8m0"] and (arch_major <= 9):
pytest.skip("Only Blackwell need ue8m0 fusion")
return
if (flags["scale_ue8m0"] and (group_size != 128)) or (
(dst_dtype == torch.int8) and flags["column_major_scales"]
):
pytest.skip()
return
x, masked_m = create_per_token_group_quant_test_data(
num_tokens=num_tokens, hidden_dim=hidden_dim, num_ranks=num_ranks, flags=flags
)
execute_kwargs = dict(
x=x,
masked_m=masked_m,
group_size=group_size,
eps=1e-10,
dst_dtype=dst_dtype,
**{k: v for k, v in flags.items() if k not in ["masked_layout_mode"]},
)
def _postprocess(x_q, x_s):
if masked_m is not None:
print(f"Mask tokens after {masked_m} to be zero")
for i in range(len(masked_m)):
x_q[i, masked_m[i] :, :] = 0
x_s[i, masked_m[i] :, :] = 0
return x_q, x_s
x_q_triton, x_s_triton = _postprocess(
*triton_per_token_group_quant_8bit(**execute_kwargs)
)
fuse_silu_and_mul = False
out_shape = (*x.shape[:-1], x.shape[-1] // (2 if fuse_silu_and_mul else 1))
fp8_dtype = torch.float8_e4m3fn
fp8_max = torch.finfo(fp8_dtype).max
fp8_min = -fp8_max
x_q = torch.empty(out_shape, device=x.device, dtype=fp8_dtype)
x_s = create_per_token_group_quant_fp8_output_scale(
x_shape=out_shape,
device=x.device,
group_size=group_size,
column_major_scales=False,
scale_tma_aligned=False,
scale_ue8m0=False,
)
execute_kwargs = dict(
input=x,
output_q=x_q,
output_s=x_s,
group_size=group_size,
eps=1e-10,
fp8_max=fp8_max,
fp8_min=fp8_min,
)
x_q_sglang, x_s_sglang = _postprocess(
*sglang_per_token_group_quant_8bit(**execute_kwargs)
)
try:
assert_all_close_or_tiny_diff(x_q_triton, x_q_sglang)
torch.testing.assert_close(
x_s_triton.contiguous(),
x_s_sglang.contiguous(),
rtol=1e-3,
atol=1e-5,
msg=lambda message: message + f" {x_s_triton=} {x_s_sglang=}",
)
except AssertionError:
print(
f"{x.shape=} {x_q_triton.shape=} {x_s_triton.shape=} {x_q_sglang.shape=} {x_s_sglang.shape=}"
)
print(f"{x=}")
print(f"{masked_m=}")
print(f"{x_q_triton=}")
print(f"{x_s_triton=}")
print(f"{x_q_sglang=}")
print(f"{x_s_sglang=}")
raise
LAYOUTS = [
(False, False, False),
(True, False, False),
(True, True, False),
(True, True, True),
]
CONFIGS = list(
itertools.product(
[1, 4, 16, 64, 127, 128, 512, 1024, 4096, 8192],
[512, 1536, 2048, 4096, 6144, 7168, 16384],
[16, 32, 64, 128],
LAYOUTS,
[fp8_type_],
)
)
@pytest.mark.parametrize(
"num_tokens, hidden_dim, group_size, layout, dst_dtype", CONFIGS
)
def test_jit_matches_aot_v2_byte_identical(
num_tokens, hidden_dim, group_size, layout, dst_dtype
):
column_major_scales, scale_tma_aligned, scale_ue8m0 = layout
arch_major, _ = torch.cuda.get_device_capability(torch.cuda.current_device())
if scale_ue8m0 and arch_major <= 9:
pytest.skip("UE8M0 fusion is Blackwell-only")
if hidden_dim % group_size != 0:
pytest.skip("hidden_dim must be divisible by group_size")
torch.manual_seed(num_tokens * 131 + hidden_dim + group_size)
x = (torch.randn(num_tokens, hidden_dim, device="cuda", dtype=torch.bfloat16)) * 3.0
fp8_max = torch.finfo(dst_dtype).max
fp8_min = -fp8_max
def _alloc():
q = torch.empty_like(x, dtype=dst_dtype)
s = create_per_token_group_quant_fp8_output_scale(
x_shape=x.shape,
device=x.device,
group_size=group_size,
column_major_scales=column_major_scales,
scale_tma_aligned=scale_tma_aligned,
scale_ue8m0=scale_ue8m0,
)
return q, s
q_aot, s_aot = _alloc()
aot_per_token_group_quant_8bit(
x,
q_aot,
s_aot,
group_size,
1e-10,
fp8_min,
fp8_max,
scale_ue8m0,
False,
None,
enable_v2=True,
)
q_jit, s_jit = _alloc()
jit_per_token_group_quant_8bit(
x, q_jit, s_jit, group_size, 1e-10, fp8_min, fp8_max, scale_ue8m0=scale_ue8m0
)
# AOT v2 uses -use_fast_math reciprocal; this JIT uses precise division, so an
# exact fp8 midpoint can round to an adjacent code (1-ULP, JIT more accurate).
qj = q_jit.view(torch.uint8)
qa = q_aot.view(torch.uint8)
if not torch.equal(qj, qa):
mism = qj != qa
bj = qj[mism].to(torch.int16)
ba = qa[mism].to(torch.int16)
same_sign = (bj & 0x80) == (ba & 0x80)
one_ulp = (bj - ba).abs() == 1
assert bool(
(same_sign & one_ulp).all()
), f"q mismatch > 1 fp8 ULP {num_tokens=} {hidden_dim=} {group_size=} {layout=}"
assert mism.float().mean() < 0.01, (
f"too many fp8 ties ({int(mism.sum())}/{mism.numel()}) "
f"{num_tokens=} {hidden_dim=} {group_size=} {layout=}"
)
if scale_ue8m0:
assert torch.equal(
s_jit[:num_tokens].reshape(num_tokens, -1).view(torch.int32),
s_aot[:num_tokens].reshape(num_tokens, -1).view(torch.int32),
), f"ue8m0 scale mismatch {num_tokens=} {hidden_dim=} {group_size=}"
else:
assert torch.equal(
s_jit[:num_tokens].float(), s_aot[:num_tokens].float()
), f"float scale mismatch {num_tokens=} {hidden_dim=} {group_size=}"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -26,7 +26,6 @@ from sglang.kernels.ops.quantization.fp8_kernel import ( # noqa: E402
fp8_dtype,
fp8_max,
fp8_min,
sglang_per_token_group_quant_fp8,
)
G = 128
@@ -115,44 +114,17 @@ def test_v2_jit_matches_aot(dtype, num_tokens, hidden, fuse_silu_and_mul, scale_
assert torch.equal(x_s, s_ref), "scales differ"
ROW_MAJOR_UE8M0_CASES = get_ci_test_range(
list(
itertools.product(
[torch.bfloat16, torch.float16], [1, 33, 128], [128, 512, 4096, 7168]
)
),
[
(torch.bfloat16, 1, 128),
(torch.bfloat16, 17, 1536),
(torch.bfloat16, 33, 7168),
(torch.bfloat16, 38, 4096),
(torch.float16, 128, 4096),
],
)
@pytest.mark.parametrize("dtype,num_tokens,hidden", ROW_MAJOR_UE8M0_CASES)
def test_sglang_per_token_group_quant_fp8_row_major_ue8m0(dtype, num_tokens, hidden):
"""Row-major scale_ue8m0=True quantizes WITH the rounded (power-of-2) scale.
Verify: (1) scales are exact powers of 2, (2) dequant ≈ original within FP8 tolerance.
"""
torch.manual_seed(num_tokens * 1000 + hidden)
x = torch.randn(num_tokens, hidden, device="cuda", dtype=dtype)
x_q, x_s = sglang_per_token_group_quant_fp8(x, G, scale_ue8m0=True)
torch.cuda.synchronize()
# Scales must be exact powers of 2
log2_s = torch.log2(x_s.abs())
assert torch.equal(log2_s, log2_s.round()), "scales are not power-of-2"
# Dequant should approximate original within FP8 precision
x_deq = x_q.float().view(num_tokens, -1, G) * x_s.unsqueeze(-1)
x_deq = x_deq.view(num_tokens, hidden)
rel_err = (x.float() - x_deq).abs() / (x.float().abs() + 1e-6)
assert (
rel_err.mean() < 0.05
), f"mean relative dequant error too large: {rel_err.mean():.4f}"
# 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