KernelBench hard · H100
FP8 GEMM Qwen 3.8 Max
manually audited: clean
Genuine custom Hopper fp8 GEMM. The final solution launches Triton tl.dot on float8_e4m3fn operands with fp32 accumulators, applies the live per-output weight scale, and writes bf16. Large aligned shapes use a persistent TMA kernel; K=4127 uses explicit zero-padding copies before the same GEMM; M<=64 uses a transposed split-K GEMM with a zeroed fp32 atomic workspace and a scale/convert epilogue. There is no vendor GEMM call, constant output, benchmark/check discriminator, output memoization, CUDA graph, identity-keyed reuse, or fake-compute path. The publish-grade isolated regrade records peak_fraction=0.2362 and correct=true.
Per-shape vs governing ceilingeach shape graded against whichever binds — fp8 compute or HBM bandwidth
compute-bound memory-bound · bar + right column = official fraction of the ceiling (the geomean input)
geomean(47.7% · 40.9% · 2.6% · 60.9%) = 23.6%
Kernel source (redacted)
"""FP8 e4m3 GEMM solution: y = ((x @ w.T) * weight_scale).to(bf16).
Custom Triton kernels running real fp8 x fp8 WGMMA tensor-core MMAs
(fp8_e4m3 inputs, fp32 accumulate) on Hopper SM90:
* Large / aligned shapes: persistent TMA kernel with grouped tile ordering;
per-channel scale applied in the epilogue.
* Odd K (row stride not 16-byte aligned, which TMA requires): fuse a
zero-padding copy into aligned buffers, then run the same TMA kernel.
* Skinny M (decode-like, memory-bound): transposed-operand split-K kernel
(w as the M-tiled operand so WGMMA gets full m64 tiles) with fp32 atomic
accumulation into a workspace plus a tiny scale+convert epilogue kernel.
"""
import torch
import torch.nn as nn
import triton
import triton.language as tl
from triton.tools.tensor_descriptor import TensorDescriptor
E4M3_MAX = 448.0
# ---------------------------------------------------------------------------
# Kernel 1: persistent TMA fp8 GEMM (requires 16-byte-aligned row strides).
# ---------------------------------------------------------------------------
@triton.jit
def _fp8_gemm_tma(
a_desc, b_desc, c_desc, S,
M, N, K,
BM: tl.constexpr, BN: tl.constexpr, BK: tl.constexpr,
GROUP_M: tl.constexpr, NUM_SMS: tl.constexpr,
):
start_pid = tl.program_id(0)
num_pid_m = tl.cdiv(M, BM)
num_pid_n = tl.cdiv(N, BN)
k_tiles = tl.cdiv(K, BK)
num_tiles = num_pid_m * num_pid_n
for tile_id in tl.range(start_pid, num_tiles, NUM_SMS, flatten=True):
num_pid_in_group = GROUP_M * num_pid_n
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
off_m = pid_m * BM
off_n = pid_n * BN
acc = tl.zeros((BM, BN), dtype=tl.float32)
for ki in range(k_tiles):
a = a_desc.load([off_m, ki * BK])
b = b_desc.load([off_n, ki * BK])
acc = tl.dot(a, b.T, acc)
rn = off_n + tl.arange(0, BN)
scale = tl.load(S + rn, mask=rn < N, other=0.0)
acc = acc * scale[None, :]
c_desc.store([off_m, off_n], acc.to(tl.bfloat16))
# ---------------------------------------------------------------------------
# Kernel 2: zero-padding copy (for K whose row stride is not 16-byte aligned).
# ---------------------------------------------------------------------------
@triton.jit
def _pad_copy(SRC, DST, K, KPAD, BLK: tl.constexpr):
pid = tl.program_id(0)
nblk = tl.cdiv(KPAD, BLK)
row = pid // nblk
col = (pid % nblk) * BLK + tl.arange(0, BLK)
v = tl.load(SRC + row * K + col, mask=col < K, other=0.0)
tl.store(DST + row * KPAD + col, v, mask=col < KPAD)
# ---------------------------------------------------------------------------
# Kernel 3: transposed split-K fp8 GEMM for skinny M (TMA loads).
# Computes y.T tiles: (n tile) x (all m) so the WGMMA M dimension is large.
# ---------------------------------------------------------------------------
@triton.jit
def _fp8_gemm_skinny(
w_desc, x_desc, WS,
M, N, K,
SPLIT_K: tl.constexpr,
BM: tl.constexpr, BN: tl.constexpr, BK: tl.constexpr,
):
pid = tl.program_id(0)
num_pid_n = tl.cdiv(N, BM)
pid_n = pid % num_pid_n
pid_k = pid // num_pid_n
k_per_split = tl.cdiv(K, SPLIT_K * BK) * BK
k_start = pid_k * k_per_split
k_end = min(k_start + k_per_split, K)
off_n = pid_n * BM
acc = tl.zeros((BM, BN), dtype=tl.float32)
for k in range(k_start, k_end, BK):
a = w_desc.load([off_n, k]) # (BM, BK) tile of w
b = x_desc.load([0, k]) # (BN, BK) tile of x
acc = tl.dot(a, b.T, acc)
rn = off_n + tl.arange(0, BM)
rm = tl.arange(0, BN)
mask = rn[:, None] < N
WS_ptr = WS + rn[:, None] * M + rm[None, :]
tl.atomic_add(WS_ptr, acc, mask=mask, sem="relaxed")
@triton.jit
def _epilogue_skinny(WS, Y, S, M, N, BM: tl.constexpr, BN: tl.constexpr):
pid = tl.program_id(0)
num_pid_n = tl.cdiv(N, BN)
pid_m = pid // num_pid_n
pid_n = pid % num_pid_n
rm = pid_m * BM + tl.arange(0, BM)
rn = pid_n * BN + tl.arange(0, BN)
mask = (rm[:, None] < M) & (rn[None, :] < N)
v = tl.load(WS + rn[None, :] * M + rm[:, None], mask=mask, other=0.0)
sc = tl.load(S + rn, mask=rn < N, other=0.0)
tl.store(Y + rm[:, None] * N + rn[None, :], (v * sc[None, :]).to(tl.bfloat16), mask=mask)
def _tma_aligned(t: torch.Tensor) -> bool:
# TMA requires every stride except the innermost to be a multiple of 16 bytes.
return t.stride(0) % 16 == 0 and t.data_ptr() % 16 == 0
# Hand-tuned configs on H100 PCIe (BM, BN, BK, GROUP_M, num_warps, num_stages).
_LARGE_CONFIGS = {
(4096, 4096, 4096): (128, 128, 128, 8, 4, 4),
(4096, 14336, 4096): (128, 256, 128, 16, 8, 3),
}
# (BM, BN(tile over M), BK, SPLIT_K, num_warps, num_stages)
_SKINNY_CONFIGS = {
(32, 8192, 8192): (128, 32, 256, 4, 4, 3),
}
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.empty(N, K, dtype=torch.bfloat16)
nn.init.normal_(w, std=0.02)
s = (w.float().abs().amax(dim=1, keepdim=True) / E4M3_MAX).clamp(min=1e-12)
w_fp8 = (w.float() / s).to(torch.float8_e4m3fn)
self.register_buffer("weight", w_fp8)
self.register_buffer("weight_scale", s.squeeze(1).to(torch.float32))
self._workspace = None
self._pad_bufs = None
self._NUM_SMS = None
def _num_sms(self, device):
if self._NUM_SMS is None:
self._NUM_SMS = torch.cuda.get_device_properties(device).multi_processor_count
return self._NUM_SMS
def forward(self, x: torch.Tensor) -> torch.Tensor:
M, K = x.shape
N = self.weight.shape[0]
w = self.weight
ws = self.weight_scale
y = torch.empty(M, N, device=x.device, dtype=torch.bfloat16)
if M <= 64:
return self._forward_skinny(x, w, ws, y, M, N, K)
if _tma_aligned(x) and _tma_aligned(w):
return self._forward_tma(x, w, ws, y, M, N, K)
return self._forward_padded(x, w, ws, y, M, N, K)
# -- large aligned -------------------------------------------------------
def _large_config(self, M, N, K):
cfg = _LARGE_CONFIGS.get((M, N, K))
if cfg is not None:
return cfg
if N >= 8192:
return (128, 256, 128, 16, 8, 3)
return (128, 128, 128, 8, 4, 4)
def _forward_tma(self, x, w, ws, y, M, N, K):
BM, BN, BK, GROUP_M, num_warps, num_stages = self._large_config(M, N, K)
a_desc = TensorDescriptor.from_tensor(x, [BM, BK])
b_desc = TensorDescriptor.from_tensor(w, [BN, BK])
c_desc = TensorDescriptor.from_tensor(y, [BM, BN])
NUM_SMS = self._num_sms(x.device)
grid = (min(NUM_SMS, triton.cdiv(M, BM) * triton.cdiv(N, BN)),)
_fp8_gemm_tma[grid](
a_desc, b_desc, c_desc, ws, M, N, K,
BM, BN, BK, GROUP_M, NUM_SMS,
num_warps=num_warps, num_stages=num_stages,
)
return y
# -- odd K: pad-copy into aligned buffers, then TMA -----------------------
def _forward_padded(self, x, w, ws, y, M, N, K):
KPAD = ((K + 127) // 128) * 128
bufs = self._pad_bufs
if bufs is None or bufs[0].shape[0] != M or bufs[0].shape[1] != KPAD or bufs[1].shape[0] != N:
xp = torch.empty(M, KPAD, dtype=torch.float8_e4m3fn, device=x.device)
wp = torch.empty(N, KPAD, dtype=torch.float8_e4m3fn, device=x.device)
bufs = (xp, wp)
self._pad_bufs = bufs
xp, wp = bufs
BLK = 1024
_pad_copy[(M * triton.cdiv(KPAD, BLK),)](x, xp, K, KPAD, BLK, num_warps=4)
_pad_copy[(N * triton.cdiv(KPAD, BLK),)](w, wp, K, KPAD, BLK, num_warps=4)
return self._forward_tma(xp, wp, ws, y, M, N, K)
# -- skinny ----------------------------------------------------------------
def _forward_skinny(self, x, w, ws, y, M, N, K):
cfg = _SKINNY_CONFIGS.get((self.M, self.N, self.K)) or (128, max(32, M), 256, 4, 4, 3)
BM, BN, BK, SPLIT_K, num_warps, num_stages = cfg
BN = max(BN, triton.next_power_of_2(M))
need = N * M
ws_buf = self._workspace
if ws_buf is None or ws_buf.numel() < need:
ws_buf = torch.empty(need, dtype=torch.float32, device=x.device)
self._workspace = ws_buf
ws_buf = ws_buf[:need].view(N, M)
ws_buf.zero_()
w_desc = TensorDescriptor.from_tensor(w, [BM, BK])
x_desc = TensorDescriptor.from_tensor(x, [BN, BK])
grid = (triton.cdiv(N, BM) * SPLIT_K,)
_fp8_gemm_skinny[grid](
w_desc, x_desc, ws_buf, M, N, K,
SPLIT_K, BM, BN, BK,
num_warps=num_warps, num_stages=num_stages,
)
EBM, EBN = 32, 128
egrid = (triton.cdiv(M, EBM) * triton.cdiv(N, EBN),)
_epilogue_skinny[egrid](ws_buf, y, ws, M, N, EBM, EBN, num_warps=4)
return y
def get_inputs():
x = (torch.rand(M, K) * 8 - 4).to(torch.float8_e4m3fn)
return [x]
def get_init_inputs():
return [M, N, K]
M = 4096
N = 4096
K = 4096
20260803_034407_qwen-claude_qwen3.8-max_01_fp8_gemm