Quantization & inference optimisation, section 3 of 4, 5h

Making generation faster

By the end: You can predict which serving technique will help a given workload, and by roughly how much, from arithmetic rather than from a benchmark.

  • Prefill and decode as two different workloads
  • Arithmetic intensity and the roofline
  • The decode floor: weight bytes over bandwidth
  • Continuous batching and paged attention
  • Speculative decoding and acceptance rate

New to quantization & inference optimisation? Every number inside a model is stored with a certain precision: how many binary digits it gets, and so how finely it can be told apart from its neighbours. Sixteen bits per number is the usual starting point. Quantization stores them with fewer — often eight, or four — which shrinks the model in memory by the same proportion and speeds it up, because what limits inference is usually how fast numbers can be moved to the processor rather than the arithmetic itself. Why it matters

You can already say why generating tokens is expensive. This section is about the techniques that make it less so: batching that never pauses to refill, a KV cache allocated in pages, a small model drafting tokens for a large one to check. Each has a workload it suits and a workload where it does nothing or makes things worse, so the section is built around predicting which will help before you spend a week implementing it.

Two workloads, one model#

A generation request has two phases that behave nothing like each other. Prefill reads the whole prompt at once: every token goes through every layer together, so each weight is read once and used for hundreds or thousands of positions. That is a large matrix multiplication and it is compute-bound. Decode produces one token at a time: each step reads every weight again and uses it for a single position, which is very little arithmetic for a great deal of memory traffic, and it is memory-bound.

Prefill    scales with prompt length      compute-bound     sets time to first token
Decode     scales with output length      memory-bound      sets time per output token
Most serving decisions follow from this one split. Reporting a single average latency averages two unrelated things.

Almost every technique below is an answer to the second row. Quantization helps because there are fewer weight bytes to read. Batching helps because the same read serves more requests. Speculative decoding helps because a memory-bound step has spare arithmetic going unused.

Roofline reasoning#

Arithmetic intensity is the number of floating point operations performed per byte moved to and from memory. Every accelerator has its own ratio of peak compute to peak bandwidth. If your intensity is below the hardware's ratio you are memory-bound, and no faster arithmetic will help; above it you are compute-bound.

arithmetic intensity = FLOPs performed / bytes moved

card with 1000 TFLOP/s and 3 TB/s  ->  ratio about 330 FLOP per byte

decode at batch 1    about 2 FLOP per weight byte   ->  far below: memory-bound
decode at batch 256  about 500 FLOP per weight byte ->  above: compute-bound
prefill, 2k prompt   thousands of FLOP per byte     ->  compute-bound

The useful consequence is that you can compute the floor on decode speed before renting anything. At batch size one, a decode step must read every weight, so the time per token cannot be less than the weight bytes divided by the memory bandwidth.

8B parameters at 16-bit  = 16 GB read per token
card with 2 TB/s         = 16 / 2000 s = 8 ms per token = about 125 tokens/s

the same model at 4-bit  = 4 GB  -> 2 ms -> about 500 tokens/s (ceiling)
A ceiling, not a prediction: the KV cache is read too, and no kernel reaches peak bandwidth. Measuring far below it means something else is wrong.

Continuous batching#

Static batching collects requests, runs them together and returns when the longest finishes. A request wanting 20 tokens waits behind one wanting 500, and the GPU spends the tail of the batch mostly idle. Continuous batching admits and retires sequences at every decode step instead, so a finished sequence leaves immediately and a waiting one takes its slot.

It wins so much because decode is memory-bound. One step reads the weights once whether it serves one sequence or sixty, so the extra sequences use arithmetic that was being wasted. Throughput rises nearly linearly with batch size until either the KV cache fills the remaining memory or the batch grows large enough to become compute-bound, which is where the roofline arithmetic tells you to stop.

Paged attention#

The naive way to hold a KV cache is to reserve, per sequence, enough contiguous memory for the maximum length it might reach. A request that stops after 200 tokens of a possible 8,000 has held onto memory nobody could use. Paged attention allocates the cache in small fixed-size blocks, the way an operating system pages memory, and hands them out as a sequence grows.

Waste falls from most of the reserved cache to under one block per sequence, and the blocks make prefix sharing possible: two requests with the same system prompt can point at the same blocks until they diverge. The work that introduced it reported two to four times the throughput at the same latency, which is concurrency bought purely by not wasting memory.

Speculative decoding#

A small draft model generates the next few tokens cheaply. The large model then checks all of them in a single forward pass, because verifying k proposed positions costs about what one decode step costs: the weights are read once either way, and a memory-bound step has the arithmetic to spare. Tokens are accepted up to the first one the large model would not have produced, and generation continues from there.

draft proposes   the cat sat on the mat
large verifies   the cat sat on the  |  rug      -> 5 accepted, 1 corrected

one large-model pass produced 6 tokens instead of 1

Whether it pays depends on the acceptance rate. Each round costs the draft model's k steps plus one large pass, so the draft has to be both much cheaper, commonly ten to twenty times, and right often enough. Acceptance in the region of two thirds and above is where the arithmetic usually works; below that the drafting costs more than the accepted tokens save. Acceptance is also task-dependent, which is why it should be measured on your own traffic rather than taken from a paper. Code and structured output accept well; open-ended prose accepts worse.

If output quality changed, it is a bug and not a trade-off

The verification step accepts tokens by a rule that leaves the output distributed exactly as the large model alone would have produced. Speculative decoding is not an approximation and buys latency, not quality. A measurable quality change means the implementation is wrong.

The one place it disappoints is a server already batching heavily. It spends spare compute, and at large batch sizes there is none spare, so it can make throughput worse while still improving latency for a single stream.

Where people go wrong

Reaching for speculative decoding to fix a throughput problem. It converts spare compute into lower latency, and a server that is already batching well has no spare compute, so throughput can fall while the demo on one stream looks faster.

If this comes up in an interview

Why does prompt length change time to first token but barely change time per output token?

They are different workloads. Prefill processes the whole prompt in parallel, so it is compute-bound and scales with prompt length, and it is what time to first token measures. Each decode step reads the whole model to produce one token, so it is memory-bound and its cost is dominated by weight bytes over bandwidth, which does not change with prompt length. Only the growing KV cache adds a little.

Then they push further

Which one does your bill depend on?

Depends on the traffic. Long prompts with short answers, such as classification or extraction, are dominated by prefill and by input token pricing. Short prompts with long answers are dominated by decode, which is the slower and more expensive side per token.

How would you cut time to first token specifically?

Shorten the prompt, cache the shared prefix so it is not re-prefilled on every call, stream so the user sees output as it arrives, and keep long-context requests off the instances serving interactive traffic.

What gets marked up. Strong answers name the two phases, say which is compute-bound and which memory-bound, and then use that split to explain a serving decision.

How would you work out the fastest a model could possibly generate on a given GPU?

At batch size one, each token requires reading every weight, so divide the weight bytes by the memory bandwidth. An 8B model at 16-bit is 16 GB, and on a 2 TB/s card that is 8 ms per token, about 125 tokens per second, before the KV cache reads and any inefficiency. Quantizing to 4-bit cuts the bytes by four and raises the ceiling by roughly the same factor.

Then they push further

What changes at batch size 64?

The weight read is amortised across 64 sequences, so total throughput rises nearly linearly while each sequence's token rate holds up until the batch becomes compute-bound. What limits it first is usually KV cache memory, not arithmetic.

Why does continuous batching raise throughput so much?

Decode is memory-bound, so one step reads the weights once regardless of how many sequences are in the batch, and extra sequences use arithmetic that would otherwise be idle. Continuous batching keeps that batch full by admitting and retiring sequences at every step, rather than letting the whole batch wait for its longest member on mixed-length traffic.

What is speculative decoding, and what does it cost?

A small draft model proposes several tokens and the large model verifies them in one pass, accepting the longest prefix it agrees with. It is exact: the output distribution is the large model's. It costs the draft model's compute and only pays off above an acceptance rate of roughly two thirds, and it helps least on a server already running large batches, where there is no spare compute to spend.

Build this: Serve a small open model and measure time to first token and time per output token separately at batch 1, 8 and 32. Compute the memory-bandwidth floor for the weights and compare it with the measured batch-1 rate. Then enable speculative decoding, measure the acceptance rate on your own prompts, and see whether it helped at each batch size.

Stop 28 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeData quality & labellingWhere ground truth actually comes from.9h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.