Many slow cores instead of few fast ones
A CPU has tens of cores, deep caches and branch prediction.
A GPU has thousands of cores, shallow caches and no speculation.
A GPU is built for one instruction applied to enormous amounts of data, and every architectural choice follows from that.
Threads are scheduled in groups of 32 that must follow the same instruction. A branch where half the warp goes each way runs both sides in sequence, which is why divergent code is slow.
Latency is hidden rather than avoided: when a warp waits for memory, another warp runs, so occupancy matters more than any single thread's speed.
The roofline
Arithmetic intensity decides which limit applies, and most operations in a neural network are on the wrong side of it.
An activation function reads and writes more bytes than it does arithmetic, so it runs at a tiny fraction of peak no matter how it is written.
That is the whole argument for kernel fusion: doing several elementwise operations in one pass over memory turns several bandwidth-bound kernels into one.
The memory hierarchy is the program
Data moves from registers to shared memory to L2 to HBM, slower at every step.
Shared memory is roughly an order of magnitude faster than the main memory on the card, and it is measured in kilobytes per block.
Tiling exists to exploit exactly that: load a block of the matrices into shared memory, reuse it many times, then move on.
FlashAttention is the same idea applied to attention, and it is why it is faster despite doing more arithmetic.
One kernel launch, in order
- The host copies data to device memory, if it is not already there.
- The host launches a grid of thread blocks.
- Each block is assigned to a streaming multiprocessor and runs its warps.
- Blocks cooperate through shared memory inside a block, and not across blocks.
- Results are written to device memory, and the host synchronises when it needs them.
That overhead is why a model made of many tiny operations runs poorly, and why CUDA graphs, which replay a whole recorded sequence of launches, help small-batch inference so much.
What to check when it is slow
A card reporting 100 percent utilisation may be doing bandwidth-bound work at 5 percent of peak FLOPs, so the useful number is achieved FLOPs against the roofline.
Tensor cores need aligned shapes.
Dimensions that are multiples of 8 or 16 let the matrix units engage, and a hidden size of 1,000 rather than 1,024 can cost a large fraction of the throughput.
Host and device run asynchronously, so timing a kernel without an explicit synchronisation measures the launch rather than the work, which is the most common benchmarking mistake.