[Fix] Support 128-aligned hidden sizes in the W4AFP8 DeepEP low-latency requant kernel (#35593)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f744607567
commit
b6dcd393d6
@@ -1944,6 +1944,7 @@ def _fp8_per_token_quant_to_per_tensor_quant_kernel(
|
||||
k,
|
||||
K_SCALE_BLOCK_SIZE: tl.constexpr,
|
||||
K_BLOCK_SIZE: tl.constexpr,
|
||||
HAS_K_TAIL: tl.constexpr,
|
||||
):
|
||||
pid_k, pid_m, pid_e = (
|
||||
tl.program_id(axis=0),
|
||||
@@ -1959,6 +1960,11 @@ def _fp8_per_token_quant_to_per_tensor_quant_kernel(
|
||||
return
|
||||
output_scale_val_inv = 1.0 / tl.load(output_scale_ptr).to(tl.float32)
|
||||
k_offsets = pid_k * K_BLOCK_SIZE + tl.arange(0, K_BLOCK_SIZE)
|
||||
# k only has to be a multiple of the 128-wide scale group (e.g. 3584), so the
|
||||
# last k block can be partial. Specialize on it: hidden sizes that fill
|
||||
# every block keep the unmasked loads, and their codegen is unchanged.
|
||||
if HAS_K_TAIL:
|
||||
k_mask = k_offsets < k
|
||||
scale_offsets = (k_offsets // K_SCALE_BLOCK_SIZE) * x_scale_stride2
|
||||
|
||||
x_ptrs = x_ptr + pid_e * m * k + k_offsets
|
||||
@@ -1966,10 +1972,22 @@ def _fp8_per_token_quant_to_per_tensor_quant_kernel(
|
||||
x_scale_ptrs = x_scale_ptr + pid_e * x_scale_stride0 + scale_offsets
|
||||
|
||||
for tok_idx in tl.range(token_id, last_effective_id, pid_m_dim):
|
||||
hidden = tl.load(x_ptrs + tok_idx * k).to(tl.float32)
|
||||
scale_fp32 = tl.load(x_scale_ptrs + tok_idx * x_scale_stride1).to(tl.float32)
|
||||
if HAS_K_TAIL:
|
||||
hidden = tl.load(x_ptrs + tok_idx * k, mask=k_mask, other=0.0)
|
||||
x_scale = tl.load(
|
||||
x_scale_ptrs + tok_idx * x_scale_stride1, mask=k_mask, other=0.0
|
||||
)
|
||||
else:
|
||||
hidden = tl.load(x_ptrs + tok_idx * k)
|
||||
x_scale = tl.load(x_scale_ptrs + tok_idx * x_scale_stride1)
|
||||
hidden = hidden.to(tl.float32)
|
||||
scale_fp32 = x_scale.to(tl.float32)
|
||||
hidden = hidden * scale_fp32 * output_scale_val_inv
|
||||
tl.store(output_ptrs + tok_idx * k, hidden.to(output_ptr.dtype.element_ty))
|
||||
quantized = hidden.to(output_ptr.dtype.element_ty)
|
||||
if HAS_K_TAIL:
|
||||
tl.store(output_ptrs + tok_idx * k, quantized, mask=k_mask)
|
||||
else:
|
||||
tl.store(output_ptrs + tok_idx * k, quantized)
|
||||
|
||||
|
||||
def fp8_per_token_to_per_tensor_quant_triton(
|
||||
@@ -1986,8 +2004,7 @@ def fp8_per_token_to_per_tensor_quant_triton(
|
||||
assert output_scale.numel() == 1
|
||||
|
||||
K_BLOCK_SIZE = 1024
|
||||
assert x.size(2) % K_BLOCK_SIZE == 0
|
||||
grid = (x.size(2) // K_BLOCK_SIZE, 32, x.size(0))
|
||||
grid = (triton.cdiv(x.size(2), K_BLOCK_SIZE), 32, x.size(0))
|
||||
_fp8_per_token_quant_to_per_tensor_quant_kernel[grid](
|
||||
x,
|
||||
x_scale,
|
||||
@@ -1999,6 +2016,7 @@ def fp8_per_token_to_per_tensor_quant_triton(
|
||||
x.size(2),
|
||||
K_SCALE_BLOCK_SIZE=K_SCALE_BLOCK_SIZE,
|
||||
K_BLOCK_SIZE=K_BLOCK_SIZE,
|
||||
HAS_K_TAIL=x.size(2) % K_BLOCK_SIZE != 0,
|
||||
num_warps=8,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Unit test for ``fp8_per_token_to_per_tensor_quant_triton`` across hidden sizes.
|
||||
|
||||
W4AFP8 DeepEP low-latency requantizes the fp8 dispatch payload with this kernel
|
||||
before the first CUTLASS grouped GEMM. The payload's hidden size is only
|
||||
guaranteed to be a multiple of the fp8 scale-group size (128) -- e.g. 3584 for
|
||||
Kimi-K3 -- so the kernel must handle a ``k`` tail that does not fill a whole
|
||||
``K_BLOCK_SIZE`` (1024) block, and must still leave the rows past ``masked_m``
|
||||
untouched.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.moe.ep_moe_kernels import (
|
||||
fp8_per_token_to_per_tensor_quant_triton,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=20, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
dev = "cuda"
|
||||
FP8 = torch.float8_e4m3fn
|
||||
K_SCALE_BLOCK_SIZE = 128
|
||||
# Every value the kernel can produce below is a multiple of 0.25, so this
|
||||
# sentinel cannot be matched by a kernel that wrongly writes a padding row.
|
||||
SENTINEL = 0.375
|
||||
OUTPUT_SCALE = 2.0
|
||||
|
||||
|
||||
def _build(num_experts, m, k, seed):
|
||||
g = torch.Generator(device="cpu").manual_seed(seed)
|
||||
# Integers in [-8, 8] with power-of-two per-token-group scales keep every
|
||||
# intermediate exactly representable in e4m3, so the reference below matches
|
||||
# bit-for-bit regardless of the rounding mode of the final cast.
|
||||
x = torch.randint(-8, 9, (num_experts, m, k), generator=g).float()
|
||||
exps = torch.randint(-1, 2, (num_experts, m, k // K_SCALE_BLOCK_SIZE), generator=g)
|
||||
x_scale = torch.pow(2.0, exps.float())
|
||||
return x.to(dev).to(FP8), x_scale.to(dev)
|
||||
|
||||
|
||||
def _ref(x, x_scale):
|
||||
dequant = x.float() * x_scale.repeat_interleave(K_SCALE_BLOCK_SIZE, dim=2)
|
||||
return (dequant * (1.0 / OUTPUT_SCALE)).to(FP8)
|
||||
|
||||
|
||||
# 7168: exact multiple of K_BLOCK_SIZE (the DeepSeek-V3 hidden size).
|
||||
# 3584 / 1152: only 128-aligned, so the last k block is partially masked.
|
||||
@pytest.mark.parametrize("k", [7168, 3584, 1152])
|
||||
def test_masked_rows_and_k_tail(k):
|
||||
num_experts, m = 4, 48
|
||||
masked = [0, 1, 17, m]
|
||||
|
||||
x, x_scale = _build(num_experts, m, k, seed=k)
|
||||
masked_m = torch.tensor(masked, dtype=torch.int32, device=dev)
|
||||
output_scale = torch.tensor([OUTPUT_SCALE], dtype=torch.float32, device=dev)
|
||||
output = torch.full((num_experts, m, k), SENTINEL, device=dev).to(FP8)
|
||||
|
||||
fp8_per_token_to_per_tensor_quant_triton(
|
||||
x=x,
|
||||
x_scale=x_scale,
|
||||
masked_m=masked_m,
|
||||
output_scale=output_scale,
|
||||
output=output,
|
||||
)
|
||||
|
||||
ref = _ref(x, x_scale)
|
||||
for e, valid in enumerate(masked):
|
||||
torch.testing.assert_close(
|
||||
output[e, :valid].float(), ref[e, :valid].float(), rtol=0, atol=0
|
||||
)
|
||||
# Padding rows are not part of any expert's GEMM problem size and must
|
||||
# stay as the caller left them.
|
||||
padding = output[e, valid:].float()
|
||||
torch.testing.assert_close(
|
||||
padding, torch.full_like(padding, SENTINEL), rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
Reference in New Issue
Block a user