cs336 / lessons/06 · gpuslesson 7 / 20

Part II — Systems

GPUs — the hardware you're renting

Lesson 05 quoted two numbers as if they were given to us: a "peak FLOP/s" the runtime estimate divided by, and an "MFU" of ~50% that quietly halved your calendar. Neither was explained — they were placeholders for the machine. The whole budget is denominated in GPU-hours, yet we have not once looked at what a GPU actually is: how it moves bytes, how it does math, and why those two rates are wildly out of balance. This lesson opens the box. The single concept inside — arithmetic intensity, plotted on the roofline — explains where the 50% comes from and which operations in your model are throwing tensor cores away.

The previous step left this broken
Lesson 05 turned a dollar budget into a model size with time ≈ 6ND / (GPUs · peak · MFU), then plugged in "peak ≈ 990 TFLOP/s" and "MFU ≈ 50%" with no justification. But MFU is not a constant of nature — it is the fraction of the chip's compute you manage to keep busy, and that fraction is decided by a tug-of-war between two hardware rates: how fast the GPU can multiply (TFLOP/s) and how fast it can feed itself data (TB/s). Until you know those two rates and how an operation's structure determines which one binds it, you cannot say why MFU is 50% rather than 90%, nor which kernels to fix. You are reasoning about utilization while blind to the machine.
Linear position
Forced by: the budget is GPU-hours and lesson 5's runtime estimate hinges on "peak" and "MFU" — numbers you cannot defend without knowing how the chip moves data and does math.
New idea: the GPU memory hierarchy (HBM ↔ SRAM ↔ registers) and tensor cores; the master concept arithmetic intensity = FLOPs ÷ bytes moved, plotted on the roofline, which classifies every op as memory-bound or compute-bound relative to a ridge point (≈300 FLOP/byte on an H100).
Forces next: most LM ops — norms, softmax, every elementwise step, attention at decode — sit far below the ridge, so naive one-kernel-per-op execution leaves the tensor cores idle while the chip waits on HBM. The fix is to do more math per byte loaded: kernel fusion (lesson 7).
The plan
Five moves. (1) Climb the memory hierarchy with H100 numbers — HBM, SRAM, registers — and see the bandwidth cliff. (2) Put compute next to it: ~1,000 TFLOP/s of tensor-core bf16, and stare at the gap between TFLOP/s and TB/s. (3) Define arithmetic intensity and derive the roofline and its ridge point. (4) Classify the LM op zoo — GEMM is compute-bound and good; norms, softmax, elementwise, attention-decode are memory-bound. (5) Sketch the execution model (grid/block/warp, occupancy), say where MFU<100% comes from, and drag the roofline explorer until an op falls off the cliff.

1 · The memory hierarchy: a bandwidth cliff

A GPU is not one pool of memory; it is a steep pyramid, and the price of moving a byte changes by orders of magnitude as you climb it. Take an NVIDIA H100 (SXM) as the running example — the chip most frontier runs are actually rented on.

levelcapacitybandwidthwho sees it
HBM (global / "VRAM")≈80 GB≈3.3 TB/sthe whole GPU; off-chip, where your weights, activations, and KV cache live
SRAM (L2 + per-SM shared/L1)tens of MB total (~50 MB L2; ~228 KB shared per SM)~20× HBM (tens of TB/s aggregate on-chip)a single thread block (shared mem) or all SMs (L2); on-chip scratchpad
registerstiny (256 KB/SM, a few KB per thread)fastest — effectively free, read every cycleone thread; the operands the ALUs actually consume

Read the table as a cliff. The 80 GB of HBM is where everything you allocated in PyTorch actually sits, and it is the slow tier: 3.3 TB/s sounds enormous until you compare it to the compute rate in section 2. On-chip SRAM is roughly 20× faster per byte but holds only tens of megabytes total — you cannot park a 7B model there, you can only stage a tile of it. Registers are faster still and essentially free, but a thread has only a handful of kilobytes. The entire performance game on a GPU is: get data into SRAM/registers once, do as much math on it as possible, and write back to HBM as little as you can. Every byte you re-read from HBM that you could have kept on-chip is bandwidth you set on fire.

  registers   few KB/thread     fastest          <- ALUs eat from here
      ^
  SRAM        ~228 KB/SM        ~20x HBM BW       <- staging tile, shared within a block
  (L1/shared) ~50 MB L2
      ^
  HBM         ~80 GB            ~3.3 TB/s         <- everything lives here; the slow tier
      ^
  host/PCIe   TB                ~tens of GB/s     <- off-GPU; avoid in the hot loop entirely

One concrete consequence to keep in your pocket: an operation that just reads a big tensor from HBM and writes it back — say, adding a bias — is paying full HBM latency for almost no math. Whether that op is fast has nothing to do with how many TFLOP/s the chip has. It is gated entirely by 3.3 TB/s. Hold that thought; it is the whole lesson.

2 · Compute: ~1,000 TFLOP/s, and the gap that is the whole story

Now the other rate. An H100's tensor cores — dedicated matrix-multiply-accumulate units, separate from the general CUDA cores — do roughly 990 TFLOP/s of bf16 (call it ~1 PFLOP/s; with FP8 and sparsity the marketing numbers roughly double). The CUDA cores that handle elementwise math (an add, a GELU, a comparison) are far slower — on the order of tens of TFLOP/s of fp32. So "the GPU's compute" is really "the tensor cores doing matmuls," and everything else is a sideshow that mostly exists to feed them.

Put the two rates side by side and the central tension of GPU programming appears:

compute
≈990×10¹² bf16 FLOP/s (tensor cores)
bandwidth
≈3.3×10¹² bytes/s (HBM)
ratio
≈300 FLOP per byte — the chip can do ~300 math ops in the time it takes to fetch one byte from HBM

That ratio is the crux of Part II. The hardware can perform on the order of 300 floating-point operations in the time it takes to read a single byte from HBM. So unless an operation does ≥300 FLOPs of useful work for every byte it pulls from HBM, the tensor cores spend most of their cycles idle, waiting on memory. The TFLOP/s number on the spec sheet — the one you bragged about in the runtime estimate — is reachable only by operations dense enough to clear that bar. Most operations in a language model are not. The gap between TFLOP/s and TB/s is not a footnote; it is the reason MFU is 50% and not 95%, and the reason every remaining lesson in Part II exists.

3 · Arithmetic intensity and the roofline

To make "does this op clear the bar" precise, define one number per operation. Arithmetic intensity (AI) is the ratio of useful work to data traffic:

arithmetic intensity  =  (FLOPs performed) ÷ (bytes moved to/from HBM)    [FLOP / byte]

It is a property of the algorithm and its data layout, not of the chip. An op with low AI touches a lot of memory per unit of math (bandwidth-hungry); an op with high AI reuses each loaded byte for many operations (compute-hungry). Now overlay the two hardware rates. The time an op takes is bounded below by whichever ceiling is binding:

time  ≥  max( FLOPs ÷ peakCompute ,   bytes ÷ peakBandwidth )

Turn that into achievable throughput as a function of AI and you get the roofline: a curve that rises linearly while bandwidth is the limit, then flattens once compute takes over.

achievable FLOP/s  =  min( peakCompute ,   AI × peakBandwidth )

The two regimes meet at the ridge point — the arithmetic intensity at which a perfectly bandwidth-bound op would exactly saturate compute:

ridge  =  peakCompute ÷ peakBandwidth  ≈  990×10¹² ÷ 3.3×10¹²  ≈  300 FLOP / byte

The verdict is binary and unforgiving. An op whose AI is below ≈300 can never reach peak FLOP/s no matter how good your kernel is — it is memory-bound, capped at AI × 3.3 TB/s, and the tensor cores idle. An op whose AI is above ≈300 is compute-bound — it can in principle saturate the tensor cores, and is the only kind of op that earns the spec-sheet TFLOP/s. The roofline is the single picture that says, for any operation, the best speed it could possibly hit on this chip and which resource you would have to buy more of to go faster.

The ridge moves with precision
The ridge is peakCompute ÷ peakBandwidth, so changing either rate slides it. Switch to FP8 and peakCompute roughly doubles → the ridge climbs to ~600 FLOP/byte, making more ops memory-bound (harder to feed the faster cores). Quantizing weights to fewer bytes cuts the bytes moved for weight-bound ops, raising their AI. "Below the ridge" is always relative to a specific (compute, bandwidth, dtype) configuration — which is why decode-time INT4 (lesson 18) is a bandwidth play, not a compute one.

4 · Classifying the LM op zoo

Now walk the operations a Transformer actually runs and place each on the roofline. The pattern is stark: exactly one family is compute-bound, and it is the one you want to dominate the budget.

operationarithmetic intensityregimewhy
matmul / GEMM (training, large M·N·K)high — scales with the shared dimension, easily >300compute-bound ✓each loaded tile of A and B is reused across many output elements; the math grows as M·N·K while traffic grows as M·K + K·N
LayerNorm / RMSNorm≈2–4 FLOP/bytememory-boundread the tensor, compute a mean/variance, write it back — a handful of FLOPs per element read
softmax≈3–5 FLOP/bytememory-boundmax, subtract, exp, sum, divide — still only a few FLOPs per element, dominated by the read/write
elementwise (GELU, residual add, dropout, scale)≈1–4 FLOP/bytememory-boundone or two FLOPs per element loaded; pure bandwidth
attention at decode (one query token)low — re-reads the whole KV cache per step for a single querymemory-bounda matrix-times-vector: each cached K/V byte is used once, no reuse → bandwidth-bound (the heart of lesson 18)

The lesson of the table: a big training GEMM has arithmetic intensity in the hundreds-to-thousands because a tile of the weight matrix, once in SRAM, is multiplied against many rows of activations — the loaded bytes are reused heavily, so the math piles up while the traffic does not. That is why matmuls are the only ops that hit the tensor-core ceiling, and why you want them to be the overwhelming majority of your FLOPs. Everything else — every norm, every activation, every softmax, and decode-time attention — does only a few FLOPs per byte and is pinned to the 3.3 TB/s wall, the tensor cores sitting idle the whole time.

This is precisely why real training MFU lands at ~40–55% rather than near 100% (lesson 5): even though GEMMs dominate the FLOP count, the memory-bound ops between them consume wall-clock time during which the tensor cores do nothing. The chip is fast at the one thing and starved on everything else, and "everything else" still has to run.

A back-of-envelope you can do in your head
RMSNorm on a bf16 tensor of B·T·d elements reads ~2 bytes and does ~4 FLOPs per element → AI ≈ 2 FLOP/byte. Against the H100 roofline that op tops out at 2 × 3.3 TB/s ≈ 6.6 TFLOP/s — under 1% of the 990 TFLOP/s peak. No kernel, no compiler, no amount of cleverness moves a single-pass RMSNorm above that line; only changing what gets moved (fusing it into a neighbor so the tensor never round-trips HBM) helps. Hold that 1% — it is the motivation for lesson 7.

5 · The execution model, briefly — and where MFU leaks

You do not need the full programming model to reason about the budget, but you need the vocabulary, because the leaks in MFU live here. A GPU runs a kernel (one function) as a grid of thread blocks; each block is scheduled onto one streaming multiprocessor (SM — an H100 has ~132) and runs as groups of 32 threads called warps that execute in lockstep. Threads in a block share that SM's fast SRAM and can cooperate; threads in different blocks cannot. Occupancy is how many warps an SM keeps resident at once — enough resident warps let the scheduler hide memory latency by switching to a ready warp while another waits on HBM. Low occupancy (too few warps, too many registers per thread) means the SM stalls with nothing to run during a memory wait.

grid → block → warp
a kernel launch is a grid of blocks; a block lands on one SM; a warp is 32 threads in lockstep. The unit of scheduling is the warp.
occupancy
resident warps per SM. High occupancy hides HBM latency by overlapping waits with other warps' work; low occupancy stalls.
launch overhead
each kernel has fixed launch cost. Many tiny ops (one per elementwise step) means launch + HBM round-trip dominates the actual math.

That gives the four reasons MFU comes in under 100%, all visible from this lesson:

The depth here is its own track. For the line-level programming model — threads, warps, divergence, the launch — see gpu_kernels · 01 (the execution model); for the memory hierarchy and coalesced access, gpu_kernels · 02; and for the kernel-engineering mindset that turns a roofline into a fast kernel, gpu_kernels · 10 (kernels from first principles). This lesson only needs you to hold the roofline and know which side of the ridge your ops fall on.

6 · Drive the roofline

The plot below is the H100 roofline: the diagonal is the bandwidth ceiling (AI × 3.3 TB/s), the flat top is the compute ceiling (990 TFLOP/s), and they meet at the ridge (~300 FLOP/byte). Pick a preset op or drag the slider to set an arithmetic intensity. The marker lands on the roofline at that AI, and the readout shows the best achievable throughput and what fraction of peak that is. Drag below the ridge and the verdict turns red: memory-bound — you're wasting tensor cores. Drag past it and the op finally reaches the flat ceiling where the spec-sheet TFLOP/s lives. Notice how far left every real LM op except big GEMM sits.

Roofline explorer — where does your op hit the ceiling?
The marker is your op's best possible throughput on an H100 at its arithmetic intensity — no kernel can beat the roofline. Below the ridge (~300 FLOP/byte) the op is capped by 3.3 TB/s of HBM and the tensor cores idle; above it the op is capped by 990 TFLOP/s of compute. The presets are real LM ops: notice all of them except the big GEMM live far down the bandwidth slope.
Arithmetic intensity
Achievable throughput
% of 990 TFLOP/s peak
Ridge point
~300

The widget makes the spine of Part II visible: the great majority of an LM's distinct operations sit on the bandwidth slope at single-digit percentages of peak, and no kernel rewrite can lift a single-pass op off that line. The only way to recover the wasted tensor cores is to change what gets moved — to stop round-tripping each intermediate through HBM. That is fusion.

Failure modes & checklist

Failure modes

  • Reading peak FLOP/s as a promise. Assuming a kernel runs near 990 TFLOP/s because the chip can. Signal: a profiler shows 2% of peak on a norm/softmax — it was always memory-bound, the peak was never reachable.
  • Optimizing the wrong ceiling. Hand-tuning the math of a memory-bound op (fewer FLOPs in a softmax). Signal: no speedup — the op is gated by bytes, not FLOPs; you must cut HBM traffic, not arithmetic.
  • Ignoring the gap between op count and time. Believing GEMMs dominate runtime because they dominate the FLOP count. Signal: the profile is full of tiny memory-bound kernels eating wall-clock between the GEMMs — the source of MFU<100%.
  • Forgetting the ridge moves with dtype. Switching to FP8 and expecting all ops to speed up. Signal: compute-bound GEMMs get faster but memory-bound ops don't — the ridge climbed and they're further below it.
  • Treating decode like prefill. Assuming generation is compute-bound like training. Signal: tiny GPU utilization at decode despite a big model — it's a memory-bound matrix-vector against the KV cache (lesson 18).

Checklist

  • Compute AI first (FLOPs ÷ HBM bytes) and compare to the ~300 ridge before touching a kernel.
  • Below the ridge → cut bytes, not FLOPs: fuse, keep data in SRAM, avoid round-tripping intermediates through HBM.
  • Above the ridge → feed the cores: ensure tiles are big enough and occupancy high enough to actually hit peak.
  • Want matmuls to dominate the budget; treat every memory-bound op between them as MFU you're leaking.
  • Re-derive the ridge for your dtype (peakCompute ÷ peakBandwidth) — it is not a fixed 300.

Checkpoint

Try it
Open the roofline explorer. (1) Click "LayerNorm (AI≈2)" and read off the achievable throughput and % of peak — confirm it is well under 1% and explain in one sentence why no kernel can beat it. (2) Click "big GEMM (AI≈800)" and watch the marker jump to the flat compute ceiling: what fraction of peak now, and which resource binds it? (3) Drag the slider slowly up from 4 and find the AI where the verdict flips from memory-bound to compute-bound — that value is the ridge; confirm it matches 990 ÷ 3.3. (4) By hand: if you switched the chip to FP8 (peakCompute ≈ 1,980 TFLOP/s, same 3.3 TB/s), what is the new ridge, and does softmax become more or less starved? State why in one line.

Where this points next

The roofline just delivered an uncomfortable verdict: the overwhelming majority of an LM's distinct operations — every norm, every activation, every softmax, attention at decode — live far below the ridge and can reach only a few percent of the chip's compute, because they do only a handful of FLOPs per byte they pull from HBM. And the way frameworks run them makes it worse: naive PyTorch launches each op as its own kernel, so every intermediate tensor is written to HBM by one kernel and immediately read back by the next, paying full 3.3 TB/s round-trips for math that takes nanoseconds. The tensor cores starve between every step. The forced fix is to stop moving data we don't need to move — to load a tile once into SRAM and do a whole chain of operations on it before writing back, so many ops share one HBM round-trip. That is kernel fusion, and its headline case is FlashAttention. Next: 07 · Kernels — fusion and FlashAttention.

Takeaway
A GPU is a steep memory pyramid feeding a very fast matrix engine, and the two are badly mismatched. On an H100, HBM holds ≈80 GB at ≈3.3 TB/s; on-chip SRAM is tens of MB at ~20× that bandwidth; registers are tiny and free. The tensor cores do ≈990 TFLOP/s of bf16 — so the chip can do ~300 FLOPs in the time it takes to read one byte from HBM. That ratio is the ridge point of the roofline: an op's arithmetic intensity (FLOPs ÷ bytes moved) decides whether it is memory-bound (AI below ~300, capped at AI × 3.3 TB/s, tensor cores idle) or compute-bound (AI above ~300, able to hit peak). In a Transformer, only big GEMMs clear the ridge; norms, softmax, elementwise, and decode-attention all do a few FLOPs per byte and are pinned to the bandwidth wall — which is exactly why training MFU is ~40–55% and not ~100%, since the chip idles its cores during all the memory-bound work between the matmuls. You cannot lift a single-pass op off the bandwidth line by computing less; you can only move fewer bytes — fuse the ops so intermediates never round-trip HBM. That necessity forces lesson 7.

Interview prompts