Worked examples — Tensor cores and matrix operations
This page is the drill ground for Tensor Cores. The parent note told you what a Tensor Core is and why it exists. Here we count out every kind of situation you can be handed — every awkward matrix shape, every precision choice, every "does it even fit?" edge case — and work each one to the number.
If you have not yet met the machinery of tiled matrix multiply, skim the parent first. Everything else we build from zero right here.
Two pieces of notation, built first
Two symbols recur on every line below. We define them before using them.
The scenario matrix
Before working problems, let us enumerate the cases. A Tensor Core problem is fully described by a few knobs. If a knob can take a value that breaks the naive approach, that value gets its own row.
| Cell | Case class | What is being tested | Example |
|---|---|---|---|
| A | Clean fit | Dimensions are exact multiples of the tile | Ex 1 |
| B | Ragged fit (needs padding) | Dimension not a multiple of tile → padding / performance cliff | Ex 2 |
| C | Degenerate: , and / | The sum / the output has zero size — what is the answer? | Ex 3 |
| D | Skinny / vector-like ( or ) | Tile mostly wasted — utilisation question | Ex 4 |
| E | Instruction-count / speedup arithmetic | Counting FMAs and Tensor Core ops | Ex 5 |
| F | Precision choice (FP16 vs BF16 vs TF32 vs INT8) | Which mode, and the accuracy cost | Ex 6 |
| G | Accumulator overflow / range | When low precision silently fails | Ex 7 |
| H | Real-world word problem (roofline / timing) | Bytes vs FLOPs, is it compute-bound? | Ex 8 |
| I | Exam-style twist (transpose / layout) | row_major vs col_major, off-by-tile traps |
Ex 9 |
We will use these symbols throughout — defined once, here:
Ex 1 — Cell A · The clean fit
What we are doing: counting how many tile-sized bites cover the whole output.
Figure below (s01). Alt-text: a blue output rectangle is sliced by a thin gray grid into tiles down and tiles across; one tile is filled orange and labelled "one output tile ()"; a green arrow labelled " accumulation steps" points into that orange tile to show it is built up by summing K-chunks. Read the three grid counts straight off the picture.

- Count output-row tiles. (the rows of the grid in s01). Why this step? The output is ; along its height we lay down tiles of height . It divides evenly → Cell A, clean.
- Count output-column tiles. (the columns of the grid). Why? Same logic across the width, tile width .
- Count inner (K) tiles. (the green arrow's depth). Why? Each output tile is not finished in one shot — the shared dimension is sliced into chunks of , and we accumulate those chunks. Each chunk's product is added onto the running tile via the in : the previous step's is fed back as this step's .
- Multiply the three counts. instructions.
Why multiply? Every output tile (there are orange squares' worth) needs one
mmaper K-chunk (there are ).
Verify: total FMAs the hard way . Each mma does FMAs. Then . ✓ Same number two ways.
Ex 2 — Cell B · The ragged fit (padding & the performance cliff)
What we are doing: handling a dimension the tile size does not divide.
Figure below (s02). Alt-text: a horizontal strip of tiles; the first tiles ( columns) are solid blue and labelled "real, clean"; the th tile is split — an orange slice ( columns, "real: 129–130") next to a gray hatched slice ( columns, "padding: 131–136, wasted"). A red bracket over the th tile reads "one extra tile for 2 extra columns = the cliff." Watch how the tiny orange sliver drags in a whole gray tile.

- Test divisibility. — not an integer. This is the ragged case. Why this step? Tensor Cores only speak in whole tiles. A partial column strip cannot be issued as "16.25 tiles."
- Round up with the ceiling. Number of column-tiles (recall , defined above) — the tiles in s02. Why round up, not down? Dropping the extra columns would lose data. We must cover columns 129 and 130 (the orange sliver), so we allocate a full th tile.
- Compute padded width. columns of tile space for real columns. Why? The last tile spans columns 129–136; columns 131–136 are padding (zeros in , garbage-then-discarded in ) — the gray hatched slice.
- Wasted columns . Why care? Those columns still cost full Tensor Core cycles. This is the performance cliff: adding 2 real columns cost you a whole extra tile.
Verify: wasted fraction of the last tile idle in that strip. Overall extra work vs. a clean : instructions go from to , i.e. , a 6.25% overhead to serve 2 extra columns. ✓
Ex 3 — Cell C · Degenerate sizes (, and or )
What we are doing: evaluating the tile counts and the defining sum at every boundary size.
Figure below (s03). Alt-text: three small panels side by side. Panel (a) "": a solid blue output square exists but is filled with the label "" — the output tiles are real, the contraction is empty. Panel (b) "": a flat blue line of zero height labelled " — no rows, no tiles." Panel (c) "": a thin blue vertical line of zero width labelled " — no cols, no tiles." Under all three a gray caption reads "instructions run in every case."

- Case (a), (empty contraction). The defining sum has an empty index range. By the universal convention a sum over zero terms is the additive identity , so is the zero matrix (panel (a): a real square, filled with ).
Why 0 and not "undefined"? An empty sum must leave an accumulator unchanged; the identity that "adds nothing" is .
Instruction count:
mmacalls. The output tiles exist ( is non-empty) but there is nothing to accumulate. - Case (b), (no output rows). is — an empty matrix (no elements at all; panel (b) is a zero-height line). Why? If has zero rows, the product has zero rows; there is literally no to compute. Instruction count: .
- Case (c), (no output columns). is — again an empty matrix (panel (c) is a zero-width line). Why? Symmetric to (b): zero output columns → nothing to write. Instruction count: .
- The unifying rule. If any of the three tile counts is , the product runs zero instructions. But the reason differs: in (a) the output tiles exist and must still be initialised to zero; in (b)/(c) there are no output tiles to initialise at all.
Why mention initialisation? Cell C's classic bug: for you must still
fill_fragment(c_frag, 0.0f)or you return garbage — even though no multiply ever happens.
Verify: (a) is zeros, ops; (b) is , ops; (c) is , ops. ✓
Ex 4 — Cell D · Skinny matrix (matrix–vector, )
What we are doing: measuring waste when a dimension is far smaller than the tile.
Figure below (s04). Alt-text: one output tile drawn as an -wide, -tall blue grid. The leftmost column is filled orange and labelled "useful output (1 column of )"; the remaining columns are gray with diagonal hatching, labelled "wasted tile width (7 of 8 columns padding)." A red caption underneath reads "utilisation ." Watch the orange strip occupy exactly one-eighth of the blue tile.

- Output shape. is . Column-tiles needed . Why? Even a single column forces a full width- tile — one orange column, but a whole blue tile allocated (see figure).
- Useful columns per tile out of (the orange strip vs. the whole blue tile in s04). Why? Columns 2–8 of that tile compute against padding (zero) columns of — real cycles, discarded results (the gray hatched region).
- Column utilisation . Why this matters? Tensor Cores shine on fat GEMMs. A matrix–vector product wastes of the width dimension — it is memory-bound, not compute-bound (see Ex 8).
- The row dimension is fine. is a multiple of , so height utilisation is . Overall tile utilisation . Why separate the dimensions? Utilisation multiplies across independent tile axes.
Verify: column util ; overall . In s04, the orange strip (real output) is exactly one of the eight columns of the blue tile. ✓
Ex 5 — Cell E · Speedup arithmetic
What we are doing: dividing total work by work-per-instruction.
Figure below (s05). Alt-text: a horizontal bar chart with a logarithmic x-axis (instruction count). A long orange bar reaches labelled "scalar FMAs (one per instruction)"; a much shorter blue bar reaches labelled "Tensor Core mma ops (2048 FMAs each)." A red annotation between them reads " fewer instructions." The blue bar is visibly a tiny fraction of the orange.

- Total FMAs. (the orange bar). Why cube? Each of the outputs needs FMAs.
- FMAs per
mma. . Why? One instruction fills an output tile using a length- contraction FMAs. - Reduction factor. instructions (the blue bar), i.e. the instruction count drops by a factor . Why the tile product? Every FMA hidden inside one instruction is one fewer instruction you issue.
Verify: ; Tensor Core ops; factor . ✓
Recall Why doesn't 2048× fewer instructions mean 2048× faster?
Because raw speedup is bounded by clock, memory bandwidth, and pipeline fill — instruction count is only the ceiling. Real speedup is 8–16×, set by hardware throughput, not by the FMA-per-op ratio. ::: Instruction reduction is an upper bound; memory and clock cap the actual gain.
Ex 6 — Cell F · Choosing the precision (FP16 vs BF16 vs TF32 vs INT8)
What we are doing: exercising all four precision modes on concrete numbers.
Figure below (s06). Alt-text: two stacked panels. Top panel — three horizontal bit-layout bars (FP32, BF16, FP16) of equal total length, each split into a blue "sign" cell, an orange "exponent" segment, and a green "mantissa" segment; FP32 and BF16 share the same wide orange exponent (labelled "range: 8 exp bits"), while FP16's exponent is visibly shorter ("range: 5 exp bits") and its mantissa longer than BF16's. A red arrow marks " falls off FP16's range." Bottom panel — a number line from to with evenly spaced orange ticks illustrating INT8's coarse grid, annotated "step ."

What we are doing: exercising all four precision modes on concrete numbers.
- (i) FP16 vs BF16 — the range test. Compare to each format's smallest normal. → FP16 underflows it toward zero. → BF16 keeps it. Why? FP16 spends only 5 bits on the exponent (narrow range, top panel of s06); BF16 keeps FP32's full 8-bit exponent, trading mantissa bits for range. Choose BF16 for training.
- (ii) TF32 — the free speed test. Relative rounding error is about one unit in the last mantissa bit: , versus FP32's . Why care? relative error is under — invisible to most models — and TF32 needs no code change while running faster than FP32. Choose TF32 as the default fast FP32.
- (iii) INT8 — the quantisation step test. With levels () spread across a range of width , the step between neighbouring representable weights is (the tick spacing, bottom panel of s06). Why this matters? Any weight is rounded to the nearest multiple of ; INT8 accepts this coarse grid in exchange for throughput and smaller weights vs FP32 — fine for inference after training. Choose INT8 for post-training inference.
Verify: (i) and ; (ii) , , and ; (iii) . ✓ See 8.3.4-Mixed-precision-training and 8.4.2-Quantization-techniques.
Ex 7 — Cell G · Accumulator overflow (why FP32 accumulate exists)
What we are doing: bounding the accumulator and comparing to its capacity.
- Worst-case per-product magnitude. . Why max? is the largest signed INT8 value; the worst single term.
- Worst-case sum. . Why multiply by ? The contraction adds such terms; the worst case is all at max sign.
- Compare to INT16 ceiling. → massive overflow after just a handful of terms. Why does this ruin everything? An overflowed INT16 wraps around silently → garbage output, no error thrown.
- Compare to INT32 ceiling (). → fits comfortably. Why hardware uses INT32 accumulate: it guarantees no overflow for realistic , which is exactly why Tensor Cores pair INT8 inputs with an INT32 accumulator (and FP16 inputs with FP32) — the accumulate result lives in the wide type even when are narrow.
Verify: ; (INT16 overflows); (INT32 safe). ✓ This is the concrete reason "mixed precision" always accumulates wide.
Ex 8 — Cell H · Real-world word problem (compute- vs memory-bound)
What we are doing: applying the roofline model — the single test that says "compute or memory."
- FLOPs. Matrix–vector does FMAs FMAs that in FLOPs FLOPs. Why ? Each FMA is a multiply and an add.
- Bytes moved. We must read (dominant): FP16 elements bytes bytes B. ( and are negligible.) Why dominates? is only elements; is — a million times more data.
- Arithmetic intensity FLOP/byte. Why this ratio? It tells us how much compute we squeeze from each byte fetched.
- Machine balance FLOP/byte. Compare: our intensity → memory-bound by a huge margin.
- Time estimate (memory). s s. Why use bandwidth, not FLOP/s? When memory-bound, the memory pipe sets the time; the Tensor Cores idle.
Verify: intensity ; balance ; (memory-bound); s. ✓ (Compute-only time would be s — far shorter, confirming memory dominates.)
Ex 9 — Cell I · The exam twist (layout / transpose trap)
What we are doing: tracking layout flags as transpose operators, because that is exactly what they are.
- What layout means. Reading a row-major buffer as if col-major is the same as reading the transpose. So the
col_majorload flag applies a transpose to whatever bytes it reads. Why? Row-major stored, read column-major, yields mathematically. - Student pre-transposes in memory → the buffer now holds . Why they did it: to "get" the transpose they wanted.
- Then the col_major flag transposes again → the fragment holds . Why this is the trap: two transposes cancel. .
- Result:
mma_synccomputes — not the desired . The two flips cancelled; the student silently gets the wrong product (no error). Fix: do exactly one transpose — either flip the buffer or set the layout flag, never both.
Verify: with , : intended but double-flip gives . The two matrices differ → confirms the bug. ✓ (See 7.3.6-cuBLAS-and-cuDNN: libraries expose an explicit transA/transB flag so you flip once, deliberately.)
Recap
Recall Which cell forces you to round the tile count
up with the ceiling? Cell B (ragged fit): a dimension not divisible by the tile size uses tiles, creating padding and a performance cliff. ::: The ragged / padding case — use ceiling, expect wasted columns.
Recall Empty sizes: how do
, , and differ?
All three run 0 mma instructions. But still produces a real (zero-filled) output tile that must be initialised, whereas or produce an empty matrix with no output tiles at all. ::: Zero instructions in every case; only still needs accumulator init.
Recall Why does INT8 Tensor Core hardware accumulate in INT32?
Because a length- contraction of INT8 products can reach , which overflows INT16 for even modest but stays well inside INT32's range. ::: To avoid silent overflow of the accumulated sum.
Recall Skinny matmul (
): compute-bound or memory-bound? Memory-bound — arithmetic intensity FLOP/byte sits far below the machine balance (), and tile utilisation is only . ::: Memory-bound and low utilisation; batch to fix.
Related depth: 6.2.1-GPU-vs-CPU-architecture, 6.2.8-Memory-hierarchy-in-GPUs, 6.2.10-Warp-scheduling-and-execution.