Prerequisites
Please make sure to check off these prerequisites before submitting a bug report.
I tested the released version 1.3.0 rather than master, on two TensorFlow
versions (2.15.1 and 2.18.1). The root cause is a missing read of
dilation_rate in the Keras converter, so I do not expect it to differ on
master, but I have not verified that and have left the box unchecked.
I searched open and closed issues for "dilation" and "dilated" and found nothing
relevant; PRs #472 and #1008 mention the word only incidentally.
Quick summary
A Keras Conv1D with dilation_rate > 1 converts without any warning, but the
generated HLS computes an ordinary (dilation=1) convolution. The emitted
firmware is byte-identical to the dilation_rate=1 case, so the synthesised
hardware silently computes a different function than the trained model.
Details
There is no error and no warning. Conversion succeeds, C-synthesis succeeds and
the design meets timing — the only symptom is wrong numerical output, which is
easy to misattribute to quantisation. Dilated 1D convolutions are common in
sequence models and in detector pulse-shape networks, so a user can unknowingly
deploy an incorrect network.
Environment:
- hls4ml 1.3.0 (installed from PyPI)
- TensorFlow 2.15.1; also reproduced on TensorFlow 2.18.1
- Vitis HLS 2024.2
- Part
xczu2eg-sfvc784-1-e
Steps to Reproduce
- Install hls4ml 1.3.0 and TensorFlow 2.15.1. No clone is required — the bug is
visible at conversion time, before any HLS tool is invoked.
- Save the reproducer from this gist:
https://gist.github.com/VINAYTHUTE/b26ddf478e7704d8b1ebb960c39125a4
- Run it:
python3 hls4ml_dilation_bug.py
It builds the same model at three dilation rates:
# one tap per kernel position, so the output shows which input samples
# were actually gathered
inp = Input(shape=(64, 1), name="wave")
out = Conv1D(4, 3, padding="same", dilation_rate=D, # D = 1, 2, 4
use_bias=False, name="conv")(inp)
converts each with io_stream and the Vitis backend, writes the projects,
and compares the generated firmware/parameters.h plus a checksum of the
emitted firmware.
- Observe that all three dilation rates produce byte-identical firmware. The
full generated projects are left in ./hls4ml_dilation_repro/, one per
dilation rate, for inspection.
Expected behavior
Either the dilation is applied to the generated convolution, or conversion
fails with a clear error stating that dilation_rate != 1 is unsupported.
Actual behavior
For all three dilation rates:
- the parsed layer's
dilation attribute is None
parameters.h emits static const unsigned dilation = 1
in_width is 66 in every case, although padding='same' with K=3 requires
66 / 68 / 72 for dilation 1 / 2 / 4 respectively
- the generated firmware files are byte-identical (md5
78aabad3b060ed08)
- no warning or error is emitted at any point
io_parallel is affected identically
The Keras reference outputs differ substantially between these dilation rates
(max |dilated − undilated| ≈ 3.72 on the test input), so this is a genuine
functional divergence rather than a cosmetic config difference.
Console output:
hls4ml 1.3.0 | tensorflow 2.15.1
1) parsed layer attribute, emitted config, and firmware digest
Keras dilation_rate = 1
hls4ml parsed 'dilation' attribute : None
parameters.h: static const unsigned in_width = 66
parameters.h: static const unsigned filt_width = 3
parameters.h: static const unsigned dilation = 1
firmware md5 (first 16) : 78aabad3b060ed08
Keras dilation_rate = 2
hls4ml parsed 'dilation' attribute : None
parameters.h: static const unsigned in_width = 66
parameters.h: static const unsigned filt_width = 3
parameters.h: static const unsigned dilation = 1
firmware md5 (first 16) : 78aabad3b060ed08
Keras dilation_rate = 4
hls4ml parsed 'dilation' attribute : None
parameters.h: static const unsigned in_width = 66
parameters.h: static const unsigned filt_width = 3
parameters.h: static const unsigned dilation = 1
firmware md5 (first 16) : 78aabad3b060ed08
2) does hls4ml warn the user?
NO warning mentions dilation -- the failure is silent
3) Keras reference outputs for each dilation rate
dilation_rate=1: out[0,:6,0] = [0. 1.7641 0.4002 0.9787 2.2409 1.8676]
dilation_rate=2: out[0,:6,0] = [0. 0. 1.7641 0.4002 0.9787 2.2409]
dilation_rate=4: out[0,:6,0] = [0. 0. 0. 0. 1.7641 0.4002]
max |dilated - undilated| = 3.7241
4) io_parallel is affected as well
dilation_rate=1: static const unsigned dilation = 1
dilation_rate=4: static const unsigned dilation = 1
RESULT: FAIL -- dilation_rate is silently ignored
* firmware is byte-identical across dilation_rate 1/2/4
* dilation_rate=2 emitted 'dilation = 1'
* dilation_rate=2: in_width should be 68 (effective kernel 5), got 66
* dilation_rate=4 emitted 'dilation = 1'
* dilation_rate=4: in_width should be 72 (effective kernel 9), got 66
* no warning is emitted for an unsupported dilation
* io_parallel emits the same dilation for 1 and 4
Optional
Possible fix
hls4ml/converters/keras/convolution.py never reads dilation_rate from the
Keras config:
layer['filt_width'] = keras_layer['config']['kernel_size'][0]
layer['stride_width'] = keras_layer['config']['strides'][0]
# dilation_rate is never read -> layer['dilation'] is never set
The layer's config_cpp then calls self.get_attr('dilation', 1), so the unset
attribute silently falls back to the default of 1 — which is why nothing
surfaces anywhere in the chain. compute_padding_1d() is also called with
filt_width rather than the effective width 1 + (filt_width - 1) * dilation,
so the zero-padding is likewise computed as if dilation were 1.
However, the backend does not implement dilation either: CONFIG_T::dilation
is declared in nnet_conv1d.h (and dilation_height / dilation_width in
nnet_conv2d.h) but is never read by any compute code — it is a
declared-but-unimplemented placeholder. So simply propagating dilation from
the parser would emit a config that looks correct while the generated hardware
still ignores it, which is arguably worse than the present behaviour.
Suggested minimal fix — raise at conversion time when dilation_rate != 1,
until the backend implements it:
dilation = keras_layer['config'].get('dilation_rate', [1])[0]
if dilation != 1:
raise Exception(
f"Layer {layer['name']}: dilation_rate={dilation} is not supported. "
"hls4ml's Conv1D implementation ignores dilation, which would "
"silently produce incorrect hardware."
)
I'm happy to open a PR for this if it's the direction you'd prefer.
Additional context
Implementing dilation properly is a larger change (line buffers plus padding
computation) and probably belongs in a separate feature request — this issue is
only about the silent failure.
I hit this while deploying a 1D CNN denoiser for silicon-photomultiplier
detector waveforms, where dilated convolutions are a natural choice for widening
the receptive field over a 256-sample window. The model converted and
synthesised cleanly, met timing, and produced plausible-looking but incorrect
output; it took a while to trace back to dilation rather than to quantisation,
which is what motivated writing the reproducer.
Prerequisites
Please make sure to check off these prerequisites before submitting a bug report.
I tested the released version 1.3.0 rather than master, on two TensorFlow
versions (2.15.1 and 2.18.1). The root cause is a missing read of
dilation_ratein the Keras converter, so I do not expect it to differ onmaster, but I have not verified that and have left the box unchecked.
I searched open and closed issues for "dilation" and "dilated" and found nothing
relevant; PRs #472 and #1008 mention the word only incidentally.
Quick summary
A Keras
Conv1Dwithdilation_rate > 1converts without any warning, but thegenerated HLS computes an ordinary (dilation=1) convolution. The emitted
firmware is byte-identical to the
dilation_rate=1case, so the synthesisedhardware silently computes a different function than the trained model.
Details
There is no error and no warning. Conversion succeeds, C-synthesis succeeds and
the design meets timing — the only symptom is wrong numerical output, which is
easy to misattribute to quantisation. Dilated 1D convolutions are common in
sequence models and in detector pulse-shape networks, so a user can unknowingly
deploy an incorrect network.
Environment:
xczu2eg-sfvc784-1-eSteps to Reproduce
visible at conversion time, before any HLS tool is invoked.
https://gist.github.com/VINAYTHUTE/b26ddf478e7704d8b1ebb960c39125a4
io_streamand the Vitis backend, writes the projects,and compares the generated
firmware/parameters.hplus a checksum of theemitted firmware.
full generated projects are left in
./hls4ml_dilation_repro/, one perdilation rate, for inspection.
Expected behavior
Either the dilation is applied to the generated convolution, or conversion
fails with a clear error stating that
dilation_rate != 1is unsupported.Actual behavior
For all three dilation rates:
dilationattribute isNoneparameters.hemitsstatic const unsigned dilation = 1in_widthis 66 in every case, althoughpadding='same'withK=3requires66 / 68 / 72 for dilation 1 / 2 / 4 respectively
78aabad3b060ed08)io_parallelis affected identicallyThe Keras reference outputs differ substantially between these dilation rates
(max |dilated − undilated| ≈ 3.72 on the test input), so this is a genuine
functional divergence rather than a cosmetic config difference.
Console output:
Optional
Possible fix
hls4ml/converters/keras/convolution.pynever readsdilation_ratefrom theKeras config:
The layer's
config_cppthen callsself.get_attr('dilation', 1), so the unsetattribute silently falls back to the default of 1 — which is why nothing
surfaces anywhere in the chain.
compute_padding_1d()is also called withfilt_widthrather than the effective width1 + (filt_width - 1) * dilation,so the zero-padding is likewise computed as if dilation were 1.
However, the backend does not implement dilation either:
CONFIG_T::dilationis declared in
nnet_conv1d.h(anddilation_height/dilation_widthinnnet_conv2d.h) but is never read by any compute code — it is adeclared-but-unimplemented placeholder. So simply propagating
dilationfromthe parser would emit a config that looks correct while the generated hardware
still ignores it, which is arguably worse than the present behaviour.
Suggested minimal fix — raise at conversion time when
dilation_rate != 1,until the backend implements it:
I'm happy to open a PR for this if it's the direction you'd prefer.
Additional context
Implementing dilation properly is a larger change (line buffers plus padding
computation) and probably belongs in a separate feature request — this issue is
only about the silent failure.
I hit this while deploying a 1D CNN denoiser for silicon-photomultiplier
detector waveforms, where dilated convolutions are a natural choice for widening
the receptive field over a 256-sample window. The model converted and
synthesised cleanly, met timing, and produced plausible-looking but incorrect
output; it took a while to trace back to dilation rather than to quantisation,
which is what motivated writing the reproducer.