A GPU kernel can be blazingly fast on paper — millions of FLOPs available every microsecond — and still run slowly, because those FLOPs have to wait on data that hasn't arrived from memory yet. Whether that wait actually matters, and how much, is not a vague intuition. For a given kernel, it's an exact number.
A toy kernel: an N×N matrix multiply. Slide N and watch two clocks race — one for moving the matrices through HBM (High Bandwidth Memory, the GPU's main memory), one for the arithmetic itself. Whichever is longer is the kernel's actual runtime; the other one is free, because it overlaps.
An N×N matmul reads two N×N operands and writes one N×N result — 3 arrays of numbers, 4 bytes each — and performs the standard floating-point operations (one multiply and one add per output element, summed over N):
- — the matrix size: an N×N by N×N matmul.
- — total bytes moved through memory for this kernel.
- — total floating-point operations this kernel performs.
Each quantity turns into a time by dividing by the rate at which it can happen — bandwidth for bytes, throughput for FLOPs — and the kernel's actual runtime is whichever time is larger, since a well-written kernel overlaps the two:
- — HBM bandwidth, in bytes per unit time.
- — compute throughput, in FLOPs per unit time.
- — the kernel's actual runtime, the roofline model's prediction.
- Bytes grow as N², FLOPs grow as N³
Every extra unit of N adds proportionally less memory traffic than compute. That single fact is the whole story: it guarantees the two curves cross exactly once.
- Below the crossover: memory-bound
Small N means little arithmetic relative to how much data has to move — the GPU sits idle waiting on HBM. This is where flash-attention-style tiling and kernel fusion earn their keep.
- Above the crossover: compute-bound
Large N means the arithmetic dominates — moving the data was cheap by comparison. Here, the number that matters is how many FLOPs/sec the hardware can sustain, not how fast memory is.
The same two curves, now on a fixed scale so their crossing point is visible directly: transfer time climbing gently as , compute time climbing steeply as , meeting at exactly one N along the way.
- At N = 8: bytes and FLOPs
- Two times, exactly equal
With and :
Same number — N = 8 is exactly the crossover for these constants.
- One step either side
- At : , . , — memory-bound, transfer dominates.
- At : , . , — compute-bound, the arithmetic now dominates instead.
Of these three matmul sizes, exactly one runs compute-bound rather than memory-bound. Find it.
"Optimize the kernel" only means something once you know which side of this max you're on. A memory-bound kernel gets faster from moving less data — tiling, fusing operations, staying in SRAM (the small, fast on-chip memory) instead of round-tripping to HBM — while more FLOPs/sec on the hardware wouldn't help it at all. A compute-bound kernel is the reverse. Profiling a real kernel is exactly this diagnosis: measure its arithmetic intensity, compare it to the hardware's own crossover point, and know before optimizing which resource is actually the bottleneck. The next chapter uses that same memory budget from the other direction — shrinking the model itself, so there's less to move in the first place.