ConvTranspose1d
_QuantizedConvTransposeNdConvTranspose1d(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 1-D transposed (fractionally-strided) convolution.
The int8-weight, dequantize-to-float replacement that
lucid.quantization.convert installs for a calibrated float
lucid.nn.ConvTranspose1d. Transposed convolutions upsample — they
are the decoder / generator counterpart of lucid.nn.quantized.Conv1d
in 1-D signal models — and share the exact 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_transpose1d, 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, k) 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, k).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, k)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.ConvTranspose1d(8, 4, 3))
>>> model.qconfig = Q.get_default_qconfig()
>>> prepared = Q.prepare(model) # insert activation/weight observers
>>> _ = prepared(lucid.randn(2, 8, 16)) # calibrate on representative data
>>> qmodel = Q.convert(prepared) # float -> quantized ConvTranspose1d
>>> type(qmodel[0]).__name__
'ConvTranspose1d'
>>> qmodel(lucid.randn(2, 8, 16)).shape # int8-weight upsampling
(2, 4, 18)
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.ConvTranspose1d(
... 8, 4, (3,), (1,), (0,), (0,), (1,), 1, True
... )
>>> bool((broken.weight_int8 == 0).all().item())
TrueSee Also
- lucid.nn.quantized.ConvTranspose2d—The 2-D upsampling sibling.
- lucid.nn.quantized.Conv1d—The (down-sampling) 1-D convolution counterpart.
- lucid.quantization.convert—Installs this layer from a calibrated float model.