Skip to content
13/30Chapter 13 of 30

Making Inference Cheap: KV Cache, Batching and Quantization

The same model answering the same question in 8.8 seconds and in 78.9, byte-identical output. Then INT4, measured three ways not asserted.

On this page

The same model, on the same machine, answering the same question with the same 48 tokens. The two outputs are identical token for token — checked, not assumed.

TEXT
with a key-value cache:     8.85 s   ( 6.01 tokens/second)
without a key-value cache: 78.95 s   ( 0.60 tokens/second)

One argument changed: use_cache=False. Nothing about the model, the prompt, the sampling or the arithmetic is different, and the second run is not more accurate for its trouble. It is nine times slower for nothing.

That is the shape of this chapter. Everything in it — the cache, the batch, the quantized weights — is an attempt to stop paying for work that does not change the answer, or to find out what a cheaper answer costs. Chapter 10 established the price list for training. This is the price list for the side you pay forever: a deployed model spends roughly 2N2N FLOPs for every token it emits, on every request, for the rest of its life.

To generate a token, a decoder-only transformer takes the whole sequence so far, runs it through every layer, and reads the probability distribution off the last position. Then it appends the chosen token and does it again. That description is correct, and it is what the slow run does.

It is also enormously wasteful, and the reason is the causal mask from Chapter 9. Position 7's key and value vectors are computed from position 7's input and the positions before it. When position 8 arrives, position 7 cannot see it — that is what causal means — so position 7's key and value are exactly the same numbers as before. The slow run recomputes them anyway, at every step.

So store them. That store is the key-value cache, the single most consequential optimisation in language model serving:

generate.pyPYTHON
out = model(prompt_ids, use_cache=True)          # prefill: the whole prompt
past = out.past_key_values                        
nxt = out.logits[:, -1].argmax(-1, keepdim=True)

for _ in range(n - 1):
    out = model(nxt, past_key_values=past, use_cache=True)   
    past = out.past_key_values                                
    nxt = out.logits[:, -1].argmax(-1, keepdim=True)

Look at what is fed to the model inside the loop: nxt, one token. Not the sequence. The new token's query attends against every cached key, and the cached keys were never going to change. This is not an approximation — the identical-output check above is the point. The cache does not trade quality for speed; it deletes redundant arithmetic.

To see the scaling cleanly, strip the transformer away and time a single attention head with d=64d = 64, one step of generation computed both ways:

tokens in contextrecompute everythingwith a cacheratioscore matrix
1280.59 ms0.062 ms10x65,536 B vs 512 B
2561.20 ms0.163 ms7x262,144 B vs 1,024 B
5127.03 ms0.078 ms90x1,048,576 B vs 2,048 B
102417.31 ms0.114 ms152x4,194,304 B vs 4,096 B
204859.83 ms0.214 ms279x16,777,216 B vs 8,192 B
4096236.18 ms0.284 ms832x67,108,864 B vs 16,384 B

The right-hand column is the cause. Recomputing builds the full n×nn \times n attention matrix every step — the O(n2)O(n^2) from Chapter 9's asymptotic-notation box, paid once per token. With the cache you build a 1×n1 \times n row instead: at 4,096 tokens, 67 MB of scores against 16 KB.

Counting multiply-accumulates instead of milliseconds removes the machine from the argument. To generate TT tokens from a cold start:

tokens generatedwith a cacherecomputingratio
1282.6 M192.0 M73x
51223.1 M7.36 G318x
2048293.7 M392.6 G1,336x

Per step the cached version is linear in the context and the uncached one quadratic; summed over a generation, O(T2)O(T^2) against O(T3)O(T^3), with the ratio growing without limit. The nine-fold difference in the opening was measured over 48 tokens — short of that table's first row.

The cache also changes what has to be in memory. On an 8 GB laptop GPU generating 256 tokens in fp16, taking the allocator's peak and subtracting the resident weights:

peak working memory
with a cache21.8 MB
recomputing181.7 MB

8.3 times more memory, spent to produce the same tokens more slowly. This is the promise made in Chapter 5, arriving from an unexpected direction: there, reverse-mode autodiff had to keep every intermediate alive for the backward pass, and activations dominated training memory. At inference there is no backward pass and nothing to retain for it — so what dominates memory instead is the cache, and it is a deliberate choice rather than an unavoidable cost.

Prefill and decode are two different machines

Link to the section: Prefill and decode are two different machines

Look again at the fast run: its first token behaved unlike the other forty-seven.

TEXT
prefill, 40 prompt tokens : 1.0224 s   ->  25.6 ms per token
decode,  47 steps         : 0.1665 s mean per step

The prompt cost 25.6 ms per token and each generated token cost 166 ms. Same model, same hardware, same weights, a six-fold difference per token — and it goes the way most people do not expect. The prompt is the cheap part. Generation splits into two phases with genuinely different physics:

One forward pass over the whole prompt. Every token is processed in parallel, so each weight matrix is loaded from memory once and multiplied against a matrix of hundreds of token vectors — a matrix-matrix product, with lots of arithmetic per byte moved, which is what a GPU is built for. Prefill is compute-bound, and its cost is roughly linear in the prompt length.

One forward pass per token, batch of one and sequence of one. Every weight matrix is still loaded from memory in full, and multiplied against a single vector — a matrix-vector product, with almost no arithmetic per byte moved. Decode is memory-bandwidth-bound, and its cost per token barely depends on the length of the context.

Both halves are measurable. Prefill, one pass over PP tokens:

prompt tokenssecondsms per token
160.351521.97
320.525416.42
641.049116.39
1281.655212.93
2563.096512.10

Decode, one token against a cache of CC:

cached tokensms for one token
16110.05
6497.57
256108.53
1024103.86

Read the second table twice. Going from 16 tokens of context to 1,024 — sixty-four times more history to attend over — changed the cost of a step by nothing measurable. Attention against the cache is real work, but it is dwarfed by the fixed cost of dragging half a billion weights through the memory bus to produce one vector. That fixed cost is the reason for everything in the next section.

These two phases are the origin of the two numbers every serving system reports. Time to first token is essentially prefill, and it grows with the prompt, which is why a long conversation feels slow to start. Tokens per second is 1/decode step1/\text{decode step}, and it is roughly constant, which is why the reply then flows evenly. A chat that starts slow and then streams smoothly is not a rendering trick. It is these two tables.

The cache trades arithmetic for memory, and the memory it wants is not small. For every token in the context, every layer holds one key vector and one value vector per key-value head:

bytes per token=2×L×Hkv×dhead×bytes per element\text{bytes per token} = 2 \times L \times H_{kv} \times d_{\text{head}} \times \text{bytes per element}

The 2 is for keys and values; everything else is the architecture. For the model measured throughout this chapter — 24 layers, 14 query heads, 2 key-value heads, head dimension 64 — in fp16 that is 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 bytes per token.

Formulae in this field have a habit of being off by a factor of two, so check it against the allocator rather than believing it:

TEXT
KV cache tensors per layer: (1, 2, 295, 64) float16
measured: 3,624,960 bytes for 295 tokens = 12,288 bytes/token
formula : 2 * 24 * 2 * 64 * 2                = 12,288 bytes/token

Exact, and it stays exact across every shape tried:

batchcontextmeasured cachepredictedpeak working memory
15126.0 MB6.0 MB15.4 MB
116,384192.0 MB192.0 MB207.3 MB
165,536768.0 MB768.0 MB793.7 MB
84,096384.0 MB384.0 MB401.5 MB
322,048768.0 MB768.0 MB794.2 MB
641,024768.0 MB768.0 MB797.0 MB
128512768.0 MB768.0 MB816.4 MB

The last three rows deserve a second look. Thirty-two users with 2,048 tokens each, sixty-four with 1,024, one hundred and twenty-eight with 512 — the cache is 768 MB in every case, because all three hold 65,536 tokens. The cache depends only on the total number of tokens resident, not on how they are distributed among users. That fact is the foundation of the batching section.

Chapter 9 introduced multi-query and grouped-query attention and deferred the reason to this chapter. The reason is that formula, and specifically the HkvH_{kv} in it.

Standard multi-head attention gives every query head its own key and value heads. The model here has 14 query heads; with full multi-head attention its cache would be 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 bytes per token — 84 KB instead of 12 KB, exactly seven times more, the ratio of query heads to key-value heads.

Multi-query attention1 takes this to the limit: all query heads share a single key-value head. Grouped-query attention2 is the compromise that won — a handful of key-value heads, each shared by a group of query heads — because MQA's quality loss was real and GQA's is not. Neither buys any arithmetic. They exist to divide that formula by an integer, and they spread across the industry the moment long contexts made the cache the binding constraint.

Which it does, quickly. For a 7B-class model with 32 layers and 8 key-value heads of dimension 128, the cache is 128 KB per token in fp16:

context tokensone user8 users64 users
4,0000.49 GB3.91 GB31.2 GB
32,0003.91 GB31.25 GB250.0 GB
128,00015.62 GB125.00 GB1,000.0 GB
1,000,000122.07 GB976.56 GB7,812.5 GB

That model's own weights are 13.0 GB in fp16, the figure in the table at the end of this chapter. So at a 128,000-token context, one user's cache is larger than the model. This is the arithmetic Chapter 16 turns into money, and it is why a long conversation is not merely a slow one — it occupies a fixed slice of a machine for as long as the request is alive.

Batching: the number that goes up and the number that goes down

Link to the section: Batching: the number that goes up and the number that goes down

Decode is memory-bound: the weights are dragged through the bus to produce one token, and the arithmetic units idle. So put more work in the same step. Run several requests at once, and the weights, read once, serve all of them. Measured on the same model, each request holding a 64-token cache and decoding one token:

batchlatency per stepthroughputlatency vs B=1
10.1286 s7.78 tok/s1.00x
20.1839 s10.88 tok/s1.43x
40.1909 s20.95 tok/s1.49x
80.2781 s28.76 tok/s2.16x
160.3430 s46.64 tok/s2.67x
320.6302 s50.78 tok/s4.90x

Read the two right-hand columns against each other, because they are the whole point. Going from one request to sixteen multiplies throughput by 6.0 and multiplies the wait for any individual request by 2.67. The batch made the server better and every user worse.

That is not a bug to be tuned away; it is the trade itself, and it has a name on each side. Latency is what a person waiting for a reply experiences. Throughput is what the invoice is divided by. No setting improves both.

Note also where it stops. From 16 to 32, throughput gains 9 % while latency nearly doubles: the step has stopped being memory-bound and become compute-bound, and past that knee the batch buys nothing. Every deployment has such a knee; its location has to be measured on yours, but its existence does not.

The naive way to batch is to collect BB requests, run them together, and return when all are done. But they do not finish together: some replies are twenty tokens and some five hundred. A fixed batch runs until its longest member finishes, and every finished request keeps occupying its slot, contributing padding, until then.

Take 64 requests with a realistic skew of output lengths — median 18 tokens, longest 231, 1,874 in total — and simulate both policies at the measured per-step cost for eight slots:

policywall clockthroughputmean latency per requestwasted slot-steps
static batches of 8176.9 s10.6 tok/s83.2 s3,214
continuous, 8 slots109.0 s17.2 tok/s8.1 s0

Throughput improves by 1.6x. Mean latency improves by more than ten times, because under static batching a request that finished in four steps still waits for a 231-token neighbour before anyone hears about it.

Continuous batching3 is the fix, and it is as simple as it sounds: the batch is not a group but a set of slots, and a slot that frees admits the next queued request on the very next step. The scheduler works at the granularity of one token rather than one request. Every serving stack in production does this now.

It has a second half, which is the cache. Slots that come and go leave cache memory fragmented, and reserving each slot its maximum possible context wastes most of the reservation. PagedAttention4 borrows the answer from operating systems: store the cache in fixed-size blocks with a block table per sequence, so a sequence's cache can be physically scattered while remaining logically contiguous — which also lets two sequences with a shared prefix share the blocks holding it. That is what vLLM is built on, and why a serving engine is a memory allocator with a transformer attached.

Quantization, and the first thing that goes wrong

Link to the section: Quantization, and the first thing that goes wrong

The other half of the bill is the weights themselves. Half a billion parameters at four bytes each is 1.98 GB; at two bytes, 0.99 GB; at one byte, 0.49 GB. Fewer bits per weight shrinks the model on disk, shrinks it in memory, and — because decode is bandwidth-bound — makes each step faster, since there are fewer bytes to move.

The simplest scheme is symmetric absolute-maximum quantization, and it fits in three lines:

quantize.pyPYTHON
qmax  = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax                        
Wq    = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale                                  # dequantized

Pick a scale so the largest weight maps to the largest integer, divide, round, store the integers and the scale. Reconstruct by multiplying back. Nothing about it is clever, and it works — right up until it does not.

Measured on the real weights of the model: all 168 projection matrices, 357.8 million parameters, relative error WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

schememean relative errorworst matrix
INT8, one scale for the whole matrix0.04000.1487
INT8, one scale per output row0.01000.0149
INT4, one scale for the whole matrix0.60260.9931
INT4, one scale per output row0.17900.2589
INT4, one scale per group of 1280.13230.1992
NF4, one scale per block of 640.09520.1205
INT3, one scale per group of 1280.30440.4123
INT2, one scale per group of 1280.77900.8076

The fourth row is the collapse. A relative error of 0.99 on the worst matrix means the reconstruction retains essentially nothing of the original — the matrix has been replaced by noise of about the right magnitude. The cause is visible in the same experiment on a single matrix:

TEXT
model.layers.12.mlp.down_proj.weight   (896 x 4864)
mean |w| 0.01386   std 0.01822   max |w| 0.43945   max/std 24.1
weights beyond 6 sigma: 692 of 4,358,144   (0.016 %)

One weight in six thousand sits beyond six standard deviations, and the largest is 24 out. With a single scale for the whole matrix, that one weight sets the step size for all 4.3 million of them. At 8 bits there are 256 steps and the typical weight still lands on a meaningful one. At 4 bits there are 16, the outermost reserved for a value almost nothing has, and the ordinary weights — which is all of them — round to two or three distinct levels.

Everything after that row is the same repair at different granularities: give the scale a smaller territory. Per output row divides the error by 3.4; per group of 128 consecutive weights divides it again. The cost is bookkeeping — a 16-bit scale per group of 128 is 4+16/128=4.1254 + 16/128 = 4.125 bits per weight instead of 4 — and it buys most of the gap back.

NF4 goes at it from the other side.5 The levels do not have to be equally spaced. Weights within a block are approximately normally distributed, so choose the sixteen levels as the quantiles of a normal distribution: dense near zero where the weights actually are, sparse in the tails where they are not. Same four bits, same block scaling, at a smaller block — 4.25 bits per weight against group-128's 4.125 — and the measured error drops from 0.1323 to 0.0952, 28 % lower. Part of that is the finer block and the rest is putting the levels where the mass is, and separating the two would need a third row.

Chapter 2's floating-point box ended with a promise: that this chapter would quantize weights to 8 and 4 bits and find a handful of outlier features refusing to be squeezed. Here they are, and they explain why "just round the numbers" was never going to work on activations.

The weights above were badly behaved. The activations are in a different league. Take an ordinary 84-token prompt, capture the residual stream at each layer, and measure the largest magnitude each of the 896 dimensions reaches:

layerlargest |h|median dimension's largest |h|ratiodimensions above 6x the median
16.190.33918x2
41543.481.550996x34
81571.631.4981049x36
121575.031.5461019x34
161579.601.617977x32
201577.982.361668x24
24204.4410.76019x12

Dimension 62 reaches 1,579.6 while the median dimension never exceeds 1.6. It is not a fluke of one token or one layer: the same dimension is there at layer 4 and still there at layer 20, with almost the same value. These are the outlier features,6 and they are systematic — a property of the trained model, not of the input.

The histogram of those 896 per-dimension maxima at layer 16 makes the shape unmistakable:

TEXT
     0 -      1 | ######################################## 254
     1 -      2 | ######################################## 283
     2 -      4 | ######################################## 226
     4 -      8 | ######################################## 93
     8 -     16 | ##################                       18
    16 -     32 | #########                                9
    32 -     64 | #######                                  7
    64 -    128 | #####                                    5
   128 -    256 |                                          0
   256 -    512 |                                          0
   512 -   1024 |                                          0
  1024 -   4096 | #                                        1

Nine hundred dimensions in a tidy pile below 8, nothing at all for three octaves, then one dimension alone at the far end. Now quantize that tensor to INT8 and count what happens:

schemerelative errordistinct integer levels used, whole tensor
one scale for the whole tensor0.108314 of 256
one scale per token (per row)0.0433158
whole tensor, 1 outlier dimension kept in fp320.044248
whole tensor, 4 outlier dimensions kept in fp320.027957
whole tensor, 16 outlier dimensions kept in fp320.0085102

Fourteen levels out of 256. The scale was set by 1,579.6, so every step is 12.44 wide, and the typical activation — median magnitude 0.26, ninety-ninth percentile 2.51 — has nowhere to land. Per dimension it is starker:

TEXT
single tensor-wide scale = 12.4378
  dim 826 (max |h| = 4.77):  1 distinct level out of 256
  dim 336 (max |h| = 1.62):  1 distinct level out of 256
  dim  96 (max |h| = 0.69):  1 distinct level out of 256

after excluding the top 4 dimensions, scale = 0.5749  (22x smaller)
  dim 826: 8 levels    dim 336: 4 levels    dim  96: 3 levels

One level. The whole dimension, every token, quantized to the same number. Eight bits were allocated and roughly zero were used, and the model reading those activations is handed a constant.

That measurement is the justification for every technique people actually use:

Keep the outliers out of it. LLM.int8()6 decomposes the matrix multiply: the dimensions with extreme magnitudes are computed in 16 bits, everything else in INT8, and the halves are summed. The table above is the receipt — removing four dimensions cuts the error by a factor of nearly four. SmoothQuant7 instead migrates the difficulty: divide the activations by a per-channel factor and multiply the matching weight column by it, which leaves the product unchanged and moves the outlier out of the tensor that cannot absorb it into the one that can.

Choose the rounding, do not just round. Nothing above asks what the matrix is for. GPTQ8 quantizes column by column and, after each, adjusts the remaining full-precision columns to compensate for the error already committed — minimising the error of the layer's output on real inputs rather than of its weights. AWQ9 notes that a small fraction of weight channels matter far more than the rest, finds them from activation statistics, and scales them up before quantizing so they land on finer levels. Both need a calibration set; neither needs gradients.

Show details

GGUF, and what a file format has to do with any of this.

GGUF is not a quantization method; it is the container llama.cpp uses, and the confusion in gguf vs gptq comparisons comes from treating the two as the same kind of thing. GGUF holds tensors, tokenizer, architecture metadata and chat template in one memory-mappable file, and carries a family of block schemes inside it — names like Q4_K_M encode bits per weight, block size, and whether some tensors are kept at higher precision.

The engineering difference that matters: GPTQ and AWQ produce weights optimised for a GPU kernel, while GGUF's schemes are decoded cheaply on a CPU with the file mapped rather than loaded. That is why the same nominal "4-bit 7B model" exists in both worlds at different sizes and different quality, and why the honest comparison is never the format — it is the measurement below, run on your own task.

Almost every article about quantization stops at the previous section: it explains the method, quotes a compression ratio, and asserts that quality is "largely preserved". Chapter 4 was about not fooling yourself, so let us find out.

Same model, weights quantized in place with each scheme, then three measurements: perplexity on 2,048 tokens of held-out English prose — here, the draft of this course, which is why the repository substitutes a fixed public-domain book and prints a table of the same shape with different numbers — a battery of 16 short factual questions with known answers under greedy decoding, and the fraction of tokens on which the quantized model agrees with the full-precision one given identical context.

schememean weight errorperplexityquestion batteryagrees with fp32
fp32 (reference)0.000023.0813/16100.0 %
INT8 per tensor0.040023.5813/16
INT8 per row0.010022.9613/1698.6 %
INT4 per tensor0.6026365,416,0000/16
INT4 per row0.179046.186/1658.3 %
INT4 group 1280.132331.0810/1671.5 %
NF4 block 640.095224.5511/1684.7 %
INT3 group 1280.3044213.090/165.6 %
INT2 group 1280.779026,325,4360/160.0 %

Four things in that table are worth stating plainly.

INT8 done properly is free. Per-row INT8 scores 22.96 against the reference's 23.08 — a gap of one part in two hundred, which is noise and should be read as "identical". Which way the noise points is not stable: on the repository's public-domain corpus the same two schemes come out 22.24 against 22.18: half that distance, and pointing the other way. It agrees with the full-precision model on 142 of 144 generated tokens. A quarter of the memory against the fp32 reference, half against the fp16 you would actually deploy, and no detectable cost. INT8 done carelessly is nearly free too: one scale per matrix costs 0.5 perplexity points and no battery answers. Eight bits is forgiving enough that granularity barely matters, which is exactly why people generalise from INT8 to INT4 and get hurt.

INT4 with one scale per tensor destroys the model. Perplexity 365 million: not degraded, annihilated. Granularity is then the whole game — per-tensor 365,416,000, per-row 46.18, per-group-of-128 31.08, NF4 24.55. Same four bits per weight, a factor of fifteen million between worst and best.

Perplexity is a coarse instrument and the battery a coarser one. Between NF4 and group-128 INT4 the perplexity gap is 6.5 points and the battery differs by one question — and Chapter 4's confidence interval says one question of sixteen distinguishes nothing whatsoever. There is a sharper demonstration than the interval: run the same battery with the model's stock repetition penalty switched off, which is what greedy decoding actually means, and those two rows swap places. One question of sixteen is not a small effect, it is no effect. Chapter 8's warning applies too: perplexity is comparable only between models sharing a tokenizer, so a number from someone else's write-up cannot be compared with yours.

The agreement column is the sharpest of the three, and nearly free: run the full-precision model greedily, then ask the quantized one, at every position, what it would have chosen given the same prefix. It has 144 independent observations instead of 16, needs no ground truth, and degrades smoothly where the battery degrades in jumps. It is also exactly the quantity the next section needs.

This is the promise Chapter 1 made about this chapter, arriving on schedule: the mathematics says a 4-bit model is possible, and the engineering decides whether it is usable.

Chapter 12 announced this and left the bill here.

The idea comes straight out of the prefill/decode split. Verifying a proposed sequence of γ\gamma tokens costs one forward pass over γ\gamma positions — a matrix-matrix product, barely more expensive than the pass over one. So:

A small, cheap model generates γ\gamma candidate tokens autoregressively.

The large model runs one forward pass over all γ\gamma candidates at once, producing what it would have said at each position.

Keep the longest prefix on which the two agree, plus the token the large model supplies for free at the first disagreement. Discard the rest and start again.

The output distribution is unchanged. With greedy decoding that is obvious — a token is accepted only if the target would have produced it. With sampling it requires a modified acceptance rule, and Leviathan et al. prove the resulting distribution is exactly the target's.10 This is the second exact optimisation in this chapter.

Everything therefore hinges on the acceptance rate α\alpha, which is measurable — it is the agreement column above, which is why it was computed there. Using each quantized model as a draft for the full-precision target, over 144 generated positions:

draft modelacceptancelongest accepted runexpected tokens per target pass, γ=4\gamma = 4
fp32 (the target itself)100.0 %485.00
INT8 per row98.6 %484.86
NF4 block 6484.7 %203.69
INT4 group 12871.5 %132.85
INT4 per row58.3 %72.24
INT3 group 1285.6 %21.06
INT2 group 1280.0 %01.00

The expected tokens accepted per verification pass, at draft length γ\gamma, is

E[tokens]=1αγ+11α\mathbb{E}[\text{tokens}] = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}

and the net speedup divides that by the draft's own cost, a fraction cc of the target per token:

acceptancec=0.05c=0.05, γ=4\gamma=4c=0.1c=0.1, γ=4\gamma=4c=0.2c=0.2, γ=4\gamma=4c=0.1c=0.1, γ=8\gamma=8
30 %1.19x1.02x0.79x0.79x
50 %1.61x1.38x1.08x1.11x
70 %2.31x1.98x1.54x1.78x
90 %3.41x2.93x2.28x3.40x

The bold entry is the one to remember: speculative decoding can make generation slower. At 30 % acceptance with a draft costing a fifth of the target, you pay for five forward passes and keep 1.4 tokens. The last column is the other trap — a longer draft only helps when acceptance is high, because the tail of a γ\gamma-token guess is almost never reached. At 90 % acceptance γ=8\gamma = 8 is worth 3.40x and at 30 % it is worth 0.79x: the same configuration, a win or a loss depending on a number measured on your traffic.

Quantization shrinks a model by storing the same function in fewer bits. Distillation shrinks it by training a smaller model to imitate a larger one11 — an idea that predates deep learning by nearly a decade.12

The subtle part is what the student learns from. Not the correct answer: it could have been trained on that directly. What the teacher adds is the whole distribution. Ask the model what follows a phrase and look past the argmax:

TEXT
"She poured the milk into the"
  ' jug' 0.1355   ' cup' 0.1051   ' bowl' 0.0605   ' large' 0.0380   ' milk' 0.0360

The hard label says jug and nothing else. The soft label says jug, and also that cup was nearly as good, bowl plausible, and large — an adjective, a completely different grammatical continuation — still live. That is the original argument: this is a 7, but it looks quite a lot like a 1, and the resemblance is information the hard label throws away.

It is also why distillation uses a temperature. Dividing the logits by TT before the softmax flattens the distribution and raises the relative weight of the runners-up: on this phrase, the ratio between the top token and the third falls from 2.24 at T=1T = 1 to 1.50 at T=2T = 2 — the square root of the first, which is what dividing the logits by two does to a ratio. Same ordering, more of the loss's attention on the near misses. The student's gradient carries the teacher's uncertainty and not only its verdict.

Everything in this chapter is now one sum:

memory=N×bytes per weightfixed+T×2LHkvdhead×bytesgrows with every token+runtime overheadcall it 1.5 GB\text{memory} = \underbrace{N \times \text{bytes per weight}}_{\text{fixed}} + \underbrace{T \times 2 L H_{kv} d_{\text{head}} \times \text{bytes}}_{\text{grows with every token}} + \underbrace{\text{runtime overhead}}_{\text{call it 1.5 GB}}

where TT is the total tokens resident across all concurrent requests. Applying it: the 7B and 70B rows assume 8 key-value heads of dimension 128, the 13B row full multi-head attention with 40 heads, which is how those generations of model were built — and it shows.

8 GB

modelprecisionweightsfree after overheadcontext tokens that fit
7Bfp1613.0 GBdoes not fit
7Bint86.5 GBdoes not fit
7Bint4 (g128)3.4 GB3.1 GB25,710
13Bint4 (g128)6.2 GB0.3 GB337
70Bint4 (g128)33.6 GBdoes not fit

16 GB

modelprecisionweightsfree after overheadcontext tokens that fit
7Bfp1613.0 GB1.5 GB11,972
7Bint86.5 GB8.0 GB65,378
7Bint4 (g128)3.4 GB11.1 GB91,246
13Bint812.1 GB2.4 GB3,136
13Bint4 (g128)6.2 GB8.3 GB10,822

24 GB

modelprecisionweightsfree after overheadcontext tokens that fit
7Bfp1613.0 GB9.5 GB77,508
7Bint86.5 GB16.0 GB130,914
7Bint4 (g128)3.4 GB19.1 GB156,782
13Bint812.1 GB10.4 GB13,622
13Bint4 (g128)6.2 GB16.3 GB21,308
70Bint4 (g128)33.6 GBdoes not fit

Look at the 13B row in the 8 GB table. The weights fit — 6.2 GB of 8 — so by the usual way of talking, a 13B model "runs on an 8 GB card". It has 337 tokens of context, which is not a conversation but barely a prompt. "Does it fit" is the wrong question. The right one is "with how much context, and for how many users at once".

Look also at the two 16 GB int8 rows. The 7B gets 65,378 tokens and the 13B gets 3,136 — a twenty-fold difference from 5.6 GB of extra weights, because the 13B here has multi-head attention and its cache costs 800 KB per token against the 7B's 128 KB. Two models of similar size, one unusable for long context, for a reason that appears in no model card's headline.

Thirteen chapters ago this was a perceptron with two weights and a bias. It is now a transformer that has been designed, trained, aligned, taught to spend compute on hard questions, and served at a measured cost per token — with no box left in it unopened.

That ends here, and it ends on purpose.

Chapter 14 begins with the model somewhere else. Not in your process, not in your memory, not in a variable you can print: on a machine you do not administer, behind an API key, a port and a bill. Everything measured here is still happening — the prefill still runs before the first token, the cache still grows with the conversation, the batch you are in still belongs to somebody else and still decides your latency — but from now on you observe it through a stream of Server-Sent Events, a finish_reason, and an HTTP 429 with a Retry-After header. The questions change with the vantage point: not how is this gradient computed but why did my invoice triple. So does the language, and Chapter 14 explains that rule rather than announcing it — up to here the code held weights, gradients, logits and tokenizer bytes; from there on it holds a connection, a retry, a cancellation and accumulated state. The thirteen chapters behind you are not discarded by the crossing. They are the description of what is running on the other side of the port.


Two omissions are deliberate. FlashAttention (Dao et al., arXiv:2205.14135) is not a different attention — it computes the same function by tiling the operation so the n×nn \times n score matrix is never written to memory, which is why the 67 MB in this chapter's second table is smaller in practice than the arithmetic suggests. And the kernels themselves are delegated: lecture 10 of Stanford's CS336 covers inference systems in the depth this does not attempt, and the llama.cpp repository and the GGUF specification are the primary sources for the CPU side.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). The paper is largely a memory-bandwidth argument, and reads as one.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Includes the uptraining recipe that converts an existing multi-head checkpoint, which is why GQA spread so fast.

  3. Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. and Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. Introduces iteration-level scheduling — continuous batching — and selective batching.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. The paper vLLM is built on; §3 is the operating-systems analogy in full.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 is defined in §3; the sixteen level values used in the measurement above are the ones this paper derives.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). The outlier-feature analysis in §4 is the source of the phenomenon measured above, including the finding that outliers emerge systematically at scale. 2

  7. Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022).

  8. Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022).

  9. Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023).

  10. Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Theorem 1 is the proof that the output distribution is unchanged; Chen et al. (arXiv:2302.01318) published the same idea independently.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). The temperature and the "dark knowledge" argument.

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, nine years earlier, for ensembles rather than transformers.

Ready to let LIA do the choosing?

Build with every AI model in one place — start free today.