AS
AS

Quantization: Squeezing a Model Into Fewer Bits

· 24 phút đọc

Llama 3 8B is eight billion numbers. Stored the standard way, two bytes apiece, that’s 16 GB, which is more than a 16 GB laptop has to spare once the operating system has taken its share. Yet models of exactly that size run on exactly that kind of laptop every day, at a perfectly usable speed.

The trick sounds like vandalism. Take nearly every one of those eight billion carefully trained numbers and round it to a 4-bit code. Not four digits: four bits, so at most sixteen possible values, about as coarse as a ruler with sixteen marks. (Each small group of weights gets its own ruler, and that detail turns out to matter a lot.) The file shrinks to under a third of its size, and the model still answers nearly as well as before.

Why doesn’t that wreck it? Where does it finally break? And before you download 40 GB, how do you work out whether a model will fit on your machine? The answers start with a little counting.

Fewer bits, fewer possible values

n bits can make 2n different patterns, so they can hold at most 2n distinct values: 65,536 for 16 bits, 256 for 8, just 16 for 4. Every bit you remove halves the menu.

The weightsWeightsThe learned numbers that scale inputs inside the network. Set during training, frozen at inference. of a trained model (Part 3) are usually stored as 16-bit floating-point numbers (the formats are called fp16 and bf16), two bytes each. That makes the memory bill easy to estimate:

memory ≈ parameters × bits per weight ÷ 8 bytes
÷ 8 because a byte is 8 bits

QuantizationQuantizationStoring weights with fewer bits (8, 4…) to save memory and speed up inference, at some cost in accuracy. stores the weights with fewer bits, typically 8 or 4, by rounding each one to the nearest value on a coarser grid. Halve the bits and you halve the memory. The price is that every weight is now slightly wrong. Fewer bits mean bigger errors, and if you push too far the model degrades badly. Most of this post is about why “slightly wrong” is so often good enough, and how to tell when it isn’t.

Rounding a weight by hand

Here’s the simplest scheme that works well: symmetric 4-bit integers, “int4” for short. A 4-bit integer holds the whole numbers from −8 to 7. Weights aren’t whole numbers; they’re fractions like 0.23 or −0.41. (Real LLM weights are often smaller still, a few hundredths, but the method works the same at any size.) So we stretch the integer ruler to fit the weights.

Take a small block of weights and find the largest one by size, ignoring its sign. Say it’s 0.8. Map it to the end of the ruler, 7. That fixes the distance between neighboring marks, which is called the scale:

s = max |w| ÷ 7 = 0.8 ÷ 7 ≈ 0.1143
the scale is stored once per block, as a 16-bit number

Each weight is then stored as an integer: divide it by s and round. To use the weight later, multiply back. Two weights from this block, worked in full:

  • 0.23 ÷ 0.1143 = 2.01, which rounds to 2, stored as the bit pattern 0010. It comes back as 2 × 0.1143 = 0.2286, off by about 0.0014.
  • −0.8 ÷ s = −7 exactly (s is 0.8 ÷ 7, so this one can’t miss), stored as 1001. It comes back as −7 × s = −0.8, with no error at all.

Two facts fall out of this. The block’s largest weight always comes back exactly, because it set the scale. And no weight can be off by more than half a step, s/2 ≈ 0.057, because rounding never moves a number more than halfway to the next mark. (You may have spotted that −8 never gets used: the ruler runs from −7 to 7, so the largest weight lands on an end mark whatever its sign. Some formats, such as llama.cpp’s Q4_0, use the sixteenth code by mapping the largest weight to −8.)

Interactive
Round every weight to the nearest rung
Bits per weight
16 codes; this scheme uses −7…7
0.80.40.0−0.4−0.8−7−6−5−4−3−2−101234567qww₇
true weight stored value (on a rung) rounding errorrungs: q × s
Weight 7: 0.23, stored as 2, restored as 0.2286, error 0.0014
the arithmetic for w₇
scales = max|w| ÷ 7 = 0.80 ÷ 7 = 0.1143
store0.23 ÷ 0.1143 = 2.01→ round →q = 2bits 0010
restore2 × 0.1143 = 0.2286error 0.0014
Step s
0.1143
144 bits = 4.5 per weight
Mean error
0.0321
average |w − restored|
Worst error
0.0557
never above s/2 = 0.0571
Rounded to 0
5 of 32
weights erased
Thirty-two weights from one block, rounded live with a symmetric integer grid. Change the bits, then tap or drag across the bars (or focus the chart and use the arrow keys) to see any weight's arithmetic. Weight 7 (0.23) and weight 21 (−0.8, the block's largest) are the worked example from the text.

Twenty-seven. At 2 bits the step is 0.8, so anything smaller than 0.4 rounds to zero, and in a block shaped like this, where most weights are small, that’s nearly all of them. The block is mostly erased.

Now walk back up. Each bit you add roughly halves the step, and the errors shrink with it. At 4 bits the average error is 0.032, about a tenth of a typical weight in this block. At 8 bits it’s 0.0014 and the red slivers vanish: 255 levels make a very fine ruler, which is why 8-bit quantization is usually close to lossless. Also look at the small print under “Step s”: 32 weights at 4 bits take 144 bits, because the block’s 16-bit scale has to be stored too. That’s 4.5 bits per weight, not 4. Hold on to that number.

One big weight spoils its block

Why bother with small blocks? You could compute one scale for a whole weight matrix, millions of weights, and the scale would cost next to nothing per weight.

The trouble is what sets the scale: the single largest weight. Trained weights mostly cluster near zero, but a few are much bigger than the rest, and one of those stretches the ruler for everyone who shares its scale. The marks spread apart to make room for the giant, and the small weights, which are most of them, fall into the gap around zero.

Interactive
One big weight spoils its whole block
Block size
00000000000000000000000000000000000
small → large error one block, one scale
Bits per weight
4.0
4 + about 0 for the scale
Rounding noise
12%
typical error ÷ typical weight
Rounded to 0
35 of 256
Step s
0.080
one step for all
noise vs storage, one dot per block size
0%25%50%44.556bits per weight, scales included →832128whole
256 bell-shaped weights rounded to 4 bits, live. Each square is one weight, shaded by how far rounding moved it; a 0 means it was rounded to zero. Turn on the outlier with one shared scale, then shrink the blocks. “Whole tensor” means one scale for an entire matrix of millions of weights, so its overhead is about zero.

With the outlier and one shared scale, 155 of the 256 weights round to zero, and the rounding noise jumps from 12% of a typical weight to 50%. One number did that. Shrink the blocks to 32 and the damage is fenced into the outlier’s own block: everywhere else the step drops back to about 0.06, and the noise falls to 22%. With no outlier at all it would be 9%, so most of what’s left comes from that one spoiled block. Smaller blocks do better still, but look at the cost along the bottom of the chart: every block carries its own 16-bit scale, so blocks of 8 spend 2 extra bits on every weight. Spent instead on 5-bit codes in blocks of 16, the same 6 bits per weight would cut the noise to 6%, less than half what blocks of 8 manage.

Practical schemes settle in between. The common choice is the one you just used: blocks of 32 weights, one 16-bit scale each. That adds 16 ÷ 32 = 0.5 bits per weight, so “4-bit” with blocks of 32 really means about 4.5 bits per weight. (Formats with blocks of 128 pay a little over 4.1.)

Will it fit? Doing the memory math

Now the payoff. Here is the formula applied to an 8-billion-parameter model like Llama 3 8B:

PrecisionArithmeticMemory
16-bit8 × 10⁹ × 16 ÷ 816 GB
8-bit8 × 10⁹ × 8 ÷ 88 GB
4-bit8 × 10⁹ × 4 ÷ 84 GB raw, about 4.5–5 GB in practice

Why does 4-bit land near 5 GB rather than 4? Two reasons, and you’ve already met the first. The block scales add half a bit per weight: 4.5 bits, so about 4.5 GB. Second, some layers are more sensitive to rounding than others, so practical recipes often keep them at higher precision. The usual suspects are the embedding table, which turns token IDs into vectors, and the output layer, which turns the final vector into a score for every token. In Llama 3 8B, each of those holds 4,096 numbers for each of its 128,256 tokens: 128,256 × 4,096 ≈ 525 million weights, about a billion for the pair. Keep them at 8 bits instead of 4.5 and they cost an extra half a gigabyte: about 5 GB in total.

The weights aren’t the only thing in memory, though. While a model generates, it keeps the KV cacheKV cacheStored attention keys and values from earlier tokens. Speeds up generation within a request.: the attention keys and values of every token in the context, saved so they aren’t recomputed at each step. In Llama 3 8B that’s the 131,072 bytes per token you worked out in Part 4: 2 (a key and a value) × 32 layers × 8 shared key-value heads × 128 numbers × 2 bytes. That’s nothing for one token and a lot for the 128k tokens (131,072) that Llama 3.1 8B, the same shape with a longer window, can hold. Every conversation served at the same time needs its own cache. And on top of all that come the activations (the intermediate results of the current step) and the runtime’s own overhead.

Playground
Will it fit?
Model
Weights
8k tokens
the KV cache holds every token in the window
Conversations at once
Total memory
18.1GB
weights alone: 16.0 GB (16.00 bits per weight)
Units
Where the memory goes
Weights 8B × 16 bits ÷ 816.0 GB
Block scales none at 16 bits—
Sensitive layers nothing extra: already 8 bits or more—
KV cache 131 kB per token × 8,192 tokens × 11.07 GB
Runtime & activations about 1 GB (illustrative)1.00 GB
Does it fit?
16 GB laptop room 12.9 GBshort 5.19 GB
24 GB GPU room 25.8 GB7.70 GB spare
80 GB GPU room 85.9 GB67.8 GB spare
8 × 80 GB server room 687 GB669 GB spare
Room: the laptop's 16 GiB of RAM minus about 4 GiB for the OS and apps (assumed). Memory chips come in powers of two, so a “24 GB” GPU holds 24 GiB (25.8 GB). The server assumes the model splits perfectly across 8 GPUs.
The formula from the text, plus everything else that has to live in memory. Start with 8B at 16 bits, step down to 4, then drag the context to 128k. Parameter counts are nominal and layer shapes follow Llama 3 and 3.1 models of each size (128k contexts need 3.1); the runtime figure and the laptop's free memory are illustrative.

At 128k tokens the cache weighs 17.2 GB, more than three times the 4-bit weights, and quantizing the weights did nothing for it. (Some engines can store the KV cache in 8 or 4 bits as well: the same trick, applied to a different pile of numbers.) At 8 bits the 70B model fits an 80 GB card with a 16k context, but at 32k this calculator says it misses by about 0.2 GB, close enough that the real answer depends on your engine. At 4 bits there’s room to spare. Now switch back to the 8B model, set the context to 8k and serve 16 conversations at once: 16 × 8,192 = 131,072 tokens, the same 17.2 GB of cache as one 128k conversation.

One last trap before you compare any of this with your hardware: units. The formula counts billions of bytes, but memory chips come in powers of two, so a “16 GB” laptop and a “24 GB” GPU really hold 16 and 24 gibibytes (GiB, 230 bytes each). The 16-bit Llama 3 8B, 16 × 10⁹ bytes, is 16 GB but only about 14.9 GiB. Some tools count in GiB while still printing “GB”, so the same model looks 7% smaller in one window than in another. Flip the Units switch in Fig. 3: every number shrinks by 7%, and nothing that fit stops fitting. Pick one unit and convert everything to it before you compare.

Smaller is also faster

Here’s a puzzle. A 4-bit model has to unpack every weight and multiply it by its block’s scale before using it, which is extra work. Yet it usually generates text faster than the 16-bit original. Why? To produce one token, the model runs once from bottom to top, and every weight has to travel from memory to the chip’s arithmetic units. For a single conversation that trip, not the arithmetic, is the bottleneck: Part 3 calls the decode phase memory-bandwidth bound. So there’s a simple ceiling on speed:

tokens per second ≤ memory bandwidth ÷ bytes of weights
a ceiling for one conversation, when every token reads every weight; real engines land below it

Take an illustrative 1,000 GB/s of bandwidth, the class of a high-end graphics card. With 16 GB of weights the ceiling is about 62 tokens per second; with 5 GB it’s about 200. Fewer bytes per weight, more tokens per second.

It’s a ceiling, not a promise. The 4-bit codes have to be unpacked and multiplied by their scales on the fly, which costs some compute. And the gain shrinks where arithmetic is the bottleneck instead of memory: reading a long prompt (the prefill), or serving many conversations at once. That’s where hardware with native low-precision arithmetic, such as the FP8 units on recent data-center GPUs, earns its keep, provided the activations are rounded to 8 bits too, not just the weights.

How much does rounding hurt?

Each weight’s error is small, but there are billions of them, and their errors flow through dozens of layers, each feeding the next. Whether they cancel out or pile up into a different answer isn’t something you can read off any single weight. You have to run the model and look.

So let’s run one. The figure below holds a real neural network, tiny by LLM standards at 1,185 parameters, trained to say which of two interleaved spirals a point belongs to. The task needs precise boundaries, so it shows damage quickly. Its weights are rounded live with the same scheme as Fig. 1 and Fig. 2, blocks of 32 and all (its 65 biases stay at full precision, as is common).

Playground
Round a real network and watch it forget
Weights rounded to
Network
spiral 1 spiral 2 answers “spiral 1”answer changed now wrong
Test accuracy
100%
400 held-out points
Area that flipped
0.0%
vs the full-precision original
For LLMs: the rule of thumb
What practitioners report, not data. The cliff comes sooner for small models and for math or code.
original
near-lossless
sweet spot
often a cliff
16
8
6
5
4
3
2
bits per weight
A real network (1,185 parameters) that learned to tell two spirals apart, with its weights rounded live (blocks of 32, one scale each). Hatched red is where its answer changed; ✕ marks test points it now gets wrong. A, B and C are the same recipe trained from three random starts. Bottom: the rule of thumb practitioners report for LLMs, not data.

At 8 bits the test scores don’t budge and only hairline slivers of the map flip. At 6, only C slips: it loses a single test point of 400. At 5 bits A is still perfect, while B misses 9 points and C misses 6. At 4 the three part ways: A makes its first mistakes but still gets 99% of the test points right, B gets 92% and C 81%, though all three came out of the same recipe, trained from different random starting weights. At 3 bits all of them fall apart, and at 2 they do about as well as a coin toss.

Why does it hold up so well, then collapse so fast? The network answers by computing a score and checking its sign, so a point flips only if rounding pushes its score across zero: what matters is the nudge compared with the margin. At 6 bits, rounding moves network C’s scores by about 7% of their typical size, while half the test points score beyond ±9.4, so nothing happens to them. The one point C loses was a near-tie to begin with: its score went from −0.75 to +1.4. At 4 bits the nudges grow to between 35% (A) and 83% (C) of a typical score, and even confident points flip. An LLM faces the same test at every token: when the next word wins by a mile, a little noise changes nothing, but when two candidates are nearly tied, it can tip the choice.

LLMs show the same shape, and the practical rule of thumb is: 8-bit is usually near-lossless, 4-bit is the common sweet spot, and at 3 bits and below quality often drops sharply, especially for small models and for math or code. Why small models? A common explanation is that they have less spare capacity to absorb the damage. Math and code are less forgiving: a flipped near-tie in prose usually still reads fine, but in code one wrong digit or bracket breaks the whole answer.

The toolbox: PTQ, QAT and tiny floats

Everything so far has been post-training quantization (PTQ): take a finished model and round its weights, with no retraining. The simplest form is the round-to-nearest you’ve been doing. Two well-known methods do better by watching the model at work on a small calibration set of sample inputs:

  • GPTQ rounds a layer’s weights one column at a time, and after each step adjusts the weights it hasn’t rounded yet to cancel the error it just introduced. Say two weights feed the same output, and their inputs always arrive nearly equal. If rounding pushes the first one down by 0.05, nudging the second up by 0.05 before rounding it cancels the damage to the output. GPTQ does that bookkeeping for a whole layer, using the calibration inputs to learn which inputs move together.
  • AWQ (activation-aware weight quantization) notices that a small share of weights matter far more than the rest: the ones that multiply consistently large inputs, which amplify their errors. It scales those weights up before rounding, giving them finer steps relative to their size, and scales their inputs down to match, so the layer computes the same thing with its important weights protected.

The files you’ll most often download to run a model locally are neither. GGUFGGUF / GPTQ / AWQCommon formats and methods for quantized models. is the file format of llama.cpp, the engine inside many local-model apps. Its standard recipes are PTQ computed from the weights alone, no calibration needed (an optional “importance matrix” built from sample text can refine them), and its type names spell them out. Q8_0 is plain round-to-nearest: 8 bits in blocks of 32 with one 16-bit scale each, 8.5 bits per weight, Fig. 1’s scheme at 8 bits. Q4_K_M is a 4-bit “k-quant”: each block of 32 stores a scale and a minimum (an offset, so its grid needn’t be centered on zero), the blocks are grouped into super-blocks of 256 whose scales and minimums are themselves quantized to 6 bits, and the M (medium) mix keeps some sensitive tensors at higher precision. It averages a little under 5 bits per weight, which is how an 8B model becomes a file of about 5 GB.

The second family is quantization-aware training (QAT). Instead of rounding a finished model and hoping, you train it, or briefly fine-tune it, with the rounding simulated in every forward pass, so the weights settle into values that survive it. There’s a wrinkle. Rounding is a staircase, flat almost everywhere, so its gradient is zero and backpropagation (Part 6) would learn nothing through it. The standard fix is to pretend, on the backward pass only, that the rounding isn’t there. QAT usually gives the best quality at low bit-widths, but it costs a training run, so it’s mostly done by the people who make the model.

The third idea changes the grid itself. Integers are evenly spaced, but weights aren’t evenly spread: most are small. Low-precision floating-point formats put more of their levels near zero. A float is scientific notation in binary: a few significant bits (the mantissa) times a power of two (the exponent). Small exponents give closely spaced values near zero and large ones give widely spaced values far out, so in effect every number carries its own little scale. FP8 fits a sign, an exponent and a mantissa into 8 bits (two common layouts, E4M3 and E5M2, trade precision for range), and recent data-center GPUs compute in it natively. FP4 (the layout called E2M1) has just sixteen codes, ±0, 0.5, 1, 1.5, 2, 3, 4 and 6, and is used with a scale per small block, just like int4. Look at the spacing: half-steps near zero, a step of 2 at the far end.

Interactive
Sixteen codes, two ways to space them
Block
Random
int4 15 evenly spaced levels, step = max ÷ 7
−0.800.8
FP4 ±0, 0.5, 1, 1.5, 2, 3, 4, 6 × (max ÷ 6): bunched near zero
−0.800.8
a weight its rounding error a levelworst case between levels
int4 error
0.0359
root-mean-square
FP4 error
0.0305
root-mean-square
FP4 wins: 15% less error. Mostly small weights, plus 0.72 and −0.8 far out: on this block FP4’s fine steps near zero pay off.
Your random drawsbell-shaped: no draws yetheavy-tailed: no draws yet
The same block on both grids, 4 bits and one scale per block, computed live. Each dot is a weight at its true value, lifted by its rounding error (red); each grey tent is the most error possible between two levels. Try the three blocks, then draw random ones: no spacing wins everywhere.

On the flat block FP4 loses badly, with 69% more error: its wide outer steps sit right where most of the values are. On Fig. 1’s block, many small weights plus 0.72 and −0.8, FP4 has 15% less error, and with an outlier its lead grows to 34%. Neither grid is better everywhere; each is a bet on where the values are. Fig. 1’s block is a lucky one for FP4, though. Over thousands of random bell-shaped blocks of 32, int4 comes out slightly ahead (FP4 has about 4% more error and wins about 4 blocks in 10), while on heavy-tailed blocks, a few large values among many tiny ones, FP4 wins about 7 in 10. Trained networks often have tails like that. The 4-bit base model in QLoRAQLoRALoRA fine-tuning on top of a 4-bit quantized base model. (Part 9) makes the same kind of bet with its own format, NF4, whose sixteen levels are spaced to match a bell curve.

Check yourself
Can you predict it?
1.
A block's largest weight is 0.6. Using symmetric int4 (scale = max ÷ 7), what does the weight 0.25 come back as?
2.
In a block of 32 weights, 31 lie between −0.2 and 0.2 and one is 3.5. With symmetric int4 and one scale for the block, what happens to the 31?
3.
A colleague says: "A 70B model at 4-bit is 70 × 4 ÷ 8 = 35 GB, so it fits on our 40 GB GPU with room for a 32k context." What's the biggest problem?
4.
Same GPU, same 8B model, one user. Why does the 4-bit version usually generate tokens faster than the 16-bit one?
5.
Your 4-bit 8B model scores within 1% of the 16-bit original on a general chat benchmark. You want it to write SQL. What's the right next step?

That completes the model itself: pre-trained, shaped, adapted cheaply and now cheap to run. But a trained model is frozen, and it only knows its training data. To answer questions about your private documents or yesterday’s news, or to take actions, it has to be connected to outside systems. The next five parts cover how, starting with turning text into vectors you can search by meaning.

Share: