Tiled matrix multiplication: assign outputs and count useful work
Derive matrix tiles, accumulation, edge masks, and conventional FLOP counts before comparing a kernel with a transfer-bound performance ceiling.
On this page
The question for today
What you’ll understand
- Map an output tile to the reduction dimension of a matrix product.
- Count conventional multiply-add work and define the transferred-byte boundary.
- Handle incomplete tiles without reading or writing outside valid tensors.
Matrix multiplication is a collection of dot products, but its performance depends on sharing their inputs and assigning their outputs well. A tile groups several outputs so nearby work can cooperate. Start with the mathematical product, valid index ranges, and an accumulation policy. Only then compare the implementation's work and data movement with the hardware resources it actually uses.
A tile owns an output region
For A with shape M by K and B with shape K by N, output C has shape M by N. One block or program instance can own a rectangular output tile and accumulate its partial sums across chunks of K. Assign each valid output exactly once unless the algorithm explicitly combines partial outputs through a sound protocol. A split-K implementation is a different reduction design, with extra combination work and numerical-order consequences. Do not add overlapping output stores merely to make the grid dimensions convenient.
When M, N, or K is not divisible by the chosen tile sizes, partial tiles require masks or a separately correct edge path. Invalid input lanes can contribute zeros to the dot product if that matches the operation, while invalid output coordinates must not be stored. The loaded layout still follows the tensor's actual strides. Bounds checks cannot be removed merely because a benchmark used square aligned dimensions. Also state whether the operation computes C=AB or a more general scaled combination involving previous C, because that changes both semantics and memory traffic.
Define which bytes cross which boundary
An ideal single-read model for C=AB counts each input once and each output once. It is a lower traffic model at a named external boundary, not the total number of operand accesses made by all threads. Different tiles can reload the same input, caches can satisfy some loads, and shared-memory staging adds another transfer layer. Use an instruction count, a logical tensor-byte count, and measured device-memory bytes as separate metrics. A conventional FLOP numerator divided by inconsistent byte counters can produce a misleading intensity.
Arithmetic intensity is work divided by transferred bytes at the chosen boundary. A roofline model compares that intensity with a memory-bandwidth ceiling and a compute ceiling using matching units. It does not predict an exact kernel time: launch overhead, insufficient parallelism, instruction dependencies, conversion, and synchronization can reduce achieved throughput. A tiny matrix can be latency-bound even with favorable intensity. Compare against a tuned library under the same dtype, shape, layout, and precision contract before interpreting a custom tile as an improvement.
Tile size is a resource tradeoff
Larger output tiles can reuse more input values, but they require more accumulators and can reduce the number of independent blocks available for a small problem. They also change register use, shared-memory needs, edge waste, and scheduling. The best shape need not be square, especially when M and N differ greatly. A blocked algorithm's mathematical correctness is independent of whether its tile saturates a particular GPU. Separate a valid tile design from a demonstrated performance result, and record the architecture and compiler when tuning.
The roofline lab has a fixed synthetic compute ceiling of 1,000 GFLOP/s. Reset, set I=8 FLOP/byte and B=80 GB/s, predict ceiling 640 GFLOP/s, then Play or Step. Separately Reset, set I=16 and B=80, predict the compute-capped ceiling 1,000, then Play or Step. These reachable settings do not change an actual matrix tile or GPU. They illustrate why improving reuse can help only until another ceiling dominates, while the displayed ridge intensity remains 12.5 FLOP/byte for the selected bandwidth.
Work it through
Count a small dense product
Compute a 16-by-16 product with K=16 using four-byte inputs and output, C=AB, and the ideal one-read input/one-write output boundary.
Conventional work is 2×16×16×16=8,192 FLOPs. This is a counting convention for multiply-add contributions, not an instruction count.
Each matrix contains 256 elements. Reading A and B and writing C gives 3×256×4=3,072 ideal boundary bytes, with no previous-C read.
The ideal intensity is 8,192/3,072=8/3≈2.667 FLOP/byte. A real tiled implementation can move more bytes and take additional overhead, so this is not a throughput measurement.
The operation has 8,192 conventional FLOPs and 3,072 ideal boundary bytes under explicit assumptions. Tile tuning must preserve those semantics and validate actual traffic.
The living notebook
Explore the model
Interactive lab · roofline
Reuse can move the bottleneck
Reset, set I=8 and B=80, predict 640 GFLOP/s, then Play or Step. Separately Reset, set I=16 and B=80, predict the fixed 1,000 GFLOP/s compute ceiling, then Play or Step. The synthetic roofline has no matrix tiles, launch overhead, or measured GPU counters.
- Attainable ceiling
- 640 GFLOP/s
- Ridge intensity
- 12.5 FLOP/byte
- Ceiling regime
- Memory-bound ceiling
Synthetic roofline ceiling min(1000, B·I), in GFLOP/s, with B in decimal GB/s and I in FLOP/byte. Playback traces the roof without changing the selected kernel. This upper-bound model is not a measured throughput prediction, hardware comparison or price model. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forArithmetic intensity belongs to a defined transfer boundary and only bounds attainable throughput.
Pause & reason
Check your understanding
Your turn
Try it yourself
For M=8,N=4,K=16 with two-byte A and B and four-byte C, find conventional FLOPs and ideal boundary bytes for C=AB.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Use each tensor's own element width and omit old-C traffic only because the operation overwrites C.
Compare with the solution
Work is 2×8×4×16=1,024 FLOPs. Input A occupies 8×16×2=256 bytes, B occupies 16×4×2=128 bytes, and output C occupies 8×4×4=128 bytes. The ideal total is 512 bytes, giving intensity 2 FLOP/byte before any actual rereads, staging, or overhead.
Keep the idea
Take it with you
- Output ownership and edge handling are correctness requirements before tuning.
- Conventional FLOPs, instructions, and transferred bytes are different counters.
- Tile size trades input reuse against accumulators, occupancy, and problem parallelism.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.