Part A · Foundations › Memory hierarchy, roofline, arithmetic intensity

Module 1·Part A — Foundations·16 min

Memory hierarchy, roofline, arithmetic intensity

What the roofline actually bounds, why the ridge point keeps running away from us, and the exact point at which the model stops being useful.

The core mental model

Roofline is one inequality: P=min(π, βI)P = \min(\pi,\ \beta I), where π\pi is peak compute, β\beta is peak bandwidth, and II is arithmetic intensity in FLOPs per byte. Everything interesting is in II, and specifically in the denominator, because arithmetic intensity is not a property of an algorithm. It is a property of a triple: algorithm, boundary, implementation. The FLOPs are fixed by the math. The bytes are whatever crosses one specific interface, and you get to choose which interface you are asking about. A tiled GEMM and a naive GEMM do identical arithmetic and have identical minimum traffic, yet differ by three orders of magnitude in achieved II at the HBM boundary. Anyone who quotes “the arithmetic intensity of attention” without naming a boundary has said nothing.

Which is why the roofline is hierarchical. Each level of the memory system has its own β\beta and therefore its own roof, and a kernel sits somewhere different under each one. A kernel can be comfortably above the HBM ridge and still be starved by shared-memory bandwidth or L2. The ridge point I=π/βI^{*} = \pi/\beta is where the two roofs meet, and its long-run behavior is the single most important fact about the last decade of accelerators: peak FLOPs has grown roughly 3× per generation while HBM bandwidth has grown roughly 1.5×, so . On an H100 with dense fp16 tensor cores it is about 295 FLOPs per byte. Being compute-bound is now a rare and privileged condition, achieved by engineering, not stumbled into. Essentially all of kernel optimization is manufacturing reuse until you reach the ridge.

2k2k2k2k110generationgrowth, indexed to V100 = 1peak FLOP/sHBM bandwidth
Peak compute and memory bandwidth, indexed to V100 = 1. Compute pulls away, and the gap between the two lines is the ridge point: V100 ≈ 139 FLOP/byte, H100 ≈ 295, B200 ≈ 281. The ridge is the ratio of these two curves, which is why it climbs without anyone deciding it should.

Map that onto inference and the two phases separate cleanly. Prefill is GEMM-shaped: for a square fp16 matmul of size nn, HBM-boundary intensity is 2n3/6n2=n/32n^3 / 6n^2 = n/3, so it crosses the H100 ridge somewhere around n900n \approx 900 and is solidly compute-bound at realistic sizes. Decode is GEMV-shaped: each weight element is read once and used for two FLOPs, giving . The consequence is the number that governs single-stream serving: batch-1 decode latency has a hard floor of model bytes divided by memory bandwidth, and no amount of compute changes it. The clean way to see the whole batching story is that decode intensity is approximately the batch size, because the weights are read once and amortized across BB tokens. on the weight matmuls, which is exactly why continuous batching exists and exactly why it does nothing for the tail of a single request.

1101001k1101001karithmetic intensity — FLOP per byteachieved TFLOP/sridge 295989 TFLOP/s3.35 TB/smemory-boundcompute-bounddecode, batch 1prefill, n ≈ 900
Where the two inference phases actually sit on an H100. Decode is not a little below the roof — it is two and a half decades below it, pinned to the bandwidth slope. Prefill sits at the ridge. Same weights, same hardware, same arithmetic.
1101001k1101001karithmetic intensity — FLOP per byteachieved TFLOP/sridge 295989 TFLOP/s peak3.35 TB/smemory-boundcompute-boundbatch moreB = 1B = 32B ≈ 300
Batching is a move to the right, not up. Each doubling of batch size doubles intensity and slides the point along the bandwidth slope; the machine only stops being memory-bound when the point reaches the ridge at B ≈ 300. Nothing about the kernel changed.

The last paragraph is the one to carry forward, because it is the seam this whole sequence is built on. Roofline is a steady-state throughput model. It silently assumes enough concurrent work to saturate the pipes, no dependent-stall bubbles, warm caches, and fixed overheads amortized to zero. At batch 1 against a hard deadline, every one of those assumptions fails. You are not under either roof, you are in a regime the model does not describe, and its central prescription (raise II by batching and reusing) is precisely the move that is unavailable to you. Part B replaces the objective with critical-path depth. Keep the roofline for what it is genuinely excellent at, which is telling you what you are not limited by.

Numbers worth memorizing

The latency ladder, in the units that matter. Cycle counts assume roughly 1.7 GHz on the GPU and 3 GHz on the CPU. Treat these as order-of-magnitude anchors, not datasheet values.

LevelLatencyBandwidthCapacity
GPU register file~1 cyceffectively ∞256 KB / SM
GPU shared memory / L1~25–35 cyc, ~20 ns~128 B/clk/SM228 KB / SM
GPU L2~200–350 cyc, ~150–250 ns~5–10 TB/s50 MB (H100)
GPU HBM3~350–450 ns3.35 TB/s80 GB
NVLink 4 (peer GPU)~1.5–2.5 µs450 GB/s / dir
PCIe Gen5 ×16 round trip~1–2 µs64 GB/s / dir
CPU L1d4–5 cyc, ~1.3 ns~200 GB/s/core32–48 KB
CPU L214–16 cyc, ~5 ns~100 GB/s/core1–2 MB
CPU L3 (LLC)40–60 cyc, ~15–25 ns~400 GB/s30–100 MB
DRAM, local socket~80–100 ns200–400 GB/s
DRAM, one NUMA hop~130–160 nslower, contended
Cut-through switch hop~50–100 ns

Ridge points. This is the table that explains why your kernel is memory-bound.

MachinePeak π\piβ\betaRidge II^{*}
H100 SXM, fp8 tensor, dense1979 TFLOP/s3.35 TB/s~590
H100 SXM, fp16 tensor, dense989 TFLOP/s3.35 TB/s~295
H100 SXM, fp32 CUDA cores67 TFLOP/s3.35 TB/s~20
A100 80GB, fp16 tensor312 TFLOP/s2.0 TB/s~156
Server CPU, AVX-512 fp32~2–3 TFLOP/s~0.4 TB/s~6–8

Intensities of things you actually run, all at the HBM boundary, fp16 unless noted.

WorkloadII (FLOP/byte)Regime
Elementwise add, SAXPY0.08–0.17memory, hopelessly
LayerNorm, softmax~0.5memory
GEMV / batch-1 decode weight matmul~1memory, ~300× under ridge
Decode attention, KV-cache read~1memory
Decode at batch BBB\approx Bmemory until BIB \approx I^{*}
Square GEMM, dimension nnn/3n/3compute above n900n \approx 900
FlashAttention prefill~O(dhead)O(d_{head})compute-ish
Training forward+backward100s–1000scompute

Batch-1 decode floors on one H100 at 3.35 TB/s, assuming perfect bandwidth utilization. Real achieved bandwidth is 70–85% of peak, so multiply the times by ~1.25 for an honest number.

ModelWeight bytesFloor / tokenCeiling
8B fp1616 GB4.8 ms210 tok/s
8B fp88 GB2.4 ms420 tok/s
70B fp16140 GB42 ms24 tok/s
70B fp870 GB21 ms48 tok/s
70B int435 GB10.4 ms96 tok/s

Little’s Law, concurrency=latency×throughput\text{concurrency} = \text{latency} \times \text{throughput}, applied to memory:

  • H100 HBM: 3.35 TB/s×400 ns1.34 MB3.35\ \text{TB/s} \times 400\ \text{ns} \approx 1.34\ \text{MB} in flight, which is roughly 10,500 outstanding 128 B sectors, or ~80 per SM. Occupancy is not a virtue in itself. It is the mechanism by which you meet this number.
  • One CPU core: ~10–16 line-fill buffers × 64 B / 80 ns ≈ 8–12 GB/s, an order of magnitude below the socket’s 200–400 GB/s. A single core cannot saturate its own DRAM. This is a memory-level-parallelism limit, not a bandwidth limit, and it is why prefetching and multiple independent pointer chases matter so much on CPU.

Critical thinking

The profiler reports 45% of peak FLOPs and 35% of peak HBM bandwidth. Where is the bottleneck?

Neither roof, which means the binding constraint is not in this roofline at all. Roofline gives an upper bound. Sitting under both roofs tells you only that something outside the model is limiting you. Ranked suspects:

  1. You are bound at a level this roofline cannot see. Shared-memory or L2 bandwidth, or register-file bandwidth. Draw the hierarchical roofline before drawing conclusions.
  2. Latency-bound, not bandwidth-bound. Insufficient memory-level parallelism to satisfy Little’s Law: low occupancy, long dependent chains, address arithmetic on the critical path. The pipes are wide open and you are not putting enough in flight.
  3. Issue-bound. The limiter is instruction throughput on a pipe that is neither FMA nor memory: address math, predication, shared-memory instructions, or the epilogue.
  4. Wave quantization. A partly-empty final wave dilutes the whole-kernel average. 132 SMs and 140 blocks means a second wave running at 6% utilization.
  5. Synchronization. __syncthreads() in an inner loop serializes what the roofline assumes is concurrent.

The discipline worth internalizing: roofline names what you are not limited by. It never names the limiter.

You fuse two memory-bound elementwise kernels. Bound the speedup before you measure.

Count bytes at the binding boundary, not FLOPs. Two passes over NN bytes, each reading and writing: 4N4N total. Fused: 2N2N. Ceiling is , and that is the whole answer at the HBM level.

Reality lands below it for three reasons and above it for one. Below: if NN is small enough that the intermediate stayed resident in L2, the second pass never touched HBM, so fusion bought you nothing at that boundary. On H100, with 50 MB of L2, anything under ~25 MB of working set is already in this regime. Fusion also raises register pressure, which lowers occupancy, which lowers memory-level parallelism, which lowers achieved bandwidth, so the fused kernel can run at a worse point on the same roof. Above: you also eliminated a kernel launch, roughly 3–5 µs, which dominates everything else for small NN.

Generalized: the ceiling on any fusion is bytes-before over bytes-after, evaluated at whichever boundary is actually binding. The same arithmetic sizes recomputation-versus-storage tradeoffs, FlashAttention included.

Decode is memory-bound. So why does speculative decoding help, and where is its ceiling?

Because in decode, arithmetic intensity is tokens-verified-per-weight-load. Reading the weights is the entire cost. The FLOPs are free, since you are sitting 300× below the ridge with all that compute idle. Verifying kk drafted tokens in a single forward pass loads the weights exactly once and does k×k\times the arithmetic: II goes from ~1 to ~kk, and you move right along the memory roof at constant byte cost. You are spending headroom you already owned.

Four ceilings, in the order you hit them:

  1. Acceptance. Expected accepted tokens per round is set by draft-target agreement, typically 2–4 for a well-matched draft. Everything past that is discarded work.
  2. The draft model’s own serial decode, which is itself batch-1 memory-bound and sits on the critical path.
  3. KV cache traffic. The “bytes are free” premise holds only while weights dominate. At long context the KV cache becomes the larger read and it scales with the tokens you process, so the IkI \approx k argument decays.
  4. The ridge itself. Once effective batch × kk approaches ~300 you become compute-bound and further kk buys nothing.

The same one-line test explains GQA, quantization, and continuous batching: they are all byte-count or intensity moves. None of them is a FLOP move. In a memory-bound regime, FLOP moves are not interesting.

Batch 1, hard 5 µs deadline, forever. What does roofline tell you?

Close to nothing, and the direction it points is wrong. Three distinct failures:

  • It is asymptotic. min(π,βI)\min(\pi, \beta I) presumes enough concurrent work to reach steady state and hide latency. At batch 1 the machine is overwhelmingly idle and you are under both roofs by a wide margin, in a regime the model does not describe.
  • Its only lever is unavailable. “Raise II” means batch or reuse. Batch is pinned at 1 by the problem statement, and there is no reuse to find inside a single sample.
  • It assumes fixed costs amortize to zero. At 5 µs nothing amortizes. Kernel launch alone is 3–5 µs. A PCIe round trip is 1–2 µs. Either one consumes the budget before a single FLOP retires.

The replacement model is latency=(dependent stage latencies)+fixed overheads\text{latency} = \sum(\text{dependent stage latencies}) + \text{fixed overheads}, with the objective being depth of the critical path measured in cycles or gate delays, not bytes moved. That reframing is the content of Part B, and it is why this workload class ends up on an FPGA or ASIC with weights held in on-chip SRAM and registers. Flattening the memory hierarchy to a single level does not make “bytes over bandwidth” smaller. It removes the term from the equation.

Would faster memory fix this kernel? Answer without buying the faster memory.

Measure II at the binding boundary and compare to II^{*}. Below the ridge, performance scales roughly linearly in β\beta and not at all in π\pi. Above it, the reverse. That is the model’s answer, and it is worth exactly as much as your byte accounting.

Two empirical tests that need no model at all. Clock sweep: pin the memory clock and sweep the SM clock, then swap. The slope of each curve tells you which roof you are on. Byte ablation: halve the bytes without touching the math, by quantizing weights fp16 → fp8. If the kernel gets ~2× faster you were memory-bound. If it barely moves, you were not, and you just learned something more reliable than any profiler counter.

Self-check

State the definition of arithmetic intensity so that it survives the follow-up “at which level?”

FLOPs performed divided by bytes moved across a named boundary, for a specific implementation. The FLOPs come from the algorithm; the bytes come from the implementation and the boundary. Naive and tiled GEMM share FLOPs and share a minimum byte count, yet differ by ~1000× in achieved II at HBM. An unqualified intensity number is not wrong so much as meaningless.

What is the ridge point for dense fp16 tensor cores on H100, and what square GEMM size crosses it?

I=989 TFLOP/s/3.35 TB/s295I^{*} = 989\ \text{TFLOP/s} / 3.35\ \text{TB/s} \approx 295 FLOP/byte. A square fp16 GEMM of dimension nn has HBM-boundary intensity 2n3/6n2=n/32n^3/6n^2 = n/3, so it crosses at n885n \approx 885, call it 900. Below that, a “compute” kernel is memory-bound, which is why small-batch matmuls disappoint and why the fp8 ridge at ~590 pushes that crossover out to n900n \approx 900 again once you account for halved bytes.

Batch-1 decode floor for a 70B model in fp8 on one H100. Then explain what tensor parallelism does to that number.

70 GB / 3.35 TB/s ≈ 21 ms per token, so ~48 tok/s, before derating for ~80% achieved bandwidth. Tensor parallelism across PP GPUs divides the per-GPU weight bytes by PP and multiplies aggregate bandwidth by PP, so the floor drops roughly as 1/P1/P. What it adds is an all-reduce per layer on the critical path: two collectives per transformer block over NVLink, at a few microseconds of latency each. For 80 layers that is a real fraction of the budget, and it is a latency cost, not a bandwidth cost, so it does not shrink as you add GPUs. It is the reason TP scaling on decode goes sublinear well before the roofline says it should.

How many bytes must be in flight to saturate H100 HBM? Why can one CPU core not saturate its socket's DRAM?

3.35 TB/s×400 ns1.34 MB3.35\ \text{TB/s} \times 400\ \text{ns} \approx 1.34\ \text{MB}, roughly 10,500 outstanding 128 B sectors, ~80 per SM. That is the real reason occupancy matters: it is the mechanism for meeting Little’s Law, not a goal in itself.

One CPU core is capped by its line-fill buffers, roughly 10–16 outstanding misses. At 64 B each over ~80 ns that is 8–12 GB/s, against 200–400 GB/s for the socket. The limit is memory-level parallelism, not bandwidth, which is why the fix is more independent streams and prefetch distance, and why a single-threaded pointer chase is the worst case in both directions.

Two kernels have identical arithmetic intensity. One reaches the memory roof; the other sits at 40% of it. Give three reasons.

Insufficient memory-level parallelism to satisfy Little’s Law, from low occupancy or long dependent chains. Poor access patterns, so the bytes requested are far fewer than the bytes transferred — intensity computed from useful bytes flatters an uncoalesced kernel that is actually pulling whole lines to use four of them. And structural stalls the model ignores: __syncthreads(), wave quantization, or partition camping across memory channels.

The general form: identical II means identical position on the horizontal axis, and says nothing about whether you can generate enough concurrency to reach the roof above you.

One sentence: why does roofline mispredict at batch 1 with a hard deadline?

It is a steady-state throughput asymptote that assumes saturating concurrency and amortized fixed costs, and at batch 1 there is no concurrency to saturate with and nothing over which to amortize, so the binding quantity is critical-path depth rather than bytes over bandwidth.