A GPU is an enormous calculator fed by a narrow pipe. This chapter shows how the chip works, how to predict a kernel's speed before writing it, and how to write the kernels (in CUDA, Triton, and NumPy simulations) that keep the calculator fed.
Chapter 11 · Section 11.1 · 50 min

How a GPU works

Here is a puzzle to start the chapter. An NVIDIA H100 can perform about 101510^{15} bfloat16 floating-point operations per second. Ask it to add two vectors of a billion float32 numbers each, a billion additions in total. How long does that take?

If you divide a billion additions by the GPU's 67 trillion float32 operations per second, you get 15 microseconds. The real answer is about 3.6 milliseconds, 240 times longer. The additions are not the work. The work is moving the data. The two inputs are 4 GB each and the output is another 4 GB, and the GPU's memory delivers about 3.35 TB per second, so just moving 12 GB takes 12/3350≈3.612 / 3350 \approx 3.6 ms. During that time the arithmetic units are idle more than 99 percent of the time.

That one calculation contains most of this chapter. A GPU is two machines bolted together, an enormous calculator and a comparatively narrow pipe that feeds it. Whether your kernel runs at 1 percent or 80 percent of the chip's peak depends almost entirely on how you use the pipe. FlashAttention (§12.3), fused kernels, the batching that makes inference affordable, and the model FLOPs utilization that every pretraining team tracks (§6.7) are all consequences of it.

This section builds the mental model. We look at how the chip is organized (streaming multiprocessors, warps, and the SIMT execution model), walk the memory hierarchy with real numbers for an H100, see how a GPU hides the long wait for memory by juggling thousands of threads, and meet the tensor cores that supply almost all of its arithmetic. The next section turns this into a quantitative tool, the roofline model.

Two ways to spend a billion transistors

A CPU core is built to make one stream of instructions finish as soon as possible. Most of its silicon goes to things that reduce the latency of that one stream. Large caches keep data close, branch predictors guess which way an if will go, and out-of-order execution finds independent instructions to run while an earlier one waits. A modern server CPU has a few dozen such cores.

A GPU makes the opposite bet. It assumes you have millions of independent pieces of work, such as the elements of a tensor, and it only cares how many of them finish per second. So it spends its silicon on arithmetic units and on the storage needed to keep many threads alive at once, and it spends very little on making any single thread fast. When a thread has to wait for memory, the GPU does not try to predict or reorder anything. It switches to another thread that is ready.

In plain words, a CPU is a latency machine and a GPU is a throughput machine. The same program can be 50 times faster on one or the other depending on whether it has enough independent work to fill the GPU.

The chip, from the outside in

An H100 in its SXM form (the version on server boards) is organized as a set of 132 identical streaming multiprocessors, or SMs. The SM is the unit that actually runs your code. Everything you launch is split into chunks of work called thread blocks, and each block runs entirely on one SM. The full GH100 die has 144 SMs, and the shipping product enables 132 of them, which improves manufacturing yield.

Each SM is divided into four partitions (NVIDIA calls them processing blocks). A partition has its own warp scheduler, which can issue one instruction per clock cycle, 32 lanes of float32 arithmetic, one tensor core, a slice of the register file, and units for loads, stores, and special functions such as exp. The four partitions share 256 KB of fast on-chip memory that serves as both the L1 cache and the programmer-managed shared memory. Outside the SMs sit a 50 MB L2 cache shared by the whole chip and 80 GB of HBM3, the stacked DRAM that NVIDIA calls GPU memory.

per SM (H100 SXM)amountacross 132 SMs
partitions, each with one warp scheduler4528 schedulers
float32 lanes12816,896
tensor cores4528
register file64K 32-bit registers, 256 KBabout 33 MiB
L1 plus shared memory256 KB, up to 228 KB of it as shared memoryabout 29 MiB of shared memory
resident threads, at most2,048 (64 warps)270,336

Look at the last row. One H100 can hold over a quarter of a million threads resident at the same time, each with its own registers. This is the most important number in the table, and the rest of the section explains why a GPU needs it.

Warps and the SIMT model

When you write a CUDA kernel, you write the program for one thread (§11.3 shows the syntax). The hardware never schedules threads one at a time, though. It groups 32 consecutive threads of a block into a warp, and the warp is the unit of execution. Each cycle, a partition's scheduler picks one warp that is ready and issues one instruction for all 32 of its threads, which each apply it to their own registers. NVIDIA calls this single instruction, multiple threads (SIMT).

SIMT looks like SIMD (the vector instructions of a CPU), but the programming model is different. In SIMD you write vector code explicitly. In SIMT you write scalar code, and the hardware discovers the vector parallelism by running 32 copies in lockstep. The catch is what happens when those 32 copies disagree.

Suppose a kernel contains if (x > 0) { A } else { B } and, within one warp, some threads have positive x and others do not. The warp cannot issue A and B at the same time, because it issues one instruction at a time. So it runs A with the "else" threads masked off, then B with the "if" threads masked off. This is warp divergence, and its cost is simple to state. A warp pays for every path that at least one of its threads takes.

In plain words, if any lane in a warp needs a branch, the whole warp waits while that branch runs.

Since the Volta generation, threads in a warp can be scheduled more independently, so diverged threads can interleave and even synchronize with each other. That fixes some deadlocks, but it does not make the two paths run simultaneously, so the cost model above still holds. The cell simulates it. The same data either arrives in random order, so that nearly every warp sees both signs, or sorted, so that each warp sees only one.

Python · cell 1

With random data almost every warp diverges and pays 30 cycles, twice the 15 it would pay with no divergence. Sorting the same data makes the cost almost ideal, with only the one warp that straddles zero paying for both paths. Real kernels avoid divergence by making branch conditions depend on the warp index rather than the thread index where possible, and by using arithmetic (max, where-style selects) instead of branches for short paths.

Warm-up

The cost of divergence

Implement divergent_cycles(path, costs). path is an integer array with one entry per thread, giving which code path (an index into costs) that thread takes. Threads are grouped into warps of 32 consecutive threads, and the last warp may be partial. Each warp pays the sum of costs[p] over the distinct paths p taken by at least one of its threads. Return the total over all warps.

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

The memory hierarchy, with real numbers

Every byte a kernel uses lives somewhere in a hierarchy of memories, and each level trades size for speed. The numbers below are for an H100 SXM. Capacities and HBM bandwidth come from NVIDIA's datasheet and tuning guide. The latencies are empirical measurements, taken with pointer-chasing microbenchmarks on an H800 (a Hopper GPU with the same SM design) by Luo et al. (2024), and they are best-case figures for an idle chip.

levelcapacitylatency, measuredbandwidthmanaged by
registers256 KB per SM, 64K registersan operand of the instructionseveral operands per lane per cyclethe compiler
shared memoryup to 228 KB per SMabout 29 cycles128 bytes per cycle per SM, about 31 TB/s in totalthe programmer
L1 cachethe rest of the 256 KBabout 41 cyclessame storage as shared memorythe hardware
L2 cache50 MB, shared by all SMsabout 263 cyclesseveral TB/sthe hardware
HBM380 GBabout 479 cycles3.35 TB/sthe programmer, with cudaMalloc
another GPU over NVLink 4its 80 GBmicroseconds450 GB/s each waythe programmer, through NCCL
host memory over PCIe Gen5the CPU's RAMmicroseconds64 GB/s each waythe programmer

The last two rows leave the chip. NVLink joins the GPUs of one server, and its bandwidth is quoted as a total over both directions, 600 GB/s for an A100, 900 GB/s for an H100, and 1.8 TB/s for a B200, so half of each in either direction. Even the fastest is several times slower than HBM, which is why distributed training works so hard to overlap communication with compute.

Three things in this table are worth committing to memory.

The bandwidth gap. Shared memory delivers about nine times the bandwidth of HBM. The total comes from 32 banks, each 4 bytes wide, on every SM, so 32×4=12832 \times 4 = 128 bytes per cycle per SM, times 132 SMs, times a clock of about 1.83 GHz, gives 128×132×1.83×109≈3.1×1013128 \times 132 \times 1.83\times10^{9} \approx 3.1\times 10^{13} bytes per second. (Banks matter for performance too, and §11.3 counts bank conflicts.) Registers are faster still.

The pyramid is upside down near the top. On a CPU, the register file is tiny and the caches are big. On a GPU, each SM's register file (256 KB) is as large as its L1 and shared memory combined, because it has to hold the private state of up to 2,048 resident threads. Kernels that compute a lot per byte, like matrix multiplication, keep their working set in registers.

Latency is long, and it is measured in cycles. A load from HBM takes about 479 cycles before the data arrives even on an idle chip, and more under load. At about 1.83 GHz that is about 260 nanoseconds, enough time for one SM's tensor cores to perform about two million floating-point operations.

The cell below turns the table into the numbers that drive kernel design.

Python · cell 2

The first line is the whole problem in two numbers. Each cycle, an SM can perform 4,096 bfloat16 FLOPs on its tensor cores, but it receives only about 14 bytes, seven bfloat16 numbers, from HBM. A kernel that does not reuse each number it loads hundreds of times cannot possibly keep the tensor cores busy. That ratio, 295 FLOPs per byte, is the ridge point of the H100, and §11.2 is built around it.

Hiding latency with many warps

A warp that issues a load from HBM cannot use the result for about 500 cycles. If that warp were the only one on its partition, the partition would sit idle for those cycles. The GPU's answer is to keep many warps resident and to switch between them for free. Each warp's registers stay in the register file the whole time, so a "context switch" is just the scheduler choosing a different warp in the next cycle. While some warps wait for memory, others compute.

How many warps are enough? Suppose each warp alternates between issuing one load, waiting ℓ\ell cycles for it, and then issuing cc instructions that use the data. The scheduler can issue one instruction per cycle. With WW warps, the partition has W(c+1)W(c + 1) instructions to issue in every window of c+1+ℓc + 1 + \ell cycles, so the fraction of cycles in which it issues anything is about

u≈min⁡ ⁣(1,W (c+1)c+1+ℓ),so full utilization needsW≥c+1+ℓc+1.u \approx \min\!\left(1, \frac{W\,(c + 1)}{c + 1 + \ell}\right), \qquad\text{so full utilization needs}\quad W \ge \frac{c + 1 + \ell}{c + 1}.

In plain words, you need enough warps that their combined work covers one memory latency. The longer the latency and the less work each warp does per load, the more warps you need.

Try it in the widget. With a latency of 480 cycles and only 8 instructions per load, a partition needs about 55 warps to stay busy, far more than the 16 it can hold (64 per SM, 4 partitions). That is why memory-bound kernels never reach the issue rate, and why their speed is set by bandwidth instead.

How many warps does it take to hide memory latency?
One scheduler issues one instruction per cycle from whichever warp is ready. Each warp issues a load, waits for it, then issues a few instructions that use the data.
Loading interactive figure…

The same reasoning, applied to bytes instead of instructions, is Little's law from queueing theory. The amount of data in flight must equal the bandwidth times the latency. To sustain 3.35 TB/s with a latency of about 260 ns, the chip must have about 3.35×1012×2.6×10−7≈0.93.35\times10^{12} \times 2.6\times10^{-7} \approx 0.9 MB of loads outstanding at every moment, about 6.6 KB per SM, or about 52 warp-wide loads of 128 bytes each. Under load the latency grows, so in practice it is more. There are three ways to get there.

  • More warps. Raise occupancy, the number of resident warps per SM divided by the maximum (64 on an H100).
  • More independent loads per warp. Each thread issues several loads before using any of them (instruction-level parallelism), or uses 16-byte vector loads, which move 512 bytes per warp instruction.
  • Asynchronous copies. Since Ampere, cp.async copies global memory into shared memory without tying up registers, and Hopper's Tensor Memory Accelerator (TMA) moves whole tiles with one instruction. A kernel can then load the next tile while computing on the current one.

Occupancy and what limits it

How many warps fit on an SM depends on what each block asks for. An SM has a fixed budget of resident threads (2,048), registers (65,536), shared memory (228 KB on an H100), and blocks (32), and a block is only scheduled when all of its resources fit. Whichever budget runs out first limits occupancy. Registers are allocated per warp in chunks of 256, and on Ampere and Hopper each block also reserves 1 KB of shared memory for the system.

A worked example. A kernel with 256 threads per block (8 warps) that uses 64 registers per thread needs 64×32=2,04864 \times 32 = 2{,}048 registers per warp, so 16,384 per block, and 65,536/16,384=465{,}536/16{,}384 = 4 blocks fit, 32 warps, 50 percent occupancy. Reduce register use to 32 per thread and 8 blocks fit, 64 warps, 100 percent. The widget lets you try other kernels.

Registers have a hard ceiling too. A thread can address at most 255 of them, and if a kernel needs more, or if you cap the count to raise occupancy (with __launch_bounds__ or the -maxrregcount compiler flag), the compiler spills the excess to local memory. Despite the name, local memory lives in HBM, cached in L1 and L2, so every spilled value turns a free register access into a memory access. The compiler's -Xptxas -v report and Nsight Compute both show spills, and a kernel that suddenly slows down after a small change often has one.

What limits how many warps can live on one SM?
A block is scheduled only when its threads, registers, and shared memory all fit. Whichever budget runs out first sets the occupancy.
Loading interactive figure…
Interview

An occupancy calculator

Implement occupancy(threads_per_block, regs_per_thread, smem_per_block) for an H100, using these simplified rules. The SM holds at most 64 warps, 32 blocks, 65,536 registers, and 228 KB (228×1024228 \times 1024 bytes) of shared memory. A block has ⌈threads/32⌉\lceil \text{threads}/32 \rceil warps. Registers are allocated per warp, rounded up to a multiple of 256. Each block's shared memory is its request plus 1,024 reserved bytes, rounded up to a multiple of 128. Return a dictionary with "blocks" (resident blocks per SM), "warps" (resident warps), "occupancy" (warps divided by 64), and "limiter", the first of "warps", "registers", "shared", "blocks" whose limit equals the final block count.

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

Tensor cores, where the FLOPs are

The float32 lanes are not where an H100's headline number comes from. Each SM's four tensor cores each compute a small matrix multiply-accumulate, D=AB+C\mathbf{D} = \mathbf{A}\mathbf{B} + \mathbf{C}, where the four matrices are tiles a few elements on a side, as a single operation. On an H100 each tensor core completes 512 bfloat16 multiply-adds, 1,024 FLOPs, per cycle. Across the chip that is

132 SMs×4 tensor cores×1,024 FLOPs per cycle×1.83×109 cycles per second≈989 TFLOP/s,132 \text{ SMs} \times 4 \text{ tensor cores} \times 1{,}024 \text{ FLOPs per cycle} \times 1.83\times10^{9} \text{ cycles per second} \approx 989 \text{ TFLOP/s},

against 67 TFLOP/s from all 16,896 float32 lanes. The tensor cores supply about 15 times as much arithmetic as everything else combined, and they only do one thing, multiply matrices. (NVIDIA does not publish the clock behind its tensor-core peak, so we inferred 1.83 GHz from it. The float32 and FP64 peaks correspond instead to the maximum boost clock of 1.98 GHz, 16,896×2×1.98×109≈6716{,}896 \times 2 \times 1.98\times10^{9} \approx 67 TFLOP/s, so treat every per-cycle figure in this section as approximate to within about 8 percent. The A100's 312 TFLOP/s checks the same way, 108×4×512×1.41×109108 \times 4 \times 512 \times 1.41\times10^{9}, with half the per-SM rate and its 1.41 GHz boost clock.)

The rate depends on the number format, roughly doubling each time the operands halve in size. These are dense peaks from NVIDIA's datasheets. The datasheets print numbers twice as large for most rows, which assume 2:4 structured sparsity (two zeros in every four weights), and you should halve those for ordinary dense work. Number formats themselves are the subject of §8.5. The B200 column is one GPU of an HGX B200 server (eight GPUs at about 1,000 W each). Blackwell parts run at other power limits in other systems, with somewhat different peaks.

formatA100 SXM, dense TFLOP/sH100 SXM, dense TFLOP/sB200, dense TFLOP/s
FP64, ordinary lanes9.73437
FP64, tensor cores19.56737
FP32, ordinary lanes19.56775
TF32, tensor cores1564951,125
BF16 or FP16, tensor cores3129892,250
FP8, tensor coresnot supported1,9794,500
FP4, tensor coresnot supportednot supported9,000
INT8, tensor cores (TOPS)6241,9794,500
HBM bandwidth, TB/s2.0 (80 GB)3.358.0

Read the last row against the others. From A100 to B200 the 16-bit tensor-core rate grew about sevenfold while bandwidth grew about fourfold, so each generation needs more FLOPs per byte to stay busy, a trend §11.2 makes precise.

Two consequences matter for machine learning. First, anything that is not a matrix multiplication runs on hardware that is 15 times slower, so the non-matmul parts of a model can take a surprising share of the time unless they are cheap in FLOPs and fused (§11.7). Second, special functions are slower still. The H100 has 16 special-function units per SM, which evaluate exp and friends at about 3.9 trillion per second in total, about 250 times less than the matmul rate. Shah et al. (2024) point out that this is why the exponentials in attention's softmax become a real cost in FlashAttention-3 (§12.4).

Why sizes should be multiples of 64

A tensor-core kernel does not compute C=AB\mathbf{C} = \mathbf{A}\mathbf{B} element by element. It divides C\mathbf{C} into tiles, for example 128×128128\times128, gives each tile to one thread block, and loads operands in 16-byte vectors. Matrix shapes that do not fit this structure waste work in three ways.

Alignment. If a bfloat16 matrix has a row length that is not a multiple of 8 elements (16 bytes), rows do not start on 16-byte boundaries, and the fastest kernels, which assume aligned vector loads, cannot be used. NVIDIA's performance guide recommends dimensions that are multiples of 8 for 16-bit formats, and larger multiples help further.

Tile quantization. A dimension of 129 needs two tiles of 128, and the second does almost no useful work but costs as much as the first.

Wave quantization. Tiles are spread over the 132 SMs in waves. A matrix product with 132 tiles takes one wave, and one with 133 tiles takes two, the second wave using one SM while 131 sit idle.

This is why Karpathy padded GPT-2's vocabulary from 50,257 to 50,304 tokens (a multiple of 128) when reproducing it, and measured a training step falling from about 96 to 93 milliseconds, a few percent for free. The 47 extra rows of the embedding table are never produced by the tokenizer (§2.5).

Python · cell 3

The 1536×14081536\times1408 product has exactly 132 tiles and runs in one perfect wave, while 1536×15361536\times1536, which is only 9 percent more work, needs 144 tiles and therefore two waves, wasting almost half the GPU. The 129-row product shows tile and wave quantization at once, with half its tiles nearly empty and half the SMs idle. Libraries soften this with smaller tiles for awkward shapes and with schemes such as split-K and stream-K that divide the long inner dimension, but the effect is real and shows up as a sawtooth when you plot matmul speed against size.

Core

Tiles and waves

Implement gemm_waves(M, N, tile_m, tile_n, n_sm) returning a tuple (tiles, waves, efficiency). Each output tile of size tile_m by tile_n occupies one SM for one wave, tiles at the edges count as full tiles, and efficiency is the fraction of SM-tile slots, waves * n_sm * tile_m * tile_n, that compute real output elements, M * N.

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

Host, device, and the cost of launching work

The CPU (the host) and the GPU (the device) have separate memories joined by PCIe, which on an H100 moves about 64 GB/s in each direction, 50 times less than HBM. So tensors should be moved to the GPU once and stay there, and data loading should overlap with compute (§0.9).

The host also drives everything. Each operation in PyTorch's eager mode is a kernel launch, a request the CPU places in a queue (a CUDA stream) for the GPU to run. Launches are asynchronous. The CPU queues work and moves on, and it waits only at a synchronization point such as .item(), .cpu(), or printing a GPU tensor. Each launch costs the CPU a few microseconds, and PyTorch's own dispatch adds more. A kernel that runs for less time than that leaves the GPU idle between kernels, a third regime besides compute-bound and memory-bound that Horace He calls overhead-bound. Fusing kernels and capturing CUDA graphs are the cures (§11.7).

In PyTorch

You can see the two machines on any computer. The block below queries the GPU if one is present, then measures the same contrast on the CPU, a bandwidth-bound elementwise add against a compute-bound matrix product. The CPU's numbers are much smaller than an H100's, but their ratio tells the same story.

🔥 In PyTorchfor a machine with PyTorch

Read more

Watch next

🎬 How do Graphics Cards Work? Exploring GPU Architecture · Branch EducationA beautifully animated tour of a GPU chip, its many cores, and its memory, as a visual warm-up.
🎬 Lecture 4 Compute and Memory Basics · GPU MODEThomas Viehmann walks through SMs, warps, occupancy, and the memory hierarchy, following the PMPP book.
🎬 Let's reproduce GPT-2 (124M) · Andrej Karpathy · starts at 88:14Karpathy on tensor cores and what TF32 buys a real training step, with the A100's numbers on screen.