kernelbench.com

KernelBench hard · H100

FP8 GEMM MiniMax-M3

11.8%geomean peak fraction across shapes
harnessminimax-claudeagent session45mtotal wall48mcheck69sbenchmark2moutput tokensgpu-lock wait2mgpu-lock held66sregimecompute

Per-shape vs governing ceilingeach shape graded against whichever binds — fp8 compute or HBM bandwidth

4096×4096×40960.218 ms41.7%631 TFLOPS · 42% of 1,513 TF fp8 peak · also 0.31 TB/s (15% of HBM)
4096×4096×41272.793 ms3.3%50 TFLOPS · 3% of 1,513 TF fp8 peak · also 0.02 TB/s (1% of HBM)
32×8192×81920.095 ms3.0%0.71 TB/s · 35% of 2.0 TB/s HBM · also 45 TFLOPS (3% of compute)
4096×14336×40960.658 ms48.3%731 TFLOPS · 48% of 1,513 TF fp8 peak · also 0.29 TB/s (14% of HBM)

compute-bound memory-bound · bar + right column = official fraction of the ceiling (the geomean input)

geomean(41.7% · 3.3% · 3.0% · 48.3%) = 11.9%

Kernel source (redacted)
"""FP8 e4m3 GEMM solution — Triton kernel.

y = ((x @ w.T) * weight_scale).to(bf16)  where x, w are fp8_e4m3.

This implements a real fp8 x fp8 tensor-core MMA via Triton tl.dot
(fp32 accumulate) and applies the per-output-channel dequant scale.
"""
import torch
import torch.nn as nn
import triton
import triton.language as tl


# --- Triton FP8 GEMM kernel ----------------------------------------------------
def _gen_configs():
    cfgs = []
    # Standard compute-bound large tiles.
    for bm, bn, bk, ns, nw in [
        (128, 128, 128, 4, 8),
        (128, 256, 128, 3, 8),
        (256, 128, 128, 3, 8),
        (128, 128, 64,  4, 4),
        (256, 256, 64,  3, 8),
        (128, 256, 64,  3, 8),
        (256, 128, 64,  3, 8),
        (128, 128, 128, 3, 4),
        (64,  128, 128, 4, 4),
        (128, 64,  128, 4, 4),
    ]:
        cfgs.append(triton.Config({"BLOCK_M": bm, "BLOCK_N": bn, "BLOCK_K": bk, "GROUP_M": 8},
                                  num_stages=ns, num_warps=nw))
    # Skinny-M (decode): BLOCK_M small or padded.
    for bm, bn, bk, ns, nw in [
        (16,  256, 128, 4, 4),
        (16,  128, 128, 4, 4),
        (32,  256, 128, 4, 8),
        (32,  128, 128, 4, 4),
        (64,  256, 128, 4, 8),
    ]:
        cfgs.append(triton.Config({"BLOCK_M": bm, "BLOCK_N": bn, "BLOCK_K": bk, "GROUP_M": 8},
                                  num_stages=ns, num_warps=nw))
    # Wider GROUP_M variants for better L2 reuse.
    for bm, bn, bk, ns, nw in [
        (128, 128, 128, 4, 8),
        (256, 128, 128, 3, 8),
    ]:
        for gm in (4, 16):
            cfgs.append(triton.Config({"BLOCK_M": bm, "BLOCK_N": bn, "BLOCK_K": bk, "GROUP_M": gm},
                                      num_stages=ns, num_warps=nw))
    return cfgs


@triton.autotune(
    configs=_gen_configs(),
    key=["M", "N", "K"],
)
@triton.jit
def fp8_gemm_kernel(
    A_ptr, B_ptr, C_ptr, S_ptr,
    M, N, K,
    sa, sb,
    stride_am, stride_ak,
    stride_bn, stride_bk,
    stride_cm, stride_cn,
    BLOCK_M: tl.constexpr,
    BLOCK_N: tl.constexpr,
    BLOCK_K: tl.constexpr,
    GROUP_M: tl.constexpr,
):
    pid = tl.program_id(axis=0)
    num_pid_m = tl.cdiv(M, BLOCK_M)
    num_pid_n = tl.cdiv(N, BLOCK_N)
    # Group order for L2 reuse: walk BLOCK_M groups of BLOCK_M*GROUP_M rows,
    # with all BLOCK_N tiles of each row-band, before moving to the next.
    num_pid_in_group = GROUP_M * num_pid_n
    group_id = pid // 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 + ((pid % num_pid_in_group) % group_size_m)
    pid_n = (pid % num_pid_in_group) // group_size_m

    offs_am = (pid_m * BLOCK_M + tl.arange(0, BLOCK_M)) % M
    offs_bn = (pid_n * BLOCK_N + tl.arange(0, BLOCK_N)) % N
    offs_k = tl.arange(0, BLOCK_K)

    a_ptrs = A_ptr + offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak
    b_ptrs = B_ptr + offs_bn[:, None] * stride_bn + offs_k[None, :] * stride_bk

    acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
    for k in range(0, tl.cdiv(K, BLOCK_K)):
        # Masked loads for the final partial tile.
        a = tl.load(a_ptrs, mask=offs_k[None, :] < K - k * BLOCK_K, other=0.0)
        b = tl.load(b_ptrs, mask=offs_k[None, :] < K - k * BLOCK_K, other=0.0)
        # fp8 x fp8 -> fp32 accumulate.
        acc = tl.dot(a, b.T, acc, out_dtype=tl.float32)
        a_ptrs += BLOCK_K * stride_ak
        b_ptrs += BLOCK_K * stride_bk

    # Per-output-channel dequant: scale (M, N) by weight_scale (N,).
    offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
    scale = tl.load(S_ptr + offs_n, mask=offs_n < N, other=0.0)
    acc = acc * scale[None, :]

    # Write as bf16.
    offs_cm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
    offs_cn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
    c_ptrs = C_ptr + offs_cm[:, None] * stride_cm + offs_cn[None, :] * stride_cn
    c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
    tl.store(c_ptrs, acc.to(tl.bfloat16), mask=c_mask)


def fp8_gemm_triton(x: torch.Tensor, weight: torch.Tensor, weight_scale: torch.Tensor) -> torch.Tensor:
    """x: (M, K) fp8_e4m3; weight: (N, K) fp8_e4m3; weight_scale: (N,) f32.
    Returns bf16 (M, N).
    """
    assert x.is_cuda and weight.is_cuda
    M, K = x.shape
    N = weight.shape[0]
    assert weight.shape == (N, K)
    assert weight_scale.shape == (N,)

    # Make sure layouts are right (x is row-major, weight row-major, output row-major).
    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, weight, y, weight_scale,
        M, N, K,
        1.0, 1.0,  # scales (unused — kernel takes fp8 directly and applies weight_scale)
        x.stride(0), x.stride(1),
        weight.stride(0), weight.stride(1),
        y.stride(0), y.stride(1),
    )
    return y


# --- Module: mirrors reference.Model interface -------------------------------
class Model(nn.Module):
    """y = ((x @ w.T) * weight_scale).to(bf16).

    Same buffers as reference.Model so the graded state_dict loads.
    """

    def __init__(self, M: int, N: int, K: int):
        super().__init__()
        self.M, self.N, self.K = M, N, K
        self.register_buffer("weight", torch.empty(N, K, dtype=torch.float8_e4m3fn))
        self.register_buffer("weight_scale", torch.empty(N, dtype=torch.float32))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return fp8_gemm_triton(x, self.weight, self.weight_scale)


# Local module-level shims (reference.py style — overwritten by benchmark/check).
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]

20260618_064748_minimax-claude_MiniMax-M3_01_fp8_gemm