Conv1d
_QuantizedConvNdConv1d(in_channels: int, out_channels: int, kernel_size: _IntTuple, stride: _IntTuple, padding: _IntTuple, dilation: _IntTuple, groups: int, bias: bool)Implementing kernel
C++ engine symbols that back this Python API.Quantized 1-D convolution — int8 kernel, dequantize-to-float compute.
The inference-time replacement that lucid.quantization.convert
installs in place of a calibrated float lucid.nn.Conv1d. It is the
quantized member of the 1-D conv family — audio / time-series front ends,
1-D residual stacks, token-mixing convolutions — and shares the exact sidecar
recipe of the quantized lucid.nn.quantized.Linear.
Representation (sidecar design B). The learned float kernel is quantized
once, at from_float time, into an int8 code tensor plus a
per-output-channel scale / zero_point; the float kernel is then
dropped. Only the int8 codes, the qparams, and the (still-float) bias live in
the module, so the checkpoint is far smaller than the float layer's. Each
forward dequantizes the kernel back to float, runs the ordinary
F.conv1d, and finally fake-quantizes the output onto the activation grid
that calibration observed. Keeping the convolution itself in float means the
result matches a genuine int8 kernel to within a rounding step and the
layer runs unchanged on any device, with no int8 conv kernel required.
Per-channel weights. The kernel (out_channels, in_channels/groups, k)
is quantized independently for every output channel (axis 0): each filter
carries its own scale and zero-point. Because different output filters often
span very different dynamic ranges, per-channel quantization tracks them far
more tightly than a single per-tensor scale, and is the standard
accuracy-preserving choice for convolution kernels. The bias is small and
precision-sensitive, so it is left in float.
Encoding happens once (at from_float); decode + convolve run on every
forward:
where are the per-output-channel weight scale / zero-point
(index runs over out_channels) and the scalar
calibrated output (scale, zero_point) with grid bounds
(0, 255 for the default quint8). The
decode → float-conv → re-quantize round-trip is what makes the output
bit-equivalent to a true int8 kernel without needing one.
Parameters
in_channelsintout_channelsintkernel_sizeint or tuple of intstrideint or tuple of intpaddingint or tuple of intdilationint or tuple of intgroupsintbiasboolAttributes
weight_int8Tensorint8 kernel codes, shape (out_channels, in_channels/groups, k).weight_scaleTensor(out_channels,).weight_zero_pointTensor(out_channels,).scaleTensorzero_pointTensorbiasTensor or None(out_channels,), or None.Notes
- Instances are normally produced by
lucid.quantization.convert(orfrom_float), not constructed directly: a freshly constructed layer has identity qparams (scale = 1,zero_point = 0) and zeroed kernel codes, and only becomes meaningful oncefrom_float/load_state_dictfills the buffers. - Only int8 codes + qparams + float bias enter the
state_dict; the float kernel never does. The weight payload shrinks ~3.55x(int8) and a whole model checkpoint ~3.97x(measured onresnet_18, a conv-heavy net). - The kernel is quantized per-output-channel on axis 0 — each filter gets its own scale / zero-point, far tighter than one per-tensor scale for wide kernels; the bias stays float.
- This layer wins on memory, not compute — the convolution runs in float
(CPU stream = Accelerate, GPU stream = MLX), so its speed tracks the float
layer's. There is no weight-only int8 conv GEMM analogue of
lucid.nn.quantized.QuantizedLinearMLX; the payoff is the smaller checkpoint and working set. - Device-transparent: because compute stays in float, the layer runs unchanged on CPU or Metal and follows the input tensor's device.
- Calibration is required — the output
(scale, zero_point)come from an activation observer that must see representative data betweenprepareandconvert; an uncalibrated observer keeps its±infseed, collapsingscaletowardepsand the output toward zero (from_floatwarns loudly in that case). - Only integer / tuple padding with
padding_mode="zeros"is supported; string"same"/"valid"padding raisesNotImplementedErroratfrom_float. _activationis the identity here; the fusedlucid.nn.quantized.ConvReLU1doverrides it to apply ReLU before the output fake-quant so calibration and inference see the same post-ReLU range.
Examples
>>> import lucid, lucid.nn as nn
>>> import lucid.quantization as Q
>>> model = nn.Sequential(nn.Conv1d(8, 16, 3))
>>> model.qconfig = Q.get_default_qconfig()
>>> prepared = Q.prepare(model) # insert activation/weight observers
>>> _ = prepared(lucid.randn(4, 8, 32)) # calibrate on representative data
>>> qmodel = Q.convert(prepared) # float Conv1d -> quantized Conv1d
>>> type(qmodel[0]).__name__
'Conv1d'
>>> qmodel(lucid.randn(4, 8, 32)).shape # int8-weight inference
(4, 16, 30)
Constructing the layer directly is **not** a substitute for that workflow — a
bare instance carries zeroed codes and identity qparams, so it returns garbage
until from_float / load_state_dict populates the buffers:
>>> broken = nn.quantized.Conv1d(8, 16, (3,), (1,), (0,), (1,), 1, True)
>>> bool((broken.weight_int8 == 0).all().item())
TrueSee Also
- lucid.nn.quantized.Conv2d—The 2-D workhorse (per-channel int8 kernel).
- lucid.nn.quantized.ConvReLU1d—Quantized 1-D conv with a fused ReLU.
- lucid.nn.quantized.Linear—The fully-connected analogue (per-channel int8).
- lucid.quantization.convert—Installs this layer from a calibrated float model.