Lakshya

LLM Systems Engineering · Chapter 1 of 20

Inference economics: one calculation that explains everything

Prefill and decode are different machines. Once you can compute the roofline and the KV cache — the model’s running notes on the conversation so far — every serving decision follows.

10 min read2 diagramsAll 20 chapters

This is the chapter that turns LLM serving from a list of techniques into a single problem. There are two numbers to be able to derive — the hardware's ridge point and the model's KV cache per token — and between them they explain continuous batching, paged attention, quantisation, speculative decoding, prefix caching and disaggregated serving. If you can do this arithmetic out loud you will be ahead of most candidates for any inference or AI platform role.

Prefill and decode have opposite bottlenecks

Prefill processes the whole prompt at once. Every token can be computed in parallel, so the GPU does an enormous amount of arithmetic per byte of weights it reads. Its arithmetic intensity is roughly the sequence length. It is compute-bound.

Decode generates one token at a time. For each token it must re-read the entire weight matrix, plus the growing KV cache, to produce a single output. At batch size one, the arithmetic intensity is about one operation per byte. It is memory-bandwidth-bound, severely.

THE ONE CALCULATION THAT EXPLAINS EVERY SERVING DECISION TFLOP/s arithmetic intensity — FLOP per byte moved (log scale) memory-bandwidth bound compute bound ridge ≈ 295 FLOP/byte DECODE, batch 1 — ≈1 FLOP/byte the GPU runs at roughly 0.3% of its compute PREFILL — intensity ≈ sequence length THE ARITHMETIC (H100 SXM) ~989 TFLOP/s dense BF16 ~3.35 TB/s HBM3 989e12 ÷ 3.35e12 ≈ 295 You need a batch of several hundred before decode becomes compute-bound. Per-sequence decode ceiling: bandwidth ÷ weight bytes 70B BF16 (140 GB) → ~24 tok/s 70B FP8 (70 GB) → ~48 tok/s Continuous batching, paged attention, quantisation, speculative decoding and prefill/decode disaggregation all exist for one reason: to drag decode rightward toward the ridge. Once you see that, the whole serving literature stops being a list of tricks and becomesone problem. It also explains why quantisation roughly doubles decode speed when you halve the weight bytes — and does almost nothing for prefill.
The roofline, with the two workloads placed on it. The ridge point is simply peak FLOP/s divided by memory bandwidth. Decode at batch 1 sits so far left that the GPU is running at a fraction of a percent of its compute.

Two derivations to memorise, because they answer most questions

The ridge point. Peak compute divided by memory bandwidth. On an H100 SXM that is roughly 989 TFLOP/s over 3.35 TB/s, or about 295 FLOP per byte. Anything with lower intensity than that is bandwidth-limited no matter how good your kernels are.

The per-sequence decode ceiling. Memory bandwidth divided by the bytes of weights you must read per token. A 70B model at BF16 is 140 GB, so on 3.35 TB/s you cannot exceed about 24 tokens per second for a single sequence — regardless of batching, kernels or software. At FP8 the weights are 70 GB and the ceiling doubles to about 48.

That second number is the one that settles arguments. When someone says their optimisation will make single-stream generation faster, you can check in ten seconds whether it is physically possible.

It also explains a fact people find surprising: quantisation roughly doubles decode throughput when it halves the weight bytes, and does almost nothing for prefill, because prefill was never bandwidth-limited.

The KV cache, and why it caps concurrency rather than the weights

FIRST — WHAT THE KV CACHE ACTUALLY IS To write its next word, a model looks back at every word so far. Redoing that from scratch each time would be enormously wasteful, so it keeps working notes about each word and reuses them. Those notes are the KV cache — K and V for “keys and values”, the two things it stores per word. The notes live in GPU memory, beside the model itself. The model’s size never changes. The notes grow with every word, and with every user you serve at the same time. HOW BIG ARE THE NOTES, PER WORD? bytes per word = 2 × layers × KV heads × head size × bytes per number   2 — because it stores two things per word, a key and a value   layers — a model is a stack of layers, and each one keeps its own notes   KV heads — how many parallel “readers” each layer keeps notes for. This is the term people get wrong   head size — how many numbers each reader records. bytes per number — 2 for a half-precision number The trap: count KV heads, not the model’s total heads. Nearly every current model shares one set of notes across several readers — grouped-query attention — which makes the cache four to eight times smaller than a naive count suggests. a 70B model, shared notes80 layers, 8 KV heads320 KiB per word2.5 GiB for one long requestthe same, notes NOT shared80 layers, 64 KV heads2.5 MiB per word20 GiB — eight times worsea smaller 8B model32 layers, 8 KV heads128 KiB per word1 GiB for one long request Why the notes, not the model, cap your users: an 80 GB card holding a 70 GB model has ~10 GB left. At 320 KiB per word that is about 32,000 words of notes in total, shared across everyone at once. Eight users with a long documenteach, and it is full.
The formula, the grouped-query correction, and the consequence. The cache, not the model, is what decides how many users you can serve at once.

The formula is worth carrying: 2 × n_layers × n_kv_heads × head_dim × bytes_per_element per token. The 2 is the key and the value. The term people get wrong is n_kv_heads — every modern model uses grouped-query or multi-query attention, where many query heads share one key/value head, so the cache is four to eight times smaller than a multi-head calculation suggests.

Work it for a 70B-class model with 80 layers, 8 KV heads and a head dimension of 128, in FP16: 2 × 80 × 8 × 128 × 2 = 327,680 bytes, or 320 KiB per token. A single 8,000-token request therefore holds about 2.5 GiB of cache.

Now the consequence. An 80 GB accelerator cannot hold that model at BF16 at all. At FP8 the weights take 70 GB, leaving roughly 10 GB — which at 320 KiB per token is about 32,000 tokens of KV in total, shared across every concurrent request. Eight users at 4K context and you are full.

This is why FP8 KV-cache quantisation is one of the highest-leverage settings in serving: it halves the cache and therefore roughly doubles concurrency, at unchanged weights. And it is why architectural work on sparse and compressed attention matters commercially rather than academically — recent sparse-attention designs report roughly a tenth of the KV footprint at million-token context, which is the difference between long context being a research demo and a product.

Continuous batching

Static batching pads every sequence in a batch to the longest one and holds the whole batch until the slowest finishes. Since output lengths vary enormously, most of the GPU's time is spent generating padding for sequences that finished long ago.

Continuous batching — also called in-flight or iteration-level batching, and originating in the Orca paper — decides batch membership at every decode step. A finished sequence is evicted immediately and a queued one admitted in its place.

The measured effect is large. A widely-cited benchmark on a 13B model reported up to 23× throughput over naive static batching, and — more instructive — showed static batching's throughput collapsing as output-length variance grew while continuous batching stayed flat. Treat the headline multiple with care: it compares against unoptimised serving rather than against a tuned static batcher. The shape of the result, that the advantage grows with variance, is the durable finding.

Chunked prefill is the companion technique. A long prompt is split into token ranges scheduled across several iterations, so one large prefill does not stall every in-flight decode. It trades a little time-to-first-token for much better inter-token latency at the tail.

PagedAttention

Before it, serving systems allocated one contiguous KV buffer per request, sized to the maximum possible sequence length. Since most requests are far shorter, 60 to 80% of KV memory was wasted on reservation and fragmentation.

PagedAttention borrows operating-system virtual memory: fixed-size KV blocks, a per-request block table, and non-contiguous physical placement. Reported waste falls below 4%, and throughput improves two to four times at equal latency. Copy-on-write block sharing also makes parallel sampling and beam search nearly free, because the shared prefix is shared physically rather than copied.

The clean way to describe it in an interview: it is paging, applied to the KV cache, and it works for the same reason paging worked for RAM — it decouples the address space a request thinks it has from the memory it actually occupies.

Speculative decoding, and the caveat that matters more than the headline

A small, cheap drafter proposes k tokens. The target model verifies all k in a single forward pass — which costs almost the same as generating one token, because decode is bandwidth-bound and the weights had to be read anyway. You keep the longest accepted prefix. Speedup is roughly the number of tokens accepted per verification step.

Reported figures at batch 1 on a 70B model put modern draft-head methods at 3.0 to 3.4× decode speedup, with acceptance rates around 0.80 to 0.88 on code and instruction-following.

And now the part that is usually left out. Speculative decoding's gains shrink and can invert as batch size grows. Verification adds FLOPs, and at high concurrency the system has already moved toward being compute-bound — so you are adding arithmetic to the resource that has become scarce.

Independent benchmarks report speculative decoding underperforming standard decoding at high concurrency, and throughput degrading beyond about five draft tokens as acceptance falls. Optimal draft length is batch-size dependent.

So teach it as a latency optimisation for low-concurrency serving, not a throughput optimisation. It is the right tool for an interactive single-user experience and frequently the wrong one for a busy shared endpoint — and being able to say why, from the roofline, is exactly the kind of reasoning these roles are hiring for.

Disaggregated prefill and decode

If prefill is compute-bound and decode is bandwidth-bound, running them on the same GPU pool means one of them is always misallocated. Disaggregation runs them on separate pools and ships the KV cache between them over a fast interconnect.

This is now the direction of the industry rather than a research idea — it is supported across the major serving engines and deployed at several large operators. The most instructive production example pools spare cluster CPU memory and SSD into a shared KV store, so requests with overlapping prefixes skip prefill entirely; that reframes the KV cache from a per-request cost into a cluster-wide asset.

A caution on the headline numbers: vendor claims of large multiples usually combine disaggregation with other changes such as wide expert parallelism, so they should not be attributed to disaggregation alone. The mechanism is sound; the specific multiple is marketing.

Check yourself

Why is decode memory-bandwidth-bound?Each generated token requires re-reading the whole weight matrix to produce one output, so arithmetic intensity is about one operation per byte at batch 1 — far below the hardware's ridge point.
Compute the KV cache per token for an 80-layer model with 8 KV heads, head dim 128, in FP16.2 × 80 × 8 × 128 × 2 = 327,680 bytes, or 320 KiB. Use KV heads, not query heads — grouped-query attention is the whole point.
Why does quantisation help decode much more than prefill?Decode is limited by bytes of weights read per token, so halving the bytes roughly doubles the ceiling. Prefill was compute-bound, so fewer bytes changes little.
When is speculative decoding the wrong choice?At high concurrency. Verification adds FLOPs to a system that has already become compute-bound, and measured results show it underperforming standard decoding there.
← All chaptersQuantisation, with the evidence →