ConvTranspose3d
_QuantizedConvTransposeNdConvTranspose3d(in_channels: int, out_channels: int, kernel_size: _IntTuple, stride: _IntTuple, padding: _IntTuple, output_padding: _IntTuple, dilation: _IntTuple, groups: int, bias: bool)Implementing kernel
C++ engine symbols that back this Python API.Quantized 3-D transposed (fractionally-strided) convolution.
The volumetric upsampling layer (video / 3-D medical decoders), and the
int8-weight, dequantize-to-float replacement that
lucid.quantization.convert installs for a calibrated float
lucid.nn.ConvTranspose3d. It is the up-projection counterpart of
lucid.nn.quantized.Conv3d, and shares the sidecar recipe of the
quantized lucid.nn.quantized.Conv2d family.
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. Each forward dequantizes the kernel back to float, runs the
ordinary F.conv_transpose3d, and finally fake-quantizes the output onto
the activation grid that calibration observed. Keeping the transposed
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.
Per-channel weights (axis 1). The transposed-weight layout (in_channels, out_channels/groups, kd, kh, kw) places the output channels on axis 1,
not axis 0, so the kernel is quantized per-channel along axis 1 — each output
channel gets its own scale, far tighter than one per-tensor scale for the wide
dynamic range of volumetric kernels. The scheme is symmetric (qint8,
zero_point = 0), matching the reference default; the bias stays float.
Encoding happens once (at from_float); decode + transposed-convolve run
on every forward:
where is the per-output-channel (axis-1) weight scale — with a zero
zero-point because the weight scheme is symmetric — and the scalar
calibrated output (scale, zero_point) with grid bounds
(0, 255 for the default quint8).
Parameters
in_channelsintout_channelsintkernel_sizeint or tuple of intstrideint or tuple of intpaddingint or tuple of intoutput_paddingint or tuple of intdilationint or tuple of intgroupsintbiasboolAttributes
weight_int8Tensorint8 kernel codes, shape
(in_channels, out_channels/groups, kd, kh, kw).weight_scaleTensor(out_channels/groups,).weight_zero_pointTensor(out_channels/groups,) —
all zero under the symmetric scheme.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 conv-heavy checkpoint ~3.97x(measured onresnet_18). - The kernel is quantized per-channel on axis 1 (the output-channel axis
of the
(in, out/groups, kd, kh, kw)layout) with a symmetricqint8scheme (from_floatruns a freshPerChannelMinMaxObserverover the weight); the bias stays float. - This layer wins on memory, not compute — the transposed convolution runs in float (CPU stream = Accelerate, GPU stream = MLX), so its speed tracks the float layer's; 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, collapsing the output toward zero (from_floatwarns). - Only integer / tuple padding is supported; string padding raises
NotImplementedErroratfrom_float.
Examples
>>> import lucid, lucid.nn as nn
>>> import lucid.quantization as Q
>>> model = nn.Sequential(nn.ConvTranspose3d(8, 4, 2, stride=2))
>>> model.qconfig = Q.get_default_qconfig()
>>> prepared = Q.prepare(model) # insert observers
>>> _ = prepared(lucid.randn(1, 8, 4, 8, 8)) # calibrate on representative data
>>> qmodel = Q.convert(prepared) # float -> quantized ConvTranspose3d
>>> type(qmodel[0]).__name__
'ConvTranspose3d'
>>> qmodel(lucid.randn(1, 8, 4, 8, 8)).shape # int8-weight 2x upsampling
(1, 4, 8, 16, 16)
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.ConvTranspose3d(
... 8, 4, (2, 2, 2), (2, 2, 2), (0, 0, 0), (0, 0, 0), (1, 1, 1), 1, True
... )
>>> bool((broken.weight_int8 == 0).all().item())
TrueSee Also
- lucid.nn.quantized.ConvTranspose2d—The 2-D upsampling sibling.
- lucid.nn.quantized.Conv3d—The (down-sampling) 3-D convolution counterpart.
- lucid.quantization.convert—Installs this layer from a calibrated float model.