KernelBench hard · RTX PRO 6000
FP8 GEMM Claude Fable 5
manually audited: clean
Genuine fp8 e4m3 GEMM. Primary path is a CUTLASS 3.x SM120 warp-specialized cooperative kernel (TMA mainloop, mma.sync e4m3, fp32 accumulate, 256x128x64 tile) built at import via load_inline, with the per-channel weight scale fused into the epilogue through a custom EVT (Sm90RowBroadcast * accumulator). The off-alignment K=4127 shape is handled by a custom funnel-shift pad-copy kernel staging operands into zero-padded 16B-aligned buffers. Skinny M<64 decode uses a hand-tuned Triton tl.dot kernel replayed through a CUDA graph; a Triton TMA persistent kernel is the full fallback (verified live via KBH_SOL_NO_CUTLASS=1 check.py PASS in-session). No forbidden ops: the solution never calls torch._scaled_mm (all 6 transcript mentions are the agent reading problem.yaml and sota.py). template_mutated=false.
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(67.6% · 62.1% · 7.7% · 72.2%) = 39.1%
Kernel source (redacted)
"""FP8 e4m3 GEMM for SM120 (RTX PRO 6000 Blackwell).
y = (x @ w.T) * weight_scale; x fp8_e4m3 (M,K), w fp8_e4m3 (N,K), out bf16.
Primary path: CUTLASS 3.x SM120 warp-specialized cooperative kernel
(TMA + mma.sync e4m3, fp32 accumulate, 256x128x64 tile) with the per-channel
scale fused into the epilogue via an EVT (Sm90RowBroadcast * accumulator),
compiled at import time with torch.utils.cpp_extension.load_inline.
Skinny-M shapes (decode) use a bandwidth-oriented Triton fp8 tl.dot kernel
with the scale fused into the store.
K not divisible by 16 (TMA stride requirement) is handled by staging the
operands into zero-padded 16B-aligned buffers.
If the CUTLASS build is unavailable, a Triton TMA persistent kernel covers
the compute shapes instead.
"""
import os
import torch
import torch.nn as nn
import triton
import triton.language as tl
E4M3_MAX = 448.0
_NUM_SMS = None
def _num_sms():
global _NUM_SMS
if _NUM_SMS is None:
_NUM_SMS = torch.cuda.get_device_properties(
torch.cuda.current_device()).multi_processor_count
return _NUM_SMS
def _alloc_fn(size: int, alignment: int, stream):
return torch.empty(size, dtype=torch.int8, device="cuda")
triton.set_allocator(_alloc_fn)
# ===========================================================================
# CUTLASS SM120 extension (primary compute path)
# ===========================================================================
_CUTLASS_SRC = r"""
#include <torch/extension.h>
#include <ATen/cuda/CUDAContext.h>
#include "cutlass/cutlass.h"
#include "cute/tensor.hpp"
#include "cutlass/epilogue/collective/collective_builder.hpp"
#include "cutlass/epilogue/fusion/sm90_visitor_tma_warpspecialized.hpp"
#include "cutlass/gemm/collective/collective_builder.hpp"
#include "cutlass/gemm/device/gemm_universal_adapter.h"
#include "cutlass/gemm/kernel/gemm_universal.hpp"
#include "cutlass/util/packed_stride.hpp"
using namespace cute;
using ElementA = cutlass::float_e4m3_t;
using ElementB = cutlass::float_e4m3_t;
using ElementD = cutlass::bfloat16_t;
using ElementAcc = float;
using LayoutA = cutlass::layout::RowMajor; // x (M,K), K-major
using LayoutB = cutlass::layout::ColumnMajor; // w (N,K), K-major
using LayoutD = cutlass::layout::RowMajor;
// One SM120 kernel definition: warp-specialized cooperative, TMA mainloop,
// fp32 accumulate, epilogue D = bf16(acc * scale[n]).
using TileShape = Shape<_256, _128, _64>;
using ClusterShape = Shape<_1, _1, _1>;
static constexpr auto RoundStyle = cutlass::FloatRoundStyle::round_to_nearest;
using CustomEVT = cutlass::epilogue::fusion::Sm90EVT<
cutlass::epilogue::fusion::Sm90Compute<cutlass::multiplies, ElementD,
float, RoundStyle>,
cutlass::epilogue::fusion::Sm90RowBroadcast<0, TileShape, float>,
cutlass::epilogue::fusion::Sm90AccFetch>;
using CollectiveEpilogue =
typename cutlass::epilogue::collective::CollectiveBuilder<
cutlass::arch::Sm120, cutlass::arch::OpClassTensorOp, TileShape,
ClusterShape, cutlass::epilogue::collective::EpilogueTileAuto,
ElementAcc, float, void, LayoutD, 8, ElementD, LayoutD, 8,
cutlass::epilogue::TmaWarpSpecializedCooperative,
CustomEVT>::CollectiveOp;
using CollectiveMainloop =
typename cutlass::gemm::collective::CollectiveBuilder<
cutlass::arch::Sm120, cutlass::arch::OpClassTensorOp,
ElementA, LayoutA, 16,
ElementB, LayoutB, 16,
ElementAcc, TileShape, ClusterShape,
cutlass::gemm::collective::StageCountAutoCarveout<static_cast<int>(
sizeof(typename CollectiveEpilogue::SharedStorage))>,
cutlass::gemm::KernelTmaWarpSpecializedCooperative>::CollectiveOp;
using GemmKernel = cutlass::gemm::kernel::GemmUniversal<
Shape<int, int, int, int>, CollectiveMainloop, CollectiveEpilogue, void>;
using Gemm = cutlass::gemm::device::GemmUniversalAdapter<GemmKernel>;
void fp8_gemm(torch::Tensor x, torch::Tensor w, torch::Tensor s,
torch::Tensor y, int64_t M, int64_t N, int64_t K) {
using StrideA = typename Gemm::GemmKernel::StrideA;
using StrideB = typename Gemm::GemmKernel::StrideB;
using StrideD = typename Gemm::GemmKernel::StrideD;
auto stride_A = cutlass::make_cute_packed_stride(StrideA{}, {int(M), int(K), 1});
auto stride_B = cutlass::make_cute_packed_stride(StrideB{}, {int(N), int(K), 1});
auto stride_D = cutlass::make_cute_packed_stride(StrideD{}, {int(M), int(N), 1});
get<0>(stride_A) = x.stride(0);
get<0>(stride_B) = w.stride(0);
cutlass::KernelHardwareInfo hw_info;
hw_info.device_id = x.get_device();
hw_info.sm_count =
cutlass::KernelHardwareInfo::query_device_multiprocessor_count(
hw_info.device_id);
typename Gemm::Arguments args{
cutlass::gemm::GemmUniversalMode::kGemm,
{int(M), int(N), int(K), 1},
{reinterpret_cast<ElementA const*>(x.data_ptr()), stride_A,
reinterpret_cast<ElementB const*>(w.data_ptr()), stride_B},
{{}, nullptr, stride_D,
reinterpret_cast<ElementD*>(y.data_ptr()), stride_D},
hw_info};
args.epilogue.thread = {
{reinterpret_cast<float const*>(s.data_ptr())}, // scale row broadcast
{}, // accumulator
{} // multiply node
};
Gemm gemm;
TORCH_CHECK(gemm.can_implement(args) == cutlass::Status::kSuccess,
"CUTLASS SM120 fp8 GEMM cannot implement this problem");
size_t ws_size = Gemm::get_workspace_size(args);
auto ws = torch::empty(
{static_cast<int64_t>(ws_size)},
torch::TensorOptions().dtype(torch::kUInt8).device(x.device()));
auto stream = at::cuda::getCurrentCUDAStream();
TORCH_CHECK(gemm.initialize(args, ws.data_ptr()) == cutlass::Status::kSuccess,
"CUTLASS initialize failed");
TORCH_CHECK(gemm.run(stream) == cutlass::Status::kSuccess,
"CUTLASS run failed");
}
// Row-realignment pad copy: src (M,K) contiguous fp8 rows at arbitrary byte
// alignment -> dst (M,K_pad) with 16B-aligned rows, zero-filled K..K_pad.
// u32-granularity with funnel shifts so both sides stay vectorized/coalesced.
__global__ void pad_copy_kernel(const uint8_t* __restrict__ src,
uint8_t* __restrict__ dst,
int64_t M, int64_t K, int64_t K_pad) {
int64_t words_per_row = K_pad >> 2; // K_pad % 4 == 0
int64_t total_words = M * words_per_row;
int64_t total_src = M * K;
uint32_t* dst32 = reinterpret_cast<uint32_t*>(dst);
for (int64_t idx = blockIdx.x * (int64_t)blockDim.x + threadIdx.x;
idx < total_words; idx += (int64_t)gridDim.x * blockDim.x) {
int64_t row = idx / words_per_row;
int64_t col = (idx - row * words_per_row) << 2; // byte col within row
uint32_t out = 0;
if (col + 4 <= K) {
int64_t addr = row * K + col; // src byte offset
if (addr + 7 < total_src) {
int64_t base = addr >> 2;
uint32_t sh = (addr & 3) * 8;
const uint32_t* src32 = reinterpret_cast<const uint32_t*>(src);
uint32_t w0 = src32[base];
uint32_t w1 = src32[base + 1];
out = sh ? __funnelshift_r(w0, w1, sh) : w0;
} else {
uint8_t b[4];
#pragma unroll
for (int j = 0; j < 4; ++j) b[j] = src[addr + j];
out = b[0] | (b[1] << 8) | (b[2] << 16) | (uint32_t(b[3]) << 24);
}
} else if (col < K) { // straddles the K boundary: partial bytes + zeros
uint8_t b[4] = {0, 0, 0, 0};
for (int j = 0; j < 4 && col + j < K; ++j) b[j] = src[row * K + col + j];
out = b[0] | (b[1] << 8) | (b[2] << 16) | (uint32_t(b[3]) << 24);
}
dst32[idx] = out;
}
}
void pad_copy(torch::Tensor src, torch::Tensor dst, int64_t M, int64_t K,
int64_t K_pad) {
auto stream = at::cuda::getCurrentCUDAStream();
int64_t total_words = M * (K_pad >> 2);
int threads = 256;
int blocks = (int)std::min<int64_t>((total_words + threads - 1) / threads,
65535 * 8);
pad_copy_kernel<<<blocks, threads, 0, stream>>>(
reinterpret_cast<const uint8_t*>(src.data_ptr()),
reinterpret_cast<uint8_t*>(dst.data_ptr()), M, K, K_pad);
}
"""
_CUTLASS_DECL = """
void fp8_gemm(torch::Tensor x, torch::Tensor w, torch::Tensor s, torch::Tensor y, int64_t M, int64_t N, int64_t K);
void pad_copy(torch::Tensor src, torch::Tensor dst, int64_t M, int64_t K, int64_t K_pad);
"""
def _find_cutlass():
here = os.path.dirname(os.path.abspath(__file__))
candidates = [
os.environ.get("CUTLASS_PATH", ""),
os.path.join(here, "scratch", "cutlass"),
os.path.join(here, "cutlass"),
os.path.expanduser("~/.cache/kbh_cutlass/cutlass"),
]
for c in candidates:
if c and os.path.isfile(os.path.join(c, "include", "cutlass", "cutlass.h")):
return c
# Last resort: shallow clone.
try:
import subprocess
dst = os.path.expanduser("~/.cache/kbh_cutlass/cutlass")
os.makedirs(os.path.dirname(dst), exist_ok=True)
subprocess.run(
["git", "clone", "--depth", "1",
"https://github.com/NVIDIA/cutlass.git", dst],
check=True, capture_output=True, timeout=600)
if os.path.isfile(os.path.join(dst, "include", "cutlass", "cutlass.h")):
return dst
except Exception:
pass
return None
def _build_cutlass_ext():
if os.environ.get("KBH_SOL_NO_CUTLASS") == "1": # testing hook
return None
if torch.cuda.get_device_capability(0)[0] < 12:
return None
root = _find_cutlass()
if root is None:
return None
from torch.utils.cpp_extension import load_inline
def _try_build(build_dir):
if build_dir is not None:
os.makedirs(build_dir, exist_ok=True)
return load_inline(
name="fp8_gemm_sm120_sol",
cpp_sources=[_CUTLASS_DECL],
cuda_sources=[_CUTLASS_SRC],
functions=["fp8_gemm", "pad_copy"],
extra_include_paths=[
os.path.join(root, "include"),
os.path.join(root, "tools", "util", "include"),
],
extra_cuda_cflags=[
"-O3",
"-gencode=arch=compute_120a,code=sm_120a",
"--expt-relaxed-constexpr",
"-DNDEBUG",
],
build_directory=build_dir,
verbose=False,
)
here = os.path.dirname(os.path.abspath(__file__))
# Prefer a build dir next to this file (ships the compiled .so with the
# directory), fall back to torch's default extension cache.
for build_dir in (os.path.join(here, ".cutlass_ext_build"), None):
try:
return _try_build(build_dir)
except Exception:
continue
return None
_EXT = _build_cutlass_ext()
# ===========================================================================
# Triton kernels: skinny-M path and fallback persistent TMA path
# ===========================================================================
# Note: deliberately NOT autotuned. Triton's autotuner benchmarks without an
# L2 flush; with the whole weight matrix L2-resident it picks configs that
# lose ~20% when the weights actually stream from DRAM (the graded regime).
# This config measured best under an explicit L2 flush.
@triton.jit
def _fp8_gemm_skinny(
x_ptr, w_ptr, s_ptr, y_ptr,
M, N, K,
stride_xm, stride_wn,
BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
EVEN_N: tl.constexpr, EVEN_K: tl.constexpr,
):
pid_n = tl.program_id(0)
rm = tl.arange(0, BLOCK_M)
rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
rk = tl.arange(0, BLOCK_K)
m_mask = rm[:, None] < M
n_mask = rn[:, None] < N
x_ptrs = x_ptr + rm[:, None] * stride_xm + rk[None, :]
w_ptrs = w_ptr + rn[:, None] * stride_wn + rk[None, :]
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for k in range(0, K, BLOCK_K):
if EVEN_K:
a = tl.load(x_ptrs, mask=m_mask, other=0.0)
if EVEN_N:
b = tl.load(w_ptrs)
else:
b = tl.load(w_ptrs, mask=n_mask, other=0.0)
else:
km = (k + rk)[None, :] < K
a = tl.load(x_ptrs, mask=m_mask & km, other=0.0)
if EVEN_N:
b = tl.load(w_ptrs, mask=km, other=0.0)
else:
b = tl.load(w_ptrs, mask=n_mask & km, other=0.0)
acc = tl.dot(a, tl.trans(b), acc)
x_ptrs += BLOCK_K
w_ptrs += BLOCK_K
if EVEN_N:
sc = tl.load(s_ptr + rn)
else:
sc = tl.load(s_ptr + rn, mask=rn < N, other=0.0)
acc = acc * sc[None, :]
tl.store(y_ptr + rm[:, None] * N + rn[None, :], acc.to(tl.bfloat16),
mask=m_mask & (rn[None, :] < N))
def _fallback_configs():
out = []
for bm, bn, bk, ns, nw, es in [
(128, 256, 64, 3, 8, True),
(128, 128, 128, 3, 8, False),
(128, 128, 64, 5, 8, False),
(64, 256, 128, 3, 4, True),
]:
out.append(triton.Config(
{"BLOCK_M": bm, "BLOCK_N": bn, "BLOCK_K": bk, "GROUP_M": 8,
"EPILOGUE_SUBTILE": es},
num_stages=ns, num_warps=nw))
return out
@triton.autotune(configs=_fallback_configs(), key=["M", "N", "K", "stride_xm"])
@triton.jit
def _fp8_gemm_tma(
x_ptr, w_ptr, s_ptr, y_ptr,
M, N, K,
stride_xm, stride_wn,
BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
GROUP_M: tl.constexpr, EPILOGUE_SUBTILE: tl.constexpr,
NUM_SMS: tl.constexpr,
):
start_pid = tl.program_id(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
x_desc = tl.make_tensor_descriptor(
x_ptr, shape=[M, K], strides=[stride_xm, 1],
block_shape=[BLOCK_M, BLOCK_K])
w_desc = tl.make_tensor_descriptor(
w_ptr, shape=[N, K], strides=[stride_wn, 1],
block_shape=[BLOCK_N, BLOCK_K])
y_desc = tl.make_tensor_descriptor(
y_ptr, shape=[M, N], strides=[N, 1],
block_shape=[BLOCK_M, BLOCK_N // 2 if EPILOGUE_SUBTILE else BLOCK_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
offs_am = pid_m * BLOCK_M
offs_bn = pid_n * BLOCK_N
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for ki in range(k_tiles):
offs_k = ki * BLOCK_K
a = x_desc.load([offs_am, offs_k])
b = w_desc.load([offs_bn, offs_k])
acc = tl.dot(a, b.T, acc)
rn = offs_bn + tl.arange(0, BLOCK_N)
scale = tl.load(s_ptr + rn, mask=rn < N, other=0.0)
acc = acc * scale[None, :]
if EPILOGUE_SUBTILE:
acc1 = tl.reshape(acc, (BLOCK_M, 2, BLOCK_N // 2))
acc1 = tl.permute(acc1, (0, 2, 1))
acc0, accn = tl.split(acc1)
y_desc.store([offs_am, offs_bn], acc0.to(tl.bfloat16))
y_desc.store([offs_am, offs_bn + BLOCK_N // 2], accn.to(tl.bfloat16))
else:
y_desc.store([offs_am, offs_bn], acc.to(tl.bfloat16))
def _next_pow2(v: int) -> int:
n = 1
while n < v:
n *= 2
return n
# ===========================================================================
# Model
# ===========================================================================
class Model(nn.Module):
"""y = ((x @ w.T) * weight_scale).to(bf16) via fp8 tensor-core MMA."""
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))
# Zero-padded 16B-aligned staging buffers, used only when K % 16 != 0.
# Round to a multiple of 64: measured faster than the minimal multiple
# of 16 (better DRAM/TMA behavior of the padded stride).
self._k_pad = (K + 63) // 64 * 64
self._x_pad = None
self._w_pad = None
self._w_pad_key = None
self._skinny_graph = None
self._skinny_key = None
def _staged(self, x: torch.Tensor):
"""Copy x / weight into zero-padded buffers with 16B-aligned strides."""
K_pad = self._k_pad
if self._x_pad is None or self._x_pad.shape[0] != x.shape[0]:
self._x_pad = torch.zeros(
x.shape[0], K_pad, dtype=torch.float8_e4m3fn, device=x.device)
if self._w_pad is None:
self._w_pad = torch.zeros(
self.weight.shape[0], K_pad, dtype=torch.float8_e4m3fn,
device=x.device)
self._copy_pad(x, self._x_pad)
# The weight buffer can be mutated in place between calls (any
# in-place torch op bumps _version), so restage when stale.
w_key = (self.weight.data_ptr(), self.weight._version,
self._w_pad.data_ptr())
if w_key != self._w_pad_key:
self._copy_pad(self.weight, self._w_pad)
self._w_pad_key = (self.weight.data_ptr(), self.weight._version,
self._w_pad.data_ptr())
return self._x_pad, self._w_pad
def _copy_pad(self, src, dst):
rows, k = src.shape
if (_EXT is not None and src.is_contiguous()
and src.data_ptr() % 4 == 0 and dst.data_ptr() % 16 == 0):
_EXT.pad_copy(src.view(torch.uint8), dst.view(torch.uint8),
rows, k, self._k_pad)
else:
dst[:, :k].view(torch.int8).copy_(src.view(torch.int8))
def forward(self, x: torch.Tensor) -> torch.Tensor:
w, s = self.weight, self.weight_scale
M, K = x.shape
N = w.shape[0]
if M < 64:
return self._forward_skinny(x, w, s, M, N, K)
K_eff = K
if K % 16 != 0:
x, w = self._staged(x)
K_eff = self._k_pad # trailing columns are zero: no effect on sum
y = torch.empty((M, N), dtype=torch.bfloat16, device=x.device)
if _EXT is not None:
try:
_EXT.fp8_gemm(x, w, s, y, M, N, K_eff)
return y
except RuntimeError:
pass # e.g. unusual N alignment: fall through to Triton
NUM_SMS = _num_sms()
grid = lambda meta: (min(
NUM_SMS,
triton.cdiv(M, meta["BLOCK_M"]) * triton.cdiv(N, meta["BLOCK_N"])),)
_fp8_gemm_tma[grid](
x, w, s, y, M, N, K_eff, x.stride(0), w.stride(0), NUM_SMS=NUM_SMS)
return y
def _launch_skinny(self, x, w, s, y, M, N, K):
BLOCK_M = max(16, _next_pow2(M))
_fp8_gemm_skinny[(triton.cdiv(N, 64),)](
x, w, s, y, M, N, K, x.stride(0), w.stride(0),
BLOCK_M=BLOCK_M, BLOCK_N=64, BLOCK_K=256,
EVEN_N=(N % 64 == 0), EVEN_K=(K % 256 == 0),
num_stages=3, num_warps=4)
def _forward_skinny(self, x, w, s, M, N, K):
# Launch overhead is a measurable fraction of this memory-bound
# kernel, so replay it through a CUDA graph with a static input slot.
key = (x.shape, x.stride(), w.data_ptr(), s.data_ptr(), x.device.index)
if self._skinny_graph is None or self._skinny_key != key:
try:
x_st = x.clone()
y_st = torch.empty((M, N), dtype=torch.bfloat16, device=x.device)
self._launch_skinny(x_st, w, s, y_st, M, N, K) # warm cache
torch.cuda.synchronize()
g = torch.cuda.CUDAGraph()
with torch.cuda.graph(g):
self._launch_skinny(x_st, w, s, y_st, M, N, K)
self._skinny_graph = (g, x_st, y_st)
self._skinny_key = key
except Exception:
self._skinny_graph, self._skinny_key = None, None
y = torch.empty((M, N), dtype=torch.bfloat16, device=x.device)
self._launch_skinny(x, w, s, y, M, N, K)
return y
g, x_st, y_st = self._skinny_graph
x_st.copy_(x)
g.replay()
return y_st
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]
20260721_152955_or-fable_anthropic_claude-fable-5_01_fp8_gemm