Lesson 11 · Optimization arc opens

GPU Architecture · Optimization

The Tensor Core

A second kind of arithmetic unit that multiplies whole matrix tiles in one shot — what it does, what work it speeds up, and why feeding it is the hard part.

Until now, one thread has meant one lane doing scalar arithmetic (Lesson 2). But every recent NVIDIA SM carries a second kind of math unit beside those lanes — the Tensor Core — and it is the reason a modern GPU is quoted in hundreds of "AI TFLOPS." It does not make your existing kernels faster on its own. It accelerates exactly one shape of computation, and getting the speedup is a memory problem before it is a math problem. This lesson opens the optimization arc: taking the machine you now understand and making it fast.

The one win

A CUDA core multiplies two numbers; a Tensor Core multiplies two matrices. Its single operation is D = A×B + C on small tiles, with low-precision inputs accumulated in high precision. Anything you can express as dense matrix multiply — neural nets, convolutions, attention, dense linear algebra — rides it. Anything else does not. And because it is so fast, the whole game becomes keeping it fed from on-chip memory.

First, a correction: where it actually sits

It is tempting to picture a Tensor Core inside a CUDA core. It isn't. Recall the SM from Lesson 2: four sub-partitions, each with ~32 FP32 lanes, a warp scheduler, and a register file. A Tensor Core sits beside those lanes — one per sub-partition, four per SM on current architectures — and it is a warp-level shared resource, not something a single thread owns.1

One SM sub-partition

~32 CUDA-core lanes

Each does one scalar FMA per clock — a*b + c on single numbers. One thread per lane.

1 Tensor Core

Does a whole matrix-multiply-accumulate tile per operation. Fed by the whole warp at once.

So you cannot think "each thread has a Tensor Core." A warp issues one Tensor Core instruction and all 32 lanes cooperate to feed a single tile through it. That warp-level cooperation is baked into the API you'll see below — every call ends in _sync. (This is also why AMD's equivalent, the Matrix Core, is wavefront-level, and why optimized matmul code on either vendor is organized around warps/wavefronts, not threads.)

The one operation it supports

A Tensor Core is not general-purpose. It cannot branch, load, add scalars, or compute a transcendental. It does exactly one thing, parameterized by tile shape and datatype — a matrix multiply-accumulate, "MMA":

D16×16
=
A16×16
×
B16×16
+
C16×16
One warp instruction: a full tile of multiply-accumulates that would take hundreds of scalar FMAs on the lanes.

The hardware works in fixed tile shapes — the canonical one you program against is 16×16×16 (a 16×16 tile of D, formed from a 16×16 slab of A times a 16×16 slab of B). To multiply larger matrices you tile them into 16×16 blocks and sweep, accumulating — which is exactly what the code does. One MMA replaces the inner loop of scalar FMAs; that is the whole source of the speedup.

The datatypes — this is the "what gets sped up" lever

The trick is mixed precision: inputs come in low precision, but the products accumulate into a wider format so error doesn't compound over a long dot product. Each GPU generation widened the menu, and each new type unlocked a different workload:2

GenerationInput types addedAccumulateWhat it unlocked
VoltaFP16FP32the first deep-learning training boom
TuringINT8, INT4, INT1INT32quantized inference
AmpereTF32, BF16, FP64, 2:4 sparsityFP32 / FP64FP32 code sped up untouched; HPC on Tensor Cores
HopperFP8 (E4M3 / E5M2)FP32large-language-model training & inference
BlackwellFP4, FP6 (microscaling)FP32your RTX 5050's 5th-gen cores; low-precision inference

Two of these are worth pausing on:

What actually gets sped up

The rule is simple and broad: anything you can express as dense matrix multiplication (GEMM). A surprising fraction of real computing reduces to it — which is the whole reason this hardware exists.

Rides the Tensor Core

Stays on the CUDA cores

In a real network the Tensor Cores do the heavy layers and the ordinary lanes do everything between them — the activation functions, the residual adds, the layernorms. Both kinds of unit run; the art is overlapping them.

The catch: you have to feed them

Here is where the optimization arc really begins. A Tensor Core is so fast that data movement, not arithmetic, becomes the bottleneck. Go back to the roofline from Lesson 4: adding Tensor Cores raises the compute ceiling enormously — but the memory bandwidth line doesn't move. So the machine balance shifts hard, and the arithmetic intensity you need to stay compute-bound shoots up.

Why tiling (Lesson 6) is the answer

If every 16×16 MMA fetched its inputs from DRAM, you'd be pinned to the bandwidth wall and the Tensor Cores would sit idle waiting. The fix is the two-sided tiling pattern from Lesson 6: stage tiles of A and B into shared memory once, then let many MMAs reuse them. Each byte crosses the memory bus once and feeds a whole row/column of tile products. That reuse — arithmetic intensity — is the only thing that converts Tensor Core peak into real throughput. This is why __syncthreads() and shared memory, which felt abstract in Lesson 6, are load-bearing here.

Newer generations attack the same feed problem in hardware: Hopper's wgmma reads operands directly from shared memory and runs asynchronously; Blackwell adds dedicated tensor memory (TMEM) to shorten the path further. All of it is in service of one goal — keep the tile inputs close so the Tensor Core never starves.

How you actually use them

You will almost always stay at the top of this ladder. Descend only when you need more control than the level above gives you.

1

Libraries — cuBLAS, cuDNN, CUTLASS, or a framework

These dispatch to Tensor Cores automatically when your dtype and shape qualify. PyTorch calling cuBLAS/cuDNN is how the overwhelming majority of Tensor Core cycles are actually issued. You write a matmul; the library picks the hardware.3

2

WMMA API — nvcuda::wmma in CUDA C++

Warp-cooperative fragments with load_matrix_sync / mma_sync / store_matrix_sync. The level where you can see the tile mechanism — and the level the code example uses.1

3

Inline PTX — mma.sync / wgmma

Maximum control over exactly which instruction and layout, for squeezing the last few percent. This is what CUTLASS emits under the hood; you rarely write it by hand.

Reading the code

The companion example, 04-wmma-matmul.cu, computes the same matrix multiply two ways — scalar FP32 on the CUDA cores and 16×16 tiles on the Tensor Cores via WMMA — checks both against a CPU reference, and times them. The WMMA kernel is the whole idea in a dozen lines:

// One WARP owns one 16×16 output tile.
wmma::fragment<wmma::matrix_a, 16,16,16, half, wmma::row_major> aFrag;
wmma::fragment<wmma::matrix_b, 16,16,16, half, wmma::row_major> bFrag;
wmma::fragment<wmma::accumulator, 16,16,16, float> acc;

wmma::fill_fragment(acc, 0.0f);                // C tile starts at zero
for (int k = 0; k < n; k += 16) {
  wmma::load_matrix_sync(aFrag, A + rowTile*n + k, n);   // stage a 16×16 A tile
  wmma::load_matrix_sync(bFrag, B + k*n + colTile, n);   // and a 16×16 B tile
  wmma::mma_sync(acc, aFrag, bFrag, acc);                // acc += aFrag × bFrag  (one MMA)
}
wmma::store_matrix_sync(C + rowTile*n + colTile, acc, n, wmma::mem_row_major);

Four things to notice, each a direct echo of the concepts above:

On your box

Build and run it on the 5050: nvcc -arch=sm_120 04-wmma-matmul.cu -o wmma && ./wmma. You'll see the Tensor Core version land several times the naive kernel's GFLOP/s while agreeing with the CPU reference. Then — the arc's real payoff — profile it: ncu --set full ./wmma and watch this kernel report as compute-bound on the Tensor pipe, where the naive one is limited by the CUDA-core FMA rate. This is a modest, un-tiled WMMA kernel; closing the gap to cuBLAS is a shared-memory feeding problem, which is exactly where the arc heads next.

Check yourself

From memory — one click locks each answer.

Mental modelA Tensor Core, relative to a CUDA core, is best described as —

What acceleratesWhich of these will not be sped up by Tensor Cores?

The catchYou replace a kernel's inner loop with mma_sync but see little speedup. The most likely reason —

Primary source (≈20 min)

CUDA C++ Programming Guide — Warp Matrix Functions (WMMA): fragment types, load/mma/store_matrix_sync, supported tile shapes and datatypes. Read it here. Then skim NVIDIA's "Programming Tensor Cores in CUDA" for the picture end to end: developer.nvidia.com.

The optimization arc is open. You now have a second engine and the one law that governs it: feed it or waste it. Next we make that law bite — a properly shared-memory-tiled matmul (WMMA fed from on-chip tiles, and a plain FP32 tiled version measured against your 320 GB/s) profiled under Nsight Compute, so you watch arithmetic intensity climb and the memory-stall reasons from Lesson 10 fall on your own code. Every physical lesson becomes a number you can move. Say the word when you're ready.

Notes & citations

  1. CUDA C++ Programming Guide — Warp Matrix Functions: the nvcuda::wmma API operates at warp scope on opaque fragment operands; load_matrix_sync, mma_sync, store_matrix_sync, fill_fragment; supported shapes (incl. 16×16×16) and input/accumulator type pairs. Tensor Cores are per-sub-partition SM units (one per sub-partition, four per SM on Turing and later). docs.nvidia.com/cuda/cuda-c-programming-guide
  2. Datatype/generation history: Volta whitepaper (FP16 in, FP32 accumulate; 4×4×4 per Tensor Core) Volta whitepaper; Ampere whitepaper (TF32, BF16, FP64 Tensor Cores, 2:4 structured sparsity) Ampere whitepaper; Hopper (FP8 E4M3/E5M2, Transformer Engine, asynchronous wgmma) and Blackwell (FP4/FP6 microscaling, 5th-gen Tensor Cores) — NVIDIA Tensor Core overview: nvidia.com/tensor-cores. Illustrative throughput: A100 dense FP16 Tensor ≈ 312 TFLOPS vs FP32 ≈ 19.5 TFLOPS (~16×).
  3. Library dispatch: cuBLAS uses Tensor Cores for GEMM when types/dimensions permit (cuBLAS docs); cuDNN for convolution/attention (cuDNN docs); CUTLASS is NVIDIA's open template library for writing Tensor Core GEMM kernels (github.com/NVIDIA/cutlass).
← Lesson 10 ⌂ Home