API Card: supermma#
Note
This chapter is part of the agent-targeted Agent BLAS Performance Guide and is also published as Markdown for
agent consumption, in the markdown/ folder of the HTML documentation tree.
supermma is the warp-level tensor-core instruction class based on mma.sync. It is available through
both the per-tile BLAS().execute(...) API and the pipeline API. A pipeline can stage operands with TMA or
cooperative copies while its computation still uses supermma; choosing a pipeline does not by itself select
wgmma or tcgen05. Some GPUs have neither of those instruction classes — Ampere, Ada, and the
consumer Blackwell parts SM 120 / SM 121 — so on them every tensor-core GEMM, pipelined or not, uses
supermma. Precisions without a supported tensor-core instruction use the separate FMA class.
This card describes the shared-memory operands and register accumulators, then shows both APIs. Two shipped
examples show the per-tile API: example/cublasdx/01_gemm_introduction/introduction_example.cu (the
three execute forms, plain layouts, double) and
example/cublasdx/10_gemm_block_performance/single_gemm_performance.hpp (suggested layouts with the suggested
accumulator, half, MaxAlignment(), 128 threads: the configuration this card describes).
Descriptor. Precision, SM, tile shape and thread count determine whether a tensor-core instruction is
available. WithPipeline() enables the pipeline API; the additional gates in the Hopper and Blackwell cards determine whether
the computation uses wgmma or tcgen05 instead. The descriptor below uses supermma with either API.
Precisionon tensor cores:halfinputs with ahalforfloataccumulator from SM 75;bfloat16withfloat,tf32withfloat,int8/uint8(any pairing) withint32anddoublefrom SM 80;fp8(e4m3ore5m2, any pairing) withhalforfloatfrom SM 89.The complex
Typeof each of those triples is on the table as well. Everything else is FMA:floatinputs on every SM (Precision<float>is never promoted totf32; ask forcublasdx::tfloat32_tand accept the rounding),fp8below SM 89,bfloat16,tf32and 8-bit integers below SM 80.On SM 1000, 1030 and 1070 real
floatFMA GEMMs can issuefma.f32x2(SASSFFMA2, two results per instruction), so acceptFFMA2as well asFFMAin the SASS check below.BlockDima power of two from 32 to 1024 for the suggested layouts and accumulator to exist; any count in that range compiles, but a non-power-of-two block gets the plain layouts and the default accumulator.Without
BlockDimthe library’ssuggested_block_dimapplies; for a matrix-vector product (M == 1orN == 1) it isM * Nrounded up to whole warps and clamped to[32, 256].With an explicit
BlockDima matrix-vector product uses at mostM * Nthreads (whole warps above 32) and the rest are inactive; this cap does not apply underWithPipeline()or to complex integer types, which keep everyBlockDimthread.Sizedivisible by the warp tiling: the warps form a near-square grid (2 x 2 for 128 threads) and each covers a16 x 8instruction, so with 128 threadsMmust be a multiple of 32 andNof 16 (Mof 16 andNof 32 whenN > M);Ka multiple of 16 for 16-bit inputs, of 8 fortf32, of 32 for 8-bit inputs and of 4 fordouble.Scalar FMA is chosen only for
M == 1orN == 1(matrix-vector) andK == 1(outer product), for every precision; any other shape, including a small tile, uses the tensor-core instruction when the precision has one.The library can adjust the thread tiling to fit
MandN, so confirm the generated instruction in the SASS rather than inferring it from divisibility alone.Alignment<16, 16, 16>(=MaxAlignment()) forldmatrixonAandB, and forstmatrixonCwhenChas the operands’ element size, on SM 90 and up. The default alignment is the element size, so ahalfdescriptor without it stayssupermmabut loads shared memory with plain vector loads: a gate on the loads, not on the class.No
LeadingDimensionand noexperimental::Tile: either one silently turns the suggested layouts and the suggested accumulator into the plain ones.Arrangement: free for 16-bit operands (ldmatrix.trans);tf32and 8-bit operands getldmatrixonly K-major, that is TN. On the FMA pathfloatwants a column-majorAand a row-majorB, the orientations its shared loads vectorize.floatandcomplex<float>FMA with both operands K-major (row-majorA, column-majorB) and plain shared layouts whose row stride is a multiple of 32 bytes (that is, the leading dimension times the element size) put every row of a tile on the same banks;the library re-tiles the thread grid along
C’s contiguous axis to avoid the conflicts, and FMA GEMMs with large per-thread fragments are split alongKto cap live registers.Neither needs an operator; both depend on the leading dimensions you pass, so see the finetuning chapter before padding by hand.
The descriptor below satisfies all of them from SM 80 up:
using BLAS = decltype(cublasdx::Size<128, 128, 32>()
+ cublasdx::Precision<__nv_bfloat16, __nv_bfloat16, float>()
+ cublasdx::Type<cublasdx::type::real>() + cublasdx::Function<cublasdx::function::MM>()
+ cublasdx::Arrangement<cublasdx::row_major, cublasdx::col_major, cublasdx::row_major>() // TN; free for bf16
+ cublasdx::Block() + cublasdx::BlockDim<128>() + cublasdx::StaticBlockDim() // power of two
+ cublasdx::Alignment<16, 16, 16>() // ldmatrix needs 16
+ cublasdx::SM<900>()); // no modifier needed
StaticBlockDim() promises a launch of exactly BLAS::block_dim threads. For this descriptor, every
thread owns part of the result and is_thread_active() is true. Verify the selected instruction in the SASS
as described below.
Per-Tile API#
Host side. For a per-tile call, size the shared memory, raise the limit and launch with the descriptor’s block.
const unsigned smem = cublasdx::get_shared_storage_size_ab<BLAS>(); // A and B tiles; add scratch after
cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, int(smem + scratch_bytes));
kernel<<<grid, BLAS::block_dim, smem + scratch_bytes, stream>>>(...);
The suggested layouts occupy the same bytes as the plain ones, so
get_shared_storage_size_absizes both (16 KiB for the card’s tile in bf16);get_shared_storage_size<BLAS>()adds theCtile for the shared-memory-output form below, andcublasdx::make_shared_storage_calculator()takes explicit layouts when the tiles are not the descriptor’s.Declare the kernel with
__launch_bounds__(BLAS::max_threads_per_block).
Device side. The register-accumulator form, one K tile per execute:
extern __shared__ __align__(16) cublasdx::byte smem[];
auto [smem_a, smem_b] = cublasdx::slice_shared_memory_ab<BLAS>(smem);
auto a_s = cublasdx::make_tensor(smem_a, BLAS::suggest_layout_smem_a());
auto b_s = cublasdx::make_tensor(smem_b, BLAS::suggest_layout_smem_b());
auto acc = BLAS::suggest_accumulator(); // zero; pairs with suggested layouts
for (int kt = 0; kt < k_tiles; ++kt) {
cublasdx::copy<BLAS, cublasdx::alignment_of<BLAS>::a>(a_tile(kt), a_s); // every block_dim thread
cublasdx::copy<BLAS, cublasdx::alignment_of<BLAS>::b>(b_tile(kt), b_s);
cublasdx::copy_wait(); // cp.async wait + __syncthreads
BLAS().execute(a_s, b_s, acc); // acc += A_kt * B_kt
__syncthreads(); // before smem_a / smem_b are rewritten
}
if (acc.is_thread_active()) {
acc.partition_and_store(c_tile);
}
The shared-memory-output form, C = alpha * A * B + beta * C in place with plain layouts:
auto [smem_a, smem_b, smem_c] = cublasdx::slice_shared_memory<BLAS>(smem);
auto c_s = cublasdx::make_tensor(smem_c, BLAS::get_layout_smem_c()); // a_s, b_s, three copies, copy_wait
BLAS().execute(alpha, a_s, b_s, beta, c_s);
__syncthreads();
cublasdx::copy<BLAS, cublasdx::alignment_of<BLAS>::c>(c_s, c_tile);
The accumulator is zero when constructed and every
execute(a, b, acc)adds to it, so one accumulator carries a whole K loop;acc.clear()restarts it.BLAS().execute(a, b)with no accumulator returns a fresh one, suggested when the layouts are suggested.suggest_accumulator()pairs with the suggested layouts of both operands;get_accumulator()pairs with any layouts. The suggested accumulator with a plain layout is a compile error (Suggested accumulator can be used only with suggested layouts); suggested layouts with the default accumulator compile and run the default configuration, so the swizzle buys nothing.Load functors (
execute(a, b, acc, a_op, b_op)) can transform operands on thesupermmapath. Thewgmmaandtcgen05instruction paths require identity loads.cublasdx::copyruns on the descriptor’sblock_dimthreads andcopy_wait()ends with a block barrier;executeends with none, so put a__syncthreads()between it and the next write to the operands it read.Epilogue accessors have the meaning given in the Hopper card:
reduce_and_store,process_and_store,partition_and_store,make_partition_and_copy/partition_and_copy,acc.axpby(alpha, beta, c),get_results()(a copy) andmap_fragment_index(i).Guard the ones that touch memory with
acc.is_thread_active()where your own code touches memory: withStaticBlockDim()and a launch of exactlyblock_dimthreads it is always true.Otherwise it is
falsefor every thread outside the GEMM (for example the surplus threads of a matrix-vector product),is_predicated()is alsotruewhen some block threads are inactive, and the library accessors (partition_and_*,process_and_*,reduce_and_store,axpby, the copies) are no-ops there, so they need no guard;get_results()is unspecified on inactive threads.A block launched with more threads than
BlockDimmust keep the extra threads out ofexecuteand the copies, and the linear thread index followsBlockDim’s rank, not the launch’s.
Pipeline API#
Add WithPipeline() to the same descriptor and pass global-memory tensors to suggest_pipeline. The
computation remains supermma for this descriptor. The shipped
example/cublasdx/01_gemm_introduction/introduction_pipeline.cu also demonstrates a supermma pipeline.
using PipelinedBLAS = decltype(BLAS() + cublasdx::WithPipeline());
auto A = cublasdx::make_gmem_tensor<cublasdx::row_major>(a_ptr, M, K, lda);
auto B = cublasdx::make_gmem_tensor<cublasdx::col_major>(b_ptr, K, N, ldb);
auto p = cublasdx::suggest_pipeline<Depth, PipelinedBLAS>(A, B);
if (!p) { /* handle p.error() before launching */ }
const auto smem_bytes = p->buffer_size(); // add any application scratch separately
cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, int(smem_bytes));
kernel<<<grid, p->get_block_dim(), smem_bytes, stream>>>(..., p->get_device_handle());
Use the pipeline’s block dimensions, buffer size and buffer alignment. The supermma pipeline uses the
same block threads for loading, computing and the epilogue; it adds no producer warpgroup. On Hopper and later,
TMA is selected when the operand layouts and alignments support it; otherwise the pipeline uses cooperative
copies. Neither copy method changes the instruction class.
On the device, obtain the tile and let the pipeline drive the K loop. Take the accumulator from the tile
pipeline rather than constructing it with BLAS::suggest_accumulator():
extern __shared__ __align__(device_pipeline.buffer_alignment()) cublasdx::byte smem[];
auto tile = device_pipeline.get_tile(smem, row, col);
tile.execute([&](auto& acc) {
auto out = cublasdx::get_tile(C, PipelinedBLAS::c_shape, row, col);
acc.partition_and_store(out);
});
Pass the handle as __grid_constant__ DevicePipeline const device_pipeline and declare the kernel with
__launch_bounds__(DevicePipeline::max_threads_per_block, 1). The second argument does not have to be 1:
a higher minimum-blocks value lets several blocks share an SM so one block’s epilogue overlaps another’s
compute — a supermma-device substitute for warp specialization. It trades away registers, so measure it.
For a persistent loop, construct the tile once
and call device_pipeline.reset_tile(tile, row, col) for each subsequent tile. Use tile.epilogue_sync()
when the epilogue needs to synchronize participating threads. With a reusable accumulator, call
finish_accumulation() from every thread that participates in tile.execute(acc) before reading its result.
The pipeline requires whole Size tiles in the global dimensions and matching K extents for A and B.
Handle pipeline-creation errors, including alignment errors, before launch. Read or discard each completed
accumulator before executing the next tile. Load functors can be passed as
tile.execute(epilogue, a_op, b_op); EnableInputStreaming() forbids non-identity load functors.
Checking The Generated Instructions#
Confirm the path in the SASS: HMMA for 16-bit and tf32 inputs (HMMA.16816.F32.BF16 for the card’s
descriptor), IMMA for 8-bit integers, with LDSM present when the suggested layouts took effect
(LDSM.16.M88.4 here). FMA uses FFMA or FFMA2 for float inputs. Check each kernel separately;
these instruction names identify the computation for either API.