Sahaj.blogs
11 Mar 2026 · StudyPhase 12 · LLMs

LLMs · Quantisation

Post-training quantisation for inference: absmax and zero-point int8, per-tensor vs per-channel scales, how one outlier ruins a whole tensor's scale, and PyTorch dynamic quantisation on a small model.

Quantisation stores weights (and sometimes activations) as low-bit integers:

  • absmax (symmetric): q = round(x / s) with s = max|x| / 127
  • zero-point (asymmetric): adds an offset, for ranges not centred on 0, like ReLU outputs

Choices that matter:

  • granularity: one scale per tensor, per output channel, or per block
  • outliers: a single huge value forces a big scale, so all the small values collapse to a few integers. LLM.int8() (Dettmers et al., 2022) found such outlier features in large Transformers and handled them separately in higher precision.
import torch, torch.nn as nn, time
torch.manual_seed(0)

def absmax_int8(x):
    s = x.abs().max() / 127
    return (x / s).round().clamp(-127, 127) * s

def zeropoint_uint8(x):
    lo, hi = x.min(), x.max()
    s = (hi - lo) / 255; z = (-lo / s).round()
    return ((x / s + z).round().clamp(0, 255) - z) * s

w = torch.randn(256, 256) * 0.02
print(f"absmax int8 error   : {(absmax_int8(w) - w).abs().mean():.2e}")
relu_out = torch.relu(torch.randn(10000))
print(f"ReLU outputs, absmax {(absmax_int8(relu_out) - relu_out).abs().mean():.2e} vs zero-point {(zeropoint_uint8(relu_out) - relu_out).abs().mean():.2e}")

w_out = w.clone(); w_out[0, 0] = 2.0                     # one outlier
per_tensor = absmax_int8(w_out)
per_channel = torch.stack([absmax_int8(row) for row in w_out])
mask = torch.ones_like(w_out, dtype=torch.bool); mask[0] = False
print(f"with one outlier -> per-tensor error {(per_tensor - w_out)[mask].abs().mean():.2e} | per-channel {(per_channel - w_out)[mask].abs().mean():.2e}")

model = nn.Sequential(nn.Linear(1024, 4096), nn.ReLU(), nn.Linear(4096, 1024)).eval()
qmodel = torch.ao.quantization.quantize_dynamic(model, {nn.Linear}, dtype=torch.qint8)
x = torch.randn(64, 1024)
with torch.no_grad():
    rel = ((qmodel(x) - model(x)).norm() / model(x).norm()).item()
print(f"dynamic int8 model: relative output error {rel:.3%}")

Output:

absmax int8 error   : 1.80e-04
ReLU outputs, absmax 4.15e-03 vs zero-point 2.08e-03
with one outlier -> per-tensor error 3.94e-03 | per-channel 1.19e-04
dynamic int8 model: relative output error 2.398%

#llm#quantisation#inference

Sources

  1. Dettmers et al. (2022), LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale — arXiv
  2. PyTorch: Quantization — PyTorch
  3. Hugging Face: Quantization concepts — Hugging Face