KernelBench hard · H100
W4A16 GEMM MiniMax-M3
10.1%geomean peak fraction across shapes
harnessminimax-claudeagent session45mtotal wall51mcheck3mbenchmark3moutput tokens—gpu-lock wait6mgpu-lock held13sregimememory
Per-shape vs governing ceilingeach shape graded against whichever binds — bf16 compute or HBM bandwidth
1×12288×40960.101 ms13.0%0.27 TB/s · 13% of 2.0 TB/s HBM · also 1 TFLOPS (0% of compute)
32×12288×40960.111 ms12.3%0.25 TB/s · 12% of 2.0 TB/s HBM · also 29 TFLOPS (4% of compute)
256×12288×40960.200 ms8.6%129 TFLOPS · 17% of 756 TF bf16 peak · also 0.18 TB/s (9% of HBM)
1×4096×40960.093 ms4.7%0.10 TB/s · 5% of 2.0 TB/s HBM · also 0 TFLOPS (0% of compute)
16×14336×40960.099 ms15.7%0.32 TB/s · 16% of 2.0 TB/s HBM · also 19 TFLOPS (3% of compute)
compute-bound memory-bound · bar + right column = official fraction of the ceiling (the geomean input)
geomean(13.0% · 12.3% · 8.6% · 4.7% · 15.7%) = 10.0%
Kernel source (redacted)
"""W4A16 weight-only quantized GEMM (Triton, fused unpack + matmul).
Scheme: AWQ/GPTQ-style asymmetric int4 with explicit zero-points and per-group
bf16 scales. Inputs:
x: (M, K) bf16
w_q: (K//2, N) uint8 -- two int4 weights packed per byte
-- (low nibble = even-K row, high = odd)
scales: (K//group, N) bf16
zeros: (K//group, N) bf16 -- stored as float zero-point
out: (M, N) bf16
Dequant (per group along K):
w_bf[k, n] = (w_q_unpacked[k, n] - zeros[k // group, n]) * scales[k // group, n]
We never call any prebuilt W4A16 kernel (marlin/bnb/etc.) - the unpack+matmul
is fused in one Triton kernel.
"""
from __future__ import annotations
import torch
import triton
import triton.language as tl
GROUP_SIZE = 128
# ---------------------------------------------------------------------------
# Triton kernel
# ---------------------------------------------------------------------------
# Tile sizes:
# BLOCK_M -- rows of the output per program (handles M=1 with masking)
# BLOCK_N -- columns of the output per program
# BLOCK_K -- K reduction block. We set this == GROUP_SIZE so each iteration
# is one quant group, which lets us load scales/zeros once and
# dequant a whole tile.
#
# Inner dot: x_even/w_bf_lo and x_odd/w_bf_hi are each (M, 64) @ (64, N), so
# the inner reduction is 64 (K-axis is the *post-split* K, i.e. BLOCK_K/2).
# Hopper WGMMA needs K in {16, 32, 64, ...}, so 64 is fine.
# ---------------------------------------------------------------------------
@triton.jit
def _w4a16_gemm_kernel(
x_ptr, w_ptr, scales_ptr, zeros_ptr, out_ptr,
M, N, K,
stride_xm, stride_xk,
stride_wk, stride_wn,
stride_sk, stride_sn,
stride_zk, stride_zn,
stride_om, stride_on,
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
BLOCK_K: tl.constexpr,
GROUP_SIZE: 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)
HALF_K: tl.constexpr = BLOCK_K // 2
n_groups: tl.constexpr = 0 # set at runtime via K
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
# Pre-build stride-scaled base pointers.
x_row_base = x_ptr + offs_m[:, None] * stride_xm
w_base = w_ptr + offs_n[None, :] * stride_wn
n_groups = K // BLOCK_K
for g in range(0, n_groups):
k_start = g * BLOCK_K
kp_start = g * HALF_K
# ---- x tile (BLOCK_M, BLOCK_K) bf16, fully contiguous in K ----
x = tl.load(
x_row_base + (k_start + tl.arange(0, BLOCK_K))[None, :] * stride_xk,
mask=offs_m[:, None] < M,
other=0.0,
)
# ---- packed weights (HALF_K, BLOCK_N) uint8 ----
w_packed = tl.load(
w_base + (kp_start + tl.arange(0, HALF_K))[:, None] * stride_wk
)
# ---- unpack + dequant ----
w_lo = (w_packed & 0xF).to(tl.bfloat16)
w_hi = (w_packed >> 4).to(tl.bfloat16)
scales = tl.load(scales_ptr + g * stride_sk + offs_n * stride_sn)
zeros_ = tl.load(zeros_ptr + g * stride_zk + offs_n * stride_zn)
w_bf_lo = (w_lo - zeros_[None, :]) * scales[None, :]
w_bf_hi = (w_hi - zeros_[None, :]) * scales[None, :]
# ---- split x into even/odd K rows via reshape+split ----
# x of shape (M, BLOCK_K=128) -> reshape to (M, 64, 2)
# x_reshape[m, i, c] = x[m, 2i + c] (c is the parity)
# tl.split peels off the size-2 minor dim.
x_r = tl.reshape(x, (BLOCK_M, HALF_K, 2))
x_even, x_odd = tl.split(x_r)
acc = tl.dot(x_even, w_bf_lo, acc)
acc = tl.dot(x_odd, w_bf_hi, acc)
out = acc.to(tl.bfloat16)
tl.store(
out_ptr + offs_m[:, None] * stride_om + offs_n[None, :] * stride_on,
out,
mask=offs_m[:, None] < M,
)
# ---------------------------------------------------------------------------
# Python launch wrapper
# ---------------------------------------------------------------------------
def _pick_block_m(M: int) -> int:
# Tile size choices for the M dim. Triton recompiles per BLOCK_M, so we
# only include a small handful of sizes. For tiny M (decode), pad to 16
# so the WGMMA / mma instructions can use their M=16 m-tile.
if M <= 16:
return 16
if M <= 32:
return 32
if M <= 64:
return 64
return 128
def w4a16_gemm(x: torch.Tensor, w_q: torch.Tensor,
scales: torch.Tensor, zeros: torch.Tensor) -> torch.Tensor:
"""Fused W4A16 GEMM: y = x @ dequant(w_q, scales, zeros)."""
M, K = x.shape
_, N = w_q.shape
out = torch.empty((M, N), dtype=torch.bfloat16, device=x.device)
BLOCK_M = _pick_block_m(M)
BLOCK_N = 128
BLOCK_K = GROUP_SIZE # == 128
grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(N, BLOCK_N))
_w4a16_gemm_kernel[grid](
x, w_q, scales, zeros, out,
M, N, K,
x.stride(0), x.stride(1),
w_q.stride(0), w_q.stride(1),
scales.stride(0), scales.stride(1),
zeros.stride(0), zeros.stride(1),
out.stride(0), out.stride(1),
BLOCK_M=BLOCK_M,
BLOCK_N=BLOCK_N,
BLOCK_K=BLOCK_K,
GROUP_SIZE=GROUP_SIZE,
num_warps=4,
num_stages=3,
)
return out
# ---------------------------------------------------------------------------
# Model + get_inputs / get_init_inputs (must match reference.py interface).
# The buffers are registered so the harness's state_dict load (strict=True)
# works after the reference model has been built and quantized.
# ---------------------------------------------------------------------------
class Model(torch.nn.Module):
def __init__(self, M: int, N: int, K: int, group_size: int = GROUP_SIZE):
super().__init__()
assert K % group_size == 0
assert K % 2 == 0
self.M, self.N, self.K = M, N, K
self.group_size = group_size
n_groups = K // group_size
# Buffers are placeholders; the real values are loaded by the harness
# from the reference model's state_dict. Names must match reference.py.
self.register_buffer("w_q", torch.empty(K // 2, N, dtype=torch.uint8))
self.register_buffer("scales", torch.empty(n_groups, N, dtype=torch.bfloat16))
self.register_buffer("zeros", torch.empty(n_groups, N, dtype=torch.bfloat16))
def forward(self, x: torch.Tensor) -> torch.Tensor:
return w4a16_gemm(x, self.w_q, self.scales, self.zeros)
M = 1
N = 12288
K = 4096
def get_inputs():
x = torch.randn(M, K, dtype=torch.bfloat16)
return [x]
def get_init_inputs():
return [M, N, K]
20260618_065423_minimax-claude_MiniMax-M3_07_w4a16_gemm