Chapter 25 · Hardware

GPUs, the Memory Wall, and Why Inference Is Slow

A GPU generating a token from a 70-billion-parameter model spends over 99% of its time waiting for memory and almost none of it doing arithmetic. This chapter explains why that is true, why it is not a bug, and what every serving technique and architectural fashion of the last three years has been doing about it.

37 sections H100 · Llama 3 · Gemma 2/3/4 Every number derived from published specs 14 drills Reading time ~75 min

25.1 The number that explains everything

Take a 70-billion-parameter language model, quantised to 8 bits so it fits, and put it on an NVIDIA H100 — roughly the most capable inference card widely deployed today. Ask it to generate one token. Here is where the time goes.

Decode time breakdown 99.7% of a decode step is spent reading weights from memory, not computing. Where one token's time goes reading 65.8 GB of weights from HBM — 21.1 ms actual arithmetic: 0.071 ms (0.34% of the step) The GPU spends 99.7% of a decode step waiting on memory. Ceiling: 48 tokens/second for one user. Buying a faster GPU moves the green sliver, not the red bar. That is the whole reason this chapter exists.
Figure 25.4 — To scale. The green sliver is the arithmetic; everything red is waiting for weights to arrive. Tensor cores are not the bottleneck.

The arithmetic takes 0.071 milliseconds. Reading the weights out of memory so that arithmetic can happen takes 21.1 milliseconds. The GPU spends 99.7% of the step waiting, and 295.0 times longer fetching numbers than using them.

This is not a misconfiguration. It is not a bad kernel, an old driver, or a framework overhead you can tune away. It is what the hardware does when you ask it to do this particular job, and every serious technique in LLM serving — batching, paged attention, quantisation, speculative decoding, mixture of experts — is a response to this one fact.

[!] The consequence people find hardest to accept

Buying a GPU with faster tensor cores will not make single-stream generation meaningfully faster. The tensor cores are already idle 99.7% of the time. You would be shortening the part of the step that was never the problem.

Most engineers meet this backwards. They learn that GPUs are fast at matrix multiplication, observe that transformers are mostly matrix multiplication, and conclude that inference should be compute-bound. Then they watch a production endpoint produce forty tokens a second on hardware rated for a thousand trillion operations a second, and have no framework for the gap.

This chapter builds that framework from the hardware up. By the end you should be able to predict, on paper and before deploying anything, roughly how fast a given model will run on a given card, what will break first as you add users, and which of the dozen available optimisations actually addresses your problem rather than someone else's.

[=] What this chapter assumes

That you know what a transformer is and roughly what attention does (chapter 2), and that you have deployed a model behind an API at least once (chapters 6 and 9). No CUDA experience is needed. No hardware background is needed. Every specification used here is published by the vendor, and every derived number is computed rather than quoted.

25.2 The bet a GPU makes

A CPU and a GPU are given the same problem — silicon is finite, memory is slow — and they answer it in opposite ways. Understanding the two answers is most of understanding why inference behaves as it does.

CPU versus GPU design A CPU spends silicon on a few fast threads with large caches; a GPU spends it on 270,336 resident threads with tiny caches. Two ways to spend the same silicon CPU: make one thread fast core +cache core +cache core +cache core +cache Big caches, branch prediction, out-of-order execution. Goal: never wait. A cache miss stalls the core, so most of the die fights misses. ~8-64 threads in flight GPU: have so many threads that waiting stops mattering 270,336 threads resident Tiny caches, no branch prediction, in-order execution. Goal: always have someone ready. A stall is free if another warp can run Neither is better. They are answers to different questions, and the GPU's answer only works if you bring enough work.
Figure 25.1 — The same silicon budget, spent on opposite bets. The GPU's bet only pays if you give it enough parallel work to hide the waiting.

A CPU is built so that one thread runs as fast as possible. When that thread asks for something not in cache, everything stops until it arrives: hundreds of cycles of nothing. So a CPU spends most of its transistor budget on not waiting — large caches, branch prediction, speculative and out-of-order execution. All of it exists to keep a small number of threads from stalling.

A GPU gives up on that entirely. It assumes stalls are unavoidable and makes them irrelevant by having enormously many threads available. When one warp stalls on memory, the scheduler runs another. With 270,336 threads resident on an H100 across 132 streaming multiprocessors, there is essentially always something ready to run.

[i] Latency versus throughput, stated precisely

A CPU minimises latency: the time for one task to finish. A GPU maximises throughput: the total work completed per second, cheerfully accepting that any individual task takes longer.

This is why the GPU's bet only pays off if you bring enough work. One user generating one token at a time is close to the worst case: it is a latency problem handed to a throughput machine. Remember this sentence; most of section D is about manufacturing enough work to make the bet pay.

25.3 Inside a streaming multiprocessor

The H100's 132 streaming multiprocessors are the actual units of execution. Each SM holds up to 2048 resident threads, its own register file, its own shared memory, and two quite different kinds of arithmetic hardware.

CUDA cores do ordinary scalar arithmetic — one multiply-add per cycle per core, on ordinary floating-point numbers. They are general-purpose and, by modern standards, slow: about 67.0 TFLOP/s in FP32 across the whole chip, which sounds enormous until you see the next number.

Tensor cores do one thing: they multiply small matrices and accumulate the result, as a single instruction. Not element by element — a whole tile at once. That specialisation buys about 989.0 TFLOP/s at FP16 — 14.8× the CUDA-core rate, and the reason a single card can claim nearly a thousand trillion operations a second.

[!] The trap hidden in that speedup

Tensor cores made the arithmetic hundreds of times faster. They did nothing whatsoever for the memory feeding it. Every generation that widens this gap makes it harder, not easier, to keep the hardware busy — a point section B makes precisely, because it is the single most counter-intuitive idea in the chapter.

25.4 Warps, and why 32 is everywhere

Threads on a GPU do not execute individually. They are grouped into warps of exactly 32 threads, and a warp is the true unit of scheduling: all 32 threads issue the same instruction in the same cycle, each on its own data. NVIDIA calls this SIMT — single instruction, multiple threads.

This is why 32 appears constantly in GPU programming advice. A block of 100 threads does not run as 100 threads; it runs as four warps, the last of which has 28 threads sitting idle doing nothing while the other four work. Block sizes that are not multiples of 32 waste a fraction of the machine for no reason at all.

Each SM can hold 64 warps resident at once, which across the chip is 8448 warps in flight. The scheduler picks, every cycle, from whichever of them is ready. That pool is the entire latency-hiding mechanism.

25.5 Warp divergence

The lockstep has a cost, and it appears the moment your code branches.

If some threads in a warp take the if and others take the else, the hardware cannot run both at once. It runs the if branch with the else threads disabled, then the else branch with the if threads disabled. Both paths execute, serially. A two-way branch inside a warp costs you 50% of that warp's throughput.

[=] Divergence is about warps, not about branching

A branch where every thread in the warp goes the same way costs nothing at all. The penalty comes from disagreement within a warp, not from the existence of a conditional. Code branching on threadIdx / 32 is free; the same code branching on threadIdx % 32 is not.

25.6 The memory hierarchy

Four levels, each roughly an order of magnitude slower and larger than the one above it. Everything in this chapter is ultimately about the distance between the top and the bottom.

The H100 memory hierarchy, fastest first
LevelSizeScopeRough latency
Registers33.0 MB totalone thread~1 cycle
Shared memory / L129.4 MB totalone block~20-30 cycles
L2 cache50.0 MBwhole chip~200 cycles
HBM80.0 GBwhole chip~400-800 cycles

Two things in that table deserve a second look, because they are unlike a CPU.

First, the register file is 33.0 MB. That is the same order of magnitude as the L2 cache, where on a CPU registers are measured in bytes against megabytes of cache. Each SM's registers are roughly 512.0× a typical CPU core's architectural register budget. This is what it costs to keep 270,336 threads resident: every one of them needs its state kept live, because the whole point is switching between them for free.

Second, per thread that generosity evaporates — about 128.0 bytes each. GPU threads are not small CPU threads. They are extremely numerous and individually impoverished.

25.7 Coalescing: the cheapest own goal

HBM is not read a byte at a time. It is read in sectors of 32 bytes, and this single implementation detail decides whether a memory-bound kernel runs at full speed or at an eighth of it.

When the 32 threads of a warp read 32 consecutive 4-byte values, the hardware coalesces them into 4 transactions and every byte fetched gets used. When those same threads read with a large stride — each landing in a different sector — the hardware issues 32 transactions and discards most of what it fetched. Identical arithmetic, identical thread count, the memory traffic.

[R] Why this shows up as a layout decision

This is the real reason deep learning frameworks care so much about tensor layout, contiguity and .contiguous() calls, and why a transposed matrix can be dramatically slower to consume than the same numbers stored the other way round. The arithmetic is unchanged; the number of bytes dragged across the memory bus is not.

25.8 Occupancy and hiding latency

Now the pieces combine into the mechanism the whole design depends on.

A warp requests data from HBM and stalls for several hundred cycles. On a CPU this would be a disaster. On a GPU the scheduler simply switches to another resident warp, at zero cost, because that warp's registers were never swapped out — this is what the enormous register file bought. If enough warps are resident, the memory latency of any one of them is completely hidden behind the execution of the others.

Occupancy is the fraction of the maximum resident warps you actually achieve. Low occupancy means too few warps to hide stalls, so the SM idles waiting. Occupancy is usually limited by registers per thread or shared memory per block: use too much of either and fewer blocks fit, so fewer warps are resident, so less latency is hidden.

[!] Occupancy is necessary, not sufficient

Latency hiding only works if some warp has arithmetic to do while others wait. If every warp is waiting on memory — which, as section C shows, is exactly the situation during decode — then having more of them waiting changes nothing. 100% occupancy and 3% utilisation is an entirely coherent state, and a common one.

25.9 Compute outran memory

Everything so far describes a machine that is very good at arithmetic provided the data arrives. This section is about the data not arriving.

Between 2016 and 2024, peak compute on NVIDIA's flagship datacentre parts grew by a factor of 106.1. Memory bandwidth over the same period grew by a factor of 10.9. Both improved enormously, which is precisely why the problem is easy to miss: nothing got worse in absolute terms. What changed is the ratio between them, and the ratio is what determines whether your kernel runs at peak.

The memory wall as a ratio The arithmetic intensity required to saturate compute rose from 29 to 281 FLOP per byte across five GPU generations. Operations you must do per byte, just to keep up 100 200 300 29 P100 2016 139 V100 2017 153 A100 2020 295 H100 2022 281 B200 2024 FLOP needed per byte fetched Compute grew 106x in 8 years; the memory feeding it grew 10.9x. So the bar rises: each generation demands more arithmetic per byte before its own tensor cores are busy. Miss the bar, and you are waiting on memory.
Figure 25.2 — Each bar is the arithmetic intensity needed to keep that GPU's maths units busy. It has risen 9.7x, which is the memory wall stated as one number. B200 is the first generation to bend the curve back down, by pairing more compute with much faster HBM.

Read that chart as a demand rather than an achievement. Each bar is the number of arithmetic operations you must perform on every byte you fetch, just to keep that generation's maths units busy. On a P100 you needed 29.0 operations per byte. On an H100 you need 281.0. Fall short and the tensor cores idle, however impressive the number on the datasheet.

[+] The exception worth noticing

B200 is the first generation in this series to bend the curve back down, from 295 to 281, by pairing a large compute increase with a much larger jump in HBM bandwidth. That is what a vendor's response to the memory wall looks like in silicon, and it is a useful corrective to the idea that the trend is a law of nature. It is a design choice, and it can be unchosen — expensively.

25.10 Arithmetic intensity

The quantity those bars measure has a name, and it is the single most useful number in this chapter.

[i] Arithmetic intensity

Arithmetic intensity is the number of floating-point operations a computation performs per byte it moves from memory. It is a property of the algorithm, not of the hardware — the same kernel has the same intensity on every GPU ever made.

Compare the extremes. Adding two vectors reads 8 bytes, does 1 addition, writes 4 bytes: an intensity of about 0.08, hopelessly memory-bound on any hardware. Multiplying two large square matrices does n operations for every element it loads, giving an intensity that grows with the matrix — which is exactly why matrix multiplication is the operation GPUs were optimised for.

Now the question that decides everything: how much arithmetic intensity does your workload have, and how much does the hardware demand?

25.11 The roofline model

Williams, Waterman and Patterson gave us the standard way to answer that in 2009, and it remains the right mental model. Plot achievable performance against arithmetic intensity and you get two regimes joined at a corner.

Roofline model Decode sits far below the ridge point in the memory-bound region; prefill sits on the compute-bound roof. The roofline: where your kernel actually lives 1 10 100 1,000 10 100 1,000 ridge = 591 FLOP/byte memory-bound slope = bandwidth compute-bound decode, batch 1 2 FLOP/byte — 295x below the ridge prefill, 8,192 tokens TFLOP/s achieved arithmetic intensity (FLOP per byte moved), log scale Same weights, same card, opposite sides of the ridge — which is why one configuration cannot serve both phases well.
Figure 25.3 — Prefill and decode are the same weights on the same GPU, on opposite sides of the ridge. No single configuration is right for both, which is why serving systems separate them.

On the left, performance is limited by bandwidth: you are moving bytes as fast as the memory system allows and the arithmetic units are partly idle. The slope of that line is the memory bandwidth. On the right, performance is capped by the arithmetic units themselves — the flat roof.

The corner between them is the ridge point: the arithmetic intensity at which the two limits are equal. For an H100 at FP8 it sits at 590.7 FLOP per byte. Below it you are memory-bound; above it you are compute-bound. That one number tells you which half of the machine you are actually using.

[=] How to use this in practice

Compute your kernel's intensity: total FLOPs divided by total bytes moved. Compare it to the ridge. If you are below it, faster tensor cores will not help you and neither will lower-precision maths — only moving fewer bytes will. If you are above it, the reverse. Most engineering effort is wasted by optimising the side you are not on.

25.12 Tensor cores move the ridge right

Here is the conclusion people resist, and it follows directly from the definition.

The ridge point is peak compute divided by peak bandwidth. Make the compute faster without touching the memory system, and the ridge moves right: you now need more arithmetic per byte before the hardware is saturated. On the H100 the ridge sits at 20.0 FLOP/byte for ordinary FP32 on CUDA cores, 295.3 for FP16 tensor cores, and 590.7 for FP8.

[!] Faster hardware makes saturation harder

Each faster mode is easier to be memory-bound in. The workload has not changed; the bar it must clear has risen. This is why "we upgraded the GPUs and throughput barely moved" is such a common and such a predictable experience.

25.13 What a forward pass actually moves

Enough theory. Take Llama 3 70B, quantised to 8 bits, and ask what physically happens when it produces one token.

Every weight in the model participates. There is no way to produce a token without consulting all 70.6 billion parameters, so all 65.8 GB of them must travel from HBM into the SMs. At 3.35 TB/s that trip takes 21.1 ms and cannot be made faster by any amount of compute.

The arithmetic performed on those weights is about two operations per parameter — one multiply, one add — so roughly 141.0 GFLOP. At the FP8 tensor-core rate that takes 0.071 ms.

One decode step, 70B at 8-bit on one H100
QuantityValue
Bytes read from HBM65.8 GB
Arithmetic performed141.0 GFLOP
Arithmetic intensity2.0 FLOP/byte
Ridge point of the hardware590.7 FLOP/byte
Time to fetch21.1 ms
Time to compute0.071 ms
Ceiling47.5 tokens/second

An intensity of 2.0 against a ridge of 590.7 puts this workload 295.0× into the memory-bound region. Not slightly below the ridge. Not marginally misconfigured. Two and a half orders of magnitude away from the regime the hardware was designed for.

[i] Why the intensity is exactly 2

Two FLOPs per parameter, one byte per parameter at 8-bit precision. The ratio is structural: it does not depend on model size, layer count or hidden dimension. Every dense transformer generating one token at a time has an arithmetic intensity of about 2 at 8-bit, or about 1 at 16-bit. You cannot engineer your way out of it, because it is arithmetic, not implementation.

25.14 Prefill and decode

Generation has two phases with completely different characters, and conflating them is the most common source of confused performance analysis.

Prefill processes your prompt. Every token of it is available at once, so they go through the model together as a matrix, not a vector. The weights are still read once — but now they serve hundreds or thousands of tokens.

Decode generates the response, one token at a time, each depending on the last. There is no way to parallelise across tokens you have not generated yet. The weights are read once per token.

Prefill intensity rises with prompt length; decode's never does
PhaseTokens at once IntensityRegime
Decode12.0memory-bound
Prefill128256.0memory-bound
Prefill5121024.0compute-bound
Prefill2,0484096.0compute-bound
Prefill8,19216384.0compute-bound

The same weights, on the same card, in the same request, cross the ridge somewhere around a prompt of 512 tokens. Prefill is a compute problem. Decode is a memory problem. They want different batch sizes, different parallelism strategies and different hardware, and section D is largely the story of serving systems slowly accepting that.

25.15 Why decode is memory-bound — the short version

If you remember one paragraph from this chapter, this is it.

[+] The whole argument in four lines

Producing one token requires reading every weight. Reading every weight of a 70B model at 8-bit means moving 65.8 GB. At 3.35 TB/s that takes 21.1 ms. The arithmetic done on those bytes takes 0.071 ms. Therefore the GPU waits 99.7% of the time, and one user can never exceed about 47.5 tokens per second no matter what else you change.

25.16 The KV cache

Weights are not the only thing decode must read. Attention needs the keys and values of every previous token, and recomputing them each step would be quadratic madness, so they are cached. That cache is the second great consumer of memory, and unlike the weights it grows.

For Llama 3 70B the cache costs 320.0 KB per token. That figure is already the product of a memory-saving design: with ordinary multi-head attention it would be 2560.0 KB, and grouped-query attention — sharing each key-value head across several query heads — cuts it by 8.0×.

KV cache for a single conversation, Llama 3 70B
Context lengthKV cache
8,192 tokens2.5 GB
131,072 tokens40.0 GB

A single 128k-token conversation needs 40.0 GB of KV cache — a substantial fraction of an entire 80 GB card, for one user. This is why long context is expensive in a way that has nothing to do with the model being large and everything to do with memory the model never had at training time.

25.17 What actually fits on one card

Now put the two consumers together, and a widely-believed piece of folklore falls over.

Llama 3 70B at 16-bit precision is 131.5 GB of weights. An H100 has 80.0 GB of HBM. The model does not fit. It overflows by 51.5 GB — more than half a card — before a single byte of KV cache, and needs 2 cards minimum.

[!] "70B runs on an 80GB GPU" is false at full precision

It runs on one card quantised. At 8-bit the weights are 65.8 GB, leaving 14.2 GB for everything else. Quantisation is not a performance optimisation here; it is the difference between one card and two.

With 14.2 GB of headroom, KV cache at 320.0 KB per token allows about 5 simultaneous 8k-token conversations — and 0 at 128k. Not few. Zero. One long conversation does not fit alongside the model it is talking to.

25.18 TTFT, TPOT, and goodput

Because the two phases differ, one latency number cannot describe a serving system. The standard vocabulary separates them.

The metrics that matter, and which phase each measures
MetricMeasuresGoverned by
TTFT
time to first token
How long before anything appearsPrefill — compute-bound, scales with prompt length
TPOT
time per output token
How fast text then streamsDecode — memory-bound, roughly constant
ThroughputTokens/second across all usersBatch size, which is capped by KV cache
GoodputThroughput that met its latency targetBoth, plus the scheduler

[i] Goodput is the honest one

A server can post magnificent throughput while every individual user waits intolerably, simply by batching aggressively. Goodput counts only the tokens delivered inside their latency SLO, which is the only number that corresponds to a satisfied user. Optimise throughput and you will eventually ship something nobody wants to use.

25.19 MFU and MBU

One more pair, because using the wrong one will send you optimising the wrong half of the machine.

MFU — model FLOPs utilisation — is the fraction of peak arithmetic you achieved. MBU — model bandwidth utilisation — is the fraction of peak memory bandwidth you achieved. Take a server producing a perfectly respectable 25.0 tokens per second on our 70B model:

The same healthy server, measured two ways
MetricValueVerdict
MFU0.18%Looks catastrophic
MBU52.7%Looks respectable

Both are correct. The MFU is genuinely near zero, because decode is not trying to use the arithmetic units and never was. Judging a decode server by MFU is like judging a lorry by its top speed: you will conclude something true and entirely beside the point.

[=] Which to report

Use MBU for decode and MFU for prefill and training. A decode server at 80% MBU is close to the physical limit of its hardware and further kernel optimisation will achieve nothing; the only remaining moves are to read fewer bytes, or to get more tokens out of each read.

25.20 Batching, the only real lever

If decode is slow because reading the weights costs 21.1 ms and the arithmetic is nearly free, there is one obvious response: make that read serve more than one token.

Run n sequences at once. The weights are still read once. Each read now produces n tokens instead of one, so arithmetic intensity becomes 2n instead of 2, and throughput rises almost linearly while the cost per user stays flat.

Batching and the KV cache wall Throughput scales linearly with batch size until the KV cache exhausts memory at batch 5, long before the compute ridge at batch 512. Batching is the only lever — and it runs out of memory first 100 1,000 10,000 1 8 64 256 512 1024 KV cache runs out at batch 5 (8k context) compute-bound at last batch 512 batch size tokens/s One weight read serves the whole batch, so throughput rises 65x by batch 64 with no loss per user. But the ridge sits 102x beyond what KV cache allows. On one card at 8k context, saturating the tensor cores is unreachable.
Figure 25.5 — Green is memory-bound, where extra batch is free throughput. The amber line is where you actually stop: out of memory, not out of compute.
Batching a 70B model on one H100
BatchIntensity RegimeTotal tok/s Per user
12.0memory47.047.5
816.0memory380.047.5
64128.0memory3037.047.5
256512.0memory12147.047.5
5121024.0compute14015.027.4

Batch 64 delivers 65.0× the throughput of batch 1 with no loss in per-user speed — the closest thing to a free lunch anywhere in this chapter. It is free precisely because the machine was idle: you are filling waiting time, not stealing from anyone.

It stays free until batch 512, where intensity finally crosses the ridge and decode becomes compute-bound. Past that point per-user speed starts falling, because the arithmetic units are genuinely saturated and users are now competing for them.

[!] Except you will never get there

Every sequence in the batch needs its own KV cache. At 8k context on a card holding an 8-bit 70B model, the KV budget runs out at batch 5. The compute ridge sits at batch 512 — 102.0× further away than you can reach.

Saturating the tensor cores during decode is not difficult on this hardware. It is impossible. You run out of memory capacity two orders of magnitude before you run out of compute, which is why almost every serving innovation of the last three years is about the KV cache rather than about arithmetic.

25.21 Static batching wastes the card

The naive implementation collects n requests, runs them together and returns when all are finished. It is simple, and it wastes most of what batching just bought.

Requests finish at different times. One asks for twenty tokens, another for two thousand. Under static batching the short request's slot sits idle until the longest one completes, and a batch of 32 that started full might be doing useful work in three slots by the end. Meanwhile new requests wait for the whole batch to drain.

25.22 Continuous batching

Orca (Yu et al., OSDI 2022) made the fix that now underpins every serious serving engine: schedule at the granularity of an iteration rather than a request.

After every single decode step, the scheduler removes finished sequences and admits waiting ones. The batch is rebuilt continuously, so a slot freed by a completed request is reused on the very next step rather than at the end of the batch. No sequence waits for another to finish.

[+] Why it works so well

It converts a queueing problem into a scheduling problem. Utilisation stops depending on requests conveniently having similar lengths — which they never do — and the reported gains over static batching are large enough (Orca measured over an order of magnitude in throughput under realistic load) that continuous batching is now simply assumed. vLLM, SGLang and TensorRT-LLM all do it.

One complication: attention cannot be batched the way the feed-forward layers can, because each sequence has a different length and a different KV cache. So engines use selective batching — batch the big matrix multiplications where the weights are shared, run attention per sequence.

25.23 Chunked prefill

Continuous batching creates a new problem. Prefill and decode now share a scheduler, and they are not comfortable neighbours.

A 4,000-token prefill occupies the GPU for a long, compute-bound stretch. Every user currently streaming tokens stalls for the duration. One arriving long prompt produces a visible hitch in everyone else's output — a latency spike with no obvious cause in the logs of the request that suffered.

Sarathi (Agrawal et al., 2023) proposed splitting prefill into fixed-size chunks and mixing a chunk into each iteration alongside the decodes. A step now carries a slice of prefill work and the ongoing decodes, so prefill makes steady progress without ever monopolising a step.

[=] The trade, stated plainly

Chunking slightly increases TTFT for the prefilling request — it is sharing each step rather than owning several — in exchange for removing the stall from every other user. It trades a little of one person's latency for a lot of everyone else's, which is nearly always the right trade in a shared system, and it also usefully raises the arithmetic intensity of an otherwise memory-bound decode step.

25.24 Prefill/decode disaggregation

Chunked prefill manages the conflict. The more radical answer is to stop running the two phases on the same hardware at all.

We established in 25.14 that prefill is compute-bound and decode is memory-bound. They want different batch sizes, different parallelism and arguably different cards. DistServe (Zhong et al., 2024) and NVIDIA's Dynamo run two pools: prefill workers produce the KV cache, ship it over the interconnect, and decode workers stream the output.

[!] What it costs

The KV cache must physically move between GPUs, which is why this only makes sense with a fast interconnect — NVLink at 900.0 GB/s, not PCIe at 128.0 GB/s. Disaggregation converts a scheduling problem into a networking problem, and is worth it only at a scale where you can keep both pools busy.

25.25 Paged attention and prefix caching

Since KV cache is the binding constraint, how it is allocated matters as much as how big it is.

The naive approach reserves a contiguous block per sequence, sized for the maximum possible length. A request that might generate 4,000 tokens but actually generates 200 wastes 95% of its reservation, and because the blocks are contiguous, the free memory between them fragments into unusable gaps.

PagedAttention (Kwon et al., 2023 — the paper that launched vLLM) borrowed the answer from operating systems: allocate the KV cache in fixed-size pages, keep a block table mapping logical positions to physical pages, and let a sequence's cache be scattered across memory. Waste falls to at most one partial page per sequence, and fragmentation disappears.

[+] The second benefit, which turned out to be the bigger one

Pages can be shared. Two requests with the same system prompt point at the same physical pages for that prefix — stored once, prefilled once. SGLang's RadixAttention generalises this by holding cached prefixes in a radix tree so any shared prefix is found automatically. For agent workloads, where every call repeats a long system prompt and a growing history, this is frequently a larger win than everything else in this section combined.

25.26 Engines and orchestrators

Two categories that get conflated constantly, to the confusion of anyone choosing between them.

Two different jobs
LayerJobExamples
Engine Run the model fast on one GPU or one node: kernels, batching, KV cache vLLM, SGLang, TensorRT-LLM
Orchestrator Route across a fleet of replicas: placement, autoscaling, prefix-aware routing NVIDIA Dynamo, llm-d, Kubernetes-based routers

You need both, and they solve different problems. An engine cannot help you when one replica holds the cached prefix for a conversation and the request lands on another; that is a routing decision. An orchestrator cannot help you when your kernels are inefficient.

25.27 Quantisation

Section D fought for more tokens per weight read. Section E attacks the read itself.

If decode time is the time to stream the weights, then halving the bytes per weight halves decode time. Not approximately — exactly, because the relationship is direct.

Precision against the decode floor, 70B on one H100
PrecisionWeights Floor per tokenCeiling Left for KV
FP16/BF16131.5 GB42.1 ms23.7 tok/s-51.5 GB
FP865.8 GB21.1 ms47.5 tok/s14.2 GB
INT432.9 GB10.5 ms94.9 tok/s47.1 GB

Going from 16-bit to 8-bit gives 2.0× the decode speed and frees 65.7 GB for KV cache, which raises the batch size, which raises throughput again. The two effects compound.

[=] Why quantisation helps decode more than prefill

Prefill is compute-bound, so its speed depends on the arithmetic rate, which lower precision improves only if the hardware has a faster path for it. Decode is memory-bound, so its speed depends on bytes moved, which lower precision improves always. Same change, different mechanism, very different magnitude.

25.28 GQA, and Gemma's sliding window

Quantisation shrinks the weights. The KV cache needs its own answer, and this is where model architecture starts making decisions on the hardware's behalf.

Grouped-query attention is the now-universal first step: share each key-value head across several query heads. Llama 3 70B uses 8 KV heads for 64 query heads, cutting KV cache by 8.0× against full multi-head attention.

Gemma asks a more radical question. Every layer in a standard transformer keeps a cache that grows without bound — but does every layer need to see the whole conversation? Gemma's answer is no, and it interleaves two kinds of layer: local layers that attend only within a fixed window, and global layers that see everything. Only the global ones keep a cache that grows.

Sliding-window attention across Gemma generations Gemma interleaves local and global attention so only a fraction of layers keep a cache that grows with context. Four generations of spending the innovation budget on memory Llama 3 70B — every layer remembers everything all global Gemma 2 — 1 global in 2, window 4,096 1:1 Gemma 3 — 1 global in 6, window 1,024 5:1 Gemma 4 — 5:1, wider global heads, K=V, MoE 5:1+ global: cache grows local: capped by window KV cache for one 128k-token conversation Llama 3 70B 40.0 GB Gemma 2 27B 23.7 GB Gemma 3 27B 10.7 GB Same job, 3.7x less memory. Not better engineering downstream — most of Gemma's layers were never asked to remember that far back.
Figure 25.6 — Red layers keep a cache that grows with the conversation; pale layers are capped by their window. Gemma 3 needs only 10 of 62 layers to remember everything.
KV cache for one 128k-token conversation
ModelPattern WindowGlobal layers KV cache
Llama 3 70Ball global80 of 8040.0 GB
Gemma 2 27B1:1409623 of 4623.72 GB
Gemma 3 27B5:1102410 of 6210.74 GB

Gemma 3 holds that conversation in 3.7× less KV cache than Llama 3 70B, and the difference is qualitative rather than incremental: on the same leftover HBM, Llama fits 0 such conversations and Gemma 3 fits 1. Cannot, versus can.

Notice also the direction of travel between generations. Gemma 2 used a 1:1 pattern with a 4096-token window, saving 1.9×. Gemma 3 moved to 5:1 with a 1024-token window, saving 5.8×. The same team, tightening the same screw, one release later.

[!] The counterweight, since only citing the flattering half would be a sales pitch

Gemma 3's vocabulary is 262,144 tokens against Llama's 128,256 — 2.0× larger. That makes its output projection 2.62 GB against 1.96 GB, and that matrix is read on every single token. Sliding-window attention buys context headroom and pays for some of it back at the output layer.

25.29 Gemma 4: three more attacks

Gemma 4 keeps the 5:1 pattern and adds three further mechanisms, each aimed at the same bottleneck. They are worth understanding individually, because they are independent and you will meet them separately in other models.

Asymmetric head dimensions. Global layers use a head dimension of 512 while local layers use 256 — 2× wider for the layers that must remember everything. This makes the global layers more expensive, which looks backwards until you notice there are only 5 of them out of 30. Spend capacity where memory is needed; withhold it where the window already caps the cost.

K=V sharing. Setting keys and values to the same tensor halves the cache exactly — 2.0×, by construction.

Shared KV layers. Consecutive layers can reuse one cache rather than each keeping its own. Combined with K=V sharing this reaches 2.5×.

25.30 Mixture of experts

The last idea in this section is the sharpest illustration of the chapter's whole thesis, and Gemma 4 ships it as 26B-A4B.

A mixture-of-experts model replaces each feed-forward block with many "experts" and routes each token to a small subset. The model has 26.0 billion parameters in total, but any given token only activates 4.0 billion of them — 6.5× sparsity.

Now apply everything this chapter has established. Decode time is the time to read the weights you need. A dense 26.0B model would read 24.2 GB per token, taking 7.8 ms. The MoE reads only 3.7 GB, taking 1.2 ms6.5× faster.

[+] Why this is the right trade on this hardware

The full 24.2 GB still has to live in HBM, so MoE buys latency by spending capacity. On a card with spare gigabytes and no spare bandwidth, that is exactly the correct direction to trade. It decodes at the speed of a 4.0B model while knowing what a 26.0B model knows — and it only makes sense because of the asymmetry this chapter has been describing.

25.31 Training: where the constraint flips

Everything so far has been about inference, where bandwidth is the binding constraint and capacity is a secondary irritation. Training inverts that, and the inversion catches people who learned their intuitions from serving.

Training is compute-bound in the way inference is not. A training step processes a whole batch of sequences at once, so arithmetic intensity is high and the tensor cores genuinely work. The rule of thumb is 6N FLOPs per token for a model of N parameters — roughly 2N for the forward pass and 4N for the backward pass, which must compute gradients with respect to both activations and weights.

What breaks instead is capacity. The weights are the smallest part of what a training step must hold.

25.32 Optimiser state and activations

Train a 70.6B model with mixed precision and Adam, and count what must be resident:

Memory to train a 70B model, before activations
WhatPrecisionSize
Weights16-bit131.5 GB
Gradients16-bit131.5 GB
Master weights32-bit263.0 GB
Adam first moment32-bit263.0 GB
Adam second moment32-bit263.0 GB
Total1052.0 GB

That is 16.0 bytes per parameter against the 2 bytes inference needs — and it requires at minimum 14 H100s before you have stored a single activation or processed a single token.

[i] The asymmetry worth internalising

The same 70B model infers comfortably on one card once quantised, and cannot be trained on fewer than 14. Inference asks "can I stream the weights fast enough?"; training asks "can I hold eight copies of everything at once?". They are different questions about the same model, and the answers differ by an order of magnitude in hardware.

Activations are the fourth consumer, and the one you can actually trade against compute. Every intermediate tensor from the forward pass is needed during the backward pass. Gradient checkpointing discards most of them and recomputes them on the way back — typically around 30% more compute for a large reduction in memory. On the memory-constrained side of the problem, that is usually a bargain.

25.33 The parallelism menu

Once one card is not enough, you must split the work, and the choice of how is dictated almost entirely by what each option costs in communication.

Four ways to split, and what each demands of the network
StrategySplits CommunicationWhere it belongs
DataThe batch Gradient all-reduce each stepAnywhere; the default
TensorIndividual matrices Every layer, every step — heaviestInside one node only
PipelineLayers into stages Activations at stage boundaries — lightAcross nodes
ExpertMoE experts Token routing between expertsWherever the experts live

Tensor parallelism communicates at every layer of every step, so it is only viable where bandwidth between GPUs is enormous — which means inside a single NVLink-connected node. Pipeline parallelism only passes activations at stage boundaries, so it tolerates slower links and is what you use to cross between nodes. That is not a preference; it follows from the numbers in the next section.

25.34 Interconnect, and why topology decides

Every step away from the silicon costs an order of magnitude.

Bandwidth at each hop, H100
PathBandwidthRelative to HBM
HBM (on-package)3.35 TB/s
NVLink (GPU to GPU, in node)900.0 GB/s3.7× slower
PCIe Gen5 (to host, across nodes)128.0 GB/s26.2× slower

NVLink is 7.0× faster than PCIe, and HBM is another 3.7× faster than NVLink. This hierarchy explains the physical design of AI datacentres: eight GPUs in a node on NVLink, nodes connected by InfiniBand, and a parallelism strategy chosen to keep the chattiest communication inside the fastest tier.

[=] Scale up before you scale out

Scale up means a bigger node with more GPUs on one fast fabric. Scale out means more nodes over a slower network. Always exhaust scale-up first: keeping tensor parallelism inside a node and pipeline parallelism between them is the whole game, and getting it backwards will cost you more than any kernel optimisation will win back.

25.35 A decision guide

The chapter's practical residue. Diagnose first, then choose.

What to do about a slow model
SymptomLikely causeWhat actually helps
Single user, slow tokens Memory-bound decode; you are at the physical floor Quantise. Smaller model. Speculative decoding. Not a faster GPU.
Throughput poor under load Batch too small Continuous batching; raise the batch until KV cache runs out
Batch capped low KV cache exhausted Paged attention, KV quantisation, shorter context, a sliding-window model
TTFT poor, TPOT fine Prefill is compute-bound and queued Chunked prefill; prefix caching; more prefill capacity
Streaming stutters when others arrive Long prefills monopolising steps Chunked prefill, then disaggregation if it persists
Repeated system prompts Re-prefilling identical tokens Prefix caching, and prefix-aware routing across replicas
Model will not fit Capacity, not bandwidth Quantise first; then tensor parallelism inside a node
MFU near zero during decode Nothing. This is correct. Measure MBU instead

25.36 Key takeaways

What to carry out of this chapter

  • Decode is memory-bound, structurally. Reading 65.8 GB of weights takes 21.1 ms; the arithmetic on them takes 0.071 ms. The GPU waits 99.7% of the time, and no kernel work changes that.
  • Arithmetic intensity tells you which half of the machine you are using. Below the ridge (590.7 FLOP/byte at FP8) only moving fewer bytes helps. Batch-1 decode sits at 2.0.
  • Faster tensor cores make saturation harder, not easier. They move the ridge right. This is why upgrading GPUs so often disappoints.
  • Batching is nearly free until memory runs out. 65.0× throughput at batch 64 with no per-user cost — but KV cache caps you at batch 5 at 8k context, which is 102.0× short of the compute ridge.
  • The KV cache is the binding constraint in modern serving. Paged attention, prefix caching, GQA and sliding windows all exist because of it.
  • Architecture is now designed around this. Gemma 3 keeps only 10 of 62 layers globally attentive, holding a 128k conversation in 3.7× less cache than Llama 3 70B. MoE reads 3.7 GB of a 24.2 GB model per token.
  • Training inverts the constraint. Adam needs 16.0 bytes per parameter, so a model that infers on one card needs 14 to train.
  • Measure MBU for decode, MFU for prefill and training. The same server reads 0.18% by one and 52.7% by the other.

25.37 Drills

Fourteen questions in the order the chapter built them. Work out the answer before opening each one — several have a plausible wrong answer that is the whole reason the question is worth asking.

1. Your 70B model generates 40 tokens/second for a single user. Your manager approves a GPU with double the tensor-core throughput. What happens?

Essentially nothing. Single-stream decode is memory-bound: the step is 21.1 ms of weight reading and 0.071 ms of arithmetic. Doubling compute halves the smaller number and leaves the floor where it was.

Worse, it moves the ridge point further right, so the workload is now more memory-bound than before. Spend the money on faster HBM, quantisation, or a smaller model.

2. Why is batch-1 decode's arithmetic intensity a constant, regardless of model size?

Because both terms scale with parameter count. A model of N parameters does about 2N FLOPs per token and reads N bytes of weights at 8-bit. The ratio is 2 whatever N is.

This is why a 7B model is just as memory-bound as a 70B one during decode. It is faster in absolute terms — fewer bytes to read — but sits in exactly the same place relative to the ridge.

3. A colleague reports 3% MFU on your inference server and calls it a disaster. Are they right?

No, though the number is correct. Decode is not attempting to use the arithmetic units. MFU measures the wrong resource for this phase.

Ask for MBU instead. The same server can read 0.18% by MFU and 52.7% by MBU. If MBU is high, you are near the physical limit of the hardware and further kernel work is wasted effort.

4. Prefill and decode run the same weights on the same GPU. Why do they need different treatment?

They sit on opposite sides of the ridge. Prefill processes many tokens against one weight read, so its intensity rises with prompt length and it becomes compute-bound at around 512 tokens. Decode processes one token per weight read forever.

So they want different batch sizes and different parallelism, and running them on one scheduler means long prefills stall active decodes. Chunked prefill mitigates this; disaggregation separates them entirely.

5. Batching gave you 65× throughput at batch 64 with no per-user slowdown. Why can you not simply keep going to batch 512?

KV cache. Each sequence needs its own, at 320.0 KB per token for Llama 3 70B. With 14.2 GB of HBM left after the weights, 8k-token conversations run out at batch 5.

The compute ridge is at batch 512 — 102.0× beyond reach. On one card at this context length, compute saturation during decode is not merely hard, it is unreachable.

6. Someone says "we run Llama 3 70B on a single 80GB H100". What should you check?

The precision. At 16-bit the weights alone are 131.5 GB, overflowing an 80 GB card by 51.5 GB before any KV cache exists. It needs 2 cards.

At 8-bit it is 65.8 GB and fits with 14.2 GB spare. The claim is true only with quantisation, which at this size is a deployment requirement rather than an optimisation.

7. Why does quantisation speed up decode more reliably than it speeds up prefill?

Different bottlenecks. Decode time is weight-reading time, so halving bytes per weight halves the floor — 2.0× going from 16-bit to 8-bit, exactly.

Prefill is compute-bound, so it only benefits if the hardware has a faster arithmetic path at that precision. There is a second decode benefit too: smaller weights leave 65.7 GB more for KV cache, raising the achievable batch.

8. Static batching keeps the GPU at high utilisation on your benchmark but disappoints in production. Why?

Your benchmark almost certainly uses uniform request lengths. Real traffic does not: a batch containing one 20-token and one 2,000-token request holds the short one's slot idle for the duration.

Continuous batching rebuilds the batch every iteration, so finished slots are reused on the next step. The gap between the two only appears when request lengths vary, which is exactly what benchmarks tend to hide.

9. What does PagedAttention actually fix, and what turned out to be its larger benefit?

It fixes allocation waste. Reserving contiguous worst-case KV blocks per sequence wastes most of the reservation and fragments the rest. Paging the cache reduces waste to at most one partial page per sequence.

The larger benefit was unplanned: pages can be shared. Requests with a common system prompt reference the same physical pages, so that prefix is stored and prefilled once. For agent workloads that repeat long prompts, this frequently dominates the original saving.

10. Gemma 3 holds a 128k conversation in far less KV cache than Llama 3 70B. What is the mechanism, and what does it cost?

Interleaved local and global attention, 5:1 with a 1024-token window. Only 10 of 62 layers keep a cache that grows with context; the rest are capped by their window. That is 10.74 GB against 40.0 GB — 3.7× less.

The cost is at the other end: Gemma 3's vocabulary is 2.0× Llama's, making its output projection 2.62 GB against 1.96 GB — read on every token.

11. Gemma 4 gives its global layers wider heads than its local ones. Is that not backwards?

It looks it, since it makes the expensive layers more expensive. But there are only 5 global layers out of 30, and they are the only ones whose cache grows with the conversation.

So the model spends capacity on the few layers that must genuinely remember, and withholds it from the many whose cost is already capped by their window. Asymmetric hardware costs deserve asymmetric architectural spending.

12. An MoE model has 26.0B parameters but activates 4.0B per token. Which number determines decode speed, and which determines whether it fits?

Active parameters determine speed, total parameters determine fit. Decode reads only the 3.7 GB it routes to, giving a 1.2 ms floor against 7.8 ms for the equivalent dense model.

But all 24.2 GB must live in HBM. MoE buys latency by spending capacity — the right trade on a card with spare gigabytes and no spare bandwidth.

13. Your 70B model serves happily on one GPU. Why does fine-tuning it need at least 14?

Because the constraint changes from bandwidth to capacity. Inference holds weights and KV cache. Training with Adam holds weights, gradients, fp32 master weights, and two optimiser moments — 16.0 bytes per parameter, or 1052.0 GB, before a single activation.

This is why LoRA and other parameter-efficient methods exist: they shrink the optimiser state, which is the part that actually broke the budget, not the weights.

14. Why does tensor parallelism belong inside a node while pipeline parallelism crosses between them?

Communication volume against link speed. Tensor parallelism synchronises within every layer of every step, so it needs NVLink at 900.0 GB/s. Pipeline parallelism only passes activations at stage boundaries, so it tolerates PCIe or InfiniBand at 128.0 GB/s.

NVLink is 7.0× faster than PCIe. Getting this backwards — tensor parallelism across nodes — is one of the most expensive configuration mistakes available, and no amount of kernel tuning will recover it.

[i] Where this leaves you

You can now predict on paper roughly how fast a model will run on a given card, say which resource will run out first as load grows, and tell whether a proposed optimisation addresses your bottleneck or somebody else's. That is the difference between tuning a serving stack and guessing at it.

Chapter 6 puts these ideas to work in vLLM specifically. Chapter 2 has the transformer mechanics underneath the KV cache arithmetic. Chapter 16 covers the service architecture around a model server, and chapter 14 the platform it runs on.