Quantizing a language model means storing its numbers in fewer bits, usually 8 or 4 instead of 16. Each step of generation then reads fewer bytes from memory, so it runs faster, and the model fits on fewer GPUs. The simplest way is to round each weight to the nearest of a few evenly spaced levels, and for large models that often fails badly. This section builds the methods that make 8-bit and 4-bit models work, the ones frontier models ship with. They share one idea. A weight matters only through the outputs it helps compute, so a good method preserves each layer's outputs on real inputs, not each weight on its own.
Here is the gap that idea closes. OPT-175B is an open language model from Meta with 175 billion parameters. Round each of its weights to the nearest level of a 4-bit grid, with one grid per output channel. This is called round to nearest, or RTN. The model's perplexity on WikiText-2, a set of Wikipedia articles, rises from 8.34 to 10.54. (Perplexity is the exponential of the average next-token loss, so lower is better.) That is worse than the 16-bit OPT-13B, a model thirteen times smaller, which scores 10.13.
Now quantize the same model to the same grid in a different way. First run 128 passages of ordinary text through the model, to see what each layer's inputs look like. Then pick each weight's level with that knowledge. This method is GPTQ (Frantar et al. 2022), and it brings the perplexity to 8.37. At 3 bits the gap is enormous, about 7,300 for rounding against 8.68 for GPTQ (Table 3 of the paper). Both models use the same bits, the same levels, and the same storage format. A GPU runs them with the same kernel, the program that does the multiply. Only the choice of level for each weight differs.
What GPTQ does differently is look at the layer's output instead of its weights. Every weight multiplies an input, and the inputs of a layer are correlated. So when one weight is rounded up, a weight that sees a similar input can be nudged down to cancel most of the damage. This section derives that idea exactly, as a formula called the optimal brain surgeon update, and turns it into GPTQ.
Three more methods then reshape a layer, without changing what it computes, so that plain rounding becomes accurate. One moves the activations' outliers into the weights. One gives finer levels to the weights that meet large inputs. One rotates the numbers so that no coordinate stands out. After them come the 8-bit and 4-bit floating-point formats, whose levels keep the same relative precision at every size, and whose 4-bit versions give every small block of numbers its own scale in hardware. Then comes quantization of the KV cache, the other large stream of bytes a decode step reads. The section ends with how to measure honestly what all of this costs in quality, and with the models that are now trained in their low-precision format from the start.
What this section builds on
§20.2 built the basic tools. Here are the ones we need, restated.
A quantizer with b bits replaces each real number by one of a few evenly spaced levels. The distance between neighboring levels is the stepΔ. The symmetric quantizer uses the integer codes from −qmax to qmax, with qmax=2b−1−1, which is 7 at 4 bits and 127 at 8 bits. It stores x as the code xq=clamp(round(x/Δ),−qmax,qmax) and reads it back as Q(x)=Δxq. The usual step is the largest magnitude divided by qmax, so that the largest number lands on the top level. For example, the weights (0.3,−0.9,0.4) at 4 bits get the step 0.9/7=0.129 and come back as (0.257,−0.9,0.386). The zero-point quantizer shifts the codes to 0,…,2b−1 with an integer offset z, as Q(x)=Δ(xq−z), so that it can cover a lopsided range such as [0,5].
Which numbers share one step is the granularity. Weights usually get one step per output channel, or one per group of g consecutive input channels within an output channel. Activations usually get one step per token. Each step is stored in 16 bits, so b-bit codes in groups of g cost bˉ=b+16/g bits per weight. At b=4 and g=128 that is 4.125 bits.
Two more facts from §20.2 carry most of this section. The first is about which scales a matrix product can carry. In XW, a scale that depends only on the token or only on the output channel can be applied once, after the integer sum. A scale that depends on the input channel, the index being summed over, cannot. It would have to be applied to every product inside the sum. We call this the factoring theorem. The second fact is that LLM activations have outlier features. These are a few input channels whose values are 3 to 20 times larger than everything else. In models of about 6.7 billion parameters and more they appear in every layer and in about 75% of the tokens (Dettmers et al. 2022). Together the two facts explain why 8-bit activations are hard. The outliers live in input channels, and an input-channel scale is exactly the one an integer matrix product cannot carry.
§20.2 also separated two kinds of quantization by what they speed up. Weight-only quantization, written W4A16 for 4-bit weights and 16-bit activations, shrinks the weight bytes that each decode step reads. That helps when the step waits on memory, as decode does at small batch. Weight-activation quantization, written W8A8 for 8-bit weights and activations, also runs the multiply itself in low precision. It uses the GPU's low-precision tensor cores, its matrix-multiply units, which raises the peak FLOP rate.
Weights use the book's (input, output) layout, W∈Rdin×dout and Y=XW, with one token per row of X∈Rn×din. So an output channel is a column of W, and an input channel is a row of W and a column of X. As in §20.1, π is a GPU's peak FLOP rate and β its memory bandwidth.
The quantity to protect is the layer's output
Round to nearest makes each stored weight as close as possible to its original value. That sounds like the right goal, but the network never uses a weight by itself. It computes XW, and each output is a dot product. §20.2 showed that the error of a dot product is the sum of each weight's error times the input it meets. So the errors of different weights add up in the output. Whether they reinforce or cancel depends on the inputs. A tiny example shows how much that matters.
Python · cell 1
The cell checks every number. The line marked OBS computes the compensation with a formula that the next part derives.
A real layer has thousands of inputs, so we need the general version of this calculation. Post-training quantization (PTQ) quantizes a trained model without training it further. It works one linear layer at a time. It runs a small calibration set, typically about a hundred passages of ordinary text, through the model and records each layer's inputs X. Then it looks for the quantized weights that best reproduce the layer's outputs on those inputs.
In plain words, a quantized layer is good if it gives the same outputs on real inputs. How much a weight error hurts depends on how strongly the inputs excite that weight and on how the inputs move together. Two properties of this objective carry the rest of the section. It splits into one independent problem per output column, so the columns can be quantized in parallel. And all columns share one Hessian, which depends only on the inputs, never on the weights. The two-input example is one column whose error is averaged over tokens, so its Hessian is H=2Σ with Σ=X⊤X/n.
Finding the best grid point for a quadratic objective is a hard combinatorial problem. It is the closest vector problem on a lattice, which is NP-hard in general. So no method solves it exactly for a layer with millions of weights. What we can do cheaply is the move from the example, made exact and repeated one weight at a time. An interviewer who asks what GPTQ optimizes wants this objective, and the fact that every output column shares the same Hessian.
The optimal brain surgeon update
The example made two moves. It put one weight on the grid. Then it shifted the weights that were still free to make up for it. Here is that pair of moves in general.
Take one output column, drop its index, and write w∈Rdin for its current weights. Suppose w is the best point we can reach with the coordinates we are still free to change. Then a change δ of those free coordinates raises the error by exactly 21δ⊤Hδ. The reason is that the error is quadratic and its slope along the free coordinates is zero at the best point. At the start this holds trivially, since w^=w gives zero error. Now we must move coordinate q onto the grid. Which change δ of the free coordinates does that most cheaply?
∂ Proof with a Lagrange multiplier
The objective is strictly convex, since H is positive definite, and the constraint is linear. So the point where the gradient of the Lagrangian vanishes is the unique minimizer. Write r=quant(wq)−wq and
L(δ,λ)=21δ⊤Hδ+λ(eq⊤δ−r).
Setting the gradient in δ to zero gives Hδ+λeq=0, so δ=−λH−1eq. The constraint then reads −λeq⊤H−1eq=−λ[H−1]qq=r. So λ=−r/[H−1]qq=(wq−quant(wq))/[H−1]qq, which gives δ⋆. Its cost is 21λ2eq⊤H−1HH−1eq=21λ2[H−1]qq=r2/(2[H−1]qq). ∎
In plain words, the cheapest way to put one weight on the grid moves every other weight along column q of H−1. The move is scaled so that weight q lands exactly on its level. That column, scaled so its q-th entry is 1, is the way of changing wq by one unit that changes the layer's outputs least.
The cost has a meaning worth knowing. It is the squared rounding gap times the squared length of the part of input q that the other inputs cannot reproduce. If input q is almost a copy of the others, the other weights can absorb nearly all of its error, and the cost is small. If input q is unlike any other, nothing can absorb its error, and the cost is what rounding alone would pay.
Check it on the two-input example. The part of x1 that x2 cannot predict has mean square 1−0.82=0.36 per token. So the cost is 0.04×0.36=0.0144, as the cell printed, a tenth of what rounding paid. The update is 0.2(1,−0.8), because [H−1]11=1.389 and [H−1]21=−1.111. It moves w1 up by 0.2 onto its level and w2 down by 0.16.
When the inputs are uncorrelated, H is diagonal. Then the update touches only wq, and the method is plain rounding. That limit says exactly when GPTQ helps and when it cannot. It is the first thing an interviewer probes after the derivation.
Repeating the step
One step is not enough, since every weight must end on the grid. So quantize one coordinate, update the others, and repeat on the coordinates still free. Two facts make the repetition exact rather than a rule of thumb.
∂ Proof that the greedy steps stay exact
Let P be the quantized coordinates and F the free ones, and suppose the current w minimizes E over wF with wP fixed, so the gradient of E in wF is zero there. Because E is quadratic, a change δF of the free coordinates alone gives E(w+δ)=E(w)+21δF⊤HFδF exactly, since the linear term vanishes. Quantizing q∈F is the theorem with HF, and its minimizer is the best point with wq fixed at its level. So after the step the coordinates in F∖{q} again minimize E given the new quantized set, which is item 1. The step raises E by exactly its cost, and E starts at zero, so summing the steps gives item 2. ∎
In plain words, each step puts one more weight on the grid and leaves the rest at their best values given the ones already fixed. So the greedy sweep never has to look back, and its total cost is just the sum of what each step reports.
Each step needs the inverse of the free set's Hessian, (HF∖{q})−1, and that inverse changes every step. It does not have to be recomputed from scratch. Removing one coordinate from an inverse is one step of Gaussian elimination, as the next derivation shows.
∂ Removing a coordinate from an inverse in one elimination step
Let A=H−1 and partition both matrices with coordinate q first (by a symmetric permutation), H=[hqqhh⊤H22], A=[aqqaa⊤A22]. We want H22−1, the inverse with row and column q removed.
The bottom block row of HA=I gives two equations, haqq+H22a=0 and ha⊤+H22A22=I. The first gives H22−1h=−a/aqq. Multiply the second on the left by H22−1 to get H22−1ha⊤+A22=H22−1, and substitute,
H22−1=A22−aqqaa⊤.
The top block row gives one more identity, hqqaqq+h⊤a=1. With a=−aqqH22−1h it becomes 1/aqq=hqq−h⊤H22−1h, the Schur complement used above to interpret the cost.
In the unpartitioned form the update of the inverse is the GPTQ paper's equation (3), H−q−1=(H−1−H:,q−1Hq,:−1/[H−1]qq)−q. That is one step of Gaussian elimination on H−1, costing O(d2) instead of the O(d3) of a fresh inverse. It is the Schur complement of aqq in A, just as 1/aqq is the Schur complement of H22 in H.
Core
One optimal brain surgeon step
Write obs_quantize_one(w, H, q, grid), one step of the optimal brain surgeon update. The inputs are these.
w, a weight vector of length d.
H, a symmetric positive definite d×d Hessian.
q, the index of the weight to quantize.
grid, a 1-D array of the values a weight may take.
Move w[q] to its nearest grid value (the first one in grid on a tie), which need not be evenly spaced, and shift every other weight by the optimal compensation. Return (w_new, increase), where increase is the rise of the loss, 21δ⊤Hδ for your change δ. Do not modify the inputs. The first test is the two-input example above. The others compare your answer with a direct constrained least-squares solve.
import numpy as np
def obs_quantize_one(w, H, q, grid):
raise NotImplementedError
Tests, verified solutions, and AI help, open once you add your API key.
GPTQ, the same update made cheap
Optimal brain quantization handles each output column separately, always choosing the cheapest weight next. A column of length din takes din steps of O(din2) each, so the layer costs O(doutdin3). A Llama-scale feed-forward down projection has din=14,336 and dout=4,096. That is about 1016 operations for one matrix. OBQ took about an hour for ResNet-50, an image model with 25 million parameters. Its cubic cost puts models with billions of parameters out of reach. GPTQ keeps the update and removes the cost in three steps.
Step 1, one order for every column. The greedy order differs from column to column, because it depends on the weights. But the free set's Hessian HF depends only on which coordinates are free and on the inputs. Suppose every column quantizes its coordinates in the same fixed order, row 1, then row 2, and so on. Then every column has the same free set at every step. The inverse is updated once per row for the whole layer instead of once per weight. The cost falls to O(max(doutdin2,din3)), a saving of a factor min(din,dout). The authors found that on large layers an arbitrary order is nearly as good as the greedy one. Their explanation is that the greedy order saves its few large-error weights for last. By then few free weights remain to absorb their errors, which cancels most of the greedy order's advantage.
Step 2, lazy batches. Quantizing row q updates every later row. That is a rank-one update of a huge matrix, with a couple of arithmetic operations for each number read from memory. So the GPU spends its time waiting on memory, which the roofline model of §11.2 calls memory-bound. But the rounding of row q depends only on the updates row q has already received, so updates to rows far ahead can wait. GPTQ processes 128 rows at a time. Inside the block it applies each row's update to the block's own rows at once. It also stores the scaled errors of the block's rows. At the end of the block it applies them to every later row in one matrix product. The arithmetic is identical, and the GPU does one large matrix multiply per block instead of 128 rank-one updates.
Step 3, Cholesky. Repeated elimination steps accumulate rounding error. On models beyond a few billion parameters the inverse HF−1 became indefinite in some layers, meaning it lost the positive definiteness the update relies on. That sent the updates in wrong directions and ruined those layers. The fix uses the Cholesky factorization of §0.10. Every symmetric positive definite matrix A factors as A=U⊤U, with U upper triangular with a positive diagonal. (That section writes it as LL⊤ with L=U⊤.) A fast, numerically stable algorithm computes it. Step q only uses row q of HF−1 from the diagonal onward. For a fixed order those rows are exactly the rows of one Cholesky factor.
∂ Proof that the Cholesky rows are the rows GPTQ needs
Write A=H−1. The Cholesky algorithm computes the first row of U as U11=A11 and U1k=A1k/U11. It then continues on the trailing block A22−aa⊤/A11, where a is the rest of the first column of A, since U1,:⊤U1,: removes exactly that rank-one piece. By the elimination derivation above, that trailing block is HF2−1, the inverse after removing coordinate 1. By induction, row q of U is row q of HFq−1 (from the diagonal on) divided by the square root of its diagonal entry, which is the claim. Substituting into the OBS update and its cost gives the last line. ∎
So GPTQ computes one Cholesky factorization of the inverse at the start and never updates an inverse again. It also adds dampening, a small number λdamp=0.01⋅meandiag(H) added to the diagonal of H. Without it, X⊤X is singular when the calibration set is small or two features are copies of each other. Dampening keeps H positive definite in both cases. It is also a regularizer, because 21δ⊤(H+λdampI)δ=∥Xδ∥2+2λdamp∥δ∥2. The extra term is a small penalty on moving weights far from where training put them.
The first cell below implements GPTQ without lazy batches, which is shorter and computes the same numbers. The second compares it with rounding on a synthetic layer of 512 inputs and 256 outputs. The layer's inputs are driven by 32 shared hidden factors, so they are strongly correlated, as LLM activations are. Four of its input channels are also eight times louder than the rest. The cell checks the second part of the greedy theorem too, that the per-step costs add up to the final error.
Python · cell 2
Python · cell 3
The widget runs the same sweep on a layer small enough to see, 10 inputs and 6 outputs, on a 3-bit grid. Each step rounds one input row, and the weights not yet rounded change to absorb its error. The plot tracks the relative output error of plain rounding (RTN) and of GPTQ as the rows are rounded. Press Play to run the whole sweep, or Next row to take one step at a time. The slider sets ρ, the correlation between neighboring inputs. Inputs i and j have correlation ρ∣i−j∣.
How can rounding the same weights to the same grid give a better layer?
A layer with 10 inputs and 6 outputs, rounded to a 3-bit grid one input row at a time.
Loading interactive figure…
Two questions decide whether you can trust this method in production. How much calibration data does it need? And what happens when the inputs carry no correlation to exploit? The next cell answers both on the same simulated layer.
Python · cell 4
With 128 calibration tokens, fewer than the layer's 512 inputs, GPTQ fits the calibration set far better (error 0.024) than it does new inputs (0.058). That is overfitting. With fewer tokens than inputs, X⊤X is singular, and dampening is what keeps the solve finite. The gap closes as the calibration set grows, to 0.0445 against 0.0457 at 8,192 tokens. Real GPTQ runs use 128 sequences of 2,048 tokens, about 262,000 tokens, far more than the width of any layer.
With uncorrelated inputs GPTQ is slightly worse than rounding on new data, 0.145 against 0.135. There is no true correlation to exploit, so the method fits the noise in its estimate of H. GPTQ's gain is a property of the inputs, not of the weights.
Interview
GPTQ with lazy batches and groups
Write gptq(W, X, b, g=None, block=128, damp=0.01), the full algorithm of the definition above. The inputs are weights W∈Rdin×dout and calibration inputs X∈Rn×din. Return the quantized weights W^ in dequantized form, with the same shape. The rules are these.
Quantizer. Symmetric, with qmax=2b−1−1, rounding half to even (np.round), and clamping to [−qmax,qmax].
Steps without groups. Each output column has one step, maxk∣Wkj∣/qmax, computed from the original weights before the sweep.
Steps with groups of g rows. Each group in each column has its own step, the largest magnitude of the group's current (already updated) weights divided by qmax, computed when the group's first row is reached.
Use a step of 1 wherever the largest magnitude is 0.
Hessian. Form H=2X⊤X, add damp times the mean of its diagonal to the diagonal, and take the upper Cholesky factor of H−1.
Sweep. Quantize rows in the order 0,1,…, and apply the updates lazily in blocks of block rows.
You may assume g divides din and, when g is given, that block is a multiple of g. The answer must not depend on block.
Tests, verified solutions, and AI help, open once you add your API key.
In practice two refinements matter. The first is groups, usually g=128 for bˉ=4.125 bits, which keep each column's outliers from setting the step for the whole column. GPTQ with groups of 128 is one of the most common 4-bit formats for published checkpoints, and every major serving engine runs it. The second is that a weight-only 4-bit model is fast only with kernels built for it. MARLIN (Frantar et al. 2024) is a set of GPU kernels for 4-bit weights times 16-bit activations. It overlaps memory access, unpacking, and 16-bit tensor-core work, and so keeps close to the full 4 times speedup over FP16 for batches up to 16 to 32. Inside the vLLM serving engine it gives up to 2.8 times faster generation end to end.
An interviewer who asks about GPTQ usually follows up with when it fails. The answer is in the calibration cell. It needs correlated inputs, and it needs enough calibration data that looks like production.
Moving the difficulty instead of absorbing it
GPTQ changes which levels the weights land on. A second family of methods changes the layer first, without changing what it computes, so that plain rounding becomes accurate. All of it rests on one identity. You may insert any invertible matrix and its inverse between the activations and the weights. §20.2 met its diagonal case right after the factoring theorem.
In plain words, dividing an input channel by some number and multiplying the matching weight row by the same number leaves every output unchanged. The same holds for rotating the inputs and rotating the weights back.
The identity is free only if computing XT−1 costs nothing at inference. For a diagonal T that holds whenever the operation that produced X scales each channel separately. Then 1/ck can be folded into that operation once, offline. Three places in a Transformer block allow this.
After a normalization. An RMSNorm (§5.2) ends by multiplying channel k by a learned gain γk. Replace γk by γk/ck. The query, key, value, gate, and up projections that read the normalized vector then absorb ck in their rows.
Between the value and output projections. The values reach WO through attention weights that mix tokens but not channels. So column k of WV can absorb 1/ck for row k of WO.
Inside the feed-forward block. The down projection reads silu(xWgate)⊙(xWup), which is linear in the up branch. So column k of Wup absorbs 1/ck for row k of Wdown.
An orthogonal T needs more care, and we come back to it with rotations.
SmoothQuant moves activation outliers into the weights
W8A8 needs activation scales per tensor or per token, the only ones that factor out of an integer matrix product. But LLM activations put their outliers in a few fixed input channels, and those channels are large in about three quarters of the tokens. So even a per-token scale is set by the outliers, and the ordinary values of every token land on a few levels. The weights, on the other hand, are tame. The fix is to use the diagonal identity to move part of each channel's range from the activations to the weights. This method is SmoothQuant (Xiao et al. 2022).
Here is the idea by hand. Take a layer whose ordinary input channels have activations and weights up to about 1. One outlier channel has activations that reach 50, while its weights reach only 0.1. Measure the spread of a side as its largest channel over its typical one. The activations then have a spread of 50, and the weights a spread of 1. Divide the outlier channel by c=50/0.1=22.4, and multiply its weight row by the same number, so the product is unchanged. Its activations now reach 50/22.4=2.24, and its weights 0.1×22.4=2.24. Both sides have a spread of about 2, which an 8-bit grid handles easily.
In plain words, the product of the two ranges, mkvk, is fixed by the layer, and α decides how to share it. At α=1 the activations become perfectly flat and the weights inherit the outliers. At α=0 it is the reverse. At α=21 both sides of channel k carry the same range. Activation channels are far more uneven than weight rows, so sharing the unevenness helps both.
The cell below tests this with the simulated layer's weights and new activations, in which eight channels are 50 times larger than the rest. It sweeps α and prints the spread of each side and the W8A8 error.
Python · cell 5
Before smoothing, the spread is about 58 in the activations and 1.5 in the weights. An 8-bit grid handles a spread of 8 easily, but a spread of 58 leaves the typical activations on a handful of levels. The spreads then trade places exactly as the theorem says. They are 62.2 on the activations and 1.0 on the weights at α=0, the reverse at α=1, and 7.9 on both at α=21. The W8A8 error, with one scale for the activations and one per column for the weights, falls from 0.0704 without smoothing to 0.0143 at α=21. That is about five times less. Either extreme is worse, because it hands all of the difficulty to one side.
The widget lets you move α and the size of two outlier channels. It draws each channel's largest activation and largest weight, before and after smoothing, beside the int8 output error at every α.
How much of an activation outlier can the weights absorb?
Two of 24 channels are outliers. Smoothing divides X channel k by cₖ and multiplies W row k by cₖ.
Loading interactive figure…
In the paper, α=0.5 worked for every OPT and BLOOM model. GLM-130B, a 130-billion-parameter bilingual model whose activations have about 30% outliers, needed 0.75. The int8 models ran inside FasterTransformer, NVIDIA's inference library, up to 1.56 times faster than FP16 and with half the memory. That put the 530-billion-parameter MT-NLG on one node of eight A100 GPUs, where FP16 needed sixteen. In an interview, the usual question is what α does at its extremes, and the theorem above is the whole answer.
Interview
SmoothQuant migration
Write smoothquant(X, W, alpha) for activations X∈Rn×din and weights W∈Rdin×dout. Return (c, X_hat, W_hat, err).
c holds the per-input-channel factors ck of the definition.
X_hat is X^=Xdiag(c)−1, and W_hat is W^=diag(c)W.
err is the relative W8A8 error ∥Q8(X^)Q8(W^)−XW∥F/∥XW∥F.
For Q8, use the given quant8_tensor (one scale for the whole tensor) on the activations and the given quant8_columns (one scale per output column) on the weights. You may assume no column of X and no row of W is all zeros, so every ck is finite and positive.
import numpy as np
def smoothquant(X, W, alpha):
c = np.ones(W.shape[0]) # placeholder: no smoothing
X_h, W_h = X / c, c[:, None] * W
Y = X @ W
err = np.linalg.norm(quant8_tensor(X_h) @ quant8_columns(W_h) - Y) / np.linalg.norm(Y)
return c, X_h, W_h, err
Tests, verified solutions, and AI help, open once you add your API key.
AWQ gives finer levels to the weights that meet large inputs
In weight-only quantization the activations stay in 16 bits, so their outliers do no harm by themselves. But they still decide which weights matter. Recall from §20.2 the error of a dot product with rounded weights. If each weight's rounding error is like independent noise with variance Δw2/12, where Δw is the weight step, the output error has variance (Δw2/12)∥x∥2. Each weight's error is multiplied by the activation it meets. So the weights in rows that meet large activations matter most.
Lin et al. (2023) measured this on OPT-6.7B at 3 bits with groups of 128. Rounding gives a WikiText-2 perplexity of 23.54 there, against 10.86 in FP16. Keeping just 1% of the weight rows in FP16, chosen by the average size of the activations they meet, brought it to 11.39. Keeping 1% chosen by weight magnitude gave 22.37, and a random 1% gave 24.23 (their Table 1).
Mixing precisions inside one matrix is awkward for kernels. So AWQ, activation-aware weight quantization, protects those rows by scaling instead. Scale row k of W by ck>1 and the activation channel by 1/ck. Under the same noise model, the scaled weight's rounding error has variance Δ′2/12, where Δ′ is the step of its group after scaling. That error now meets the activation xk/ck. Its contribution to the output error therefore has standard deviation Δ′∣xk∣/(12ck). Before scaling it was Δ∣xk∣/12. The ratio is
ΔΔ′⋅ck1.
In plain words, scaling a row up gives its weights finer levels relative to their size, as long as the group's step does not grow with it. Suppose the scaled weights of row k stay below the largest magnitude in their group. Then the group's step is unchanged, Δ′=Δ, and that channel's error falls by the factor ck. The cost appears when ck grows so large that row k holds the group maximum. Then Δ′ grows with ck, and every other weight in the group gets a coarser grid.
AWQ's Table 2 shows the trade-off on OPT-6.7B at 3 bits with g=128, with the 1% salient channels (those that meet the largest activations) scaled by s. Perplexity goes from 23.54 at s=1 to 11.92 at s=2, then back up to 12.36 at s=4, where 21.2% of the groups' steps had changed.
α=0 means no scaling, which is plain rounding. α=1 scales each channel in proportion to its activation size. The exponent turns a search over din scales into a search over one number. And it takes only aˉ, one average per channel, from the calibration data.
That is the design point. GPTQ fits a full din×din Hessian and can overfit its calibration set, while AWQ estimates only din averages. The paper tested this on OPT-6.7B at 3 bits, calibrating on PubMed abstracts and evaluating on Enron emails, and the reverse. The mismatch cost AWQ 0.5 to 0.6 perplexity, against 2.3 to 4.9 for GPTQ. With 16 calibration sequences, AWQ beat GPTQ calibrated on 192.
The cell below repeats the 1% experiment on our simulated layer, at 4 bits with groups of 128, and then runs AWQ's search over α.
Python · cell 6
On our simulated layer, keeping the five rows with the loudest inputs in full precision cuts the error from 0.118 to 0.090. Keeping five rows chosen by weight norm or at random does nothing. AWQ's scaling gets part of the way there with every weight still in 4 bits, 0.104 at its best α=0.25. At α=0.75 and above it is worse than plain rounding, because the loud rows take over their groups' steps.
AWQ's speedups come from the bytes, about 4 times fewer. Its inference engine, TinyChat, turned them into 3.2 to 3.3 times the speed of Hugging Face's FP16 implementation on desktop, laptop, and mobile GPUs. AWQ changes the layer and GPTQ chooses the levels, so the two can be applied one after the other. An interviewer comparing them wants this split, and the calibration-robustness numbers above.
Core
AWQ's activation-aware scale search
Write awq_search(X, W, b=4, g=128, grid=21). First compute aˉ, the mean of ∣X∣ over tokens for each input channel. Then, for each α in np.linspace(0, 1, grid),
set c = a_bar ** alpha,
quantize diag(c)W with the given quant_groups(., b, g), and
measure the relative output error ∥(X/c)Q(diag(c)W)−XW∥F/∥XW∥F, with the activations kept in full precision.
Return (alpha, c, err) for the smallest error, taking the first one on ties.
import numpy as np
def awq_search(X, W, b=4, g=128, grid=21):
c = np.ones(W.shape[0])
Y = X @ W
err = np.linalg.norm(X @ quant_groups(W, b, g) - Y) / np.linalg.norm(Y)
return 0.0, c, err # placeholder: never scales
Tests, verified solutions, and AI help, open once you add your API key.
Rotations spread outliers over every coordinate
Smoothing flattens channels one at a time, and it needs a weight side able to take the range. At 4 bits even smoothed activations are too uneven. A per-token step of max/7 leaves the typical values of a token on two or three levels. A rotation does something no diagonal scaling can. It spreads each outlier over every coordinate.
A tiny example shows the effect. The vector (8,0,0,0) has one spike. Multiply it by a 4×4 matrix whose entries are all ±21, in the pattern of a Hadamard matrix (defined below), and it becomes (4,4,4,4), up to signs. Its length is still 8, but now no entry stands out, and a 4-bit grid with step 4/7 stores it exactly. §12.4 used the same trick for FP8 attention, since (QR)(KR)⊤=QK⊤ for any orthogonal R. The idea, called incoherence processing, came from a method for quantizing weights to 2 bits, QuIP (Chee et al. 2023). QuIP multiplied by random orthogonal matrices built as Kronecker products of small ones, and its successor QuIP# switched to the random-sign Hadamard rotation used here (Tseng et al. 2024). Here we make the effect precise and then place rotations in a whole network.
Measure how spiky a vector is by its peak-to-RMS ratio, the largest magnitude over the root mean square,
μ(x)=∥x∥2/d∥x∥∞,
which lies between 1 (all entries equal in size) and d (one nonzero entry). A uniform quantizer's step is set by the peak. What we care about is its noise compared with the typical value, so μ is the number that hurts. The spike above has μ=2, and its rotation has μ=1.
Now the rotation. Let Hd be the Sylvester Hadamard matrix of size d, a power of two. It is built by H1=[1] and H2d=[HdHdHd−Hd]. Its entries are ±1, and Hd⊤Hd=dI. We always write its size as a subscript, to keep it apart from the Hessian H. Let R=DHd/d, where D is diagonal with random signs Djj∈{±1}. Then R is orthogonal. Because of the recursive structure, applying it costs O(dlogd) instead of the O(d2) of a general matrix.
∂ Proof with Hoeffding's inequality
Entry i of xR is yi=d1∑jDjjxj(Hd)ji, a sum of independent terms. Each term has mean zero and lies in an interval of length 2∣xj∣/d. Hoeffding's inequality says that a sum of independent, zero-mean terms, term j confined to an interval of length ℓj, satisfies P(∣y∣≥t)≤2exp(−2t2/∑jℓj2). Here that gives P(∣yi∣≥t)≤2exp(−2t2/∑j(2∣xj∣/d)2)=2exp(−t2d/(2∥x∥2)). A union bound over the d entries gives P(maxi∣yi∣≥t)≤2dexp(−t2d/(2∥x∥2)), which equals δ at t=∥x∥2ln(2d/δ)/d. A rotation preserves the norm, so ∥xR∥2/d=∥x∥/d, and dividing gives the bound. ∎
In plain words, after a random-sign Hadamard rotation no coordinate can stand out by more than a logarithmic factor. Every output coordinate is a sign-scrambled average of all the inputs, so one spike is shared out among all of them. The same rotation applied to W, as R⊤W, spreads the weights' outliers too. QuIP calls this making the weights incoherent, and its guarantees rest on it.
Where can R live in a Transformer at no cost? The natural place is the residual stream, the running sum of vectors that every block reads from and adds to (§5.1). It works because an RMSNorm commutes with rotations once its gain is moved out of the way. RMSNorm divides a vector by its root mean square, and a rotation does not change the root mean square.
∂ Proof of computational invariance
∥xR∥=∥x∥, so rms(xR)=rms(x) and N0(xR)=N0(x)R. A reading layer then computes N0(hR)R⊤diag(γ)W=N0(h)diag(γ)W, its original output. A writing layer adds fWR=(fW)R. So if the stream was hR before the residual add, it is (h+fW)R after. By induction over blocks, starting from the rotated embedding, the stream is the original one rotated everywhere. The gain must be folded first, because diag(γ) and R do not commute. ∎
In plain words, store the residual stream rotated, give every layer that reads it the inverse rotation inside its weights, and give every layer that writes it the rotation. Every layer then computes what it computed before, while the activations it sees, rotated, have no outliers.
Rotating the residual stream and folding the rotation into the weights is QuaRot (Ashkboos et al. 2024). It builds on the same invariance that SliceGPT, a pruning method by largely the same authors (Ashkboos et al. 2024), used to delete rows and columns of weight matrices. The residual rotation is folded into the weights offline and costs nothing at run time.
A few activations are not on the residual stream, and there the rotation cannot be folded away. One is the input of the down projection. Another is the input of the attention output projection. The others are the queries and keys after the rotary position encoding (RoPE), which rotates pairs of their channels by angles that depend on the token's position (§5.5). These need fast Hadamard transforms during inference, each O(dlogd) per token. QuaRot counts the cost as one and a half transforms per layer, where rotating every weight matrix without folding would need two per matrix.
The cell below takes activations with six outlier channels 25 times larger than the rest. It compares W4A4 errors, 4-bit weights and 4-bit activations, with and without a random-sign Hadamard rotation.
Python · cell 7
The rotation takes the activations' peak-to-RMS ratio from 34.0 to 4.5. The theorem bounds each token's ratio. Every token here has about the same norm, so a union bound over all 1,024 tokens with δ=0.01 promises at most about 2ln(2⋅512⋅1024/0.01)=6.1 for the whole matrix. The rotation roughly halves the W4A4 output error, from 0.361 to 0.172, while XW is unchanged in exact arithmetic. What remains is mostly the rounding noise of 15-level grids on both operands. QuaRot's default, GPTQ on the rotated weights, reduces it further. In an interview, expect to explain both halves, why a rotation removes spikes and why it costs nothing on the residual stream.
Interview
The fast Walsh-Hadamard transform and incoherence
This exercise has two parts.
Write fwht(x), the normalized fast Walsh-Hadamard transform along the last axis of an array. The last dimension n must be a power of two, and the result must equal xHn/n for the Sylvester matrix (scipy.linalg.hadamard(n)). Use O(nlogn) operations per vector and never form the matrix. Return a new array and leave x unchanged. Raise ValueError if n is not a power of two.
Write incoherence(W, signs) for W∈Rn×dout and a vector of n signs. Return the peak-to-RMS ratio max∣Wij∣/RMS(W) before and after the rotation W→R⊤W, with R=diag(signs)Hn/n.
Tests, verified solutions, and AI help, open once you add your API key.
Low-precision floats, FP8 and FP4
Integers space their levels evenly, so one large value makes every small value coarse. Floating-point numbers space their levels geometrically. They have the same number of levels between every two consecutive powers of two, a range called a binade. So a small value keeps its relative precision however large its neighbors are.
§8.5 built the 8-bit float formats. E4M3 has 1 sign bit, 4 exponent bits, and 3 mantissa bits. With 3 mantissa bits, every normal number is stored to a relative error of at most 2−4=6.25%. The normal range spans a factor of about 28,700, from 2−6 to 448. Below 2−6 come the subnormals, evenly spaced values that fill the gap down to zero with less relative precision. Suppose one scale per tensor maps the largest magnitude to 448. Then an outlier 50 times the typical value still leaves the typical values well inside the normal range.
The cell compares three 8-bit schemes on the activations of the SmoothQuant cell, which have eight outlier channels 50 times the rest. It reports the signal-to-quantization-noise ratio (SQNR) of §20.2. That is the signal's power over the error's power, in decibels, where every 10 dB is ten times less noise.
Python · cell 8
Overall, all three look tolerable, because the outliers carry most of the energy. The 504 ordinary channels tell the real story. INT8 with one scale per tensor leaves them at 6.9 dB, and per-token INT8 at 13.4 dB. FP8 with one scale per tensor gives them 31.5 dB, the same as everywhere else in the tensor. This is why FP8 W8A8 is the low-risk 8-bit format, and why it usually needs no smoothing. On an H100 it also doubles the dense peak, from 989 to 1,979 TFLOP/s, so it speeds up compute-bound prefill as well as memory-bound decode.
FP4, and two ways to scale it
At 4 bits a float has room for 1 sign bit, 2 exponent bits, and 1 mantissa bit, the format E2M1. Its values follow from the bit layout of §8.5 with exponent bias 1. Exponent field 0 gives the subnormals 21−1⋅m/2∈{0,0.5}. Exponent fields 1, 2, and 3 give 2e−1(1+m/2), that is {1,1.5}, {2,3}, and {4,6}. E2M1 has no infinities or NaNs, so all 16 codes are numbers. They make 15 distinct values, counting +0 and −0 as one. The cell lists them.
Python · cell 9
Its nonzero magnitudes, 0.5 to 6, cover a factor of only 12, far too little for a whole tensor. So FP4 works only with a scale for every small block of values. Two such block formats have hardware support on NVIDIA's Blackwell GPUs, the generation after the H100 (B200 and its successors). §8.5 defined both for 4-bit training and showed how each scale places a block's largest value. Here we restate them next to the 8-bit formats a server chooses among, and compare them on the question that matters for serving, the error they leave in fixed weights and activations.
The two FP4 formats differ in exactly two ways, and both favor NVFP4. First, a block of 16 values has a smaller spread than a block of 32. Second, a power-of-two scale cannot map the block maximum onto the top level 6. With the floor rule above, the block maximum lands somewhere in [4,8) after scaling, and maxima above 6 are clipped. Some training recipes instead round the scale up to avoid clipping. Then the maximum can land as low as 3, and the levels 4 and 6 go unused (NVIDIA 2025, appendix B.4). NVFP4's E4M3 scale has three mantissa bits, so it maps each block maximum to within about 6% of the top level. The price is 0.25 more bits per value than MXFP4.
The cell below implements both formats, plus INT4 with one FP16 scale per 32 values for comparison, and measures their SQNR on two kinds of data. One is Gaussian. The other is heavy-tailed, a Student-t distribution with 3 degrees of freedom, which has rare values far larger than the typical ones.
Python · cell 10
At the same 4.5 bits per value, NVFP4 and INT4 with an FP16 scale per 32 values are within 0.2 dB on Gaussian data. On heavy-tailed data NVFP4 wins by 4 dB. Most values there are small next to their block's maximum, and the float grid's fine levels near zero pay off. MXFP4 is 1.5 to 1.7 dB behind both on Gaussian data, the cost of its coarse scale. These are synthetic distributions, so measure real weights and activations the same way before choosing. An interviewer comparing MXFP4 and NVFP4 wants the two differences above, block size and scale precision, and their cost in bits.
The table collects the formats of this part.
Format
Element
Block
Scale
Bits per value
Multiplies on
INT8, per channel or token
integer
a row or column
FP16 or FP32
about 8
INT8 tensor cores
FP8, per tensor, row, or block
E4M3
a tensor, row, or 128×128
FP32
about 8
FP8 tensor cores (the L40 and H100 generations and later)
MXFP8
E4M3
32
E8M0
8.25
Blackwell
INT4 weight-only, g=128
integer
128
FP16
4.125
16-bit tensor cores after unpacking
MXFP4
E2M1
32
E8M0
4.25
Blackwell (FP4)
NVFP4
E2M1
16
E4M3, plus FP32 per tensor
4.5
Blackwell (FP4)
Bits per value turn directly into weight bytes. A model with N parameters at bˉ bits each stores MW=bˉN/8 bytes, the weight bytes of §20.1's model of a decode step. The cell computes what each format does to a 70B model and checks one published case to the decimal.
Python · cell 11
A dense 70B model makes the same point. In NVFP4 its weights are 39.7 GB, so a single 80 GB GPU has about 40 GB left. That is room for about 123,000 tokens of BF16 cache, or 246,000 in FP8, before counting activations and workspace. The cache's share of memory is the subject of the next part.
Quantizing the KV cache
The KV cache stores, for every token in every sequence being served, the keys and values that attention reads at each later step (§6.5). It costs mKV=2LnkvdksKV bytes per token. Here L is the number of layers, nkv the number of key-value heads, dk their width, and sKV the bytes per number. For Llama 3.1 8B that is 2×32×8×128×2=131,072 bytes per token in BF16. So one user with 8,192 tokens of context holds 1.07 GB.
§20.1 asked how many users one H100 can serve with this model, at 50 ms per output token and 8,192 tokens of context each. Memory ran out at 59 users, long before the latency target. Quantizing the weights from 16 to 4 bits frees only 12 GB. Quantizing the cache shrinks every user. The cell redoes that count, the users that fit in memory and the users the latency target allows, for five formats.
Python · cell 12
Which axis to quantize along
Activations are quantized per token because per-token scales factor out of a matrix product with the weights. The cache is different. The attention kernel dequantizes it to 16 bits as it reads it, or folds a per-channel key step into the query, which stays in 16 bits. So the cache can have one scale per token or one per channel. The right choice depends on where the outliers are, and keys and values differ.
Keys have outlier channels, a few coordinates that are large in almost every token. Two methods for low-bit caches both found this, KIVI (Liu et al. 2024) and KVQuant (Hooper et al. 2024). With per-token quantization, those channels set every token's step, and every other coordinate of every key is rounded coarsely. Per-channel quantization instead gives each coordinate a step set by its own range across tokens. With a zero point, which shifts the levels to cover the channel's own minimum to maximum, the outlier channel's large typical value then costs nothing, because the step depends only on how much the channel varies.
Two more facts make keys the delicate half. A channel that is nearly the same in every token adds nearly the same amount to all of a query's scores, and the softmax ignores a shift shared by all scores. And key errors enter the softmax's exponent. A score error δsj=q⊤δkj/dk multiplies the weight of key j by about eδsj, and it grows with the size of the query.
Values have no fixed outlier channels, and they enter the output linearly, o=∑jajvj. The output is a weighted mix of a few tokens' values, so each of those tokens must be accurate relative to its own size. Per-token quantization keeps each value's error inside that token. Per-channel steps would be set by the largest token in the context and would crush the small ones.
The cell builds keys with two outlier channels and values whose tokens differ in size. Then it measures the attention-output error for all four combinations of axes.
Python · cell 13
At 4 bits, keys per channel and values per token give a relative attention-output error of 0.148, against 0.374 for per-token keys and values. The order of the four choices is the same at 3 bits. Interviewers ask for exactly this rule, keys per channel and values per token, and for the reason behind each half.
Per-channel keys raise a systems problem. The cache grows one token at a time, and a per-channel step needs a group of tokens to compute. So KIVI keeps the newest tokens, up to 128, in full precision and quantizes keys only in completed groups of 32 tokens. At 2 bits it reported 2.6 times less peak memory for Llama-2-7B, weights included. That allowed up to 4 times larger batches and 2.35 to 3.47 times the throughput.
In production the common choice is simpler. It is an FP8 cache with one scale per tensor, which costs little quality and halves mKV. In the vLLM serving engine it is the flag --kv-cache-dtype fp8. vLLM's published recipe for NVIDIA's Nemotron 3 Ultra (vLLM 2026) and its benchmarks of DeepSeek-V4-Pro (vLLM 2026) both run with an FP8 cache.
Core
Quantizing keys and values along the right axis
This exercise has two parts, for K,V∈RT×dk with one token per row.
Write quantize_kv(K, V, b, k_mode, v_mode), where each mode is "channel" or "token". "channel" means one range per column, with statistics over the tokens. "token" means one range per row. Use the zero-point quantizer of §20.2 with one change. Each slice takes its range from its own minimum and maximum, without stretching it to include 0, so a key channel that sits near 15 in every token spends all its levels on its own small range. The step is Δ=(max−min)/(2b−1), or 1 if the slice is constant. The zero point is the integer z=round(−min/Δ), which may now lie outside [0,2b−1]. A value x gets the code clip(round(x/Δ)+z,0,2b−1) and comes back as Δ(code−z). Round half to even, as np.round does. Return the dequantized (K_hat, V_hat).
Write attn_error(Q, K, V, K_hat, V_hat), the relative Frobenius error of softmax(QK^⊤/dk)V^ against the exact attention output, with a numerically stable softmax over the keys.
Tests, verified solutions, and AI help, open once you add your API key.
Measuring what quantization costs
A quantized model can match its parent's perplexity and benchmark average and still be a different model. Perplexity averages the loss of the true next token only, so it misses changes in the rest of the distribution. An accuracy average can stay put while many individual answers change in both directions. Three measurements do better.
Divergence from the original. Run both models on the same real prompts. At each position, compare the original's next-token distribution p with the quantized model's q. The comparison is the KL divergence DKL(p∥q)=∑vpvlog(pv/qv) (§0.5), averaged over positions. This needs no labels, and it compares every token's whole distribution. It also has an exact link to the error in the logits, the scores the softmax turns into probabilities.
∂ Proof of the KL expansion
logpv=zv−lse(z) and logqv=zv+δv−lse(z+δ). So, using ∑vpv=1, DKL=∑vpv(logpv−logqv)=−p⊤δ+lse(z+δ)−lse(z). The gradient of lse at z is p, and its Hessian is the softmax Jacobian diag(p)−pp⊤ (§4.3). Taylor's theorem then gives lse(z+δ)=lse(z)+p⊤δ+21δ⊤(diag(p)−pp⊤)δ+O(∥δ∥3), and the quadratic form is Varp(δ). ∎
In plain words, the KL counts only the spread of the logit error, weighted by where the model puts its probability. An error that shifts every logit equally costs nothing, and an error on tokens the model never considers costs almost nothing. Those are exactly the errors a quality metric should ignore.
Flips. On a labeled benchmark, compare the answers item by item. Out of n items, let n10 count the ones the original gets right and the quantized model gets wrong, and n01 the reverse. The accuracy changes by (n01−n10)/n. The fraction of answers that flipped is (n01+n10)/n, which is never smaller and can be much larger.
Dutta et al. (2024) measured both on seven benchmarks, among them MMLU, a multiple-choice knowledge benchmark. Accuracy changed by only 0 to 2% under every quantization scheme they tried. Yet only 8-bit GPTQ weights kept the flips negligible. The bitsandbytes library's 4-bit weights and activations (W4A4) flipped 8 to 16% of MMLU answers, and even Llama 2 70B flipped 3 to 8% under 4-bit quantization. They also found flips well correlated with KL.
Because both models answer the same items, the right test of whether accuracy changed is a paired one (§0.11). McNemar's exact test looks only at the n01+n10 items where the models disagree. It asks whether n01, the count that improved, is consistent with a fair coin flip on each of them.
The cell checks the KL expansion on random logits and then runs the flip arithmetic on a made-up benchmark of 1,000 questions.
Python · cell 14
The quadratic approximation matches the exact KL to three digits once the logit error is small. The benchmark example is the trap in miniature. Accuracy moved by 0.4 points, and McNemar's test finds no evidence of a change (p=0.70). Yet 6% of the answers are different. Both statements are true. The models are about equally accurate on this benchmark, and they are not the same model.
Long and hard tasks. Errors compound over long generations and long contexts, so evaluate on those directly, not only on short multiple-choice questions. Liu et al. (2025) quantized reasoning models, which write long chains of thought before answering. They called an accuracy drop of at most 1% lossless and one of 3% or more risky. Weights, activations, and KV cache all in 8 bits were lossless across model sizes and tasks. 4-bit weight-only and 4-bit KV-cache quantization were nearly lossless on most models, though 4-bit weights cost the smallest, 1.5B, model 2.1 points. 3-bit weights were risky. Mildly quantized models did not reason for longer. But aggressive low-bit settings made small models produce longer outputs, a cost the accuracy table does not show.
A sound evaluation of a quantized model follows from all this. It is also the answer to the most common interview question on quantization, how you would know a quantized model is safe to ship.
Compute the KL to the original on a sample of real production prompts, and per layer group if a number looks high.
Report accuracy changes with paired statistics, and report flips, on the benchmarks that matter for the product.
Run long-context retrieval and multi-step reasoning tasks, and compare output lengths.
Sample many long generations and compare a quality signal over them (a reward model or a judge), as Meta did for FP8, because rare corrupted outputs do not move averages.
Calibrate on data that looks like production (other languages, code, tool outputs), and test on data the calibration did not see.
How to put error bars on benchmark scores in general is the subject of §22.2.
Models born in low precision
Post-training quantization asks a finished model to tolerate rounding. Since 2025 several labs have instead trained or post-trained models in their deployment format. One way is quantization-aware training (QAT). It runs the quantizer in the forward pass during training, and it passes gradients through the rounding as if the rounding were the identity, the straight-through estimator of §20.2. The other way is to train in low precision from the start. Each release below is a result reported by its developer.
Gemma 3 QAT (Google, April 2025). Int4 checkpoints trained with QAT cut Gemma 3 27B from 54 GB in BF16 to 14.1 GB, small enough for a 24 GB consumer GPU. Q4_0, a common 4-bit format of the llama.cpp library, loses some perplexity when applied to the BF16 model directly. QAT cut that loss by 54% (Google 2025).
gpt-oss (OpenAI, August 2025). The MoE weights were post-trained in MXFP4, as in the example above.
NVFP4 pretraining (NVIDIA, September 2025). A 12-billion-parameter model was trained on 10 trillion tokens with NVFP4 matrix multiplications (NVIDIA 2025). It reached 62.58% on MMLU-Pro, a harder version of MMLU, against 62.62% for the same model trained in FP8. It used random Hadamard transforms on the inputs of the weight-gradient products, two-dimensional 16×16 block scales for the weights, stochastic rounding for gradients, and 16% of the linear layers (those of the first two and the last eight blocks) kept in BF16. The pretraining recipe is taught in §8.5.
Kimi K2 Thinking (Moonshot AI, November 2025). QAT during post-training made the MoE weights INT4, weight-only. The model card reports roughly twice the generation speed in low-latency serving, with all benchmark results reported at INT4 (Moonshot AI 2025).
2026 releases. Checkpoints in NVFP4, MXFP4, and MXFP8 are now often published alongside or instead of BF16 ones. vLLM's posts of June 2026 describe serving NVIDIA's Nemotron 3 Ultra from an NVFP4 checkpoint (vLLM 2026) and MiniMax M3 from MXFP8 expert weights (vLLM 2026).
DeepSeek-V3 is the earlier case. It was pretrained in FP8 with 1×128 and 128×128 block scales (§8.5), and its released weights are in that format. Training in the other direction, small adapters on top of frozen 4-bit weights, is QLoRA (§14.6). Its NF4 format is a non-uniform 4-bit code whose 16 levels sit at quantiles of a normal distribution, the usual shape of pretrained weights, with a scale per block.
Choosing a method
The table puts the methods of this section side by side, by what each changes, what data it needs, and where it is used.
Method
What it changes
Calibration data
Optimizes
Typical use
RTN with groups
nothing, each weight is rounded
none
each weight's own error
8-bit weights, quick baselines
GPTQ
which level each weight gets
128 sequences of 2,048 tokens, for the full Hessian
layer output error, second order
3 and 4-bit weight-only (W4A16)
AWQ
per-channel scales, then rounding
a small set (16 sequences sufficed in the paper), for channel magnitudes
layer output error, over one exponent
4-bit weight-only, robust to the calibration domain
SmoothQuant
per-channel scales between activations and weights
channel maxima
balance of ranges
INT8 W8A8
QuaRot, SpinQuant
orthogonal rotations, folded and online
none for QuaRot's rotations, 800 samples for SpinQuant's (plus GPTQ's data for the weights)
spikiness, or the loss directly
4-bit weights, activations, and cache
FP8 with scales
the number format
none for dynamic scales
relative precision
W8A8 on H100 and later, the low-risk default
MXFP4, NVFP4
the number format, block scales in hardware
none, or QAT
per-block range
W4A4 on Blackwell, natively low-precision models
KV cache (FP8, KIVI, KVQuant)
the cache format and axis
none, or a little
attention output
long contexts and large batches
In PyTorch
The block below is GPTQ as its authors run it, with lazy blocks of rows and the Cholesky factor of the inverse Hessian computed once with PyTorch's kernels. It then stores FP8 E4M3 weights with one scale per output column, using PyTorch's native float8 type. Production tools (GPTQModel, llm-compressor, AutoAWQ, TensorRT Model Optimizer) wrap the same steps in calibration loops over the model's blocks and pack the codes for their kernels.
🎬 EfficientML.ai Lecture 13 - LLM Deployment Techniques (MIT 6.5940, Fall 2024) · MIT HAN LabSong Han's lecture on quantizing LLMs for deployment (SmoothQuant, AWQ, W4A8 serving), from the group that wrote two of this section's methods.🎬 AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration [MLSys'24 Best Paper] · MIT HAN LabThe authors' talk on salient channels, the scaling argument, and the TinyChat speedups.🎬 Lecture 7 Advanced Quantization · GPU MODEHow weight-only and dynamic quantization, including GPTQ, are put into practice and made fast in PyTorch kernels.