The atomic (activation, weight) observer / fake-quant recipe.
A QConfig is the smallest unit of quantization policy: it pairs the
recipe for how activations are observed / fake-quantized with the recipe for
how weights are. It carries no state of its own — only two factories. When
lucid.quantization.prepare (static PTQ) or
lucid.quantization.prepare_qat (QAT) walks a model and decides a module
should be quantized, it calls qconfig.activation() and qconfig.weight() to
mint a fresh observer (PTQ) or lucid.quantization.FakeQuantize (QAT)
for that specific insertion site. One QConfig therefore parameterizes the
quantization of an entire network without any two layers sharing observer state.
Conceptually it is the direct analogue of an AMP autocast policy: a small,
declarative object the framework consults while rewriting the graph, rather than
something that participates in the forward pass itself. A QConfigMapping
sits one level above and answers the orthogonal question of which QConfig
applies to which module.
Why factories, not instances. Both fields hold zero-arg callables — almost
always produced by Observer.with_args(...) or FakeQuantize.with_args(...),
which capture the desired lucid.quantization.QScheme /
lucid.quantization.QDtype / ch_axis and defer construction. Storing
factories (not built modules) is what lets one recipe instantiate independent,
per-layer observers across the whole model; sharing a single observer instance
between layers would conflate their statistics and corrupt calibration.
Attributes
activationCallable[..., nn.Module]weightCallable[..., nn.Module]qint8).Notes
- Factory semantics (
with_args).HistogramObserver.with_args(qscheme=..., qdtype=...)returns a zero-arg callable (a captured partial); calling it yields a configured observer. This is why the two fields are typedCallable[..., nn.Module]and notnn.Module. - PTQ vs QAT. For static PTQ the factories build plain observers (which only
record statistics); for QAT they build
lucid.quantization.FakeQuantizemodules (which also apply straight-through rounding so gradients see the quantization error). The two default recipes differ only in that wrapper. - Pairing, not a scalar. Activations and weights almost always want different schemes — activations are asymmetric (post-ReLU ranges start at 0, hence per-tensor-affine) while weights are near-zero-mean (hence per-channel-symmetric). Bundling both keeps that pairing explicit and consistent.
- Being a
typing.NamedTuple, aQConfigis immutable and unpacks positionally asactivation, weight = qconfig.
Examples
Build a recipe by hand from two with_args factories:
>>> import lucid.quantization as Q
>>> from lucid.quantization.observer import (
... HistogramObserver, PerChannelMinMaxObserver)
>>> qcfg = Q.QConfig(
... activation=HistogramObserver.with_args(
... qscheme=Q.per_tensor_affine, qdtype=Q.quint8),
... weight=PerChannelMinMaxObserver.with_args(
... ch_axis=0, qscheme=Q.per_channel_symmetric, qdtype=Q.qint8),
... )
>>> act_observer = qcfg.activation() # fresh observer for one insertion site
>>> wt_observer = qcfg.weight() # independent observer, no shared state
>>> type(act_observer).__name__
'HistogramObserver'
In practice the defaults cover the common case, so hand-building is rare:
>>> qcfg = Q.get_default_qconfig() # static-PTQ recipe
>>> qat = Q.get_default_qat_qconfig() # QAT recipe (FakeQuantize-wrapped)See Also
QConfigMapping—Decides whichQConfigapplies to which module.get_default_qconfig—The standard static-PTQ recipe.get_default_qat_qconfig—The standard QAT recipe.- lucid.quantization.FakeQuantize—The QAT observer + straight-through wrapper.