Repository navigation
Add Conv1DTranspose and Conv2DTranspose to the Vivado and Vitis backends - #1597
Conversation
… alpha and PyTorch output_size
|
Given that QKerasV3 had a new release with QConv1dTranspose, this can be reviewed |
|
From Claude: ReviewThanks for this. The design is solid. Each kernel tap belongs to exactly one output phase, so a block of up to Issues1. QKeras-v3 transposed convolutions lose
keras.layers.Conv1DTranspose(3, 3, strides=2, output_padding=1).get_config() # no 'output_padding' on 3.14.1So
The Reading the attribute from the layer fixes it. With this change, all 8 config = layer.get_config()
# Keras < 3.15 leaves output_padding out of get_config() for transposed convolutions
if 'Transpose' in layer.__class__.__name__ and config.get('output_padding') is None:
config['output_padding'] = getattr(layer, 'output_padding', None)A more general guard would also help. The handler discards the output shape the Keras 2 handler computes ( 2. Minimum The description says the minimum version would be raised once a release includes 3. Catapult and Altera silently drop the layer The new classes are added to the shared Suggestions
What I checkedBy hand:
C simulation (Vivado backend, Keras 3.15.1 on JAX, g++ 13.3). All of these matched Keras to within about 1.5e-4:
QKeras (TensorFlow 2.21, qkeras-v3 1.2.3):
The inferred result type is tighter than for an equivalent Not checked: HLS synthesis or co-simulation, the PyTorch tests (no CPU-only PyTorch was available in my environment), and CI results. |
|
Thanks for the thorough review, especially for running the extra cases. Fixed:
Left for separate PRs:
|
PLW2901, selected on this branch, flags n_out being reassigned when the call gives no output_size. The parser came in from main with fastmachinelearning#1597.
Description
This PR adds the transposed convolution layers
Conv1DTransposeandConv2DTransposeto the Vivado and Vitisbackends, for
io_parallelandio_streamand for theLatencyandResourcestrategies. VitisUnified uses theVitis templates, so it gets the layers too.
Fixes #896.
The QKeras-v3 test needs
QConv1DTranspose, which qkeras-v3 1.2.3 adds (fastmachinelearning/qkerasV3#10). Theqkeras-v3andtesting-qkeras-v3extras now require that version.How the layer is computed
With stride
s, each input position produces a block ofsoutput positions along each axis (s_h × s_win 2D).Kernel element
twrites to position(t × dilation) mod sof a block and reads the input that is(t × dilation) div spositions before the block. One block therefore needs the inputs of all kernel elements, thatis
k_h × k_w × Cvalues, and costsk_h × k_w × C × Fmultiplications: the same as one output pixel of a stride-1convolution with the same kernel, for up to
s_h × s_woutput pixels. No value is multiplied by an inserted zero.VivadoBackend._split_conv_transpose_kernelbuilds the weight matrix from the framework kernel and sets theattributes that describe the split (
slot_groups,slot_shifts,n_buf,first_block_*,n_blocks_*). They aredeclared as backend attributes and listed in
docs/ir/attributes.rst.io_parallel. As in the stock convolution, a generated function gathers the inputs of each block, and a second
generated function writes the results of a block to the output, dropping the positions that
padding='same'oroutput_paddingremove. Both are generated with fixed indices for each partition, asim2colis. With computedindices, Vivado HLS 2020.1 reached an interval of 2 instead of 1 and Vitis HLS took much longer in C synthesis.
ReuseFactoris the number of cycles for one block (Resourceaccepts up tok_h × k_w × C), andParallelizationFactoris the number of blocks computed at the same time. The interval isnumber of blocks / ParallelizationFactor × ReuseFactor.io_stream. Two processes run in a
DATAFLOWregion. The first reads one input pixel per position, keeps therows and columns that later kernel elements need, and computes a block at each input position in
ReuseFactorcycles. The second writes the output pixels in row order, one per cycle, and keeps the blocks of the current row of
blocks. The interval is the larger of
positions × ReuseFactorand the number of output pixels, so a reuse factor upto about
s_h × s_w(output pixels divided by input positions) costs no time. The depth of the FIFO between the twoprocesses is chosen in the config template by a cycle model of the two processes.
ParallelizationFactorhas noeffect in
io_stream.Binary inputs. A binary activation that hls4ml merges with the batch normalization or scale before it gives a
1-bit output, in which 0 stands for -1. The layer keeps that 1-bit input, which needs two things:
by a flag: the generated
fill_buffersets the flags inio_parallel(only for binary inputs), andio_streamcomputes them from the position.
agreements minus its own number of products, plus the bias. The stock count step (
nnet::cast) uses the number ofinputs of the whole layer and would add the bias twice, so the layer does this step itself.
Both are chosen from the product that hls4ml picked for the layer (
both_binaryordata_binary). Converting theinput to -1/+1 in an added layer in front was tried too and costs more. In 64 builds of two layers (both tools, both
I/O types and strategies, binary and 4-bit kernels), that version used 1.5 to 2.9 times the LUTs of this one in
io_parallel, 1.4 to 2.2 times inio_streamwithLatency, and 0.9 to 1.7 times inio_streamwithResource,with the same interval (one cycle longer in two builds) and up to two cycles more latency. Ternary inputs need none
of this: hls4ml stores them as 2-bit integers, in which 0 is a zero.
Frontends
padding='valid'and'same',output_padding, dilation. The output size andthe crop come from one helper,
converters/utils.py: compute_conv_transpose_output, which agrees with Keras 2.14and Keras 3.15 in 1080 cases.
alpha) differs between input channels is handled by applying the scale beforethe layer; a scale per filter is applied after the layer, as for the other layers (
QKerasFactorizeAlpha). When theinput comes from a binary activation, a power-of-two scale before the layer gets an exact fixed-point type instead
of the power-of-two type: the latter is applied as a shift, which reads the stored 0 and 1 as numbers, while a
fixed-point scale is applied by
data_binary, which reads them as -1 and +1. This is the case for the defaultbinary()andternary()kernels. The scale layer is named<layer>_input_alpha.output_paddingfrom the layer, because Keras before 3.15 leaves it out ofget_config()of transposed convolutions. It also compares the output shape that the Keras 2 handler computes withthe one Keras gives, and raises an error if they differ; this applies to every QKeras-v3 layer.
ConvTranspose1dandConvTranspose2dwith stride, padding,output_padding, dilation (alsotogether with a stride) and an
output_sizegiven toforward()as constants.groups > 1and anoutput_sizeknown only at run time are refused.
data_format='channels_first'is refused.Changes to shared code
pipeline_style.py,resource_strategy.py,im2col_codegen.py,linear.py: the two new classes are added wherethe convolution classes are listed (DATAFLOW for convolutions, the weight transpose for
Resource, the generatedfunctions, merging a linear activation into the layer).
infer_precision.py: the accumulator width uses the largest number of kernel elements that add to one output.nnet_function_stubs.h: stubs for the two generated functions.QKerasFactorizeAlpha: one check that sends a transposed layer with a scale per input channel to a new function;every other layer takes the existing path unchanged.
Not included
layer out. Refusing layers that a backend does not implement is left to a separate PR.
PixelShuffle. AConv2dfollowed byPixelShufflecan be rewritten as oneConv2DTranspose; this couldbe a follow-up.
Resourcereuse factors larger thank_h × k_w × C, which are replaced by the closest valid one with a warning,and the other strategies, which are refused.
Known limits
Latency: the tool chains several DSP adders in one clock cycle, so the 5 ns clock ismissed by 0.04 to 0.84 ns (the stock
Conv2Din some of the reference models misses it too). The same happens inio_streamResourcewith a small reuse factor in the largest layer tested (by 1.2 and 1.65 ns). A clock uncertainty of 27% fixes itin all
io_streamLatencybuilds and in all but twoio_parallelLatencybuilds that missed it, and in the 8-bitio_streamResourcebuild. The two largestio_parallellayers (about 1200 DSPs each) need 35%, and the 16-bitio_streamResourcebuildneeds 45%. In all of these builds the interval is unchanged, the latency grows by at most two clock cycles, and co-simulation gives
the same outputs as C simulation. This is documented under the known issues in
docs/backend/vitis.rst. Vitis HLS meets timing.io_parallel: Vivado HLS 2020.1 refuses to partition an output of more than 4096 values; onVitis HLS, C synthesis and co-simulation take long (example below: 14 and 77 minutes), and the
Resourcestrategy keeps the whole output in registers (75k FF in the example below).
io_streamis the better choice forlarge layers.
io_streamon Vitis HLS: in 5 of the 43 Vitis builds of the sweep below, the interval is one cycle longer thanexpected. These are builds where the first process is the slower one, and Vitis HLS reports a start delay of one
cycle for the loop of that process (
stp(delay=1)in the C synthesis report). Vivado HLS 2020.1 is not affected.This is listed under the known issues in
docs/backend/vitis.rst.quantized_po2) kernel gives wrong outputs, as it does in the existing layers(
Dense,Conv2D): hls4ml then picks the shift product (weight_exponential), which reads the stored 0 as thenumber 0 instead of -1. A separate PR will add a product for this case for all layers. The same product will
also fix a binary input that reaches the layer through another layer, such as a
Reshape, with a kernel scaleper input channel (the default
binary()andternary()kernels): only a binary activation directly before thelayer gets the fixed-point scale described under frontends.
layers either, so it is the default precision. A wide layer can overflow
ap_fixed<16,6>, asQConv2Ddoes.docs/backend/vitis.rstnow list this, together with the other layers that were found not to work with 2025.1:Conv1DandConv2Dinio_parallel(co-simulation does not finish),Densewith theResourcestrategy (Csynthesis does not finish), and the
Fusedstrategy.Type of change
io_type, Strategy, or config attributeAffected areas
Backends:
hls4ml.model)Frontends:
Components:
hls4ml/templates/Configurations affected and exercised
io_typeio_parallel,io_streamLatency,Resourceio_parallel,io_streamLatency,Resourceio_stream(axi_stream,axi_master)Latency,ResourceVitisUnified was built for 4 models (the example below with
LatencyandResourceoveraxi_stream, a 1D layerwith
Latencyand a 2D layer with unequal strides withResourceoveraxi_master). C synthesis and co-simulationpass, and
predict()gives the same outputs as the Vitis backend. There the interval is set by thefloatinterface,which moves one value per clock cycle (about 2100 cycles for the 2048 outputs of the example).
New configuration axis introduced by this PR: none.
Impact on generated HLS
20 projects of existing layers (Keras
Conv1D,Conv2D,UpSampling2D+Conv2D,DepthwiseConv2D+ pointwiseConv2D,Dense+BatchNormalization+ softmax, and a PyTorchConv2dmodel;io_parallelandio_stream;LatencyandResource; Vitis backend) were written withmainand with this branch. They are identical, apart fromthe two new headers and the two new stub classes in
nnet_function_stubs.h, which every project copies.Numerical behaviour: new layers. The outputs match Keras and PyTorch within the fixed-point precision, and match
QKeras exactly in the QKeras tests. RTL co-simulation gives the same outputs as C simulation in every build listed
below.
Example:
Conv2DTranspose(8, 3, strides=2, padding='same')on an 8×8×4 input (output 16×16×8), partxcvu13p-flga2577-2-e, 5 ns clock,ap_fixed<16,6>. Before this PR, the usual substitute wasUpSampling2D(2)followed by
Conv2D(8, 3, padding='same'): the same output size, but not the same operation (it repeats inputs insteadof inserting zeros). Latency and interval are from co-simulation, resources from logic synthesis. The interval is the
one between later inputs; the first input can take one cycle more.
io_parallel, Latencyio_parallel, Latencyio_parallel, Resource RF 36io_parallel, Resource RF 9io_stream, Latencyio_stream, Latencyio_stream, Resource RF 36io_stream, Resource RF 9io_parallel, Latencyio_parallel, Latencyio_parallel, Resource RF 36io_parallel, Resource RF 9io_stream, Latencyio_stream, Latencyio_stream, Resource RF 36io_stream, Resource RF 9The
Resourcerows use the default reuse factor of each layer, so they do not use the same number of multipliers:Conv2DTransposeneeds 36 multiplications per block for 4 output pixels, the reference 36 per output pixel. TheUpSampling2D + Conv2D
io_parallelLatency build on Vitis misses the 5 ns clock by 4.2 ns; allConv2DTransposebuilds on Vitis meet it. On Vivado HLS 2020.1, the
Conv2DTransposeLatencybuilds miss it by 0.52 ns(
io_parallel) and 0.33 ns (io_stream), and the UpSampling2D + Conv2Dio_streamLatencybuild by 0.55 ns (seeknown limits).
Sweep. Besides the example, 230 builds of the new layers with both tools covered 19 layers (1D and 2D; strides 1
to 4, unequal strides, kernels smaller and larger than the stride,
validandsame,output_padding, dilation),the
Latencystrategy, theResourcestrategy at small, default and largest reuse factors,ParallelizationFactor2 to 17 in
io_parallel, and 8-bit precision: 144 builds inio_paralleland 86 inio_stream. In every build,co-simulation passes and gives the same outputs as C simulation. The interval equals the expected one in all
io_parallelbuilds andin all
io_streambuilds apart from the 5 Vitis builds described under known limits. Timing: Vitis HLS meets 5 ns inall builds but one, which misses by 0.001 ns; Vivado HLS 2020.1 misses it in 18 of the 30
io_paralleland 8 of the 16io_streamLatencybuilds and in twoio_streamResourcebuilds, as described above.Binary inputs. 40 builds of two layers with a binary input (
Conv2DTransposewith a 3×3 kernel, stride 2 and'same'padding, on an 8×8×4 input with 8 filters and on a 4×4×16 input with 16 filters; both tools, both I/O typesand both strategies; binary and 4-bit kernels, and the default
binary()kernel on the second layer): co-simulationpasses and gives the same outputs as C simulation, the interval equals the expected one, and both tools meet 5 ns.
Tests
test/pytest/test_conv_transpose.py(new, 67 tests, inKERAS3_LIST): Keras 3Conv1DTransposeandConv2DTransposeon Vivado with both strategies and both I/O types, including strides larger and smaller than thekernel, stride 1 with a 5×5 kernel and
'same'padding, andoutput_padding; one layer on Vitis, which uses thesame kernels; reuse factors that split a block, in both I/O types, and parallelization factor settings; the parsed
attributes; dilation in both I/O types (against a NumPy reference, because Keras on TensorFlow cannot run a dilated
transposed convolution); PyTorch
ConvTranspose1dandConvTranspose2dwith padding,output_padding, stridetogether with dilation, and
output_size, in both I/O types.test/pytest/test_qkerasV3.py:test_qconv_transpose,QConv1DTranspose(withoutput_padding) andQConv2DTransposewithvalidandsamepadding andalpha1 andauto_po2, on Vivado and Vitis; outputs equalto QKeras. QKeras-v3 layers are parsed by the Keras 2 handlers, so this test also covers the new Keras 2 handler.
QConv1DTransposewas added to QKeras-v3 in Add QConv1DTranspose and apply output_padding in QConv2DTranspose qkerasV3#10 (released in 1.2.3), which also makesQKeras-v3 apply
output_padding. Without theoutput_paddingchange in the QKeras-v3 handler, the 4 1D'valid'cases fail on Keras 3.14.1, the version the
testing-qkeras-v3extra pins.test/pytest/test_qkerasV3.py:test_qconv_transpose_binary_input,QConv2DTransposeafter a binary activation(whose output hls4ml stores in 1 bit), with a binary kernel, the default
binary()kernel (power-of-two scale perinput channel) and a 4-bit kernel, both I/O types and both strategies, on Vitis; outputs equal to QKeras. All 12
cases fail without the binary-input changes.
Results:
test_conv_transpose.py: 67 passed with Keras 3.15 and with Keras 3.14.1. It also passes (67) with Keras 2(TensorFlow 2.14), where the Keras 2 handler parses the layers.
test_qkerasV3.pywith qkeras-v3 1.2.3, on Keras 3.14.1 and on Keras 3.15: 163 passed, among them the 16test_qconv_transposeand the 12test_qconv_transpose_binary_inputcases. The 16 failures aretest_btnncasesthat fail to compile in this environment, on
maintoo.mainand on this branch, with the same results:test_keras_v3_api.py(93 passed; 22 XLS failures from a missing module and 26 Catapult projects that do notcompile in this environment),
test_keras_api.py(98 passed),test_pytorch_api.py(198 passed),test_qkeras.pywith QKeras 0.9.0 (192 passed, 3 recurrent-layer failures) and
test_binary_cnn.py(9 passed).this environment.
Test configuration: AlmaLinux 9.3. Keras 3: Python 3.11.15, Keras 3.15.0, TensorFlow 2.21.0, PyTorch 2.14.1,
NumPy 2.4.6, qkeras-v3 1.2.3; Keras 3.14.1 for the runs on that version. Keras 2: Python 3.10.20, TensorFlow 2.14.1, PyTorch 2.14.1, NumPy 1.26.4, QKeras 0.9.0.
Vivado HLS 2020.1 and Vitis HLS 2024.1 for synthesis.
AI assistance disclosure
Tool(s) and model(s): Claude Code (Claude Opus 5.5)
Where it was used: the whole change (design, parsers, backend, HLS code, tests, synthesis runs and this description),
directed and reviewed by me.
If anything other than None is ticked, confirm all of the following:
it. I have read and understood every line and can explain and defend it
in review.
no invented APIs, config keys, pragmas or citations.
actually performed, not from agent output.
my knowledge it does not reproduce third-party code that would conflict
with it.
Checklist
Required:
pre-commiton the files I edited.test/pytestcovering this change (a bug fix shouldadd a test that fails on
mainand passes here).commented-out blocks or unrelated changes.
If applicable:
docs/updated.Release note