Chapter 11 · Section 11.2 · 50 min

The roofline model

Your profiler shows two kernels from the same Transformer layer on an H100. The matrix multiplication in the MLP runs at 700 TFLOP/s, about 70 percent of the chip's bfloat16 peak. The LayerNorm right after it runs at 6 TFLOP/s, well under 1 percent. A colleague proposes spending a week rewriting the LayerNorm to make it "as efficient as the matmul". Should they?

No, and by the end of this section you will be able to prove it in one line. The LayerNorm is already running within about 10 percent of the fastest speed any LayerNorm can run on that GPU. Its limit is not arithmetic, it is the rate at which the GPU can read and write memory, and a LayerNorm does only a handful of operations per byte. The tool that makes this kind of judgment quick and reliable is the roofline model (Williams, Waterman & Patterson, 2009). It needs two numbers about the hardware and two numbers about the kernel, and it tells you which resource limits the kernel, how fast it could possibly go, and therefore whether optimizing it is worth your time.

Two limits on every kernel

Recall from §11.1 that a GPU is a fast calculator fed by a comparatively narrow pipe. Describe the hardware by two numbers.

  • π\pi, the peak compute throughput in FLOP/s. For an H100 SXM in dense bfloat16 on tensor cores, π=989\pi = 989 TFLOP/s.
  • β\beta, the peak memory bandwidth in bytes per second between HBM and the chip. For the same GPU, β=3.35\beta = 3.35 TB/s.

Describe a kernel by two numbers as well.

  • FF, the number of floating-point operations it performs.
  • QQ, the number of bytes it moves between HBM and the chip, reads plus writes.

The kernel cannot finish before its arithmetic is done, which takes at least F/πF/\pi, and it cannot finish before its data has moved, which takes at least Q/βQ/\beta. GPUs overlap the two (that was the point of latency hiding in §11.1), so the best possible time is the larger of the two,

t  ≥  max⁡ ⁣(Fπ,  Qβ).t \;\ge\; \max\!\left(\frac{F}{\pi},\; \frac{Q}{\beta}\right).

In plain words, a kernel is limited by whichever of its two jobs, computing or moving data, takes longer on this machine.

Now divide FF by both sides. The achieved throughput P=F/tP = F/t obeys

P  ≤  min⁡ ⁣(π,  β⋅I),whereI=FQP \;\le\; \min\!\left(\pi,\; \beta \cdot I\right), \qquad\text{where}\qquad I = \frac{F}{Q}

is the kernel's arithmetic intensity, its FLOPs per byte of memory traffic. The two limits meet at the ridge point

I⋆=πβ.I^\star = \frac{\pi}{\beta}.

A kernel with I<I⋆I < I^\star is memory-bound. Its best possible throughput is βI\beta I, which grows in proportion to its intensity, and making its arithmetic faster changes nothing. A kernel with I>I⋆I > I^\star is compute-bound. Its ceiling is π\pi, and reducing its memory traffic changes nothing.

In plain words, arithmetic intensity is how much work you do with each byte you fetch. Below the ridge point, bytes are the bottleneck and the speed you can reach is proportional to intensity. Above it, FLOPs are.

Back to the puzzle. A LayerNorm does about 8 FLOPs per element (we count them below) and, in bfloat16, reads 2 bytes and writes 2 bytes per element, so I≈8/4=2I \approx 8/4 = 2 FLOPs per byte. Its ceiling is βI=3.35×2=6.7\beta I = 3.35 \times 2 = 6.7 TFLOP/s. At 6 TFLOP/s it is at 90 percent of its roof. The only way to make it substantially faster is to move fewer bytes, for example by fusing it into a neighboring kernel so that its input never makes the round trip to HBM (§11.7).

The roofline picture

Plot attainable throughput against intensity, both on logarithmic axes. The bound βI\beta I is a straight line of slope 1 (the slanted roof), the bound π\pi is a horizontal line (the flat roof), and they meet at I⋆I^\star. Every kernel is a point under this roof. Its horizontal position is its intensity, a property of the algorithm, and the gap between the point and the roof above it is how much an implementation could still gain.

The widget draws the roofline for several GPUs and number formats and places the operations of a Transformer on it. Change the hardware and watch the ridge point move. Change the matrix size or the batch size and watch matmuls slide along the slanted roof until they hit the flat one.

Is this operation limited by compute or by memory bandwidth?
Attainable TFLOP/s is the lower of the flat roof (peak compute) and the slanted roof (bandwidth times intensity). Each dot is an operation, mostly from a Transformer layer of width 4,096, plus a square matmul and a kernel of your own.
Loading interactive figure…

The ridge points of the GPUs you are likely to use, in dense bfloat16 unless stated, all from NVIDIA's datasheets.

GPUπ\pi, dense TFLOP/sβ\beta, TB/sridge point I⋆I^\star, FLOPs per byte
A100 40GB SXM3121.555201
A100 80GB SXM3122.039153
H100 SXM9893.35295
H100 SXM, FP81,9793.35591
H100 SXM, FP32 without tensor cores673.3520
H200 SXM9894.8206
B200 (one GPU of an HGX B200)2,2508.0281

The trend matters more than any one entry. Compute has grown faster than bandwidth for decades (the A100 to H100 step tripled π\pi but less than doubled β\beta), so ridge points drift upward, and every generation turns more kernels memory-bound. The H200 shows the same pressure from the other side. It is an H100 with more and faster memory (141 GB at 4.8 TB/s), so on the roofline its only effect is to lower the ridge point and speed up memory-bound work. Its larger capacity matters too, for bigger batches and longer KV caches, but that is a different limit from the ones the roofline draws.

The intensity of a Transformer's operations

Let's derive the intensity of each operation in a Transformer layer. Write ss for the bytes per element (2 in bfloat16), and count ideal traffic, each input read once and each output written once, which is what a well-written kernel achieves.

Elementwise operations

Adding two tensors of nn elements does F=nF = n FLOPs and moves Q=3nsQ = 3ns bytes (two reads, one write), so

Iadd=n3ns=13s≈0.17in bfloat16.I_{\text{add}} = \frac{n}{3ns} = \frac{1}{3s} \approx 0.17 \quad\text{in bfloat16}.

That is about 1,800 times below the H100's ridge point, so an elementwise add runs at about 0.06 percent of peak FLOP/s, and that is the best case. Activation functions are no different. GELU costs perhaps 8 to 20 FLOPs per element depending on how you count tanh or erf, still an intensity of a few FLOPs per byte. Every elementwise operation is memory-bound on every modern GPU, and its time is set by the bytes alone.

Softmax and LayerNorm

A row softmax over nn entries finds the maximum (nn comparisons), subtracts it (nn), exponentiates (nn), sums (nn), and divides (nn), about 5n5n FLOPs, while a fused kernel reads the row once and writes it once, 2ns2ns bytes. So Isoftmax=5/(2s)=1.25I_{\text{softmax}} = 5/(2s) = 1.25 in bfloat16. (We count exp as one FLOP, although it runs on the slower special-function units of §11.1. The kernel is memory-bound either way.)

A LayerNorm over a row of dd features (§5.2) computes the mean (about dd FLOPs), the variance (subtract, square, add, about 3d3d), the normalization (subtract and multiply by 1/σ2+ϵ1/\sqrt{\sigma^2+\epsilon}, where σ2\sigma^2 is the row's variance and ϵ\epsilon a small constant, 2d2d), and the affine map with the learned gain γ\boldsymbol\gamma and shift β\boldsymbol\beta (2d2d), about 8d8d FLOPs in all, against 2ds2ds bytes for the row plus 2ds2ds for γ\boldsymbol\gamma and β\boldsymbol\beta, which are read once for all rows and so vanish per row. That gives ILN≈8/(2s)=2I_{\text{LN}} \approx 8/(2s) = 2.

Matrix multiplication

Multiply A∈RM×K\mathbf{A} \in \R^{M\times K} by B∈RK×N\mathbf{B} \in \R^{K\times N} (generic operands here, with KK the inner dimension). Each of the MNMN outputs is a dot product of length KK, so F=2MNKF = 2MNK (one multiply and one add per term, the book's convention since §4.7). The minimum traffic is reading both inputs and writing the output once, Q=s(MK+KN+MN)Q = s(MK + KN + MN), so

Imm=2MNKs (MK+KN+MN).I_{\text{mm}} = \frac{2MNK}{s\,(MK + KN + MN)}.

For square matrices, M=N=K=nM = N = K = n, this is I=2n3/(3sn2)=2n/(3s)I = 2n^3/(3sn^2) = 2n/(3s), or n/3n/3 in bfloat16. The intensity grows with the size, because each loaded number takes part in nn multiply-adds. A square bfloat16 matmul is compute-bound on an H100 once n/3>295n/3 > 295, that is n≳890n \gtrsim 890. At n=4096n = 4096 the intensity is 1,365 and the matmul is firmly compute-bound.

In plain words, matmul is the one operation whose work grows faster than its data, by a factor of the matrix size, which is why it is the only operation that can use the tensor cores fully.

The case that matters most in practice is a linear layer, a batch of BB token vectors times a d×dd\times d weight matrix, M=BM = B and K=N=dK = N = d. Then

Ilinear(B)=2Bd2s (Bd+d2+Bd)=2Bd2s (d2+2Bd)  ≈  2Bswhen B≪d.I_{\text{linear}}(B) = \frac{2Bd^2}{s\,(Bd + d^2 + Bd)} = \frac{2Bd^2}{s\,(d^2 + 2Bd)} \;\approx\; \frac{2B}{s} \quad\text{when } B \ll d.

In bfloat16 the intensity of a linear layer is about BB, the number of tokens processed together. The weights dominate the traffic, and each weight that is read is used once per token in the batch. So batch size is intensity, which is the single most useful fact in this section.

Attention

Take one head with sequence length TT and head width dkd_k, as in §4.3. A naive implementation runs three kernels. The scores S=QK⊤\mathbf{S} = \mathbf{Q}\mathbf{K}^\top read Q\mathbf{Q} and K\mathbf{K} (2Tdk2Td_k elements) and write S\mathbf{S} (T2T^2). The softmax reads S\mathbf{S} and writes A\mathbf{A} (2T22T^2). The output AV\mathbf{A}\mathbf{V} reads A\mathbf{A} and V\mathbf{V} (T2+TdkT^2 + Td_k) and writes O\mathbf{O} (TdkTd_k). The FLOPs are 2T2dk2T^2d_k for each product plus about 5T25T^2 for the softmax, so

Inaive=4T2dk+5T2s (4Tdk+4T2)  ≈  4dk+54swhen T≫dk.I_{\text{naive}} = \frac{4T^2 d_k + 5T^2}{s\,(4Td_k + 4T^2)} \;\approx\; \frac{4d_k + 5}{4s} \quad\text{when } T \gg d_k .

With dk=128d_k = 128 in bfloat16 this is about 65 FLOPs per byte (63 at T=4096T = 4096, where the 4Tdk4Td_k term still counts a little, as the cell below shows), well below the H100's ridge of 295. Naive attention is memory-bound, and the reason is the 4T24T^2 elements of score and weight traffic, which do not shrink as the head gets wider. A fused kernel that never writes S\mathbf{S} or A\mathbf{A} to HBM moves only Q\mathbf{Q}, K\mathbf{K}, V\mathbf{V}, and O\mathbf{O}, about 4Tdk4Td_k elements if each is moved once, and its intensity becomes about T/sT/s, thousands of FLOPs per byte. That is the core idea of FlashAttention. Fitting it into on-chip memory forces K\mathbf{K} and V\mathbf{V} to be streamed several times, and §12.3 counts that traffic exactly.

The cell computes all of these on an H100, for a 4096×40964096\times4096 activation in bfloat16, linear layers of width 4,096 at several batch sizes, and one attention head at T=4096T = 4096.

Python · cell 1

Look at the times. The add, the softmax, and the LayerNorm each take 20 to 30 microseconds on 16 million elements, while a linear layer on 512 tokens of width 4,096, which does over 17 billion FLOPs, takes about 17. The linear layer at batch 1 and at batch 128 takes about the same time, around 10 microseconds, because in both cases the time is spent reading the same 32 MB of weights. The same table, summarized, is the roofline view of a Transformer layer on an H100.

operationintensity, bfloat16bound on an H100
elementwise (add, GELU, dropout, residual)0.2 to a fewmemory
softmax, LayerNorm, RMSNormabout 1 to 2memory
linear layer, BB tokensabout BB (for B≪dB \ll d)memory below about 300 tokens, compute above
naive attentionabout dk/2d_k/2memory
fused attentionabout T/2T/2compute (for long sequences)
Interview

Intensity and bound for common operations

Implement op_cost(op, shape, s=2) returning the pair (F, Q) of FLOPs and ideal HBM bytes, with these conventions from the text.

  • "add", shape (n,), has nn FLOPs and moves 3ns3ns bytes.
  • "matmul", shape (M, N, K), has 2MNK2MNK FLOPs and moves s(MK+KN+MN)s(MK + KN + MN) bytes.
  • "softmax", shape (rows, n), has 5 FLOPs per element and reads and writes each element once.
  • "layernorm", shape (rows, d), has 8 FLOPs per element, reads and writes each element once, and also reads γ\boldsymbol\gamma and β\boldsymbol\beta once (2ds2ds bytes).
  • "attention_naive", shape (T, d_k), has 4T2dk+5T24T^2d_k + 5T^2 FLOPs and moves s(4Tdk+4T2)s(4Td_k + 4T^2) bytes.

Then implement roofline(F, Q, peak, bw) returning (t, bound), the lower bound on the time in seconds and the string "compute" or "memory". Call a kernel exactly at the ridge point compute-bound.

Hidden tests, hints, and AI help, from $1.99 a month.

Batch size, prefill, and decode

The linear-layer formula explains the economics of serving language models, which §6.5 introduced. Prefill processes the prompt's pp tokens together, so every weight matrix sees B=pB = p tokens and the work is compute-bound for prompts of a few hundred tokens or more. Decode produces one token per sequence per step, so with a single sequence B=1B = 1, the intensity is about 1, and the GPU runs at about β×1=3.35\beta \times 1 = 3.35 TFLOP/s, a third of a percent of peak.

Put numbers on Llama 3 8B in bfloat16, about 16 GB of weights. One decode step must read all of them, at least 16 GB/3.35 TB/s≈4.816\,\text{GB}/3.35\,\text{TB/s} \approx 4.8 ms, so one sequence gets at most about 210 tokens per second however clever the kernels are. Batching BB sequences reuses each weight BB times. Until the batch approaches the ridge point, the step takes about the same 4.8 ms, so throughput grows almost linearly with BB. This is why inference servers batch requests aggressively, and why the cost per token of serving a model falls so steeply as load grows.

There is a catch that the weights-only picture hides. Each sequence also reads its own KV cache at every step, and that traffic is not shared across the batch. Llama 3 8B's cache is 2×32 layers×8 KV heads×128×22 \times 32 \text{ layers} \times 8 \text{ KV heads} \times 128 \times 2 bytes, 128 KiB per token (the formula is from §6.5, and the 8 shared key-value heads are the grouped-query attention of §12.5), so a sequence with 4,096 tokens of context reads another 0.5 GB per step. The cell models one decode step as max⁡(F/π,Q/β)\max(F/\pi, Q/\beta), with QQ the weights plus every sequence's cache.

Python · cell 2

With weights alone, throughput rises linearly until about B≈300B \approx 300 and then flattens at the compute roof. With a 4,096-token context, it bends over much earlier, because the cache traffic grows with the batch. As B→∞B \to \infty the intensity tends to the FLOPs per token divided by the cache bytes per token, about 34, far below the ridge point. Decode with long contexts is memory-bound at any batch size. That single fact motivates grouped-query and latent attention (§12.5), KV-cache quantization, and the capacity planning of serving systems. (Memory capacity bites too. The 64 GB left after the weights hold about 120 such sequences.)

Interview

The batch size that makes a linear layer compute-bound

Implement min_batch(d, peak, bw, s=2) returning the smallest integer B≥1B \ge 1 for which a linear layer, a (B×d)(B\times d) activation times a (d×d)(d\times d) weight with the ideal traffic s(d2+2Bd)s(d^2 + 2Bd), has intensity at least the ridge point peak / bw. Return None if no batch size is large enough. Solve the inequality in closed form rather than searching, so that it is instant for any dd, and take care with rounding at the boundary.

Hidden tests, hints, and AI help, from $1.99 a month.

Model FLOPs utilization, seen from the hardware

§6.7 defined model FLOPs utilization (MFU) for a training run, the FLOPs the model mathematically requires per second divided by the peak of all its GPUs,

MFU=(FLOPs per token)×(tokens per second)ngpu×π,\text{MFU} = \frac{(\text{FLOPs per token}) \times (\text{tokens per second})}{n_{\text{gpu}} \times \pi},

where ngpun_{\text{gpu}} is the number of GPUs, π\pi is the peak of one GPU, and training costs about 6N+12LdT6N + 12LdT FLOPs per token for a model with NN parameters (no longer a matrix dimension), LL layers of width dd, and context length TT. In roofline terms, MFU is how close a whole run sits to the flat roof, averaged over every kernel, every gap between kernels, and every stall. Three choices decide what number comes out, and each is a question about the hardware.

Which FLOPs. MFU counts only what the model needs, so it cannot be inflated by doing extra work. Hardware FLOPs utilization (HFU) counts what the chip actually executed, recomputation included. The forward pass is a third of a training step's FLOPs, 2N+4LdT2N + 4LdT per token, so full activation recomputation (§10.1), which repeats it, makes HFU about 8/6=4/38/6 = 4/3 of MFU. Selective recomputation (Korthikanti et al., 2022) recomputes only the attention scores, softmax, and weighted sum of values, which take a lot of memory but few FLOPs, about 4LdT4LdT per token. HFU says how busy the tensor cores were, and MFU how much of that work was useful. A configuration that recomputes more can show a higher HFU and still train more slowly, which is why MFU is the number to compare across runs.

Which peak. π\pi must be the dense tensor-core peak in the format the matmuls run in (the table in §11.1). Dividing by the datasheet's sparse figure halves the reported MFU, and dividing by the 67 TFLOP/s of the FP32 lanes would inflate it fifteenfold. For runs whose matmuls are in FP8, conventions differ. Against the BF16 peak, DeepSeek-V3's pretraining works out to about 35 percent, and against the FP8 peak it actually used, about 17 percent (§8.9), so always say which peak you used.

Which time. Tokens per second is measured over whole steps on the wall clock, so idle gaps between kernels, communication that is not overlapped, and data loading all count against MFU. Every kernel can sit on its own roof while the run's MFU is poor. Even the kernels set a ceiling below 100 percent. Suppose the matmuls, which carry nearly all the FLOPs, run at 75 percent of the peak, while memory-bound operations, which carry almost none, take a quarter of the step. Then MFU is at most 0.75×0.75≈560.75 \times 0.75 \approx 56 percent before a single byte is communicated.

A worked example. An 8-billion-parameter model with L=32L = 32, d=4096d = 4096, and T=4096T = 4096 needs 6×8×109+12×32×4096×4096≈5.44×10106 \times 8\times10^{9} + 12 \times 32 \times 4096 \times 4096 \approx 5.44\times10^{10} FLOPs per token. Trained on 64 H100s at 600,000 tokens per second, its MFU is 5.44×1010×6×105/(64×989×1012)≈525.44\times10^{10} \times 6\times10^{5} / (64 \times 989\times10^{12}) \approx 52 percent. With full recomputation the chip executes 2N+4LdT≈1.8×10102N + 4LdT \approx 1.8\times10^{10} more FLOPs per token, an HFU of about 69 percent, and the same run divided by the sparse peak would report 26 percent. For a published pair, PaLM 540B reported an MFU of 46.2 percent and an HFU of 57.8 percent (Chowdhery et al., 2022), a ratio of 1.25. Published large dense runs typically report MFUs between about 35 and 55 percent. §10.7 reproduces published MFUs and shows how parallelism choices move them.

Core

MFU and HFU of a training run

Implement utilization(n_params, n_layers, d_model, seq_len, tokens_per_s, n_gpus, peak, recompute="none", sparse_peak=False) returning the pair (mfu, hfu). Model FLOPs per token are 6N+12LdT6N + 12LdT. Hardware FLOPs per token add the recomputed work, nothing for "none", 4LdT4LdT for "selective" (the attention core's forward pass), and 2N+4LdT2N + 4LdT for "full" (the whole forward pass). peak is one GPU's datasheet number, and when sparse_peak is true it is the 2:4 sparsity figure, twice the dense peak you must divide by.

Hidden tests, hints, and AI help, from $1.99 a month.

More roofs, and what the model leaves out

The roofline we drew has one memory roof, HBM. Real chips have one for each level of the hierarchy, and a kernel whose data fits in L2 or shared memory is limited by those faster roofs instead. Profilers such as Nsight Compute draw this hierarchical roofline, with a point per level, and the level whose point sits closest to its roof is the bottleneck. A tiled matmul, for example, can be limited by shared memory bandwidth if its register tiles are too small (§11.5).

There are also lower ceilings below the flat roof. A kernel that cannot use tensor cores (most non-matmul code) is capped by the 67 TFLOP/s of the FP32 lanes, with a ridge point of only 20. And there is a regime the roofline does not draw at all. A kernel that moves 100 KB and does a million FLOPs should take about 30 nanoseconds by the roofline, but launching it costs a few microseconds. Such work is overhead-bound, and no amount of kernel tuning helps. Only doing more work per launch, by batching, fusing, or capturing CUDA graphs, does (§11.7). Horace He's essay names the three regimes, compute, memory bandwidth, and overhead, and gives the right first question for any slow model, which of the three am I in?

In PyTorch

You can draw a roofline for your own CPU. The block times matrix products of growing size and an elementwise add, and reports intensity and achieved throughput. Small matmuls sit on the slanted part (and lower still, because of call overhead), large ones approach the flat part.

🔥 In PyTorchfor a machine with PyTorch

Read more

Watch next

🎬 Horace He: Building Machine Learning Systems for a Trillion Trillion Floating Point Operations · Jane StreetThe author of Making Deep Learning Go Brrrr on compute, memory, and overhead as the three regimes of ML performance.
🎬 Lecture 8: CUDA Performance Checklist · GPU MODEMark Saroufim's checklist of CUDA optimizations, from coalescing and occupancy to arithmetic intensity, and how to tell which ones matter.