The register-blocked kernel is close to the limit of the FP32 CUDA
cores’ performance; to go even faster, we must enlist the Tensor Cores,
which were built specifically for matrix multiplication. The Tensor
Cores implement machine instructions that multiply matrix tiles in a
single operation (dozens of multiply-add instructions), and the
nvcuda::wmma API exposes it through fragments –
opaque per-warp tiles that the entire warp loads, multiplies, and stores
cooperatively.
Section 17.1 read C one element at a time – a row of A dotted with a column of B – but the product is equally a sum of outer products: over k, each term is the rank-1 product of column k of A and row k of B. A single Tensor Core instruction evaluates a rank-8 slab of that sum: a 16×16 output tile from a 16×8 fragment of A and an 8×16 fragment of B.
To keep things simple for illustrative purposes, this chapter only uses the TF32 feature of Tensor Cores (introduced in Ampere), which essentially coerces FP32 inputs to FP16 precision. Over time, the formats supported by Tensor Cores have steadily increased, with a trend toward smaller and smaller FP formats. The latest, FP4, can only represent numbers between +/-6.0! NVIDIA has been able to use these narrower FP formats to deliver higher peak FLOPS rates than otherwise would be possible.
For single precision, the natural entry point is TF32, a 19-bit input
format with the 8-bit exponent of float (so the same
dynamic range) but only a 10-bit mantissa. Inputs are rounded from
float to TF32 on the way into the fragment; the
multiply-accumulate happens in the Tensor Core; and the result
accumulates in full FP32. The reduced input precision keeps
float’s range (exponents are not changed), but discards
thirteen bits of mantissa, so a TF32 GEMM is an approximate
GEMM – but with precision enough for many deep-learning workloads.
for ( int kk = 0; kk < BK; kk += 8 ) { wmma::fragment<wmma::matrix_a,16,16,8,wmma::precision::tf32,wmma::row_major> aF[TMw]; wmma::fragment<wmma::matrix_b,16,16,8,wmma::precision::tf32,wmma::row_major> bF[TNw]; for ( int i = 0; i < TMw; i++ ) { wmma::load_matrix_sync( aF[i], &As[warpRow*(TMw*16)+i*16][kk], BK ); for ( int t = 0; t < aF[i].num_elements; t++ ) aF[i].x[t] = wmma::__float_to_tf32( aF[i].x[t] ); // round float -> TF32 } for ( int j = 0; j < TNw; j++ ) { wmma::load_matrix_sync( bF[j], &Bs[kk][warpCol*(TNw*16)+j*16], BN ); for ( int t = 0; t < bF[j].num_elements; t++ ) bF[j].x[t] = wmma::__float_to_tf32( bF[j].x[t] ); } for ( int i = 0; i < TMw; i++ ) for ( int j = 0; j < TNw; j++ ) wmma::mma_sync( cF[i][j], aF[i], bF[j], cF[i][j] ); // accumulate in FP32}
sgemm_wmma).Wrapped in the same shared-memory staging as before, with a 128×64
block tile, the WMMA kernel reaches 8718 GFLOP/s –
higher than cuBLAS’s FP32 number, using inputs the FP32 kernels could
not. But there’s still performance to be gained: the Tensor Cores retire
multiply-accumulates so quickly that the kernel can no longer keep them
fed. With the arithmetic so cheap, the global-memory load of the next
slab is once more the limiting factor. The kernel is memory-bound, and
it stalls at each __syncthreads() waiting for data to
arrive.
Figure 17-4. A warp-level MMA multiplies a 16×8 fragment of A by an 8×16 fragment of B into a 16×16 FP32 accumulator.