KernelBench hard · H100
FP8 GEMM Tencent Hy3
19.0%geomean peak fraction across shapes
agent session47mtotal wall51mcheck70sbenchmark2moutput tokens—gpu-lock wait2mgpu-lock held54sregimecompute
Per-shape vs governing ceilingeach shape graded against whichever binds — fp8 compute or HBM bandwidth
4096×4096×40960.242 ms37.5%568 TFLOPS · 38% of 1,513 TF fp8 peak · also 0.28 TB/s (14% of HBM)
4096×4096×41270.332 ms27.5%417 TFLOPS · 28% of 1,513 TF fp8 peak · also 0.20 TB/s (10% of HBM)
32×8192×81920.112 ms2.5%0.60 TB/s · 30% of 2.0 TB/s HBM · also 38 TFLOPS (3% of compute)
4096×14336×40960.639 ms49.7%752 TFLOPS · 50% of 1,513 TF fp8 peak · also 0.30 TB/s (15% of HBM)
compute-bound memory-bound · bar + right column = official fraction of the ceiling (the geomean input)
geomean(37.5% · 27.5% · 2.5% · 49.7%) = 19.0%
Kernel source (redacted)
"""FP8 e4m3 GEMM using Triton fp8 tensor-core MMA.
y = ((x @ w.T) * weight_scale).to(bf16)
x: fp8_e4m3 (M, K). w: fp8_e4m3 (N, K) normalized to the e4m3 range.
weight_scale: (N,) per-output-channel dequant scale.
Notes:
* A real fp8 x fp8 MMA (fp8 inputs, fp32 accumulate) is run via tl.dot.
* K is padded to a multiple of 128 so every K-tile is full, keeping the fp8
MMA codegen on the fast path (partial-K tail tiles generate slow code on
non-128-multiple K shapes such as 4127).
* The per-channel dequant scale is fused into the store to avoid a second
pass over the output.
"""
import torch
import triton
import triton.language as tl
OP_TYPE = "gemm"
SUPPORTED_PRECISIONS = ["fp8_e4m3"]
HARDWARE_REQUIRED = ["RTX_PRO_6000", "H100", "B200"]
E4M3_MAX = 448.0
@triton.autotune(
configs=[
triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 128}, num_warps=4, num_stages=3),
triton.Config({"BLOCK_M": 128, "BLOCK_N": 128, "BLOCK_K": 256}, num_warps=4, num_stages=3),
triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 128}, num_warps=4, num_stages=3),
triton.Config({"BLOCK_M": 128, "BLOCK_N": 128, "BLOCK_K": 128}, num_warps=4, num_stages=3),
triton.Config({"BLOCK_M": 256, "BLOCK_N": 256, "BLOCK_K": 128}, num_warps=8, num_stages=3),
triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 256}, num_warps=8, num_stages=3),
triton.Config({"BLOCK_M": 64, "BLOCK_N": 256, "BLOCK_K": 128}, num_warps=4, num_stages=3),
triton.Config({"BLOCK_M": 128, "BLOCK_N": 128, "BLOCK_K": 128}, num_warps=4, num_stages=5),
triton.Config({"BLOCK_M": 64, "BLOCK_N": 128, "BLOCK_K": 128}, num_warps=4, num_stages=3),
triton.Config({"BLOCK_M": 256, "BLOCK_N": 256, "BLOCK_K": 256}, num_warps=8, num_stages=4),
triton.Config({"BLOCK_M": 128, "BLOCK_N": 512, "BLOCK_K": 128}, num_warps=8, num_stages=3),
triton.Config({"BLOCK_M": 256, "BLOCK_N": 512, "BLOCK_K": 128}, num_warps=8, num_stages=3),
triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 128}, num_warps=4, num_stages=5),
triton.Config({"BLOCK_M": 512, "BLOCK_N": 256, "BLOCK_K": 128}, num_warps=8, num_stages=4),
triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 256}, num_warps=8, num_stages=4),
],
key=["M", "N", "K"],
)
@triton.jit
def _fp8_gemm_kernel(
x_ptr, w_ptr, y_ptr, scale_ptr,
M, N, K,
stride_xm, stride_xk,
stride_wn, stride_wk,
stride_ym, stride_yn,
BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
):
pid_m = tl.program_id(0)
pid_n = tl.program_id(1)
offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
offs_k = tl.arange(0, BLOCK_K)
x_ptrs = x_ptr + offs_m[:, None] * stride_xm + offs_k[None, :] * stride_xk
w_ptrs = w_ptr + offs_n[:, None] * stride_wn + offs_k[None, :] * stride_wk
# K is padded to a multiple of BLOCK_K by the caller, so every tile is full
# and the fp8 MMA codegen stays on the fast path (no partial-K tail tile).
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for k in range(0, tl.cdiv(K, BLOCK_K)):
x = tl.load(x_ptrs, mask=offs_m[:, None] < M, other=0.0).to(tl.float8e4nv)
w = tl.load(w_ptrs, mask=offs_n[:, None] < N, other=0.0).to(tl.float8e4nv)
acc = tl.dot(x, tl.trans(w), acc, out_dtype=tl.float32)
x_ptrs += BLOCK_K * stride_xk
w_ptrs += BLOCK_K * stride_wk
# Fused per-channel dequant scale.
scale = tl.load(scale_ptr + offs_n, mask=offs_n < N).to(tl.float32)
acc = acc * scale[None, :]
y = acc.to(tl.bfloat16)
y_ptrs = y_ptr + offs_m[:, None] * stride_ym + offs_n[None, :] * stride_yn
tl.store(y_ptrs, y, mask=(offs_m[:, None] < M) & (offs_n[None, :] < N))
class Model(torch.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)
torch.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))
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.contiguous()
w = self.weight # (N, K) fp8
M, N, K = self.M, self.N, self.K
# Pad K to a multiple of 128 so every K-tile is full (avoids the slow
# partial-K fp8 MMA codegen on non-128-multiple K shapes like 4127).
Kpad = (K + 127) // 128 * 128
if Kpad != K:
x = torch.nn.functional.pad(x, (0, Kpad - K))
w = torch.nn.functional.pad(w, (0, Kpad - K))
y = torch.empty(M, N, dtype=torch.bfloat16, device=x.device)
grid = lambda meta: (
triton.cdiv(M, meta["BLOCK_M"]),
triton.cdiv(N, meta["BLOCK_N"]),
)
_fp8_gemm_kernel[grid](
x, w, y, self.weight_scale,
M, N, Kpad,
x.stride(0), x.stride(1),
w.stride(0), w.stride(1),
y.stride(0), y.stride(1),
)
return y
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]
20260709_090111_hy3_hy3_01_fp8_gemm