KernelBench hard · RTX PRO 6000
FP8 GEMM Claude Fable 5
manually audited: clean
Genuine fp8 e4m3 x e4m3 GEMM with a real custom kernel stack. Primary path: CUTLASS 4.0 SM100 kernels compiled in-run via load_inline against a cloned CUTLASS checkout — tcgen05 UMMA, TMA warp-specialized, with a custom EVT epilogue (Sm90RowBroadcast(scale) * AccFetch) that fuses the per-channel weight_scale and writes bf16 directly. Two variants: 2SM MmaTile 256x256x128 cluster (2,2) for large shapes, 1SM 64x64x128 for skinny decode (memory bound). Fallback: persistent Triton TMA-descriptor kernel with warp_specialize. Off-alignment K=4127 is handled by zero-padding both operands to K=4224 (mathematically identical). A CUDA-graph launch wrapper and a padded-weight cache are both keyed on (data_ptr, shape, _version), so in-place writes invalidate them — empirically verified to recompute on live data. No forbidden ops (torch._scaled_mm absent), no reference import, no cross-run contamination. Rebench peak_fraction 0.2110 matches the original result.json 0.2110; the geomean is dragged by the structurally memory-bound decode shape (M=32: frac 0.027 at 1922 GB/s — near HBM bandwidth, not a weakness of the kernel) while compute shapes reach 0.36-0.50 of the 4500 TF fp8 ceiling (1.6-2.2 PFLOPS real).
Kernel source (redacted)
"""FP8 e4m3 x FP8 e4m3 GEMM for B200 (SM100) — real fp8 tensor-core MMA.
y = (x @ w.T) * weight_scale -> bf16
x: (M, K) fp8_e4m3 row-major; w: (N, K) fp8_e4m3 row-major (K-major == the TN
layout fp8 MMAs want); weight_scale: (N,) fp32 per-output-channel scale.
Primary path: CUTLASS 4.x SM100 kernels (tcgen05 2SM UMMA, TMA warp-specialized,
built via torch cpp_extension against the CUTLASS headers cloned into this
directory). The per-channel scale is fused into the epilogue with a custom EVT
(RowBroadcast(scale) * Accumulator), so the kernel writes scaled bf16 directly.
Variants (picked per shape, measured on this box):
* large shapes: MmaTile 256x256x128, cluster (2,2), 2SM MMA (~2.6-3.0 PFLOPS)
* skinny M<=64 (decode): MmaTile 64x64x128, 1SM, maximizes CTA count for
HBM streaming (the shape is memory-bound: must read all of W once)
TMA requires 16B-aligned row strides, so for K=4127 both operands are zero-padded
to K=4224 (identical result; zeros contribute nothing). The weight pad is cached
keyed on (data_ptr, _version) so in-place weight updates (e.g. numeric-stress
rescaling via copy_) invalidate it; x is re-padded on every call — nothing is
memoized across differing inputs.
Fallback path (if the extension can't build): a persistent Triton kernel using
TMA tensor descriptors + tl.dot on fp8 (lowers to tcgen05) with warp
specialization — same math, ~70% of the CUTLASS speed.
"""
import os
from pathlib import Path
# The PATH nvcc on this box is a broken wrapper (REAL_NVCC unset) and CUDA_HOME
# may point at a toolkit that does not exist; force a real one.
_ch = os.environ.get("CUDA_HOME")
if not _ch or not os.path.exists(os.path.join(_ch, "bin", "nvcc")):
for _cand in ("/usr/local/cuda-12.8", "/usr/local/cuda-12", "/usr/local/cuda"):
if os.path.exists(os.path.join(_cand, "bin", "nvcc")):
os.environ["CUDA_HOME"] = _cand
break
import torch
import torch.nn as nn
import triton
import triton.language as tl
E4M3_MAX = 448.0
_HERE = Path(__file__).resolve().parent
# =========================================================================
# Triton fallback: persistent TMA-descriptor kernel (tcgen05 via tl.dot)
# =========================================================================
def _alloc(size, alignment, stream):
return torch.empty(size, device="cuda", dtype=torch.int8)
triton.set_allocator(_alloc)
def _configs():
return [
triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 128, "GROUP_M": 8}, num_warps=8, num_stages=3),
triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 128, "GROUP_M": 16}, num_warps=8, num_stages=3),
triton.Config({"BLOCK_M": 64, "BLOCK_N": 64, "BLOCK_K": 256, "GROUP_M": 1}, num_warps=4, num_stages=4),
]
@triton.autotune(configs=_configs(), key=["M", "N", "K"])
@triton.jit
def _fp8_gemm_desc_kernel(
x_ptr, w_ptr, s_ptr, y_ptr,
M, N, K,
NUM_SMS,
BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
GROUP_M: tl.constexpr,
):
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
a_desc = tl.make_tensor_descriptor(x_ptr, [M, K], [K, 1], [BLOCK_M, BLOCK_K])
b_desc = tl.make_tensor_descriptor(w_ptr, [N, K], [K, 1], [BLOCK_N, BLOCK_K])
y_desc = tl.make_tensor_descriptor(y_ptr, [M, N], [N, 1], [BLOCK_M, BLOCK_N])
num_pid_in_group = GROUP_M * num_pid_n
for tile_id in tl.range(tl.program_id(0), num_tiles, NUM_SMS, flatten=True, warp_specialize=True):
group_id = tile_id // num_pid_in_group
first_pid_m = group_id * GROUP_M
group_size_m = tl.minimum(num_pid_m - first_pid_m, GROUP_M)
pid_m = first_pid_m + ((tile_id % num_pid_in_group) % 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):
a = a_desc.load([offs_am, ki * BLOCK_K])
b = b_desc.load([offs_bn, ki * BLOCK_K])
acc = tl.dot(a, b.T, acc)
offs_n = offs_bn + tl.arange(0, BLOCK_N)
scale = tl.load(s_ptr + offs_n, mask=offs_n < N, other=0.0)
out = (acc * scale[None, :]).to(tl.bfloat16)
y_desc.store([offs_am, offs_bn], out)
@triton.jit
def _pad_k_kernel(src, dst, M, K, K_pad, BLOCK: tl.constexpr):
"""(M, K) contiguous fp8 -> (M, K_pad) contiguous, zero tail. Byte-wise."""
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
total = M * K_pad
dmask = offs < total
m = offs // K_pad
k = offs - m * K_pad
v = tl.load(src + m * K + k, mask=dmask & (k < K), other=0)
tl.store(dst + offs, v, mask=dmask)
def _pad_k(t8: torch.Tensor, K_pad: int) -> torch.Tensor:
"""Zero-pad the last dim of a contiguous fp8 (R, K) tensor to K_pad."""
R, K = t8.shape
out = torch.empty((R, K_pad), device=t8.device, dtype=t8.dtype)
total = R * K_pad
_pad_k_kernel[(triton.cdiv(total, 4096),)](
t8.view(torch.uint8), out.view(torch.uint8), R, K, K_pad,
BLOCK=4096, num_warps=4,
)
return out
# =========================================================================
# CUTLASS SM100 extension (primary path)
# =========================================================================
_CUTLASS_SOURCE = r"""
// SM100 (B200) fp8 e4m3 x e4m3 GEMM with per-column (N) scale epilogue.
// y[m,n] = (sum_k x[m,k] * w[n,k]) * s[n], output bf16.
// A = x (M,K) row-major e4m3; B = w (N,K) row-major == (K,N) column-major;
// D = y (M,N) row-major bf16. Custom EVT: D = RowBroadcast(s) * Acc.
#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/gemm/kernel/tile_scheduler.hpp"
#include "cutlass/util/packed_stride.hpp"
#include "cutlass/kernel_hardware_info.h"
using namespace cute;
#if !defined(CUTLASS_ARCH_MMA_SM100_SUPPORTED)
#error "CUTLASS_ARCH_MMA_SM100_SUPPORTED not defined - need CUDA 12.8+ and sm_100a"
#endif
using ElementA = cutlass::float_e4m3_t;
using ElementB = cutlass::float_e4m3_t;
using ElementD = cutlass::bfloat16_t;
using ElementAcc = float;
using ElementCompute = float;
using LayoutA = cutlass::layout::RowMajor; // (M,K)
using LayoutB = cutlass::layout::ColumnMajor; // (K,N) col-major == w (N,K) row-major
using LayoutD = cutlass::layout::RowMajor; // (M,N)
constexpr int AlignA = 16;
constexpr int AlignB = 16;
constexpr int AlignD = 8;
constexpr auto RoundStyle = cutlass::FloatRoundStyle::round_to_nearest;
template <class MmaTile, class Cluster, bool TwoSm>
struct GemmConfig {
using KernelSchedule = cute::conditional_t<TwoSm,
cutlass::gemm::KernelTmaWarpSpecialized2SmSm100,
cutlass::gemm::KernelTmaWarpSpecialized1SmSm100>;
using EpilogueSchedule = cute::conditional_t<TwoSm,
cutlass::epilogue::TmaWarpSpecialized2Sm,
cutlass::epilogue::TmaWarpSpecialized1Sm>;
// Per-CTA tile (2SM MMA splits M across the CTA pair)
static constexpr int kTileM = decltype(size<0>(MmaTile{}))::value / (TwoSm ? 2 : 1);
static constexpr int kTileN = decltype(size<1>(MmaTile{}))::value;
static constexpr int kTileK = decltype(size<2>(MmaTile{}))::value;
using CtaTileShape = Shape<Int<kTileM>, Int<kTileN>, Int<kTileK>>;
using RowScale = cutlass::epilogue::fusion::Sm90RowBroadcast<
0, CtaTileShape, float, float, Stride<_0, _1, _0>>;
using CustomEVT = cutlass::epilogue::fusion::Sm90EVT<
cutlass::epilogue::fusion::Sm90Compute<cutlass::multiplies, ElementD, ElementCompute, RoundStyle>,
RowScale,
cutlass::epilogue::fusion::Sm90AccFetch>;
using CollectiveEpilogue = typename cutlass::epilogue::collective::CollectiveBuilder<
cutlass::arch::Sm100, cutlass::arch::OpClassTensorOp,
MmaTile, Cluster,
cutlass::epilogue::collective::EpilogueTileAuto,
ElementAcc, ElementCompute,
void, LayoutD, AlignD,
ElementD, LayoutD, AlignD,
EpilogueSchedule,
CustomEVT>::CollectiveOp;
using CollectiveMainloop = typename cutlass::gemm::collective::CollectiveBuilder<
cutlass::arch::Sm100, cutlass::arch::OpClassTensorOp,
ElementA, LayoutA, AlignA,
ElementB, LayoutB, AlignB,
ElementAcc,
MmaTile, Cluster,
cutlass::gemm::collective::StageCountAutoCarveout<
static_cast<int>(sizeof(typename CollectiveEpilogue::SharedStorage))>,
KernelSchedule>::CollectiveOp;
using GemmKernel = cutlass::gemm::kernel::GemmUniversal<
Shape<int, int, int, int>,
CollectiveMainloop,
CollectiveEpilogue>;
using Gemm = cutlass::gemm::device::GemmUniversalAdapter<GemmKernel>;
};
template <class Config>
void run_gemm(torch::Tensor x, torch::Tensor w, torch::Tensor s, torch::Tensor y) {
using Gemm = typename Config::Gemm;
int M = x.size(0), K = x.size(1), N = w.size(0);
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{}, {M, K, 1});
auto stride_B = cutlass::make_cute_packed_stride(StrideB{}, {N, K, 1});
auto stride_D = cutlass::make_cute_packed_stride(StrideD{}, {M, N, 1});
static cutlass::KernelHardwareInfo hw_info = [] {
cutlass::KernelHardwareInfo info;
info.device_id = 0;
info.sm_count = cutlass::KernelHardwareInfo::query_device_multiprocessor_count(0);
return info;
}();
typename Gemm::Arguments args{
cutlass::gemm::GemmUniversalMode::kGemm,
{M, N, 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()), 0.f, {_0{}, _1{}, _0{}}}, // RowBroadcast(s)
{}, // AccFetch
{}, // multiplies
};
Gemm gemm;
auto status = gemm.can_implement(args);
TORCH_CHECK(status == cutlass::Status::kSuccess,
"CUTLASS cannot implement: ", cutlassGetStatusString(status),
" M=", M, " N=", N, " K=", K);
size_t ws_size = Gemm::get_workspace_size(args);
void* ws_ptr = nullptr;
torch::Tensor ws;
if (ws_size > 0) {
ws = torch::empty({static_cast<long>(ws_size)},
torch::TensorOptions().dtype(torch::kUInt8).device(x.device()));
ws_ptr = ws.data_ptr();
}
auto stream = at::cuda::getCurrentCUDAStream().stream();
status = gemm.initialize(args, ws_ptr);
TORCH_CHECK(status == cutlass::Status::kSuccess, "CUTLASS initialize failed");
status = gemm.run(stream);
TORCH_CHECK(status == cutlass::Status::kSuccess, "CUTLASS run failed");
}
// Large shapes: 2SM UMMA, MmaTile 256x256x128, cluster (2,2).
using CfgBig = GemmConfig<Shape<_256, _256, _128>, Shape<_2, _2, _1>, true>;
// Skinny decode shapes: 1SM, MmaTile 64x64x128 -> max CTA count for streaming W.
using CfgSkinny = GemmConfig<Shape<_64, _64, _128>, Shape<_1, _1, _1>, false>;
void fp8_gemm_big(torch::Tensor x, torch::Tensor w, torch::Tensor s, torch::Tensor y) {
run_gemm<CfgBig>(x, w, s, y);
}
void fp8_gemm_skinny(torch::Tensor x, torch::Tensor w, torch::Tensor s, torch::Tensor y) {
run_gemm<CfgSkinny>(x, w, s, y);
}
"""
_EXT = None
_EXT_TRIED = False
def _cutlass_include_dirs():
root = _HERE / "cutlass"
if not (root / "include" / "cutlass" / "cutlass.h").exists():
# Best-effort fetch if the checkout is missing (fresh workspace).
import subprocess
try:
subprocess.run(
["git", "clone", "--depth", "1", "--branch", "v4.0.0",
"https://github.com/NVIDIA/cutlass.git", str(root)],
check=True, capture_output=True, timeout=600,
)
except Exception:
return None
return [str(root / "include"), str(root / "tools" / "util" / "include")]
def _get_ext():
global _EXT, _EXT_TRIED
if _EXT is not None or _EXT_TRIED:
return _EXT
_EXT_TRIED = True
try:
incs = _cutlass_include_dirs()
if incs is None:
return None
from torch.utils.cpp_extension import load_inline
_EXT = load_inline(
name="fp8_cutlass_sol",
cpp_sources="",
cuda_sources=_CUTLASS_SOURCE,
extra_include_paths=incs,
extra_cuda_cflags=[
"-O3", "-std=c++17",
"-gencode=arch=compute_100a,code=sm_100a",
"--expt-relaxed-constexpr",
"-DNDEBUG",
],
functions=["fp8_gemm_big", "fp8_gemm_skinny"],
verbose=False,
)
except Exception:
_EXT = None
return _EXT
class Model(nn.Module):
"""y = ((x @ w.T) * weight_scale).to(bf16) with a genuine fp8 x fp8 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))
self._wpad = None # (key, padded weight) — invalidated on in-place update
self._num_sms = None
self._graphs = {} # (ptrs, versions) -> (CUDAGraph, output, warm count)
self._graphs_ok = True
def _padded_weight(self, K_pad: int) -> torch.Tensor:
w = self.weight
key = (w.data_ptr(), w._version, K_pad)
if self._wpad is None or self._wpad[0] != key:
self._wpad = (key, _pad_k(w.contiguous(), K_pad))
return self._wpad[1]
def _compute(self, x: torch.Tensor) -> torch.Tensor:
w = self.weight
s = self.weight_scale
M, K = x.shape
N = w.shape[0]
x = x.contiguous()
if K % 16 != 0:
# TMA needs 16B-aligned row strides: zero-pad K (identical result).
K_pad = triton.cdiv(K, 128) * 128
x = _pad_k(x, K_pad)
w = self._padded_weight(K_pad)
K = K_pad
if not w.is_contiguous():
w = w.contiguous()
y = torch.empty((M, N), device=x.device, dtype=torch.bfloat16)
ext = _get_ext()
if ext is not None:
try:
if M <= 64:
ext.fp8_gemm_skinny(x, w, s, y)
else:
ext.fp8_gemm_big(x, w, s, y)
return y
except Exception:
pass # fall through to Triton
if self._num_sms is None:
self._num_sms = torch.cuda.get_device_properties(x.device).multi_processor_count
grid = lambda META: (
min(self._num_sms, triton.cdiv(M, META["BLOCK_M"]) * triton.cdiv(N, META["BLOCK_N"])),
)
_fp8_gemm_desc_kernel[grid](x, w, s, y, M, N, K, self._num_sms)
return y
def forward(self, x: torch.Tensor) -> torch.Tensor:
# CUDA-graph the launch sequence (same treatment torch.compile
# reduce-overhead gets). Keyed on input/weight identity AND version:
# any new input tensor, in-place input write, or in-place weight
# update changes the key and takes the eager path, so a replay never
# serves stale data — the captured kernels re-read the live buffers.
if self._graphs_ok and not torch.cuda.is_current_stream_capturing():
w = self.weight
key = (
x.data_ptr(), x.shape, x._version,
w.data_ptr(), w._version,
self.weight_scale.data_ptr(), self.weight_scale._version,
)
ent = self._graphs.get(key)
if ent is not None and ent[0] is not None:
ent[0].replay()
return ent[1]
count = 0 if ent is None else ent[2]
if count >= 2:
# Pipeline is warm (ext built, Triton autotune settled): capture.
try:
torch.cuda.synchronize()
g = torch.cuda.CUDAGraph()
with torch.cuda.graph(g):
y = self._compute(x)
self._graphs[key] = (g, y, count)
g.replay()
return y
except Exception:
self._graphs_ok = False
else:
self._graphs[key] = (None, None, count + 1)
return self._compute(x)
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]
20260719_031420_or-fable_anthropic_claude-fable-5_01_fp8_gemm