ObserverBase
ModuleObserverBase(qscheme: QScheme, qdtype: QDtype, ch_axis: int | None = None, eps: float = 1e-08)Abstract base for calibration observers that yield (scale, zero_point).
An observer is the calibration workhorse of the quantization subsystem: an
lucid.nn.Module that watches a tensor stream during a calibration pass,
accumulates a summary statistic (a running min/max, a channel-wise range, or a value
histogram), and on demand distills that statistic into the (scale, zero_point)
pair a quantized tensor needs. Its forward is the identity — it observes x
and returns it untouched — so an observer can be spliced inline into a prepared graph
or wrapped by a FakeQuantize without perturbing numerics. Concrete
subclasses override forward (to fold each batch into the running statistic)
and calculate_qparams (to reduce that statistic to qparams).
What calculate_qparams computes. Every min/max-family subclass funnels its
observed range through the shared affine / symmetric calibration in
lucid.quantization.calculate_qparams. The range is first widened to include
real zero (so 0.0 is always representable), then mapped onto the integer grid
[q_min, q_max] of qdtype:
with a floor guarding a degenerate
(constant-input) range. Affine spends a free zero_point to pack an asymmetric
range tightly (ideal for post-ReLU activations); symmetric pins zero_point so
real 0 lands exactly on a code (the standard choice for signed weights).
Choosing a subclass. Pick along three axes. Granularity — per-tensor
(MinMaxObserver, one pair for the whole tensor) vs per-channel
(PerChannelMinMaxObserver, one pair per output channel, far tighter for
conv / linear weights whose channels differ in range). Outlier robustness — a hard
running min/max grabs every spike, an EMA variant
(MovingAverageMinMaxObserver) smooths per-batch outliers, and
HistogramObserver clips the tails to minimise L2 error on heavy-tailed
activations. Fixed-range — ops whose output range is known a priori use
FixedQParamsObserver, and dtype-only carriers use
PlaceholderObserver / NoopObserver.
Parameters
qschemeQSchemeper_tensor_affine /
per_tensor_symmetric / per_channel_affine / per_channel_symmetric.
Selects the affine-vs-symmetric branch above and whether qparams are scalar or
per-channel vectors.qdtypeQDtypequant_min / quant_max
(e.g. quint8 → [0, 255], qint8 → [-128, 127]) and the on-device
storage dtype that physically holds the integer codes.ch_axisint= NoneNone for per-tensor schemes
regardless of the value passed.epsfloat= 1e-8scale, forwarded to
lucid.quantization.calculate_qparams to avoid division by zero.Attributes
Notes
- Identity forward.
forwardreturns its input unchanged; only the side effect of updating the running buffers matters. This is what lets an observer be spliced into a prepared model transparently. - Buffer re-registration contract. Subclasses update their running statistics by
re-registering the buffer (
register_bufferwith the same name), never by plain attribute assignment: Lucid'sModule.__setattr__drops a name from_bufferson reassignment, so re-registration is the only correct in-place update — and it transparently absorbs the scalar-seed → per-channel-vector shape change on the first observation. - Not instantiated directly.
ObserverBaseis abstract; itscalculate_qparamsraisesNotImplementedError. Use a concrete subclass, orwith_argsto defer construction into alucid.quantization.QConfig. - H4 purity. Every statistic is computed on the public
lucid.*op surface — no external numeric libraries — so the same code path is correct on both the Accelerate (CPU) and MLX (GPU) streams.
Examples
>>> import lucid
>>> import lucid.quantization as Q
>>> obs = Q.MinMaxObserver() # a concrete ObserverBase subclass
>>> _ = obs(lucid.randn(64, 128)) # forward observes and returns x unchanged
>>> scale, zero_point = obs.calculate_qparams()
>>> scale.shape, zero_point.shape
((), ())
Deferring construction into a lucid.quantization.QConfig recipe:
>>> factory = Q.PerChannelMinMaxObserver.with_args(ch_axis=0)
>>> obs2 = factory() # zero-arg factory builds a fresh observer
>>> type(obs2).__name__
'PerChannelMinMaxObserver'See Also
- lucid.quantization.MinMaxObserver—Per-tensor running min/max (the common default).
- lucid.quantization.PerChannelMinMaxObserver—Per-channel weight observer.
- lucid.quantization.HistogramObserver—Tail-clipping observer for activations.
- lucid.quantization.calculate_qparams—The shared range →
(scale, zero_point)map. - lucid.quantization.QConfig—Pairs activation + weight observer factories.
- lucid.quantization.FakeQuantize—Wraps an observer for quantization-aware training.
Used by 4
Constructors
1Class methods
1with_args(kwargs: object = {})Return a zero-arg factory that builds this observer with kwargs.
Mirrors the reference framework's Observer.with_args ergonomic so
a lucid.quantization.QConfig can carry an observer recipe
rather than a live instance.