Scale, zero point and the clipping range
On this page
Figure 1. Sections 1 and 2 set the grid; Sections 3 and 4 choose the range.
Quantization turns a float tensor into integers and back. It sets the grid (scale and zero point), rounds each value onto it, and chooses where the range should end so outliers and rounding cost the least. The same round trip runs wherever a quantized tensor is stored or computed; when each tensor's range gets fixed, once after training (post-training quantization, PTQ) or during training (quantization-aware training, QAT), is the subject of Post-Training Quantization and Quantization-Aware Training.
Fewer bits per number cost less memory, bandwidth and energy. Fewer bits mean faster inference (running the trained model on new inputs), and integer operations use less power than float on most hardware. Two terms are used throughout. A tensor is the array of numbers a layer holds or passes on, and an activation is a layer's output, the numbers it hands to the next layer.
Quantizing to int8 stores each number as the nearest of 256 evenly spaced grid levels instead of a 32-bit float; the spacing of the levels (the scale) and where zero sits (the zero point) decide how much is lost. It is the same rounding an ADC applies to a voltage, see ADC Fundamentals.
The scale is the spacing between grid levels and the zero point is the integer that stands for float 0 (Jacob et al., 2018). Float zero must land exactly on a grid level, because zero padding and ReLU zeros are everywhere and must stay exact (both taught in CNN Basics).
Weights use the symmetric grid, because they are usually centered on 0. The card below runs the recipe on a 4-bit grid, integers from −7 to 7. Take three illustrative weights, 0.42, −0.10 and 0.05. The largest is 0.42, so each bin is 0.42 divided by 7, 0.06 wide. The weight 0.05 sits 0.83 bins from zero, rounds to bin 1 and reads back as 0.06; −0.10 rounds to bin −2 and reads back as −0.12. Each line of the card is one of those moves.
Activations use the asymmetric grid, because ReLU outputs are never negative.
Figure 2. 256 equal bins over an illustrative range, one integer each.
Figure 3. Illustrative value 0.37; each bit lost about doubles the step.
Quantizing a tensor with the shifted grid takes four moves: find its range, cut the range into 256 buckets, round each float to its nearest bucket, and read the bucket back as a float. The gap between the float and the value read back is the rounding error.
Figure 4. Six illustrative floats quantized to 8 bits, step by step.
The listing runs the same round trip in PyTorch on the six floats from the walkthrough.
import torch
x = torch.tensor(
[-0.21, -0.05, 0.12,
0.34, 0.52, 0.77])
s = (x.max() - x.min()) / 255
z = torch.round(-x.min() / s)
xq = torch.round(x / s) + z
xq = xq.clamp(0, 255)
x_hat = s * (xq - z)
# worst error, at most s/2
err = (x - x_hat).abs().max()
Real weights rarely fill their range evenly. On the symmetric grid from Scale and zero point the scale is set by the biggest weight, so the largest weight sets the width of every bin. Figure 5 puts the 288 first-layer weights (32 filters of 3 by 3) of the MNIST CNN from our run of the companion notebook on a 4-bit grid; filter 15's 0.427 is that weight. It is only about 7% larger than the next weight, so leaving it out narrows every bin by about 6% and lowers the error by about 6% too; the far larger outlier channels of Figure 6 cost far more.
Figure 5. Dropping the largest weight narrows every bin by about 6%.
At low bit-widths the decision that matters most is where to cut the range. A wide range keeps the outliers but makes each bin wide, so ordinary values lose precision. A tight range gives narrow bins but clips the outliers to the range end.
For example, on the 288 real weights of Figure 5 on the full 16-level 4-bit grid (that figure used the 15-level symmetric grid, so its MSE differs), our notebook run cut the range to 0.94 of the min-max one: a few outliers get clipped, every bin gets narrower, and the total error falls from 0.00021 to 0.00018. The first card says how a range sets the scale and splits the error; the second names the two ways to pick the range.
A third technique (Nagel et al., 2021) sets the range from the batch-norm parameters when a layer has batch norm (a layer that rescales activations to a steady range), so only large outliers get clipped.

Figure 6. Paper Fig. 5: "Per (output) channel weight ranges of the first depthwise-separable layer in MobileNetV2 …" arXiv:2106.08295
These per-channel weight ranges (one range per output channel, a channel being the output of one convolution filter) come from MobileNetV2 (a small network built for phones, built from depthwise-separable convolutions), after batch-norm folding (the batch norm scale and shift absorbed into the convolution weights). Most channels are tiny, a few are enormous, and one global min-max range set by the outlier channels costs every normal channel rounding error.
The listing runs both strategies on the full 16-level grid, 0 to 15, with the 288 real weights of Figure 5 as w; its last lines are the run quoted above.
import numpy as np
# round trip on 16 levels, 0 to 15
def rt(x, lo, hi):
s = (hi - lo) / 15
q = np.round((x - lo) / s)
q = np.clip(q, 0, 15)
return lo + s * q
def mse(w, lo, hi):
return np.mean((w - rt(w, lo, hi))**2)
def compare(w):
mm = mse(w, w.min(), w.max())
ts = np.linspace(0.2, 1.0, 41)
errs = [mse(w, t * w.min(),
t * w.max())
for t in ts]
return mm, ts[np.argmin(errs)], min(errs)
# on the 288 real weights:
# mm 0.00021, t 0.94, best 0.00018
t and keeps the lowest error.ImageNet accuracy with min-max and MSE clipping ranges, from the quantization white paper: MSE wins at 4 bits.
W8 and W4 mean weights quantized to 8 and 4 bits. ImageNet validation accuracy (%, the held-out split of a large labeled image dataset), symmetric weight quantization with activations in FP32 (the original 32-bit floats), averaged over 5 runs. FP32 baselines: ResNet18 69.68, MobileNetV2 71.72.
| Model (FP32) | W8 Min‑Max | W8 MSE | W4 Min‑Max | W4 MSE |
|---|---|---|---|---|
| ResNet18 (69.68) | 69.57 | 69.45 | 0.12 | 18.82 |
| MobileNetV2 (71.72) | 71.16 | 71.15 | 0.59 | 13.77 |
"For high bit-widths, the MSE and min-max approaches are mostly on par. However, at lower bit-widths the MSE approach clearly outperforms the min-max." (the quantization white paper's Section 3.1)
Figure 7. Illustrative weights; too tight clips, too wide fattens bins.
A network has hundreds of tensors, and activations only exist at run time: who measures their ranges, and when? Post-Training Quantization and Quantization-Aware Training answers that, running this same round trip in PyTorch with observers, QConfig and convert.