ConvTranspose2d
_QuantizedConvTransposeNdConvTranspose2d(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 2-D transposed (fractionally-strided) convolution.
The upsampling counterpart of lucid.nn.quantized.Conv2d, and the
int8-weight, dequantize-to-float replacement that
lucid.quantization.convert installs for a calibrated float
lucid.nn.ConvTranspose2d. It is the standard decoder / up-projection
layer of segmentation heads and generative up-samplers, and shares the sidecar
recipe of the quantized lucid.nn.quantized.Conv2d.
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_transpose2d, 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, 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 wide
kernels. The scheme is symmetric (qint8, zero_point = 0), matching
the reference default for transposed weights; 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, 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, 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.ConvTranspose2d(16, 8, 2, stride=2))
>>> model.qconfig = Q.get_default_qconfig()
>>> prepared = Q.prepare(model) # insert observers
>>> _ = prepared(lucid.randn(1, 16, 8, 8)) # calibrate on representative data
>>> qmodel = Q.convert(prepared) # float -> quantized ConvTranspose2d
>>> type(qmodel[0]).__name__
'ConvTranspose2d'
>>> qmodel(lucid.randn(1, 16, 8, 8)).shape # int8-weight 2x upsampling
(1, 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.ConvTranspose2d(
... 16, 8, (2, 2), (2, 2), (0, 0), (0, 0), (1, 1), 1, True
... )
>>> bool((broken.weight_int8 == 0).all().item())
TrueSee Also
- lucid.nn.quantized.ConvTranspose1d—The 1-D upsampling sibling.
- lucid.nn.quantized.ConvTranspose3d—The volumetric upsampling sibling.
- lucid.nn.quantized.Conv2d—The (down-sampling) 2-D convolution counterpart.
- lucid.quantization.convert—Installs this layer from a calibrated float model.