GEMM + Amax (SM100)#
This is an experimental API and subject to change.
Overview#
Block-scaled GEMM + amax: A persistent, batched dense GEMM on NVIDIA Blackwell GPUs (SM100+) that supports low-precision inputs (FP8, FP4) with per-block scale factors, producing the full GEMM output C and global amax reduction. Implemented with CUTLASS/CUTE.
Inputs: quantized
AandB(FP8 or FP4), and corresponding scale-factor tensorsSFAandSFBthat dequantize along theKdimension in groups of sizesf_vec_size.Outputs: full GEMM result
CandAmax.
Shapes#
Inputs
A: shape(M, K, L)B: shape(N, K, L)SFA: shape(32, 4, ceil_div(M, 128), 4, ceil_div(K, 4·sf_vec_size), L)SFB: shape(32, 4, ceil_div(N, 128), 4, ceil_div(K, 4·sf_vec_size), L)
Outputs
C: shape(M, N, L)Amax: shape(1, 1, 1)
L is the batch dimension.
Equations#
Let block size along K be sf_vec_size ∈ {16, 32}. Dequantization is performed using the provided scale factors for groups of sf_vec_size along K (per M/N blocks defined by the atom tiling):
\( \hat{A}[m, k, l] = \operatorname{dequantize}(A[m, k, l], \text{SFA}, \text{sf_vec_size}) \)
\( \hat{B}[n, k, l] = \operatorname{dequantize}(B[n, k, l], \text{SFB}, \text{sf_vec_size}) \)
\( C[m, n, l] = \sum_{k} \hat{A}[m, k, l] \, \hat{B}[n, k, l] \)
\( \mathrm{Amax} = \max_{m, n, l} |C[m, n, l]| \)
Diagram#
A (MxKxL), SFA B (NxKxL), SFB
| dequantize(.; SFA) | dequantize(.; SFB)
v v
A_hat (MxKxL) B_hat (NxKxL)
\__ GEMM over K ______________________
\
C (MxNxL or packed)
|
+-- reduce: Amax = max |C|
|
v
Amax (1x1x1)
API Usage#
The tensor parameters are type-erased: torch tensors and JAX arrays are both accepted. torch is only imported when torch tensors/dtypes are passed, and jax only when JAX arrays are passed. Dtype parameters (c_dtype, acc_dtype) accept torch dtypes, numpy/ml_dtypes dtypes, dtype name strings, or cutlass types.
High-level wrapper#
result = gemm_amax_wrapper_sm100(
a_tensor,
b_tensor,
sfa_tensor,
sfb_tensor,
c_major="n",
c_dtype=torch.float32,
acc_dtype=torch.float32,
mma_tiler_mn=(128, 128),
cluster_shape_mn=(1, 1),
sf_vec_size=32,
stream=None,
)
c, amax = result
# Key access: result["c_tensor"], result["amax_tensor"]
Class API#
from cuda.bindings import driver as cuda
op = GemmAmaxSm100(
sample_a=a,
sample_b=b,
sample_sfa=sfa,
sample_sfb=sfb,
sample_c=c,
sample_amax=amax,
acc_dtype=torch.float32,
mma_tiler_mn=(128, 128),
cluster_shape_mn=(1, 1),
sf_vec_size=32,
)
assert op.check_support()
op.compile()
op.execute(a, b, sfa, sfb, c, amax, current_stream=None)
Using JAX arrays#
Two integration levels are available:
gemm_amax_jax_sm100(recommended for jitted programs) — an XLA custom call built oncudnn.jax.call(CuTeDSL’s nativecutlass.jaxbridge). The kernel runs on XLA’s compute stream (correctly ordered with surrounding ops), outputs are XLA-managed fresh arrays, and the call composes withjax.jit. No manual synchronization is needed. Requires thejaxdependency group (pip install --group jax; jax >= 0.5).
from cudnn import gemm_amax_jax_sm100
@jax.jit
def quantized_matmul(a, b, sfa, sfb):
c, amax = gemm_amax_jax_sm100(a, b, sfa, sfb, c_dtype=jnp.float32, sf_vec_size=32)
return c, amax
Calling it eagerly works but re-traces the custom call on every invocation; call it from inside a jitted function in hot loops.
The eager entry points below (
gemm_amax_wrapper_sm100,GemmAmaxSm100) also accept JAX arrays via DLPack. In hot loops prefer the class API with pre-allocated output buffers (~15 µs CPU per launch) over the wrapper — per-calljnpoutput allocation in the wrapper costs hundreds of µs of XLA dispatch.
JAX arrays are always row-major (C-contiguous) and immutable-by-contract, so the JAX contract differs from torch in a few ways (all entry points):
A/Bmust be k-major(M, K, 1)/(N, K, 1)C-contiguous arrays, and the batch dim must beL == 1(batch-outermost layouts are not expressible as JAX arrays).Csupportsc_major="n"only (the wrapper raises for"m").SFA/SFBare passed in the physical C-contiguous atom shape(L, ceil_div(MN, 128), ceil_div(K, 4·sf_vec_size), 32, 4, 4)— byte-identical in memory to the torch-style logical view above (which is this allocation permuted by(3, 4, 1, 5, 2, 0)). Both forms are accepted for either framework.Packed fp4 has no JAX dtype; the intended vehicle is a
uint8container tensor ((M, K // 2, 1)), but that container path is currently disabled kernel-side for torch and JAX alike. FP8 flavors (float8_e4m3fn,float8_e5m2,float8_e8m0fnuvia ml_dtypes) are fully supported.Eager entry points only: with no explicit stream, the kernel launches on the CUDA legacy default stream, which XLA does not track.
jax.block_until_ready(...)your inputs before calling, and synchronize the device (or the stream you passed) before reading the outputs. (gemm_amax_jax_sm100has neither caveat — XLA orders it on its own stream.)The eager wrapper allocates outputs with
jnp.empty/jnp.fulland the kernel writes into them via DLPack. This is outside JAX’s functional model: eager use only — do not call the wrapper underjax.jitor with donated buffers. Usegemm_amax_jax_sm100underjit.
import jax, jax.numpy as jnp
import ml_dtypes
import numpy as np
from cudnn import gemm_amax_wrapper_sm100
m, n, k, sf_vec_size = 512, 256, 256, 32
a = jax.device_put(np.random.randn(m, k, 1).astype(ml_dtypes.float8_e5m2))
b = jax.device_put(np.random.randn(n, k, 1).astype(ml_dtypes.float8_e5m2))
sfa = jax.device_put(np.ones((1, m // 128, k // (4 * sf_vec_size), 32, 4, 4), dtype=ml_dtypes.float8_e8m0fnu))
sfb = jax.device_put(np.ones((1, n // 128, k // (4 * sf_vec_size), 32, 4, 4), dtype=ml_dtypes.float8_e8m0fnu))
jax.block_until_ready((a, b, sfa, sfb))
c, amax = gemm_amax_wrapper_sm100(a, b, sfa, sfb, c_dtype=jnp.float32, sf_vec_size=sf_vec_size)
from cuda.bindings import runtime as cudart
cudart.cudaDeviceSynchronize() # kernel ran on the legacy stream, outside XLA's tracking
Parameters#
Input/Output tensors#
Input tensor A:
a_tensor(wrapper) orsample_a/a_tensor(class)Shape:
(M, K, L)Stride:
(1, M, M·K)form-major or(K, 1, M·K)fork-majorDtype:
{float4_e2m1fn_x2, uint8, float8_e4m3fn, float8_e5m2}(uint8is interpreted as packed fp4x2)
Input tensor B:
b_tensor(wrapper) orsample_b/b_tensor(class)Shape:
(N, K, L)Stride:
(1, N, N·K)forn-major or(K, 1, N·K)fork-majorDtype: Must match
A
Input tensor SFA:
sfa_tensor(wrapper) orsample_sfa/sfa_tensor(class)Shape:
(ATOM_M0, ATOM_M1, ceil_div(M, ATOM_M0·ATOM_M1), ATOM_K, ceil_div(K, ATOM_K·sf_vec_size), L)Dtype:
{float8_e8m0fnu, float8_e4m3fn, int8}(int8is interpreted asfloat8_e8m0fnu)
Input tensor SFB:
sfb_tensor(wrapper) orsample_sfb/sfb_tensor(class)Shape:
(ATOM_M0, ATOM_M1, ceil_div(N, ATOM_M0·ATOM_M1), ATOM_K, ceil_div(K, ATOM_K·sf_vec_size), L)Dtype:
{float8_e8m0fnu, float8_e4m3fn, int8}(int8is interpreted asfloat8_e8m0fnu)
Output tensor C:
result["c_tensor"](wrapper) orsample_c/c_tensor(class)Shape:
(M, N, L)Stride:
(1, M, M·N)form-major or(N, 1, M·N)forn-major. Provided asc_majorargument for wrapperDtype:
{float32, float16, bfloat16, float8_e5m2, float8_e4m3fn, float4_e2m1fn_x2, uint8}. Provided asc_dtypeargument for wrapper
Output tensor Amax:
result["amax_tensor"](wrapper) orsample_amax/amax_tensor(class)Shape:
(1, 1, 1)Dtype:
float32
Common parameters#
acc_dtype: torch.dtypeAccumulator dtype. Default:
torch.float32(only supported value)
mma_tiler_mn: Tuple[int, int]Kernel tile size
(TILE_M, TILE_N). Default:(128, 128)TILE_M ∈ {128};TILE_M = 256is currently disabledTILE_N ∈ {128, 256}
cluster_shape_mn: Tuple[int, int]Thread Block cluster shape
(CLUSTER_M, CLUSTER_N). Default:(1, 1)Constraints: values in
{1, 2, 4}
sf_vec_size: intSize of K-group per scale factor:
{16, 32}. Default:32
CUDA stream (
current_streamin class API,streamin wrapper)
Wrapper-specific parameters: gemm_amax_wrapper_sm100#
a_tensor,b_tensor,sfa_tensor,sfb_tensor: see Input/Output tensorsc_major: str: see Input/Output tensors. Default:"n"c_dtype: torch.dtype: see Input/Output tensors. Default:torch.float32
Wrapper return values#
Returns a TupleDict with keys:
c_tensor: GEMM output tensorCamax_tensor: Max-abs reduction output
Tuple unpacking order is: (c_tensor, amax_tensor).
Class-specific parameters: GemmAmaxSm100#
GemmAmaxSm100 (constructor)#
sample_a,sample_b,sample_sfa,sample_sfb,sample_c,sample_amax: see Input/Output tensors
GemmAmaxSm100.execute#
a_tensor,b_tensor,sfa_tensor,sfb_tensor,c_tensor,amax_tensor: see Input/Output tensors
Support surface and constraints#
Layouts and strides#
For
A/B ∈ {float4_e2m1fn_x2, uint8}(packed FP4),AandBmust bek-major.For
C ∈ {float4_e2m1fn_x2, uint8}(packed FP4),Cmust ben-major.For all
float4_e2m1fn_x2/uint8cases, the innermost tensor dimension will be divided by 2 due to 2x packing. i.e.Awould be shaped(M, K // 2, L)instead of(M, K, L).A,B,Cmust be 16-byte aligned along the contiguous dimension.
Dtypes#
A/Bmust have the same dtype.sf_vec_size ∈ {16, 32}with coupling:sf_dtype == float8_e4m3fnis unsupported withsf_vec_size == 32A/B ∈ {float8_e4m3fn, float8_e5m2}is unsupported withsf_vec_size == 16
A/B ∈ FP8andC ∈ FP8together are currently disabledC ∈ {float4_e2m1fn_x2, uint8}requiresA/B ∈ {float4_e2m1fn_x2, uint8}
Tiling and cluster#
A/B ∈ {float4_e2m1fn_x2, uint8}andN_tile == 256requiresK > 128mma_tiler_mn == (128, 256),sf_vec_size == 16,C ∈ {float32, float16, bfloat16}is currently disabled
Shapes and divisibility#
SFA/SFBshapes must follow the atom tiling andsf_vec_sizerules aboveWhen
Cis packed FP4, use(M, ceil_div(N, 2), L)andn-major strides
Environment#
Requires CUDA with SM100+ compute capability
All tensors must reside on the same CUDA device
JAX-specific constraints#
L == 1;A/Bk-major;Cn-major onlySFA/SFBin the physical atom shape(L, MN', K', 32, 4, 4)(see “Using JAX arrays”)The wrapper/class entry points are eager-only (use
gemm_amax_jax_sm100underjax.jit); synchronize before reading outputs
Usage examples#
For usage examples, see test cases in test/python/fe_api/gemm/test_gemm_amax.py (torch) and test/python/fe_api/gemm/test_gemm_amax_jax.py (JAX)