Contents Working with Large Language Models Code Course home

Chapter 24Efficiency: Smaller, Cheaper, Faster

The other axis

Most of this book has followed capability, through larger models, longer contexts and better scores. There is a second axis, and outside research it is usually the binding one, because a model that cannot run within your budget, memory or latency is not available to you at any quality.

This chapter is about that axis, and it divides cleanly into three questions. How do I adapt a model without paying for full fine-tuning, how do I make the model smaller, and how do I make it answer faster?

Adapting without paying for it

Full fine-tuning updates every weight, and the memory cost is worse than the parameter count suggests. For a 77 billion parameter model in half precision:

what must be held memory
weights, fp16 14 GB
gradients, fp16 14 GB
Adam’s two moments, fp32 56 GB
total, before activations 84 GB

Each fp32 quantity is four bytes per parameter, so one moment over 77 billion parameters is 2828 GB and two are 5656. Mixed precision training usually keeps an fp32 master copy of the weights as well, which is another 2828 GB and takes the total past 110110; the table leaves it out, so treat 8484 GB as the floor rather than the figure.

Either way it is several high-end accelerators to fine-tune a model that fits on one for inference, and the result is a whole new 1414 GB of weights per task.

LoRA

Low-rank adaptation (Hu et al. 2022) rests on one observation, which is that the change a fine-tune makes to a weight matrix has low intrinsic rank even though the matrix does not. So do not learn the change directly but factor it:

W′=W+BA,B∈ℝd×r,A∈ℝr×k,r≪min⁡(d,k).(24.1)\begin{equation} W' \;=\; W \;+\; BA, \qquad B \in \mathbb{R}^{d \times r}, \quad A \in \mathbb{R}^{r \times k}, \quad r \ll \min(d, k). \label{eq:lora} \quad\text{(24.1)} \end{equation}

Freeze WW entirely and train only AA and BB. The arithmetic is the argument, since a full matrix has dkdk parameters while the adapter has r(d+k)r(d + k).

rank rr full matrix adapter share
1 16,777,216 8,192 0.05%
4 16,777,216 32,768 0.20%
8 16,777,216 65,536 0.39%
16 16,777,216 131,072 0.78%
64 16,777,216 524,288 3.13%

Those figures are for d=k=4096d = k = 4096, a typical attention projection. At r=8r = 8 you train four tenths of one per cent of the weights, and the optimiser state shrinks in the same proportion, which is where most of the memory in the table above went.

Three practical consequences follow, and the third is the one people underrate. You can fine-tune a large model on one accelerator. An adapter is megabytes rather than gigabytes, so you can keep hundreds of them. And because Equation (24.1) is an addition to a frozen WW, adapters can be swapped at serving time without reloading the base model, which gives one model in memory and many behaviours.

QLoRA combines the idea with a quantised base model and puts fine-tuning a 6565 billion parameter model within reach of a single accelerator.

Making the model smaller

Quantisation

Weights are usually trained in 1616 bits and they rarely need 1616 bits to be used.

precision bytes per parameter a 7B model
fp32 4 28.0 GB
fp16 2 14.0 GB
int8 1 7.0 GB
int4 0.5 3.5 GB

The int4 row is the interesting one, because a model that needed a data-centre card at fp16 now fits on a consumer GPU. Quality loss at int8 is usually indistinguishable in practice, and at int4 the loss is measurable but often acceptable, which is almost always a better trade than moving to a genuinely smaller model.

What the rounding actually does

A quantised weight is an integer code and a scale shared across a group of weights. For a symmetric scheme the scale is set by the largest magnitude in the group,

S=maxi|wi|qmax,qi=round⁡(wiS),ŵi=qiS,(24.2)\begin{equation} S = \frac{\max_i |w_i|}{q_{\max}}, \qquad q_i = \operatorname{round}\!\left(\frac{w_i}{S}\right), \qquad \hat{w}_i = q_i S, \label{eq:quantise} \quad\text{(24.2)} \end{equation}

where qmaxq_{\max} is 127127 for int8 and 77 for int4. Take eight weights from one row of a hidden layer and put them through Equation (24.2) twice:

ww int8, S=0.004016S = 0.004016 int4, S=0.072857S = 0.072857
2-3(l)5-6 qq error qq error
+0.12+0.12 +30+30 0.000470.00047 +2+2 0.025710.02571
−0.45-0.45 −112-112 0.000240.00024 −6-6 0.012860.01286
+0.33+0.33 +82+82 0.000710.00071 +5+5 0.034290.03429
−0.08-0.08 −20-20 0.000310.00031 −1-1 0.007140.00714
+0.51+0.51 +127+127 0.000000.00000 +7+7 0.000000.00000
−0.22-0.22 −55-55 0.000870.00087 −3-3 0.001430.00143
+0.07+0.07 +17+17 0.001730.00173 +1+1 0.002860.00286
−0.37-0.37 −92-92 0.000550.00055 −5-5 0.005710.00571
rms error 0.000790.00079 0.016190.01619

Halving the bit width multiplies the root mean square error by 20.620.6, because the step between neighbouring codes grows from 0.0040160.004016 to 0.0728570.072857. The largest weight is exact in both columns, since it is what defines the scale.

Why one weight can ruin a row

Equation (24.2) has a weakness that matters enormously in practice. The scale is set by the largest magnitude, so a single extreme value drags it upward and every other weight in the group inherits a coarser step. Trained transformers reliably produce such outliers in a few coordinates.

Add one weight of +4.20+4.20 to the same row and the int8 scale moves from 0.0040160.004016 to 0.0330710.033071, which is 8.28.2 times coarser. The outlier itself is then stored exactly, with no error at all, while the small weights that make up the rest of the row carry up to 17.3217.32 per cent error. There are far more of them, and they are the ones doing the work.

The repair is to stop sharing one scale across the whole row:

scheme scale rms error over the row
one scale for the whole row 0.0330710.033071 0.009960.00996
two scales, normal weights 0.0040160.004016 0.000740.00074
two scales, the outlier 0.0330710.033071

Thirteen times less error, for a cost that is easy to miss. The scales now have to be stored too, and one fp16 scale for every group of 6464 weights adds two bytes per 6464, which is a quarter of a bit per weight. So int4 with group scales really costs 4.254.25 bits, and every quantisation scheme in use is negotiating that same trade between the size of a group and the damage one outlier inside it can do.

Try it yourself. code/worked_examples/quantization.py produces every table in this section. Run --round for the two precisions, --outlier for the damage one weight does and --channel for the repair. Change the outlier from 4.204.20 to 1.001.00 and watch how much of the error disappears.

Run it in ColabNotebookSource

Distillation and pruning

Distillation trains a small student to match a large teacher’s output distribution, and the teacher’s full distribution carries more information than a hard label does, because it says which wrong answers were nearly right.

Pruning removes weights or whole structures that contribute little, where unstructured pruning gives better compression ratios while structured pruning gives speedups on real hardware, because it removes shapes rather than scattered entries.

The general finding across all three techniques is the same. Models are substantially over-parameterised for inference, and most of that slack can be recovered without retraining from scratch.

Making inference faster

Where the time actually goes

Generation has two phases with completely different cost profiles, and conflating them causes most confusion about latency. Prefill processes the whole prompt in parallel, so it is compute-bound and fast per token. Decoding produces one token at a time, each depending on the last, so it is memory-bound, because every step reads the entire weight matrix to produce one token. The second phase therefore dominates wall-clock time on any answer of reasonable length, and it is the one worth optimising.

The KV cache, and its cost

Each decoding step recomputes attention over the whole prefix, so caching the keys and values avoids that, and it is the single most important inference optimisation there is. It also becomes the memory problem. For D=4096D = 4096 and L=32L = 32 layers in fp16, where one token costs 2×L×D×22 \times L \times D \times 2 bytes, or half a mebibyte:

context length KV cache
2,048 1.07 GB
32,768 17.18 GB

At long context the cache can exceed the weights, since 17.1817.18 GB against the 1414 GB of fp16 weights is a cache larger than the model. That is why grouped-query attention exists, sharing key and value projections across heads, and it is one of the few architecture changes since the original transformer driven purely by serving cost.

Batching, and the quadratic wall

Throughput comes from batching, because decoding is memory-bound and processing many sequences at once costs little more than one. Serving systems therefore batch aggressively and schedule continuously rather than waiting for a batch to fill.

Underneath all of it sits the O(n2)O(n^2) of Chapter 16, where quadrupling the context multiplies the attention cost sixteen-fold, and every long-context technique is an attempt to pay less than that. FlashAttention does not change the complexity but the memory traffic, computing attention in tiles that fit in fast on-chip memory, which is a large practical speedup for no change in the result.

Living with a deployed model

Two effects only appear after shipping, and neither is a modelling problem. Drift is the first, because the world changes and the training data does not, so a model’s accuracy on live traffic decays even though the model has not changed, and the response is monitoring rather than retraining on a schedule. Feedback loops are the second, because a deployed model influences the data it will later be trained on, so a recommender changes what people read and a text model changes what people write, and that loop is now measurable in web corpora.

The engineering discipline here is the empirical method of Chapter 14 applied to a system rather than an experiment. State the metric, hold out the evaluation, log the failures, and look at them.

Further reading.

Hu et al. (2022) introduce LoRA and its rank ablations are worth reading for the intuition about intrinsic dimension. Dettmers et al. (2023) combine it with quantisation. Dao et al. (2022) explain why memory traffic rather than arithmetic is the bottleneck. LoRA is peft, quantised loading is bitsandbytes, and vLLM is the serving stack that made paged attention and continuous batching standard.

Count the adapter. For d=k=4096d = k = 4096, compute the LoRA parameter count at r=4,8,32r = 4, 8, 32 and express each as a share of the full matrix. Then repeat for d=k=768d = k = 768. Why is the share so much larger on the smaller matrix, and what does that imply about where LoRA pays best?

The memory budget. Work out the full fine-tuning memory for a 1313 billion parameter model in fp16 with Adam, then the same figure with LoRA at r=8r = 8 applied to attention projections only. State every assumption you made.

Choose a precision. A 77B model must run on a card with 88 GB of memory, leaving 22 GB for activations and the KV cache. Which precision can you use? Now the context must be 8,1928{,}192 tokens. Recompute and say what has to give.

Prefill against decode. Explain why the first phase is compute-bound and the second memory-bound. Then predict which phase dominates for a 2,0002{,}000 token prompt with a 2020 token answer, and for a 5050 token prompt with a 2,0002{,}000 token answer.

Size the cache. For D=5120D = 5120, L=40L = 40 and fp16, compute the KV cache at 4,0964{,}096 and 65,53665{,}536 tokens. At which length does it exceed the weights of a 1313B model? Name one architectural change that reduces it.

Pick the technique. For each constraint, choose one method from this chapter and justify it: (a) one accelerator, must serve forty customer- specific behaviours; (b) model must run on a phone; (c) latency is fine but throughput is a tenth of what is needed; (d) context must grow from 4k to 64k.

The exact weight is the useless one. In both columns of the quantisation table the weight +0.51+0.51 has zero error, because it sets the scale. Explain why the most faithfully stored weight in a group is always the one you learn least from. Then quantise the same eight weights after deleting +0.51+0.51 and report what happens to the other seven.

Price the outlier. With one scale for the row, the outlier costs the small weights up to 17.3217.32 per cent error. Compute the rms error for group sizes of 88, 44 and 22 weights, counting the stored scales each time at two bytes per group. Plot error against bits per weight and say where you would stop.

Where int4 stops paying. Halving the bit width multiplied the rms error by 20.620.6 on this row. Work out what that factor would be for a row with no outlier and a flat spread of weights, and explain why real transformer rows behave worse than your calculation.

Make the library agree with your count. Wrap a single 4096×40964096 \times 4096 linear layer in a peft LoraConfig at r=8r = 8 and call print_trainable_parameters. It reports 65,53665{,}536 trainable weights, exactly the figure Problem 1 asks you to compute. It also reports the fraction as 0.38910.3891 per cent where this chapter says 0.39060.3906. Neither is a rounding error. Find the difference in the two denominators, and say which one you would quote to someone deciding whether to fine-tune.

Code for this chapter

Each one reproduces the tables above. Open it in Colab, change an input and watch which numbers move.

From Natural Language Processing: From Corpus Statistics to Grounded LLMs, by Ramaseshan Ramachandran.