Conv3d
_QuantizedConvNdConv3d(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 3-D convolution — int8 kernel, dequantize-to-float compute.
The volumetric member of the quantized conv family (video, 3-D medical
imaging), and the inference-time replacement that
lucid.quantization.convert installs for a calibrated float
lucid.nn.Conv3d. Volumetric kernels are the largest weights in a
3-D net, so the int8 checkpoint / working-set savings matter most here.
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.conv3d, 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, kd, kh, kw) 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, kd, kh, kw).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.ConvReLU3doverrides 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.Conv3d(3, 8, 3))
>>> model.qconfig = Q.get_default_qconfig()
>>> prepared = Q.prepare(model) # insert observers
>>> _ = prepared(lucid.randn(1, 3, 8, 16, 16)) # calibrate on representative data
>>> qmodel = Q.convert(prepared) # float Conv3d -> quantized Conv3d
>>> type(qmodel[0]).__name__
'Conv3d'
>>> qmodel(lucid.randn(1, 3, 8, 16, 16)).shape # int8-weight inference
(1, 8, 6, 14, 14)
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.Conv3d(3, 8, (3, 3, 3), (1, 1, 1), (0, 0, 0), (1, 1, 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.Conv1d—The 1-D sibling (per-channel int8 kernel).
- lucid.nn.quantized.ConvReLU3d—Quantized 3-D conv with a fused ReLU.
- lucid.quantization.convert—Installs this layer from a calibrated float model.