KernelBench hard · B200
FP8 GEMM GLM-5.2
passdid not score
harnesszai-claudeagent session1h 40mtotal wall1h 40mcheck14sbenchmark8soutput tokens6,702gpu-lock wait0sgpu-lock held22sregimecompute
Per-shape vs governing ceilingeach shape graded against whichever binds — fp8 compute or HBM bandwidth
4096×4096×40960.076 ms40.3%1,812 TFLOPS · 40% of 4,500 TF fp8 peak · also 0.88 TB/s (11% of HBM)
4096×4096×41270.091 ms33.8%1,523 TFLOPS · 34% of 4,500 TF fp8 peak · also 0.74 TB/s (9% of HBM)
32×8192×81920.044 ms2.2%1.55 TB/s · 19% of 8.0 TB/s HBM · also 98 TFLOPS (2% of compute)
4096×14336×40960.200 ms53.5%2,410 TFLOPS · 54% of 4,500 TF fp8 peak · also 0.97 TB/s (12% of HBM)
compute-bound memory-bound · bar + right column = official fraction of the ceiling (the geomean input)
geomean(40.3% · 33.8% · 2.2% · 53.5%) = 20.0%
Kernel source (redacted)
"""FP8 e4m3 x e4m3 GEMM for NVIDIA B200 (SM100, Blackwell).
Real fp8 x fp8 tensor-core MMA via Triton's TMA-descriptor persistent kernel:
cp.async.bulk.tensor (TMA) loads + tcgen05.mma (5th-gen tensor cores) with fp32
accumulate, and the per-output-channel dequant scale folded into a subtile
epilogue. Matches reference.py's interface (same weight/weight_scale buffers).
K must be a multiple of 32 (the fp8 MMA native K-dim) AND a multiple of 16 for
the TMA descriptor stride requirement; when K isn't (e.g. K=4127) we zero-pad K
to the next multiple of 128 (zero contributes nothing to the dot product).
"""
import torch
import torch.nn as nn
import triton
import triton.language as tl
E4M3_MAX = 448.0
_PAD_MULT = 128
NUM_SMS = torch.cuda.get_device_properties(0).multi_processor_count
def _set_tma_allocator():
def _alloc(size, align, stream):
return torch.empty(size, device="cuda", dtype=torch.int8)
triton.set_allocator(_alloc)
_set_tma_allocator()
# --------------------------------------------------------------------------
# Pad kernel: zero-pad a 2D (rows, K) fp8 tensor along dim=1 to (rows, K_pad).
# --------------------------------------------------------------------------
@triton.jit
def _pad_kernel(src, dst, ROWS, K, K_PAD,
stride_sm, stride_sk, stride_dm, stride_dk,
BLOCK_M: tl.constexpr, BLOCK_K: tl.constexpr):
pid = tl.program_id(0)
num_k = tl.cdiv(K_PAD, BLOCK_K)
pid_m = pid // num_k
pid_k = pid % num_k
rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
rk = pid_k * BLOCK_K + tl.arange(0, BLOCK_K)
mmask = rm < ROWS
valid = (rk < K)[None, :] # src region (zero elsewhere)
dstk = (rk < K_PAD)[None, :] # stay inside dst row (no wraparound)
gptr = src + rm[:, None] * stride_sm + rk[None, :] * stride_sk
vals = tl.load(gptr, mask=mmask[:, None] & valid, other=0.0)
dptr = dst + rm[:, None] * stride_dm + rk[None, :] * stride_dk
tl.store(dptr, vals, mask=mmask[:, None] & dstk)
def _pad_k(t: torch.Tensor, K: int, K_pad: int) -> torch.Tensor:
if K_pad == K:
return t
rows = t.shape[0]
out = torch.empty((rows, K_pad), dtype=t.dtype, device=t.device)
# K_pad is always a multiple of 128, so BLOCK_K=128 tiles it exactly.
BLOCK_M, BLOCK_K = 32, 128
grid = (triton.cdiv(rows, BLOCK_M) * triton.cdiv(K_pad, BLOCK_K),)
_pad_kernel[grid](t, out, rows, K, K_pad,
t.stride(0), t.stride(1), out.stride(0), out.stride(1),
BLOCK_M=BLOCK_M, BLOCK_K=BLOCK_K, num_warps=4)
return out
# --------------------------------------------------------------------------
# Persistent TMA FP8 GEMM kernel.
# --------------------------------------------------------------------------
@triton.jit
def _compute_pid(tile_id, num_pid_in_group, num_pid_m, GROUP_M, NUM_SMS):
group_id = tile_id // num_pid_in_group
first_pid_m = group_id * GROUP_M
group_size_m = min(num_pid_m - first_pid_m, GROUP_M)
pid_m = first_pid_m + (tile_id % group_size_m)
pid_n = (tile_id % num_pid_in_group) // group_size_m
return pid_m, pid_n
def _gemm_configs():
cfgs = []
# Compute-bound: 128x256 subtile epilogue is the sweet spot on B200.
for ns in (3, 4):
for gm in (8,):
cfgs.append(triton.Config(
{"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 128, "GROUP_M": gm,
"EPILOGUE_SUBTILE": True, "NUM_SMS": NUM_SMS}, num_warps=8, num_stages=ns))
cfgs.append(triton.Config(
{"BLOCK_M": 128, "BLOCK_N": 128, "BLOCK_K": 128, "GROUP_M": gm,
"EPILOGUE_SUBTILE": False, "NUM_SMS": NUM_SMS}, num_warps=8, num_stages=ns))
# Skinny / decode (small M).
for bm in (32, 64):
for bn in (128, 256):
for nw in (4, 8):
epi = bn >= 256
cfgs.append(triton.Config(
{"BLOCK_M": bm, "BLOCK_N": bn, "BLOCK_K": 128, "GROUP_M": 8,
"EPILOGUE_SUBTILE": epi, "NUM_SMS": NUM_SMS}, num_warps=nw, num_stages=4))
return cfgs
@triton.autotune(configs=_gemm_configs(), key=["M", "N", "K"])
@triton.jit
def _fp8_gemm_kernel(
a_ptr, b_ptr, c_ptr, scale_ptr,
M, N, K,
BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
GROUP_M: tl.constexpr, EPILOGUE_SUBTILE: tl.constexpr, NUM_SMS: tl.constexpr,
):
"""C[M,N] = (A[M,K] @ weight.T) * scale[N], A=x fp8, B=weight (N,K) fp8."""
start_pid = tl.program_id(axis=0)
num_pid_m = tl.cdiv(M, BLOCK_M)
num_pid_n = tl.cdiv(N, BLOCK_N)
k_tiles = tl.cdiv(K, BLOCK_K)
num_tiles = num_pid_m * num_pid_n
a_desc = tl.make_tensor_descriptor(a_ptr, shape=[M, K], strides=[K, 1], block_shape=[BLOCK_M, BLOCK_K])
b_desc = tl.make_tensor_descriptor(b_ptr, shape=[N, K], strides=[K, 1], block_shape=[BLOCK_N, BLOCK_K])
c_desc = tl.make_tensor_descriptor(c_ptr, shape=[M, N], strides=[N, 1],
block_shape=[BLOCK_M, BLOCK_N if not EPILOGUE_SUBTILE else BLOCK_N // 2])
tile_id_c = start_pid - NUM_SMS
num_pid_in_group = GROUP_M * num_pid_n
for tile_id in tl.range(start_pid, num_tiles, NUM_SMS, flatten=True, warp_specialize=True):
pid_m, pid_n = _compute_pid(tile_id, num_pid_in_group, num_pid_m, GROUP_M, NUM_SMS)
offs_am = pid_m * BLOCK_M
offs_bn = pid_n * BLOCK_N
accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for ki in range(k_tiles):
offs_k = ki * BLOCK_K
a = a_desc.load([offs_am, offs_k])
b = b_desc.load([offs_bn, offs_k])
accumulator = tl.dot(a, b.T, accumulator)
tile_id_c += NUM_SMS
pid_m, pid_n = _compute_pid(tile_id_c, num_pid_in_group, num_pid_m, GROUP_M, NUM_SMS)
offs_cm = pid_m * BLOCK_M
offs_cn = pid_n * BLOCK_N
if EPILOGUE_SUBTILE:
acc = tl.reshape(accumulator, (BLOCK_M, 2, BLOCK_N // 2))
acc = tl.permute(acc, (0, 2, 1))
acc0, acc1 = tl.split(acc)
cols0 = offs_cn + tl.arange(0, BLOCK_N // 2)
cols1 = offs_cn + BLOCK_N // 2 + tl.arange(0, BLOCK_N // 2)
s0 = tl.load(scale_ptr + cols0)
s1 = tl.load(scale_ptr + cols1)
c0 = (acc0 * s0[None, :]).to(tl.bfloat16)
c_desc.store([offs_cm, offs_cn], c0)
c1 = (acc1 * s1[None, :]).to(tl.bfloat16)
c_desc.store([offs_cm, offs_cn + BLOCK_N // 2], c1)
else:
cols = offs_cn + tl.arange(0, BLOCK_N)
s = tl.load(scale_ptr + cols)
c = (accumulator * s[None, :]).to(tl.bfloat16)
c_desc.store([offs_cm, offs_cn], c)
def _fp8_gemm(x: torch.Tensor, w: torch.Tensor, scale: torch.Tensor,
out: torch.Tensor) -> torch.Tensor:
M, K = x.shape
N = w.shape[0]
grid = (NUM_SMS,)
_fp8_gemm_kernel[grid](x, w, out, scale, M, N, K)
return out
# --------------------------------------------------------------------------
# Split-K path for skinny M (decode-like, memory/latency bound). Each (N-tile,
# K-split) CTA writes an fp32 partial; a reduction kernel sums splits, applies
# the per-channel scale, and casts to bf16. Splitting K multiplies the CTA count
# so all SMs stay busy streaming the weight from HBM.
# --------------------------------------------------------------------------
@triton.jit
def _splitk_kernel(
a_ptr, b_ptr, part_ptr, M, N, K,
K_PER_SPLIT: tl.constexpr,
BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
):
pid = tl.program_id(0)
num_n = tl.cdiv(N, BLOCK_N)
split_id = pid // num_n
pid_n = pid % num_n
k_start = split_id * K_PER_SPLIT
k_end = k_start + K_PER_SPLIT
if k_end > K:
k_end = K
offs_am = tl.arange(0, BLOCK_M)
offs_bn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
offs_k = tl.arange(0, BLOCK_K)
a_ptrs = a_ptr + offs_am[:, None] * K + offs_k[None, :]
b_ptrs = b_ptr + offs_bn[:, None] * K + offs_k[None, :]
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for ki in range(k_start, k_end, BLOCK_K):
kk = ki + offs_k
a = tl.load(a_ptrs + ki[None, :], mask=kk[None, :] < k_end, other=0.0)
b = tl.load(b_ptrs + ki[None, :], mask=kk[:, None] < k_end, other=0.0)
acc = tl.dot(a, b.T, acc)
part_ptrs = part_ptr + split_id * M * N + offs_am[:, None] * N + offs_bn[None, :]
tl.store(part_ptrs, acc)
@triton.jit
def _splitk_reduce(part_ptr, c_ptr, scale_ptr, M, N,
SK: tl.constexpr, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr):
pid_m = tl.program_id(0)
pid_n = tl.program_id(1)
rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for s in tl.static_range(0, SK):
p = part_ptr + s * M * N + rm[:, None] * N + rn[None, :]
acc += tl.load(p)
sc = tl.load(scale_ptr + rn)
out = (acc * sc[None, :]).to(tl.bfloat16)
tl.store(c_ptr + rm[:, None] * N + rn[None, :], out)
def _choose_sk(M, N, K, BN, BK):
"""Pick a K split count so num_n * SK ~= 2-3x NUM_SMS; SK must divide K//BK."""
num_n = (N + BN - 1) // BN
target = max(1, (2 * NUM_SMS) // max(1, num_n))
k_per_bk = K // BK
best, best_sk = 1, 1
for sk in (1, 2, 4, 8, 16, 32):
if k_per_bk % sk != 0:
continue
if abs(sk - target) < abs(best_sk - target):
best_sk, best = sk, sk
return max(1, best_sk)
def _fp8_gemm_splitk(x: torch.Tensor, w: torch.Tensor, scale: torch.Tensor,
part_buf: torch.Tensor, out: torch.Tensor) -> torch.Tensor:
M, K = x.shape
N = w.shape[0]
BM, BN, BK = 32, 128, 128
SK = _choose_sk(M, N, K, BN, BK)
KPS = K // SK
num_n = triton.cdiv(N, BN)
_splitk_kernel[(num_n * SK,)](
x, w, part_buf, M, N, K, K_PER_SPLIT=KPS,
BLOCK_M=BM, BLOCK_N=BN, BLOCK_K=BK, num_warps=4, num_stages=4,
)
_splitk_reduce[(triton.cdiv(M, 32), triton.cdiv(N, 256))](
part_buf, out, scale, M, N, SK=SK, BLOCK_M=32, BLOCK_N=256, num_warps=4,
)
return out
class Model(nn.Module):
def __init__(self, M: int, N: int, K: int):
super().__init__()
self.M, self.N, self.K = M, N, K
w = torch.zeros(N, K, dtype=torch.float8_e4m3fn)
self.register_buffer("weight", w)
self.register_buffer("weight_scale", torch.ones(N, dtype=torch.float32))
self._w_pad = None
self._w_version = -1
self._part_buf = None
self._out_buf = None
self._out_shape = None
def _get_out(self, M, N, device):
if self._out_buf is None or self._out_shape != (M, N):
self._out_buf = torch.empty((M, N), dtype=torch.bfloat16, device=device)
self._out_shape = (M, N)
return self._out_buf
def forward(self, x: torch.Tensor) -> torch.Tensor:
M, K = x.shape
N = self.N
out = self._get_out(M, N, x.device)
K_pad = ((K + _PAD_MULT - 1) // _PAD_MULT) * _PAD_MULT
if K_pad != K:
x = _pad_k(x, K, K_pad)
if self.weight._version != self._w_version or self._w_pad is None:
self._w_pad = _pad_k(self.weight, K, K_pad)
self._w_version = self.weight._version
w = self._w_pad
else:
w = self.weight
# Skinny M (decode-like, M<=32): split-K to saturate HBM bandwidth.
# Split-K kernel uses BLOCK_M=32 so it covers all M rows here.
if M <= 32:
BN, BK = 128, 128
SK = _choose_sk(M, N, K, BN, BK)
if self._part_buf is None or self._part_buf.shape != (SK, M, N):
self._part_buf = torch.empty((SK, M, N), dtype=torch.float32, device=x.device)
return _fp8_gemm_splitk(x, w, self.weight_scale, self._part_buf, out)
return _fp8_gemm(x, w, self.weight_scale, out)
M = 4096
N = 4096
K = 4096
def get_inputs():
x = (torch.rand(M, K) * 8 - 4).to(torch.float8_e4m3fn)
return [x]
def get_init_inputs():
return [M, N, K]
20260620_111911_zai-claude_glm-5.2_01_fp8_gemm