KernelBench hard · H100
FP8 GEMM Claude Opus 5
manually audited: clean
Hand-written SM90a WGMMA+TMA fp8 GEMM: wgmma.mma_async e4m3 with fp32 accumulation, 128B-swizzle SMEM tiles, mbarrier multistage pipeline, per-channel scale folded into SMEM-staged epilogue, funnel-shift pad kernel for K%16!=0 (the 4127 shape), L2 evict_first on the streaming operand for decode shapes. Only caches are content-independent (TMA descriptors keyed on ptr/shape/box; weight-pad copy keyed on data_ptr + _version, so in-place edits re-pad). Grader files Read-only, template_mutated false, no KBH_NUMERIC_STRESS games, zero foreign-archive access (cross-run strings are ps/lock listings of the same-wave sibling). Passed full check.py + stress on the sequential isolated re-grade (idle H100, 2026-07-27); clean 0.3032 vs contended 0.2989. Notable: epilogue comment measures the power-limit effect of partial-sector stores (SM clock 1058->1327 MHz with epilogue removed) -- energy-aware optimization.
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(54.8% · 46.8% · 5.3% · 61.9%) = 30.3%
Kernel source (redacted)
"""FP8 e4m3 GEMM for SM90 (H100): y = (x @ w.T) * weight_scale, bf16 out.
Hand-written Hopper kernel: TMA (cp.async.bulk.tensor) feeds a multistage SMEM
pipeline consumed by wgmma.mma_async.m64nNk32.f32.e4m3.e4m3 warpgroup MMAs, so
both operands stay fp8 and the tensor cores run at the fp8 rate with fp32
accumulation. The per-output-channel dequant scale is folded into the epilogue.
Layout notes (the parts that are easy to get wrong):
* wgmma fp8 is TN-only, so both A (M,K) and B (N,K) are K-major -- which is
exactly how x and weight already sit in memory. No transposes.
* SMEM tiles use the 128B-swizzle atom that TMA writes natively:
offset(r,k) = r*128 + (k ^ ((r % 8) * 16)), 8-row atoms every 1024B.
The matching GMMA descriptor is LBO=1 (16B), SBO=64 (1024B),
base_offset=0 -- which requires a 1024B-aligned tile base in SMEM.
* cuTensorMapEncodeTiled requires globalStrides % 16 == 0, so a K that is
not a multiple of 16 (e.g. 4127) is padded up first; the K tail inside a
128-wide tile needs no work because TMA zero-fills out-of-bounds elements
and zeros contribute nothing to the accumulator.
"""
import os
import sys
import torch
import torch.nn as nn
# --------------------------------------------------------------------------
# wgmma wrappers. One inline-asm wrapper per BN (N/2 fp32 accumulator regs).
# --------------------------------------------------------------------------
def _wgmma_wrappers(ns):
out = []
for n in ns:
nr = n // 2
regs = ", ".join("%%%d" % i for i in range(nr))
outs = ",\n ".join(
", ".join('"+f"(d[%d])' % j for j in range(i, min(i + 4, nr)))
for i in range(0, nr, 4)
)
out.append(
"""
__device__ __forceinline__ void wgmma_n{n}(float (&d)[{nr}], uint64_t da, uint64_t db, int sd) {{
asm volatile(
"{{\\n"
".reg .pred p;\\n"
"setp.ne.b32 p, %{p2}, 0;\\n"
"wgmma.mma_async.sync.aligned.m64n{n}k32.f32.e4m3.e4m3 "
"{{{regs}}}, %{p0}, %{p1}, p, 1, 1;\\n"
"}}\\n"
: {outs}
: "l"(da), "l"(db), "r"(sd));
}}
""".format(n=n, nr=nr, regs=regs, outs=outs, p0=nr, p1=nr + 1, p2=nr + 2)
)
return "".join(out)
_CUDA_SRC = r"""
#include <torch/extension.h>
#include <ATen/cuda/CUDAContext.h>
#include <ATen/cuda/EmptyTensor.h>
#include <cuda.h>
#include <cuda_runtime.h>
#include <cuda_bf16.h>
#include <cstdint>
#define DEVI __device__ __forceinline__
DEVI uint32_t smem_u32(void const* p) {
return static_cast<uint32_t>(__cvta_generic_to_shared(p));
}
DEVI void mbar_init(uint64_t* b, uint32_t cnt) {
asm volatile("mbarrier.init.shared::cta.b64 [%0], %1;" :: "r"(smem_u32(b)), "r"(cnt));
}
DEVI void fence_bar_init() {
asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory");
}
DEVI void mbar_arrive(uint64_t* b) {
asm volatile("mbarrier.arrive.shared::cta.b64 _, [%0];" :: "r"(smem_u32(b)) : "memory");
}
DEVI void mbar_expect(uint64_t* b, uint32_t bytes) {
asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;"
:: "r"(smem_u32(b)), "r"(bytes) : "memory");
}
// try_wait in a C++ loop: keeps the PTX label-free so it can be inlined twice.
DEVI void mbar_wait(uint64_t* b, uint32_t parity) {
uint32_t a = smem_u32(b), ok = 0;
while (!ok) {
asm volatile("{ .reg .pred p;"
" mbarrier.try_wait.parity.shared::cta.b64 p, [%1], %2;"
" selp.b32 %0, 1, 0, p; }"
: "=r"(ok) : "r"(a), "r"(parity) : "memory");
}
}
DEVI void tma_2d(void const* desc, uint64_t* bar, void* dst, int c0, int c1) {
asm volatile("cp.async.bulk.tensor.2d.shared::cluster.global"
".mbarrier::complete_tx::bytes [%0], [%1, {%3, %4}], [%2];"
:: "r"(smem_u32(dst)), "l"(reinterpret_cast<uint64_t>(desc)),
"r"(smem_u32(bar)), "r"(c0), "r"(c1) : "memory");
}
// The harness dirties 128 MB of L2 immediately before the timed call, so a
// bandwidth-bound kernel that allocates its streaming reads into L2 also pays to
// write those dirty lines back. Tagging the streaming operand evict_first makes
// the kernel's own (clean) lines the preferred victims instead: worth 15% on the
// M=32 shape, where the weight matrix is read once and never reused.
DEVI uint64_t l2_evict_first() {
uint64_t p;
asm volatile("createpolicy.fractional.L2::evict_first.b64 %0, 1.0;" : "=l"(p));
return p;
}
DEVI void tma_2d_h(void const* desc, uint64_t* bar, void* dst, int c0, int c1, uint64_t pol) {
asm volatile("cp.async.bulk.tensor.2d.shared::cluster.global"
".mbarrier::complete_tx::bytes.L2::cache_hint [%0], [%1, {%3, %4}], [%2], %5;"
:: "r"(smem_u32(dst)), "l"(reinterpret_cast<uint64_t>(desc)),
"r"(smem_u32(bar)), "r"(c0), "r"(c1), "l"(pol) : "memory");
}
DEVI void wgmma_fence() { asm volatile("wgmma.fence.sync.aligned;" ::: "memory"); }
DEVI void wgmma_commit() { asm volatile("wgmma.commit_group.sync.aligned;" ::: "memory"); }
template <int N> DEVI void wgmma_wait() {
asm volatile("wgmma.wait_group.sync.aligned %0;" :: "n"(N) : "memory");
}
// GMMA SMEM descriptor for a K-major 128B-swizzle tile (LBO=16B, SBO=1024B).
DEVI uint64_t gmma_desc(uint32_t addr) {
return (uint64_t)((addr >> 4) & 0x3FFFu) | ((uint64_t)1 << 16) | ((uint64_t)64 << 32)
| ((uint64_t)1 << 62);
}
WGMMA_WRAPPERS
template <int BN> DEVI void wgmma_n(float (&d)[BN / 2], uint64_t a, uint64_t b, int sd);
template <> DEVI void wgmma_n<256>(float (&d)[128], uint64_t a, uint64_t b, int sd) { wgmma_n256(d, a, b, sd); }
template <> DEVI void wgmma_n<96>(float (&d)[48], uint64_t a, uint64_t b, int sd) { wgmma_n96(d, a, b, sd); }
// --------------------------------------------------------------------------
// y[m, n] = sum_k x[m, k] * w[n, k] * scale[n]
// One CTA per (BM x BN) output tile; NWG warpgroups split BM into 64-row slabs.
// --------------------------------------------------------------------------
template <int BM, int BN, int BK, int STAGES, int NWG, int WAIT, int GROUP_M, int L2H = 0>
__global__ void __launch_bounds__(128 * NWG)
kgemm(const __grid_constant__ CUtensorMap dA, const __grid_constant__ CUtensorMap dB,
const float* __restrict__ sc, __nv_bfloat16* __restrict__ Y,
int M, int N, int num_k, int tiles_m, int tiles_n) {
constexpr int NACC = BN / 2;
constexpr int ASZ = BM * BK, BSZ = BN * BK;
constexpr int KB = BK / 32;
constexpr int NTHR = 128 * NWG;
static_assert(BM == 64 * NWG, "one warpgroup per 64 rows");
extern __shared__ uint8_t smem_raw[];
uint32_t a0 = smem_u32(smem_raw);
uint8_t* base = smem_raw + ((((a0 + 1023) & ~1023u)) - a0); // wgmma needs 1024B
uint8_t* sA = base;
uint8_t* sB = base + STAGES * ASZ;
uint64_t* bfull = (uint64_t*)(base + STAGES * (ASZ + BSZ));
uint64_t* bempty = bfull + STAGES;
float* sScale = (float*)(bempty + STAGES);
const int tid = threadIdx.x;
const int wg = tid >> 7;
const int lane = tid & 31;
const int warp = (tid >> 5) & 3;
// Group-swizzled tile order: GROUP_M row-tiles share the same B stripes,
// which keeps the working set of a wave inside L2.
int tile = blockIdx.x;
int per_group = GROUP_M * tiles_n;
int gi = tile / per_group;
int gsz = min(GROUP_M, tiles_m - gi * GROUP_M);
int r = tile - gi * per_group;
const int mt = gi * GROUP_M + (r % gsz);
const int nt = r / gsz;
if (tid == 0) {
#pragma unroll
for (int s = 0; s < STAGES; ++s) { mbar_init(&bfull[s], 1); mbar_init(&bempty[s], NWG); }
fence_bar_init();
}
// The scale fetch is a cold DRAM read and the barrier below is what releases the
// prologue TMA, so staging it straight into SMEM would put a ~600ns memory round
// trip in front of every CTA's first copy. Issue the load into a register here and
// only sink it to SMEM once the TMA is on its way: worth ~1% on every shape.
static_assert(BN <= NTHR, "one scale staged per thread");
int sn = nt * BN + tid;
float sv = (tid < BN && sn < N) ? sc[sn] : 0.f;
__syncthreads();
const uint32_t txb = ASZ + BSZ;
constexpr int AHEAD = STAGES - WAIT; // stages the producer runs in front
int ps = 0;
uint32_t pep = 1;
uint64_t polB = 0;
if constexpr (L2H) polB = l2_evict_first();
// B is the streaming operand: with L2H it is fetched with an evict_first hint so
// it never displaces (or forces the writeback of) anything else in L2.
auto tmaB = [&](int st, int kn) {
if constexpr (L2H) tma_2d_h(&dB, &bfull[st], sB + st * BSZ, kn * BK, nt * BN, polB);
else tma_2d(&dB, &bfull[st], sB + st * BSZ, kn * BK, nt * BN);
};
if (tid == 0) {
#pragma unroll 1
for (int kn = 0; kn < AHEAD && kn < num_k; ++kn) {
mbar_expect(&bfull[ps], txb);
tma_2d(&dA, &bfull[ps], sA + ps * ASZ, kn * BK, mt * BM);
tmaB(ps, kn);
if (++ps == STAGES) { ps = 0; pep ^= 1; }
}
}
if (tid < BN) sScale[tid] = sv;
__syncthreads(); // the epilogue reads every other thread's scale
float acc[NACC];
const uint32_t aoff = smem_u32(sA) + wg * (64 * BK);
const uint32_t boff = smem_u32(sB);
uint64_t dsa[STAGES], dsb[STAGES];
#pragma unroll
for (int s = 0; s < STAGES; ++s) {
dsa[s] = gmma_desc(aoff + s * ASZ);
dsb[s] = gmma_desc(boff + s * BSZ);
}
const bool leader = (tid & 127) == 0;
int cs = 0, es = 0;
uint32_t cph = 0;
#pragma unroll 1
for (int kt = 0; kt < num_k; ++kt) {
mbar_wait(&bfull[cs], cph);
uint64_t da = dsa[cs], db = dsb[cs];
wgmma_fence();
// Every wgmma here must sit in straight-line code: ptxas (C7520) inserts a
// warpgroup arrive around any wgmma in a divergent path, which serializes the
// async MMAs and costs ~20%. That rules out skipping the zero-filled wgmmas
// of a partial last k-block -- they are cheaper than the barrier would be.
#pragma unroll
for (int kb = 0; kb < KB; ++kb)
wgmma_n<BN>(acc, da + kb * 2, db + kb * 2, (kt == 0 && kb == 0) ? 0 : 1);
wgmma_commit();
wgmma_wait<WAIT>();
if (kt >= WAIT) { // stage cs-WAIT is free to refill
if (leader) mbar_arrive(&bempty[es]);
if (++es == STAGES) es = 0;
}
if (tid == 0) {
int kn = kt + AHEAD;
if (kn < num_k) {
if (kn >= STAGES) mbar_wait(&bempty[ps], pep);
mbar_expect(&bfull[ps], txb);
tma_2d(&dA, &bfull[ps], sA + ps * ASZ, kn * BK, mt * BM);
tmaB(ps, kn);
if (++ps == STAGES) { ps = 0; pep ^= 1; }
}
}
if (++cs == STAGES) { cs = 0; cph ^= 1; }
}
wgmma_wait<0>();
// Epilogue. wgmma m64nN accumulator r maps to
// row = 16*warp + lane/4 + 8*((r%4)/2), col = 8*(r/4) + 2*(lane%4) + (r%2)
// so each thread owns two column-adjacent values on two rows 8 apart.
//
// Writing that layout straight to global costs one 4B store per thread per
// column block: 128 B of line touched by 32 lanes spread over 8 rows, i.e.
// 16 B sectors that are only half used. The traffic is absorbed by L2, so it
// barely shows up in per-CTA time -- but the *energy* of 4096 partial sectors
// per tile is what pushes the part into its 350 W cap and drops the SM clock
// (measured: 1058 -> 1327 MHz with the epilogue removed). Staging the tile
// through SMEM first turns it into 512 full 128 B lines and buys back ~8%.
if constexpr (BN == 256) {
if (nt * BN + BN <= N) { // CTA-uniform: ragged-N tiles take the path below
// wgmma_wait<0> is per *warpgroup*; wg1 may still be reading sA/sB.
__syncthreads();
__nv_bfloat16* sY = (__nv_bfloat16*)base; // BM*BN*2 = 64 KB, reuses the pipe
const int mq = wg * 64 + warp * 16 + (lane >> 2);
const int sq = (lane & 3) << 1;
// XOR-swizzle the 16 B chunk index by row&7: the 8 rows a warp writes at
// once then land on 8 distinct chunks, covering all 32 banks.
#pragma unroll
for (int i = 0; i < NACC / 4; ++i) {
float g0 = sScale[sq + i * 8], g1 = sScale[sq + i * 8 + 1];
*(__nv_bfloat162*)(sY + mq * BN + ((i ^ (mq & 7)) << 3) + sq) =
__floats2bfloat162_rn(acc[4 * i] * g0, acc[4 * i + 1] * g1);
const int q2 = mq + 8;
*(__nv_bfloat162*)(sY + q2 * BN + ((i ^ (q2 & 7)) << 3) + sq) =
__floats2bfloat162_rn(acc[4 * i + 2] * g0, acc[4 * i + 3] * g1);
}
__syncwarp(); // each warp reads back only the 16 rows it just wrote
const int rb = wg * 64 + warp * 16;
#pragma unroll
for (int j = 0; j < 16; ++j) {
const int rl = rb + j, rg = mt * BM + rl;
const int cq = (lane ^ (rl & 7)) << 3; // chunk physically holding logical chunk `lane`
if (rg < M)
*(uint4*)(Y + (size_t)rg * N + nt * BN + (lane << 3)) =
*(const uint4*)(sY + rl * BN + cq);
}
return;
}
}
const int r0 = mt * BM + wg * 64 + warp * 16 + (lane >> 2);
const int c0 = nt * BN + ((lane & 3) << 1);
const int s0 = (lane & 3) << 1;
__nv_bfloat16* y0 = Y + (size_t)r0 * N;
__nv_bfloat16* y1 = y0 + (size_t)8 * N;
const bool ok0 = r0 < M, ok1 = r0 + 8 < M;
#pragma unroll
for (int i = 0; i < NACC / 4; ++i) {
float g0 = sScale[s0 + i * 8], g1 = sScale[s0 + i * 8 + 1];
int c = c0 + i * 8;
if (c + 1 < N) {
if (ok0) *(__nv_bfloat162*)(y0 + c) = __floats2bfloat162_rn(acc[4 * i] * g0, acc[4 * i + 1] * g1);
if (ok1) *(__nv_bfloat162*)(y1 + c) = __floats2bfloat162_rn(acc[4 * i + 2] * g0, acc[4 * i + 3] * g1);
} else if (c < N) {
if (ok0) y0[c] = __float2bfloat16(acc[4 * i] * g0);
if (ok1) y1[c] = __float2bfloat16(acc[4 * i + 2] * g0);
}
}
}
// ------------------------------------------------------------------ host side
static CUtensorMap make_map(const void* p, int rows, int cols, int box_r, int box_c) {
CUtensorMap m{};
uint64_t gd[2] = {(uint64_t)cols, (uint64_t)rows};
uint64_t gs[1] = {(uint64_t)cols};
uint32_t bd[2] = {(uint32_t)box_c, (uint32_t)box_r};
uint32_t es[2] = {1, 1};
CUresult rc = cuTensorMapEncodeTiled(&m, CU_TENSOR_MAP_DATA_TYPE_UINT8, 2, (void*)p, gd, gs, bd,
es, CU_TENSOR_MAP_INTERLEAVE_NONE, CU_TENSOR_MAP_SWIZZLE_128B,
CU_TENSOR_MAP_L2_PROMOTION_L2_128B,
CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE);
TORCH_CHECK(rc == CUDA_SUCCESS, "cuTensorMapEncodeTiled failed ", (int)rc);
return m;
}
// Encoding a tensor map is a ~3us driver call; the descriptor depends only on
// (address, shape, box), so cache it -- a recycled allocation is still valid.
// Returned BY VALUE on purpose: two live descriptors are needed per launch, and
// a reference into the table would dangle if the second lookup evicted its slot.
struct MapKey { const void* p; int rows, cols, br, bc; };
static CUtensorMap get_map(const void* p, int rows, int cols, int br, int bc) {
constexpr int NC = 64;
static thread_local MapKey keys[NC] = {};
static thread_local CUtensorMap maps[NC];
static thread_local int n = 0, rr = 0;
for (int i = 0; i < n; ++i)
if (keys[i].p == p && keys[i].rows == rows && keys[i].cols == cols && keys[i].br == br &&
keys[i].bc == bc)
return maps[i];
int i = n < NC ? n++ : rr; // round-robin once full
if (n == NC) rr = (rr + 1 == NC) ? 0 : rr + 1;
keys[i] = MapKey{p, rows, cols, br, bc};
maps[i] = make_map(p, rows, cols, br, bc);
return maps[i];
}
// Row-wise pad copy: dst (rows x dcols) <- src (rows x scols) with
// dcols = round_up(scols, 16); the [scols, dcols) tail is zero-filled.
//
// A byte-per-thread copy only reaches ~830 GB/s: a single 1-byte load in flight
// per thread cannot cover HBM latency. Source rows are not 16B-aligned (that is
// the whole reason this kernel exists), but a row's byte offset within its 16B
// word is uniform along the row, so 16B-aligned loads can be re-aligned with a
// funnel shift and stored 16B at a time -- ~2.6x faster.
//
// This is at the card's copy ceiling: 33.8 MB in 27.6us (1.23 TB/s) matches a
// flat contiguous copy of the same size to 0.1us. Four rows per block (more
// loads in flight), streaming cache hints and cudaMemcpy2DAsync (0.55 TB/s) are
// all no better. It still costs ~39us in front of the GEMM rather than 27.6, but
// that gap is not recoverable by overlapping: run concurrently on a side stream
// with no dependency edge the pair takes 189.9us against 190.9 serialized, and
// chunking both over rows so each GEMM chunk starts as its rows land costs a
// further 5-12us. The two simply contend for the whole machine.
#define PICK(Q) \
o.x = __funnelshift_r(wv[(Q) + 0], wv[(Q) + 1], b); \
o.y = __funnelshift_r(wv[(Q) + 1], wv[(Q) + 2], b); \
o.z = __funnelshift_r(wv[(Q) + 2], wv[(Q) + 3], b); \
o.w = __funnelshift_r(wv[(Q) + 3], wv[(Q) + 4], b);
__global__ void kpad(const uint8_t* __restrict__ src, uint8_t* __restrict__ dst, int scols,
int dcols) {
const uint8_t* s = src + (size_t)blockIdx.x * scols;
uint8_t* d = dst + (size_t)blockIdx.x * dcols;
const int sh = (int)((uintptr_t)s & 15); // uniform for the whole row
const uint8_t* sa = s - sh; // 16B-aligned
const int q = sh >> 2, b = (sh & 3) * 8;
const int nch = dcols >> 4;
for (int c = threadIdx.x; c < nch; c += blockDim.x) {
const int off = c << 4;
if (off + 16 <= scols) { // wholly inside the row
uint32_t wv[8];
*(uint4*)&wv[0] = *(const uint4*)(sa + off);
*(uint4*)&wv[4] = *(const uint4*)(sa + off + 16);
uint4 o;
switch (q) { // uniform across the block
case 0: PICK(0) break;
case 1: PICK(1) break;
case 2: PICK(2) break;
default: PICK(3)
}
*(uint4*)(d + off) = o;
} else { // final chunk: mask the tail
uint8_t v[16];
#pragma unroll
for (int j = 0; j < 16; ++j) v[j] = (off + j < scols) ? s[off + j] : (uint8_t)0;
*(uint4*)(d + off) = *(const uint4*)v;
}
}
}
#undef PICK
// A launch is 3.75us of host time here, and the harness starts its timer on an
// idle stream, so all of it is charged to the kernel. Going straight to the
// driver with a CUfunction resolved once saves 0.65us of that -- the runtime
// otherwise maps the host stub back to a function and re-marshals the arguments
// on every call. Passing kernelParams rather than a packed buffer leaves the
// parameter layout to the driver, so there is no hand-coded ABI here.
static CUfunction cufunc(const void* fn, int smem) {
if (smem) cudaFuncSetAttribute(fn, cudaFuncAttributeMaxDynamicSharedMemorySize, smem);
cudaFunction_t cf = nullptr;
cudaGetFuncBySymbol(&cf, fn);
return (CUfunction)cf;
}
#define LAUNCH(BM, BN, BK, ST, NWG, WAIT, GM, L2H) \
do { \
constexpr int smem = ST * (BM * BK + BN * BK) + 16 * ST + BN * 4 + 1024; \
static CUfunction fh = \
cufunc((const void*)kgemm<BM, BN, BK, ST, NWG, WAIT, GM, L2H>, smem); \
int tm = (M + BM - 1) / BM, tn = (N + BN - 1) / BN; \
int nk = (Kp + BK - 1) / BK; \
int m = M, n = N; \
CUtensorMap dA = get_map(xu.data_ptr(), M, Kp, BM, BK); \
CUtensorMap dB = get_map(wu.data_ptr(), N, Kp, BN, BK); \
void* args[] = {&dA, &dB, &sc, &yp, &m, &n, &nk, &tm, &tn}; \
cuLaunchKernel(fh, tm * tn, 1, 1, 128 * NWG, 1, 1, smem, st, args, nullptr); \
} while (0)
at::Tensor fp8_gemm(at::Tensor x, at::Tensor w, at::Tensor s) {
TORCH_CHECK(x.is_cuda() && w.is_cuda() && s.is_cuda(), "cuda tensors required");
TORCH_CHECK(x.dim() == 2 && w.dim() == 2, "2-d operands required");
TORCH_CHECK(x.scalar_type() == at::kFloat8_e4m3fn && w.scalar_type() == at::kFloat8_e4m3fn,
"fp8_e4m3 operands required");
if (!x.is_contiguous()) x = x.contiguous();
if (!w.is_contiguous()) w = w.contiguous();
const int M = x.size(0), K = x.size(1), N = w.size(0);
TORCH_CHECK(w.size(1) == K, "K mismatch");
const int Kp = (K + 15) & ~15; // TMA global stride must be a multiple of 16B
at::Tensor xu = x, wu = w;
if (Kp != K) {
// kpad writes every byte of each padded row (tail included), so the
// destinations need no pre-zeroing; the weight copy is redone whenever the
// weight buffer is written to in place.
static at::Tensor xpad, wpad;
static void* wkey = nullptr;
static int64_t wver = -1;
auto st0 = at::cuda::getCurrentCUDAStream();
// The x copy is the launch the harness actually sees on this shape: the GEMM
// that follows it is enqueued while the copy is still running.
static CUfunction fpad = cufunc((const void*)kpad, 0);
int kc = K, kpc = Kp;
auto pad = [&](const void* src, void* dst, int rows) {
const uint8_t* sp = (const uint8_t*)src;
uint8_t* dp = (uint8_t*)dst;
void* a[] = {&sp, &dp, &kc, &kpc};
cuLaunchKernel(fpad, rows, 1, 1, 256, 1, 1, 0, st0, a, nullptr);
};
if (!xpad.defined() || xpad.size(0) != M || xpad.size(1) != Kp)
xpad = at::empty({M, Kp}, x.options().dtype(at::kByte));
pad(x.data_ptr(), xpad.data_ptr(), M);
if (wkey != w.data_ptr() || wver != (int64_t)w._version() || !wpad.defined() ||
wpad.size(0) != N || wpad.size(1) != Kp) {
if (!wpad.defined() || wpad.size(0) != N || wpad.size(1) != Kp)
wpad = at::empty({N, Kp}, w.options().dtype(at::kByte));
pad(w.data_ptr(), wpad.data_ptr(), N);
wkey = w.data_ptr();
wver = (int64_t)w._version();
}
xu = xpad;
wu = wpad;
}
// The harness times one call on an idle GPU, so every host microsecond before
// the launch is charged to the kernel: ~7us of the shortest shape's 54us is CPU
// enqueue. empty_cuda is the allocator path at::empty dispatches to, minus the
// dispatcher (0.5us cheaper); sc is held in a named tensor so a non-contiguous
// scale's temporary copy outlives the launch.
at::Tensor y(at::detail::empty_cuda({M, N}, at::kBFloat16, x.device(), c10::nullopt));
auto st = at::cuda::getCurrentCUDAStream();
at::Tensor sct = s.contiguous();
const float* sc = (const float*)sct.data_ptr();
__nv_bfloat16* yp = (__nv_bfloat16*)y.data_ptr();
if (M <= 64) {
// Decode-shaped: pure DRAM streaming, so use narrow tiles (one warpgroup,
// deep pipeline) to spread the weight rows over every SM, and keep the
// single-use weight out of L2 (15% here; a wash on the compute-bound shapes,
// which do reuse B across the row-tiles of a group).
LAUNCH(64, 96, 128, 6, 1, 2, 1, 1);
} else {
LAUNCH(128, 256, 128, 4, 2, 1, 16, 0);
}
return y;
}
"""
def _build():
from torch.utils.cpp_extension import load_inline
os.environ["TORCH_CUDA_ARCH_LIST"] = "9.0a" # wgmma needs the 'a' target
src = _CUDA_SRC.replace("WGMMA_WRAPPERS", _wgmma_wrappers((256, 96)))
inc = []
try: # torch's headers need these
import pybind11
inc = ["-I" + pybind11.get_include()]
except Exception:
pass
return load_inline(
name="fp8_gemm_sm90a",
cpp_sources="#include <torch/extension.h>\n"
"at::Tensor fp8_gemm(at::Tensor, at::Tensor, at::Tensor);",
cuda_sources=src,
functions=["fp8_gemm"],
extra_cflags=["-O3"] + inc,
extra_cuda_cflags=["-O3", "--use_fast_math"] + inc,
extra_ldflags=["-lcuda"],
verbose=False,
)
_EXT = None
try:
_EXT = _build()
except Exception as _e: # pragma: no cover
print("fp8_gemm: CUDA build failed (%s); using torch fallback" % _e, file=sys.stderr)
class Model(nn.Module):
"""y = ((x @ w.T) * weight_scale).to(bf16) with an fp8 x fp8 wgmma kernel."""
_bound = None # cached (weight, weight_scale); see _apply
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) / 448.0).clamp(min=1e-12)
self.register_buffer("weight", (w.float() / s).to(torch.float8_e4m3fn))
self.register_buffer("weight_scale", s.squeeze(1).to(torch.float32))
def _apply(self, *args, **kwargs):
# .to()/.cuda() swap in new buffer tensors, so drop the cached handles.
# (load_state_dict copies in place and keeps them valid.)
self._bound = None
return super()._apply(*args, **kwargs)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# At the decode shape the whole GEMM is ~45 us, so the ~2 us that two
# nn.Module buffer lookups cost per call is worth caching away.
b = self._bound
if b is None:
b = self._bound = (self.weight, self.weight_scale)
if _EXT is not None and x.is_cuda:
return _EXT.fp8_gemm(x, b[0], b[1])
y = (x.to(torch.bfloat16) @ b[0].to(torch.bfloat16).T).float()
return (y * b[1][None, :]).to(torch.bfloat16)
# Same reason: nn.Module.__call__ spends ~2.5 us per call on hook
# bookkeeping this model never uses. forward() stays the implementation.
__call__ = forward
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]
20260725_063736_or-opus_anthropic_claude-opus-5_01_fp8_gemm