Part 2 · Chapter 10

The hardware

What a GPU is fast at, why moving bytes is the real cost, and why FlashAttention exists.

Where we are

Chapter 8 priced Beacon's run in operations: 3.8 × 10²⁵. Chapter 9 built the corpus those operations consume. This chapter is about the machines that execute them, and it has one idea at its centre: arithmetic is cheap and moving data is expensive. Every engineering decision in the rest of Part 2 and in Chapter 19 descends from that. By the end you can look at any operation in the model, from Chapter 2's matmul to Chapter 3's T² scores, and say whether a GPU will spend its time computing or waiting.

The question this chapter answers: what does a GPU actually do well, what limits it, and how does the model's arithmetic have to be arranged to keep it busy?

Picture this

A kitchen with a thousand cooks and one small pantry door. Each cook can chop a vegetable in a second. But every vegetable comes through the door, and the door passes one crate a minute. If each crate holds a thousand vegetables, the cooks are busy: a crate arrives, a thousand cooks chop for a second, done, next crate. If each crate holds ten vegetables, ten cooks chop for a second and nine hundred and ninety stand idle for fifty-nine seconds. The kitchen is not limited by cooks. It is limited by the door.

The obvious fix is to bring in fewer, fuller crates. The clever fix is to notice that some recipes need the same vegetable chopped, mixed, chopped again, and to keep it on the counter between steps instead of sending it back through the door each time. A cook's counter is tiny, but it is right there. A tray in the pantry is bigger but behind the door. The warehouse across town holds everything and takes an hour.

The cooks are the GPU's cores. The door is memory bandwidth. Crates per minute is bytes per second; vegetables per crate is how much arithmetic you get to do per byte fetched. The counter is on-chip SRAM, the pantry is HBM, the warehouse is the other GPUs and the network. FlashAttention is the recipe rewritten so the vegetable never goes back through the door.

Map it
In the kitchenIn the machineThe word we will use
A thousand cooks chopping in lockstepThousands of simple cores executing the same instruction on different dataSIMT, streaming multiprocessor (SM)
A cook who chops a whole tray in one motionA unit that multiplies a small matrix block in one instructiontensor core
The pantry doorBytes per second between HBM and the coresmemory bandwidth
Vegetables per crateFLOPs performed per byte movedarithmetic intensity
Counter · pantry · warehouseRegisters and SRAM · HBM · other GPUs and networkmemory hierarchy
"Are we limited by cooks or by the door?"A plot of achievable FLOP/s against intensityroofline model
The recipe rewritten to keep the vegetable on the counterAttention computed in tiles that never leave SRAMFlashAttention
Fraction of the hour the cooks were choppingAchieved FLOP/s over peak, for the whole runmodel FLOPs utilisation (MFU)

10.1What a GPU is

A CPU has a handful of cores, each built to run one thread as fast as possible: deep pipelines, branch prediction, large caches. A GPU has thousands of cores, each simple, arranged in groups (streaming multiprocessors, SMs) that execute one instruction across many data elements at once. An H100 has 132 SMs, and each SM runs thousands of threads in lockstep groups public. Ask it to do a thousand different things and it crawls. Ask it to do the same thing to a thousand numbers and it flies.

That is a description of a matmul. Chapter 2 showed that every entry of XW is an independent dot product; a [16384, 8192] × [8192, 28672] product is 470 million independent dot products of length 8192, which is exactly the shape of work a GPU was built for. Since about 2017 GPUs have gone further and added tensor cores: hardware that multiplies a small block, such as 16 × 16 by 16 × 16, in a single instruction public. Nearly all of a GPU's headline "FLOP/s" number is tensor-core throughput on matmul-shaped work at reduced precision. Feed it anything else and you get a fraction.

GPUDense BF16 matmulHBM sizeHBM bandwidthOn-chip SRAM per SMEvidence
A100 (2020)312 TFLOP/s80 GB2.0 TB/s192 KBpublic
H100 SXM (2022)989 TFLOP/s80 GB3.35 TB/s228 KBpublic
B200 (2024)≈ 2.25 PFLOP/s≈ 180–192 GB≈ 8 TB/s≈ 228 KBpublic, spec sheets vary by SKU

Read across a row. From A100 to H100, matmul throughput tripled; bandwidth went up 1.7×. The gap between "how fast can it compute" and "how fast can it be fed" widened, and it keeps widening with every generation. That trend is why the rest of this chapter is about bytes.

Spec-sheet FLOP/s are often quoted "with sparsity", which doubles the number under an assumption that half the weights are zero in a structured pattern. Frontier training does not use that mode. The dense numbers above are the ones that matter.

10.2The memory hierarchy

Data lives at several distances from the cores, and each step outward is roughly ten times larger and several times slower.

registers · 256 KB per SM ~100 TB/s, on the cores shared memory / L1 · 228 KB per SM ~20 TB/s · SRAM · FlashAttention's tile lives here L2 cache · 50 MB per GPU ~7 TB/s HBM · 80 GB per GPU · 3.35 TB/s "GPU memory" NVLink · 8 GPUs per node · 0.9 TB/s per GPU 640 GB total InfiniBand / Ethernet · thousands of GPUs · 0.05 TB/s per port bigger, farther, slower a byte on its way to the cores climbs the pyramid; the goal is to use it many times before it descends
Where can a byte be, and what does each level cost? H100 numbers, public from the spec sheet and vendor documentation for the fixed levels; the two SRAM bandwidths are order-of-magnitude from microbenchmarks inferred. Each ring outward is larger and slower. The animated dot is one value being fetched from HBM into registers, used, and eventually written back.
Static view of the widget. Llama 3 8B's 16 GB of weights fit in HBM and stream through it in about 5 ms; they do not fit in any on-chip level. A 64 KB attention tile fits in one SM's shared memory and moves in nanoseconds. Beacon's 810 GB fits only across a node's NVLink-connected GPUs, taking about 0.9 s to move once.

Two levels matter most for the model. HBM is where the weights and activations live between operations; every matmul reads its inputs from there and writes its outputs back. Shared memory (SRAM on each SM) is where a kernel keeps the block it is working on; it is tiny, about 228 KB, but roughly six times the bandwidth of HBM and right next to the cores. The art of a fast kernel is to load a block from HBM into SRAM once, do as much arithmetic on it as possible, and write back once.

Beyond the GPU, NVLink connects the eight GPUs of a node at about 900 GB/s per GPU on H100 public, and InfiniBand or Ethernet connects nodes at 400 Gb/s per port, 50 GB/s, about 70 times slower than HBM public. Chapter 11 is about arranging a training run so that the slow links carry as little as possible.

10.3Arithmetic intensity and the roofline

Here is the tool that turns the kitchen picture into a number. For any operation, count the FLOPs it performs and the bytes it must move between HBM and the cores. Their ratio is the operation's arithmetic intensity, in FLOPs per byte. Compare it to the GPU's ratio of peak FLOP/s to bandwidth, the ridge point:

H100:  ridge  =  989 × 10¹² FLOP/s  ÷  3.35 × 10¹² B/s   ≈  295 FLOP per byte

intensity  <  295   →   memory-bound: the cores finish before the next bytes arrive; time = bytes / bandwidth
intensity  >  295   →   compute-bound: bytes arrive before the cores are done;   time = FLOPs / peak

An operation needs to do about 300 floating-point operations on every byte it fetches, or the H100's cores wait. That is a demanding number, and most of the operations in a transformer do not meet it.

By hand

Three operations from a Llama 3 70B block, in bf16 (2 bytes per number). Shapes: C = 8192, MLP hidden H = 28672, sequence T = 8192, two sequences in the batch.

1 · the MLP expand matmul during prefill:  [16384, 8192] × [8192, 28672]
    FLOPs   = 2 × 16384 × 8192 × 28672                    ≈ 7.7 × 10¹²
    bytes   = 2 × (16384×8192 + 8192×28672 + 16384×28672)  ≈ 1.7 × 10⁹      (read X, read W, write Y)
    intensity ≈ 4,600 FLOP/B     ≫ 295     compute-bound.  Time ≈ 7.7×10¹² / 989×10¹² ≈ 7.8 ms

2 · the same matmul during decode: one token per sequence:  [2, 8192] × [8192, 28672]
    FLOPs   = 2 × 2 × 8192 × 28672                        ≈ 9.4 × 10⁸
    bytes   = 2 × (2×8192 + 8192×28672 + 2×28672)         ≈ 4.7 × 10⁸      (the weight matrix dominates)
    intensity ≈ 2 FLOP/B         ≪ 295     memory-bound.   Time ≈ 4.7×10⁸ / 3.35×10¹² ≈ 140 µs, cores idle 99%

3 · attention scores for one head, one sequence, the matrix written out:  [8192, 128] × [128, 8192]
    FLOPs   = 2 × 8192 × 8192 × 128                       ≈ 1.7 × 10¹⁰
    bytes   = 2 × (2×8192×128 + 8192×8192)                ≈ 1.4 × 10⁸      (writing the 8k × 8k result is most of it)
    intensity ≈ 124 FLOP/B       < 295     memory-bound, and the softmax that follows reads it all back again
$ python code/ch10/roofline.py
ridge point = 295 FLOP/byte

matmul [16k, 8k]×[8k, 28k] (prefill) intensity   4587.5 FLOP/B  compute   7782.2 µs  memory    500.8 µs  → compute-bound
matmul [2, 8k]×[8k, 28k]  (decode) intensity      2.0 FLOP/B  compute      0.9 µs  memory    140.3 µs  → memory-bound
Q·Kᵀ one head, T=8k, materialised  intensity    124.1 FLOP/B  compute     17.4 µs  memory     41.3 µs  → memory-bound
RMSNorm over [16k, 8k]             intensity      1.0 FLOP/B  compute      0.5 µs  memory    160.3 µs  → memory-bound
softmax over [8k, 8k]              intensity      1.2 FLOP/B  compute      0.3 µs  memory    80.1 µs  → memory-bound

The same matrix multiply is compute-bound at prefill and memory-bound at decode, by a factor of a thousand in intensity, because the weight matrix is read once either way but multiplied against 16,384 rows in one case and 2 in the other. This is Chapter 7's "decode is memory-bound", now with the numbers. And every elementwise operation (norms, activations, softmax) sits at intensity around 1: they are pure memory traffic, and a naive implementation that runs them as separate passes over HBM costs more time than the matmuls they surround.

Static view of the widget. The H100 roofline: a diagonal of slope 3.35 TB/s meeting a flat ceiling at 989 TFLOP/s at the ridge, 295 FLOP/B. The prefill matmul sits on the ceiling. The decode matmul at batch 2 sits far down the diagonal at 2 FLOP/B; at batch 128 it reaches 120 FLOP/B, still below the ridge. Norms and softmax sit at the bottom left.
Static view of the widget. For a [M, 8192] × [8192, 28672] matmul in bf16, intensity is about 2·M/(2 + M/14336 + M/4096) FLOP/B: 2 at M = 1, 120 at M = 128, 850 at M = 1024, 4,600 at M = 16384. Only M above roughly 300 rows makes the H100 compute-bound on this shape.

Three consequences that the rest of the book keeps using:

10.4Why FlashAttention exists

Chapter 3's attention, executed literally: compute S = QKᵀ, a [T, T] matrix, and write it to HBM. Read it back, compute the row softmax, write P. Read P back, multiply by V. Three passes over a matrix with T² entries. At T = 128k that matrix is 33 GB per head per layer in bf16, larger than most GPUs' memory, and each of the three passes moves it at 3.35 TB/s. The arithmetic, by contrast, is a modest 4·T²·d: high intensity when done in one go, but the literal implementation never does it in one go.

The obstacle to doing it in one go is the softmax. Each row's probabilities need the row's maximum (for numerical safety) and the row's sum, and both depend on all T scores in the row. You cannot blend the values with the first hundred keys' probabilities until you know the denominator, which needs the remaining 127,900. So it seems the whole row must exist before any of it can be used.

FlashAttention's insight, from a 2022 paper by Dao and colleagues public, is that the softmax can be computed incrementally. Keep three running quantities per query row: the maximum seen so far m, the sum of exponentials so far ℓ, and the unnormalised output so far o. Load a tile of keys and values into SRAM, compute the tile's scores, and fold them in. When a new tile raises the maximum, multiply the old ℓ and o by e^(m_old − m_new) to rescale them to the new reference point, then add the tile's contributions. At the end, divide o by ℓ. The result is bit-for-bit the same softmax-weighted blend; the [T, T] matrix was never stored anywhere.

Math box · the online softmax

For one query row, after processing keys 1 … j:

m_j  =  max(m_{j−1}, s_j)                                   running maximum of scores
ℓ_j  =  ℓ_{j−1} · e^(m_{j−1} − m_j)  +  e^(s_j − m_j)         running sum, rescaled to the new max
o_j  =  o_{j−1} · e^(m_{j−1} − m_j)  +  e^(s_j − m_j) · v_j    running output, rescaled the same way

final output  =  o_T / ℓ_T   =   Σ_j e^(s_j − m_T) v_j  /  Σ_j e^(s_j − m_T)   =   softmax(s) · V

The rescaling factor e^(m_old − m_new) is what makes the trick exact: every earlier term was computed relative to an old maximum, and multiplying by the factor converts it to the new one. In practice tiles of 64 to 128 keys are folded in at once rather than one key at a time, but the three lines are the whole idea. The backward pass (Chapter 5) recomputes the tile's scores from Q and K instead of storing P; that costs extra FLOPs and saves the T² memory again, which at these intensities is the right trade.

Static view of the widget. Eight keys in tiles of two. After tile 1: m = 1.90, ℓ = 1.20. Tiles 2 and 3 raise nothing: ℓ grows to 1.75, then 2.56. Tile 4 contains the largest score, 2.40; m rises, and ℓ and o are rescaled by e^(1.90 − 2.40) = 0.607 before the tile is added, giving ℓ = 2.76. The final o/ℓ equals the slow softmax(q·Kᵀ)·V exactly. Zero score bytes were written to HBM.

What changes and what does not:

Attention as written (Chapter 3)FlashAttention
FLOPs, forward4·T²·d per head4·T²·d per head (same)
Memory for scoresT² per head, in HBMone tile, in SRAM
HBM traffic≈ 3·T² reads/writes≈ T·d, read Q, K, V once
Bound bymemorycompute (on H100, about 75% of peak in FA3 public)
Max context on one GPUa few thousand tokenslimited by the KV cache, not by the scores

The arithmetic did not shrink. Attention is still quadratic in T in work, and Chapter 12's sparse and linear variants exist because of that. But the memory went from quadratic to linear, and the traffic through the pantry door fell by orders of magnitude, so the cores are finally the limit. Every frontier model since 2023 is trained and served with a FlashAttention-style kernel public, and the 128k and million-token contexts advertised today would be impossible without it.

Back of the envelope

Beacon at 128k tokens, one layer, forward.

full score matrix, one head          T² × 2 bytes  =  128,000² × 2          ≈  32.8 GB
× 128 heads                                                                 ≈  4.2 TB   per layer   (the H100 has 80 GB)

FlashAttention working set, one tile  (Q tile 128×128 + K tile + V tile + score tile) × 2 bytes  ≈  128 KB  (SRAM has 228 KB)

reduction in resident memory                                                ≈  250,000×
arithmetic, unchanged                 4 × T² × d  =  4 × 128,000² × 128     ≈  8.4 TFLOP per head per layer
$ python code/ch10/flash_memory.py
full score matrix, one head, one layer :     32.8 GB
× 128 heads                          :      4.2 TB per layer
FlashAttention working set, one tile    :    128.0 KB   (fits in 228 KB of SRAM per SM)
reduction factor                        :   250000×
arithmetic per head, unchanged          :     8.39 TFLOP  (the T² work stays; the T² memory goes)

Four terabytes per layer is the size of the thing that must never exist. And 8.4 TFLOP per head per layer, times 128 heads, times 126 layers, is 135 PFLOP of attention arithmetic for one forward pass over one 128k-token sequence: about two minutes of an H100 at peak. That is why long-context training is expensive even with the memory problem solved, and why Chapter 12 looks at attention variants that cut the work itself.

10.5MFU: how much of the machine you actually used

Put it together over a whole run. Chapter 5 gave the arithmetic a training step must do: about 6·N FLOPs per token. Multiply by tokens per second achieved and divide by the cluster's peak, and you have model FLOPs utilisation: the fraction of the time the tensor cores were doing the model's matmuls rather than waiting for bytes, for other GPUs, or for the CPU.

MFU  =  (6 × N × tokens per second)  /  (number of GPUs × peak FLOP/s per GPU)

Llama 3 405B, 16,000 H100s:   reported 38–43% MFU        public
                              ⇒ 16,000 × 989 TFLOP/s × 0.40  ≈  6.3 × 10¹⁸ useful FLOP/s
                              ⇒ 3.8 × 10²⁵ FLOPs  ÷  6.3 × 10¹⁸   ≈  70 days of pure training time

Forty percent is a good number. The other sixty percent goes to attention's non-matmul work, to elementwise operations that were not fully fused, to communication between GPUs (Chapter 11), to pipeline gaps, and to the occasional restart (Chapter 13). Small models on one GPU reach 50–60%; frontier runs across thousands of GPUs rarely exceed 45% public across the reports that state it. When someone quotes a cluster's peak in exaFLOP/s, divide by two and a half before planning anything.

10.6Beacon's numbers

QuantityValueEvidence
Training hardware, Llama 3 405B16,000 H100 GPUs, 8 per node, NVLink within, RoCE Ethernet betweenpublic
Reported MFU38–43%public
Attention kernelFlashAttention-style, fusedpublic for open models; universal inferred
Weights resident in HBM at serving, bf16810 GB across ≥ 5 GPUs, in practice a full 8-GPU nodecomputed, Chapter 4
Closed frontier labsCluster sizes reported in the tens of thousands of GPUs; MFU, interconnect, and kernels not disclosedunknown
Break it

Run the model on a CPU. A server CPU manages a few TFLOP/s on matmul and a few hundred GB/s of bandwidth: both a hundred times worse than an H100, and the ratio between them is similar, so the same operations are bound the same way, only slower. Beacon's decode would produce a token every few seconds per conversation. Training would take centuries. GPUs are not a convenience; the arithmetic budget of Chapter 8 is unreachable without them.

Keep the GPUs but halve HBM bandwidth. Peak FLOP/s unchanged, ridge point doubles to 590 FLOP/B. Prefill matmuls at 4,600 FLOP/B barely notice. Decode, norms, softmax, and unfused attention all take twice as long. Serving throughput halves; training MFU falls by whatever fraction of step time was memory-bound. Bandwidth, not FLOP/s, is the spec that decides most of what a GPU can do for a language model, which is why HBM capacity and speed are what each generation advertises.

Materialise the attention matrix. Context length is capped by HBM: on an 80 GB GPU with 32 heads, a single layer's scores in bf16 exceed memory at about 11k tokens. Every context longer than that is impossible, and shorter ones spend most of their attention time moving the matrix through the door three times. This is the world before 2022.

Run every elementwise operation as its own kernel. Each RMSNorm, activation, residual add, and rotary rotation becomes a separate read-and-write of the full activation tensor. For a block that has roughly ten such operations around two matmuls, the elementwise traffic exceeds the matmul traffic and the block runs at a fraction of peak. Fusing them into the matmul epilogues is the difference between 20% and 40% MFU.

Put the batch's sequences on different nodes and attend across them. Attention needs every key and value for a sequence on the GPU computing that sequence's queries. Split a sequence across the 50 GB/s link and each layer's attention waits for keys to arrive at one-seventieth of HBM speed. Chapter 11's parallelism schemes are designed precisely so that the heavy traffic stays inside the node.

Rebuild the model

Say it back. A GPU is thousands of simple cores running the same instruction on different data, with tensor cores that multiply small matrix blocks in one step; it is fast at matmul and at nothing else. Its data sits in a hierarchy: registers and SRAM on the chip, tiny and fast; HBM beside it, large and slower; other GPUs over NVLink; other nodes over a network seventy times slower than HBM. Arithmetic intensity, FLOPs per byte moved from HBM, decides whether an operation is limited by the cores or by the door, and the H100's ridge is about 300 FLOP per byte. Big-batch matmuls clear it; decode matmuls, norms, softmax, and attention as literally written do not. Attention's problem is that its T² score matrix must be written and read three times; FlashAttention removes the matrix by computing the softmax incrementally with a running max, sum, and output, folding in one tile of keys and values at a time inside SRAM, leaving the arithmetic quadratic but the memory linear. Over a run, the fraction of peak the model's matmuls actually achieve is the MFU, about 40% for the best public frontier runs. Beacon trained on 16,000 H100s at that utilisation, and its 128k-token attention would need four terabytes per layer if anyone tried to store it.

count FLOPs2·M·K·N, 4·T²·d count bytesHBM reads + writes intensityvs ridge ≈ 300 bound by?cores or the door fix the shapebatch · fuse · tile (Flash) arithmetic is cheap, bytes are expensive; keep the byte on the counter and use it many times
What is the whole chapter in one line? Count the FLOPs, count the bytes, compare their ratio to the machine's ridge, and reshape the computation until the cores are the limit. FlashAttention is that procedure applied to Chapter 3.
Exercises
  1. By hand. Redo the online softmax for the four scores [1.0, 3.0, 2.0, 3.5] with values [1, 2, 3, 4] (scalars), processing one score at a time. Track m, ℓ, o after each step, including the rescale factor. Confirm o/ℓ at the end equals the ordinary softmax-weighted average computed directly.
  2. Calculation. A B200 has about 2.25 PFLOP/s dense bf16 and 8 TB/s. Compute its ridge point. For the decode matmul [M, 8192] × [8192, 28672], what batch size M makes it compute-bound on the B200, and on the H100? What does that imply about the minimum batch a serving system must gather to use the newer GPU fully?
  3. Code. Extend roofline.py with a function that takes a model's (C, H, L, V) and a batch of M tokens and reports total bytes and FLOPs for one forward pass through all matmuls (attention projections, MLP, unembedding), then the overall intensity. Run it for Llama 3 8B at M = 1, 64, 4096. At which M does the whole forward pass cross the H100's ridge?
Further reading