Int8 Weight Quantization (absmax) - Post-training & Efficiency
Implement Int8 Weight Quantization (absmax) in Python with NumPy. medium exercise with examples, hints, and instant feedback. No setup or signup required.
Int8 Weight Quantization (absmax)
What you'll build
Symmetric (absmax) int8 quantization of a weight matrix, per output channel and per tensor; dequantization; an int8 matmul that rescales an int32 accumulator; and a report comparing the two schemes' errors.
Background
Storing weights as int8 instead of float32 cuts memory by 4×, and int8 matmuls are fast on modern hardware. Absmax quantization maps the largest magnitude to ±127 and rounds everything else to the nearest step. The catch is outliers: one large weight sets a large step for everything sharing its scale. Giving each output row its own scale (per-channel, the "vector-wise" quantization of LLM.int8()) contains that damage.
Formula
For a group of weights w that share one scale:
scale = max(max|w|, 1e-8) / 127
w_q = clip(round(w / scale), -127, 127) stored as int8
w_hat = w_q · scale dequantized
Rounding is round-half-to-even (np.round / torch.round), so the per-weight error is at most scale / 2.
- Per-tensor: one scale for the whole matrix.
- Per-channel: one scale per row of W, which has shape (d_out, d_in).
Int8 matmul for y = x · Wᵀ:
- Quantize the activations per row of x (one scale per token): x_q of shape (n, d_in) and s_x of shape (n,).
- Accumulate the integer products in int32: acc = x_q · W_qᵀ, shape (n, d_out).
- Rescale with both scales: y ≈ acc · s_x[:, None] · s_w[None, :].
Inputs
W: (d_out, d_in) float arrayx: (n, d_in) float arrayW_q: (d_out, d_in) int8 array;w_scales/scales: (d_out,) float array (or a Python float indequantize)
Expected return format
quantize_per_channel(W)returns a tuple(W_q, scales): an int8 array of shape (d_out, d_in) and a float array of shape (d_out,)quantize_per_tensor(W)returns(W_q, scale): an int8 array and a Python floatdequantize(W_q, scales)returns a float array of W_q's shape;scalesis a (d_out,) array (one per row) or a scalarint8_matmul(x, W_q, w_scales)returns an (n, d_out) float array approximating x · Wᵀquantization_report(W)returns a dictionary:'per_channel_error': max |W - dequantize(*quantize_per_channel(W))| as a Python float'per_tensor_error': max |W - dequantize(*quantize_per_tensor(W))| as a Python float
Edge cases and conventions
- An all-zero row must not divide by zero: the 1e-8 floor on the absmax keeps it at 0.
- The symmetric range is [-127, 127]; -128 is never used.
- Exact halves round to even, e.g. 63.5 becomes 64 and 62.5 becomes 62.
Constraints
NumPy only, vectorized. Cast to int32 before the integer matmul: int8 products overflow int8 immediately.
Follow-up
- Why do LLM activations need special care (outlier features), and how does LLM.int8() handle them with mixed precision?
- How do zero-point (asymmetric) quantization and 4-bit group-wise quantization (e.g. block size 64) change the error?
Examples
Input: quantize_per_channel(np.array([[0.7, -1.2, 0.3], [2.0, 0.1, -0.5]])) Output: (array([[74, -127, 32], [127, 6, -32]], dtype=int8), array([0.009449, 0.015748]))
Each row's largest magnitude maps to ±127: scales are 1.2/127 and 2.0/127, and 0.7/0.009449 = 74.08 rounds to 74
Input: quantization_report(np.array([[0.01, -0.02, 0.013], [5.0, -3.0, 1.7]]))
Output: {'per_channel_error': 0.007874, 'per_tensor_error': 0.01937}With one scale (5/127 = 0.039) the small first row quantizes to [0, -1, 0] and is nearly erased; per-row scales keep it
Practice in Python
import numpy as np
def quantize_per_channel(W):
"""
Absmax-quantize each row of W with its own scale.
Args:
W: (d_out, d_in) float weights
Returns:
(W_q, scales): W_q int8 (d_out, d_in), scales float (d_out,)
with scales = max(row absmax, 1e-8) / 127
"""
# Your code here
pass
def quantize_per_tensor(W):
"""
Absmax-quantize all of W with one scale.
Returns:
(W_q, scale): W_q int8 with W's shape, scale a Python float
"""
# Your code here
pass
def dequantize(W_q, scales):
"""
Map int8 values back to floats.
Args:
W_q: (d_out, d_in) int8 array
scales: (d_out,) per-row scales, or a single scalar
Returns:
(d_out, d_in) float array W_q * scale
"""
# Your code here
pass
def int8_matmul(x, W_q, w_scales):
"""
Approximate x @ W.T with int8 operands.
Args:
x: (n, d_in) float activations (quantize per row)
W_q: (d_out, d_in) int8 weights
w_scales: (d_out,) per-row weight scales
Returns:
(n, d_out) float array
"""
# Your code here
pass
def quantization_report(W):
"""Returns {'per_channel_error': float, 'per_tensor_error': float} (max abs errors)."""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 8 tests, or use the progressive hints.