How (scale, zero_point) map real values onto the integer grid.
A quantization scheme fixes two independent choices about the affine map
that turns a real value x into an
integer code q:
- Granularity — is there one
(scale, zero_point)for the whole tensor (per-tensor), or one pair per channel along a chosen axis (per-channel)? Per-channel tracks layers whose channels span very different magnitudes far more tightly, at the cost of a small vector of parameters instead of a scalar. - Symmetry — is
zero_pointfree (affine / asymmetric), so the grid can straddle an arbitrary[min, max], or is it pinned (symmetric) so that real 0 maps exactly to a fixed code and the grid is centred on zero?
The four members are the cross-product of the two useful granularities with the two
symmetries. Four module-level aliases in lowercase mirror the members so call sites
read naturally, e.g. qscheme=lucid.quantization.per_channel_symmetric.
Notes
PER_TENSOR_AFFINE
One (scale, zero_point) for the whole tensor with a free zero_point.
The asymmetric grid fits ranges that do not straddle zero (e.g. post-ReLU
activations, all ) without wasting codes on an unused half. The
default activation scheme. Exposed as per_tensor_affine.
PER_TENSOR_SYMMETRIC
One scale for the whole tensor with zero_point pinned (0 for a
signed grid, mid-range for unsigned), so real 0 maps exactly to a code and the
grid is symmetric about zero. Exposed as per_tensor_symmetric.
PER_CHANNEL_AFFINE
One (scale, zero_point) per channel along ch_axis with a free
zero_point — rarely needed, for weights whose per-channel ranges are
strongly skewed. Exposed as per_channel_affine.
PER_CHANNEL_SYMMETRIC
One scale per channel with a pinned zero_point — the standard,
accuracy-preserving choice for convolution / linear weights, and the form
the low-precision GEMM kernels expect for their weight operand. Exposed as
per_channel_symmetric.
- Affine vs symmetric, precisely. Affine derives
scalefrom the fullmax - minspan and solves forzero_pointso the endpoints land on grid bounds; symmetric derivesscalefrommax(|min|, |max|)over a half-range and fixeszero_point— seelucid.quantization.calculate_qparams. - Why symmetric weights. Pinning
zero_point = 0removes the cross-terms in an integer matmul (aw_q · x_qproduct needs no weight-offset correction), which is exactly what the packed low-precision GEMM assumes for its weight side. - The two boolean helpers
is_per_channel/is_symmetriclet callers branch on granularity / symmetry without matching individual members.
Examples
>>> import lucid.quantization as Q
>>> Q.per_channel_symmetric is Q.QScheme.PER_CHANNEL_SYMMETRIC
True
>>> Q.per_channel_symmetric.is_per_channel
True
>>> Q.per_channel_symmetric.is_symmetric
True
>>> Q.per_tensor_affine.is_symmetric
FalseSee Also
QDtype—The companion descriptor that fixes the integer grid (bits / range).- lucid.quantization.calculate_qparams—Derives the scale + zero-point per this scheme.
- lucid.quantization.QParams—Bundles a scheme with its derived parameters.
Used by 4
Properties
2True for the two per-channel schemes, False for per-tensor.
Lets calibration / quantization code branch on granularity — a per-channel
scheme derives one (scale, zero_point) per channel along ch_axis, so
the qparams are length-C vectors rather than scalars.
True when zero_point is pinned (the symmetric schemes).
A symmetric scheme fixes zero_point (to 0 for a signed grid, or the grid
midpoint for unsigned) and derives scale from max(|min|, |max|), so
real 0 always maps exactly to a code; an affine scheme instead solves for a
free zero_point. See lucid.quantization.calculate_qparams.