FakeQuantize
ModuleFakeQuantize(observer: type[ObserverBase] = MovingAverageMinMaxObserver, observer_kwargs: object = {})Observer + straight-through fake-quantization, toggleable for QAT.
The building block of quantization-aware training (QAT). A FakeQuantize sits
on a weight or activation and, on every forward, does two things: it lets its wrapped
observer refresh (scale, zero_point) from the live statistics, then it applies
lucid.quantization.fake_quantize — rounding the tensor onto the integer grid
and back to float — so the downstream graph sees the quantization error while
training still runs in float. This is what lets a network learn weights that are
robust to the eventual int8 inference numerics: the forward simulates quantization,
and a straight-through estimator (STE) keeps the operation differentiable so the
rounding does not block gradients.
Observer vs fake-quant, decoupled. The two behaviours are independently gated by
enable_observer / enable_fake_quant, which drives the standard QAT
schedule: observe + fake-quant early so statistics settle and the net adapts, then
disable_observer to freeze the grid once ranges converge (and, in fused
stacks, freeze BN stats). With the observer off but fake-quant on, the layer keeps
quantizing against the last-learned qparams; with fake-quant off it is a pure
passthrough (the observer may still watch).
The straight-through fake-quant. Writing for the current
(scale, zero_point) and [q_\min, q_\max] for the grid, the forward
computes the quantize→dequantize round-trip in the float domain:
The step has a zero (a.e.) derivative, which would kill training, so the backward substitutes the straight-through estimator — gradient passes through unchanged inside the grid and is zeroed where the code saturates:
\frac{\partial \hat{x}}{\partial x} = \begin{cases} 1 & q_\min \le \operatorname{round}(x/S) + Z \le q_\max \\ 0 & \text{otherwise} \end{cases}so . The qparams are treated as constants of the step (they are refreshed by the observer, not learned through this gradient).
Parameters
(scale, zero_point) are derived. The default matches the activation
recipe of lucid.quantization.get_default_qat_qconfig.**observer_kwargsobject= {}qscheme / qdtype /
ch_axis / averaging_constant — so one FakeQuantize can wrap any
observer configuration (per-tensor activation or per-channel weight).Attributes
activation_post_processObserverBasecalculate_qparams supplies the
grid on each forward.scaleTensorC for per-channel) quantization step, refreshed from the
observer while observing is enabled. Seeded to 1.0.zero_pointTensorscale. Seeded to 0.0.qdtypeQDtypeqschemeQSchemech_axisint or NoneNone for per-tensor),
passed to lucid.quantization.fake_quantize so per-channel qparams
broadcast correctly.Notes
- QAT schedule. Typical use: start with both toggles on; call
disable_observeronce activation ranges stabilise to freeze the grid, so the final quantized weights are trained against fixed qparams. - STE saturation mask. The backward zeroes gradient for values whose code lands
outside
[q_min, q_max]— saturated activations receive no learning signal, which nudges the observer's range to cover them. - Buffer re-registration. Refreshed
scale/zero_pointare written back viaregister_buffer(same name), the same in-place-update contract the observers use, so the qparams persist throughstate_dict. - Per-channel weights. When wrapping a per-channel observer (
ch_axisset), the fake-quant applies a distinct(scale, zero_point)per channel — the QAT analogue of the per-channel int8 weight used at inference bylucid.nn.quantized.Linear. - Default QAT recipe.
lucid.quantization.get_default_qat_qconfigbuilds oneFakeQuantizefor activations (wrappinglucid.quantization.MovingAverageMinMaxObserver) and one for weights (wrappinglucid.quantization.PerChannelMinMaxObserver). - Use
with_argsto defer construction into alucid.quantization.QConfigas a zero-arg factory.
Examples
>>> import lucid
>>> import lucid.quantization as Q
>>> fq = Q.FakeQuantize() # wraps MovingAverageMinMaxObserver
>>> y = fq(lucid.randn(8, 16)) # observe, then round-trip through grid
>>> y.shape
(8, 16)
>>> bool(fq.scale.item() > 0) # qparams refreshed from the observer
True
Freezing the observer holds the grid fixed while fake-quant keeps applying it — the
QAT "freeze ranges near convergence" step:
>>> _ = fq.disable_observer()
>>> frozen = fq.scale.item()
>>> _ = fq(lucid.randn(8, 16) * 100.0) # a wild batch cannot move the grid now
>>> fq.scale.item() == frozen
True
Turning fake-quant off makes the module a pure passthrough (identity):
>>> _ = fq.disable_fake_quant()
>>> x = lucid.randn(2, 4)
>>> bool((fq(x) == x).all().item())
TrueSee Also
- lucid.quantization.fake_quantize—The underlying STE round-trip primitive.
- lucid.quantization.MovingAverageMinMaxObserver—Default wrapped activation observer.
- lucid.quantization.PerChannelMinMaxObserver—Default wrapped weight observer.
- lucid.quantization.get_default_qat_qconfig—Builds the default QAT FakeQuantize pair.
- lucid.nn.quantized.Linear—The int8 inference layer QAT trains toward.
Used by 8
Constructors
1Class methods
1with_args(kwargs: object = {})Return a zero-arg factory building this module with kwargs.
Instance methods
6Delegate to the wrapped observer.
disable_fake_quant()Pass activations through unchanged (observer may still run).
disable_observer()Freeze the observer (stop refreshing scale / zero_point).
enable_fake_quant(enabled: bool = True)Enable/disable the fake-quant transform; returns self.
enable_observer(enabled: bool = True)Enable/disable statistic collection; returns self for chaining.
Refresh qparams (if observing) then fake-quantize (if enabled).