calculate_qparams(min_val: _RangeLike, max_val: _RangeLike, qscheme: QScheme, qdtype: QDtype, eps: float = 1e-08)Derive (scale, zero_point) from an observed value range.
The shared calibration math behind every observer. Given the [min_val, max_val]
range an observer recorded, it solves for the affine map
that packs that range into the target
grid [q_\min, q_\max]. The observed range is first extended to include zero
(min <- min(min, 0), max <- max(max, 0)) so that real 0 is always exactly
representable — a property the padding / masking paths rely on.
The scheme's symmetry then selects the formula:
\textbf{affine:}\quad s = \frac{\max - \min}{q_\max - q_\min},\qquad z = \operatorname{clip}\!\bigl(q_\min - \operatorname{round}(\min / s),\; q_\min,\; q_\max\bigr) \textbf{symmetric:}\quad s = \frac{\max(\lvert\min\rvert,\ \lvert\max\rvert)}{(q_\max - q_\min) / 2}, \qquad z = \begin{cases} 0 & \text{signed} \\ \lfloor (q_\max + q_\min + 1)/2 \rfloor & \text{unsigned} \end{cases}scale is floored at eps in every branch so a degenerate (constant) range
never yields a zero or infinite step. The whole computation is elementwise, so
scalar min_val / max_val produce per-tensor (scalar) parameters and
length-C inputs produce per-channel (vector) parameters — the function does not
itself branch on granularity, only on symmetry.
Parameters
min_valTensor or floatC vectors for a per-channel scheme. Plain Python floats
are lifted to scalar tensors.max_valTensor or floatC vectors for a per-channel scheme. Plain Python floats
are lifted to scalar tensors.qschemeQSchemeQScheme.is_symmetric flag is consulted
here (granularity is implied by the shape of min_val / max_val).qdtypeQDtypequant_min / quant_max and signed.epsfloat= 1e-8scale to avoid a division-by-zero step on a degenerate
(constant-valued) range.Returns
Notes
- Zero is always representable. Extending the range to include 0 guarantees a code for real 0, which matters wherever padding / masking injects exact zeros.
- Symmetric
zero_point. Signed grids pin it to 0 (so integer matmuls need no weight-offset correction); unsigned grids pin it to the grid midpoint. - Granularity is shape-driven. There is no per-channel branch — passing vectors in yields vectors out, so a single code path serves both granularities.
- Mirrors the reference framework's calibration but stays entirely on Lucid tensor ops (no external numeric library).
Examples
>>> import lucid.quantization as Q
>>> # affine quint8 over an asymmetric activation range:
>>> s, z = Q.calculate_qparams(-2.0, 6.0, Q.per_tensor_affine, Q.quint8)
>>> round(float(s.item()), 4), int(z.item()) # (6 - (-2)) / 255, then clip
(0.0314, 64)
>>> # symmetric qint8 weight — zero_point pinned to 0:
>>> s, z = Q.calculate_qparams(-0.8, 0.5, Q.per_channel_symmetric, Q.qint8)
>>> int(z.item())
0
>>> # degenerate (constant) range floors scale at eps, never 0:
>>> s, _ = Q.calculate_qparams(3.0, 3.0, Q.per_tensor_affine, Q.qint8)
>>> bool(s.item() > 0)
TrueSee Also
QParams—Bundles the returned(scale, zero_point)with its scheme / dtype.- lucid.quantization.QScheme—Selects affine vs symmetric here.
- lucid.quantization.QDtype—Supplies
quant_min/quant_max/signed. - lucid.quantization.quantize—Applies the derived parameters to produce codes.