diff --git a/.gitignore b/.gitignore
index 476728453b..73a860c91b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,7 +11,7 @@ my-hls-test
*.tar.gz
docs/_build
docs/autodoc/*
-hls4mlprj_*
+test/pytest/*_test_*
*~
*.ipynb
*.ipynb_checkpoints/
diff --git a/.gitmodules b/.gitmodules
index 98c3df68fd..1b4e10b96c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,3 +10,9 @@
[submodule "hls4ml/templates/catapult/ac_math"]
path = hls4ml/templates/catapult/ac_math
url = https://github.com/hlslibs/ac_math.git
+[submodule "hls4ml/templates/bambu/nnet_utils/gcem"]
+ path = hls4ml/templates/bambu/nnet_utils/gcem
+ url = https://github.com/kthohr/gcem
+[submodule "hls4ml/templates/bambu/ac_types"]
+ path = hls4ml/templates/bambu/ac_types
+ url = https://github.com/ferrandi/ac_types.git
diff --git a/docs/backend/bambu.rst b/docs/backend/bambu.rst
new file mode 100644
index 0000000000..4c5e680632
--- /dev/null
+++ b/docs/backend/bambu.rst
@@ -0,0 +1,64 @@
+=====
+Bambu
+=====
+
+The ``Bambu`` backend targets `Bambu/PandA `_, an
+open-source high-level synthesis compiler. It converts ``hls4ml`` models to HLS
+C++ and drives Bambu to synthesizable Verilog, so no proprietary HLS tool is
+required on the critical path. Both ``io_parallel`` and ``io_stream`` are
+supported.
+
+Quick start
+===========
+
+.. code-block:: python
+
+ import hls4ml
+
+ config = hls4ml.utils.config_from_keras_model(model, granularity='name', backend='Bambu')
+
+ hls_model = hls4ml.converters.convert_from_keras_model(
+ model,
+ hls_config=config,
+ backend='Bambu',
+ part='xc7a100tcsg324-1',
+ io_type='io_parallel',
+ output_dir='my_bambu_prj',
+ )
+
+ # Compile the bridge and check numerical accuracy against Keras
+ hls_model.compile()
+ y = hls_model.predict(X)
+
+ # Run Bambu: C-simulation, HLS synthesis, RTL co-simulation and (optionally)
+ # a Vivado logic-synthesis pass for post-route resource/timing/power numbers.
+ hls_model.build(csim=True, synth=True, cosim=True, vsynth=True)
+
+ report = hls4ml.report.parse_bambu_report('my_bambu_prj')
+
+``part`` must be a device Bambu knows about; the supported names are the keys of
+``partname_to_bambu`` in ``hls4ml/backends/bambu/bambu_backend.py``. Requiring
+``vsynth=True`` (and therefore the resource/timing/power numbers) needs Vivado on
+the ``PATH``; the rest of the flow only needs the ``bambu`` executable.
+
+Post-route utilization, timing and power numbers are parsed from the reports
+Bambu's Vivado flow produces, reusing the shared report helpers in
+``hls4ml/report/vivado_report.py``.
+
+Known limitations
+=================
+
+* Large completely-partitioned arrays crash Bambu's frontend, so dense layers
+ must be kept small.
+* The softmax inverse lookup table is emitted as a ``constexpr`` array. When
+ ``fix_softmax_table_size`` shrinks the table (i.e. when
+ ``2 ** min(input_bitwidth, table_bitwidth)`` is below the default table size),
+ Bambu's clang rejects the initializer at compile time. Other softmax
+ configurations work; if you hit this, widen the input/table precision or take
+ the argmax on the host.
+* ``-m64`` combined with ``ac_channel`` crashes ``InterfaceInfer``. The default
+ path avoids this by using the headers Bambu ships.
+
+These were reported to the PandA developers and are addressed by
+`PandA-bambu#396 `_; the
+limitations above apply to current Bambu versions until that release lands.
diff --git a/docs/backend/nanoxplore.rst b/docs/backend/nanoxplore.rst
new file mode 100644
index 0000000000..f6595cb339
--- /dev/null
+++ b/docs/backend/nanoxplore.rst
@@ -0,0 +1,64 @@
+=====================
+NanoXploreAccelerator
+=====================
+
+The **NanoXploreAccelerator** backend builds on the :doc:`Bambu ` backend
+and turns a Bambu-generated HLS core into a complete accelerator: a float I/O
+wrapper, an AXI4 slave, a PLL configuration, and a versioned ``manifest.json``
+describing the result. It targets NanoXplore's `NG-ULTRA
+`_, a radiation-hardened FPGA with no HLS tool of
+its own.
+
+.. code-block:: python
+
+ hls_model = hls4ml.converters.convert_from_keras_model(
+ model,
+ hls_config=config,
+ backend='NanoXploreAccelerator',
+ )
+ hls_model.build(synth=True, bitstream=True)
+
+Defaults are the NG-ULTRA DevKit's: part ``nx2h540tsc`` and a 20 ns clock
+period, matching the board's 50 MHz oscillator.
+
+A deliberate seam
+=================
+
+``hls4ml`` never imports a vendor tool. The backend writes everything a
+place-and-route flow needs into the project directory -- the complete RTL file
+list, clock, port map and data widths, all in ``manifest.json`` and versioned so
+a mismatch fails loudly -- then shells out to a single command and reads back
+``report.json``:
+
+.. code-block:: text
+
+ hls4ml-nanoxplore-bitstream
+
+The command is configurable through the ``BitStreamCommand`` config value. The
+vendor-specific driver lives out of tree, which means the abstract layer can be
+built and tested with no vendor licence: ``build(synth=True, bitstream=False)``
+produces the wrapper, the RTL and the manifest, and stops before place and
+route.
+
+Structure
+=========
+
+``BambuAcceleratorBackend`` is abstract and unregistered. It provides the
+wrapper generation, the RTL templates, the manifest and the PLL patching, and
+leaves one abstract method, ``_generate_bitstream``. Other FPGA families can
+reuse the layer by subclassing it.
+
+``NanoXploreAcceleratorBackend`` is the registered concrete backend: NG-ULTRA
+defaults plus the CLI call.
+
+Clocking
+========
+
+Any requested ``ClockPeriod`` is turned into a solved ``NX_PLL_U``
+configuration, spliced into the generated top level, so the hardware clock and
+the timing constraint agree by construction rather than by convention. This
+needs OR-Tools:
+
+.. code-block:: bash
+
+ pip install hls4ml[nanoxplore]
diff --git a/docs/index.rst b/docs/index.rst
index 35e79f75bb..3ce9b32761 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -38,6 +38,8 @@
backend/oneapi
backend/catapult
backend/quartus
+ backend/bambu
+ backend/nanoxplore
backend/sr
.. toctree::
diff --git a/hls4ml/backends/__init__.py b/hls4ml/backends/__init__.py
index 07a089cdf8..499f0c46d4 100644
--- a/hls4ml/backends/__init__.py
+++ b/hls4ml/backends/__init__.py
@@ -13,6 +13,12 @@
from hls4ml.backends.vitis.vitis_backend import VitisBackend # isort: skip
+from hls4ml.backends.bambu.bambu_backend import BambuBackend # isort: skip
+
+from hls4ml.backends.bambu_accelerator.bambu_accelerator_backend import BambuAcceleratorBackend # isort: skip # noqa: F401
+
+from hls4ml.backends.nanoxplore_accelerator.nanoxplore_accelerator_backend import NanoXploreAcceleratorBackend # isort: skip # noqa: E501,F401
+
def _register_builtin_backends():
register_backend('Vivado', VivadoBackend)
@@ -23,6 +29,8 @@ def _register_builtin_backends():
register_backend('SymbolicExpression', SymbolicExpressionBackend)
register_backend('oneAPI', OneAPIBackend)
register_backend('Libero', LiberoBackend)
+ register_backend('Bambu', BambuBackend)
+ register_backend('NanoXploreAccelerator', NanoXploreAcceleratorBackend)
_register_builtin_backends()
diff --git a/hls4ml/backends/bambu/__init__.py b/hls4ml/backends/bambu/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/hls4ml/backends/bambu/bambu_backend.py b/hls4ml/backends/bambu/bambu_backend.py
new file mode 100644
index 0000000000..55433a20f9
--- /dev/null
+++ b/hls4ml/backends/bambu/bambu_backend.py
@@ -0,0 +1,1290 @@
+import os
+import re
+import shlex
+import shutil
+import subprocess
+from pathlib import Path
+from warnings import warn
+
+import numpy as np
+
+from hls4ml.backends import FPGABackend
+from hls4ml.backends.bambu.bambu_types import BambuArrayVariableConverter, BambuHLSTypeConverter
+from hls4ml.backends.fpga.fpga_types import APTypeConverter
+from hls4ml.model.attributes import ChoiceAttribute, ConfigurableAttribute, TypeAttribute
+from hls4ml.model.flow import register_flow
+from hls4ml.model.layers import (
+ GRU,
+ LSTM,
+ Bidirectional,
+ Conv1D,
+ Conv2D,
+ Dense,
+ DepthwiseConv1D,
+ DepthwiseConv2D,
+ Einsum,
+ EinsumDense,
+ Embedding,
+ GarNet,
+ GarNetStack,
+ Layer,
+ LayerNormalization,
+ Pooling1D,
+ Pooling2D,
+ SeparableConv1D,
+ SeparableConv2D,
+ SimpleRNN,
+ TimeDistributed,
+)
+from hls4ml.model.optimizer import get_backend_passes, layer_optimizer
+from hls4ml.model.types import (
+ FixedPrecisionType,
+ IntegerPrecisionType,
+ NamedType,
+ PackedType,
+ RoundingMode,
+ SaturationMode,
+)
+from hls4ml.report import parse_bambu_report
+from hls4ml.utils import attribute_descriptions as descriptions
+from hls4ml.utils.einsum_utils import parse_einsum
+
+partname_to_bambu = {
+ # Intel/Altera
+ '5CSEMA5F31C6': {'device_name': '5CSEMA5F31C6', 'family': 'Intel/Altera'},
+ '5SGXEA7N2F45C1': {'device_name': '5SGXEA7N2F45C1', 'family': 'Intel/Altera'},
+ 'EP2C70F896C6': {'device_name': 'EP2C70F896C6', 'family': 'Intel/Altera'},
+ 'EP2C70F896C6-R': {'device_name': 'EP2C70F896C6-R', 'family': 'Intel/Altera'},
+ 'EP4SGX530KH40C2': {'device_name': 'EP4SGX530KH40C2', 'family': 'Intel/Altera'},
+ # : "LFE335EA8FN484C",
+ # : "LFE5U85F8BG756C",
+ # : "LFE5UM85F8BG756C",
+ # ASAP7 (ASIC)
+ # : "asap7-BC",
+ # : "asap7-TC",
+ # : "asap7-WC",
+ # Standard cells / tech libraries
+ # : "nangate45",
+ # NaNGate/Nextgrids
+ # : "nx1h140tsp",
+ # : "nx1h35S",
+ 'nx2h540tsc': {'device_name': 'nx2h540tsc', 'family': 'NanoXplore'},
+ # Xilinx legacy
+ # : "xc4vlx100-10ff1513",
+ # : "xc5vlx110t-1ff1136",
+ # : "xc5vlx330t-2ff1738",
+ # : "xc5vlx50-3ff1153",
+ # : "xc6vlx240t-1ff1156",
+ # 7-series
+ 'xc7a100tcsg324-1': {
+ 'device_name': 'xc7a100t-1csg324',
+ 'family': 'Xilinx',
+ }, # 7-series Artix; matches the entry in Bambu's `Available devices` listing
+ # : "xc7vx330t-1ffg1157",
+ # : "xc7vx485t-2ffg1761",
+ # : "xc7vx690t-3ffg1930",
+ # : "xc7z020-1clg484",
+ # : "xc7z020-1clg484-YOSYS",
+ # : "xc7z045-2ffg900",
+ # UltraScale / UltraScale+
+ # : "xcku060-3ffva1156",
+ # : "xcu250-2Lfigd2104",
+ # : "xcu280-2Lfsvh2892",
+ # : "xcu50-2fsvh2104",
+ 'xczu7ev-ffvc1156-2-e': {'device_name': 'xczu7ev-2ffvc1156', 'family': 'Xilinx'},
+ 'xcu55c-fsvh2892-2L-e': {'device_name': 'xcu55c-2Lfsvh2892', 'family': 'Xilinx'},
+}
+
+
+class BambuBackend(FPGABackend):
+ def __init__(self):
+ super().__init__('Bambu')
+ self._register_layer_attributes()
+ self._register_flows()
+
+ def _register_layer_attributes(self):
+ # Add RNN-specific attributes, recurrent_reuse_factor and static implementation
+ rnn_layers = [SimpleRNN, LSTM, GRU]
+
+ for layer in rnn_layers:
+ attrs = self.attribute_map.get(layer, [])
+ attrs.append(ConfigurableAttribute('recurrent_reuse_factor', default=1, description=descriptions.reuse_factor))
+ attrs.append(
+ ConfigurableAttribute('static', value_type=bool, default=True, description=descriptions.recurrent_static)
+ )
+ attrs.append(ConfigurableAttribute('table_size', default=1024, description=descriptions.table_size))
+ attrs.append(TypeAttribute('table', default=FixedPrecisionType(18, 8), description=descriptions.table_type))
+ self.attribute_map[layer] = attrs
+
+ bidir_rnn_layers = [Bidirectional]
+ for layer in bidir_rnn_layers:
+ attrs = self.attribute_map.get(layer, [])
+ attrs.append(ConfigurableAttribute('forward_reuse_factor', default=1, description=descriptions.reuse_factor))
+ attrs.append(ConfigurableAttribute('backward_reuse_factor', default=1, description=descriptions.reuse_factor))
+ attrs.append(
+ ConfigurableAttribute('forward_recurrent_reuse_factor', default=1, description=descriptions.reuse_factor)
+ )
+ attrs.append(
+ ConfigurableAttribute('backward_recurrent_reuse_factor', default=1, description=descriptions.reuse_factor)
+ )
+ attrs.append(
+ ConfigurableAttribute('static', value_type=bool, default=True, description=descriptions.recurrent_static)
+ )
+ attrs.append(ConfigurableAttribute('table_size', default=1024, description=descriptions.table_size))
+ attrs.append(TypeAttribute('table', default=FixedPrecisionType(18, 8), description=descriptions.table_type))
+ self.attribute_map[layer] = attrs
+
+ # Add ParallelizationFactor to Conv1D/2D
+ pf_layers = [
+ Conv1D,
+ Conv2D,
+ ]
+
+ for layer in pf_layers:
+ attrs = self.attribute_map.get(layer, [])
+ attrs.append(ConfigurableAttribute('parallelization_factor', default=1, description=descriptions.conv_pf))
+ self.attribute_map[layer] = attrs
+
+ # Add ConvImplementation to Convolution+Pooling layers
+ cnn_layers = [Conv1D, Conv2D, SeparableConv1D, SeparableConv2D, DepthwiseConv2D, Pooling1D, Pooling2D]
+ for layer in cnn_layers:
+ attrs = self.attribute_map.get(layer, [])
+ attrs.append(
+ ChoiceAttribute(
+ 'conv_implementation',
+ choices=['LineBuffer', 'Encoded'],
+ default='LineBuffer',
+ description=descriptions.conv_implementation,
+ )
+ )
+ self.attribute_map[layer] = attrs
+
+ # Add LayerNorm attributes
+ ln_layers = [LayerNormalization]
+ for layer in ln_layers:
+ attrs = self.attribute_map.get(layer, [])
+ attrs.append(ConfigurableAttribute('table_range_power2', default=0, description=descriptions.table_range_power2))
+ attrs.append(ConfigurableAttribute('table_size', default=4096, description=descriptions.table_size))
+ attrs.append(
+ TypeAttribute(
+ 'table',
+ default=FixedPrecisionType(
+ 8, 5, signed=False, rounding_mode=RoundingMode.RND_CONV, saturation_mode=SaturationMode.SAT
+ ),
+ description=descriptions.table_type,
+ )
+ )
+ attrs.append(
+ TypeAttribute(
+ 'accum',
+ default=FixedPrecisionType(
+ 14, 4, signed=True, rounding_mode=RoundingMode.RND_CONV, saturation_mode=SaturationMode.SAT
+ ),
+ description=descriptions.accum_type,
+ )
+ )
+ self.attribute_map[layer] = attrs
+
+ # Add TimeStepLoopParallelism to TimeDistributed
+ attrs = self.attribute_map.get(TimeDistributed, [])
+ attrs.append(
+ ChoiceAttribute(
+ 'time_step_loop_parallelism',
+ choices=['Off', 'Unroll', 'Pipeline'],
+ default='Off',
+ description=descriptions.time_distributed_loop,
+ )
+ )
+ self.attribute_map[TimeDistributed] = attrs
+
+ def _register_flows(self):
+ # Register flows using bambu: passes (copied from vivado)
+ initializers = self._get_layer_initializers()
+ init_flow = register_flow('init_layers', initializers, requires=['optimize'], backend=self.name)
+
+ streaming_passes = [
+ 'bambu:inplace_stream_flatten', # Inform downstream changed packsize in case of skipping flatten
+ 'bambu:reshape_stream',
+ 'bambu:clone_output',
+ 'bambu:insert_zero_padding_before_conv1d',
+ 'bambu:insert_zero_padding_before_conv2d',
+ 'bambu:broadcast_stream',
+ ]
+ streaming_flow = register_flow('streaming', streaming_passes, requires=[init_flow], backend=self.name)
+
+ quantization_passes = [
+ 'bambu:merge_batch_norm_quantized_tanh',
+ 'bambu:quantize_dense_output',
+ 'fuse_consecutive_batch_normalization',
+ 'bambu:xnor_pooling',
+ ]
+ quantization_flow = register_flow('quantization', quantization_passes, requires=[init_flow], backend=self.name)
+
+ optimization_passes = [
+ 'bambu:remove_final_reshape',
+ 'bambu:optimize_pointwise_conv',
+ 'bambu:inplace_parallel_reshape',
+ 'bambu:inplace_stream_flatten',
+ 'bambu:skip_softmax',
+ 'bambu:fix_softmax_table_size',
+ 'infer_precision_types',
+ 'bambu:distributed_arithmetic_codegen',
+ 'bambu:distributed_arithmetic_einsum_codegen',
+ 'bambu:fuse_quantizer_into_d_a_layers',
+ 'bambu:process_fixed_point_quantizer_layer',
+ ]
+ optimization_flow = register_flow('optimize', optimization_passes, requires=[init_flow], backend=self.name)
+
+ bambu_types = [
+ 'bambu:transform_types',
+ 'bambu:register_bram_weights',
+ 'bambu:generate_conv_streaming_instructions',
+ 'bambu:apply_resource_strategy',
+ 'bambu:generate_conv_im2col',
+ 'bambu:generate_unrolled_dense_resource',
+ 'bambu:set_pipeline_style',
+ 'bambu:d_a_latency_dense_template',
+ 'bambu:d_a_latency_conv_template',
+ ]
+ bambu_types_flow = register_flow('specific_types', bambu_types, requires=[init_flow], backend=self.name)
+
+ templates = self._get_layer_templates()
+ template_flow = register_flow('apply_templates', self._get_layer_templates, requires=[init_flow], backend=self.name)
+
+ writer_passes = ['make_stamp', 'bambu:write_hls']
+ self._writer_flow = register_flow('write', writer_passes, requires=['bambu:ip'], backend=self.name)
+
+ fifo_depth_opt_passes = [
+ 'bambu:fifo_depth_optimization'
+ ] + writer_passes # After optimization, a new project will be written
+
+ register_flow('fifo_depth_optimization', fifo_depth_opt_passes, requires=['bambu:ip'], backend=self.name)
+
+ all_passes = get_backend_passes(self.name)
+
+ extras = [
+ # Ideally this should be empty
+ opt_pass
+ for opt_pass in all_passes
+ if opt_pass
+ not in initializers
+ + streaming_passes
+ + quantization_passes
+ + optimization_passes
+ + bambu_types
+ + templates
+ + writer_passes
+ + fifo_depth_opt_passes
+ ]
+
+ if len(extras) > 0:
+ for opt in extras:
+ warn(f'WARNING: Optimizer "{opt}" is not part of any flow and will not be executed.')
+
+ ip_flow_requirements = [
+ 'optimize',
+ init_flow,
+ streaming_flow,
+ quantization_flow,
+ optimization_flow,
+ bambu_types_flow,
+ template_flow,
+ ]
+
+ self._default_flow = register_flow('ip', None, requires=ip_flow_requirements, backend=self.name)
+
+ def get_default_flow(self):
+ return self._default_flow
+
+ def get_writer_flow(self):
+ return self._writer_flow
+
+ def create_initial_config(
+ self,
+ part='xc7a100tcsg324-1',
+ clock_period=5,
+ clock_uncertainty='12.5%',
+ io_type='io_parallel',
+ namespace=None,
+ write_weights_txt=True,
+ write_tar=False,
+ tb_output_stream='both',
+ **_,
+ ):
+ """Create initial configuration of the Bambu backend.
+
+ Args:
+ part (str, optional): The FPGA part to be used. Defaults to 'xc7a100tcsg324-1'.
+ clock_period (int, optional): The clock period. Defaults to 5.
+ clock_uncertainty (str, optional): The clock uncertainty. Defaults to 12.5%.
+ io_type (str, optional): Type of implementation used. One of
+ 'io_parallel' or 'io_stream'. Defaults to 'io_parallel'.
+ namespace (str, optional): If defined, place all generated code within a namespace. Defaults to None.
+ write_weights_txt (bool, optional): If True, writes weights to .txt files which speeds up compilation.
+ Defaults to True.
+ write_tar (bool, optional): If True, compresses the output directory into a .tar.gz file. Defaults to False.
+ tb_output_stream (str, optional): Controls where to write the output. Options are 'stdout', 'file' and 'both'.
+ Defaults to 'both'.
+
+ Returns:
+ dict: initial configuration.
+ """
+ config = {}
+
+ partname = part if part is not None else 'xc7a100tcsg324-1'
+ config['Part'] = partname
+ config['ClockPeriod'] = clock_period if clock_period is not None else 5
+ config['ClockUncertainty'] = clock_uncertainty if clock_uncertainty is not None else '12.5%'
+ config['IOType'] = io_type if io_type is not None else 'io_parallel'
+ config['HLSConfig'] = {}
+ config['WriterConfig'] = {
+ 'Namespace': namespace,
+ 'WriteWeightsTxt': write_weights_txt,
+ 'WriteTar': write_tar,
+ 'TBOutputStream': tb_output_stream,
+ }
+ if partname in partname_to_bambu.keys():
+ config['FPGAFamily'] = partname_to_bambu[partname]['family']
+
+ return config
+
+ def build(
+ self,
+ model,
+ *,
+ reset=False,
+ csim=True,
+ synth=True,
+ cosim=False,
+ validation=False,
+ export=False,
+ vsynth=False,
+ fifo_opt=False,
+ log_to_stdout=True,
+ args=None,
+ env=None,
+ run_kwargs=None,
+ ):
+ """Run Bambu HLS on given model. Enable/disable parts of synthesis process based on boolean arguments.
+ Pass extra Bambu-specific arguments via the ``args`` command.
+
+ Args:
+ model (ModelGraph): Model to be built with Bambu.
+ reset (bool, optional): If true, deletes Bambu-generated artifacts if they are present.
+ csim (bool, optional): Run C-Simulation of model on its testbench. Defaults to True.
+ synth (bool, optional): Standard CPP to HDL translation with Bambu. If set to false, Bambu is not called.
+ cosim (bool, optional): Run RTL-Cosimulation of model on its testbench. Defaults to false.
+ validation (bool, optional): Checks for bitwise equality of csim and cosim results.
+ export (bool, optional): NotImplemented (will create exported IP in project directory)
+ vsynth (bool, optional):
+ Optimize, Place, and Route synthesized design for any part that is supported by
+ Bambu. User will need the part downloaded in their Vivado installation.
+ Bambu requires cosim=True to run vsynth.
+ fifo_opt (bool, optional): NotImplemented (will optimize FIFO length based on RTL cosim)
+ log_to_stdout (bool, optional): Forward Bambu's ``stdout`` and ``stderr`` to system
+ args (str | Sequence[str] | None): Arguments appended to default Bambu command for this model.
+ env (Mapping[str, str] | None): Environment overrides applied to the subprocess.
+ run_kwargs (dict | None): Additional keyword arguments forwarded to ``subprocess.run``.
+
+ Returns:
+ dict:
+ 'CSimResults' (np.array(float), optional):
+ C Simulation array of testbench predictions
+ 'CosimResults' (np.array(float), optional):
+ RTL Cosimulation array of testbench predictions
+ 'BambuMetrics' (dict, optional):
+ Metrics returned by Bambu's evaluation
+ 'ImplementationReport' (dict, optional):
+ Parsed metrics from post-route utilization report
+ 'TimingReport' (dict, optional):
+ Parsed metrics from post-route timing summary report
+ 'PowerReport' (dict, optional):
+ Parsed metrics from post-route power report
+
+ Example:
+ result = model.build(
+ csim=True,
+ cosim=True,
+ validation=True,
+ log_to_stdout=True
+ args='-v4 --seed=5'
+ )
+ """
+
+ project_name = model.config.get_project_name()
+ project_dir = model.config.get_output_dir()
+ part_family = model.config.get_config_value('FPGAFamily')
+ # Bambu's InterfaceInfer pass (ChasePointerInterfaceRecurse) has a
+ # 64-bit-pointer bug that fires only on the ac_channel FIFO ports
+ # io_stream generates ("unexpected condition",
+ # InterfaceInfer.cpp:1068); io_parallel's BRAM address/data ports
+ # never hit it. Confirmed by bisecting Bambu's HLS flags: identical
+ # stream C++ synthesizes to AXIS Verilog once -m64 is omitted.
+ io_type = model.config.get_config_value('IOType')
+
+ # Bambu-specific command/flags
+ BASE_COMMAND = ['bambu'] + self._get_hls_sources(project_name) + [f'--top-fname={self._get_top_fname(project_name)}']
+ # `-ftemplate-depth=2048` is forwarded by Bambu directly to its
+ # clang-16 front-end. The default 1024-deep template instantiation
+ # limit is hit by `std::make_index_sequence` in libstdc++ 4.9.4
+ # (the libstdc++ shipped inside Bambu's AppImage) when N == 1024,
+ # which is exactly the upper bound `core_templates.py` clamps
+ # softmax's `exp_table_size` to. The recursive index-tuple builder
+ # at `bits/utility:215` in that libstdc++ requires N levels of
+ # depth, so any softmax whose input `data_T` width is >= 10 (e.g.
+ # `ap_fixed<16,6>` or a dense accumulator like `ac_fixed<18,10>`)
+ # trips the limit and the front-end aborts with `recursive template
+ # instantiation exceeded maximum depth of 1024`. Raising the
+ # ceiling is the fix the compiler error itself suggests, and is
+ # harmless for the smaller cases (no extra runtime/memory cost).
+ # See firmware/nnet_utils/nnet_activation.h:228 in hls4ml's bambu
+ # templates for the actual `make_index_sequence` call site.
+ CC_TEMPLATE_DEPTH = '-ftemplate-depth=2048'
+ if os.environ.get('USE_HLS4ML_AC_TYPES'):
+ # Legacy escape hatch: compile against the ac_types checkout the
+ # writer copies into firmware/ac_types, with a 64-bit host triple.
+ # The -m64 this needs trips Bambu's InterfaceInfer 64-bit pointer
+ # bug on io_stream's ac_channel FIFOs, so it is filtered below.
+ REQ_ARGS = [
+ '-lm',
+ '-Ifirmware/ac_types/include',
+ '--compiler=I386_CLANG16',
+ CC_TEMPLATE_DEPTH,
+ '--generate-interface=INFER',
+ '-v4',
+ '-m64',
+ ]
+ else:
+ # Default: Bambu's own shipped ac/ap headers (usr/include/panda),
+ # always in sync with the toolchain, 32-bit triple — no -m64
+ # needed, which also sidesteps the InterfaceInfer io_stream bug.
+ REQ_ARGS = ['-lm', '--compiler=I386_CLANG16', CC_TEMPLATE_DEPTH, '--generate-interface=INFER', '-v4']
+ if io_type == 'io_stream':
+ REQ_ARGS = [arg for arg in REQ_ARGS if arg != '-m64']
+ CMD_ARGS = []
+
+ result = {}
+
+ # --- RESET ---
+ bambu_output_patterns = [
+ 'HLS_output',
+ 'panda-temp',
+ 'vivado_reports',
+ 'bambu_results*.xml',
+ 'evaluate*.sh',
+ 'memory_allocation*.xml',
+ f'{project_name}-*_tb.exe',
+ f'{project_name}.v',
+ 'results.txt',
+ 'synthesize*.sh',
+ 'panda_libtech.v',
+ '*.mem',
+ ]
+ matches = [p for pat in bambu_output_patterns for p in Path(project_dir).glob(pat)]
+ is_dirty_directory = any(matches)
+ if reset:
+ if is_dirty_directory:
+ for p in matches:
+ if p.is_file() or p.is_symlink():
+ p.unlink(missing_ok=True)
+ elif p.is_dir():
+ shutil.rmtree(p, ignore_errors=True)
+ print(f'Removed: {p}')
+ else:
+ if is_dirty_directory:
+ warn(
+ 'WARNING: Bambu is being rerun on a directory instead of running on a fresh directory (not recommended).'
+ )
+
+ # --- CSIM ---
+ if csim:
+ self._build_testbench_exe(model)
+
+ # Execute testbench
+ ret = subprocess.run(
+ [f'./{project_name}-{model.config.get_config_value("Stamp")}_tb.exe'],
+ cwd=project_dir,
+ capture_output=True,
+ text=True,
+ )
+
+ if ret.returncode != 0:
+ raise RuntimeError(f'C++ testbench execution failed:\nSTDOUT:\n{ret.stdout}\nSTDERR:\n{ret.stderr}')
+
+ if synth:
+ clock_period = model.config.get_config_value('ClockPeriod')
+ part_name = model.config.get_config_value(
+ 'Part'
+ ) # Bambu uses its own 'device name' which does NOT always coincide with part name
+ device_name = partname_to_bambu.get(part_name, {}).get('device_name', None)
+ if device_name is None:
+ warn(
+ f'WARNING: Part name {part_name} has no registered mapping to a Bambu --device-name. '
+ f"Using '--device-name={part_name}'. "
+ "(See valid Bambu device names by running Bambu with High Verbosity flag '-v4')"
+ )
+ device_name = part_name
+ CMD_ARGS += [f'--device-name={device_name}', f'--clock-period={clock_period}']
+
+ # --- COSIM ---
+ if cosim:
+ if not synth:
+ raise ValueError('To run RTL cosimulation, C/RTL synthesis must be run.')
+ CMD_ARGS += [f'--generate-tb={self._get_cosim_testbench(project_name)}', '--simulate', '-DRTL_SIM']
+
+ # Force Verilator for NanoXplore parts. Bambu's default
+ # simulator selection picks a NanoXplore-native flow whose
+ # XML device files fail to parse on the current toolchain
+ # (`Error during XML parsing of device files`). Verilator is
+ # toolchain-agnostic and works for cosim regardless of the
+ # target FPGA family.
+ part_name = model.config.get_config_value('Part')
+ family = partname_to_bambu.get(part_name, {}).get('family', None)
+ if family == 'NanoXplore':
+ CMD_ARGS += ['--simulator=VERILATOR']
+
+ # --- VALIDATION ---
+ if validation:
+ if not csim or not cosim:
+ raise ValueError('To validate C simulation & RTL simulation equality, csim and cosim must both be run.')
+
+ # --- EXPORT ---
+ if export:
+ raise NotImplementedError() # TODO - Requires an ad-hoc .tcl script
+
+ # --- VSYNTH ---
+ if vsynth:
+ if not synth:
+ raise ValueError('To synthesize for specific part, C/RTL synthesis must be run.')
+ if not cosim:
+ raise ValueError('To synthesize for specific part in Bambu, RTL cosimulation must be run.')
+
+ CMD_ARGS += ['--evaluation']
+
+ # --- FIFO_OPT ---
+ if fifo_opt:
+ raise NotImplementedError() # TODO - Requires an ad-hoc .tcl script
+
+ # Build user's custom command with Bambu defaults
+ command_tokens = BASE_COMMAND + REQ_ARGS + CMD_ARGS
+ if args is not None:
+ command_tokens += self._normalize_bambu_command(args)
+ command_str = ' '.join(shlex.quote(str(token)) for token in command_tokens)
+
+ # Write/rewrite formatted command to build_bambu.sh for later execution
+ script_path = Path(project_dir) / 'build_bambu.sh'
+ content = script_path.read_text()
+ content = self._replace_block(
+ content, '# HLS4ML insert_bambu_command BEGIN', '# HLS4ML insert_bambu_command END', command_str
+ )
+ copy_code = self._final_report_copying_code(part_family) if vsynth else ''
+ content = self._replace_block(
+ content, '# HLS4ML insert_final_report_copying BEGIN', '# HLS4ML insert_final_report_copying END', copy_code
+ )
+ script_path.write_text(content)
+
+ if not synth:
+ # "Dry run"
+ result.update(parse_bambu_report(project_dir, part_family))
+ return result
+ else:
+ self._ensure_bambu_available()
+
+ # Alter os runtime environment
+ run_env = os.environ.copy()
+ if env is not None:
+ if not hasattr(env, 'items'):
+ raise TypeError('env must be a mapping of environment variables.')
+ for key, value in env.items():
+ if value is None:
+ run_env.pop(str(key), None)
+ else:
+ run_env[str(key)] = str(value)
+
+ # Add optional runtime keyword arguments to subprocess
+ if run_kwargs is None:
+ run_kwargs = {}
+ if not isinstance(run_kwargs, dict):
+ raise TypeError('run_kwargs must be a mapping')
+ if log_to_stdout and any(stream in run_kwargs for stream in ('stdout', 'stderr')):
+ raise ValueError('Cannot set stdout/stderr in run_kwargs when log_to_stdout=True.')
+
+ stdout_log = os.path.join(project_dir, 'build_stdout.log')
+ stderr_log = os.path.join(project_dir, 'build_stderr.log')
+ stdout_target = None
+ stderr_target = None
+ build_command = 'bash build_bambu.sh'
+ output_dir = project_dir
+
+ if log_to_stdout:
+ stdout_target = None
+ stderr_target = None
+ else:
+ if 'stdout' not in run_kwargs:
+ stdout_target = open(stdout_log, 'w')
+ else:
+ stdout_target = run_kwargs.pop('stdout')
+ if 'stderr' not in run_kwargs:
+ stderr_target = open(stderr_log, 'w')
+ else:
+ stderr_target = run_kwargs.pop('stderr')
+
+ # Run Bambu
+ try:
+ process = subprocess.Popen(
+ build_command,
+ shell=True,
+ cwd=output_dir,
+ stdout=stdout_target,
+ stderr=stderr_target,
+ env=run_env,
+ text=run_kwargs.get('text', True),
+ **run_kwargs,
+ )
+ process.communicate()
+ finally:
+ if stdout_target is not None and stdout_target is not subprocess.PIPE:
+ stdout_target.close()
+ if stderr_target is not None and stderr_target is not subprocess.PIPE:
+ stderr_target.close()
+
+ # A failed Bambu run must not fall through to report parsing: the reports
+ # would either be missing (yielding an empty result that looks like success)
+ # or left over from an earlier run when reset=False.
+ if process.returncode != 0:
+ logs = '' if log_to_stdout else f'\n stdout log: {stdout_log}\n stderr log: {stderr_log}'
+ raise RuntimeError(
+ f'Bambu build failed with exit code {process.returncode}.\n'
+ f' command: {build_command}\n'
+ f' directory: {output_dir}{logs}'
+ )
+
+ # Add main results
+ result.update(parse_bambu_report(project_dir, part_family))
+
+ return result
+
+ def _get_hls_sources(self, project_name):
+ """Return the list of C++ source files to pass to Bambu."""
+ return [os.path.join('firmware', f'{project_name}.cpp')]
+
+ def _get_top_fname(self, project_name):
+ """Return the top-level function name for Bambu synthesis."""
+ return project_name
+
+ def _get_cosim_testbench(self, project_name):
+ """Return the testbench filename for Bambu RTL co-simulation."""
+ return f'{project_name}_test.cpp'
+
+ def _build_testbench_exe(self, model):
+ ret = subprocess.run(
+ ['bash', 'build_tb_exe.sh'],
+ text=True,
+ capture_output=True,
+ cwd=model.config.get_output_dir(),
+ )
+ if ret.returncode != 0:
+ project_name = model.config.get_project_name()
+ raise RuntimeError(
+ f'Failed to build testbench executable for "{project_name}":\nSTDOUT:\n{ret.stdout}\nSTDERR:\n{ret.stderr}'
+ )
+
+ def _final_report_copying_code(self, family):
+ """Aggregate final reports in one directory based on Part Family/Software used"""
+ if family == 'Xilinx':
+ # Bambu's Vivado-flow output directory has moved across releases
+ # (`HLS_output/Synthesis/vivado_flow` in older versions,
+ # `HLS_output/xilinx/flow_backend` in current). Search the full
+ # HLS_output tree so the script keeps working across versions.
+ return (
+ 'src_root="HLS_output"\n'
+ 'dst_root="vivado_reports"\n'
+ 'mkdir -p "$dst_root"\n'
+ r'find "$src_root" -type f \( -iname "*.rpt" -o -iname "*.xml" \) -exec cp -p {} "$dst_root"/ \;'
+ )
+ else: # TODO: Add more parsing code for different families/softwares
+ return ''
+
+ def _replace_block(self, content, start, end, new_body):
+ pattern = rf'{start}.*?{end}'
+ replacement = f'{start}\n{new_body}\n{end}'
+ return re.sub(pattern, replacement, content, flags=re.S)
+
+ @staticmethod
+ def _ensure_bambu_available():
+ if shutil.which('bambu') is None:
+ raise OSError('Bambu HLS installation not found. Make sure "bambu" is on PATH.')
+
+ @staticmethod
+ def _normalize_bambu_command(args):
+ if args is not None:
+ if isinstance(args, (list, tuple)):
+ tokens = [str(token) for token in args]
+ elif isinstance(args, str):
+ tokens = shlex.split(args)
+ else:
+ raise TypeError('args must be a string or a sequence of strings.')
+ else:
+ tokens = []
+
+ return tokens
+
+ @layer_optimizer(Layer)
+ def init_base_layer(self, layer):
+ reuse_factor = layer.model.config.get_reuse_factor(layer)
+ layer.set_attr('reuse_factor', reuse_factor)
+
+ target_cycles = layer.model.config.get_target_cycles(layer)
+ layer.set_attr('target_cycles', target_cycles)
+
+ @layer_optimizer(Dense)
+ def init_dense(self, layer):
+ index_t = IntegerPrecisionType(width=1, signed=False)
+ compression = layer.model.config.get_compression(layer)
+ if layer.model.config.is_resource_strategy(layer):
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_target_reuse_factor(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ if compression:
+ layer.set_attr('strategy', 'compressed')
+ index_t = layer.get_weights('weight').type.index_precision
+ else:
+ layer.set_attr('strategy', 'resource')
+ elif layer.model.config.get_strategy(layer).lower() == 'resource_unrolled':
+ use_resource_instead = False
+ if layer.get_attr('reuse_factor', 1) == 1:
+ print(
+ f'Unrolled resource strategy cannot be combined with reuse factor 1 in layer "{layer.name}". '
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_target_reuse_factor(layer)
+ if use_resource_instead:
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ layer.set_attr('strategy', 'resource')
+ else:
+ self.set_closest_reuse_factor(layer, n_in, n_out, include_max_rf=False)
+ layer.set_attr('strategy', 'resource_unrolled')
+ elif layer.model.config.get_strategy(layer).lower() in ('distributed_arithmetic', 'da'):
+ rf = layer.get_attr('reuse_factor')
+ if rf != 1:
+ raise Exception(f'Layer {layer.name} has rf = {rf} != 1, but has strategy = "distributed_arithmetic".')
+ layer.set_attr('strategy', 'distributed_arithmetic')
+ else:
+ layer.set_attr('strategy', 'latency')
+ layer.set_attr('index_t', NamedType(f'layer{layer.index}_index', index_t))
+
+ # TODO consolidate these functions into a single `init_conv`
+ @layer_optimizer(Conv1D)
+ def init_conv1d(self, layer):
+ if len(layer.weights['weight'].data.shape) == 2: # This can happen if we assign weights of Dense layer to 1x1 Conv1D
+ layer.weights['weight'].data = np.expand_dims(layer.weights['weight'].data, axis=(0, 1))
+
+ if layer.model.config.is_resource_strategy(layer):
+ layer.set_attr('strategy', 'resource')
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_target_reuse_factor(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ elif layer.model.config.get_strategy(layer).lower() == 'resource_unrolled':
+ use_resource_instead = False
+ if layer.get_attr('reuse_factor', 1) == 1:
+ print(
+ f'Unrolled resource strategy cannot be combined with reuse factor 1 in layer "{layer.name}".'
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+ elif layer.model.config.get_config_value('IOType') == 'io_parallel':
+ print(
+ f'Unrolled resource strategy cannot be combined with io_parallel in layer "{layer.name}". '
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_target_reuse_factor(layer)
+ if use_resource_instead:
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ layer.set_attr('strategy', 'resource')
+ else:
+ self.set_closest_reuse_factor(layer, n_in, n_out, include_max_rf=False)
+ layer.set_attr('strategy', 'resource_unrolled')
+ elif layer.model.config.get_strategy(layer).lower() in ('distributed_arithmetic', 'da'):
+ rf = layer.get_attr('reuse_factor')
+ if rf != 1:
+ raise Exception(f'Layer {layer.name} has rf = {rf} != 1, but has strategy = "distributed_arithmetic".')
+ layer.set_attr('strategy', 'distributed_arithmetic')
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ out_width = layer.get_output_variable().shape[0]
+
+ # Not overriding user parallelization factor, if already set and user has not specified a value
+ user_pf = layer.model.config.get_layer_config_value(layer, 'ParallelizationFactor', None)
+ layer_pf = layer.get_attr('parallelization_factor', None)
+ chosen_pf = user_pf or layer_pf or 1
+ valid_pf = self.get_valid_conv_partition_splits(1, out_width)
+ if chosen_pf not in valid_pf:
+ closest_pf = self.get_closest_reuse_factor(valid_pf, chosen_pf)
+ valid_pf_str = ','.join(map(str, valid_pf))
+ print(
+ f'WARNING: Invalid ParallelizationFactor={chosen_pf} in layer "{layer.name}".'
+ f'Using ParallelizationFactor={closest_pf} instead. Valid ParallelizationFactor(s): {valid_pf_str}.'
+ )
+ else:
+ closest_pf = chosen_pf
+ layer.set_attr('n_partitions', out_width // closest_pf)
+ layer.set_attr('parallelization_factor', closest_pf)
+
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ @layer_optimizer(SeparableConv1D)
+ def init_sepconv1d(self, layer):
+ if layer.model.config.is_resource_strategy(layer):
+ layer.set_attr('strategy', 'resource')
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ out_width = layer.get_output_variable().shape[0]
+ chosen_pf = layer.model.config.get_layer_config_value(layer, 'ParallelizationFactor', 1)
+ valid_pf = self.get_valid_conv_partition_splits(1, out_width)
+ if chosen_pf not in valid_pf:
+ closest_pf = self.get_closest_reuse_factor(valid_pf, chosen_pf)
+ valid_pf_str = ','.join(map(str, valid_pf))
+ print(
+ f'WARNING: Invalid ParallelizationFactor={chosen_pf} in layer "{layer.name}".'
+ f'Using ParallelizationFactor={closest_pf} instead. Valid ParallelizationFactor(s): {valid_pf_str}.'
+ )
+ else:
+ closest_pf = chosen_pf
+ layer.set_attr('n_partitions', out_width // closest_pf)
+
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ # Set the output type of the depthwise phase
+ dw_out_precision, _ = layer.model.config.get_precision(layer, 'dw_output')
+ dw_out_name = layer.name + '_dw_out_t'
+ if layer.model.config.get_config_value('IOType') == 'io_stream':
+ dw_output_t = PackedType(dw_out_name, dw_out_precision, layer.get_attr('n_chan'), n_pack=1)
+ else:
+ dw_output_t = NamedType(dw_out_name, dw_out_precision)
+ layer.set_attr('dw_output_t', dw_output_t)
+
+ @layer_optimizer(DepthwiseConv1D)
+ def init_depconv1d(self, layer):
+ if layer.model.config.is_resource_strategy(layer):
+ layer.set_attr('strategy', 'resource')
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ out_width = layer.get_output_variable().shape[0]
+ chosen_pf = layer.model.config.get_layer_config_value(layer, 'ParallelizationFactor', 1)
+ valid_pf = self.get_valid_conv_partition_splits(1, out_width)
+ if chosen_pf not in valid_pf:
+ closest_pf = self.get_closest_reuse_factor(valid_pf, chosen_pf)
+ valid_pf_str = ','.join(map(str, valid_pf))
+ print(
+ f'WARNING: Invalid ParallelizationFactor={chosen_pf} in layer "{layer.name}".'
+ f'Using ParallelizationFactor={closest_pf} instead. Valid ParallelizationFactor(s): {valid_pf_str}.'
+ )
+ else:
+ closest_pf = chosen_pf
+ layer.set_attr('n_partitions', out_width // closest_pf)
+
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ @layer_optimizer(Conv2D)
+ def init_conv2d(self, layer):
+ if len(layer.weights['weight'].data.shape) == 2: # This can happen if we assign weights of Dense layer to 1x1 Conv2D
+ layer.weights['weight'].data = np.expand_dims(layer.weights['weight'].data, axis=(0, 1))
+
+ if layer.model.config.is_resource_strategy(layer):
+ layer.set_attr('strategy', 'resource')
+ self.set_target_reuse_factor(layer)
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ elif layer.model.config.get_strategy(layer).lower() == 'resource_unrolled':
+ use_resource_instead = False
+ if layer.get_attr('reuse_factor', 1) == 1:
+ print(
+ f'Unrolled resource strategy cannot be combined with reuse factor 1 in layer "{layer.name}". '
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+ elif layer.model.config.get_config_value('IOType') == 'io_parallel':
+ print(
+ f'Unrolled resource strategy cannot be combined with io_parallel in layer "{layer.name}". '
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_target_reuse_factor(layer)
+ if use_resource_instead:
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ layer.set_attr('strategy', 'resource')
+ else:
+ self.set_closest_reuse_factor(layer, n_in, n_out, include_max_rf=False)
+ layer.set_attr('strategy', 'resource_unrolled')
+ elif layer.model.config.get_strategy(layer).lower() in ('distributed_arithmetic', 'da'):
+ rf = layer.get_attr('reuse_factor')
+ if rf != 1:
+ raise Exception(f'Layer {layer.name} has rf = {rf} != 1, but has strategy = "distributed_arithmetic".')
+ layer.set_attr('strategy', 'distributed_arithmetic')
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ out_height = layer.get_output_variable().shape[0]
+ out_width = layer.get_output_variable().shape[1]
+
+ # Not overriding user parallelization factor, if already set and user has not specified a value
+ user_pf = layer.model.config.get_layer_config_value(layer, 'ParallelizationFactor', None)
+ layer_pf = layer.get_attr('parallelization_factor', None)
+ chosen_pf = user_pf or layer_pf or 1
+ if user_pf is not None and layer_pf is not None:
+ if user_pf != layer_pf:
+ warn(
+ f'For layer {layer.name}, parallelization factor of {layer_pf} is defined in the proxy-model, but is overridden by the user to {user_pf}.' # noqa: E501
+ )
+
+ valid_pf = self.get_valid_conv_partition_splits(out_height, out_width)
+ if chosen_pf not in valid_pf:
+ closest_pf = self.get_closest_reuse_factor(valid_pf, chosen_pf)
+ valid_pf_str = ','.join(map(str, valid_pf))
+ print(
+ f'WARNING: Invalid ParallelizationFactor={chosen_pf} in layer "{layer.name}".'
+ f'Using ParallelizationFactor={closest_pf} instead. Valid ParallelizationFactor(s): {valid_pf_str}.'
+ )
+ else:
+ closest_pf = chosen_pf
+ layer.set_attr('n_partitions', out_height * out_width // closest_pf)
+ layer.set_attr('parallelization_factor', closest_pf)
+
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ @layer_optimizer(SeparableConv2D)
+ def init_sepconv2d(self, layer):
+ if layer.model.config.is_resource_strategy(layer):
+ layer.set_attr('strategy', 'resource')
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ out_height = layer.get_output_variable().shape[0]
+ out_width = layer.get_output_variable().shape[1]
+ chosen_pf = layer.model.config.get_layer_config_value(layer, 'ParallelizationFactor', 1)
+ valid_pf = self.get_valid_conv_partition_splits(out_height, out_width)
+ if chosen_pf not in valid_pf:
+ closest_pf = self.get_closest_reuse_factor(valid_pf, chosen_pf)
+ valid_pf_str = ','.join(map(str, valid_pf))
+ print(
+ f'WARNING: Invalid ParallelizationFactor={chosen_pf} in layer "{layer.name}".'
+ f'Using ParallelizationFactor={closest_pf} instead. Valid ParallelizationFactor(s): {valid_pf_str}.'
+ )
+ else:
+ closest_pf = chosen_pf
+
+ layer.set_attr('n_partitions', out_height * out_width // closest_pf)
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ # Set the output type of the depthwise phase
+ dw_out_precision, _ = layer.model.config.get_precision(layer, 'dw_output')
+ dw_out_name = layer.name + '_dw_out_t'
+ if layer.model.config.get_config_value('IOType') == 'io_stream':
+ dw_output_t = PackedType(dw_out_name, dw_out_precision, layer.get_attr('n_chan'), n_pack=1)
+ else:
+ dw_output_t = NamedType(dw_out_name, dw_out_precision)
+ layer.set_attr('dw_output_t', dw_output_t)
+
+ @layer_optimizer(DepthwiseConv2D)
+ def init_depconv2d(self, layer):
+ if layer.model.config.is_resource_strategy(layer):
+ layer.set_attr('strategy', 'resource')
+ n_in, n_out = self.get_layer_mult_size(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ out_height = layer.get_output_variable().shape[0]
+ out_width = layer.get_output_variable().shape[1]
+ chosen_pf = layer.model.config.get_layer_config_value(layer, 'ParallelizationFactor', 1)
+ valid_pf = self.get_valid_conv_partition_splits(out_height, out_width)
+ if chosen_pf not in valid_pf:
+ closest_pf = self.get_closest_reuse_factor(valid_pf, chosen_pf)
+ valid_pf_str = ','.join(map(str, valid_pf))
+ print(
+ f'WARNING: Invalid ParallelizationFactor={chosen_pf} in layer "{layer.name}".'
+ f'Using ParallelizationFactor={closest_pf} instead. Valid ParallelizationFactor(s): {valid_pf_str}.'
+ )
+ else:
+ closest_pf = chosen_pf
+ layer.set_attr('n_partitions', out_height * out_width // closest_pf)
+
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ @layer_optimizer(Pooling1D)
+ def init_pooling1d(self, layer):
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ @layer_optimizer(Pooling2D)
+ def init_pooling2d(self, layer):
+ layer.set_attr('implementation', layer.model.config.get_conv_implementation(layer).lower())
+
+ @layer_optimizer(Embedding)
+ def init_embed(self, layer):
+ if layer.attributes['n_in'] is None:
+ raise Exception('Input length of Embedding layer must be specified.')
+
+ @layer_optimizer(LSTM)
+ def init_lstm(self, layer):
+ # TODO Allow getting recurrent reuse factor from the config
+ reuse_factor = layer.model.config.get_reuse_factor(layer)
+ layer.set_attr('recurrent_reuse_factor', reuse_factor)
+
+ if layer.model.config.is_resource_strategy(layer):
+ n_in, n_out, n_in_recr, n_out_recr = self.get_layer_mult_size(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ self.set_closest_reuse_factor(layer, n_in_recr, n_out_recr, attribute='recurrent_reuse_factor')
+ layer.set_attr('strategy', 'resource')
+ elif layer.model.config.get_strategy(layer).lower() == 'resource_unrolled':
+ use_resource_instead = False
+ if layer.get_attr('reuse_factor', 1) == 1:
+ print(
+ f'Unrolled resource strategy cannot be combined with reuse factor 1 in layer "{layer.name}". '
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+ n_in, n_out, n_in_recr, n_out_recr = self.get_layer_mult_size(layer)
+ if use_resource_instead:
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ self.set_closest_reuse_factor(layer, n_in_recr, n_out_recr, attribute='recurrent_reuse_factor')
+ layer.set_attr('strategy', 'resource')
+ else:
+ self.set_closest_reuse_factor(layer, n_in, n_out, include_max_rf=False)
+ self.set_closest_reuse_factor(
+ layer, n_in_recr, n_out_recr, attribute='recurrent_reuse_factor', include_max_rf=False
+ )
+ layer.set_attr('strategy', 'resource_unrolled')
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ layer.set_attr('index_t', NamedType(f'layer{layer.index}_index', IntegerPrecisionType(width=1, signed=False)))
+
+ @layer_optimizer(GRU)
+ def init_gru(self, layer):
+ reuse_factor = layer.model.config.get_reuse_factor(layer)
+ layer.set_attr('recurrent_reuse_factor', reuse_factor)
+
+ if layer.model.config.is_resource_strategy(layer):
+ n_in, n_out, n_in_recr, n_out_recr = self.get_layer_mult_size(layer)
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ self.set_closest_reuse_factor(layer, n_in_recr, n_out_recr, attribute='recurrent_reuse_factor')
+ layer.set_attr('strategy', 'resource')
+ elif layer.model.config.get_strategy(layer).lower() == 'resource_unrolled':
+ use_resource_instead = False
+ if layer.get_attr('reuse_factor', 1) == 1:
+ print(
+ f'Unrolled resource strategy cannot be combined with reuse factor 1 in layer "{layer.name}". '
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+ n_in, n_out, n_in_recr, n_out_recr = self.get_layer_mult_size(layer)
+ if use_resource_instead:
+ self.set_closest_reuse_factor(layer, n_in, n_out)
+ self.set_closest_reuse_factor(layer, n_in_recr, n_out_recr, attribute='recurrent_reuse_factor')
+ layer.set_attr('strategy', 'resource')
+ else:
+ self.set_closest_reuse_factor(layer, n_in, n_out, include_max_rf=False)
+ self.set_closest_reuse_factor(
+ layer, n_in_recr, n_out_recr, attribute='recurrent_reuse_factor', include_max_rf=False
+ )
+ layer.set_attr('strategy', 'resource_unrolled')
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ layer.set_attr('index_t', NamedType(f'layer{layer.index}_index', IntegerPrecisionType(width=1, signed=False)))
+
+ @layer_optimizer(TimeDistributed)
+ def init_time_distributed(self, layer):
+ loop_mode = layer.get_attr('time_step_loop_parallelism', 'off').lower()
+ if loop_mode == 'unroll' and layer.model.config.get_config_value('IOType') == 'io_stream':
+ warn(f'Cannot unroll time step loop in layer "{layer.name}" while using "io_stream".')
+ loop_mode = 'off'
+ layer.set_attr('time_step_loop_parallelism', loop_mode)
+
+ @layer_optimizer(Bidirectional)
+ def init_bidirectional(self, layer):
+ reuse_factor = layer.model.config.get_reuse_factor(layer)
+
+ for i, d in enumerate(['forward', 'backward']):
+ layer.set_attr(f'{d}_reuse_factor', reuse_factor)
+ layer.set_attr(f'{d}_recurrent_reuse_factor', reuse_factor)
+
+ if layer.model.config.is_resource_strategy(layer):
+ n_in, n_out, n_in_recr, n_out_recr = self.get_layer_mult_size(layer)[i]
+ self.set_closest_reuse_factor(layer, n_in, n_out, attribute=f'{d}_reuse_factor')
+ self.set_closest_reuse_factor(layer, n_in_recr, n_out_recr, attribute=f'{d}_recurrent_reuse_factor')
+ layer.set_attr('strategy', 'resource')
+
+ elif layer.model.config.get_strategy(layer).lower() == 'resource_unrolled':
+ use_resource_instead = False
+ if layer.get_attr('reuse_factor', 1) == 1:
+ print(
+ f'Unrolled resource strategy cannot be combined with reuse factor 1 in layer "{layer.name} ({d})". '
+ 'Using "resource" strategy instead.'
+ )
+ use_resource_instead = True
+
+ n_in, n_out, n_in_recr, n_out_recr = self.get_layer_mult_size(layer)[i]
+ if use_resource_instead:
+ self.set_closest_reuse_factor(layer, n_in, n_out, attribute=f'{d}_reuse_factor')
+ self.set_closest_reuse_factor(layer, n_in_recr, n_out_recr, attribute=f'{d}_recurrent_reuse_factor')
+ layer.set_attr('strategy', 'resource')
+ else:
+ self.set_closest_reuse_factor(layer, n_in, n_out, attribute=f'{d}_reuse_factor', include_max_rf=False)
+ self.set_closest_reuse_factor(
+ layer, n_in_recr, n_out_recr, attribute=f'{d}_recurrent_reuse_factor', include_max_rf=False
+ )
+ layer.set_attr('strategy', 'resource_unrolled')
+ else:
+ layer.set_attr('strategy', 'latency')
+
+ layer.set_attr('index_t', NamedType(f'layer{layer.index}_index', IntegerPrecisionType(width=1, signed=False)))
+
+ @layer_optimizer(GarNet)
+ def init_garnet(self, layer):
+ reuse_factor = layer.attributes['reuse_factor']
+
+ var_converter = BambuArrayVariableConverter(
+ type_converter=BambuHLSTypeConverter(precision_converter=APTypeConverter())
+ )
+
+ # A bit controversial but we are going to set the partitioning of the input here
+ in_layer = layer.model.graph[layer.inputs[0]]
+ in_var = layer.get_input_variable(layer.inputs[0])
+ partition_factor = in_var.shape[1] * (in_var.shape[0] // reuse_factor)
+ in_pragma = ('partition', 'cyclic', partition_factor)
+ new_in_var = var_converter.convert(in_var, pragma=in_pragma)
+ in_layer.set_attr(layer.inputs[0], new_in_var)
+
+ if layer.attributes['collapse']:
+ out_pragma = 'partition'
+ else:
+ partition_factor = layer._output_features * (layer.attributes['n_vertices'] // reuse_factor)
+ out_pragma = ('partition', 'cyclic', partition_factor)
+
+ out_name, out_var = next(iter(layer.variables.items()))
+ new_out_var = var_converter.convert(out_var, pragma=out_pragma)
+
+ layer.set_attr(out_name, new_out_var)
+
+ @layer_optimizer(GarNetStack)
+ def init_garnet_stack(self, layer):
+ self.init_garnet(layer)
+
+ @layer_optimizer(EinsumDense)
+ def init_einsum_dense(self, layer: EinsumDense) -> None:
+ kernel: np.ndarray = layer.attributes['weight_data']
+ bias: np.ndarray | None = layer.attributes['bias_data']
+ equation = layer.attributes['equation']
+ inp_shape = layer.attributes['inp_shape']
+ out_shape = layer.attributes['out_shape']
+
+ kernel_shape = kernel.shape
+ recipe = parse_einsum(equation, inp_shape, kernel_shape)
+ assert not any(recipe['direct_sum_axis']), (
+ 'Do not put direct sum indices (e.g., only appears in one of the operands) in the equation.'
+ 'Use explicit addition operator before instead.'
+ )
+ inp_tpose_idxs, ker_tpose_idxs = recipe['in_transpose_idxs']
+ out_tpose_idxs = recipe['out_transpose_idxs']
+
+ # Pre-transpose kernel (and bias) to save a transpose in cpp. Shouldn't matter for latency strategy though.
+ # hls4ml dense acts like i,ij->j
+ # parser assumes ij,j->i, so we need to transpose the kernel to match
+ kernel = kernel.transpose(ker_tpose_idxs)
+ kernel = kernel.reshape(recipe['I'], recipe['L1'], recipe['C']).transpose(0, 2, 1)
+
+ def to_original_kernel(tkernel: np.ndarray) -> np.ndarray:
+ _kernel = tkernel.transpose(0, 2, 1)
+ _kernel = _kernel.reshape(tuple(kernel_shape[i] for i in ker_tpose_idxs))
+ return _kernel.transpose(np.argsort(ker_tpose_idxs))
+
+ # TODO: for weight in bram mode (resource), broadcasting bias here shall be avoided.
+ if bias is not None:
+ bias = np.broadcast_to(bias, out_shape).transpose(np.argsort(out_tpose_idxs))
+ else:
+ # The automatically created bias is just the last dimension of the output shape
+ # Which is too small in general for einsum dense.
+ # The transpose is just to match the shape in case of have real bias, no real effect.
+ bias = np.zeros(out_shape).transpose(np.argsort(out_tpose_idxs))
+
+ layer.attributes['weight_data'] = kernel
+ layer.attributes['to_original_kernel'] = to_original_kernel
+ layer.attributes['bias_data'] = bias
+ layer.attributes['inp_tpose_idxs'] = inp_tpose_idxs
+ layer.attributes['out_tpose_idxs'] = out_tpose_idxs
+ layer.attributes['out_interpert_shape'] = recipe['out_interpert_shape']
+ layer.attributes['n_free_data'] = recipe['L0']
+ layer.attributes['n_free_kernel'] = recipe['L1']
+ layer.attributes['n_inplace'] = recipe['I']
+ layer.attributes['n_contract'] = recipe['C']
+ pf = layer.attributes.get('parallelization_factor', recipe['L0'])
+ layer.attributes['parallelization_factor'] = pf
+
+ layer.add_weights(compression=layer.model.config.get_compression(layer))
+ layer.add_bias()
+
+ strategy: str | None = layer.model.config.get_strategy(layer)
+ if not strategy:
+ layer.set_attr('strategy', 'latency')
+ return
+ if strategy in ('latency', 'resource', 'distributed_arithmetic'):
+ layer.set_attr('strategy', strategy)
+ return
+ warn(f'Invalid strategy "{strategy}" for EinsumDense layer "{layer.name}". Using "latency" strategy instead.')
+ layer.set_attr('strategy', 'latency')
+
+ @layer_optimizer(Einsum)
+ def init_einsum(self, layer: Einsum) -> None:
+ equation = layer.attributes['equation']
+ inp0_shape = layer.attributes['inp0_shape']
+ inp1_shape = layer.attributes['inp1_shape']
+
+ recipe = parse_einsum(equation, inp0_shape, inp1_shape)
+ assert not any(recipe['direct_sum_axis']), (
+ 'Do not put direct sum indices (e.g., only appears in one of the operands) in the equation.'
+ 'Use explicit addition operator before instead.'
+ )
+ inp0_tpose_idxs, inp1_tpose_idxs = recipe['in_transpose_idxs']
+ out_tpose_idxs = recipe['out_transpose_idxs']
+
+ layer.attributes.update(recipe)
+ layer.attributes['n_free0'] = recipe['L0']
+ layer.attributes['n_free1'] = recipe['L1']
+ layer.attributes['n_inplace'] = recipe['I']
+ layer.attributes['n_contract'] = recipe['C']
+ layer.attributes['out_interpert_shape'] = recipe['out_interpert_shape']
+
+ layer.attributes['inp0_tpose_idxs'] = inp0_tpose_idxs
+ layer.attributes['inp1_tpose_idxs'] = inp1_tpose_idxs
+ layer.attributes['out_tpose_idxs'] = out_tpose_idxs
+
+ pf = layer.attributes.get('parallelization_factor', recipe['L0'])
+ layer.attributes['parallelization_factor'] = pf
+
+ strategy: str | None = layer.model.config.get_strategy(layer)
+ if not strategy:
+ layer.set_attr('strategy', 'latency')
+ return
+ if strategy.lower() == 'resource':
+ layer.set_attr('strategy', 'resource')
+ return
+ if strategy.lower() in ('latency', 'distributed_arithmetic'):
+ layer.set_attr('strategy', 'latency')
+ return
+ warn(f'Invalid strategy "{strategy}" for Einsum layer "{layer.name}". Using "latency" strategy instead.')
+ layer.set_attr('strategy', 'latency')
diff --git a/hls4ml/backends/bambu/bambu_types.py b/hls4ml/backends/bambu/bambu_types.py
new file mode 100644
index 0000000000..f2c58e4b2e
--- /dev/null
+++ b/hls4ml/backends/bambu/bambu_types.py
@@ -0,0 +1,140 @@
+from hls4ml.backends.fpga.fpga_types import (
+ ArrayVariableConverter,
+ CompressedTypeConverter,
+ ExponentTypeConverter,
+ HLSTypeConverter,
+ InplaceStreamVariableConverter,
+ NamedTypeConverter,
+ StreamVariableConverter,
+ TypeDefinition,
+ TypePrecisionConverter,
+ VariableDefinition,
+)
+from hls4ml.model.types import CompressedType, ExponentType, NamedType, PackedType
+
+# region PackedType
+#
+# Bug A workaround: emit a concrete (non-template) struct per stream payload
+# type instead of `typedef nnet::array name;`. With the templated
+# typedef, Bambu's InterfaceInfer reads the AXIS TDATA Bitwidth from the
+# inner element (W) instead of the aggregate (N*W); the simulator's
+# per-beat channel layout then doesn't match the C-sim gold reference, and
+# cosim aborts with
+# ERROR: MDPI driver: Channel parameter mismatch with respect to gold
+# A concrete struct with the same members forces InterfaceInfer to use
+# sizeof(struct) for the AXIS Bitwidth.
+# (Explicit specialization of `nnet::array` does NOT help — concrete struct
+# is the only emission Bambu picks up correctly.)
+
+
+class BambuPackedTypeConverter(TypeDefinition, TypePrecisionConverter):
+ def definition_cpp(self):
+ n_elem_expr = '/' if self.unpack else '*'
+ n_elem = str(self.n_elem) + n_elem_expr + str(self.n_pack)
+ precision = self.precision.definition_cpp()
+ name = self.name
+ # User-defined element-wise `operator=` (Bug B workaround). Without
+ # it, clang lowers `pack = stream.read()` as a single aggregate
+ # copy, which Bambu's InterfaceInfer setReadInterface pass refuses
+ # to handle:
+ # error -> unexpected condition (gc->args.size() == 2)
+ # void InterfaceInfer::setReadInterface(...)
+ # The element-wise loop body lowers to per-element loads/stores
+ # that setReadInterface pattern-matches.
+ return (
+ f'struct {name} {{\n'
+ f' typedef {precision} value_type;\n'
+ f' static const unsigned size = {n_elem};\n'
+ f' {precision} data[{n_elem}];\n'
+ f' {precision} &operator[](size_t pos) {{ return data[pos]; }}\n'
+ f' const {precision} &operator[](size_t pos) const {{ return data[pos]; }}\n'
+ f' {name} &operator=(const {name} &other) {{\n'
+ f' if (&other == this) return *this;\n'
+ f' #pragma clang loop unroll(full)\n'
+ f' for (unsigned i = 0; i < size; i++) data[i] = other.data[i];\n'
+ f' return *this;\n'
+ f' }}\n'
+ f' bool operator==(const {name} &other) const {{\n'
+ f' for (unsigned i = 0; i < size; i++)\n'
+ f' if (data[i] != other.data[i]) return false;\n'
+ f' return true;\n'
+ f' }}\n'
+ f' bool operator!=(const {name} &other) const {{ return !(*this == other); }}\n'
+ f'}};\n'
+ )
+
+
+class BambuHLSTypeConverter(HLSTypeConverter):
+ def __init__(self, precision_converter):
+ self.precision_converter = precision_converter
+ self.type_map = {
+ NamedType: NamedTypeConverter,
+ CompressedType: CompressedTypeConverter,
+ ExponentType: ExponentTypeConverter,
+ PackedType: BambuPackedTypeConverter,
+ }
+
+
+# endregion
+
+# region ArrayVariable
+
+
+class BambuArrayVariableDefinition(VariableDefinition):
+ def definition_cpp(self, name_suffix='', as_reference=False):
+ return '{type} {name}{suffix}[{shape}]'.format(
+ type=self.type.name, name=self.name, suffix=name_suffix, shape=self.size_cpp()
+ )
+
+
+class BambuInplaceArrayVariableDefinition(VariableDefinition):
+ def definition_cpp(self):
+ return f'auto& {self.name} = {self.input_var.name}'
+
+
+class BambuArrayVariableConverter(ArrayVariableConverter):
+ def __init__(self, type_converter):
+ super().__init__(type_converter=type_converter, prefix='Bambu', definition_cls=BambuArrayVariableDefinition)
+
+
+class BambuInplaceArrayVariableConverter(ArrayVariableConverter):
+ def __init__(self, type_converter):
+ super().__init__(type_converter=type_converter, prefix='Bambu', definition_cls=BambuInplaceArrayVariableDefinition)
+
+
+# endregion
+
+# region StreamVariable
+
+
+class BambuStreamVariableDefinition(VariableDefinition):
+ def definition_cpp(self, name_suffix='', as_reference=False):
+ if as_reference: # Function parameter
+ return f'hls::stream<{self.type.name}> &{self.name}{name_suffix}'
+ else: # Declaration
+ return 'hls::stream<{type}> {name}{suffix}("{name}")'.format(
+ type=self.type.name, name=self.name, suffix=name_suffix
+ )
+
+
+class BambuInplaceStreamVariableDefinition(VariableDefinition):
+ def definition_cpp(self):
+ return f'auto& {self.name} = {self.input_var.name}'
+
+
+class BambuStreamVariableConverter(StreamVariableConverter):
+ def __init__(self, type_converter):
+ super().__init__(type_converter=type_converter, prefix='Bambu', definition_cls=BambuStreamVariableDefinition)
+
+
+# endregion
+
+# region InplaceStreamVariable
+
+
+class BambuInplaceStreamVariableConverter(InplaceStreamVariableConverter):
+ def __init__(self, type_converter):
+ super().__init__(type_converter=type_converter, prefix='Bambu', definition_cls=BambuInplaceStreamVariableDefinition)
+
+
+# endregion
diff --git a/hls4ml/backends/bambu/passes/__init__.py b/hls4ml/backends/bambu/passes/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/hls4ml/backends/bambu/passes/bn_quant.py b/hls4ml/backends/bambu/passes/bn_quant.py
new file mode 100644
index 0000000000..829258d807
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/bn_quant.py
@@ -0,0 +1,166 @@
+import numpy as np
+
+from hls4ml.backends.fpga.fpga_layers import BatchNormalizationQuantizedTanh
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import BatchNormalization, register_layer
+from hls4ml.model.optimizer import OptimizerPass
+from hls4ml.model.types import IntegerPrecisionType, NamedType, XnorPrecisionType
+
+batchnorm_quantized_tanh_config_template = """struct config{index} : nnet::batchnorm_quantized_tanh_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_filt = {n_filt};
+ static const unsigned n_scale_bias = (n_filt == -1) ? n_in : n_filt;
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+}};\n"""
+
+batchnorm_quantized_tanh_function_template = (
+ 'nnet::normalize_{quantize}_tanh<{input_t}, {config}>({input}, {output}, {threshold});'
+)
+
+bn_include_list = ['nnet_utils/nnet_batchnorm.h', 'nnet_utils/nnet_batchnorm_stream.h']
+
+
+class BatchNormalizationQuantizedTanhConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(BatchNormalizationQuantizedTanh)
+ self.template = batchnorm_quantized_tanh_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['n_in'] = node.get_input_variable().size_cpp()
+
+ return self.template.format(**params)
+
+
+class BatchNormalizationQuantizedTanhFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(BatchNormalizationQuantizedTanh, include_header=bn_include_list)
+ self.template = batchnorm_quantized_tanh_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ if node.get_attr('quantize') == 2:
+ params['quantize'] = 'binary'
+ params['threshold'] = node.get_weights('threshold').name
+ elif node.get_attr('quantize') == 3:
+ params['quantize'] = 'ternary'
+ params['threshold'] = node.get_weights('threshold_hi').name + ', ' + node.get_weights('threshold_lo').name
+
+ return self.template.format(**params)
+
+
+def register_bn_quant(backend):
+ # Register the layer types to the layer map
+ register_layer('BatchNormalizationQuantizedTanh', BatchNormalizationQuantizedTanh)
+
+ # Register the optimization passes
+ backend.register_pass('merge_batch_norm_quantized_tanh', MergeBatchNormAndQuantizedTanh)
+ backend.register_pass('quantize_dense_output', QuantizeDenseOutput)
+
+ # Register template passes
+ backend.register_template(BatchNormalizationQuantizedTanhConfigTemplate)
+ backend.register_template(BatchNormalizationQuantizedTanhFunctionTemplate)
+
+
+class MergeBatchNormAndQuantizedTanh(OptimizerPass):
+ def match(self, node):
+ is_match = (
+ node.class_name == 'Activation'
+ and node.get_attr('activation') in ['binary', 'binary_tanh', 'ternary', 'ternary_tanh']
+ or node.class_name == 'TernaryTanh'
+ )
+ is_match = is_match and isinstance(node.get_input_node(), BatchNormalization)
+ return is_match
+
+ def transform(self, model, node):
+ bn_layer = node.get_input_node()
+ # Make a new layer with the new attributes
+ quantize = 0
+ if 'binary' in node.get_attr('activation'):
+ quantize = 2
+ if 'ternary' in node.get_attr('activation'):
+ quantize = 3
+ attrs = {
+ 'n_in': bn_layer.get_attr('n_in'),
+ 'n_out': bn_layer.get_attr('n_in'),
+ 'n_filt': bn_layer.get_attr('n_filt'),
+ 'quantize': quantize,
+ 'trace': bn_layer.get_attr('trace'),
+ }
+ bnbt_layer = model.make_node(BatchNormalizationQuantizedTanh, 'bnbt_' + bn_layer.name, attrs, bn_layer.inputs)
+ bnbt_layer.set_thresholds(
+ bn_layer.get_weights('scale').data, bn_layer.get_weights('bias').data, node.get_attr('threshold', 0.5)
+ )
+ # Remove the BatchNormalization layer
+ model.remove_node(bn_layer)
+ # Replace the old Activation layer with this one
+ model.replace_node(node, bnbt_layer)
+
+ return True
+
+
+class QuantizeDenseOutput(OptimizerPass):
+ def match(self, node):
+ is_dense = node.class_name == 'Dense'
+ input_node = node.get_input_node()
+ is_input_bnqt = input_node is not None and input_node.class_name == 'BatchNormalizationQuantizedTanh'
+ quantizer = node.get_attr('weight_quantizer')
+ is_binary_ternary = quantizer is not None and (
+ quantizer.__class__.__name__ == 'BinaryQuantizer' or quantizer.__class__.__name__ == 'TernaryQuantizer'
+ )
+ return is_dense and is_input_bnqt and is_binary_ternary
+
+ def transform(self, model, node):
+ # Compute the required precision and update the variables
+ # Number of bits for output is log2 of number of input nodes
+ # Since this is the number of uint<1>'s which are summed
+ nbits = int(np.ceil(np.log2(node.attributes['n_in'])) + 2)
+ out_type = IntegerPrecisionType(width=nbits)
+ accum_t = NamedType(f'layer{node.index}_accum_t', out_type)
+ node.set_attr('accum_t', accum_t)
+ out_var = node.get_output_variable()
+ out_var.type.precision = out_type
+
+ quantized_data = None
+ quantized_precision = None
+ quantizer = node.get_attr('weight_quantizer')
+ if quantizer.__class__.__name__ == 'BinaryQuantizer':
+ quantized_precision = XnorPrecisionType()
+ elif quantizer.__class__.__name__ == 'TernaryQuantizer':
+ quantized_precision = IntegerPrecisionType(width=2)
+ else:
+ print(f'WARNING: Unknown quantizer - {quantizer.__class__.__name__}. Bailing out')
+ return False
+ quantizer.bits = quantized_precision.width
+ quantizer.hls_type = quantized_precision
+ quantized_data = quantizer(node.weights['weight'].data)
+
+ weights = node.weights['weight']
+ weights.data = quantized_data
+ weights.type.name = f'weight{node.index}_t'
+ weights.update_precision(quantized_precision)
+
+ bias = node.weights['bias']
+ bias.data = np.zeros(shape=(node.get_attr('n_out')))
+ bias.type.name = f'bias{node.index}_t'
+ bias.nzeros = 0
+ bias.update_precision(quantized_precision)
+
+ # If followed by the BatchNormalizationBinaryTanh, update its input
+ # Also requantise the weights
+ bd_out_nodes = node.get_output_nodes()
+ for out_node in bd_out_nodes:
+ if isinstance(out_node, BatchNormalizationQuantizedTanh):
+ var_names = []
+ if quantizer.__class__.__name__ == 'BinaryQuantizer':
+ var_names.append('threshold')
+ elif quantizer.__class__.__name__ == 'TernaryQuantizer':
+ var_names.append('threshold_hi')
+ var_names.append('threshold_lo')
+ for var_name in var_names:
+ threshold_var = out_node.weights[var_name]
+ threshold_var.update_precision(out_type)
+ threshold_var.data = np.floor(threshold_var.data)
+
+ return False
diff --git a/hls4ml/backends/bambu/passes/broadcast_stream.py b/hls4ml/backends/bambu/passes/broadcast_stream.py
new file mode 100644
index 0000000000..1b5554820c
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/broadcast_stream.py
@@ -0,0 +1,116 @@
+import numpy as np
+
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import Concatenate, Layer, Merge, register_layer
+from hls4ml.model.optimizer import OptimizerPass
+
+
+class Broadcast(Layer):
+ """Inserted between layers for broadcasting."""
+
+ def initialize(self):
+ shape = self.attributes['target_shape']
+ if shape[0] is None:
+ shape = shape[1:]
+ self.add_output_variable(shape)
+
+
+broadcast_function_template = 'nnet::broadcast_stream<{input_t}, {output_t}, {config}>({input}, {output});'
+broadcast_config_template = """struct config{index} : nnet::broadcast_config {{
+ static const unsigned in_width = {in_width};
+ static const unsigned in_height = {in_height};
+ static const unsigned in_chan = {in_chan};
+ static const unsigned out_width = {out_width};
+ static const unsigned out_height = {out_height};
+ static const unsigned out_chan = {out_chan};
+}};\n"""
+broadcast_include_list = ['nnet_utils/nnet_stream.h']
+
+
+class BroadcastConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Broadcast)
+ self.template = broadcast_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['in_height'] = node.get_input_variable().shape[0]
+ params['in_width'] = node.get_input_variable().shape[1]
+ params['in_chan'] = node.get_input_variable().shape[2]
+ params['out_height'] = node.get_output_variable().shape[0]
+ params['out_width'] = node.get_output_variable().shape[1]
+ params['out_chan'] = node.get_output_variable().shape[2]
+
+ return self.template.format(**params)
+
+
+class BroadcastFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(Broadcast, include_header=broadcast_include_list)
+ self.template = broadcast_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ return self.template.format(**params)
+
+
+def register_broadcast_stream(backend):
+ # Register the layer types to the layer map
+ register_layer('Broadcast', Broadcast)
+
+ # Register the optimization passes
+ backend.register_pass('broadcast_stream', BroadcastStream)
+
+ # Register template passes
+ backend.register_template(BroadcastConfigTemplate)
+ backend.register_template(BroadcastFunctionTemplate)
+
+
+class BroadcastStream(OptimizerPass):
+ def match(self, node):
+ if isinstance(node, Merge) and not isinstance(node, Concatenate):
+ inp1 = node.get_input_variable(node.inputs[0])
+ inp2 = node.get_input_variable(node.inputs[1])
+ return inp1.shape != inp2.shape
+ else:
+ return False
+
+ def transform(self, model, node):
+ if model.config.backend.name not in ['Vivado'] or model.config.get_config_value('IOType') != 'io_stream':
+ return False
+
+ inp = [node.get_input_variable(inp_name) for inp_name in node.inputs]
+
+ if np.prod(inp[0].shape) > np.prod(inp[1].shape):
+ idx = 1
+ attrs = {'target_shape': inp[0].shape}
+ else:
+ idx = 0
+ attrs = {'target_shape': inp[1].shape}
+
+ def supported_broadcast(inp_shape, target_shape):
+ # Must be (H, W, C)
+ if not len(inp_shape) == 3:
+ return False
+ # Supported: (1, 1, C) -> (H, W, C)
+ if inp_shape[0] == inp_shape[1] == 1 and inp_shape[2] == target_shape[2]:
+ return True
+ # Supported: (H, W, 1) -> (H, W, C)
+ if inp_shape[2] == 1 and inp_shape[0] == target_shape[0] and inp_shape[1] == target_shape[1]:
+ return True
+ return False
+
+ brdcst_inp = node.inputs[idx]
+ inp_shape = node.get_input_variable(brdcst_inp).shape
+ target_shape = attrs['target_shape']
+ if not supported_broadcast(inp_shape, target_shape):
+ raise RuntimeError(
+ f'Unsupported broadcast type for stream: {inp_shape} -> {target_shape};'
+ + 'Only (1, 1, C) -> (H, W, C) and (H, W, 1) -> (H, W, C) currently supported'
+ )
+ brdcst_out = 'broadcast_' + brdcst_inp
+ brdcst_layer = model.make_node('Broadcast', brdcst_out, attrs, [brdcst_inp].copy())
+ model.insert_node(brdcst_layer, before=node, input_idx=idx)
+ node.inputs[idx] = brdcst_out
+
+ return True
diff --git a/hls4ml/backends/bambu/passes/conv_same_pad.py b/hls4ml/backends/bambu/passes/conv_same_pad.py
new file mode 100644
index 0000000000..8946e493fc
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/conv_same_pad.py
@@ -0,0 +1,105 @@
+from hls4ml.model.layers import Conv1D, Conv2D, SeparableConv1D, SeparableConv2D
+from hls4ml.model.optimizer import OptimizerPass
+
+
+class InsertZeroPaddingBeforeConv1D(OptimizerPass):
+ name = 'insert_zero_padding_before_conv1d'
+
+ def match(self, node):
+ is_match = isinstance(node, (Conv1D, SeparableConv1D)) and (
+ (node.get_attr('pad_left') != 0) or (node.get_attr('pad_right') != 0)
+ )
+ return is_match
+
+ def transform(self, model, node):
+ if model.config.get_config_value('IOType') != 'io_stream':
+ return False
+
+ # Get the padding parameters from Conv1D layer
+ pad_left = node.get_attr('pad_left')
+ pad_right = node.get_attr('pad_right')
+
+ # Check if no padding needs to be done
+ if pad_left == pad_right == 0:
+ return False
+
+ out_width = pad_left + node.get_attr('in_width') + pad_right
+
+ attrs = {
+ 'pad_left': pad_left,
+ 'pad_right': pad_right,
+ 'in_width': node.get_attr('in_width'),
+ 'out_width': out_width,
+ 'n_chan': node.get_attr('n_chan'),
+ 'data_format': node.get_attr('data_format', 'channels_last'),
+ }
+
+ # Switch Conv1D layer padding to 'valid'
+ node.set_attr('pad_left', 0)
+ node.set_attr('pad_right', 0)
+ node.set_attr('in_width', out_width)
+
+ # Insert new ZeroPadding1D node above Conv1D
+ padding_layer = model.make_node('ZeroPadding1D', 'zp1d_' + node.name, attrs, node.inputs.copy())
+ padding_layer.get_output_variable().type.precision = node.get_input_variable().type.precision
+ model.insert_node(padding_layer)
+
+ return True
+
+
+class InsertZeroPaddingBeforeConv2D(OptimizerPass):
+ name = 'insert_zero_padding_before_conv2d'
+
+ def match(self, node):
+ is_match = isinstance(node, (Conv2D, SeparableConv2D)) and (
+ (node.get_attr('pad_left') != 0)
+ or (node.get_attr('pad_right') != 0)
+ or (node.get_attr('pad_top') != 0)
+ or (node.get_attr('pad_bottom') != 0)
+ )
+ return is_match
+
+ def transform(self, model, node):
+ if model.config.get_config_value('IOType') != 'io_stream':
+ return False
+
+ # Get the padding parameters from Conv2D layer
+ pad_top = node.get_attr('pad_top')
+ pad_bottom = node.get_attr('pad_bottom')
+ pad_left = node.get_attr('pad_left')
+ pad_right = node.get_attr('pad_right')
+
+ # Check if no padding neeeds to be done
+ if pad_top == pad_bottom == pad_left == pad_right == 0:
+ return False
+
+ out_height = pad_top + node.get_attr('in_height') + pad_bottom
+ out_width = pad_left + node.get_attr('in_width') + pad_right
+
+ attrs = {
+ 'pad_top': pad_top,
+ 'pad_bottom': pad_bottom,
+ 'pad_left': pad_left,
+ 'pad_right': pad_right,
+ 'in_height': node.get_attr('in_height'),
+ 'in_width': node.get_attr('in_width'),
+ 'out_height': out_height,
+ 'out_width': out_width,
+ 'n_chan': node.get_attr('n_chan'),
+ 'data_format': node.get_attr('data_format', 'channels_last'),
+ }
+
+ # Switch Conv2D layer padding to 'valid'
+ node.set_attr('pad_top', 0)
+ node.set_attr('pad_bottom', 0)
+ node.set_attr('pad_left', 0)
+ node.set_attr('pad_right', 0)
+ node.set_attr('in_height', out_height)
+ node.set_attr('in_width', out_width)
+
+ # Insert new ZeroPadding2D node above Conv2D
+ padding_layer = model.make_node('ZeroPadding2D', 'zp2d_' + node.name, attrs, node.inputs.copy())
+ padding_layer.get_output_variable().type.precision = node.get_input_variable().type.precision
+ model.insert_node(padding_layer, before=node)
+
+ return True
diff --git a/hls4ml/backends/bambu/passes/conv_stream.py b/hls4ml/backends/bambu/passes/conv_stream.py
new file mode 100644
index 0000000000..a8032e7430
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/conv_stream.py
@@ -0,0 +1,47 @@
+from hls4ml.model.layers import Conv1D, Conv2D, SeparableConv1D, SeparableConv2D
+from hls4ml.model.optimizer import OptimizerPass
+
+
+class GenerateConvStreamingInstructions(OptimizerPass):
+ """Generates the instructions for streaming implementation of CNNs"""
+
+ def match(self, node):
+ is_match = (
+ isinstance(node, (Conv1D, SeparableConv1D, Conv2D, SeparableConv2D))
+ and node.model.config.get_config_value('IOType').lower() == 'io_stream'
+ and node.get_attr('implementation').lower() == 'encoded'
+ )
+ return is_match
+
+ def transform(self, model, node):
+ node_class = node.__class__.__name__
+ if '1D' in node_class:
+ self._generate_1d_instructions(node)
+ elif '2D' in node_class:
+ self._generate_2d_instructions(node)
+ else:
+ raise Exception(f'Cannot generate instructions for node {node.name} ({node_class})')
+
+ def _generate_1d_instructions(self, node):
+ min_w, instructions = node.model.config.backend.compute_conv1d_instructions(
+ node.get_input_variable().shape[0],
+ node.get_input_variable().shape[1],
+ node.get_attr('filt_width'),
+ node.get_attr('stride_width'),
+ )
+ instructions_str = ','.join(str(i) for i in instructions)
+ node.set_attr('min_width', min_w)
+ node.set_attr('instructions', instructions_str)
+
+ def _generate_2d_instructions(self, node):
+ min_h, min_w, instructions = node.model.config.backend.compute_conv2d_instructions(
+ node.get_input_variable().shape[0],
+ node.get_input_variable().shape[1],
+ node.get_input_variable().shape[2],
+ node.get_attr('filt_height'),
+ node.get_attr('stride_height'),
+ )
+ instructions_str = ','.join(str(i) for i in instructions)
+ node.set_attr('min_height', min_h)
+ node.set_attr('min_width', min_w)
+ node.set_attr('instructions', instructions_str)
diff --git a/hls4ml/backends/bambu/passes/convolution_templates.py b/hls4ml/backends/bambu/passes/convolution_templates.py
new file mode 100644
index 0000000000..17dbb8710a
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/convolution_templates.py
@@ -0,0 +1,688 @@
+from hls4ml.backends.backend import get_backend
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import (
+ Conv1D,
+ Conv2D,
+ Conv2DBatchnorm,
+ DepthwiseConv1D,
+ DepthwiseConv2D,
+ SeparableConv1D,
+ SeparableConv2D,
+)
+
+# Shared multiplication template
+
+conv_mult_config_template = """struct config{index}_mult : nnet::dense_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned strategy = nnet::{strategy};
+ static const unsigned n_zeros = {nzeros};
+ static const unsigned multiplier_limit = DIV_ROUNDUP(n_in * n_out, reuse_factor) - n_zeros / reuse_factor;
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+ typedef {weight_t.name} weight_t;
+ template
+ using kernel = {dense_function};
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+# Conv1D templates
+
+conv1d_config_template = """struct config{index} : nnet::conv1d_config {{
+ static const unsigned pad_left = {pad_left};
+ static const unsigned pad_right = {pad_right};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned filt_width = {filt_width};
+ static const unsigned kernel_size = filt_width;
+ static const unsigned n_filt = {n_filt};
+ static const unsigned stride_width = {stride_width};
+ static const unsigned dilation = {dilation};
+ static const unsigned out_width = {out_width};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned n_zeros = {nzeros};
+ static const unsigned multiplier_limit =
+ DIV_ROUNDUP(kernel_size * n_chan * n_filt, reuse_factor) - n_zeros / reuse_factor;
+ static const bool store_weights_in_bram = false;
+ static const unsigned strategy = nnet::{strategy};
+ static const nnet::conv_implementation implementation = nnet::conv_implementation::{implementation};
+ static const unsigned min_width = {min_width};
+ static const ap_uint pixels[min_width];
+ static const unsigned n_partitions = {n_partitions};
+ static const unsigned n_pixels = out_width / n_partitions;
+ template
+ using fill_buffer = {fill_fn};
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+ typedef {weight_t.name} weight_t;
+ typedef {config_t} mult_config;
+ template
+ using scale_index = nnet::{scale_index_type};
+ template
+ using conv_kernel = {conv_fn};
+}};
+const ap_uint config{index}::pixels[] = {{{instructions}}};\n"""
+
+conv1d_function_template = 'nnet::conv_1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {b});'
+depthconv1d_function_template = (
+ 'nnet::depthwise_conv_1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {b});'
+)
+
+conv1d_include_list = ['nnet_utils/nnet_conv1d.h', 'nnet_utils/nnet_conv1d_stream.h']
+
+
+class Conv1DConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((Conv1D, DepthwiseConv1D))
+ self.template = conv1d_config_template
+ self.mult_template = conv_mult_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['dilation'] = node.get_attr('dilation', 1)
+ params['nzeros'] = node.get_weights('weight').nzeros
+
+ params['config_t'] = f'config{node.index}_mult'
+ if node.get_attr('in_width') == node.get_attr('min_width'):
+ params['scale_index_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_type'] = 'scale_index_regular'
+
+ namespace = params['namespace']
+ if node.model.config.get_config_value('IOType') == 'io_parallel':
+ params['fill_fn'] = f'{namespace}::fill_buffer_{node.index}'
+ else:
+ params['fill_fn'] = 'nnet::FillConv1DBuffer'
+
+ is_pointwise_parallel_latency = (
+ node.get_attr('filt_width') == 1
+ and node.get_attr('strategy').lower() == 'latency'
+ and node.model.config.get_config_value('IOType') == 'io_parallel'
+ )
+
+ n_partitions = node.attributes['n_partitions']
+
+ if is_pointwise_parallel_latency and n_partitions == 1:
+ params['conv_fn'] = 'nnet::BatchedDenseForConv1D'
+ else:
+ if node.get_attr('strategy').lower() == 'latency':
+ params['conv_fn'] = 'nnet::Conv1DLatency'
+ else:
+ params['conv_fn'] = 'nnet::Conv1DResource'
+
+ params['min_width'] = node.get_attr('min_width', node.get_attr('in_width'))
+
+ # explicit constructor
+ instr_raw = node.get_attr('instructions', '0')
+ instr_list = [s.strip() for s in instr_raw.split(',') if s.strip() != '']
+ if len(instr_list) == 1 and instr_list[0] == '0':
+ instr_list = ['0'] * int(params['min_width'])
+ index_token = params['index']
+ params['instructions'] = ','.join(f'ap_uint({v})' for v in instr_list)
+
+ conv_config = self.template.format(**params)
+
+ mult_params = self._default_config_params(node)
+ if is_pointwise_parallel_latency and n_partitions == 1:
+ mult_params['n_in'] = (
+ node.get_attr('in_width') * node.get_attr('n_chan') * node.get_attr('filt_width') // n_partitions
+ )
+ mult_params['n_out'] = node.get_attr('in_width') * node.get_attr('n_filt') // n_partitions
+ else:
+ mult_params['n_in'] = node.get_attr('n_chan') * node.get_attr('filt_width')
+ mult_params['n_out'] = node.get_attr('n_filt')
+ mult_params['nzeros'] = node.get_weights('weight').nzeros
+ mult_params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('weight').type.precision
+ )
+
+ namespace = params['namespace']
+
+ if node.get_attr('strategy').lower() == 'latency':
+ if isinstance(node, DepthwiseConv1D):
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseLatency'
+ else:
+ mult_params['dense_function'] = 'nnet::DenseLatency'
+ elif node.get_attr('strategy').lower() == 'resource':
+ if isinstance(node, DepthwiseConv1D):
+ if int(mult_params['reuse_factor']) <= int(mult_params['n_out']):
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseResource_rf_leq_nout'
+ else:
+ if int(mult_params['reuse_factor']) % int(mult_params['n_out']) == 0:
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseResource_rf_gt_nout_rem0'
+ else:
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseResource_rf_gt_nout'
+ else:
+ if int(mult_params['reuse_factor']) <= int(mult_params['n_in']):
+ mult_params['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
+ else:
+ if int(mult_params['reuse_factor']) % int(mult_params['n_in']) == 0:
+ mult_params['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
+ else:
+ mult_params['dense_function'] = 'nnet::DenseResource_rf_gt_nin'
+ elif node.get_attr('strategy').lower() == 'resource_unrolled':
+ mult_params['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}'
+ elif node.get_attr('strategy').lower() == 'distributed_arithmetic':
+ mult_params['dense_function'] = f'{namespace}::dense_da_wrapper_{node.index}'
+
+ mult_config = self.mult_template.format(**mult_params)
+
+ return mult_config + '\n' + conv_config
+
+ def match(self, node):
+ if node.get_attr('strategy') == 'distributed_arithmetic':
+ io_type = node.model.config.get_config_value('IOType')
+ if io_type == 'io_parallel':
+ # DA impl use alternate entry point for io_parallel conv
+ return False
+ return super().match(node)
+
+
+class Conv1DFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(Conv1D, include_header=conv1d_include_list)
+ self.template = conv1d_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'
+ params['w'] = node.get_weights('weight').name
+ params['b'] = node.get_weights('bias').name
+
+ return self.template.format(**params)
+
+ def match(self, node):
+ if node.get_attr('strategy') == 'distributed_arithmetic':
+ io_type = node.model.config.get_config_value('IOType')
+ if io_type == 'io_parallel':
+ # DA impl use alternate entry point for io_parallel conv
+ return False
+ return super().match(node)
+
+
+class DepthwiseConv1DFunctionTemplate(Conv1DFunctionTemplate):
+ def __init__(self):
+ super(Conv1DFunctionTemplate, self).__init__(DepthwiseConv1D, include_header=sepconv1d_include_list)
+ self.template = depthconv1d_function_template
+
+
+# Conv2D Templates
+
+conv2d_config_template = """struct config{index} : nnet::conv2d_config {{
+ static const unsigned pad_top = {pad_top};
+ static const unsigned pad_bottom = {pad_bottom};
+ static const unsigned pad_left = {pad_left};
+ static const unsigned pad_right = {pad_right};
+ static const unsigned in_height = {in_height};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned filt_height = {filt_height};
+ static const unsigned filt_width = {filt_width};
+ static const unsigned kernel_size = filt_height * filt_width;
+ static const unsigned n_filt = {n_filt};
+ static const unsigned stride_height = {stride_height};
+ static const unsigned stride_width = {stride_width};
+ static const unsigned out_height = {out_height};
+ static const unsigned out_width = {out_width};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned n_zeros = {nzeros};
+ static const unsigned multiplier_limit =
+ DIV_ROUNDUP(kernel_size * n_chan * n_filt, reuse_factor) - n_zeros / reuse_factor;
+ static const bool store_weights_in_bram = false;
+ static const unsigned strategy = nnet::{strategy};
+ static const nnet::conv_implementation implementation = nnet::conv_implementation::{implementation};
+ static const unsigned min_height = {min_height};
+ static const unsigned min_width = {min_width};
+ static const ap_uint pixels[min_height * min_width];
+ static const unsigned n_partitions = {n_partitions};
+ static const unsigned n_pixels = out_height * out_width / n_partitions;
+ template
+ using fill_buffer = {fill_fn};
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+ typedef {weight_t.name} weight_t;
+ typedef {config_t} mult_config;
+ template
+ using scale_index_height = nnet::{scale_index_height_type};
+ template
+ using scale_index_width = nnet::{scale_index_width_type};
+}};
+const ap_uint config{index}::pixels[] = {{{instructions}}};\n"""
+
+conv2d_function_template = 'nnet::conv_2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {b});'
+depthconv2d_function_template = (
+ 'nnet::depthwise_conv_2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {b});'
+)
+
+conv2d_include_list = ['nnet_utils/nnet_conv2d.h', 'nnet_utils/nnet_conv2d_stream.h']
+
+
+class Conv2DConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((Conv2D, Conv2DBatchnorm, DepthwiseConv2D))
+ self.template = conv2d_config_template
+ self.mult_template = conv_mult_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['dilation'] = node.get_attr('dilation', 1)
+ params['nzeros'] = node.get_weights('weight').nzeros
+
+ params['config_t'] = f'config{node.index}_mult'
+
+ if node.get_attr('in_height') == node.get_attr('min_height'):
+ params['scale_index_height_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_height_type'] = 'scale_index_regular'
+
+ if node.get_attr('in_width') == node.get_attr('min_width'):
+ params['scale_index_width_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_width_type'] = 'scale_index_regular'
+
+ if node.model.config.get_config_value('IOType') == 'io_parallel':
+ namespace = params['namespace']
+ params['fill_fn'] = f'{namespace}::fill_buffer_{node.index}'
+ else:
+ params['fill_fn'] = 'nnet::FillConv2DBuffer'
+
+ params['min_height'] = node.get_attr('min_height', node.get_attr('in_height'))
+ params['min_width'] = node.get_attr('min_width', node.get_attr('in_width'))
+ params['instructions'] = node.get_attr('instructions', '0')
+
+ # Build explicit-construction initializer list for 2D pixels
+ instr_raw = node.get_attr('instructions', '0')
+ instr_list = [s.strip() for s in str(instr_raw).split(',') if s.strip() != '']
+ if len(instr_list) == 1 and instr_list[0] == '0':
+ count = int(params['min_height']) * int(params['min_width'])
+ instr_list = ['0'] * count
+ index_token = params['index']
+ params['instructions'] = ','.join(
+ f'ap_uint({v})' for v in instr_list
+ )
+
+ conv_config = self.template.format(**params)
+
+ mult_params = self._default_config_params(node)
+ mult_params['n_in'] = node.get_attr('n_chan') * node.get_attr('filt_height') * node.get_attr('filt_width')
+ mult_params['n_out'] = node.get_attr('n_filt')
+ mult_params['nzeros'] = node.get_weights('weight').nzeros
+ mult_params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('weight').type.precision
+ )
+
+ namespace = params['namespace']
+ if node.get_attr('strategy').lower() == 'latency':
+ if isinstance(node, DepthwiseConv2D):
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseLatency'
+ else:
+ mult_params['dense_function'] = 'nnet::DenseLatency'
+ elif node.get_attr('strategy').lower() == 'resource':
+ if isinstance(node, DepthwiseConv2D):
+ if int(mult_params['reuse_factor']) <= int(mult_params['n_out']):
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseResource_rf_leq_nout'
+ else:
+ if int(mult_params['reuse_factor']) % int(mult_params['n_out']) == 0:
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseResource_rf_gt_nout_rem0'
+ else:
+ mult_params['dense_function'] = 'nnet::DepthwiseDenseResource_rf_gt_nout'
+ else:
+ if int(mult_params['reuse_factor']) <= int(mult_params['n_in']):
+ mult_params['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
+ else:
+ if int(mult_params['reuse_factor']) % int(mult_params['n_in']) == 0:
+ mult_params['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
+ else:
+ mult_params['dense_function'] = 'nnet::DenseResource_rf_gt_nin'
+ elif node.get_attr('strategy').lower() == 'resource_unrolled':
+ mult_params['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}'
+ elif node.get_attr('strategy').lower() == 'distributed_arithmetic':
+ mult_params['dense_function'] = f'{namespace}::dense_da_wrapper_{node.index}'
+
+ mult_config = self.mult_template.format(**mult_params)
+
+ return mult_config + '\n' + conv_config
+
+ def match(self, node):
+ if node.get_attr('strategy') == 'distributed_arithmetic':
+ io_type = node.model.config.get_config_value('IOType')
+ if io_type == 'io_parallel':
+ # DA impl use alternate entry point for io_parallel conv
+ return False
+ return super().match(node)
+
+
+class Conv2DFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((Conv2D, Conv2DBatchnorm), include_header=conv2d_include_list)
+ self.template = conv2d_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'
+ params['w'] = node.get_weights('weight').name
+ params['b'] = node.get_weights('bias').name
+
+ return self.template.format(**params)
+
+ def match(self, node):
+ if node.get_attr('strategy') == 'distributed_arithmetic':
+ io_type = node.model.config.get_config_value('IOType')
+ if io_type == 'io_parallel':
+ # DA impl use alternate entry point for io_parallel conv
+ return False
+ return super().match(node)
+
+
+class DepthwiseConv2DFunctionTemplate(Conv2DFunctionTemplate):
+ def __init__(self):
+ super(Conv2DFunctionTemplate, self).__init__(DepthwiseConv2D, include_header=sepconv2d_include_list)
+ self.template = depthconv2d_function_template
+
+
+# SeparableConv1D/2D Templates
+
+sepconv_config_template = """struct config{index} {{
+ typedef {depthwise_config} depthwise_config;
+ typedef {pointwise_config} pointwise_config;
+}};\n"""
+
+sepconv1d_function_template = (
+ 'nnet::separable_conv_1d_{data_format}<{input_t}, {dw_output_t}, {output_t}, {config}>('
+ '{input}, {output}, {d}, {p}, {z}, {b});'
+)
+sepconv2d_function_template = (
+ 'nnet::separable_conv_2d_{data_format}<{input_t}, {dw_output_t}, {output_t}, {config}>('
+ '{input}, {output}, {d}, {p}, {z}, {b});'
+)
+
+sepconv1d_include_list = ['nnet_utils/nnet_conv1d.h', 'nnet_utils/nnet_sepconv1d.h', 'nnet_utils/nnet_sepconv1d_stream.h']
+sepconv2d_include_list = ['nnet_utils/nnet_conv2d.h', 'nnet_utils/nnet_sepconv2d.h', 'nnet_utils/nnet_sepconv2d_stream.h']
+
+
+class SeparableConv1DConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(SeparableConv1D)
+ self.template = sepconv_config_template
+ self.depthwise_template = conv1d_config_template
+ self.pointwise_template = conv1d_config_template
+ self.depthwise_mult_template = conv_mult_config_template
+ self.pointwise_mult_template = conv_mult_config_template
+
+ def format(self, node):
+ # Separable master config
+ params = {}
+ params['index'] = node.index
+ params['depthwise_config'] = f'config{node.index}_depthwise'
+ params['pointwise_config'] = f'config{node.index}_pointwise'
+ sep_config = self.template.format(**params)
+
+ # Depthwise config
+ params = self._default_config_params(node)
+ # Override bias and bias_t since these are zeros in depthwise step of SepConv1D
+ params['bias'] = params['zero_bias']
+ params['bias_t'] = params['zero_bias_t']
+ params['n_filt'] = params['n_chan'] * node.get_attr('depth_multiplier') # In depthwise step n_chan == n_filt
+ params['dilation'] = node.get_attr('dilation', 1)
+ params['nzeros'] = node.get_weights('depthwise').nzeros
+ params['index'] = str(node.index) + '_depthwise'
+ params['weight_t'] = node.get_weights('depthwise').type
+ params['bias_t'] = node.get_weights('zero_bias').type
+ if node.model.config.get_config_value('IOType') == 'io_parallel':
+ namespace = params['namespace']
+ params['fill_fn'] = f'{namespace}::fill_buffer_{node.index}_dw'
+ else:
+ params['fill_fn'] = 'nnet::FillConv1DBuffer'
+
+ if node.get_attr('unscaled'):
+ params['scale_index_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_type'] = 'scale_index_regular'
+
+ params['config_t'] = f'config{node.index}_depthwise_mult'
+ # TODO - Extend unrolled Dense Resource
+ params['unrolled_function'] = 'DenseResourceUnrolled'
+ depthwise_config = self.depthwise_template.format(**params)
+
+ # Depthwise mult config
+ mult_params = self._default_config_params(node)
+ mult_params['index'] = str(node.index) + '_depthwise'
+ mult_params['n_in'] = node.get_attr('n_chan') * node.get_attr('filt_width')
+ mult_params['n_out'] = node.get_attr('n_chan')
+ mult_params['nzeros'] = node.get_weights('depthwise').nzeros
+ mult_params['weight_t'] = node.get_weights('depthwise').type
+ mult_params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('depthwise').type.precision
+ )
+ # TODO - Extend unrolled Dense Resource to depthwise Conv1D
+ mult_params['unrolled_function'] = 'DenseResourceUnrolled'
+
+ depthwise_mult_config = self.depthwise_mult_template.format(**mult_params)
+
+ # Pointwise config
+ params = self._default_config_params(node)
+ if node.get_attr('data_format') == 'channels_last':
+ params['in_width'] = node.get_output_variable().shape[0]
+ else:
+ params['in_width'] = node.get_output_variable().shape[1]
+
+ params['filt_width'] = 1
+ params['stride_width'] = 1
+ params['pad_left'] = params['pad_right'] = 0
+ params['dilation'] = node.get_attr('dilation', 1)
+ params['nzeros'] = node.get_weights('pointwise').nzeros
+ params['index'] = str(node.index) + '_pointwise'
+ params['weight_t'] = node.get_weights('pointwise').type
+ params['min_width'] = params['in_width']
+ # explicit constructor for pointwise
+ index_token = params['index']
+ params['instructions'] = f'ap_uint(0)'
+ if node.model.config.get_config_value('IOType') == 'io_parallel':
+ namespace = params['namespace']
+ params['fill_fn'] = f'{namespace}::fill_buffer_{node.index}_pw'
+ else:
+ params['fill_fn'] = 'nnet::FillConv1DBuffer'
+
+ if node.get_attr('unscaled'):
+ params['scale_index_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_type'] = 'scale_index_regular'
+
+ params['config_t'] = f'config{node.index}_pointwise_mult'
+ # TODO - Extend unrolled Dense Resource
+ params['unrolled_function'] = 'DenseResourceUnrolled'
+ pointwise_config = self.pointwise_template.format(**params)
+
+ # Pointwise mult config
+ mult_params = self._default_config_params(node)
+ mult_params['index'] = str(node.index) + '_pointwise'
+ mult_params['n_in'] = node.get_attr('n_chan')
+ mult_params['n_out'] = node.get_attr('n_filt')
+ mult_params['nzeros'] = node.get_weights('pointwise').nzeros
+ mult_params['weight_t'] = node.get_weights('pointwise').type
+ mult_params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('pointwise').type.precision
+ )
+ # TODO - Extend unrolled Dense Resource to separable Conv1D
+ mult_params['unrolled_function'] = 'DenseResourceUnrolled'
+
+ pointwise_mult_config = self.pointwise_mult_template.format(**mult_params)
+
+ return (
+ depthwise_mult_config
+ + '\n'
+ + depthwise_config
+ + '\n'
+ + pointwise_mult_config
+ + '\n'
+ + pointwise_config
+ + '\n'
+ + sep_config
+ )
+
+
+class SeparableConv1DFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(SeparableConv1D, include_header=sepconv1d_include_list)
+ self.template = sepconv1d_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['dw_output_t'] = node.get_attr('dw_output_t').name
+ params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'
+ params['d'] = node.get_weights('depthwise').name
+ params['p'] = node.get_weights('pointwise').name
+ params['b'] = node.get_weights('bias').name
+ params['z'] = node.get_weights('zero_bias').name
+
+ return self.template.format(**params)
+
+
+class SeparableConv2DConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(SeparableConv2D)
+ self.template = sepconv_config_template
+ self.depthwise_template = conv2d_config_template
+ self.pointwise_template = conv2d_config_template
+ self.depthwise_mult_template = conv_mult_config_template
+ self.pointwise_mult_template = conv_mult_config_template
+
+ def format(self, node):
+ # Separable master config
+ params = {}
+ params['index'] = node.index
+ params['depthwise_config'] = f'config{node.index}_depthwise'
+ params['pointwise_config'] = f'config{node.index}_pointwise'
+ sep_config = self.template.format(**params)
+
+ # Depthwise config
+ params = self._default_config_params(node)
+ # Override bias and bias_t since these are zeros in depthwise step of SepConv2D
+ params['bias'] = params['zero_bias']
+ params['bias_t'] = params['zero_bias_t']
+ params['n_filt'] = params['n_chan'] # In depthwise step n_chan == n_filt
+ params['dilation'] = node.get_attr('dilation', 1)
+ params['nzeros'] = node.get_weights('depthwise').nzeros
+ params['index'] = str(node.index) + '_depthwise'
+ params['weight_t'] = node.get_weights('depthwise').type
+ if node.model.config.get_config_value('IOType') == 'io_parallel':
+ namespace = params['namespace']
+ params['fill_fn'] = f'{namespace}::fill_buffer_{node.index}_dw'
+ else:
+ params['fill_fn'] = 'nnet::FillConv2DBuffer'
+
+ if node.get_attr('unscaled_h'):
+ params['scale_index_height_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_height_type'] = 'scale_index_regular'
+
+ if node.get_attr('unscaled_w'):
+ params['scale_index_width_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_width_type'] = 'scale_index_regular'
+
+ params['config_t'] = f'config{node.index}_depthwise_mult'
+ # TODO - Extend unrolled Dense Resource
+ params['unrolled_function'] = 'DenseResourceUnrolled'
+ depthwise_config = self.depthwise_template.format(**params)
+
+ # Depthwise mult config
+ mult_params = self._default_config_params(node)
+ mult_params['index'] = str(node.index) + '_depthwise'
+ mult_params['n_in'] = node.get_attr('n_chan') * node.get_attr('filt_height') * node.get_attr('filt_width')
+ mult_params['n_out'] = node.get_attr('n_chan')
+ mult_params['nzeros'] = node.get_weights('depthwise').nzeros
+ mult_params['weight_t'] = node.get_weights('depthwise').type
+ mult_params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('depthwise').type.precision
+ )
+ # TODO - Extend unrolled Dense Resource to depthwise Conv2D
+ mult_params['unrolled_function'] = 'DenseResourceUnrolled'
+ depthwise_mult_config = self.depthwise_mult_template.format(**mult_params)
+
+ # Pointwise config
+ params = self._default_config_params(node)
+ if node.get_attr('data_format') == 'channels_last':
+ params['in_height'] = node.get_output_variable().shape[0]
+ params['in_width'] = node.get_output_variable().shape[1]
+ else:
+ params['in_height'] = node.get_output_variable().shape[1]
+ params['in_width'] = node.get_output_variable().shape[2]
+
+ params['filt_height'] = params['filt_width'] = 1
+ params['stride_height'] = params['stride_width'] = 1
+ params['pad_left'] = params['pad_right'] = 0
+ params['pad_top'] = params['pad_bottom'] = 0
+ params['dilation'] = node.get_attr('dilation', 1)
+ params['nzeros'] = node.get_weights('pointwise').nzeros
+ params['index'] = str(node.index) + '_pointwise'
+ params['weight_t'] = node.get_weights('pointwise').type
+ params['min_height'] = params['in_height']
+ params['min_width'] = params['in_width']
+ # explicit constructor for pointwise
+ index_token = params['index']
+ params['instructions'] = f'ap_uint(0)'
+ if node.model.config.get_config_value('IOType') == 'io_parallel':
+ namespace = params['namespace']
+ params['fill_fn'] = f'{namespace}::fill_buffer_{node.index}_pw'
+ else:
+ params['fill_fn'] = 'nnet::FillConv2DBuffer'
+
+ if node.get_attr('unscaled_h'):
+ params['scale_index_height_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_height_type'] = 'scale_index_regular'
+
+ if node.get_attr('unscaled_w'):
+ params['scale_index_width_type'] = 'scale_index_unscaled'
+ else:
+ params['scale_index_width_type'] = 'scale_index_regular'
+ params['config_t'] = f'config{node.index}_pointwise_mult'
+ # TODO - Extend unrolled Dense Resource
+ params['unrolled_function'] = 'DenseResourceUnrolled'
+ pointwise_config = self.pointwise_template.format(**params)
+
+ # Pointwise mult config
+ mult_params = self._default_config_params(node)
+ mult_params['index'] = str(node.index) + '_pointwise'
+ mult_params['n_in'] = node.get_attr('n_chan')
+ mult_params['n_out'] = node.get_attr('n_filt')
+ mult_params['nzeros'] = node.get_weights('pointwise').nzeros
+ mult_params['weight_t'] = node.get_weights('pointwise').type
+ mult_params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('pointwise').type.precision
+ )
+ # TODO - Extend unrolled Dense Resource to separable Conv2D
+ mult_params['unrolled_function'] = 'DenseResourceUnrolled'
+ pointwise_mult_config = self.pointwise_mult_template.format(**mult_params)
+
+ return (
+ depthwise_mult_config
+ + '\n'
+ + depthwise_config
+ + '\n'
+ + pointwise_mult_config
+ + '\n'
+ + pointwise_config
+ + '\n'
+ + sep_config
+ )
+
+
+class SeparableConv2DFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(SeparableConv2D, include_header=sepconv2d_include_list)
+ self.template = sepconv2d_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['dw_output_t'] = node.get_attr('dw_output_t').name
+ params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'
+ params['d'] = node.get_weights('depthwise').name
+ params['p'] = node.get_weights('pointwise').name
+ params['b'] = node.get_weights('bias').name
+ params['z'] = node.get_weights('zero_bias').name
+
+ return self.template.format(**params)
diff --git a/hls4ml/backends/bambu/passes/core_templates.py b/hls4ml/backends/bambu/passes/core_templates.py
new file mode 100644
index 0000000000..51b0418530
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/core_templates.py
@@ -0,0 +1,398 @@
+from math import ceil, log2
+
+from hls4ml.backends.backend import get_backend
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import (
+ Activation,
+ BatchNormalization,
+ Dense,
+ HardActivation,
+ LayerNormalization,
+ ParametrizedActivation,
+ PReLU,
+ Softmax,
+)
+from hls4ml.model.optimizer.passes.hgq_proxy_model import UnaryLUT
+
+# Dense templates
+
+dense_config_template = """struct config{index} : nnet::dense_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned strategy = nnet::{strategy};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned n_zeros = {nzeros};
+ static const unsigned n_nonzeros = {nonzeros};
+ static const unsigned multiplier_limit = DIV_ROUNDUP(n_in * n_out, reuse_factor) - n_zeros / reuse_factor;
+ static const bool store_weights_in_bram = false;
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+ typedef {weight_t.name} weight_t;
+ typedef {index_t.name} index_t;
+ // Bind weights/biases compile-time on the config so the streaming
+ // `nnet::dense` can reach them without a runtime array-pointer
+ // parameter — Bambu DATAFLOW pointer params read as all-zero at
+ // runtime regardless of ROM contents.
+ static constexpr const weight_t *weights = {w};
+ static constexpr const bias_t *biases = {b};
+ template
+ using kernel = {dense_function};
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+dense_function_template = 'nnet::dense<{input_t}, {output_t}, {config}>({input}, {output});'
+
+dense_include_list = ['nnet_utils/nnet_dense.h', 'nnet_utils/nnet_dense_compressed.h', 'nnet_utils/nnet_dense_stream.h']
+
+
+class DenseConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Dense)
+ self.template = dense_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['nzeros'] = node.get_weights('weight').nzeros
+ params['nonzeros'] = node.get_weights('weight').nonzeros
+ params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('weight').type.precision
+ )
+ params['w'] = node.get_weights('weight').name
+ params['b'] = node.get_weights('bias').name
+
+ namespace = params['namespace']
+
+ if node.get_attr('strategy').lower() == 'latency':
+ params['dense_function'] = 'nnet::DenseLatency'
+ elif node.get_attr('strategy').lower() == 'resource':
+ if int(params['reuse_factor']) <= int(params['n_in']):
+ params['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
+ else:
+ params['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
+ # The 3rd case is never used
+ elif node.get_attr('strategy').lower() == 'resource_unrolled':
+ params['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}'
+ elif node.get_attr('strategy').lower() == 'distributed_arithmetic':
+ # Only triggered in io_streaming mode
+ params['dense_function'] = f'{namespace}::dense_da_wrapper_{node.index}'
+
+ return self.template.format(**params)
+
+ def match(self, node):
+ if node.get_attr('strategy') == 'distributed_arithmetic':
+ return False # DA does not use common dense template
+ return super().match(node)
+
+
+class DenseFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(Dense, include_header=dense_include_list)
+ self.template = dense_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ return self.template.format(**params)
+
+ def match(self, node):
+ if node.get_attr('strategy') == 'distributed_arithmetic':
+ return False # DA does not use common dense template
+ return super().match(node)
+
+
+# BatchNormalization templates
+
+batchnorm_config_template = """struct config{index} : nnet::batchnorm_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_filt = {n_filt};
+ static const unsigned n_scale_bias = (n_filt == -1) ? n_in : n_filt;
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned multiplier_limit = DIV_ROUNDUP(n_in, reuse_factor);
+ static const bool store_weights_in_bram = false;
+ typedef {bias_t.name} bias_t;
+ typedef {scale_t.name} scale_t;
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+batchnorm_function_template = 'nnet::normalize<{input_t}, {output_t}, {config}>({input}, {output}, {scale}, {bias});'
+
+batchnorm_include_list = ['nnet_utils/nnet_batchnorm.h', 'nnet_utils/nnet_batchnorm_stream.h']
+
+
+class BatchNormalizationConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(BatchNormalization)
+ self.template = batchnorm_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['n_in'] = node.get_input_variable().size_cpp()
+ params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('scale').type.precision
+ )
+
+ return self.template.format(**params)
+
+
+class BatchNormalizationFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(BatchNormalization, include_header=batchnorm_include_list)
+ self.template = batchnorm_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['scale'] = node.get_weights('scale').name
+ params['bias'] = node.get_weights('bias').name
+
+ return self.template.format(**params)
+
+
+# LayerNormalization templates
+
+layernorm_config_template = """struct config{index} : nnet::layernorm_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned seq_len = {seq_len};
+ static const unsigned axis = {axis};
+ static const unsigned epsilon_power_of_10 = {epsilon_power_of_10};
+ static const unsigned table_range_power2 = {table_range_power2};
+ static const unsigned table_size = {table_size};
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+ typedef {scale_t.name} scale_t;
+ typedef {table_t.name} table_t;
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+layernorm_function_template = 'nnet::layernormalize<{input_t}, {output_t}, {config}>({input}, {output}, {scale}, {bias});'
+
+layernorm_include_list = ['nnet_utils/nnet_layernorm.h']
+
+
+class LayerNormalizationConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(LayerNormalization)
+ self.template = layernorm_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['n_in'] = node.get_input_variable().size_cpp()
+ params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('scale').type.precision
+ )
+
+ return self.template.format(**params)
+
+
+class LayerNormalizationFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(LayerNormalization, include_header=layernorm_include_list)
+ self.template = layernorm_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['scale'] = node.get_weights('scale').name
+ params['bias'] = node.get_weights('bias').name
+
+ return self.template.format(**params)
+
+
+# Activation templates
+
+activ_config_template = """struct {type}_config{index} : nnet::activ_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned table_size = {table_size};
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+ typedef {table_t.name} table_t;
+}};\n"""
+
+param_activ_config_template = """struct {type}_config{index} : nnet::activ_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned table_size = {table_size};
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+ typedef {table_t.name} table_t;
+ typedef {param_t.name} param_t;
+}};\n"""
+
+hard_activ_config_template = """struct {type}_config{index} {{
+ static const unsigned n_in = {n_in};
+ static const {slope_t.name} slope;
+ static const {shift_t.name} shift;
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+}};
+const {slope_t.name} {type}_config{index}::slope = {slope};
+const {shift_t.name} {type}_config{index}::shift = {shift};\n"""
+
+softmax_config_template = """struct {type}_config{index} : nnet::activ_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_slice = {n_slice};
+ static const unsigned n_outer = {n_outer};
+ static const unsigned n_inner = {n_inner};
+ static const unsigned parallelization_factor = {parallelization_factor};
+ static const unsigned exp_table_size = {exp_table_size};
+ static const unsigned inv_table_size = {inv_table_size};
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned axis = {axis};
+ static const nnet::softmax_implementation implementation = nnet::softmax_implementation::{implementation};
+ static constexpr float exp_scale = {exp_scale};
+ typedef {exp_table_t.name} exp_table_t;
+ typedef {inv_table_t.name} inv_table_t;
+ typedef {accum_t.name} accum_t;
+ typedef {inv_inp_t.name} inv_inp_t;
+ typedef {inp_norm_t_str} inp_norm_t;
+}};\n"""
+
+activ_function_template = 'nnet::{activation}<{input_t}, {output_t}, {config}>({input}, {output});'
+param_activ_function_template = (
+ 'nnet::{activation}<{input_t}, {param_t.name}, {output_t}, {config}>({input}, {param}, {output});'
+)
+
+activ_include_list = ['nnet_utils/nnet_activation.h', 'nnet_utils/nnet_activation_stream.h']
+
+
+class ActivationConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((Activation, UnaryLUT))
+ self.template = activ_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['type'] = node.get_attr('activation')
+
+ return self.template.format(**params)
+
+
+class ParamActivationConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((ParametrizedActivation, PReLU))
+ self.template = param_activ_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['type'] = node.get_attr('activation')
+
+ return self.template.format(**params)
+
+
+class HardActivationConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(HardActivation)
+ self.template = hard_activ_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['type'] = node.get_attr('activation')
+
+ return self.template.format(**params)
+
+
+class SoftmaxConfigTemplate(ActivationConfigTemplate):
+ def __init__(self):
+ super(ActivationConfigTemplate, self).__init__(Softmax) # Skip ActivationConfigTemplate's __init__
+ self.template = softmax_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['type'] = node.get_attr('activation')
+ params.setdefault('exp_table_size', params['table_size'])
+ params.setdefault('inv_table_size', params['table_size'])
+ params.setdefault('n_inner', 1)
+ params.setdefault('n_outer', 1)
+ params.setdefault('exp_scale', 1.0)
+ params.setdefault('parallelization_factor', -1)
+
+ n_slice = params['n_in'] // params['n_inner'] // params['n_outer'] # type: ignore
+ params['n_slice'] = n_slice
+
+ if params['accum_t'].name == 'model_default_t': # type: ignore
+ scale = ceil(log2(n_slice))
+ exp_table_t = node.attributes['exp_table_t'].precision
+ signed, width, integers = exp_table_t.signed, exp_table_t.width, exp_table_t.integer
+ params['accum_t_str'] = f'ap_{"" if signed else "u"}fixed<{width + scale}, {integers + scale}>'
+ else:
+ params['accum_t_str'] = params['accum_t'].name # type: ignore
+ if params['inv_inp_t'].name == 'model_default_t': # type: ignore
+ params['inv_inp_t'] = params['exp_table_t']
+
+ if params['implementation'] == 'stable':
+ if 'inp_norm_t' not in params:
+ # Only used in stable (max-normalized) implementation
+ input_t = node.get_input_variable().type.precision
+ width, iwidth, signed = input_t.width, input_t.integer, input_t.signed # noqa: F841
+ width, iwidth = width - signed, iwidth - signed
+ if signed:
+ # Fix table size if too large
+ exp_table_size = params['inv_table_size']
+ params['exp_table_size'] = str(min(int(exp_table_size), 2**width))
+ params['inp_norm_t_str'] = f'ap_ufixed<{width}, {iwidth}>'
+ else:
+ params['inp_norm_t_str'] = params['inp_norm_t'].name # type: ignore
+ else:
+ params['inp_norm_t_str'] = 'ap_fixed<1,0>'
+
+ return self.template.format(**params)
+
+
+class SoftmaxFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(Softmax, include_header=activ_include_list)
+ self.template = activ_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ use_multidim = node.get_attr('n_inner', 1) > 1 or node.get_attr('n_outer', 1) > 1
+ use_multidim = use_multidim and node.model.config.get_config_value('IOType') == 'io_parallel'
+ params['activation'] = 'softmax' if not use_multidim else 'softmax_multidim'
+ params['config'] = f'softmax_config{node.index}'
+
+ return self.template.format(**params)
+
+
+class ActivationFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((Activation, HardActivation), include_header=activ_include_list)
+ self.template = activ_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['activation'] = node.get_attr('activation').lower()
+ params['config'] = '{}_config{}'.format(node.get_attr('activation'), node.index)
+
+ return self.template.format(**params)
+
+
+class ParametrizedActivationFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(ParametrizedActivation, include_header=activ_include_list)
+ self.template = param_activ_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['activation'] = node._get_act_function_name()
+ params['param'] = node.get_attr('activ_param', 1.0)
+ params['config'] = '{}_config{}'.format(node.get_attr('activation'), node.index)
+
+ return self.template.format(**params)
+
+
+class PReLUFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(PReLU, include_header=activ_include_list)
+ self.template = param_activ_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['activation'] = node.get_attr('activation').lower()
+ params['param'] = node.get_weights('param').name
+ params['config'] = '{}_config{}'.format(node.get_attr('activation'), node.index)
+
+ return self.template.format(**params)
diff --git a/hls4ml/backends/bambu/passes/distributed_arithmetic.py b/hls4ml/backends/bambu/passes/distributed_arithmetic.py
new file mode 100644
index 0000000000..89d27a13f7
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/distributed_arithmetic.py
@@ -0,0 +1,410 @@
+import os
+import typing
+from functools import singledispatch
+from math import prod
+
+import numpy as np
+
+from hls4ml.model.layers import Conv1D, Conv2D, Dense, EinsumDense, Layer
+from hls4ml.model.optimizer import OptimizerPass
+from hls4ml.model.optimizer.passes.bit_exact import get_input_layers, get_output_layers, im2col, pad_arrs, stride_arrs
+from hls4ml.model.optimizer.passes.hgq_proxy_model import FixedPointQuantizer
+from hls4ml.model.types import FixedPrecisionType, Source
+from hls4ml.utils.dependency import requires
+
+if typing.TYPE_CHECKING:
+ from hls4ml.model import ModelGraph
+
+
+def add_kernel_wrapper(index: int, n_in: int, n_out: int):
+ wrapper = f"""template struct dense_da_wrapper_{index} {{
+static void dense(inp_t inp[{n_in}], out_t out[{n_out}], void *weights=nullptr, void *biases=nullptr) {{
+ dense_da_{index}(inp, out);
+}}
+}};"""
+ return wrapper
+
+
+def _get_input_kif(node: Layer):
+ """Get the input k, i, f to a layer.
+ Use the results from the last FixedPointQuantzer if available, fallback to the result_t of the input variable.
+ """
+ result_t = node.get_input_variable().type.precision
+ inp_shape = node.get_input_variable().shape
+ if not isinstance(result_t, FixedPrecisionType):
+ raise ValueError(f'Input to layer {node.name} is not a fixed point type - DA optimization not supported.')
+ inp_layer = get_input_layers(node)[0]
+ if isinstance(inp_layer, FixedPointQuantizer):
+ Ks, _Bs, _Is = inp_layer.mask_kbi
+ Is, Fs = _Is - Ks, _Bs - _Is
+ Ks, Is, Fs = Ks[0], Is[0], Fs[0] # remove batch dimension
+ else:
+ Ks = np.ones(inp_shape, dtype=np.int16)
+ Is = Fs = np.full(inp_shape, 126, dtype=np.int16)
+
+ _k, _B, _I = result_t.signed, result_t.width, result_t.integer
+ _k, _i, _f = _k, _I - _k, _B - _I
+ k, i, f = np.minimum(Ks, _k), np.minimum(Is, _i), np.minimum(Fs, _f)
+ return k, i, f
+
+
+@singledispatch
+def get_kernel_inp_kif(node: Layer) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """Get the input k, i, f to a kernel. Supports Dense, Conv1/2D, and EinsumDense layers."""
+ raise NotImplementedError(f'Layer {node.name} of type {node.__class__.__name__} is not supported by DA optimizer.')
+
+
+@get_kernel_inp_kif.register
+def _(node: Dense):
+ k, i, f = _get_input_kif(node)
+ n_ch = k.shape[-1]
+ return k.reshape(-1, n_ch).max(axis=0), i.reshape(-1, n_ch).max(axis=0), f.reshape(-1, n_ch).max(axis=0)
+
+
+@get_kernel_inp_kif.register(Conv1D)
+@get_kernel_inp_kif.register(Conv2D)
+def _(layer: Conv1D | Conv2D):
+ assert layer.attributes['data_format'] == 'channels_last', 'Only channels_last format is supported'
+ kernel = layer.attributes['weight'].data
+ k_in, i_in, f_in = _get_input_kif(layer)
+ k_in, i_in, f_in = pad_arrs(layer, 0, k_in, i_in, f_in)
+ k_in, i_in, f_in = im2col(kernel.shape, k_in, i_in, f_in)
+ k_in, i_in, f_in = stride_arrs(layer, k_in, i_in, f_in)
+ n_ker_in: int = k_in.shape[-1]
+ return (
+ k_in.reshape(-1, n_ker_in).max(axis=0),
+ i_in.reshape(-1, n_ker_in).max(axis=0),
+ f_in.reshape(-1, n_ker_in).max(axis=0),
+ )
+
+
+@get_kernel_inp_kif.register
+def _(node: EinsumDense):
+ inp_tpose_idx = node.attributes['inp_tpose_idxs']
+ L = node.attributes['n_free_data']
+ I = node.attributes['n_inplace'] # noqa: E741
+ C = node.attributes['n_contract']
+
+ k, i, f = _get_input_kif(node)
+ k, i, f = k.transpose(inp_tpose_idx), i.transpose(inp_tpose_idx), f.transpose(inp_tpose_idx)
+ k, i, f = k.reshape(I, L, C), i.reshape(I, L, C), f.reshape(I, L, C)
+ return k.max(axis=1), i.max(axis=1), f.max(axis=1)
+
+
+class DistributedArithmeticCodegen(OptimizerPass):
+ """Generates C++ code for distributed arithmetic implementation of Dense and Conv1/2D layers"""
+
+ def match(self, node):
+ if not node.get_attr('strategy', None) == 'distributed_arithmetic':
+ return False
+ if 'da_codegen' in node.attributes:
+ return False
+ supported = (Dense, Conv1D, Conv2D)
+ # EinsumDense support is standalone as it requires additional configuration
+ # RNN and depthwise conv families are not supported for now
+ if not isinstance(node, supported):
+ if isinstance(node, EinsumDense):
+ return False
+ raise Exception(f'Layer {node.name} of type {node.__class__.__name__} is not supported by DA optimizer.')
+
+ rf = node.get_attr('reuse_factor', 1)
+ if rf != 1:
+ raise Exception(f'Layer {node.name} has rf = {rf} != 1, but has strategy = DA.')
+
+ return True
+
+ @requires('da')
+ def transform(self, model: 'ModelGraph', node: Layer):
+ from da4ml.codegen.hls import hls_logic_and_bridge_gen
+ from da4ml.trace import FixedVariableArray, HWConfig, comb_trace
+
+ kernel: np.ndarray = node.attributes['weight'].data
+ kernel = kernel.reshape(-1, kernel.shape[-1])
+ n_in, n_out = kernel.shape
+ fn_name = f'dense_da_{node.index}'
+
+ k, i, f = get_kernel_inp_kif(node)
+ hard_dc = int(os.environ.get('DA_HARD_DC', 2))
+ options = {'hard_dc': hard_dc, 'search_all_decompose_dc': True}
+ inp = FixedVariableArray.from_kif(k, i, f, HWConfig(1, -1, -1), solver_options=options)
+ out = inp @ kernel
+ if node.attributes['bias'] is not None:
+ bias = node.attributes['bias'].data.ravel()
+ assert len(bias) == n_out
+ out += bias
+ sol = comb_trace(inp, out)
+ node.attributes['da_kernel_cost'] = sol.cost
+
+ backend = model.config.get_config_value('Backend').lower()
+ assert backend in ('vitis', 'vivado', 'bambu')
+ flavor = 'vitis'
+
+ pragmas = ['#pragma HLS INLINE'] if flavor == 'vitis' else None
+
+ fn_str, _ = hls_logic_and_bridge_gen(sol, fn_name, flavor, pragmas=pragmas, print_latency=True)
+
+ io_type = node.model.config.get_config_value('IOType')
+ if io_type != 'io_parallel':
+ fn_str += '\n\n' + add_kernel_wrapper(node.index, n_in, n_out)
+
+ node.set_attr('da_codegen', Source(fn_str))
+
+
+class FuseQuantizerIntoDALayers(OptimizerPass):
+ """Heterogeneous quantizer can be fused into the DA CMVM kernel in some cases.
+ This would allow heterogeenous quantizarion for io stream in some cases."""
+
+ def match(self, node: Layer):
+ if not isinstance(node, FixedPointQuantizer):
+ return False
+ next_layers = get_output_layers(node)
+ if not next_layers: # Output quantizer
+ return False
+ allow = (Dense,)
+ if all(n == 1 for n in node.mask_kbi[0].shape[:-1]):
+ allow += (Conv1D, Conv2D)
+ for next_layer in next_layers:
+ if next_layer.get_attr('strategy', None) != 'distributed_arithmetic':
+ return False
+ if not isinstance(next_layer, allow):
+ return False
+ return len(next_layers) == 1 or (node.RND == 'RND' and node.SAT == 'WRAP') # avoid resource overhead
+
+ def transform(self, model: 'ModelGraph', node: FixedPointQuantizer):
+ for out_layer in get_output_layers(node):
+ k, i, f = get_kernel_inp_kif(out_layer)
+ B, I = i + f + k, i + k # noqa: E741
+
+ quantization_lines, replaces = [], []
+ for i, (_k, _B, _I) in enumerate(zip(k, B, I)):
+ u = '' if _k else 'u'
+ _src = f'model_inp[{i}]'
+ _dst = f'model_inp_q_{i}'
+ if _B > 0:
+ var_def = f'ap_{u}fixed<{_B}, {_I}, AP_{node.RND}, AP_{node.SAT}> {_dst} = {_src};'
+ else:
+ var_def = f'ap_ufixed<1, 0> {_dst} = 0;'
+ quantization_lines.append(var_def)
+ replaces.append((f'{_src};', f'{_dst};'))
+
+ replaces.append(('#pragma HLS INLINE', '#pragma HLS INLINE\n ' + '\n '.join(quantization_lines)))
+
+ da_source: Source = out_layer.attributes['da_codegen']
+ code: str = da_source.code
+ for src, dst in replaces:
+ code = code.replace(src, dst)
+ out_layer.attributes['da_codegen'] = Source(code)
+ model.remove_node(node)
+ return True
+
+
+dense_da_stream_template = """struct config{index} {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned io_type = nnet::io_stream;
+ static const unsigned strategy = nnet::distributed_arithmetic;
+ constexpr static auto dense_da = nnet::dense_da_{index};
+}};\n"""
+
+
+class DALatencyDenseTemplate(OptimizerPass):
+ # For Dense, distributed arithmetic do not call the original impl, regardless of the io_type
+ # For io_stream, a minimal config will still be generated
+ def match(self, node: Layer):
+ if node.class_name != 'Dense':
+ return False
+ if 'function_cpp' in node.attributes:
+ return False
+ return node.get_attr('strategy', None) == 'distributed_arithmetic'
+
+ def transform(self, model: 'ModelGraph', node: Layer):
+ inp_t: str = node.get_input_variable().type.name
+ out_t: str = node.get_output_variable().type.name
+ inp_name: str = node.get_input_variable().name
+ out_name: str = node.get_output_variable().name
+
+ # override function_cpp
+ io_type = node.model.config.get_config_value('IOType')
+ namespace = node.model.config.get_writer_config().get('Namespace', None) or 'nnet'
+ if io_type == 'io_parallel':
+ fn_name = f'dense_da_{node.index}<{inp_t}, {out_t}>'
+ function_cpp = f'{namespace}::{fn_name}({inp_name}, {out_name});'
+ node.attributes['function_cpp'] = function_cpp
+ else:
+ assert io_type == 'io_stream'
+ config_cpp = dense_da_stream_template.format(inp_t=inp_t, out_t=out_t, **node.attributes)
+ function_cpp = f'nnet::dense<{inp_t}, {out_t}, config{node.index}>({inp_name}, {out_name});'
+ node.attributes['config_cpp'] = config_cpp
+ node.attributes['function_cpp'] = function_cpp
+ node.attributes['include_header'] = ['nnet_utils/nnet_da_wrappers.h']
+
+ # avoid output weights and bias; alternatie entry point does not use them
+ del node.attributes['weight_data']
+ del node.attributes['bias_data']
+ del node.attributes['weight']
+ del node.attributes['weight_t']
+ del node.attributes['bias']
+ del node.attributes['bias_t']
+
+
+conv_da_parallel_template = """struct config{index} {{
+ static const unsigned in_height = {in_height};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned out_height = {out_height};
+ static const unsigned out_width = {out_width};
+ static const unsigned n_filt = {n_filt};
+ static const unsigned filt_height = {filt_height};
+ static const unsigned filt_width = {filt_width};
+ static const unsigned stride_height = {stride_height};
+ static const unsigned stride_width = {stride_width};
+
+ static const unsigned strategy = nnet::distributed_arithmetic;
+ static const unsigned n_partitions = {n_partitions};
+ static const unsigned n_pixels = {n_pixels};
+ constexpr static auto dense_da = nnet::dense_da_{index}<{inp_t}, {out_t}>;
+ template
+ using fill_buffer = nnet::fill_buffer_{index};
+}};\n"""
+
+
+class DALatencyConvTemplate(OptimizerPass):
+ def match(self, node: Layer):
+ if not node.get_attr('strategy', None) == 'distributed_arithmetic':
+ return False
+ if 'function_cpp' in node.attributes:
+ return False
+ if node.class_name not in (
+ 'Conv1D',
+ 'Conv2D',
+ 'PointwiseConv1D',
+ 'PointwiseConv2D',
+ ):
+ return False
+ if node.get_attr('implementation') != 'linebuffer':
+ return False
+ io_type = node.model.config.get_config_value('IOType')
+ return io_type == 'io_parallel'
+
+ def transform(self, model: 'ModelGraph', node: Layer):
+ fmt = node.get_attr('data_format')
+ assert fmt == 'channels_last', (
+ f'At layer {node.name}, data_format must be "channels_last" for DA optimization. Got {fmt}.'
+ )
+ inp_t: str = node.get_input_variable().type.name
+ out_t: str = node.get_output_variable().type.name
+ inp_name: str = node.get_input_variable().name
+ out_name: str = node.get_output_variable().name
+
+ ker_shape = node.attributes['weight'].data.shape
+
+ # function call generation
+ class_name = node.class_name
+ if class_name.startswith('Pointwise'):
+ class_name = class_name[9:]
+
+ ndim = len(ker_shape) - 2
+ function_cpp = f'nnet::conv{ndim}d_cl({inp_name}, {out_name});'
+ node.attributes['function_cpp'] = function_cpp
+
+ # config generation
+ params = node.attributes.attributes.copy()
+ n_pixels = prod(node.get_output_variable().shape[:-1]) // node.attributes['n_partitions']
+
+ # conv 1d case, set dummy values for heights
+ params.setdefault('in_height', -1)
+ params.setdefault('out_height', -1)
+ params.setdefault('filt_height', -1)
+ params.setdefault('stride_height', -1 if ndim == 1 else 1)
+
+ config_cpp = conv_da_parallel_template.format(inp_t=inp_t, out_t=out_t, n_pixels=n_pixels, **params)
+ node.attributes['config_cpp'] = config_cpp
+
+ # Only unrolled header is required for io_parallel
+ include_headers = [
+ 'nnet_utils/nnet_da_wrappers.h',
+ f'nnet_utils/nnet_{class_name.lower()}.h',
+ 'nnet_utils/nnet_conv_stream.h', # some properties defined in config need this
+ ]
+ node.attributes['include_header'] = include_headers
+
+ # avoid output weights and bias; alternatie entry point does not use them
+ del node.attributes['weight_data']
+ del node.attributes['bias_data']
+ del node.attributes['weight']
+ del node.attributes['bias']
+ del node.attributes['weight_t']
+ del node.attributes['bias_t']
+
+
+kernel_fn_template = """
+template
+void einsum_dense{index}_da_kernel(
+ inp_t inp_tpose[{inp_tpose}],
+ out_t out_tpose[{out_tpose}],
+ int l0
+) {{
+ {fn_call_str}
+}}
+"""
+
+
+class DistributedArithmeticEinsumCodegen(OptimizerPass):
+ """Generates C++ code for distributed arithmetic implementation of Dense layers"""
+
+ def match(self, node):
+ if not node.get_attr('strategy', None) == 'distributed_arithmetic':
+ return False
+ if 'da_codegen' in node.attributes:
+ return False
+ return isinstance(node, EinsumDense)
+
+ @requires('da')
+ def transform(self, model: 'ModelGraph', node: Layer):
+ from da4ml.codegen.hls import hls_logic_and_bridge_gen
+ from da4ml.trace import FixedVariableArray, HWConfig, comb_trace
+
+ kernel: np.ndarray = node.attributes['weight'].data
+ I, C, L_ker = kernel.shape
+ L_data = node.attributes['n_free_data']
+
+ inp_kifs = get_kernel_inp_kif(node)
+ fn_strs = []
+ fn_calls = []
+
+ backend = model.config.get_config_value('Backend').lower()
+ assert backend in ('vitis', 'vivado', 'bambu')
+ flavor = 'vitis'
+
+ node.attributes['da_kernel_cost'] = 0.0
+
+ for i in range(I):
+ _k, _i, _f = (v[i] for v in inp_kifs)
+ fn_name = f'einsum_{node.index}_da_{i}_of_{I}'
+ hard_dc = int(os.environ.get('DA_HARD_DC', 2))
+ options = {'hard_dc': hard_dc, 'search_all_decompose_dc': True}
+ inp = FixedVariableArray.from_kif(_k, _i, _f, HWConfig(1, -1, -1), solver_options=options)
+ out = inp @ kernel[i]
+ sol = comb_trace(inp, out)
+
+ node.attributes['da_kernel_cost'] += sol.cost
+
+ pragmas = ['#pragma HLS INLINE'] if flavor == 'vitis' else None
+ fn_str, _ = hls_logic_and_bridge_gen(sol, fn_name, flavor, pragmas=pragmas, print_latency=True)
+
+ fn_strs.append(fn_str)
+ fn_call = f'{fn_name}(&inp_tpose[({i} * {L_data} + l0) * {C}], &out_tpose[({i} * {L_data} + l0) * {L_ker}]);'
+ fn_calls.append(fn_call)
+
+ kernel_fn = kernel_fn_template.format(
+ index=node.index,
+ inp_tpose=L_data * C * I,
+ out_tpose=L_data * L_ker * I,
+ fn_call_str=' \n'.join(fn_calls),
+ )
+
+ code_gen = '\n\n'.join(fn_strs) + '\n\n' + kernel_fn
+ node.attributes['da_codegen'] = Source(code_gen)
+ del node.attributes['weight_data']
+ del node.attributes['weight']
+ del node.attributes['weight_t']
diff --git a/hls4ml/backends/bambu/passes/einsum.py b/hls4ml/backends/bambu/passes/einsum.py
new file mode 100644
index 0000000000..be48b91b81
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/einsum.py
@@ -0,0 +1,109 @@
+from math import ceil
+
+from hls4ml.backends.backend import get_backend
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import Einsum
+from hls4ml.utils.transpose_utils import transpose_config_gen
+
+from .reshaping_templates import transpose_config_template
+
+# Shared Dense template
+# Einsum template
+
+einsum_config_template = """
+struct config{index} {{
+ typedef config{index}_tpose_inp0 tpose_inp0_config;
+ typedef config{index}_tpose_inp1 tpose_inp1_config;
+ typedef config{index}_tpose_out tpose_out_conf;
+
+ typedef {accum_t.name} accum_t;
+
+ // Layer Sizes
+ static const unsigned n_free0 = {n_free0};
+ static const unsigned n_free1 = {n_free1};
+ static const unsigned n_contract = {n_contract};
+ static const unsigned n_inplace = {n_inplace};
+
+ // Resource reuse info
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned strategy = nnet::{strategy};
+ static const unsigned reuse_factor = {reuse_factor};
+ static const unsigned multiplier_limit = {multiplier_limit};
+ static const bool store_weights_in_bram = false; // NOT USED
+
+ template
+ using product = nnet::product::{product_type};
+}};
+"""
+
+einsum_function_template = 'nnet::einsum<{input0_t}, {input1_t}, {output_t}, {config}>({input0}, {input1}, {output});'
+
+einsum_include_list = ['nnet_utils/nnet_einsum.h']
+
+
+class EinsumConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Einsum)
+ self.template = einsum_config_template
+
+ def format(self, node: Einsum):
+ default_params = self._default_config_params(node)
+
+ strategy = node.attributes['strategy']
+ io_type = node.model.config.get_config_value('IOType')
+
+ assert io_type == 'io_parallel', 'EinsumDense layer only supports io_parallel for now'
+ assert strategy.lower() == 'latency', 'EinsumDense layer only supports Latency strategy for now'
+
+ # EinsumDense config
+ params = default_params.copy()
+ params['strategy'] = strategy
+ params['n_free0'] = node.attributes['n_free0']
+ params['n_free1'] = node.attributes['n_free1']
+ params['n_contract'] = node.attributes['n_contract']
+ params['n_inplace'] = node.attributes['n_inplace']
+ inp0_t = node.get_input_variable(node.inputs[0]).type.precision
+ inp1_t = node.get_input_variable(node.inputs[1]).type.precision
+ params['product_type'] = get_backend('bambu').product_type(inp0_t, inp1_t)
+
+ total_mults = params['n_free0'] * params['n_free1'] * params['n_contract'] * params['n_inplace']
+ params['multiplier_limit'] = ceil(total_mults / params['reuse_factor'])
+
+ einsum_conf = self.template.format(**params)
+
+ # inp/out transpose config
+ inp0_shape = node.attributes['inp0_shape']
+ inp1_shape = node.attributes['inp1_shape']
+ out_interpert_shape = node.attributes['out_interpert_shape']
+ inp0_tpose_idxs = node.attributes['inp0_tpose_idxs']
+ inp1_tpose_idxs = node.attributes['inp1_tpose_idxs']
+ out_tpose_idxs = node.attributes['out_tpose_idxs']
+ tpose_inp0_config_name = f'config{node.index}_tpose_inp0'
+ tpose_inp1_config_name = f'config{node.index}_tpose_inp1'
+ tpose_out_conf_name = f'config{node.index}_tpose_out'
+
+ conf = transpose_config_gen(tpose_inp0_config_name, inp0_shape, inp0_tpose_idxs)
+ inp0_tpose_conf = transpose_config_template.format(**conf)
+ conf = transpose_config_gen(tpose_inp1_config_name, inp1_shape, inp1_tpose_idxs)
+ inp1_tpose_conf = transpose_config_template.format(**conf)
+ conf = transpose_config_gen(tpose_out_conf_name, out_interpert_shape, out_tpose_idxs)
+ out_tpose_conf = transpose_config_template.format(**conf)
+
+ return '\n\n'.join((inp0_tpose_conf, inp1_tpose_conf, out_tpose_conf, einsum_conf))
+
+
+class EinsumFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(Einsum, include_header=einsum_include_list)
+ self.template = einsum_function_template
+
+ def format(self, node: Einsum):
+ params = {}
+ params['config'] = f'config{node.index}'
+ params['input0_t'] = node.get_input_variable(node.inputs[0]).type.name
+ params['input1_t'] = node.get_input_variable(node.inputs[1]).type.name
+ params['output_t'] = node.get_output_variable().type.name
+ params['input0'] = node.get_input_variable(node.inputs[0]).name
+ params['input1'] = node.get_input_variable(node.inputs[1]).name
+ params['output'] = node.get_output_variable().name
+ return self.template.format(**params)
diff --git a/hls4ml/backends/bambu/passes/einsum_dense.py b/hls4ml/backends/bambu/passes/einsum_dense.py
new file mode 100644
index 0000000000..9c81d73193
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/einsum_dense.py
@@ -0,0 +1,148 @@
+from hls4ml.backends.backend import get_backend
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import EinsumDense
+from hls4ml.utils.transpose_utils import transpose_config_gen
+
+from .reshaping_templates import transpose_config_template
+
+# Shared Dense template
+
+dense_config_template = """struct config{index}_dense : nnet::dense_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned strategy = nnet::{strategy};
+ static const unsigned n_zeros = {nzeros};
+ static const unsigned multiplier_limit = DIV_ROUNDUP(n_in * n_out, reuse_factor) - n_zeros / reuse_factor;
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+ typedef {weight_t.name} weight_t;
+ template
+ using kernel = nnet::{dense_function};
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+# EinsumDense template
+
+einsum_dense_config_template = """
+struct config{index} {{
+ typedef config{index}_tpose_inp tpose_inp_conf;
+ typedef config{index}_tpose_out tpose_out_conf;
+
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+
+ {kernel_config};
+
+ // Layer Sizes
+ static const unsigned n_free_data = {n_free_data};
+ static const unsigned n_free_kernel = {n_free_kernel};
+ static const unsigned n_contract = {n_contract};
+ static const unsigned n_inplace = {n_inplace};
+
+ // Resource reuse info
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned strategy = nnet::{strategy};
+ static const unsigned reuse_factor = {reuse_factor};
+ static const unsigned parallelization_factor = {parallelization_factor}; // Only useful when n_inplace > 1
+}};
+"""
+
+einsum_dense_function_template = 'nnet::einsum_dense<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {b});'
+einsum_dense_da_function_template = 'nnet::einsum_dense<{input_t}, {output_t}, {config}>({input}, {output}, {b});'
+
+einsum_dense_include_list = ['nnet_utils/nnet_einsum_dense.h', 'nnet_utils/nnet_dense.h']
+
+
+class EinsumDenseConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(EinsumDense)
+ self.template = einsum_dense_config_template
+ self.dense_template = dense_config_template
+
+ def dense_config(self, node: EinsumDense):
+ dense_params = self._default_config_params(node)
+ strategy = node.attributes['strategy']
+ dense_params['strategy'] = strategy
+ dense_params['n_in'] = node.attributes['n_contract']
+ dense_params['n_out'] = node.attributes['n_free_kernel']
+ if node.attributes['n_inplace'] == 1:
+ dense_params['nzeros'] = node.get_weights('weight').nzeros # type: ignore
+ else:
+ dense_params['nzeros'] = '-1; // Not making sense when kernels are switching'
+ dense_params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision,
+ node.get_weights('weight').type.precision, # type: ignore
+ )
+
+ dense_params['dense_function'] = 'DenseLatency' # Latency only for now
+
+ dense_config = self.dense_template.format(**dense_params)
+ return dense_config
+
+ def format(self, node: EinsumDense):
+ default_params = self._default_config_params(node)
+
+ strategy = node.attributes['strategy']
+ io_type = node.model.config.get_config_value('IOType')
+
+ assert io_type == 'io_parallel', 'EinsumDense layer only supports io_parallel and distributed_arithmetic'
+
+ # EinsumDense config
+ params = default_params.copy()
+ params['strategy'] = strategy
+ params['n_free_data'] = node.attributes['n_free_data']
+ params['n_free_kernel'] = node.attributes['n_free_kernel']
+ params['n_contract'] = node.attributes['n_contract']
+ params['n_inplace'] = node.attributes['n_inplace']
+ if strategy.lower() == 'latency':
+ params['kernel_config'] = f'typedef config{node.index}_dense dense_conf'
+ else:
+ assert strategy.lower() == 'distributed_arithmetic', 'EinsumDense layer only supports Latency strategy for now'
+ inp_t = node.get_input_variable().type.name
+ index = node.index
+ conf = f'constexpr static auto da_kernel = nnet::einsum_dense{index}_da_kernel<{inp_t}, accum_t>'
+ params['kernel_config'] = conf
+ pf = node.attributes['parallelization_factor']
+ if pf < 0:
+ pf = params['n_inplace']
+ params['parallelization_factor'] = pf
+
+ einsum_conf = self.template.format(**params)
+
+ # inp/out transpose config
+ inp_shape = node.attributes['inp_shape']
+ out_interpert_shape = node.attributes['out_interpert_shape']
+ inp_tpose_idxs = node.attributes['inp_tpose_idxs']
+ out_tpose_idxs = node.attributes['out_tpose_idxs']
+ tpose_inp_conf_name = f'config{node.index}_tpose_inp'
+ tpose_out_conf_name = f'config{node.index}_tpose_out'
+
+ conf = transpose_config_gen(tpose_inp_conf_name, inp_shape, inp_tpose_idxs)
+ inp_tpose_conf = transpose_config_template.format(**conf)
+ conf = transpose_config_gen(tpose_out_conf_name, out_interpert_shape, out_tpose_idxs)
+ out_tpose_conf = transpose_config_template.format(**conf)
+
+ if strategy.lower() == 'distributed_arithmetic':
+ return '\n\n'.join((inp_tpose_conf, out_tpose_conf, einsum_conf))
+
+ dense_config = self.dense_config(node)
+ return '\n\n'.join((inp_tpose_conf, out_tpose_conf, dense_config, einsum_conf))
+
+
+class EinsumDenseFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(EinsumDense, include_header=einsum_dense_include_list)
+ self.template = einsum_dense_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['b'] = node.get_weights('bias').name
+
+ strategy = node.attributes['strategy']
+ if strategy == 'distributed_arithmetic':
+ return einsum_dense_da_function_template.format(**params)
+
+ params['w'] = node.get_weights('weight').name
+ return einsum_dense_function_template.format(**params)
diff --git a/hls4ml/backends/bambu/passes/fifo_depth_optimization.py b/hls4ml/backends/bambu/passes/fifo_depth_optimization.py
new file mode 100644
index 0000000000..496d4965f3
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/fifo_depth_optimization.py
@@ -0,0 +1,104 @@
+import json
+
+from pyDigitalWaveTools.vcd.parser import VcdParser
+
+from hls4ml.model.optimizer.optimizer import ConfigurableOptimizerPass, ModelOptimizerPass
+
+
+def populate_values(values, name, data, depth):
+ def get_values(x):
+ return int(x[1][1:], 2)
+
+ values.append({'name': name, 'data': [], 'max': 0, 'depth': 0})
+ values[-1]['data'] = [get_values(x) for x in data]
+ values[-1]['max'] = max(values[-1]['data'])
+ values[-1]['depth'] = int(depth[0][1][1:], 2)
+ return values
+
+
+def set_big_fifos(vars_to_profile, profiling_fifo_depth):
+ for v in vars_to_profile.values():
+ if v.pragma:
+ v.pragma = (v.pragma[0], profiling_fifo_depth)
+
+
+def get_vcd_data(model):
+ model.write()
+ model.build(reset=False, csim=True, synth=True, cosim=True, validation=False, export=False, vsynth=False, fifo_opt=True)
+
+ with open(
+ model.config.get_output_dir()
+ + '/'
+ + model.config.get_project_name()
+ + '_prj'
+ + '/solution1/sim/verilog/fifo_opt.vcd'
+ ) as vcd_file:
+ vcd = VcdParser()
+ vcd.parse(vcd_file)
+ data = vcd.scope.toJson()
+ return data
+
+
+def generate_max_depth_file(model, maxs):
+ with open(model.config.get_output_dir() + '/max_depth.json', 'w') as f:
+ json.dump(maxs, f, indent=4)
+
+
+def set_fifo_depth(model, maxs):
+ for v in model.output_vars.values():
+ if v.pragma:
+ filtered_max = [x['max'] for x in maxs if v.name in x['name']]
+ if len(filtered_max) == 0:
+ continue
+ if len(filtered_max) > 1:
+ print('WARNING! Check names of FIFOs')
+ v.pragma = (v.pragma[0], filtered_max[0] + 1)
+
+
+class FifoDepthOptimization(ConfigurableOptimizerPass, ModelOptimizerPass):
+ def __init__(self):
+ self.values = []
+
+ def transform(self, model):
+ # use `large_fifo_depth = 0` to keep the default fifo depth
+ profiling_fifo_depth = getattr(self, 'profiling_fifo_depth', 100_000)
+
+ # check axi-stream or io-stream, if not one the 2 exit
+ if not (model.config.get_config_value('IOType') == 'io_stream'):
+ raise RuntimeError('To use this optimization you have to set `IOType` field to `io_stream` in the HLS config')
+
+ # initialize all the fifos to `profiling_fifo_depth` so that they will be automatically implemented in BRAMs
+ # and so they will be profiled
+ if profiling_fifo_depth:
+ vars_to_profile = {
+ k: v
+ for k, v in model.output_vars.items()
+ if v != model.get_output_variables()[0] and v != model.get_input_variables()[0]
+ }
+
+ set_big_fifos(vars_to_profile, profiling_fifo_depth)
+
+ data = get_vcd_data(model)
+
+ if len(data['children']) == 0:
+ print(
+ 'FIFO depth optimization found no FIFOs implemented using BRAMs in the design, no optimization is possible.'
+ )
+ print('Consider increasing profiling_fifo_depth.')
+ return False
+
+ n_elem = len(data['children'][0]['children'][0]['children'])
+ for i in range(n_elem):
+ name = data['children'][0]['children'][0]['children'][i]['name']
+ data_p = data['children'][0]['children'][0]['children'][i]['children'][0]['data']
+ depth = data['children'][0]['children'][0]['children'][i]['children'][1]['data']
+ populate_values(self.values, name, data_p, depth)
+
+ maxs = [{'name': i['name'], 'max': i['max'], 'depth': i['depth']} for i in self.values]
+
+ generate_max_depth_file(model, maxs)
+
+ set_fifo_depth(model, maxs)
+
+ print('[hls4ml] - FIFO optimization completed')
+ return False
diff --git a/hls4ml/backends/bambu/passes/garnet_templates.py b/hls4ml/backends/bambu/passes/garnet_templates.py
new file mode 100644
index 0000000000..4b968b0f4c
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/garnet_templates.py
@@ -0,0 +1,249 @@
+import numpy as np
+
+from hls4ml.backends.fpga.fpga_types import APTypeConverter
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import GarNet, GarNetStack
+from hls4ml.model.types import FixedPrecisionType
+
+# GarNet templates
+
+garnet_common_config_template = """
+ static const unsigned n_vertices = {n_vertices};
+ static const unsigned n_vertices_width = {n_vertices_width};
+ static const unsigned n_in_features = {n_in_features};
+ static const unsigned distance_width = {distance_width};
+ static const unsigned output_collapse = {collapse_type};
+ static const bool mean_by_nvert = {mean_by_nvert};
+
+ typedef {norm_t} norm_t;
+ typedef ap_fixed<{distance_width}, {distance_nint}, AP_TRN, AP_SAT> distance_t;
+ typedef {edge_weight_t} edge_weight_t;
+ typedef {edge_weight_aggr_t} edge_weight_aggr_t;
+ typedef {aggr_t} aggr_t;
+ typedef {output_t} output_t;
+
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned log2_reuse_factor = {log2_reuse};
+"""
+
+garnet_config_template = """struct config{index} : nnet::garnet_config {{"""
+garnet_config_template += garnet_common_config_template
+garnet_config_template += """
+ static const unsigned n_propagate = {n_propagate};
+ static const unsigned n_aggregators = {n_aggregators};
+ static const unsigned n_out_features = {n_out_features};
+
+ typedef {input_transform_weights_t} input_transform_weights_t;
+ typedef {input_transform_biases_t} input_transform_biases_t;
+ typedef {aggregator_distance_weights_t} aggregator_distance_weights_t;
+ typedef {aggregator_distance_biases_t} aggregator_distance_biases_t;
+ typedef {output_transform_weights_t} output_transform_weights_t;
+ typedef {output_transform_biases_t} output_transform_biases_t;
+
+ static const input_transform_weights_t (&input_transform_weights)[{input_transform_weights_size}];
+ static const input_transform_biases_t (&input_transform_biases)[{input_transform_biases_size}];
+ static const aggregator_distance_weights_t (&aggregator_distance_weights)[{aggregator_distance_weights_size}];
+ static const aggregator_distance_biases_t (&aggregator_distance_biases)[{aggregator_distance_biases_size}];
+ static const output_transform_weights_t (&output_transform_weights)[{output_transform_weights_size}];
+ static const output_transform_biases_t (&output_transform_biases)[{output_transform_biases_size}];
+
+ typedef config{index} base_t;
+}};
+
+const config{index}::input_transform_weights_t (&config{index}::input_transform_weights)[{input_transform_weights_size}] = {input_transform_weights};
+const config{index}::input_transform_biases_t (&config{index}::input_transform_biases)[{input_transform_biases_size}] = {input_transform_biases};
+const config{index}::aggregator_distance_weights_t (&config{index}::aggregator_distance_weights)[{aggregator_distance_weights_size}] = {aggregator_distance_weights};
+const config{index}::aggregator_distance_biases_t (&config{index}::aggregator_distance_biases)[{aggregator_distance_biases_size}] = {aggregator_distance_biases};
+const config{index}::output_transform_weights_t (&config{index}::output_transform_weights)[{output_transform_weights_size}] = {output_transform_weights};
+const config{index}::output_transform_biases_t (&config{index}::output_transform_biases)[{output_transform_biases_size}] = {output_transform_biases};
+""" # noqa: E501
+
+garnet_function_template = (
+ 'nnet::garnet{impl}<{input_t}, {integer_input_t}, {output_t}, {config}>({input}, {nvtx}, {output});'
+)
+
+garnet_include_list = ['nnet_utils/nnet_garnet.h']
+
+
+class GarNetConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(GarNet)
+ self.template = (garnet_config_template,)
+
+ def get_transforms_config(self, node, params):
+ params['n_in_features'] = node.attributes['n_in_features']
+ params['n_propagate'] = node.attributes['n_propagate']
+ params['n_aggregators'] = node.get_weights('aggregator_distance_biases').shape[0]
+ params['n_out_features'] = node.get_weights('output_transform_biases').shape[0]
+
+ for wname, weights in node.weights.items():
+ params[wname] = weights.name
+ params[f'{wname}_t'] = weights.type.name
+ params[f'{wname}_size'] = weights.data_length
+
+ def format(self, node):
+ params = self._default_config_params(node)
+
+ params['n_vertices'] = node.attributes['n_vertices']
+ params['n_vertices_width'] = int(np.log2(params['n_vertices']))
+ params['distance_width'] = 12
+ params['distance_nint'] = min(4, params['distance_width'] - 6) # this is tuned
+ params['log2_reuse'] = int(np.log2(params['reuse']))
+
+ # Define default precisions for various internal arrays (can be overridden from the config file)
+ # We always give 10 digits for the subintegral part
+ fwidth = 10
+ # Integral precision for aggr_t depends on how large the temporary sum for weighed feature mean will be
+ aggr_intw = max(params['log2_reuse'], params['n_vertices_width'] - params['log2_reuse']) + 3 # safety factor 2**3
+ aggr_w = aggr_intw + fwidth
+ # edge_weight_aggr_t does not need the safety factor
+ ew_aggr_intw = aggr_intw - 3
+ ew_aggr_w = ew_aggr_intw + fwidth
+ # Integral precision for norm is fixed to 4
+ norm_intw = 4
+ norm_w = norm_intw + fwidth
+
+ vspecs = [
+ ('edge_weight', FixedPrecisionType(10, 0, signed=False)),
+ ('edge_weight_aggr', FixedPrecisionType(ew_aggr_w, ew_aggr_intw, signed=False)),
+ ('aggr', FixedPrecisionType(aggr_w, aggr_intw)),
+ ('norm', FixedPrecisionType(norm_w, norm_intw, signed=False)),
+ ]
+ precision_converter = APTypeConverter()
+ for vname, default_precision in vspecs:
+ params[f'{vname}_t'], type_name = node.model.config.get_precision(node, var=vname)
+ if type_name.endswith('default_t'):
+ params[f'{vname}_t'] = precision_converter.convert(default_precision).definition_cpp()
+ else:
+ params[f'{vname}_t'] = precision_converter.convert(params[f'{vname}_t']).definition_cpp()
+ params['output_t'] = node.get_output_variable().type.name
+
+ if node.attributes['collapse'] in ['mean', 'max']:
+ params['collapse_type'] = 'collapse_{}'.format(node.attributes['collapse'])
+ else:
+ params['collapse_type'] = 'no_collapse'
+
+ params['mean_by_nvert'] = str(node.attributes['mean_by_nvert']).lower()
+
+ self.get_transforms_config(node, params)
+
+ return self.template[0].format(**params)
+
+
+class GarNetFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(GarNet, include_header=garnet_include_list)
+ self.template = garnet_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+
+ data = node.get_input_variable(node.inputs[0])
+ integer_input = node.get_input_variable(node.inputs[1])
+ params['input_t'] = data.type.name
+ params['input'] = data.name
+
+ params['integer_input_t'] = integer_input.type.name
+ params['nvtx'] = integer_input.name
+
+ if node.ref_impl:
+ params['impl'] = '_ref'
+ else:
+ params['impl'] = ''
+
+ return self.template.format(**params)
+
+
+# GarNetStack Templates
+
+garnet_stack_base_config_template = """struct config{index}_base : nnet::garnet_config {{"""
+garnet_stack_base_config_template += garnet_common_config_template
+garnet_stack_base_config_template += """
+ static const bool is_stack = true;
+
+ typedef config{index}_base base_t;
+}};
+
+struct config{index} : config{index}_base {{
+ static const unsigned n_sublayers = {n_sublayers};
+
+ template
+ struct sublayer_t : config{index}_base {{}};
+}};
+
+{sublayer_configs}
+"""
+
+garnet_stack_sublayer_config_template = """template<>
+struct config{index}::sublayer_t<{il}> : config{index}_base {{
+ static const unsigned n_in_features = {n_in_features};
+ static const unsigned n_propagate = {n_propagate};
+ static const unsigned n_aggregators = {n_aggregators};
+ static const unsigned n_out_features = {n_out_features};
+
+ typedef {input_transform_weights_t} input_transform_weights_t;
+ typedef {input_transform_biases_t} input_transform_biases_t;
+ typedef {aggregator_distance_weights_t} aggregator_distance_weights_t;
+ typedef {aggregator_distance_biases_t} aggregator_distance_biases_t;
+ typedef {output_transform_biases_t} output_transform_biases_t;
+
+ static const input_transform_weights_t (&input_transform_weights)[{input_transform_weights_size}];
+ static const input_transform_biases_t (&input_transform_biases)[{input_transform_biases_size}];
+ static const aggregator_distance_weights_t (&aggregator_distance_weights)[{aggregator_distance_weights_size}];
+ static const aggregator_distance_biases_t (&aggregator_distance_biases)[{aggregator_distance_biases_size}];
+ static const output_transform_biases_t (&output_transform_biases)[{output_transform_biases_size}];
+
+ typedef config{index}::sublayer_t<{next}> next_layer_t;
+}};
+
+const config{index}::sublayer_t<{il}>::input_transform_weights_t (&config{index}::sublayer_t<{il}>::input_transform_weights)[{input_transform_weights_size}] = {input_transform_weights};
+const config{index}::sublayer_t<{il}>::input_transform_biases_t (&config{index}::sublayer_t<{il}>::input_transform_biases)[{input_transform_biases_size}] = {input_transform_biases};
+const config{index}::sublayer_t<{il}>::aggregator_distance_weights_t (&config{index}::sublayer_t<{il}>::aggregator_distance_weights)[{aggregator_distance_weights_size}] = {aggregator_distance_weights};
+const config{index}::sublayer_t<{il}>::aggregator_distance_biases_t (&config{index}::sublayer_t<{il}>::aggregator_distance_biases)[{aggregator_distance_biases_size}] = {aggregator_distance_biases};
+const config{index}::sublayer_t<{il}>::output_transform_biases_t (&config{index}::sublayer_t<{il}>::output_transform_biases)[{output_transform_biases_size}] = {output_transform_biases};
+""" # noqa: E501
+
+garnet_stack_config_template = (garnet_stack_base_config_template, garnet_stack_sublayer_config_template)
+garnet_stack_function_template = (
+ 'nnet::garnet_stack<{input_t}, {integer_input_t}, {output_t}, {config}>({input}, {nvtx}, {output});'
+)
+
+
+class GarNetStackConfigTemplate(GarNetConfigTemplate):
+ def __init__(self):
+ super(GarNetConfigTemplate, self).__init__(GarNetStack)
+ self.template = garnet_stack_config_template
+
+ def get_transforms_config(self, node, params):
+ _, sublayer_template = self.template
+
+ params['n_sublayers'] = node.attributes['n_sublayers']
+ params['n_in_features'] = node.attributes['n_in_features'][0]
+ params['n_out_features'] = node.attributes['n_out_features'][-1]
+
+ sublayer_configs = []
+ for il in range(node.attributes['n_sublayers'] - 1, -1, -1):
+ sub_params = {'index': node.index, 'il': il}
+
+ for p in ['n_in_features', 'n_propagate', 'n_aggregators', 'n_out_features']:
+ sub_params[p] = node.attributes[p][il]
+
+ for wname, weights in node._sublayer_weights[il].items():
+ sub_params[wname] = weights.name
+ sub_params[f'{wname}_t'] = weights.type.name
+ sub_params[f'{wname}_size'] = weights.data_length
+
+ if il != node.attributes['n_sublayers'] - 1:
+ sub_params['next'] = il + 1
+ else:
+ sub_params['next'] = 0
+
+ sublayer_configs.append(sublayer_template.format(**sub_params))
+
+ params['sublayer_configs'] = '\n'.join(sublayer_configs)
+
+
+class GarNetStackFunctionTemplate(GarNetFunctionTemplate):
+ def __init__(self):
+ super(GarNetFunctionTemplate, self).__init__(GarNetStack, include_header=garnet_include_list)
+ self.template = garnet_stack_function_template
diff --git a/hls4ml/backends/bambu/passes/im2col_codegen.py b/hls4ml/backends/bambu/passes/im2col_codegen.py
new file mode 100644
index 0000000000..11e48d2552
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/im2col_codegen.py
@@ -0,0 +1,116 @@
+from hls4ml.model.layers import Conv1D, Conv2D, SeparableConv1D, SeparableConv2D
+from hls4ml.model.optimizer import OptimizerPass
+from hls4ml.model.types import Source
+
+
+class GenerateConvIm2col(OptimizerPass):
+ """Generates tcode for im2col step of 1D/2d convolution"""
+
+ # Note, DepthwizeConv1D/2D also matches because it inherits from Conv1D/2D
+ def match(self, node):
+ return (
+ isinstance(node, (Conv1D, Conv2D, SeparableConv1D, SeparableConv2D))
+ and node.model.config.get_config_value('IOType') == 'io_parallel'
+ )
+
+ def transform(self, model, node):
+ node_class = node.class_name
+ if 'Separable' in node_class:
+ if '1D' in node_class:
+ self._generate_separable_im2col_1d(node)
+ elif '2D' in node_class:
+ self._generate_separable_im2col_2d(node)
+ else:
+ raise Exception(f'Cannot generate instructions for node {node.name} ({node_class})')
+ else:
+ if '1D' in node_class:
+ self._generate_im2col_1d(node)
+ elif '2D' in node_class:
+ self._generate_im2col_2d(node)
+ else:
+ raise Exception(f'Cannot generate instructions for node {node.name} ({node_class})')
+
+ def _generate_im2col_1d(self, node):
+ code_str = node.model.config.backend.generate_conv1d_line_buffer_fn(
+ node.get_attr('index'),
+ node.get_attr('n_partitions'),
+ node.get_input_variable().shape[0],
+ node.get_input_variable().shape[1],
+ kernel=node.get_attr('filt_width'),
+ stride=node.get_attr('stride_width'),
+ pad=(node.get_attr('pad_left'), node.get_attr('pad_right')),
+ )
+
+ node.set_attr('line_buffer_codegen', Source(code_str))
+
+ def _generate_im2col_2d(self, node):
+ code_str = node.model.config.backend.generate_conv2d_line_buffer_fn(
+ node.get_attr('index'),
+ node.get_attr('n_partitions'),
+ node.get_input_variable().shape[0],
+ node.get_input_variable().shape[1],
+ node.get_input_variable().shape[2],
+ kernel=(node.get_attr('filt_height'), node.get_attr('filt_width')),
+ stride=(node.get_attr('stride_height'), node.get_attr('stride_width')),
+ pad=(
+ node.get_attr('pad_top'),
+ node.get_attr('pad_bottom'),
+ node.get_attr('pad_left'),
+ node.get_attr('pad_right'),
+ ),
+ )
+
+ node.set_attr('line_buffer_codegen', Source(code_str))
+
+ def _generate_separable_im2col_1d(self, node):
+ dw_code_str = node.model.config.backend.generate_conv1d_line_buffer_fn(
+ str(node.get_attr('index')) + '_dw',
+ node.get_attr('n_partitions'),
+ node.get_input_variable().shape[0],
+ node.get_input_variable().shape[1],
+ kernel=node.get_attr('filt_width'),
+ stride=node.get_attr('stride_width'),
+ pad=(node.get_attr('pad_left'), node.get_attr('pad_right')),
+ )
+
+ node.set_attr('dw_line_buffer_codegen', Source(dw_code_str))
+
+ pw_code_str = node.model.config.backend.generate_conv1d_line_buffer_fn(
+ str(node.get_attr('index')) + '_pw',
+ node.get_attr('n_partitions'),
+ node.get_output_variable().shape[0],
+ node.get_input_variable().shape[1],
+ kernel=1,
+ )
+
+ node.set_attr('pw_line_buffer_codegen', Source(pw_code_str))
+
+ def _generate_separable_im2col_2d(self, node):
+ dw_code_str = node.model.config.backend.generate_conv2d_line_buffer_fn(
+ str(node.get_attr('index')) + '_dw',
+ node.get_attr('n_partitions'),
+ node.get_input_variable().shape[0],
+ node.get_input_variable().shape[1],
+ node.get_input_variable().shape[2],
+ kernel=(node.get_attr('filt_height'), node.get_attr('filt_width')),
+ stride=(node.get_attr('stride_height'), node.get_attr('stride_width')),
+ pad=(
+ node.get_attr('pad_top'),
+ node.get_attr('pad_bottom'),
+ node.get_attr('pad_left'),
+ node.get_attr('pad_right'),
+ ),
+ )
+
+ node.set_attr('dw_line_buffer_codegen', Source(dw_code_str))
+
+ pw_code_str = node.model.config.backend.generate_conv2d_line_buffer_fn(
+ str(node.get_attr('index')) + '_pw',
+ node.get_attr('n_partitions'),
+ node.get_output_variable().shape[0],
+ node.get_output_variable().shape[1],
+ node.get_input_variable().shape[2],
+ kernel=(1, 1),
+ )
+
+ node.set_attr('pw_line_buffer_codegen', Source(pw_code_str))
diff --git a/hls4ml/backends/bambu/passes/merge_templates.py b/hls4ml/backends/bambu/passes/merge_templates.py
new file mode 100644
index 0000000000..89346d7b11
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/merge_templates.py
@@ -0,0 +1,107 @@
+from hls4ml.backends.backend import get_backend
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import Concatenate, Dot, Merge
+
+# Merge templates
+
+merge_config_template = """struct config{index} : nnet::merge_config {{
+ static const unsigned n_elem = {n_elem};
+ static const unsigned reuse_factor = {reuse};
+}};\n"""
+
+merge_function_template = 'nnet::{merge}<{input1_t}, {input2_t}, {output_t}, {config}>({input1}, {input2}, {output});'
+
+merge_include_list = ['nnet_utils/nnet_merge.h', 'nnet_utils/nnet_merge_stream.h']
+
+
+class MergeConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Merge)
+ self.template = merge_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['n_elem'] = node.get_input_variable(node.inputs[0]).size_cpp()
+
+ return self.template.format(**params)
+
+
+class MergeFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((Merge, Concatenate, Dot), include_header=merge_include_list)
+ self.template = merge_function_template
+
+ def format(self, node):
+ params = {}
+ params['merge'] = node.get_attr('op').lower()
+ params['config'] = f'config{node.index}'
+ params['input1_t'] = node.get_input_variable(node.inputs[0]).type.name
+ params['input2_t'] = node.get_input_variable(node.inputs[1]).type.name
+ params['output_t'] = node.get_output_variable().type.name
+ params['input1'] = node.get_input_variable(node.inputs[0]).name
+ params['input2'] = node.get_input_variable(node.inputs[1]).name
+ params['output'] = node.get_output_variable().name
+
+ return self.template.format(**params)
+
+
+# Dot templates
+
+dot_config_template = """struct config{index} : nnet::dot_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned multiplier_limit = DIV_ROUNDUP(n_in, reuse_factor);
+ typedef {accum_t.name} accum_t;
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+
+class DotConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Dot)
+ self.template = dot_config_template
+
+ def format(self, node):
+ inp1 = node.get_input_variable(node.inputs[0])
+ inp2 = node.get_input_variable(node.inputs[1])
+ params = self._default_config_params(node)
+ params['n_out'] = 1
+ params['n_in'] = inp1.shape[0]
+ params['product_type'] = get_backend('bambu').product_type(inp1.type.precision, inp2.type.precision)
+
+ return self.template.format(**params)
+
+
+# Concatenate templates
+
+concat_config_template = """struct config{index} : nnet::concat_config {{
+ static const unsigned n_elem1_0 = {n_elem1_0};
+ static const unsigned n_elem1_1 = {n_elem1_1};
+ static const unsigned n_elem1_2 = {n_elem1_2};
+ static const unsigned n_elem2_0 = {n_elem2_0};
+ static const unsigned n_elem2_1 = {n_elem2_1};
+ static const unsigned n_elem2_2 = {n_elem2_2};
+
+ static const int axis = {axis};
+}};\n"""
+
+
+class ConcatenateConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Concatenate)
+ self.template = concat_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ for i in range(3):
+ params.setdefault(f'n_elem1_{i}', 0)
+ params.setdefault(f'n_elem2_{i}', 0)
+ inp1 = node.get_input_variable(node.inputs[0])
+ inp2 = node.get_input_variable(node.inputs[1])
+ for i, (s1, s2) in enumerate(zip(inp1.shape, inp2.shape)):
+ params[f'n_elem1_{i}'] = s1
+ params[f'n_elem2_{i}'] = s2
+
+ return self.template.format(**params)
diff --git a/hls4ml/backends/bambu/passes/pipeline_style.py b/hls4ml/backends/bambu/passes/pipeline_style.py
new file mode 100644
index 0000000000..fb37709f94
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/pipeline_style.py
@@ -0,0 +1,131 @@
+from hls4ml.model.layers import Conv1D, Conv2D
+from hls4ml.model.optimizer import ModelOptimizerPass
+
+
+class SetPipelineStyle(ModelOptimizerPass):
+ def __init__(self):
+ pass
+
+ def transform(self, model):
+ if model.config.pipeline_style not in ['auto', 'pipeline', 'dataflow']:
+ print(
+ f'WARNING: Pipeline style set to {model.config.pipeline_style}, valid values: auto, pipeline, dataflow. '
+ 'Using "auto".'
+ )
+ self._set_pipeline_style(model, 'auto')
+
+ if model.config.pipeline_style is None or model.config.pipeline_style == 'auto':
+ if self._maybe_set_dataflow_io_stream(model):
+ return True
+
+ if self._maybe_set_dataflow_conv_layers(model):
+ return True
+
+ if self._maybe_set_dataflow_resource_strategy(model):
+ return True
+
+ if self._maybe_set_pipeline_resource_unrolled_strategy(model):
+ return True
+
+ if self._maybe_set_pipeline_io_parallel(model):
+ return True
+
+ self._set_safe_default_dataflow(model)
+ return True
+ else:
+ self._validate_hls_config(model)
+
+ return False # No model changes made
+
+ def _set_pipeline_style(self, model, pipeline_style):
+ # Could add logging here
+ model.config.pipeline_style = pipeline_style
+
+ def _maybe_set_dataflow_io_stream(self, model):
+ if model.config.get_config_value('IOType') == 'io_stream':
+ self._set_pipeline_style(model, 'dataflow')
+ return True
+
+ return False
+
+ def _maybe_set_dataflow_conv_layers(self, model):
+ for layer in model.get_layers():
+ if isinstance(layer, (Conv1D, Conv2D)) and layer.attributes['n_partitions'] != 1:
+ # pragma dataflow is having weird behavior if II is supposed to be 1
+ self._set_pipeline_style(model, 'dataflow')
+ return True
+
+ return False
+
+ def _maybe_set_dataflow_resource_strategy(self, model):
+ for layer in model.get_layers():
+ if model.config.is_resource_strategy(layer):
+ self._set_pipeline_style(model, 'dataflow')
+ return True
+
+ return False
+
+ def _maybe_set_pipeline_resource_unrolled_strategy(self, model):
+ have_unrolled = False
+ for layer in model.get_layers():
+ if model.config.get_strategy(layer).lower() == 'resource_unrolled':
+ self._set_pipeline_style(model, 'pipeline')
+ have_unrolled = True
+ break
+
+ if have_unrolled:
+ model.config.pipeline_ii = max([int(layer.get_attr('reuse_factor')) for layer in model.get_layers()])
+
+ return have_unrolled
+
+ def _maybe_set_pipeline_io_parallel(self, model):
+ if model.config.get_config_value('IOType') == 'io_parallel':
+ self._set_pipeline_style(model, 'pipeline')
+ return True
+
+ return False
+
+ def _set_safe_default_dataflow(self, model):
+ print(
+ 'WARNING: Couldn\'t determine best pipeline style, defaulting to "DATAFLOW". '
+ 'Use "PipelineStyle" property to override.'
+ )
+ self._set_pipeline_style(model, 'dataflow')
+
+ def _validate_hls_config(self, model):
+ if model.config.pipeline_style.lower() == 'pipeline':
+ if model.config.model_compression:
+ print('WARNING: Compression enabled while pipeline style set to "pipeline".')
+ if model.config.model_strategy.lower() == 'resource':
+ print(
+ 'WARNING: Model strategy "Resource" will lead to bad QoR in combination '
+ 'with pipeline style set to "pipeline".'
+ )
+ if any(isinstance(layer, (Conv1D, Conv2D)) for layer in model.get_layers()):
+ print('WARNING: Convolution layers require "dataflow" pipeline style.')
+ for layer_type, strategy in model.config.layer_type_strategy.items():
+ if strategy.lower() == 'resource' and model.config.pipeline_style.lower() == 'pipeline':
+ print(
+ f'WARNING: Strategy for layer type {layer_type} set to "Resource", while pipeline style set to '
+ '"pipeline". This will lead to bad QoR.'
+ )
+
+ for layer_name, strategy in model.config.layer_name_strategy.items():
+ if strategy.lower() == 'resource' and model.config.pipeline_style.lower() == 'pipeline':
+ print(
+ 'WARNING: Strategy for layer {} set to "Resource", while pipeline style set to "pipeline".'.format(
+ layer_name
+ )
+ )
+
+ for layer_type, compression in model.config.layer_type_compression.items():
+ if compression and model.config.pipeline_style.lower() == 'pipeline':
+ print(
+ 'WARNING: Compression enabled for layer type {}, while pipeline style set to "pipeline".'.format(
+ layer_type
+ )
+ )
+
+ for layer_name, compression in model.config.layer_name_compression.items():
+ if compression and model.config.pipeline_style.lower() == 'pipeline':
+ print(f'WARNING: Compression enabled for layer {layer_name}, while pipeline style set to "pipeline".')
diff --git a/hls4ml/backends/bambu/passes/pointwise.py b/hls4ml/backends/bambu/passes/pointwise.py
new file mode 100644
index 0000000000..747584499f
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/pointwise.py
@@ -0,0 +1,87 @@
+from hls4ml.backends.bambu.passes.convolution_templates import (
+ Conv1DConfigTemplate,
+ Conv1DFunctionTemplate,
+ Conv2DConfigTemplate,
+ Conv2DFunctionTemplate,
+ conv1d_config_template,
+ conv2d_config_template,
+ conv_mult_config_template,
+)
+from hls4ml.backends.fpga.fpga_layers import PointwiseConv1D, PointwiseConv2D
+from hls4ml.model.layers import register_layer
+from hls4ml.model.optimizer import OptimizerPass
+
+pointwise_conv1d_function_template = (
+ 'nnet::pointwise_conv_1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {b});'
+)
+pointwise_conv2d_function_template = (
+ 'nnet::pointwise_conv_2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {b});'
+)
+
+sepconv1d_include_list = ['nnet_utils/nnet_conv1d.h', 'nnet_utils/nnet_sepconv1d_stream.h']
+sepconv2d_include_list = ['nnet_utils/nnet_conv2d.h', 'nnet_utils/nnet_sepconv2d_stream.h']
+
+
+class PointwiseConv1DConfigTemplate(Conv1DConfigTemplate):
+ def __init__(self):
+ super(Conv1DConfigTemplate, self).__init__(PointwiseConv1D)
+ self.template = conv1d_config_template
+ self.mult_template = conv_mult_config_template
+
+
+class PointwiseConv1DFunctionTemplate(Conv1DFunctionTemplate):
+ def __init__(self):
+ super(Conv1DFunctionTemplate, self).__init__(PointwiseConv1D, include_header=sepconv1d_include_list)
+ self.template = pointwise_conv1d_function_template
+
+
+class PointwiseConv2DConfigTemplate(Conv2DConfigTemplate):
+ def __init__(self):
+ super(Conv2DConfigTemplate, self).__init__(PointwiseConv2D)
+ self.template = conv2d_config_template
+ self.mult_template = conv_mult_config_template
+
+
+class PointwiseConv2DFunctionTemplate(Conv2DFunctionTemplate):
+ def __init__(self):
+ super(Conv2DFunctionTemplate, self).__init__(PointwiseConv2D, include_header=sepconv2d_include_list)
+ self.template = pointwise_conv2d_function_template
+
+
+def register_pointwise(backend):
+ # Register the layer types to the layer map
+ register_layer('PointwiseConv1D', PointwiseConv1D)
+ register_layer('PointwiseConv2D', PointwiseConv2D)
+
+ # Register the optimization passes
+ backend.register_pass('optimize_pointwise_conv', OptimizePointwiseConv)
+
+ # Register template passes
+ backend.register_template(PointwiseConv1DConfigTemplate)
+ backend.register_template(PointwiseConv1DFunctionTemplate)
+ backend.register_template(PointwiseConv2DConfigTemplate)
+ backend.register_template(PointwiseConv2DFunctionTemplate)
+
+
+class OptimizePointwiseConv(OptimizerPass):
+ def match(self, node):
+ if node.get_attr('strategy') == 'distributed_arithmetic':
+ if node.class_name == 'Conv1D':
+ return False
+ return (
+ node.class_name in ('Conv1D', 'Conv2D')
+ and node.get_attr('filt_height', 1) == 1
+ and node.get_attr('filt_width') == 1
+ )
+
+ def transform(self, model, node):
+ dim = node.__class__.__name__[-2:] # '1D' or '2D'
+ # to remove warning, since these get set again
+ new_attrs = node.attributes.attributes.copy()
+ pw_node = model.make_node(
+ 'PointwiseConv' + dim, node.name, new_attrs, node.inputs.copy(), outputs=node.outputs.copy()
+ )
+ # Set strategy to ensure lowercase string is passed to the template
+ pw_node.set_attr('strategy', node.get_attr('strategy'))
+ model.replace_node(node, pw_node)
+ return True
diff --git a/hls4ml/backends/bambu/passes/pooling_templates.py b/hls4ml/backends/bambu/passes/pooling_templates.py
new file mode 100644
index 0000000000..77205a5df7
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/pooling_templates.py
@@ -0,0 +1,109 @@
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import GlobalPooling1D, GlobalPooling2D, Pooling1D, Pooling2D
+
+# Pooling templates
+
+pooling1d_config_template = """struct config{index} : nnet::pooling1d_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned n_filt = {n_filt};
+ static const unsigned pool_width = {pool_width};
+
+ static const unsigned filt_width = pool_width;
+ static const unsigned n_chan = n_filt;
+
+ static const unsigned pad_left = {pad_left};
+ static const unsigned pad_right = {pad_right};
+ static const bool count_pad = {count_pad};
+ static const unsigned stride_width = {stride_width};
+ static const nnet::Pool_Op pool_op = nnet::{pool_op};
+ static const nnet::conv_implementation implementation = nnet::conv_implementation::{implementation};
+ static const unsigned reuse_factor = {reuse};
+ typedef {accum_t.name} accum_t;
+}};\n"""
+
+pooling2d_config_template = """struct config{index} : nnet::pooling2d_config {{
+ static const unsigned in_height = {in_height};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_filt = {n_filt};
+ static const unsigned stride_height = {stride_height};
+ static const unsigned stride_width = {stride_width};
+ static const unsigned pool_height = {pool_height};
+ static const unsigned pool_width = {pool_width};
+
+ static const unsigned filt_height = pool_height;
+ static const unsigned filt_width = pool_width;
+ static const unsigned n_chan = n_filt;
+
+ static const unsigned out_height = {out_height};
+ static const unsigned out_width = {out_width};
+ static const unsigned pad_top = {pad_top};
+ static const unsigned pad_bottom = {pad_bottom};
+ static const unsigned pad_left = {pad_left};
+ static const unsigned pad_right = {pad_right};
+ static const bool count_pad = {count_pad};
+ static const nnet::Pool_Op pool_op = nnet::{pool_op};
+ static const nnet::conv_implementation implementation = nnet::conv_implementation::{implementation};
+ static const unsigned reuse_factor = {reuse};
+ typedef {accum_t.name} accum_t;
+}};\n"""
+
+global_pooling1d_config_template = """struct config{index} : nnet::pooling1d_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_filt = {n_filt};
+ static const nnet::Pool_Op pool_op = nnet::{pool_op};
+ static const unsigned reuse_factor = {reuse};
+ typedef {accum_t.name} accum_t;
+}};\n"""
+
+global_pooling2d_config_template = """struct config{index} : nnet::pooling2d_config {{
+ static const unsigned in_height = {in_height};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_filt = {n_filt};
+ static const nnet::Pool_Op pool_op = nnet::{pool_op};
+ static const unsigned reuse_factor = {reuse};
+ typedef {accum_t.name} accum_t;
+}};\n"""
+
+pooling1d_function_template = 'nnet::pooling1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+pooling2d_function_template = 'nnet::pooling2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+global_pooling1d_function_template = (
+ 'nnet::global_pooling1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+)
+global_pooling2d_function_template = (
+ 'nnet::global_pooling2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+)
+
+pooling_include_list = ['nnet_utils/nnet_pooling.h', 'nnet_utils/nnet_pooling_stream.h']
+
+
+class PoolingConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((Pooling1D, Pooling2D, GlobalPooling1D, GlobalPooling2D))
+ self.templates = {
+ 'Pooling1D': pooling1d_config_template,
+ 'Pooling2D': pooling2d_config_template,
+ 'GlobalPooling1D': global_pooling1d_config_template,
+ 'GlobalPooling2D': global_pooling2d_config_template,
+ }
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ return self.templates[node.class_name].format(**params)
+
+
+class PoolingFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((Pooling1D, Pooling2D, GlobalPooling1D, GlobalPooling2D), include_header=pooling_include_list)
+ self.templates = {
+ 'Pooling1D': pooling1d_function_template,
+ 'Pooling2D': pooling2d_function_template,
+ 'GlobalPooling1D': global_pooling1d_function_template,
+ 'GlobalPooling2D': global_pooling2d_function_template,
+ }
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'
+
+ return self.templates[node.class_name].format(**params)
diff --git a/hls4ml/backends/bambu/passes/quantization_templates.py b/hls4ml/backends/bambu/passes/quantization_templates.py
new file mode 100644
index 0000000000..0b04f7f02b
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/quantization_templates.py
@@ -0,0 +1,36 @@
+from hls4ml.backends.backend import get_backend
+from hls4ml.backends.bambu.passes.core_templates import (
+ batchnorm_config_template,
+ batchnorm_function_template,
+ batchnorm_include_list,
+)
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.optimizer.passes.qkeras import ApplyAlpha
+
+
+class ApplyAlphaConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(ApplyAlpha)
+ self.template = batchnorm_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ params['n_in'] = node.get_input_variable().size_cpp()
+ params['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('scale').type.precision
+ )
+
+ return self.template.format(**params)
+
+
+class ApplyAlphaFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(ApplyAlpha, include_header=batchnorm_include_list)
+ self.template = batchnorm_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['scale'] = node.get_weights('scale').name
+ params['bias'] = node.get_weights('bias').name
+
+ return self.template.format(**params)
diff --git a/hls4ml/backends/bambu/passes/recurrent_templates.py b/hls4ml/backends/bambu/passes/recurrent_templates.py
new file mode 100644
index 0000000000..02d3edd512
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/recurrent_templates.py
@@ -0,0 +1,518 @@
+from hls4ml.backends.backend import get_backend
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import GRU, LSTM, Bidirectional, Layer, TimeDistributed
+
+# recurrent multiplication template
+
+recr_mult_config_template_1 = """struct config{index} : nnet::dense_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned strategy = nnet::{strategy};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned n_zeros = {nzeros};
+ static const unsigned n_nonzeros = {nonzeros};
+ static const unsigned multiplier_limit = DIV_ROUNDUP(n_in * n_out, reuse_factor) - n_zeros / reuse_factor;
+ static const bool store_weights_in_bram = false;
+ typedef {accum_t.name} accum_t;
+ typedef {bias_t.name} bias_t;
+ typedef {weight_t.name} weight_t;
+ template
+ using kernel = {dense_function};
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+recr_mult_config_template_2 = """struct config{index} : nnet::dense_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned strategy = nnet::{strategy};
+ static const unsigned reuse_factor = {reuse};
+ static const unsigned n_zeros = {nzeros};
+ static const unsigned n_nonzeros = {nonzeros};
+ static const unsigned multiplier_limit = DIV_ROUNDUP(n_in * n_out, reuse_factor) - n_zeros / reuse_factor;
+ static const bool store_weights_in_bram = false;
+ typedef {accum_t.name} accum_t;
+ typedef {recurrent_bias_t.name} bias_t;
+ typedef {recurrent_weight_t.name} weight_t;
+ template
+ using kernel = {dense_function};
+ template
+ using product = nnet::product::{product_type};
+}};\n"""
+
+# activation templates
+
+activ_config_template = """struct {type}_config{index} : nnet::activ_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned table_size = {table_size};
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+ typedef {table_t.name} table_t;
+}};\n"""
+
+recr_activ_config_template = """struct {type}_config{index}_recr : nnet::activ_config {{
+ static const unsigned n_in = {n_in};
+ static const unsigned table_size = {table_size};
+ static const unsigned io_type = nnet::{iotype};
+ static const unsigned reuse_factor = {reuse};
+ typedef {table_t.name} table_t;
+}};\n"""
+
+# LSTM + GRU templates
+
+recr_config_template = """struct config{index} : nnet::{recr_type}_config {{
+ typedef {accum_t.name} accum_t;
+ typedef {weight_t.name} weight_t; // Matrix
+ typedef {recurrent_weight_t.name} recurrent_weight_t; // Matrix
+ typedef {bias_t.name} bias_t; // Vector
+ typedef {recurrent_bias_t.name} recurrent_bias_t; // Vector
+ typedef {config_mult_t1} mult_config1;
+ typedef {config_mult_t2} mult_config2;
+ typedef {recr_act_t} ACT_CONFIG_{RECR_TYPE};
+ template
+ using activation_recr = nnet::activation::{recurrent_activation};
+ typedef {act_t} ACT_CONFIG_T;
+ template
+ using activation = nnet::activation::{activation};
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned n_state = {n_state};
+ static const unsigned n_sequence = {n_sequence};
+ static const unsigned n_sequence_out = {n_sequence_out};
+ static const unsigned io_type = nnet::{strategy};
+ static const unsigned reuse_factor = {reuse};
+ static const bool store_weights_in_bram = false;
+ static const bool use_static = {static};
+ static const bool pytorch_order = {pytorch};
+}};\n"""
+
+# Bidirectional templates
+
+single_config_template = """struct config{index} : nnet::single_layer_config {{
+ typedef {accum_t.name} accum_t;
+ typedef {weight_t.name} weight_t; // Matrix
+ typedef {recurrent_weight_t.name} recurrent_weight_t; // Matrix
+ typedef {bias_t.name} bias_t; // Vector
+ typedef {recurrent_bias_t.name} recurrent_bias_t; // Vector
+ typedef {config_mult_t1} mult_config1;
+ typedef {config_mult_t2} mult_config2;
+ typedef {recr_act_t} ACT_CONFIG_{RECR_TYPE};
+ template
+ using activation_recr = nnet::activation::{recurrent_activation};
+ typedef {act_t} ACT_CONFIG_T;
+ template
+ using activation = nnet::activation::{activation};
+ static const unsigned n_in = {n_in};
+ static const unsigned n_state = {n_state};
+ static const unsigned n_mult = {n_mult};
+ static const bool pytorch_order = {pytorch};
+}};\n"""
+
+bidirectional_config_template = """struct config{index} : nnet::bidirectional_config {{
+ typedef {forward_t} FORWARD_CONFIG;
+ template
+ using RNNfunc_forward = nnet::{forward_layer};
+ typedef {backward_t} BACKWARD_CONFIG;
+ template
+ using RNNfunc_backward = nnet::{backward_layer};
+ static const unsigned n_in = {n_in};
+ static const unsigned n_out = {n_out};
+ static const unsigned n_sequence = {n_sequence};
+ static const unsigned n_sequence_out = {n_sequence_out};
+ static const unsigned io_type = nnet::{strategy};
+ static const unsigned reuse_factor = {reuse};
+ static const bool store_weights_in_bram = false;
+ static const bool use_static = {static};
+ static const bool pytorch_order = {pytorch};
+}};\n"""
+
+recr_function_template = 'nnet::{recr_type}_stack<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {wr}, {b}, {br});'
+recr_function_template_initial_states_lstm = 'nnet::{recr_type}_stack<{input_t}, {input2_t}, {input3_t}, {output_t}, {config}>({input}, {input2}, {input3}, {output}, {w}, {wr}, {b}, {br});' # noqa: E501
+recr_function_template_initial_states_gru = 'nnet::{recr_type}_stack<{input_t}, {input2_t}, {output_t}, {config}>({input}, {input2}, {output}, {w}, {wr}, {b}, {br});' # noqa: E501
+
+bidirectional_function_template = 'nnet::bidirectional_stack<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {wr}, {b}, {br}, {w_b}, {wr_b}, {b_b}, {br_b});' # noqa: E501
+
+recr_include_list = ['nnet_utils/nnet_recurrent.h']
+
+
+class RecurrentConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((LSTM, GRU))
+ self.template = recr_config_template
+ self.act_template = activ_config_template
+ self.recr_act_template = recr_activ_config_template
+ self.mult1_template = recr_mult_config_template_1
+ self.mult2_template = recr_mult_config_template_2
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ in_0, in_1 = map(str, node.get_input_variable().shape[:2])
+
+ params['n_in'] = in_1
+ params['n_sequence'] = in_0
+ if node.get_attr('return_sequences'):
+ out_0, out_1 = map(str, node.get_output_variable().shape[:2])
+ params['n_sequence_out'] = out_0
+ params['n_state'] = out_1
+ params['n_out'] = out_1
+ else:
+ params['n_sequence_out'] = 1
+ params['n_state'] = params['n_out'] = str(node.get_output_variable().shape[0])
+
+ params['config_mult_t1'] = f'config{node.index}_1'
+ params['config_mult_t2'] = f'config{node.index}_2'
+ params['recr_act_t'] = '{}_config{}_recr'.format(node.get_attr('recurrent_activation'), node.index)
+ params['act_t'] = '{}_config{}'.format(node.get_attr('activation'), node.index)
+ params['strategy'] = node.get_attr('strategy')
+ params['static'] = 'true' if node.attributes['static'] else 'false'
+ params['pytorch'] = 'true' if node.get_attr('pytorch', False) else 'false'
+ params['recr_type'] = node.class_name.lower()
+ params['RECR_TYPE'] = node.class_name
+
+ if node.class_name == 'LSTM':
+ n_recr_mult = 4
+ else: # GRU
+ n_recr_mult = 3
+
+ recr_config = self.template.format(**params)
+
+ act_params = self._default_config_params(node)
+ recr_act_params = self._default_config_params(node)
+
+ act_params['type'] = node.get_attr('activation')
+ recr_act_params['type'] = node.get_attr('recurrent_activation')
+ if node.get_attr('return_sequences'):
+ act_params['n_in'] = node.get_output_variable().shape[1]
+ recr_act_params['n_in'] = node.get_output_variable().shape[1] * (n_recr_mult - 1)
+ else:
+ act_params['n_in'] = node.get_output_variable().shape[0]
+ recr_act_params['n_in'] = node.get_output_variable().shape[0] * (n_recr_mult - 1)
+
+ act_config = self.act_template.format(**act_params)
+ recr_act_config = self.recr_act_template.format(**recr_act_params)
+
+ mult_params1 = self._default_config_params(node)
+ mult_params2 = self._default_config_params(node)
+
+ mult_params1['n_in'] = node.get_input_variable().shape[1]
+ if node.get_attr('return_sequences'):
+ mult_params1['n_out'] = node.get_output_variable().shape[1] * n_recr_mult
+ else:
+ mult_params1['n_out'] = node.get_output_variable().shape[0] * n_recr_mult
+ mult_params1['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('weight').type.precision
+ )
+ mult_params1['reuse'] = params['reuse']
+ mult_params1['index'] = str(node.index) + '_1'
+ mult_params1['nzeros'] = node.get_weights('weight').nzeros
+ mult_params1['nonzeros'] = node.get_weights('weight').nonzeros
+
+ namespace = params['namespace']
+
+ if node.get_attr('strategy').lower() == 'latency':
+ mult_params1['dense_function'] = 'nnet::DenseLatency'
+ elif node.get_attr('strategy').lower() == 'resource':
+ if int(mult_params1['reuse_factor']) <= int(mult_params1['n_in']):
+ mult_params1['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
+ else:
+ mult_params1['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
+ # The 3rd case is never used
+ elif node.get_attr('strategy').lower() == 'resource_unrolled':
+ mult_params1['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}_1'
+
+ if node.get_attr('return_sequences'):
+ mult_params2['n_in'] = node.get_output_variable().shape[1]
+ mult_params2['n_out'] = node.get_output_variable().shape[1] * n_recr_mult
+ else:
+ mult_params2['n_in'] = node.get_output_variable().shape[0]
+ mult_params2['n_out'] = node.get_output_variable().shape[0] * n_recr_mult
+ mult_params2['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights('recurrent_weight').type.precision
+ )
+ mult_params2['reuse'] = node.attributes['recurrent_reuse_factor']
+ mult_params2['index'] = str(node.index) + '_2'
+ mult_params2['nzeros'] = node.get_weights('recurrent_weight').nzeros
+ mult_params2['nonzeros'] = node.get_weights('recurrent_weight').nonzeros
+
+ if node.get_attr('strategy').lower() == 'latency':
+ mult_params2['dense_function'] = 'nnet::DenseLatency'
+ elif node.get_attr('strategy').lower() == 'resource':
+ if int(mult_params2['reuse_factor']) <= int(mult_params2['n_in']):
+ mult_params2['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
+ else:
+ mult_params2['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
+ # The 3rd case is never used
+ elif node.get_attr('strategy').lower() == 'resource_unrolled':
+ mult_params2['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}_2'
+
+ mult_config1 = self.mult1_template.format(**mult_params1)
+ mult_config2 = self.mult2_template.format(**mult_params2)
+
+ return mult_config1 + '\n' + mult_config2 + '\n' + recr_act_config + '\n' + act_config + '\n' + recr_config
+
+
+class BidirectionalConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Bidirectional)
+ self.template = bidirectional_config_template
+ self.layer_template = single_config_template
+ self.act_template = activ_config_template
+ self.recr_act_template = recr_activ_config_template
+ self.mult1_template = recr_mult_config_template_1
+ self.mult2_template = recr_mult_config_template_2
+
+ def format(self, node: Layer):
+ # ----- Bidirectional Layer Config -----#
+ params = self._default_config_params(node)
+
+ params['n_in'] = node.get_input_variable().shape[1]
+ params['n_sequence'] = node.get_input_variable().shape[0]
+ if node.get_attr('return_sequences'):
+ params['n_sequence_out'] = node.get_output_variable().shape[0]
+ else:
+ params['n_sequence_out'] = 1
+ params['n_out'] = node.get_attr('n_out')
+ params['strategy'] = node.get_attr('strategy')
+ params['static'] = 'true' if node.attributes['static'] else 'false'
+ params['pytorch'] = 'true' if node.get_attr('pytorch', False) else 'false'
+ params['forward_t'] = f'config{node.index}_forward'
+ params['backward_t'] = f'config{node.index}_backward'
+ params['forward_layer'] = node.get_attr('forward_class_name').lower() + '_class'
+ params['backward_layer'] = node.get_attr('backward_class_name').lower() + '_class'
+ if node.attributes['static']:
+ params['forward_layer'] += '_static'
+ params['backward_layer'] += '_static'
+
+ recr_config = self.template.format(**params)
+
+ # ----- Forward and Backward Layers Config -----#
+ result = ''
+ for d in ['forward', 'backward']:
+ if node.get_attr(f'{d}_class_name') == 'LSTM':
+ n_recr_mult = 4
+ else: # GRU
+ n_recr_mult = 3
+
+ # ----- Layer Config -----#
+ layer_params = self._default_config_params(node)
+ layer_params['n_in'] = params['n_in']
+ layer_params['pytorch'] = params['pytorch']
+ layer_params['n_state'] = node.get_attr(f'{d}_n_states')
+ layer_params['n_mult'] = 4
+ if node.get_attr(f'{d}_class_name').lower() == 'gru':
+ layer_params['n_mult'] = 3
+ layer_params['config_mult_t1'] = f'config{node.index}_1_{d[0]}'
+ layer_params['config_mult_t2'] = f'config{node.index}_2_{d[0]}'
+ layer_params['recr_act_t'] = '{}_config{}_recr'.format(
+ node.get_attr(f'{d}_recurrent_activation'), str(node.index) + f'_{d[0]}'
+ )
+ layer_params['act_t'] = '{}_config{}'.format(node.get_attr(f'{d}_activation'), str(node.index) + f'_{d[0]}')
+ layer_params['RECR_TYPE'] = node.get_attr(f'{d}_class_name')
+
+ layer_params['weight_t'] = layer_params[f'{d}_weight_t']
+ layer_params['recurrent_weight_t'] = layer_params[f'{d}_recurrent_weight_t']
+ layer_params['bias_t'] = layer_params[f'{d}_bias_t']
+ layer_params['recurrent_bias_t'] = layer_params[f'{d}_recurrent_bias_t']
+ layer_params['activation'] = layer_params[f'{d}_activation']
+ layer_params['recurrent_activation'] = layer_params[f'{d}_recurrent_activation']
+
+ layer_params['index'] = str(node.index) + f'_{d}'
+
+ layer_config = self.layer_template.format(**layer_params)
+
+ # ----- Activations Config -----#
+ act_params = self._default_config_params(node)
+ recr_act_params = self._default_config_params(node)
+
+ act_params['type'] = node.get_attr(f'{d}_activation')
+ recr_act_params['type'] = node.get_attr(f'{d}_recurrent_activation')
+ act_params['index'] = str(node.index) + f'_{d[0]}'
+ recr_act_params['index'] = str(node.index) + f'_{d[0]}'
+ act_params['n_in'] = node.get_attr(f'{d}_n_states')
+ recr_act_params['n_in'] = node.get_attr(f'{d}_n_states') * (n_recr_mult - 1)
+
+ act_config = self.act_template.format(**act_params)
+ recr_act_config = self.recr_act_template.format(**recr_act_params)
+
+ # ----- Mult Config -----#
+ mult_params1 = self._default_config_params(node)
+ mult_params2 = self._default_config_params(node)
+
+ mult_params1['n_in'] = node.get_input_variable().shape[1]
+ mult_params1['n_out'] = node.get_attr(f'{d}_n_states') * n_recr_mult
+ mult_params1['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights(f'{d}_weight').type.precision
+ )
+ mult_params1['reuse'] = params['reuse']
+ mult_params1['index'] = str(node.index) + f'_1_{d[0]}'
+ mult_params1['nzeros'] = node.get_weights(f'{d}_weight').nzeros
+ mult_params1['nonzeros'] = node.get_weights(f'{d}_weight').nonzeros
+
+ mult_params1['bias_t'] = mult_params1[f'{d}_bias_t']
+ mult_params1['weight_t'] = mult_params1[f'{d}_weight_t']
+ mult_params2['recurrent_bias_t'] = mult_params2[f'{d}_recurrent_bias_t']
+ mult_params2['recurrent_weight_t'] = mult_params2[f'{d}_recurrent_weight_t']
+
+ namespace = params['namespace']
+
+ if node.get_attr('strategy').lower() == 'latency':
+ mult_params1['dense_function'] = 'nnet::DenseLatency'
+ elif node.get_attr('strategy').lower() == 'resource':
+ if int(mult_params1[f'{d}_reuse_factor']) <= int(mult_params1['n_in']):
+ mult_params1['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
+ else:
+ mult_params1['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
+ # The 3rd case is never used
+ elif node.get_attr('strategy').lower() == 'resource_unrolled':
+ mult_params1['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}_1'
+
+ mult_params2['n_in'] = node.get_attr(f'{d}_n_states')
+ mult_params2['n_out'] = node.get_attr(f'{d}_n_states') * n_recr_mult
+ mult_params2['product_type'] = get_backend('bambu').product_type(
+ node.get_input_variable().type.precision, node.get_weights(f'{d}_recurrent_weight').type.precision
+ )
+ mult_params2['reuse'] = node.attributes[f'{d}_recurrent_reuse_factor']
+ mult_params2['index'] = str(node.index) + f'_2_{d[0]}'
+ mult_params2['nzeros'] = node.get_weights(f'{d}_recurrent_weight').nzeros
+ mult_params2['nonzeros'] = node.get_weights(f'{d}_recurrent_weight').nonzeros
+
+ if node.get_attr('strategy').lower() == 'latency':
+ mult_params2['dense_function'] = 'nnet::DenseLatency'
+ elif node.get_attr('strategy').lower() == 'resource':
+ if int(mult_params2[f'{d}_reuse_factor']) <= int(mult_params2['n_in']):
+ mult_params2['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
+ else:
+ mult_params2['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
+ # The 3rd case is never used
+ elif node.get_attr('strategy').lower() == 'resource_unrolled':
+ mult_params2['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}_2'
+
+ mult_config1 = self.mult1_template.format(**mult_params1)
+ mult_config2 = self.mult2_template.format(**mult_params2)
+
+ result += (
+ mult_config1 + '\n' + mult_config2 + '\n' + recr_act_config + '\n' + act_config + '\n' + layer_config + '\n'
+ )
+
+ return result + recr_config
+
+
+class RecurrentFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((LSTM, GRU), include_header=recr_include_list)
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ if params['pass_initial_states'] == 'true':
+ params['input2_t'] = node.get_input_variable(node.inputs[1]).type.name
+ params['input2'] = node.get_input_variable(node.inputs[1]).name
+ if node.class_name == 'LSTM':
+ params['input3'] = node.get_input_variable(node.inputs[2]).name
+ params['input3_t'] = node.get_input_variable(node.inputs[2]).type.name
+
+ params['w'] = node.get_weights('weight').name
+ params['b'] = node.get_weights('bias').name
+ params['wr'] = node.get_weights('recurrent_weight').name
+ params['br'] = node.get_weights('recurrent_bias').name
+ params['activation'] = node.get_attr('activation')
+ params['recurrent_activation'] = node.get_attr('recurrent_activation')
+ params['recr_type'] = node.class_name.lower()
+
+ if params['pass_initial_states'] == 'true':
+ if node.class_name == 'LSTM':
+ template = recr_function_template_initial_states_lstm
+ else:
+ template = recr_function_template_initial_states_gru
+ else:
+ template = recr_function_template
+
+ return template.format(**params)
+
+
+class BidirectionalFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((Bidirectional), include_header=recr_include_list)
+
+ def format(self, node):
+ params = self._default_function_params(node)
+
+ # TO DO: Add initial states functions for pytorch settings
+
+ params['w'] = node.get_weights('forward_weight').name
+ params['b'] = node.get_weights('forward_bias').name
+ params['wr'] = node.get_weights('forward_recurrent_weight').name
+ params['br'] = node.get_weights('forward_recurrent_bias').name
+ params['w_b'] = node.get_weights('backward_weight').name
+ params['b_b'] = node.get_weights('backward_bias').name
+ params['wr_b'] = node.get_weights('backward_recurrent_weight').name
+ params['br_b'] = node.get_weights('backward_recurrent_bias').name
+
+ template = bidirectional_function_template
+
+ return template.format(**params)
+
+
+time_distributed_config_template = """struct config{index} : nnet::time_distributed_config {{
+ static const unsigned dim = {dim};
+
+ static const unsigned n_time_steps = {n_time_steps};
+ static const unsigned in_height = {in_height};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+}};\n"""
+
+time_distributed_loop_start_template = """for (int ts = 0; ts < config{index}::n_time_steps; ts++) {{
+ {loop_mode}
+ nnet::read_time_step_{dim}d<{input_t}, {config}>(ts, {input}, {output});"""
+
+time_distributed_loop_end_template = """ nnet::write_time_step_{dim}d<{output_t}, {config}>(ts, {input}, {output});
+ }}"""
+
+time_distributed_include_list = ['nnet_utils/nnet_time_distributed.h']
+
+
+class TimeDistributedConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(TimeDistributed)
+ self.template = time_distributed_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+
+ input_shape = node.get_input_variable().shape
+ params['dim'] = len(input_shape)
+ if node.name.endswith('_end'):
+ params['dim'] += 1 # The input variable will be from the wrapped layer, without time dimension
+ params['in_height'] = input_shape[-3] if params['dim'] == 4 else 1
+ params['in_width'] = input_shape[-2] if params['dim'] >= 3 else 1
+ params['n_chan'] = input_shape[-1]
+
+ return self.template.format(**params)
+
+
+class TimeDistributedFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((TimeDistributed), include_header=time_distributed_include_list)
+ self.template_start = time_distributed_loop_start_template
+ self.template_end = time_distributed_loop_end_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+
+ input_shape = node.get_input_variable().shape
+ params['dim'] = len(input_shape)
+ if node.name.endswith('_end'):
+ params['dim'] += 1 # The input variable will be from the wrapped layer, without time dimension
+
+ loop_mode = node.get_attr('time_step_loop_parallelism')
+ if loop_mode == 'unroll':
+ params['loop_mode'] = '#pragma HLS UNROLL'
+ elif loop_mode == 'pipeline':
+ params['loop_mode'] = '#pragma HLS PIPELINE'
+ else:
+ params['loop_mode'] = ''
+
+ if node.attributes['wrapped_layer'].name == node.name + '_end':
+ return self.template_start.format(**params)
+ else:
+ return self.template_end.format(**params)
diff --git a/hls4ml/backends/bambu/passes/reshaping_templates.py b/hls4ml/backends/bambu/passes/reshaping_templates.py
new file mode 100644
index 0000000000..8bcd813b92
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/reshaping_templates.py
@@ -0,0 +1,205 @@
+from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
+from hls4ml.model.layers import Cropping1D, Cropping2D, Resize, Transpose, ZeroPadding1D, ZeroPadding2D
+from hls4ml.utils.transpose_utils import transpose_config_gen
+
+# ZeroPadding templates
+
+zeropad1d_config_template = """struct config{index} : nnet::padding1d_config {{
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned out_width = {out_width};
+ static const unsigned pad_left = {pad_left};
+ static const unsigned pad_right = {pad_right};
+}};\n"""
+
+zeropad2d_config_template = """struct config{index} : nnet::padding2d_config {{
+ static const unsigned in_height = {in_height};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned out_height = {out_height};
+ static const unsigned out_width = {out_width};
+ static const unsigned pad_top = {pad_top};
+ static const unsigned pad_bottom = {pad_bottom};
+ static const unsigned pad_left = {pad_left};
+ static const unsigned pad_right = {pad_right};
+}};\n"""
+
+zeropad1d_function_template = 'nnet::zeropad1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+zeropad2d_function_template = 'nnet::zeropad2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+
+padding_include_list = ['nnet_utils/nnet_padding.h', 'nnet_utils/nnet_padding_stream.h']
+
+
+class ZeroPaddingConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((ZeroPadding1D, ZeroPadding2D))
+ self.templates = {
+ 'ZeroPadding1D': zeropad1d_config_template,
+ 'ZeroPadding2D': zeropad2d_config_template,
+ }
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ return self.templates[node.class_name].format(**params)
+
+
+class ZeroPaddingFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((ZeroPadding1D, ZeroPadding2D), include_header=padding_include_list)
+ self.templates = {
+ 'ZeroPadding1D': zeropad1d_function_template,
+ 'ZeroPadding2D': zeropad2d_function_template,
+ }
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'
+
+ return self.templates[node.class_name].format(**params)
+
+
+# Resize templates
+
+resize_config_template = """struct config{index} : nnet::resize_config {{
+ static const unsigned height = {in_height};
+ static const unsigned width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned new_height = {out_height};
+ static const unsigned new_width = {out_width};
+}};\n"""
+
+resize_function_template = 'nnet::resize_{algorithm}<{input_t}, {config}>({input}, {output});'
+
+resize_include_list = ['nnet_utils/nnet_image.h', 'nnet_utils/nnet_image_stream.h']
+
+
+class ResizeConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Resize)
+ self.template = resize_config_template
+
+ def format(self, node):
+ params = self._default_config_params(node)
+
+ return self.template.format(**params)
+
+
+class ResizeFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__(Resize, include_header=resize_include_list)
+ self.template = resize_function_template
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['algorithm'] = node.get_attr('algorithm')
+
+ return self.template.format(**params)
+
+
+# Transpose templates
+
+
+transpose_include_list = ['nnet_utils/nnet_transpose.h', 'nnet_utils/nnet_transpose_stream.h']
+
+transpose_config_template = """struct {config_name} {{
+ static const unsigned dims = {dims};
+ static const unsigned N = {N};
+ static const unsigned* const from_shape;
+ static const unsigned* const to_shape;
+ static const unsigned* const perm;
+ static const unsigned* const perm_strides;
+}};
+
+unsigned {config_name}_from_shape[{dims}] = {{{from_shape}}};
+unsigned {config_name}_to_shape[{dims}] = {{{to_shape}}};
+unsigned {config_name}_perm[{dims}] = {{{perm}}};
+unsigned {config_name}_perm_strides[{dims}] = {{{perm_strides}}};
+
+const unsigned* const {config_name}::from_shape = {config_name}_from_shape;
+const unsigned* const {config_name}::to_shape = {config_name}_to_shape;
+const unsigned* const {config_name}::perm = {config_name}_perm;
+const unsigned* const {config_name}::perm_strides = {config_name}_perm_strides;
+"""
+
+transpose_function_template = 'nnet::transpose<{input_t}, {output_t}, {config_name}>({input}, {output});'
+
+
+class TransposeConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__(Transpose)
+
+ def format(self, node):
+ shape = tuple(node.get_input_variable().shape)
+ perm = tuple(node.get_attr('perm'))
+ name = f'config{node.index}'
+ conf = transpose_config_gen(name, shape, perm)
+ return transpose_config_template.format(**conf)
+
+
+class TransposeFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ self.template = transpose_function_template
+ super().__init__(Transpose, include_header=transpose_include_list)
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ params['config_name'] = f'config{node.index}'
+ return self.template.format(**params)
+
+
+# Cropping templates
+
+
+cropping1d_config_template = """struct config{index} : nnet::cropping1d_config {{
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned out_width = {out_width};
+ static const unsigned crop_left = {crop_left};
+ static const unsigned crop_right = {crop_right};
+}};\n"""
+
+cropping2d_config_template = """struct config{index} : nnet::cropping2d_config {{
+ static const unsigned in_height = {in_height};
+ static const unsigned in_width = {in_width};
+ static const unsigned n_chan = {n_chan};
+ static const unsigned out_height = {out_height};
+ static const unsigned out_width = {out_width};
+ static const unsigned crop_top = {crop_top};
+ static const unsigned crop_bottom = {crop_bottom};
+ static const unsigned crop_left = {crop_left};
+ static const unsigned crop_right = {crop_right};
+}};\n"""
+
+cropping1d_function_template = 'nnet::cropping1d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+cropping2d_function_template = 'nnet::cropping2d_{data_format}<{input_t}, {output_t}, {config}>({input}, {output});'
+
+cropping_include_list = ['nnet_utils/nnet_cropping.h', 'nnet_utils/nnet_cropping_stream.h']
+
+
+class CroppingConfigTemplate(LayerConfigTemplate):
+ def __init__(self):
+ super().__init__((Cropping1D, Cropping2D))
+ self.templates = {
+ 'Cropping1D': cropping1d_config_template,
+ 'Cropping2D': cropping2d_config_template,
+ }
+
+ def format(self, node):
+ params = self._default_config_params(node)
+ return self.templates[node.class_name].format(**params)
+
+
+class CroppingFunctionTemplate(FunctionCallTemplate):
+ def __init__(self):
+ super().__init__((Cropping1D, Cropping2D), include_header=cropping_include_list)
+ self.templates = {
+ 'Cropping1D': cropping1d_function_template,
+ 'Cropping2D': cropping2d_function_template,
+ }
+
+ def format(self, node):
+ params = self._default_function_params(node)
+ # Cropping1D doesn't have a data_format attribute
+ params['data_format'] = 'cf' if node.get_attr('data_format') == 'channels_first' else 'cl'
+
+ return self.templates[node.class_name].format(**params)
diff --git a/hls4ml/backends/bambu/passes/resource_strategy.py b/hls4ml/backends/bambu/passes/resource_strategy.py
new file mode 100644
index 0000000000..49dd09ba7e
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/resource_strategy.py
@@ -0,0 +1,60 @@
+import numpy as np
+
+from hls4ml.model.layers import (
+ GRU,
+ LSTM,
+ Bidirectional,
+ Conv1D,
+ Conv2D,
+ Dense,
+ SeparableConv1D,
+ SeparableConv2D,
+)
+from hls4ml.model.optimizer import OptimizerPass
+
+
+class ApplyResourceStrategy(OptimizerPass):
+ """Transposes the weights to use the dense_resource matrix multiply routine"""
+
+ def match(self, node):
+ node_matches = isinstance(node, (Dense, Conv1D, SeparableConv1D, Conv2D, SeparableConv2D, LSTM, GRU, Bidirectional))
+ is_resource_strategy = node.get_attr('strategy', '').lower() in ['resource', 'resource_unrolled']
+ already_transformed = node.get_attr('_weights_transposed', False) is True
+ return node_matches and is_resource_strategy and not already_transformed
+
+ def transform(self, model, node):
+ if isinstance(node, Dense):
+ node.weights['weight'].data = np.transpose(node.weights['weight'].data)
+ elif isinstance(node, Conv1D):
+ node.weights['weight'].data = np.transpose(node.weights['weight'].data, axes=[2, 0, 1]) # (W,C,F) => (F,W,C)
+ elif isinstance(node, SeparableConv1D):
+ node.weights['depthwise'].data = np.transpose(
+ node.weights['depthwise'].data, axes=[2, 0, 1]
+ ) # (W,C,F) => (F,W,C)
+ node.weights['pointwise'].data = np.transpose(
+ node.weights['pointwise'].data, axes=[2, 0, 1]
+ ) # (W,C,F) => (F,W,C)
+ elif isinstance(node, Conv2D):
+ node.weights['weight'].data = np.transpose(
+ node.weights['weight'].data, axes=[3, 0, 1, 2]
+ ) # (H,W,C,F) => (F,H,W,C)
+ elif isinstance(node, SeparableConv2D):
+ node.weights['depthwise'].data = np.transpose(
+ node.weights['depthwise'].data, axes=[3, 0, 1, 2]
+ ) # (H,W,C,F) => (F,H,W,C)
+ node.weights['pointwise'].data = np.transpose(
+ node.weights['pointwise'].data, axes=[3, 0, 1, 2]
+ ) # (H,W,C,F) => (F,H,W,C)
+ elif isinstance(node, (Bidirectional)):
+ for d in ['forward', 'backward']:
+ node.weights[f'{d}_weight'].data = np.transpose(node.weights[f'{d}_weight'].data)
+ node.weights[f'{d}_recurrent_weight'].data = np.transpose(node.weights[f'{d}_recurrent_weight'].data)
+ elif isinstance(node, (LSTM, GRU)):
+ node.weights['weight'].data = np.transpose(node.weights['weight'].data)
+ node.weights['recurrent_weight'].data = np.transpose(node.weights['recurrent_weight'].data)
+ else:
+ raise Exception(f'Unexpected layer {node.class_name} with resource strategy')
+
+ node.set_attr('_weights_transposed', True)
+
+ return False
diff --git a/hls4ml/backends/bambu/passes/transform_types.py b/hls4ml/backends/bambu/passes/transform_types.py
new file mode 100644
index 0000000000..0ae17cb680
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/transform_types.py
@@ -0,0 +1,52 @@
+from hls4ml.backends.bambu.bambu_types import (
+ BambuArrayVariableConverter,
+ BambuHLSTypeConverter,
+ BambuInplaceArrayVariableConverter,
+ BambuInplaceStreamVariableConverter,
+ BambuStreamVariableConverter,
+)
+from hls4ml.backends.fpga.fpga_types import APTypeConverter, StaticWeightVariableConverter
+from hls4ml.model.optimizer import GlobalOptimizerPass
+from hls4ml.model.types import InplaceTensorVariable
+
+
+class TransformTypes(GlobalOptimizerPass):
+ def __init__(self):
+ self.type_converter = BambuHLSTypeConverter(precision_converter=APTypeConverter())
+ self.array_var_converter = BambuArrayVariableConverter(type_converter=self.type_converter)
+ self.inplace_array_var_converter = BambuInplaceArrayVariableConverter(type_converter=self.type_converter)
+ self.stream_var_converter = BambuStreamVariableConverter(type_converter=self.type_converter)
+ self.inplace_stream_var_converter = BambuInplaceStreamVariableConverter(type_converter=self.type_converter)
+ self.weight_var_converter = StaticWeightVariableConverter(type_converter=self.type_converter)
+
+ def transform(self, model, node):
+ io_type = node.model.config.get_config_value('IOType')
+
+ for out_name, var in node.variables.items():
+ if io_type == 'io_stream':
+ if isinstance(var, InplaceTensorVariable):
+ new_var = self.inplace_stream_var_converter.convert(var)
+ else:
+ new_var = self.stream_var_converter.convert(var)
+ elif io_type == 'io_serial':
+ new_var = self.array_var_converter.convert(var, pragma='stream')
+ elif io_type == 'io_parallel':
+ if out_name in node.model.inputs:
+ # NOTE this needs to be changed to partition
+ new_var = self.array_var_converter.convert(var, pragma='reshape')
+ elif isinstance(var, InplaceTensorVariable):
+ new_var = self.inplace_array_var_converter.convert(var, pragma='')
+ else:
+ new_var = self.array_var_converter.convert(var, pragma='partition')
+ else:
+ raise Exception(f'Unknown IOType {io_type} in {node.name} ({node.__class__.__name__})')
+
+ node.set_attr(out_name, new_var)
+
+ for w_name, weight in node.weights.items():
+ new_weight = self.weight_var_converter.convert(weight)
+ node.set_attr(w_name, new_weight)
+
+ for t_name, type in node.types.items():
+ new_type = self.type_converter.convert(type)
+ node.set_attr(t_name, new_type)
diff --git a/hls4ml/backends/bambu/passes/unrolled_codegen.py b/hls4ml/backends/bambu/passes/unrolled_codegen.py
new file mode 100644
index 0000000000..53e72c8a40
--- /dev/null
+++ b/hls4ml/backends/bambu/passes/unrolled_codegen.py
@@ -0,0 +1,237 @@
+import math
+
+import numpy as np
+
+from hls4ml.model.layers import GRU, LSTM, Conv1D, Conv2D, Dense
+from hls4ml.model.optimizer import OptimizerPass
+from hls4ml.model.types import Source
+
+
+class GenerateUnrolledDenseResource(OptimizerPass):
+ """Generates C++ code for unrolled Dense resource"""
+
+ def match(self, node):
+ # Only apply to layers use that use Dense Matrix Multiplication
+ # TODO - Extend (& test) for Separable Conv / Depthwise Conv / Recurrent layers
+ layers_with_dense = (Dense, Conv1D, Conv2D, LSTM, GRU)
+
+ # Unrolled Dense mimics the hardware implementation of Resource strategy -> apply after Resource optimizer
+ weights_transposed = node.get_attr('_weights_transposed', False)
+
+ # RF = 1 will optimize DSPs anyway, so no need to unroll code
+ rf_gt_one = node.get_attr('reuse_factor', 1) > 1
+
+ # User requested unrolled implementation of Dense
+ is_unrolled = node.get_attr('strategy', 'latency') == 'resource_unrolled'
+
+ return isinstance(node, layers_with_dense) and weights_transposed and rf_gt_one and is_unrolled
+
+ def transform(self, model, node):
+ if isinstance(node, (LSTM, GRU)):
+ n_in, n_out, n_in_recr, n_out_recr = node.model.config.backend.get_layer_mult_size(node)
+
+ reuse_factor = node.get_attr('reuse_factor')
+ weights = node.weights['weight']
+ code_str = self._generate_unrolled_function(n_in, n_out, reuse_factor, weights, str(node.index) + '_1')
+ code_str = self._add_backend_specific_pragmas_to_generated_code(code_str, model.config.backend)
+ node.set_attr('resource_unrolled_dense_codegen_1', Source(code_str))
+
+ recr_reuse_factor = node.get_attr('recurrent_reuse_factor')
+ recr_weights = node.weights['recurrent_weight']
+ code_str = self._generate_unrolled_function(
+ n_in_recr, n_out_recr, recr_reuse_factor, recr_weights, str(node.index) + '_2'
+ )
+ code_str = self._add_backend_specific_pragmas_to_generated_code(code_str, model.config.backend)
+ node.set_attr('resource_unrolled_dense_codegen_2', Source(code_str))
+
+ else:
+ n_in, n_out = node.model.config.backend.get_layer_mult_size(node)
+ reuse_factor = node.get_attr('reuse_factor')
+ weights = node.weights['weight']
+
+ code_str = self._generate_unrolled_function(n_in, n_out, reuse_factor, weights, node.index)
+ code_str = self._add_backend_specific_pragmas_to_generated_code(code_str, model.config.backend)
+ node.set_attr('resource_unrolled_dense_codegen', Source(code_str))
+
+ def _generate_unrolled_function(self, n_in, n_out, reuse_factor, weights, function_suffix):
+ """
+ Generate a C++ function that mimics the Dense Resource implementation.
+
+ The HLS compiler produces suboptimal designs for Dense Resource when the weights processed by the same DSP are zero.
+ Latency strategy can optimize zero multiplications
+ Resource strategy, on the other hand, cannot.
+ When all the weights in the same BRAM block are zero, Vivado is unable to optimize it
+ With this (and additional TCL scripts) zero BRAM are optimized
+
+ Args:
+ node: Layer to generate code for
+ Returns:
+ generated_code: Generated C++ function (string)
+ """
+
+ # Variable instantiation and function pragmas
+ generated_code = (
+ 'template\n'
+ 'class dense_resource_unrolled_{suffix} : public DenseKernel {{{{\n'
+ ' public:\n'
+ ' static void dense(\n'
+ ' data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out],\n'
+ ' const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out],\n'
+ ' const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]\n'
+ ' ) {{{{\n'
+ ' //#pragma HLS pipeline II=CONFIG_T::reuse_factor\n'
+ '\n'
+ ' constexpr int block_factor = DIV_ROUNDUP(CONFIG_T::n_in * CONFIG_T::n_out, CONFIG_T::reuse_factor);\n'
+ ' #pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor\n'
+ ' {{weights_resource_pragma}}\n'
+ ' #pragma HLS ARRAY_PARTITION variable=biases complete\n'
+ '\n'
+ ' typename CONFIG_T::accum_t acc[CONFIG_T::n_out];\n'
+ ' #pragma HLS ARRAY_PARTITION variable=acc complete\n'
+ '\n'
+ ' InitAccum:\n'
+ ' #pragma HLS UNROLL\n'
+ ' for (int i = 0; i < CONFIG_T::n_out; i++) {{{{\n'
+ ' acc[i] = (typename CONFIG_T::accum_t) biases[i];\n'
+ ' }}}}\n'
+ '\n'
+ ).format(suffix=function_suffix)
+
+ # Unrolled multiplication, according to the three cases
+ if reuse_factor <= n_in:
+ mult_code = self._generate_unrolled_mult_code_rf_leq_nin(n_in, n_out, reuse_factor, weights)
+ elif reuse_factor > n_in and reuse_factor % n_in == 0:
+ mult_code = self._generate_unrolled_mult_code_rf_gt_nin_rem0(n_in, n_out, reuse_factor, weights)
+ else:
+ # This case shouldn't happen if my understanding of RF is correct
+ # The function fpga_backend._validate_reuse_factor() has assertion rf % n_in == 0 or rf < n_in
+ raise Exception('Not implemented...')
+
+ # Write output
+ generated_code += mult_code + '\n'
+ generated_code += (
+ ' Result:\n'
+ ' #pragma HLS UNROLL\n'
+ ' for (int i = 0; i < CONFIG_T::n_out; i++) {{\n'
+ ' res[i] = cast(acc[i]);\n'
+ ' }}\n'
+ ' }}\n'
+ '}};\n'
+ )
+
+ return generated_code
+
+ def _generate_unrolled_mult_code_rf_leq_nin(self, n_in, n_out, reuse_factor, weights):
+ # Function constants
+ mult_factor = min(n_in, reuse_factor)
+ block_factor = int(math.ceil(n_in * n_out / reuse_factor))
+ mult_limit = int(math.ceil(n_in * n_out / mult_factor))
+ mult_scale = mult_limit // n_out
+
+ # Zero DSPs are the DSP blocks that always have zero input
+ # In this case, it is the number of rows in the transposed and reshaped weight matrix
+ # The new shape is (parallel_mult, reuse_factor)
+ zeros = np.sum(~weights.data.reshape(block_factor, reuse_factor).any(1))
+
+ # Used to pad the code to make it human-readable
+ indent = ' '
+
+ # Generate unrolled multiplications
+ mult_code = f'{indent * 2}#pragma HLS ALLOCATION operation instances=mul limit={mult_limit - zeros}\n'
+ mult_code += f'{indent * 2}MULT: {{{{\n'
+
+ for ir in range(reuse_factor):
+ acc_step = 0
+ out_index = 0
+ w_index = ir
+ in_index = ir
+
+ mult_code += f'{indent * 3}M{ir}: {{{{\n'
+ for _ in range(block_factor):
+ if weights.data.flatten()[w_index] != 0:
+ mult_code += (
+ f'{indent * 4}acc[{out_index}] += '
+ 'static_cast'
+ '(CONFIG_T::template product::'
+ f'product(data[{in_index}], weights[{w_index}]));\n'
+ )
+
+ w_index += reuse_factor
+ in_index += reuse_factor
+ if in_index >= n_in:
+ in_index = ir
+ if acc_step + 1 >= mult_scale:
+ acc_step = 0
+ out_index += 1
+ else:
+ acc_step += 1
+
+ mult_code += f'{indent * 3}}}}}\n'
+
+ mult_code += f'{indent * 2}}}}}\n'
+
+ return mult_code
+
+ def _generate_unrolled_mult_code_rf_gt_nin_rem0(self, n_in, n_out, reuse_factor, weights):
+ # Function constants
+ mult_factor = min(n_in, reuse_factor)
+ block_factor = int(math.ceil(n_in * n_out / reuse_factor))
+ mult_limit = int(math.ceil(n_in * n_out / mult_factor))
+
+ # Zero DSPs are the DSP blocks that always have zero input
+ # In this case, it is the number of rows in the transposed and reshaped weight matrix
+ # The new shape is (parallel_mult, reuse_factor)
+ zeros = np.sum(~weights.data.reshape(block_factor, reuse_factor).any(1))
+
+ # Used to pad the code to make it human-readable
+ indent = ' '
+
+ # Generate out indices
+ outidx = [0] * reuse_factor
+ outstep = 0
+ outscale = reuse_factor // n_in
+ for ir in range(reuse_factor):
+ outidx[ir] = outstep
+ if (ir + 1) % n_in == 0:
+ outstep += 1
+
+ # Define variables
+ in_index = 0
+
+ # Generate unrolled multiplications
+ mult_code = f'{indent * 2}#pragma HLS ALLOCATION operation instances=mul limit={mult_limit - zeros}\n'
+ mult_code += f'{indent * 2}MULT: {{{{\n'
+
+ for ir in range(reuse_factor):
+ w_index = ir
+ out_index = outidx[ir]
+
+ mult_code += f'{indent * 3}M{ir}: {{{{\n'
+ for _ in range(block_factor):
+ if weights.data.flatten()[w_index] != 0:
+ mult_code += (
+ f'{indent * 4}acc[{int(out_index)}] += '
+ 'static_cast'
+ '(CONFIG_T::template product::'
+ f'product(data[{in_index}], weights[{w_index}]));\n'
+ )
+
+ w_index += reuse_factor
+ if w_index > n_in * n_out:
+ break
+ out_index += outscale
+ mult_code += f'{indent * 3}}}}}\n'
+
+ in_index += 1
+ if in_index >= n_in:
+ in_index = 0
+
+ mult_code += f'{indent * 2}}}}}\n'
+
+ return mult_code
+
+ def _add_backend_specific_pragmas_to_generated_code(self, code, backend):
+ weights_resource_pragma = ''
+ code = code.format(weights_resource_pragma=weights_resource_pragma)
+
+ return code
diff --git a/hls4ml/backends/bambu_accelerator/__init__.py b/hls4ml/backends/bambu_accelerator/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/hls4ml/backends/bambu_accelerator/bambu_accelerator_backend.py b/hls4ml/backends/bambu_accelerator/bambu_accelerator_backend.py
new file mode 100644
index 0000000000..5f5b9dcdde
--- /dev/null
+++ b/hls4ml/backends/bambu_accelerator/bambu_accelerator_backend.py
@@ -0,0 +1,510 @@
+import abc
+import inspect
+import json
+import os
+import pathlib
+import re
+import shutil
+import subprocess
+from warnings import warn
+
+from hls4ml.backends.bambu.bambu_backend import BambuBackend
+from hls4ml.model.flow import register_flow
+from hls4ml.model.optimizer import get_backend_passes
+from hls4ml.model.optimizer.optimizer import extract_optimizers_from_path
+
+_RTL_TEMPLATES_DIR = pathlib.Path(__file__).parent.parent.parent / 'templates' / 'bambu_accelerator' / 'rtl'
+
+_RTL_FILES: dict[str, list[str]] = {
+ 'parallel': ['top_parallel.v', 'AXISlaveParallel.v', 'axi_addr.v', 'skidbuffer.v'],
+ 'stream': ['top_stream.v', 'AXISlaveStream.v', 'sfifo.v', 'axi_addr.v', 'skidbuffer.v'],
+}
+
+
+def _read_n_words(project_dir: str) -> tuple[int, int]:
+ fw_dir = pathlib.Path(project_dir) / 'firmware'
+ pat = re.compile(r'\bN_(IN|OUT)\s*=\s*(\d+)')
+ n_in = n_out = None
+ for hfile in sorted(fw_dir.glob('*.h')):
+ for m in pat.finditer(hfile.read_text()):
+ if m.group(1) == 'IN':
+ n_in = int(m.group(2))
+ else:
+ n_out = int(m.group(2))
+ if n_in is not None and n_out is not None:
+ break
+ if n_in is None or n_out is None:
+ raise ValueError(f'Could not find N_IN/N_OUT in {fw_dir}/*.h')
+ return n_in, n_out
+
+
+def _read_fixed_point(project_dir: str) -> dict:
+ """Return {'in': {'total':, 'int':}, 'out': ...} from firmware/defines.h.
+
+ The manifest's data_widths carry the *container* width (e.g. 64-bit BRAM
+ words); the host needs the ap_fixed value format to encode and
+ decode them.
+
+ hls4ml emits input_t / result_t in two shapes and both must work:
+ io_parallel writes a flat `typedef ap_fixed input_t;`, io_stream
+ writes `struct input_t { typedef ap_fixed value_type; ... };`.
+ Handling only the flat form silently breaks every stream build.
+ """
+ defines = pathlib.Path(project_dir) / 'firmware' / 'defines.h'
+ text = defines.read_text()
+ _FIXED = r'ap_fixed\s*<\s*(\d+)\s*,\s*(-?\d+)\s*[,>]'
+ out = {}
+ for key, name in (('in', 'input_t'), ('out', 'result_t')):
+ # \b before the name so fc1_result_t never shadows result_t.
+ m = re.search(r'typedef\s+' + _FIXED + r'[^;]*\b' + name + r'\s*;', text)
+ if m is None:
+ # struct form: take value_type from inside this struct's braces
+ sm = re.search(r'\bstruct\s+' + name + r'\s*\{(.*?)\}\s*;', text, re.DOTALL)
+ if sm is not None:
+ m = re.search(r'typedef\s+' + _FIXED + r'\s*value_type\s*;', sm.group(1))
+ if m is None:
+ raise ValueError(
+ f'No ap_fixed definition for {name} in {defines} '
+ f'(looked for a flat typedef and a struct with a value_type typedef)'
+ )
+ out[key] = {'total': int(m.group(1)), 'int': int(m.group(2))}
+ return out
+
+
+def _build_manifest(
+ project_dir: str, project_name: str, clock_period_ns: float, flow: str, device: str | None = None
+) -> dict:
+ """Parse HLS output, write manifest.json, return manifest dict."""
+ from hls4ml.backends.bambu_accelerator.wrapper import (
+ build_rename_map,
+ extract_bram_depths,
+ extract_data_widths,
+ parse_module,
+ )
+
+ project_path = pathlib.Path(project_dir)
+ vfiles = list(project_path.glob(f'{project_name}_float.v'))
+ if not vfiles:
+ raise FileNotFoundError(f'No {project_name}_float.v in {project_dir}')
+ module_name, port_names, port_decls = parse_module(vfiles[0].read_text())
+ rename_map = build_rename_map(port_names, port_decls, flow)
+ in_dw, out_dw = extract_data_widths(port_names, port_decls, flow)
+ n_in, n_out = _read_n_words(project_dir)
+ bram_slots = extract_bram_depths(port_names, port_decls, flow)
+ mem_files = [p.name for p in sorted(project_path.glob('*.mem'))]
+
+ # Complete P&R file list: the private side adds exactly these, no
+ # flow-specific knowledge needed there. panda_libtech.v is Bambu's cell
+ # library (MUX2_GATE, *_FU primitives) — required by every Bambu netlist,
+ # otherwise NxMap elaboration fails with blackbox errors.
+ rtl_files = _RTL_FILES[flow] + [f'{project_name}_float.v', 'panda_libtech.v']
+
+ manifest = {
+ 'manifest_version': 1,
+ 'project_name': project_name,
+ 'top_module': 'myproject',
+ 'hls_top': f'{project_name}_float',
+ 'flow': flow,
+ 'clock_period_ns': float(clock_period_ns),
+ 'clock_mhz': round(1000.0 / float(clock_period_ns), 6),
+ 'device': device,
+ 'ports': {v: k for k, v in rename_map.items()},
+ 'data_widths': {'in': in_dw, 'out': out_dw},
+ 'n_words': {'in': n_in, 'out': n_out},
+ # BRAM depth (2**address_width); parallel only, null for stream.
+ 'bram_slots': bram_slots,
+ # ap_fixed value format; data_widths above is the container width.
+ 'fixed_point': _read_fixed_point(project_dir),
+ 'mem_files': mem_files,
+ 'rtl_files': rtl_files,
+ }
+ with open(project_path / 'manifest.json', 'w') as f:
+ json.dump(manifest, f, indent=2)
+ return manifest
+
+
+def _write_verilog_wrapper(project_dir: str, project_name: str, flow: str) -> None:
+ """Append 'myproject' wrapper to *_float.v if not already present.
+
+ flow comes from IOType via build() — the single driver of the
+ parallel/stream decision (wrapper, RTL copy, and manifest all agree).
+ """
+ from hls4ml.backends.bambu_accelerator.wrapper import (
+ generate_wrapper_verilog,
+ parse_module,
+ )
+
+ project_path = pathlib.Path(project_dir)
+ vfiles = list(project_path.glob(f'{project_name}_float.v'))
+ if not vfiles:
+ raise FileNotFoundError(f'No {project_name}_float.v in {project_dir}')
+ vfile = vfiles[0]
+ content = vfile.read_text()
+ if re.search(r'\bmodule\s+myproject\s*[(\s]', content):
+ return
+ module_name, port_names, port_decls = parse_module(content)
+ wrapper = generate_wrapper_verilog(module_name, port_names, port_decls, flow)
+ with open(vfile, 'a') as f:
+ f.write('\n' + wrapper)
+
+
+def _copy_rtl_templates(project_dir: str, flow: str) -> None:
+ """Copy RTL glue files for the given flow into project_dir."""
+ dst = pathlib.Path(project_dir)
+ for fname in _RTL_FILES[flow]:
+ src = _RTL_TEMPLATES_DIR / fname
+ if not src.exists():
+ raise FileNotFoundError(f'RTL template missing: {src}')
+ shutil.copy2(src, dst / fname)
+
+
+_PLL_BEGIN = '// HLS4ML PLL BEGIN (autogenerated for ClockPeriod; do not edit inside)'
+_PLL_END = '// HLS4ML PLL END'
+_NX_INTERNAL_REF_MHZ = 375.0 # NG-ULTRA internal reference oscillator
+
+
+def _render_pll_block(clock_period_ns: float) -> str:
+ """Solve an NX_PLL_U config for 1000/clock_period_ns MHz and adapt the
+ generated Verilog to the template's fixed interface: output on wire
+ clk_50_0mhz (historical name — the AXI glue and the private constraint
+ net rg~clk_50_0mhz key on it), reset ~rstn_i."""
+ try:
+ from hls4ml.backends.bambu_accelerator import pll_solver
+
+ target_mhz = 1000.0 / float(clock_period_ns)
+ block = pll_solver.solve_pll(_NX_INTERNAL_REF_MHZ, [target_mhz], use_external_oscillator=False)
+ except ImportError as exc:
+ raise RuntimeError(
+ f'ClockPeriod={clock_period_ns} ns needs a generated PLL, which requires '
+ f'ortools: pip install hls4ml[nanoxplore]. (A silent 50 MHz clock with a '
+ f'{clock_period_ns} ns constraint is exactly the mismatch this guards against.)'
+ ) from exc
+ block = re.sub(r'\bclk_[0-9][0-9_]*mhz\b', 'clk_50_0mhz', block)
+ block = re.sub(r'\.R\s+\(rst\)', '.R (~rstn_i)', block)
+ return block
+
+
+def _patch_pll(project_dir: str, flow: str, clock_period_ns: float) -> None:
+ """Replace the marked PLL region in the project-dir copy of the top file.
+ No-op at 50 MHz (the committed default block IS the 50 MHz solution)."""
+ if abs(1000.0 / float(clock_period_ns) - 50.0) < 1e-6:
+ return
+ top = pathlib.Path(project_dir) / f'top_{flow}.v'
+ content = top.read_text()
+ if content.count(_PLL_BEGIN) != 1 or content.count(_PLL_END) != 1:
+ raise RuntimeError(f'PLL markers missing or duplicated in {top}')
+ head, rest = content.split(_PLL_BEGIN, 1)
+ _, tail = rest.split(_PLL_END, 1)
+ block = _render_pll_block(clock_period_ns)
+ top.write_text(f'{head}{_PLL_BEGIN}\n{block}\n{_PLL_END}{tail}')
+
+
+_PARAMS_BEGIN = '// HLS4ML PARAMS BEGIN (autogenerated; do not edit inside)'
+_PARAMS_END = '// HLS4ML PARAMS END'
+
+
+def _patch_params(project_dir: str, flow: str, data_widths: dict, n_words: dict, bram_slots: dict | None) -> None:
+ """Replace the marked HLS_* localparam region in the project-dir top file.
+
+ Without this the copied template keeps the reference project's hardcoded
+ geometry: the tutorial jet-tagger shipped with the reference's
+ HLS_OUT_N_WORDS=4 against its own 5 outputs, so the slave advertised 3
+ total read beats where the firmware asked for 4 and the burst hung
+ (bsp_rc=4 DMA timeout on the board).
+
+ HLS_*_N_WORDS means different things per flow, so the two are routed
+ separately: parallel takes the BRAM DEPTH, stream the ELEMENT COUNT.
+
+ Parallel must use the depth even though both AXI slaves compute
+ N_BEATS_* as ceil(N_WORDS / WORDS_PER_BEAT) and the element count looks
+ more principled. Measured on the jet-tagger (5 outputs, 3-bit address):
+ N_WORDS=5 makes NxMap delete the whole datapath (144 LUT4 / 0 carry vs
+ 3329 / 7794 at N_WORDS=8). See extract_bram_depths.
+
+ A consequence worth knowing downstream: at the depth, the slave places its
+ cycle-counter beat at N_BEATS_OUT = ceil(depth / words_per_beat), which is
+ past the last beat holding real data. A reader that locates the counter
+ from the element count instead will read padding. The geometry needed to
+ find it is published in the manifest (`bram_slots`), so consumers can
+ derive the same beat index this function used.
+
+ Do NOT "fix" that by giving the RTL a separate element-count parameter for
+ the beat math while leaving N_WORDS at the depth: that combination has been
+ measured to break the datapath on NG-ULTRA. Validate any change here on
+ hardware -- offline signals (simulation, resource counts, timing) do not
+ catch it.
+
+ Stream keeps the element count: AXISlaveStream's LAST_BEAT_*_VALID is
+ genuinely a count of valid words in the final beat, and that path is
+ verified working on hardware.
+ """
+ from hls4ml.backends.bambu_accelerator.wrapper import generate_top_localparams
+
+ slots = n_words if flow == 'stream' else bram_slots
+ top = pathlib.Path(project_dir) / f'top_{flow}.v'
+ content = top.read_text()
+ if content.count(_PARAMS_BEGIN) != 1 or content.count(_PARAMS_END) != 1:
+ raise RuntimeError(f'PARAMS markers missing or duplicated in {top}')
+ head, rest = content.split(_PARAMS_BEGIN, 1)
+ _, tail = rest.split(_PARAMS_END, 1)
+ block = generate_top_localparams(data_widths['in'], slots['in'], data_widths['out'], slots['out'], flow)
+ top.write_text(f'{head}{_PARAMS_BEGIN}\n{block}\n{_PARAMS_END}{tail}')
+
+
+class BambuAcceleratorBackend(BambuBackend, abc.ABC):
+ """Extends BambuBackend with a float wrapper around the ap_fixed HLS core.
+
+ Generates additional files:
+ - firmware/_float.h / .cpp : flat float interface
+ - _float_test.cpp : float testbench
+ - build_tb_float_exe.sh : builds float testbench executable
+ - build_lib.sh (overwritten) : includes float wrapper in shared lib
+ """
+
+ def __init__(self, name='BambuAccelerator'):
+ super(BambuBackend, self).__init__(name=name)
+ self._register_layer_attributes()
+ self._register_flows()
+
+ def _init_file_optimizers(self):
+ """Override to walk the full MRO and deduplicate passes directories.
+
+ The default implementation only looks at direct bases + self:
+ [*self.__class__.__bases__, self.__class__]
+ For BambuBackend that's [FPGABackend, BambuBackend], so fpga/passes/ is
+ correctly included. For BambuAcceleratorBackend the direct base is
+ BambuBackend (not FPGABackend), so fpga/passes/ would be skipped and
+ passes like clone_output/reshape_stream would be missing.
+
+ We walk the full MRO (excluding object) and deduplicate by path so
+ bambu/passes/ is only loaded once even though both BambuBackend and
+ BambuAcceleratorBackend share the same directory.
+ """
+ file_optimizers = {}
+ seen_paths = set()
+ mro_classes = [c for c in type(self).__mro__ if c is not object]
+ for cls in mro_classes:
+ try:
+ opt_path = os.path.dirname(inspect.getfile(cls)) + '/passes'
+ except (TypeError, OSError):
+ continue
+ if opt_path in seen_paths:
+ continue
+ seen_paths.add(opt_path)
+ module_path = cls.__module__[: cls.__module__.rfind('.')] + '.passes'
+ cls_optimizers = extract_optimizers_from_path(opt_path, module_path, self)
+ file_optimizers.update(cls_optimizers)
+ return file_optimizers
+
+ def _register_flows(self):
+ bk = self.name.lower() # 'bambuaccelerator'
+
+ initializers = self._get_layer_initializers()
+ init_flow = register_flow('init_layers', initializers, requires=['optimize'], backend=self.name)
+
+ streaming_passes = [
+ f'{bk}:inplace_stream_flatten',
+ f'{bk}:reshape_stream',
+ f'{bk}:clone_output',
+ f'{bk}:insert_zero_padding_before_conv1d',
+ f'{bk}:insert_zero_padding_before_conv2d',
+ f'{bk}:broadcast_stream',
+ ]
+ streaming_flow = register_flow('streaming', streaming_passes, requires=[init_flow], backend=self.name)
+
+ quantization_passes = [
+ f'{bk}:merge_batch_norm_quantized_tanh',
+ f'{bk}:quantize_dense_output',
+ 'fuse_consecutive_batch_normalization',
+ f'{bk}:xnor_pooling',
+ ]
+ quantization_flow = register_flow('quantization', quantization_passes, requires=[init_flow], backend=self.name)
+
+ optimization_passes = [
+ f'{bk}:remove_final_reshape',
+ f'{bk}:optimize_pointwise_conv',
+ f'{bk}:inplace_parallel_reshape',
+ f'{bk}:inplace_stream_flatten',
+ f'{bk}:skip_softmax',
+ f'{bk}:fix_softmax_table_size',
+ 'infer_precision_types',
+ f'{bk}:distributed_arithmetic_codegen',
+ f'{bk}:distributed_arithmetic_einsum_codegen',
+ f'{bk}:fuse_quantizer_into_d_a_layers',
+ f'{bk}:process_fixed_point_quantizer_layer',
+ ]
+ optimization_flow = register_flow('optimize', optimization_passes, requires=[init_flow], backend=self.name)
+
+ bambu_types = [
+ f'{bk}:transform_types',
+ f'{bk}:register_bram_weights',
+ f'{bk}:generate_conv_streaming_instructions',
+ f'{bk}:apply_resource_strategy',
+ f'{bk}:generate_conv_im2col',
+ f'{bk}:generate_unrolled_dense_resource',
+ f'{bk}:set_pipeline_style',
+ f'{bk}:d_a_latency_dense_template',
+ f'{bk}:d_a_latency_conv_template',
+ ]
+ bambu_types_flow = register_flow('specific_types', bambu_types, requires=[init_flow], backend=self.name)
+
+ templates = self._get_layer_templates()
+ template_flow = register_flow('apply_templates', self._get_layer_templates, requires=[init_flow], backend=self.name)
+
+ writer_passes = ['make_stamp', f'{bk}:write_hls']
+ self._writer_flow = register_flow('write', writer_passes, requires=[f'{bk}:ip'], backend=self.name)
+
+ fifo_depth_opt_passes = [f'{bk}:fifo_depth_optimization'] + writer_passes
+ register_flow('fifo_depth_optimization', fifo_depth_opt_passes, requires=[f'{bk}:ip'], backend=self.name)
+
+ all_passes = get_backend_passes(self.name)
+
+ extras = [
+ opt_pass
+ for opt_pass in all_passes
+ if opt_pass
+ not in initializers
+ + streaming_passes
+ + quantization_passes
+ + optimization_passes
+ + bambu_types
+ + templates
+ + writer_passes
+ + fifo_depth_opt_passes
+ ]
+
+ if len(extras) > 0:
+ for opt in extras:
+ warn(f'WARNING: Optimizer "{opt}" is not part of any flow and will not be executed.')
+
+ ip_flow_requirements = [
+ 'optimize',
+ init_flow,
+ streaming_flow,
+ quantization_flow,
+ optimization_flow,
+ bambu_types_flow,
+ template_flow,
+ ]
+
+ self._default_flow = register_flow('ip', None, requires=ip_flow_requirements, backend=self.name)
+
+ def _get_hls_sources(self, project_name):
+ # myproject.cpp is still emitted for the CPU testbench (myproject_test.cpp)
+ # but it must NOT be handed to Bambu: the accelerator writer inlines the full
+ # layer pipeline directly into myproject_float.cpp, so Bambu only synthesises
+ # a single top-level function with all DATAFLOW streams visible at the outer
+ # scope.
+ return [os.path.join('firmware', f'{project_name}_float.cpp')]
+
+ def _get_top_fname(self, project_name):
+ return f'{project_name}_float'
+
+ def _get_cosim_testbench(self, project_name):
+ return f'{project_name}_float_test.cpp'
+
+ def build(
+ self,
+ model,
+ *,
+ reset=False,
+ csim=True,
+ synth=True,
+ cosim=False,
+ validation=False,
+ export=False,
+ vsynth=False,
+ fifo_opt=False,
+ log_to_stdout=True,
+ args=None,
+ env=None,
+ run_kwargs=None,
+ bitstream=False,
+ ):
+ """Run build, using the float testbench for C-simulation.
+
+ Mirrors the BambuBackend.build() signature exactly. csim=True compiles
+ and runs the float testbench (*_float_tb.exe) instead of the ap_fixed
+ one. All other arguments (synth, cosim, vsynth, …) behave identically
+ to BambuBackend.build().
+ """
+ # Replicate the parent's validation pre-check here because we suppress
+ # csim=True when calling super() and the parent would otherwise raise.
+ if validation and not (csim and cosim):
+ raise ValueError('To validate C simulation & RTL simulation equality, csim and cosim must both be run.')
+
+ # Pass csim=False so the parent never builds/runs the ap_fixed testbench.
+ # Pass validation=False so the parent's own pre-check doesn't fire.
+ result = super().build(
+ model,
+ reset=reset,
+ csim=False,
+ synth=synth,
+ cosim=cosim,
+ validation=False,
+ export=export,
+ vsynth=vsynth,
+ fifo_opt=fifo_opt,
+ log_to_stdout=log_to_stdout,
+ args=args,
+ env=env,
+ run_kwargs=run_kwargs,
+ )
+
+ if csim:
+ self._build_float_testbench_exe(model)
+
+ project_name = model.config.get_project_name()
+ stamp = model.config.get_config_value('Stamp')
+ project_dir = model.config.get_output_dir()
+
+ ret = subprocess.run(
+ [f'./{project_name}-{stamp}_float_tb.exe'],
+ cwd=project_dir,
+ capture_output=True,
+ text=True,
+ )
+ if ret.returncode != 0:
+ raise RuntimeError(f'Float testbench execution failed:\nSTDOUT:\n{ret.stdout}\nSTDERR:\n{ret.stderr}')
+
+ if synth:
+ project_name = model.config.get_project_name()
+ project_dir = model.config.get_output_dir()
+ clock_period_ns = float(model.config.get_config_value('ClockPeriod') or 50.0)
+ io_type = model.config.get_config_value('IOType') or 'io_parallel'
+ flow = 'stream' if io_type == 'io_stream' else 'parallel'
+ device = getattr(self, '_default_device', None)
+
+ _write_verilog_wrapper(project_dir, project_name, flow)
+ _copy_rtl_templates(project_dir, flow)
+ _patch_pll(project_dir, flow, clock_period_ns)
+ manifest = _build_manifest(project_dir, project_name, clock_period_ns, flow, device)
+ # In-memory values only — manifest.json is write-only from this side.
+ _patch_params(project_dir, flow, manifest['data_widths'], manifest['n_words'], manifest['bram_slots'])
+
+ if bitstream:
+ metrics = self._generate_bitstream(model, project_dir, manifest)
+ result['metrics_nx'] = metrics
+
+ return result
+
+ @abc.abstractmethod
+ def _generate_bitstream(self, model, project_dir: str, manifest: dict) -> dict:
+ """Run vendor P&R and return a metrics dict.
+
+ Must shell out — vendor toolchains cannot be imported into the hls4ml process.
+ Raises RuntimeError if the vendor tool fails or is not installed.
+ """
+
+ def _build_float_testbench_exe(self, model):
+ ret = subprocess.run(
+ ['bash', 'build_tb_float_exe.sh'],
+ text=True,
+ capture_output=True,
+ cwd=model.config.get_output_dir(),
+ )
+ if ret.returncode != 0:
+ raise RuntimeError(
+ f'Failed to build float testbench executable for "{model.config.get_project_name()}":\n'
+ f'STDOUT:\n{ret.stdout}\nSTDERR:\n{ret.stderr}'
+ )
diff --git a/hls4ml/backends/bambu_accelerator/pll_solver.py b/hls4ml/backends/bambu_accelerator/pll_solver.py
new file mode 100644
index 0000000000..986540984f
--- /dev/null
+++ b/hls4ml/backends/bambu_accelerator/pll_solver.py
@@ -0,0 +1,536 @@
+def solve_pll(
+ input_freq_mhz: float, targets: list[float], use_external_oscillator: bool = False, hdl: str = 'verilog'
+) -> str:
+ """
+ Solves the PLL configuration problem using Google OR-Tools.
+
+ Args:
+ input_freq_mhz (float): The input frequency in MHz.
+ targets (list[float]): A list of target frequencies in MHz.
+ use_external_oscillator (bool): Whether to use an external oscillator (default: False).
+ hdl (str): Output HDL language, 'verilog' or 'vhdl' (default: 'verilog').
+
+ Returns:
+ str: Generated HDL code string if a valid configuration is found, otherwise an error message.
+ """
+ from ortools.sat.python import cp_model # deferred: only PLL generation needs ortools
+
+ # --- 1. HARDWARE CONSTANTS ---
+ PFD_MIN, PFD_MAX = 10.0, 50.0
+ VCO_MIN, VCO_MAX = 300.0, 800.0
+ MAX_PLLS = 7
+
+ # Divider Maps
+ # Dynamic (5 slots): Ratios -> Code
+ dyn_ratios = {
+ 2: 0,
+ 4: 1,
+ 6: 2,
+ 8: 3,
+ 10: 4,
+ 20: 5,
+ 40: 6,
+ 60: 7,
+ 80: 8,
+ 100: 9,
+ 200: 10,
+ 400: 11,
+ 600: 12,
+ 800: 13,
+ 1000: 14,
+ 2000: 15,
+ }
+
+ # Static (1 slot each): Ratio -> Code
+ # S1..S4
+ static_maps = [
+ {(2 * i + 3): i for i in range(8)}, # S1
+ {(2 * i + 5): i for i in range(8)}, # S2
+ {(2 * i + 7): i for i in range(8)}, # S3
+ {(2 * i + 9): i for i in range(8)}, # S4
+ ]
+
+ print(f'--- Optimizing for: {targets} MHz ---')
+
+ # --- 2. GENERATE CANDIDATE VCOs ---
+ # Find all VCOs that can generate at least one target
+ candidate_vcos = {} # vco_freq -> {ref, fbk, pfd}
+
+ # Pre-calculate valid divider ratios
+ valid_ratios = set(dyn_ratios.keys())
+ for m in static_maps:
+ valid_ratios.update(m.keys())
+
+ # Brute force valid VCOs (filtered by target feasibility)
+ for t in targets:
+ feasible = False
+ for r in valid_ratios:
+ vco = t * r
+ if VCO_MIN <= vco <= VCO_MAX:
+ # Check if generatable from input
+ # Try Ref Divs 1..32
+ for ref_val in range(32):
+ ref_div = ref_val + 1
+ pfd = input_freq_mhz / ref_div
+ if PFD_MIN <= pfd <= PFD_MAX:
+ # Check multiplier
+ # vco = pfd * 2 * (fbk+1)
+ mult = vco / pfd
+ # Check if mult is even integer (approx)
+ if abs(mult % 2) < 1e-5 or abs(mult % 2 - 2) < 1e-5:
+ k = int(round(mult / 2))
+ fbk_val = k - 1
+ if 0 <= fbk_val <= 127:
+ # Found valid VCO
+ if vco not in candidate_vcos:
+ candidate_vcos[vco] = {'ref': ref_val, 'fbk': fbk_val, 'pfd': pfd}
+ feasible = True
+ break # Found one config for this VCO, sufficient
+ if not feasible:
+ raise Exception(
+ f'Hardware cannot generate {t} MHz. Check that the target frequency has correct precision. '
+ 'We check for tol < 1e-5, i.e. 33.33332 < 100.0/3.0 < 33.33334.'
+ )
+
+ vco_list = sorted(candidate_vcos.keys())
+ print(f'Search Space: {len(vco_list)} Candidate VCO frequencies')
+
+ if not vco_list:
+ raise Exception('Hardware cannot generate these frequencies.')
+
+ # --- 3. CP-SAT MODEL ---
+ model = cp_model.CpModel()
+
+ # Variables
+ # x[p, v]: PLL p uses VCO v
+ x = {}
+ for p in range(MAX_PLLS):
+ for v_idx, _vco in enumerate(vco_list):
+ x[p, v_idx] = model.NewBoolVar(f'x_{p}_{v_idx}')
+
+ # assign[t, p, v]: Target t assigned to PLL p on VCO v
+ assign = {}
+ for t_idx in range(len(targets)):
+ for p in range(MAX_PLLS):
+ for v_idx in range(len(vco_list)):
+ assign[t_idx, p, v_idx] = model.NewBoolVar(f'assign_{t_idx}_{p}_{v_idx}')
+
+ # Port allocation variables: use_S1[t,p,v], use_Dyn[t,p,v]...
+ # We simplify: For a specific (t, p, v) assignment, we must pick a valid port type.
+ use_dyn = {}
+ use_stat = {} # Key: (t, p, v, s_idx) s_idx 0..3
+
+ for t_idx, t in enumerate(targets):
+ for p in range(MAX_PLLS):
+ for v_idx, vco in enumerate(vco_list):
+ ratio = int(round(vco / t))
+ if ratio == 0:
+ continue
+ # Validate that this ratio actually produces the target frequency
+ if abs(vco / ratio - t) > 1e-5:
+ continue
+
+ # Create Bool vars for allocation if ratio valid
+ # Dynamic
+ if ratio in dyn_ratios:
+ use_dyn[t_idx, p, v_idx] = model.NewBoolVar(f'dyn_{t_idx}_{p}_{v_idx}')
+
+ # Static
+ for s_idx in range(4):
+ if ratio in static_maps[s_idx]:
+ use_stat[t_idx, p, v_idx, s_idx] = model.NewBoolVar(f'stat_{s_idx}_{t_idx}_{p}_{v_idx}')
+
+ # --- CONSTRAINTS ---
+
+ # 1. Coverage: Each target assigned exactly once
+ for t_idx in range(len(targets)):
+ model.Add(sum(assign[t_idx, p, v] for p in range(MAX_PLLS) for v in range(len(vco_list))) == 1)
+
+ # 2. PLL Configuration: Max 1 VCO per PLL
+ for p in range(MAX_PLLS):
+ model.Add(sum(x[p, v] for v in range(len(vco_list))) <= 1)
+
+ # 3. Link Assignment to Configuration
+ for t_idx in range(len(targets)):
+ for p in range(MAX_PLLS):
+ for v_idx in range(len(vco_list)):
+ # If target assigned to (p,v), PLL p MUST use v
+ model.Add(assign[t_idx, p, v_idx] <= x[p, v_idx])
+
+ # 4. Link Assignment to Ports
+ # assign[t,p,v] == use_dyn + sum(use_stat)
+ port_vars = []
+ if (t_idx, p, v_idx) in use_dyn:
+ port_vars.append(use_dyn[t_idx, p, v_idx])
+ for s_idx in range(4):
+ if (t_idx, p, v_idx, s_idx) in use_stat:
+ port_vars.append(use_stat[t_idx, p, v_idx, s_idx])
+
+ if not port_vars:
+ # Ratio invalid for this VCO -> Force 0
+ model.Add(assign[t_idx, p, v_idx] == 0)
+ else:
+ model.Add(assign[t_idx, p, v_idx] == sum(port_vars))
+
+ # 5. Port Capacity Constraints
+ for p in range(MAX_PLLS):
+ for v_idx in range(len(vco_list)):
+ # Max 5 Dynamic per PLL/VCO
+ dyn_vars = [use_dyn[t, p, v_idx] for t in range(len(targets)) if (t, p, v_idx) in use_dyn]
+ model.Add(sum(dyn_vars) <= 5)
+
+ # Max 1 per Static slot per PLL/VCO
+ for s_idx in range(4):
+ stat_vars = [use_stat[t, p, v_idx, s_idx] for t in range(len(targets)) if (t, p, v_idx, s_idx) in use_stat]
+ model.Add(sum(stat_vars) <= 1)
+
+ # --- OBJECTIVE ---
+ # Minimize sum of active PLLs
+ # Active PLL = Sum of x[p,v] across all v (since max 1 v per p)
+ pll_active_vars = []
+ for p in range(MAX_PLLS):
+ is_active = model.NewBoolVar(f'active_{p}')
+ model.Add(sum(x[p, v] for v in range(len(vco_list))) == is_active)
+ pll_active_vars.append(is_active)
+
+ model.Minimize(sum(pll_active_vars))
+
+ # --- SOLVE ---
+ solver = cp_model.CpSolver()
+ status = solver.Solve(model)
+
+ if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
+ if status == cp_model.OPTIMAL:
+ print(f'Optimal Solution Found: {solver.ObjectiveValue()} PLL(s) required. ')
+ else:
+ print(f'Feasible Solution Found: {solver.ObjectiveValue()} PLL(s) required.')
+ if hdl == 'vhdl':
+ return generate_vhdl_ortools(
+ solver,
+ x,
+ assign,
+ use_stat,
+ use_dyn,
+ targets,
+ vco_list,
+ candidate_vcos,
+ dyn_ratios,
+ static_maps,
+ use_external_oscillator,
+ )
+ else:
+ return generate_verilog_ortools(
+ solver,
+ x,
+ assign,
+ use_stat,
+ use_dyn,
+ targets,
+ vco_list,
+ candidate_vcos,
+ dyn_ratios,
+ static_maps,
+ use_external_oscillator,
+ )
+ else:
+ raise Exception('No valid configuration found.')
+
+
+def generate_vhdl_ortools(
+ solver,
+ x,
+ assign,
+ use_stat,
+ use_dyn,
+ targets,
+ vco_list,
+ vco_configs,
+ dyn_ratios,
+ static_maps,
+ use_external_oscillator=False,
+):
+ """
+ Generates VHDL code based on the solved PLL configuration.
+
+ Args:
+ solver (cp_model.CpSolver): The solver instance containing the solution.
+ x (dict): Dictionary of PLL-VCO assignment variables.
+ assign (dict): Dictionary of Target-PLL-VCO assignment variables.
+ use_stat (dict): Dictionary of static port usage variables.
+ use_dyn (dict): Dictionary of dynamic port usage variables.
+ targets (list[float]): List of target frequencies.
+ vco_list (list[float]): List of candidate VCO frequencies.
+ vco_configs (dict): Dictionary mapping VCO frequencies to their configuration (ref, fbk, pfd).
+ dyn_ratios (dict): Dictionary mapping dynamic divider ratios to configuration codes.
+ static_maps (list[dict]): Per static port, a dict mapping static divider ratios to configuration codes.
+ use_external_oscillator (bool): Whether to use an external oscillator (default: False).
+
+ Returns:
+ str: A string containing the generated VHDL component instantiation for the PLLs.
+ """
+ vhdl = ''
+ MAX_PLLS = 7
+
+ pll_counter = 1
+
+ for p in range(MAX_PLLS):
+ # Find active VCO
+ active_v_idx = -1
+ for v_idx in range(len(vco_list)):
+ if solver.Value(x[p, v_idx]) == 1:
+ active_v_idx = v_idx
+ break
+
+ if active_v_idx == -1:
+ continue # PLL unused
+
+ vco_freq = vco_list[active_v_idx]
+ cfg = vco_configs[vco_freq]
+
+ # Gather Assignments
+ # We need to map target -> Specific Port Name & Code
+ g = {}
+ # Init defaults
+ for i in range(1, 5):
+ g[f'clk_outdiv{i}'] = (0, 3)
+ for i in range(1, 6):
+ g[f'clk_outdivd{i}'] = (0, 4)
+ ports_map = {}
+ for i in range(1, 5):
+ ports_map[f'CLK_DIV{i}'] = 'open'
+ for i in range(1, 6):
+ ports_map[f'CLK_DIVD{i}'] = 'open'
+
+ covered_targets = []
+ used_dyn_slots = []
+
+ for t_idx, t in enumerate(targets):
+ # Check if assigned here
+ if solver.Value(assign[t_idx, p, active_v_idx]) == 0:
+ continue
+
+ covered_targets.append(t)
+ ratio = int(round(vco_freq / t))
+
+ # Determine which port was selected by solver
+ port_found = False
+
+ # Check Static
+ for s_idx in range(4):
+ if (t_idx, p, active_v_idx, s_idx) in use_stat:
+ if solver.Value(use_stat[t_idx, p, active_v_idx, s_idx]) == 1:
+ # Assigned to Static s_idx+1
+ port = f'CLK_DIV{s_idx + 1}'
+ code = static_maps[s_idx][ratio]
+ g[f'clk_outdiv{s_idx + 1}'] = (code, 3)
+ ports_map[port] = f'clk_{int(t)}mhz'
+ port_found = True
+ break
+
+ # Check Dynamic
+ if not port_found and (t_idx, p, active_v_idx) in use_dyn:
+ if solver.Value(use_dyn[t_idx, p, active_v_idx]) == 1:
+ # Assigned to Dynamic. Find first free slot.
+ # Since solver guaranteed count <= 5, we just greedy fill slots.
+ for d in range(1, 6):
+ if d not in used_dyn_slots:
+ used_dyn_slots.append(d)
+ port = f'CLK_DIVD{d}'
+ code = dyn_ratios[ratio]
+ g[f'clk_outdivd{d}'] = (code, 4)
+ ports_map[port] = f'clk_{str(t).replace(".", "_")}mhz'
+ port_found = True
+ break
+
+ # Append VHDL Block
+ vhdl += f"""
+ -- PLL {pll_counter}: VCO={vco_freq:.1f}MHz (PFD={cfg['pfd']:.1f}MHz)
+ -- Generates: {covered_targets} MHz
+ PLL_{pll_counter}: NX_PLL_U
+ generic map (
+ location => "", -- default location
+ ref_osc_on => '{'1' if not use_external_oscillator else '0'}',
+ use_pll => '1',
+ ext_fbk_on => '0', -- use internal feedback
+ fbk_delay_on => '0',
+ fbk_delay => to_bitvector(conv_std_logic_vector(0,6)),
+
+ -- ref_intdiv register = divide ratio (hardware-validated golden: 375/15 = 25 MHz PFD);
+ -- cfg['ref'] stores ratio-1 internally, so emit +1.
+ ref_intdiv => to_bitvector(conv_std_logic_vector({cfg['ref'] + 1},5)),
+ fbk_intdiv => to_bitvector(conv_std_logic_vector({cfg['fbk']},7)),
+
+ clk_outdiv1 => to_bitvector(conv_std_logic_vector({g['clk_outdiv1'][0]},{g['clk_outdiv1'][1]})),
+ clk_outdiv2 => to_bitvector(conv_std_logic_vector({g['clk_outdiv2'][0]},{g['clk_outdiv2'][1]})),
+ clk_outdiv3 => to_bitvector(conv_std_logic_vector({g['clk_outdiv3'][0]},{g['clk_outdiv3'][1]})),
+ clk_outdiv4 => to_bitvector(conv_std_logic_vector({g['clk_outdiv4'][0]},{g['clk_outdiv4'][1]})),
+
+ clk_outdivd1 => to_bitvector(conv_std_logic_vector({g['clk_outdivd1'][0]},{g['clk_outdivd1'][1]})),
+ clk_outdivd2 => to_bitvector(conv_std_logic_vector({g['clk_outdivd2'][0]},{g['clk_outdivd2'][1]})),
+ clk_outdivd3 => to_bitvector(conv_std_logic_vector({g['clk_outdivd3'][0]},{g['clk_outdivd3'][1]})),
+ clk_outdivd4 => to_bitvector(conv_std_logic_vector({g['clk_outdivd4'][0]},{g['clk_outdivd4'][1]})),
+ clk_outdivd5 => to_bitvector(conv_std_logic_vector({g['clk_outdivd5'][0]},{g['clk_outdivd5'][1]}))
+ )
+ port map (
+ REF => '{'0' if not use_external_oscillator else 'ref_clk'}',
+ FBK => '0', -- use internal feedback
+ R => rst, -- active high reset
+ VCO => open,
+ LDFO => open,
+ REFO => open,
+ OSC => open, -- optionally connect to get the internal oscillator
+ CAL_LOCKED => open,
+ PLL_LOCKED => locked_{pll_counter},
+ CLK_DIV1 => {ports_map['CLK_DIV1']},
+ CLK_DIV2 => {ports_map['CLK_DIV2']},
+ CLK_DIV3 => {ports_map['CLK_DIV3']},
+ CLK_DIV4 => {ports_map['CLK_DIV4']},
+ CLK_DIVD1 => {ports_map['CLK_DIVD1']},
+ CLK_DIVD2 => {ports_map['CLK_DIVD2']},
+ CLK_DIVD3 => {ports_map['CLK_DIVD3']},
+ CLK_DIVD4 => {ports_map['CLK_DIVD4']},
+ CLK_DIVD5 => {ports_map['CLK_DIVD5']}
+ );
+"""
+ pll_counter += 1
+
+ return vhdl
+
+
+def generate_verilog_ortools(
+ solver,
+ x,
+ assign,
+ use_stat,
+ use_dyn,
+ targets,
+ vco_list,
+ vco_configs,
+ dyn_ratios,
+ static_maps,
+ use_external_oscillator=False,
+):
+ """
+ Generates Verilog code based on the solved PLL configuration.
+
+ Args: same as generate_vhdl_ortools.
+
+ Returns:
+ str: A string containing the generated Verilog instantiation for the PLLs.
+ """
+ verilog = ''
+ MAX_PLLS = 7
+
+ pll_counter = 1
+
+ for p in range(MAX_PLLS):
+ # Find active VCO
+ active_v_idx = -1
+ for v_idx in range(len(vco_list)):
+ if solver.Value(x[p, v_idx]) == 1:
+ active_v_idx = v_idx
+ break
+
+ if active_v_idx == -1:
+ continue # PLL unused
+
+ vco_freq = vco_list[active_v_idx]
+ cfg = vco_configs[vco_freq]
+
+ # Gather assignments
+ g = {}
+ for i in range(1, 5):
+ g[f'clk_outdiv{i}'] = (0, 3)
+ for i in range(1, 6):
+ g[f'clk_outdivd{i}'] = (0, 4)
+ ports_map = {}
+ for i in range(1, 5):
+ ports_map[f'CLK_DIV{i}'] = '()'
+ for i in range(1, 6):
+ ports_map[f'CLK_DIVD{i}'] = '()'
+
+ covered_targets = []
+ used_dyn_slots = []
+
+ for t_idx, t in enumerate(targets):
+ if solver.Value(assign[t_idx, p, active_v_idx]) == 0:
+ continue
+
+ covered_targets.append(t)
+ ratio = int(round(vco_freq / t))
+
+ port_found = False
+
+ # Check Static
+ for s_idx in range(4):
+ if (t_idx, p, active_v_idx, s_idx) in use_stat:
+ if solver.Value(use_stat[t_idx, p, active_v_idx, s_idx]) == 1:
+ port = f'CLK_DIV{s_idx + 1}'
+ code = static_maps[s_idx][ratio]
+ g[f'clk_outdiv{s_idx + 1}'] = (code, 3)
+ ports_map[port] = f'(clk_{int(t)}mhz)'
+ port_found = True
+ break
+
+ # Check Dynamic
+ if not port_found and (t_idx, p, active_v_idx) in use_dyn:
+ if solver.Value(use_dyn[t_idx, p, active_v_idx]) == 1:
+ for d in range(1, 6):
+ if d not in used_dyn_slots:
+ used_dyn_slots.append(d)
+ port = f'CLK_DIVD{d}'
+ code = dyn_ratios[ratio]
+ g[f'clk_outdivd{d}'] = (code, 4)
+ ports_map[port] = f'(clk_{str(t).replace(".", "_")}mhz)'
+ port_found = True
+ break
+
+ ref_conn = "1'b0" if not use_external_oscillator else 'ref_clk'
+ ref_osc = "1'b1" if not use_external_oscillator else "1'b0"
+
+ verilog += f"""
+// PLL {pll_counter}: VCO={vco_freq:.1f}MHz (PFD={cfg['pfd']:.1f}MHz)
+// Generates: {covered_targets} MHz
+NX_PLL_U #(
+ .location (""),
+ .ref_osc_on ({ref_osc}),
+ .use_pll (1'b1),
+ .ext_fbk_on (1'b0),
+ .fbk_delay_on (1'b0),
+ .fbk_delay (6'd0),
+ // ref_intdiv register = divide ratio (hardware-validated golden: 375/15 = 25 MHz PFD);
+ // cfg['ref'] stores ratio-1 internally, so emit +1.
+ .ref_intdiv (5'd{cfg['ref'] + 1}),
+ .fbk_intdiv (7'd{cfg['fbk']}),
+ .clk_outdiv1 (3'd{g['clk_outdiv1'][0]}),
+ .clk_outdiv2 (3'd{g['clk_outdiv2'][0]}),
+ .clk_outdiv3 (3'd{g['clk_outdiv3'][0]}),
+ .clk_outdiv4 (3'd{g['clk_outdiv4'][0]}),
+ .clk_outdivd1 (4'd{g['clk_outdivd1'][0]}),
+ .clk_outdivd2 (4'd{g['clk_outdivd2'][0]}),
+ .clk_outdivd3 (4'd{g['clk_outdivd3'][0]}),
+ .clk_outdivd4 (4'd{g['clk_outdivd4'][0]}),
+ .clk_outdivd5 (4'd{g['clk_outdivd5'][0]})
+) PLL_{pll_counter} (
+ .REF ({ref_conn}),
+ .FBK (1'b0),
+ .R (rst), // active high reset
+ .VCO (),
+ .LDFO (),
+ .REFO (),
+ .OSC (),
+ .CAL_LOCKED(),
+ .PLL_LOCKED(locked_{pll_counter}),
+ .CLK_DIV1 {ports_map['CLK_DIV1']},
+ .CLK_DIV2 {ports_map['CLK_DIV2']},
+ .CLK_DIV3 {ports_map['CLK_DIV3']},
+ .CLK_DIV4 {ports_map['CLK_DIV4']},
+ .CLK_DIVD1 {ports_map['CLK_DIVD1']},
+ .CLK_DIVD2 {ports_map['CLK_DIVD2']},
+ .CLK_DIVD3 {ports_map['CLK_DIVD3']},
+ .CLK_DIVD4 {ports_map['CLK_DIVD4']},
+ .CLK_DIVD5 {ports_map['CLK_DIVD5']}
+);"""
+ pll_counter += 1
+
+ return verilog
diff --git a/hls4ml/backends/bambu_accelerator/wrapper.py b/hls4ml/backends/bambu_accelerator/wrapper.py
new file mode 100644
index 0000000000..77aa3c4f21
--- /dev/null
+++ b/hls4ml/backends/bambu_accelerator/wrapper.py
@@ -0,0 +1,339 @@
+"""
+Verilog wrapper generation for Bambu-generated HLS modules.
+
+Pure-Python library: no hls4ml imports, no CLI entry point.
+
+Typical usage::
+
+ content = Path('myproject_float.v').read_text()
+ module_name, port_names, port_decls = parse_module(content)
+ flow = detect_flow(port_names)
+ wrapper_verilog = generate_wrapper_verilog(module_name, port_names, port_decls, flow)
+ in_dw, out_dw = extract_data_widths(port_names, port_decls, flow)
+ slots = extract_bram_depths(port_names, port_decls, flow) # None for 'stream'
+ localparams = generate_top_localparams(in_dw, n_in, out_dw, n_out, flow)
+"""
+
+import re
+
+# PortDecls maps port name -> (direction, width_str)
+# direction : 'input' | 'output'
+# width_str : e.g. '[15:0]' or '' for a 1-bit port
+PortDecls = dict[str, tuple[str, str]]
+
+# BRAM interface suffixes used to detect and group memory ports (io_parallel)
+_BRAM_SUFFIXES: list[str] = [
+ '_address0',
+ '_address1',
+ '_ce0',
+ '_ce1',
+ '_we0',
+ '_we1',
+ '_d0',
+ '_d1',
+ '_q0',
+ '_q1',
+]
+
+# AXI-Stream interface suffixes used to detect and group streaming ports (io_stream).
+# Bambu emits TDATA/TVALID/TREADY on ports named like `_TDATA`.
+_AXIS_SUFFIXES: list[str] = ['_TDATA', '_TVALID', '_TREADY']
+
+# Standard HLS control ports — never renamed in the wrapper
+_STANDARD_PORTS: frozenset[str] = frozenset({'clock', 'reset', 'start_port', 'done_port'})
+
+
+def _parse_width(width_str: str) -> int:
+ """Return the bit-width encoded in a Verilog range string.
+
+ '[N:M]' -> N - M + 1. Empty string -> 1 (scalar wire).
+ """
+ if not width_str:
+ return 1
+ m = re.match(r'\[(\d+):(\d+)\]', width_str.strip())
+ return int(m.group(1)) - int(m.group(2)) + 1 if m else 1
+
+
+def parse_module(content: str) -> tuple[str, list[str], PortDecls]:
+ """Parse the last non-'myproject' module in a Verilog string.
+
+ Returns:
+ module_name : name of the HLS top-level module
+ port_names : port names in declaration order
+ port_decls : mapping from port name to (direction, width_str)
+ """
+ all_mods = list(re.finditer(r'\bmodule\s+(\w+)\s*\(', content))
+ if not all_mods:
+ raise ValueError('No module declarations found')
+
+ target = next((m for m in reversed(all_mods) if m.group(1) != 'myproject'), None)
+ if target is None:
+ raise ValueError("Only a 'myproject' module was found — nothing to wrap")
+
+ module_name = target.group(1)
+
+ # Find the matching closing parenthesis of the port list
+ paren_start = target.end() - 1 # position of '('
+ depth, i = 0, paren_start
+ while i < len(content):
+ if content[i] == '(':
+ depth += 1
+ elif content[i] == ')':
+ depth -= 1
+ if depth == 0:
+ break
+ i += 1
+ close_paren = i
+
+ # Extract ordered port names from the header, stripping comments
+ raw = content[paren_start + 1 : close_paren]
+ raw = re.sub(r'//[^\n]*', '', raw)
+ raw = re.sub(r'/\*.*?\*/', '', raw, flags=re.DOTALL)
+ port_names = [p.strip() for p in raw.split(',') if p.strip()]
+
+ # Extract direction/width from the module body (old-style port declarations)
+ body_start = close_paren + 1
+ end_match = re.search(r'\bendmodule\b', content[body_start:])
+ body = content[body_start : body_start + end_match.start()] if end_match else content[body_start:]
+
+ decl_pat = re.compile(
+ r'^\s*(input|output)\s*(?:wire\s*)?(?:reg\s*)?(?:signed\s*)?\s*(\[[^\]]*\])?\s*(\w+)\s*;',
+ re.MULTILINE,
+ )
+ port_decls: PortDecls = {m.group(3): (m.group(1), (m.group(2) or '').strip()) for m in decl_pat.finditer(body)}
+
+ return module_name, port_names, port_decls
+
+
+def detect_flow(port_names: list[str]) -> str:
+ """Classify the HLS IP by inspecting its port suffixes.
+
+ Returns 'stream' if any port ends in an AXI-Stream suffix
+ (_TDATA/_TVALID/_TREADY), else 'parallel'.
+ """
+ for port in port_names:
+ for suffix in _AXIS_SUFFIXES:
+ if port.endswith(suffix):
+ return 'stream'
+ return 'parallel'
+
+
+def _build_rename_map_bram(port_names: list[str]) -> dict[str, str]:
+ """Return a rename map {original_port: generic_port} for BRAM memory ports.
+
+ BRAM groups are identified by their suffix pattern and classified as:
+ - input group: carries _q0/_q1 (data read from BRAM into HLS)
+ - output group: carries _d0/_d1/_we* (data written from HLS to BRAM)
+
+ Multiple groups of the same class are indexed: input0_*, input1_*, ...
+ Standard control ports are not renamed.
+ """
+ groups: dict[str, dict[str, str]] = {}
+ for port in port_names:
+ if port in _STANDARD_PORTS:
+ continue
+ for suffix in _BRAM_SUFFIXES:
+ if port.endswith(suffix):
+ prefix = port[: -len(suffix)]
+ groups.setdefault(prefix, {})[suffix] = port
+ break
+
+ input_groups = sorted(p for p, m in groups.items() if {'_q0', '_q1'} & m.keys())
+ output_groups = sorted(p for p, m in groups.items() if {'_d0', '_d1', '_we0', '_we1'} & m.keys())
+
+ rename_map: dict[str, str] = {}
+ for class_groups, base in ((input_groups, 'input'), (output_groups, 'output')):
+ for idx, prefix in enumerate(class_groups):
+ new_prefix = base if len(class_groups) == 1 else f'{base}{idx}'
+ for suffix, old_name in groups[prefix].items():
+ rename_map[old_name] = f'{new_prefix}{suffix}'
+
+ return rename_map
+
+
+def _build_rename_map_axis(port_names: list[str], port_decls: PortDecls) -> dict[str, str]:
+ """Return a rename map {original_port: generic_port} for AXI-Stream ports.
+
+ Groups ports by prefix (e.g. 'input_stream' from 'input_stream_TDATA').
+ A group is an *input stream* (HLS IP consumes) when its _TDATA port is
+ an input of the HLS module; an *output stream* (HLS IP produces) when
+ its _TDATA port is an output. Prefixes become `hls_in` / `hls_out`
+ with lowercase `_t{data,valid,ready}` suffixes.
+
+ Multiple input or output streams are indexed: hls_in0_t*, hls_in1_t*, ...
+ """
+ groups: dict[str, dict[str, str]] = {}
+ for port in port_names:
+ if port in _STANDARD_PORTS:
+ continue
+ for suffix in _AXIS_SUFFIXES:
+ if port.endswith(suffix):
+ prefix = port[: -len(suffix)]
+ groups.setdefault(prefix, {})[suffix] = port
+ break
+
+ def _is_input_stream(prefix: str, members: dict[str, str]) -> bool:
+ # Classify by the direction of the _TDATA port in the HLS module.
+ tdata = members.get('_TDATA')
+ if tdata is None:
+ # Fallback: _TREADY is an output of the wrapper iff this is an
+ # input stream (the IP asserts ready for incoming data).
+ tready = members.get('_TREADY')
+ if tready and port_decls.get(tready, ('', ''))[0] == 'output':
+ return True
+ return False
+ return port_decls.get(tdata, ('', ''))[0] == 'input'
+
+ input_prefixes = sorted(p for p, m in groups.items() if _is_input_stream(p, m))
+ output_prefixes = sorted(p for p, m in groups.items() if not _is_input_stream(p, m))
+
+ rename_map: dict[str, str] = {}
+ for class_prefixes, base in ((input_prefixes, 'hls_in'), (output_prefixes, 'hls_out')):
+ for idx, prefix in enumerate(class_prefixes):
+ new_prefix = base if len(class_prefixes) == 1 else f'{base}{idx}'
+ for suffix, old_name in groups[prefix].items():
+ rename_map[old_name] = f'{new_prefix}{suffix.lower()}'
+
+ return rename_map
+
+
+def build_rename_map(port_names: list[str], port_decls: PortDecls, flow: str) -> dict[str, str]:
+ """Return {original_hls_port: wrapper_port} for non-standard ports.
+
+ Args:
+ port_names: port names in declaration order (from parse_module)
+ port_decls: mapping from port name to (direction, width_str)
+ flow: 'stream' or 'parallel'
+
+ Returns:
+ dict mapping original HLS port names to renamed wrapper port names.
+ Standard control ports (clock, reset, start_port, done_port) are absent.
+ """
+ if flow == 'stream':
+ return _build_rename_map_axis(port_names, port_decls)
+ return _build_rename_map_bram(port_names)
+
+
+def generate_wrapper_verilog(module_name: str, port_names: list[str], port_decls: PortDecls, flow: str) -> str:
+ """Return the 'myproject' wrapper Verilog instantiating module_name as u0.
+
+ BRAM (parallel) or AXI-Stream (stream) ports are renamed to generic names
+ appropriate to the flow; the instantiation connects each renamed wrapper
+ port back to the original port name.
+ """
+ rename_map = build_rename_map(port_names, port_decls, flow)
+
+ # Column-align the width field across all port declarations
+ max_w = max((len(port_decls.get(p, ('', ''))[1]) for p in port_names), default=0)
+
+ def width_col(width: str) -> str:
+ """Padded text between 'wire' and the port name."""
+ if max_w == 0:
+ return ' '
+ return f' {width:<{max_w}} ' if width else ' ' * (max_w + 2)
+
+ lines = [
+ '// Wrapper with a valid identifier for mixed-language (VHDL top) instantiation',
+ 'module myproject (',
+ ]
+ last = len(port_names) - 1
+ for idx, port in enumerate(port_names):
+ direction, width = port_decls.get(port, ('input', ''))
+ display = rename_map.get(port, port)
+ comma = '' if idx == last else ','
+ # 'input ' / 'output' are both 6 chars, keeping 'wire' aligned
+ lines.append(f' {direction:<6} wire{width_col(width)}{display}{comma}')
+
+ lines += [');', f' {module_name} u0 (']
+ for idx, port in enumerate(port_names):
+ comma = '' if idx == last else ','
+ lines.append(f' .{port}({rename_map.get(port, port)}){comma}')
+ lines += [' );', 'endmodule', '']
+
+ return '\n'.join(lines)
+
+
+def extract_data_widths(port_names: list[str], port_decls: PortDecls, flow: str) -> tuple[int, int]:
+ """Return (in_data_width_bits, out_data_width_bits) from the HLS port declarations.
+
+ Parallel mode: input width from first _q0/_q1 port, output from first _d0/_d1.
+ Stream mode: input width from first input-stream _TDATA port, output from
+ first output-stream _TDATA port (direction determined by port direction in the IP).
+ """
+ if flow == 'stream':
+ in_tdatas = [p for p in port_names if p.endswith('_TDATA') and port_decls.get(p, ('', ''))[0] == 'input']
+ out_tdatas = [p for p in port_names if p.endswith('_TDATA') and port_decls.get(p, ('', ''))[0] == 'output']
+ in_dw = _parse_width(port_decls[sorted(in_tdatas)[0]][1]) if in_tdatas else 0
+ out_dw = _parse_width(port_decls[sorted(out_tdatas)[0]][1]) if out_tdatas else 0
+ return in_dw, out_dw
+
+ in_ports = sorted(p for p in port_names if p.endswith(('_q0', '_q1')))
+ out_ports = sorted(p for p in port_names if p.endswith(('_d0', '_d1')))
+
+ in_dw = _parse_width(port_decls[in_ports[0]][1]) if in_ports else 0
+ out_dw = _parse_width(port_decls[out_ports[0]][1]) if out_ports else 0
+ return in_dw, out_dw
+
+
+def extract_bram_depths(port_names: list[str], port_decls: PortDecls, flow: str) -> dict[str, int] | None:
+ """Return {'in': depth, 'out': depth} = 2 ** (width of each `*_address0` port).
+
+ This is Bambu's BRAM depth, which it rounds up to a whole number of address
+ bits: a 5-element output array gets a 3-bit address = 8 slots. It is NOT
+ the element count from the firmware headers.
+
+ HLS_*_N_WORDS must carry the DEPTH, established empirically on NG-ULTRA
+ (jet-tagger, 5 outputs, 3-bit address port):
+
+ N_WORDS=4 (stale template) -> 2958 LUT4, 7794 carry, works, but the
+ AXI window is short and the board's
+ read burst hangs (bsp_rc=4)
+ N_WORDS=5 (element count) -> 144 LUT4, 0 carry -- NxMap deletes
+ the entire datapath. The AXI slave's
+ output_flat_padded path nominally
+ supports a non-power-of-two word count;
+ in practice synthesis collapses it.
+ N_WORDS=8 (BRAM depth) -> 3329 LUT4, 7794 carry, datapath intact
+
+ Returns None for the stream flow: AXI-Stream IPs have no address ports, and
+ AXISlaveStream's N_BEATS_*/LAST_BEAT_*_VALID arithmetic genuinely wants the
+ element count. Do not route this value there.
+ """
+ if flow == 'stream':
+ return None
+
+ inverse = {new: old for old, new in build_rename_map(port_names, port_decls, flow).items()}
+ depths = {}
+ for key, base in (('in', 'input'), ('out', 'output')):
+ port = inverse.get(f'{base}_address0') or inverse.get(f'{base}_address1')
+ if port is None:
+ raise ValueError(f'No {base} address port found -- cannot size the {key} BRAM')
+ depths[key] = 2 ** _parse_width(port_decls[port][1])
+ return depths
+
+
+def generate_top_localparams(in_dw: int, in_n: int, out_dw: int, out_n: int, flow: str) -> str:
+ """Return the localparam block string for top_parallel.v / top_stream.v.
+
+ `in_n`/`out_n` are BRAM DEPTHS for the parallel flow (see
+ extract_bram_depths -- a non-power-of-two value makes NxMap delete the
+ datapath) and ELEMENT COUNTS for the stream flow (AXISlaveStream's
+ LAST_BEAT_*_VALID arithmetic needs the true count). The caller routes it;
+ this function only formats.
+
+ Parallel flow emits the four HLS_*_DATA_W / HLS_*_N_WORDS parameters plus
+ the two derived HLS_*_ADDR_W widths that AXISlaveParallel needs.
+ Stream flow skips the ADDR_W lines (AXISlaveStream doesn't use them).
+ """
+ lines = [
+ f'localparam HLS_IN_DATA_W = {in_dw};',
+ f'localparam HLS_OUT_DATA_W = {out_dw};',
+ f'localparam HLS_IN_N_WORDS = {in_n};',
+ f'localparam HLS_OUT_N_WORDS = {out_n};',
+ ]
+ if flow != 'stream':
+ lines += [
+ 'localparam HLS_IN_ADDR_W = (HLS_IN_N_WORDS > 1) ? $clog2(HLS_IN_N_WORDS) : 1;',
+ 'localparam HLS_OUT_ADDR_W = (HLS_OUT_N_WORDS > 1) ? $clog2(HLS_OUT_N_WORDS) : 1;',
+ ]
+ return '\n'.join(lines)
diff --git a/hls4ml/backends/nanoxplore_accelerator/__init__.py b/hls4ml/backends/nanoxplore_accelerator/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/hls4ml/backends/nanoxplore_accelerator/nanoxplore_accelerator_backend.py b/hls4ml/backends/nanoxplore_accelerator/nanoxplore_accelerator_backend.py
new file mode 100644
index 0000000000..66b1e4062a
--- /dev/null
+++ b/hls4ml/backends/nanoxplore_accelerator/nanoxplore_accelerator_backend.py
@@ -0,0 +1,95 @@
+import json
+import pathlib
+import subprocess
+
+from hls4ml.backends.bambu_accelerator.bambu_accelerator_backend import BambuAcceleratorBackend
+
+
+class NanoXploreAcceleratorBackend(BambuAcceleratorBackend):
+ """Concrete BambuAccelerator backend targeting NanoXplore NG-ULTRA devices."""
+
+ _default_device: str | None = 'nx2h540tsc'
+
+ def __init__(self):
+ super().__init__()
+ # Flows, passes and the writer are registered under 'BambuAccelerator'
+ # by super().__init__() (their ids are stored on self, so lookups keep
+ # working). The instance name must be the *registered* alias though:
+ # hls4ml writes backend.name into the model config and round-trips it
+ # through get_backend(), and 'BambuAccelerator' is abstract/unregistered.
+ self.name = 'NanoXploreAccelerator'
+
+ # Pass lookups that run lazily (after the rename above) must keep using
+ # the name the passes were registered under. Without this, apply_templates
+ # resolves zero templates, no layer gets a function_cpp, and the generated
+ # _float.cpp body contains no layer calls — Bambu then dead-codes the
+ # entire datapath (no output write ports on the HLS top).
+ _passes_name = 'BambuAccelerator'
+
+ def _get_layer_templates(self):
+ from hls4ml.backends.template import Template
+ from hls4ml.model.optimizer import get_backend_passes, get_optimizer
+
+ return [name for name in get_backend_passes(self._passes_name) if isinstance(get_optimizer(name), Template)]
+
+ def _get_layer_initializers(self):
+ real_name = self.name
+ self.name = self._passes_name
+ try:
+ return super()._get_layer_initializers()
+ finally:
+ self.name = real_name
+
+ def create_initial_config(self, part='nx2h540tsc', clock_period=20, **kwargs):
+ """NG-ULTRA defaults: nx2h540tsc (mapped in partname_to_bambu) and 20 ns,
+ matching the DevKit's 50 MHz oscillator so the P&R constraint equals the
+ physical clock without a PLL. The inherited Bambu defaults (Xilinx part,
+ 5 ns) would silently mis-target both HLS scheduling and the manifest."""
+ return super().create_initial_config(part=part, clock_period=clock_period, **kwargs)
+
+ def _generate_bitstream(self, model, project_dir: str, manifest: dict) -> dict:
+ """Shell out to hls4ml-nanoxplore-bitstream and return parsed metrics."""
+ cmd = self._resolve_bitstream_command(model)
+ try:
+ # stdout/stderr inherited: P&R runs for a long time and the CLI
+ # streams live progress; capturing here would silence the chain.
+ ret = subprocess.run(
+ [cmd, project_dir],
+ check=False,
+ )
+ except FileNotFoundError:
+ raise RuntimeError(
+ f'NanoXplore bitstream driver not installed '
+ f'(command not found: {cmd!r}). '
+ f'Build produced the manifest at {project_dir}/manifest.json.'
+ )
+ if ret.returncode != 0:
+ raise RuntimeError(
+ f'hls4ml-nanoxplore-bitstream failed (rc={ret.returncode}); '
+ f'see its output above and the logs in {project_dir}'
+ )
+ report_path = pathlib.Path(project_dir) / 'report.json'
+ if report_path.exists():
+ with open(report_path) as f:
+ return json.load(f)
+ return {}
+
+ def _resolve_bitstream_command(self, model) -> str:
+ if hasattr(self, '_bitstream_command') and self._bitstream_command:
+ return self._bitstream_command
+ try:
+ cmd = model.config.get_config_value('BitStreamCommand')
+ if cmd:
+ return cmd
+ except Exception:
+ pass
+ import shutil
+
+ found = shutil.which('hls4ml-nanoxplore-bitstream')
+ if found:
+ return found
+ raise RuntimeError(
+ 'NanoXplore bitstream driver not installed. '
+ 'Set BitStreamCommand in hls4ml config or install '
+ 'hls4ml-nanoxplore-bitstream on PATH.'
+ )
diff --git a/hls4ml/backends/symbolic/symbolic_backend.py b/hls4ml/backends/symbolic/symbolic_backend.py
index bad75c2417..406c3bd5b8 100644
--- a/hls4ml/backends/symbolic/symbolic_backend.py
+++ b/hls4ml/backends/symbolic/symbolic_backend.py
@@ -89,24 +89,43 @@ def create_initial_config(
return config
- def build(self, model, reset=False, csim=True, synth=True, cosim=False, validation=False, export=False, vsynth=False):
+ def build(
+ self,
+ model,
+ reset=False,
+ csim=True,
+ synth=True,
+ cosim=False,
+ validation=False,
+ export=False,
+ vsynth=False,
+ pnr=False,
+ ):
if 'linux' in sys.platform:
found = os.system('command -v vivado_hls > /dev/null')
if found != 0:
raise Exception('Vivado HLS installation not found. Make sure "vivado_hls" is on PATH.')
- curr_dir = os.getcwd()
- os.chdir(model.config.get_output_dir())
- vivado_cmd = (
- f'vivado_hls -f build_prj.tcl "reset={reset} '
- f'csim={csim} '
- f'synth={synth} '
- f'cosim={cosim} '
- f'validation={validation} '
- f'export={export} '
- f'vsynth={vsynth}"'
+ build_opts = (
+ 'array set opt {\n'
+ f' reset {int(reset)}\n'
+ f' csim {int(csim)}\n'
+ f' synth {int(synth)}\n'
+ f' cosim {int(cosim)}\n'
+ f' validation {int(validation)}\n'
+ f' export {int(export)}\n'
+ f' vsynth {int(vsynth)}\n'
+ f' pnr {int(pnr)}\n'
+ '}\n'
)
- os.system(vivado_cmd)
+ output_dir = model.config.get_output_dir()
+ tcl_path = os.path.join(output_dir, 'build_opt.tcl')
+ with open(tcl_path, 'w') as f:
+ f.write(build_opts)
+
+ curr_dir = os.getcwd()
+ os.chdir(output_dir)
+ os.system('vivado_hls -f build_prj.tcl')
os.chdir(curr_dir)
- return parse_vivado_report(model.config.get_output_dir())
+ return parse_vivado_report(output_dir)
diff --git a/hls4ml/backends/vitis/vitis_backend.py b/hls4ml/backends/vitis/vitis_backend.py
index 27801b5a19..f5df6a30d8 100644
--- a/hls4ml/backends/vitis/vitis_backend.py
+++ b/hls4ml/backends/vitis/vitis_backend.py
@@ -117,6 +117,7 @@ def build(
vsynth=False,
fifo_opt=False,
log_to_stdout=True,
+ pnr=False,
):
if 'linux' in sys.platform:
found_vrun = os.system('command -v vitis-run > /dev/null') == 0
@@ -133,6 +134,7 @@ def build(
f' export {int(export)}\n'
f' vsynth {int(vsynth)}\n'
f' fifo_opt {int(fifo_opt)}\n'
+ f' pnr {int(pnr)}\n'
'}\n'
)
diff --git a/hls4ml/backends/vivado/vivado_backend.py b/hls4ml/backends/vivado/vivado_backend.py
index 879784465a..7b708653f9 100644
--- a/hls4ml/backends/vivado/vivado_backend.py
+++ b/hls4ml/backends/vivado/vivado_backend.py
@@ -299,28 +299,44 @@ def build(
export=False,
vsynth=False,
fifo_opt=False,
+ pnr=False,
):
+ """Run HLS / optional Vivado flow.
+
+ Args:
+ pnr: If True (default) and ``vsynth`` is True, Vivado runs through place and route.
+ If False, Vivado stops after post-synthesis reports and statistics.
+ """
if 'linux' in sys.platform:
found = os.system('command -v vivado_hls > /dev/null')
if found != 0:
raise Exception('Vivado HLS installation not found. Make sure "vivado_hls" is on PATH.')
- curr_dir = os.getcwd()
- os.chdir(model.config.get_output_dir())
- vivado_cmd = (
- f'vivado_hls -f build_prj.tcl "reset={reset} '
- f'csim={csim} '
- f'synth={synth} '
- f'cosim={cosim} '
- f'validation={validation} '
- f'export={export} '
- f'vsynth={vsynth} '
- f'fifo_opt={fifo_opt}"'
+ build_opts = (
+ 'array set opt {\n'
+ f' reset {int(reset)}\n'
+ f' csim {int(csim)}\n'
+ f' synth {int(synth)}\n'
+ f' cosim {int(cosim)}\n'
+ f' validation {int(validation)}\n'
+ f' export {int(export)}\n'
+ f' vsynth {int(vsynth)}\n'
+ f' fifo_opt {int(fifo_opt)}\n'
+ f' pnr {int(pnr)}\n'
+ '}\n'
)
+ output_dir = model.config.get_output_dir()
+ tcl_path = os.path.join(output_dir, 'build_opt.tcl')
+ with open(tcl_path, 'w') as f:
+ f.write(build_opts)
+
+ curr_dir = os.getcwd()
+ os.chdir(output_dir)
+ vivado_cmd = 'vivado_hls -f build_prj.tcl'
os.system(vivado_cmd)
os.chdir(curr_dir)
- return parse_vivado_report(model.config.get_output_dir())
+ return parse_vivado_report(output_dir)
@layer_optimizer(Layer)
def init_base_layer(self, layer):
diff --git a/hls4ml/backends/vivado_accelerator/vivado_accelerator_backend.py b/hls4ml/backends/vivado_accelerator/vivado_accelerator_backend.py
index 128a8a8345..3a27d2d48c 100644
--- a/hls4ml/backends/vivado_accelerator/vivado_accelerator_backend.py
+++ b/hls4ml/backends/vivado_accelerator/vivado_accelerator_backend.py
@@ -1,4 +1,5 @@
import os
+import sys
from hls4ml.backends import VivadoBackend
from hls4ml.model.flow import register_flow
@@ -23,78 +24,40 @@ def build(
vsynth=False,
fifo_opt=False,
bitfile=False,
+ pnr=False,
):
- # run the VivadoBackend build
- super().build(
- model,
- reset=reset,
- csim=csim,
- synth=synth,
- cosim=cosim,
- validation=validation,
- export=export,
- vsynth=vsynth,
- fifo_opt=fifo_opt,
+ if 'linux' in sys.platform:
+ found = os.system('command -v vivado_hls > /dev/null')
+ if found != 0:
+ raise Exception('Vivado HLS installation not found. Make sure "vivado_hls" is on PATH.')
+
+ build_opts = (
+ 'array set opt {\n'
+ f' reset {int(reset)}\n'
+ f' csim {int(csim)}\n'
+ f' synth {int(synth)}\n'
+ f' cosim {int(cosim)}\n'
+ f' validation {int(validation)}\n'
+ f' export {int(export)}\n'
+ f' vsynth {int(vsynth)}\n'
+ f' fifo_opt {int(fifo_opt)}\n'
+ f' bitfile {int(bitfile)}\n'
+ f' pnr {int(pnr)}\n'
+ '}\n'
)
- # Get Config to view Board and Platform
- from hls4ml.backends import VivadoAcceleratorConfig
+ output_dir = model.config.get_output_dir()
+ tcl_path = os.path.join(output_dir, 'build_opt.tcl')
+ with open(tcl_path, 'w') as f:
+ f.write(build_opts)
- vivado_accelerator_config = VivadoAcceleratorConfig(
- model.config, model.get_input_variables(), model.get_output_variables()
- )
- # now make a bitfile
- if bitfile:
- if vivado_accelerator_config.get_board().startswith('alveo'):
- self.make_xclbin(model, vivado_accelerator_config.get_platform())
- else:
- curr_dir = os.getcwd()
- os.chdir(model.config.get_output_dir())
- try:
- os.system('vivado -mode batch -source design.tcl')
- except Exception:
- print('Something went wrong, check the Vivado logs')
- os.chdir(curr_dir)
-
- return parse_vivado_report(model.config.get_output_dir())
-
- def make_xclbin(self, model, platform='xilinx_u250_xdma_201830_2'):
- """Create the xclbin for the given model and target platform.
-
- Args:
- model (ModelGraph): Compiled and build model.
- platform (str, optional): Development/Deployment target platform, must be installed first.
- The host machine only requires the deployment target platform. Refer to the Getting Started section of
- the Alveo guide. Defaults to 'xilinx_u250_xdma_201830_2'.
- """
curr_dir = os.getcwd()
- abs_path_dir = os.path.abspath(model.config.get_output_dir())
- os.chdir(abs_path_dir)
- os.makedirs('xo_files', exist_ok=True)
- try:
- os.system('vivado -mode batch -source design.tcl')
- except Exception:
- print('Something went wrong, check the Vivado logs')
- project_name = model.config.get_project_name()
- ip_repo_path = abs_path_dir + '/' + project_name + '_prj' + '/solution1/impl/ip'
- os.makedirs('xclbin_files', exist_ok=True)
- os.chdir(abs_path_dir + '/xclbin_files')
- # TODO Add other platforms
- vitis_cmd = (
- 'v++ -t hw --platform '
- + platform
- + ' --link ../xo_files/'
- + project_name
- + "_kernel.xo -o'"
- + project_name
- + "_kernel.xclbin' --user_ip_repo_paths "
- + ip_repo_path
- )
- try:
- os.system(vitis_cmd)
- except Exception:
- print('Something went wrong, check the Vitis/Vivado logs')
+ os.chdir(output_dir)
+ vivado_cmd = 'vivado_hls -f build_prj.tcl'
+ os.system(vivado_cmd)
os.chdir(curr_dir)
+ return parse_vivado_report(output_dir)
+
def create_initial_config(
self,
board='pynq-z2',
diff --git a/hls4ml/cli/__init__.py b/hls4ml/cli/__init__.py
index 4b1fef45d5..e59cc0eba8 100755
--- a/hls4ml/cli/__init__.py
+++ b/hls4ml/cli/__init__.py
@@ -193,6 +193,13 @@ def _build_vivado(args, extra_args):
action='store_true',
)
vivado_parser.add_argument('--reset', help='Remove any previous builds', action='store_true', default=False)
+ vivado_parser.add_argument('--fifo-opt', help='Optimize FIFO usage', action='store_true', default=False)
+ vivado_parser.add_argument(
+ '--pnr',
+ help='With Vivado synthesis (-l), run place and route',
+ action='store_true',
+ default=False,
+ )
if args.list_options:
vivado_parser.print_help()
@@ -207,6 +214,8 @@ def _build_vivado(args, extra_args):
validation = int(vivado_args.validation)
export = int(vivado_args.export)
vsynth = int(vivado_args.vivado_synthesis)
+ fifo_opt = int(vivado_args.fifo_opt)
+ pnr = int(vivado_args.pnr)
if vivado_args.all:
csim = synth = cosim = validation = export = vsynth = 1
@@ -217,21 +226,24 @@ def _build_vivado(args, extra_args):
print('Vivado HLS installation not found. Make sure "vivado_hls" is on PATH.')
sys.exit(1)
- os.system(
- (
- 'cd {dir} && vivado_hls -f build_prj.tcl "reset={reset} csim={csim} synth={synth} cosim={cosim} '
- 'validation={validation} export={export} vsynth={vsynth}"'
- ).format(
- dir=args.project,
- reset=reset,
- csim=csim,
- synth=synth,
- cosim=cosim,
- validation=validation,
- export=export,
- vsynth=vsynth,
- )
+ build_opts = (
+ 'array set opt {\n'
+ f' reset {reset}\n'
+ f' csim {csim}\n'
+ f' synth {synth}\n'
+ f' cosim {cosim}\n'
+ f' validation {validation}\n'
+ f' export {export}\n'
+ f' vsynth {vsynth}\n'
+ f' fifo_opt {fifo_opt}\n'
+ f' pnr {pnr}\n'
+ '}\n'
)
+ tcl_path = os.path.join(args.project, 'build_opt.tcl')
+ with open(tcl_path, 'w') as f:
+ f.write(build_opts)
+
+ os.system(f'cd {args.project} && vivado_hls -f build_prj.tcl')
def _build_quartus(args, extra_args):
diff --git a/hls4ml/report/__init__.py b/hls4ml/report/__init__.py
index 4d3641a5ac..96580cde46 100644
--- a/hls4ml/report/__init__.py
+++ b/hls4ml/report/__init__.py
@@ -1,3 +1,4 @@
+from hls4ml.report.bambu_report import parse_bambu_report # noqa: F401
from hls4ml.report.catapult_report import (
parse_catapult_report, # noqa: F401
qofr, # noqa: F401
diff --git a/hls4ml/report/bambu_report.py b/hls4ml/report/bambu_report.py
new file mode 100644
index 0000000000..66b2413d1d
--- /dev/null
+++ b/hls4ml/report/bambu_report.py
@@ -0,0 +1,148 @@
+import glob
+import os
+import xml.etree.ElementTree as ET
+
+from hls4ml.report.vivado_report import (
+ _parse_csim_results,
+ _parse_implementation_report,
+ _parse_power_report,
+ _parse_rtl_cosim_results,
+ _parse_timing_report,
+)
+
+
+def _coerce_value(raw):
+ if raw is None:
+ return None
+ if isinstance(raw, str):
+ raw = raw.strip()
+ if not raw:
+ return raw
+ try:
+ return int(raw)
+ except (ValueError, TypeError):
+ try:
+ return float(raw)
+ except (ValueError, TypeError):
+ return raw
+
+
+def _parse_result_file(path):
+ """Parse a single bambu_results XML file produced by Bambu 2026.06+.
+
+ The reference schema (PandA 2026.06) has root tag ```` with:
+ - meta as root attributes: ``args``, ``version``, ``benchmark``, ``timestamp``
+ - ```` child: flat attributes per metric (LUTS, REGISTERS, DSPS,
+ BRAMS, DRAMS, SLICES, FE, IOPINS, POWER, FREQUENCY, SLACK, DELAY, …).
+ Attribute names are vendor-specific (Xilinx has SLICES; NanoXplore has FE).
+ Absent when P&R fails (e.g. NanoXplore routing errors) — tolerated.
+ - ```` child (top-level): CYCLES, AREA, PERIOD, FREQUENCY, …
+ - ```` child: ```` or ```` sub-element each
+ containing ```` text nodes with per-execution cycle counts.
+ - ```` child: per-function scheduling info (not parsed here).
+ """
+ tree = ET.parse(path)
+ root = tree.getroot()
+
+ meta = {
+ 'Args': root.attrib.get('args'),
+ 'Version': root.attrib.get('version'),
+ 'Timestamp': root.attrib.get('timestamp'),
+ 'Benchmark': root.attrib.get('benchmark'),
+ 'File': os.path.basename(path),
+ }
+
+ metrics = {}
+
+ # Resource metrics — absent when P&R fails, silently skipped.
+ resources = root.find('resources')
+ if resources is not None:
+ for key, val in resources.attrib.items():
+ metrics[key] = _coerce_value(val)
+
+ # Top-level carries CYCLES, AREA, PERIOD, FREQUENCY, … in 2026.06.
+ # Use setdefault so values (FREQUENCY, REGISTERS, …) take precedence
+ # when both are present (resources come from synthesis, evaluation may repeat them).
+ evaluation = root.find('evaluation')
+ if evaluation is not None:
+ for key, val in evaluation.attrib.items():
+ metrics.setdefault(key, _coerce_value(val))
+
+ # Cycle counts from // text nodes.
+ # 2026.06 uses ; older PandA used ; try both.
+ timing = root.find('timing')
+ if timing is not None:
+ timing_node = timing.find('simulation') or timing.find('evaluation')
+ if timing_node is not None:
+ runs = [_coerce_value(r.text) for r in timing_node.findall('run')]
+ if runs:
+ metrics['Total cycles'] = sum(runs)
+ metrics['Number of executions'] = len(runs)
+ metrics['Average execution'] = sum(runs) / len(runs)
+
+ return {'meta': meta, 'metrics': metrics}
+
+
+def parse_bambu_report(hls_dir, part_family):
+ """Parse Bambu result files from ``hls_dir``.
+
+ Parses the ``bambu_results*.xml`` file(s) produced by Bambu 2026.06
+ (root ````, ```` attrs, ```` attrs,
+ ``//`` cycle counts). For Xilinx
+ targets, also reads the Vivado implementation, timing and power reports.
+
+ Args:
+ hls_dir: directory containing ``bambu_results*.xml`` and, for Xilinx
+ targets, the Vivado report tree.
+ part_family: ``"Xilinx"`` or ``"NanoXplore"`` (or ``None``). Controls
+ whether Vivado reports are parsed.
+
+ Returns:
+ dict with zero or more of the following keys:
+ - ``'BambuMetrics'``: dict of resource/timing metrics from the XML
+ (e.g. LUTS, REGISTERS, DSPS, CYCLES, Total cycles, …). Absent
+ when no ``bambu_results*.xml`` is found or P&R failed and the
+ file contains no ```` block (NanoXplore routing errors).
+ - ``'CSimResults'``, ``'CosimResults'``: parsed C-sim / RTL-cosim logs.
+ - ``'ImplementationReport'``, ``'TimingReport'``, ``'PowerReport'``:
+ Vivado reports (Xilinx only).
+ """
+ result = {}
+
+ # Parse CSim and Cosim
+ csim_results = _parse_csim_results(hls_dir)
+ if csim_results is not None:
+ result['CSimResults'] = csim_results
+
+ cosim_results = _parse_rtl_cosim_results(hls_dir)
+ if cosim_results is not None:
+ result['CosimResults'] = cosim_results
+
+ # Parse metrics reported by Bambu
+ pattern = os.path.join(hls_dir, 'bambu_results*.xml')
+ matches = sorted(glob.glob(pattern))
+ if matches:
+ parsed = [_parse_result_file(path) for path in matches]
+ result['BambuMetrics'] = parsed[-1]['metrics']
+
+ # Parse Vivado reports if target is from Xilinx
+ if part_family == 'Xilinx':
+ implementation_report = _parse_implementation_report(hls_dir, is_vivado_accelerator=False, percentage_columns=False)
+ if implementation_report is not None:
+ result['ImplementationReport'] = implementation_report
+ else:
+ print('Implementation report not found.')
+
+ timing_report = _parse_timing_report(hls_dir, is_vivado_accelerator=False)
+ if timing_report is not None:
+ result['TimingReport'] = timing_report
+ else:
+ print('Timing report not found.')
+
+ power_report = _parse_power_report(hls_dir, is_vivado_accelerator=False)
+ if power_report is not None:
+ result['PowerReport'] = power_report
+ else:
+ print('Power report not found.')
+
+ return result
diff --git a/hls4ml/report/vivado_report.py b/hls4ml/report/vivado_report.py
index 1a871080db..fb11c1e83a 100644
--- a/hls4ml/report/vivado_report.py
+++ b/hls4ml/report/vivado_report.py
@@ -3,6 +3,94 @@
import sys
import xml.etree.ElementTree as ET
+# Path templates for report files. Use _path() to resolve with os.path.join.
+# Placeholders: {hls_dir}, {prj_dir}, {sln_dir}, {solution}, {top}, {rtl}, {base}
+PATHS = {
+ 'project_tcl': ('{hls_dir}', 'project.tcl'),
+ 'sln_dir': ('{hls_dir}', '{prj_dir}'),
+ 'vivado_hls_app': ('{sln_dir}', 'vivado_hls.app'),
+ 'hls_app': ('{sln_dir}', 'hls.app'),
+ 'solution_dir': ('{sln_dir}', '{solution}'),
+ 'csim_log': ('{sln_dir}', 'csim', 'report', '{top}_csim.log'),
+ 'csynth_rpt': ('{sln_dir}', 'syn', 'report', '{top}_csynth.rpt'),
+ 'cosim_rpt': ('{sln_dir}', 'sim', 'report', '{top}_cosim.rpt'),
+ 'csim_results': ('{hls_dir}', 'tb_data', 'csim_results.log'),
+ 'rtl_cosim_results': ('{hls_dir}', 'tb_data', 'rtl_cosim_results.log'),
+ 'csynth_xml': ('{sln_dir}', '{solution}', 'syn', 'report', '{top}_csynth.xml'),
+ 'vivado_synth': ('{hls_dir}', 'vivado_synth.rpt'),
+ 'cosim_report': ('{sln_dir}', '{solution}', 'sim', 'report', '{top}_cosim.rpt'),
+ 'transaction_file': (
+ '{sln_dir}',
+ '{solution}',
+ 'sim',
+ '{rtl}',
+ '{top}.performance.result.transaction.xml',
+ ),
+ 'util_rpt_vivado': ('{hls_dir}', 'vivado_reports', 'post_route_util_hier.rpt'),
+ 'util_rpt_system': ('{hls_dir}', 'vivado_reports', 'post_route_util_hier_system.rpt'),
+ 'timing_summary_vivado': ('{hls_dir}', 'vivado_reports', 'post_route_timing_summary.rpt'),
+ 'timing_summary_system': ('{hls_dir}', 'vivado_reports', 'post_route_timing_summary_system.rpt'),
+ 'power_rpt_vivado': ('{hls_dir}', 'vivado_reports', 'post_route_power.rpt'),
+ 'power_rpt_system': ('{hls_dir}', 'vivado_reports', 'post_route_power_system.rpt'),
+}
+
+# Synthesis report (csynth.rpt)
+SYNTH_HEADER_LINES = 2
+SYNTH_TRUNCATE_MARKER = '* DSP48'
+
+# Vivado synth report sections (numbered headers "1.", "2.", "3.")
+VIVADO_SECTION_CLB = 1
+VIVADO_SECTION_RAM = 2
+VIVADO_SECTION_DSP = 3
+VIVADO_COLUMN_INDEX = 2 # Resource value column after split by '|'
+
+# Cosim .rpt table column indices (RTL, Status, Latency-min/avg/max, Interval-min/avg/max)
+COSIM_COL_RTL = 0
+COSIM_COL_STATUS = 1
+COSIM_COL_LATENCY_MIN = 2
+COSIM_COL_LATENCY_AVG = 3
+COSIM_COL_LATENCY_MAX = 4
+COSIM_COL_INTERVAL_MIN = 5
+COSIM_COL_INTERVAL_AVG = 6
+COSIM_COL_INTERVAL_MAX = 7
+
+# Transaction file (performance.result.transaction.xml): latency and interval column indices
+TX_LATENCY_IDX = 2
+TX_INTERVAL_IDX = 3
+
+# util report (top) line: cells to skip and column indices
+UTIL_SKIP_CELLS = 2
+UTIL_COL_TOTLUTS = 0
+UTIL_COL_LOGICLUTS = 1
+UTIL_COL_LUTRAMS = 2
+UTIL_COL_SRLS = 3
+UTIL_COL_FFS = 4
+UTIL_COL_RAMB36 = 5
+UTIL_COL_RAMB18 = 6
+UTIL_COL_URAM = 7
+UTIL_COL_DSP = 8
+UTIL_COLS_WITH_URAM = 9
+UTIL_COLS_WITHOUT_URAM = 8
+
+# Timing summary report column indices
+TIMING_COL_WNS = 0
+TIMING_COL_TNS = 1
+TIMING_COL_WHS = 4
+TIMING_COL_THS = 5
+TIMING_COL_WPWS = 8
+TIMING_COL_TPWS = 9
+
+
+def _path(name, **kwargs):
+ """Build a path from PATHS template, resolving {placeholder} with kwargs."""
+ segments = PATHS[name]
+ resolved = []
+ for seg in segments:
+ for key, val in kwargs.items():
+ seg = seg.replace('{' + key + '}', str(val))
+ resolved.append(seg)
+ return os.path.join(*resolved)
+
def read_vivado_report(hls_dir, full_report=False):
if not os.path.exists(hls_dir):
@@ -12,14 +100,14 @@ def read_vivado_report(hls_dir, full_report=False):
prj_dir = None
top_func_name = None
- if os.path.isfile(hls_dir + '/project.tcl'):
- prj_dir, top_func_name = _parse_project_script(hls_dir)
+ if os.path.isfile(_path('project_tcl', hls_dir=hls_dir)):
+ prj_dir, top_func_name, _ = _parse_project_script(hls_dir)
if prj_dir is None or top_func_name is None:
print('Unable to read project data. Exiting.')
return
- sln_dir = hls_dir + '/' + prj_dir
+ sln_dir = _path('sln_dir', hls_dir=hls_dir, prj_dir=prj_dir)
if not os.path.exists(sln_dir):
print(f'Project {prj_dir} does not exist. Rerun "hls4ml build -p {hls_dir}".')
return
@@ -29,14 +117,15 @@ def read_vivado_report(hls_dir, full_report=False):
for sln in solutions:
print(f'Reports for solution "{sln}":\n')
- _find_reports(sln_dir + '/' + sln, top_func_name, full_report)
+ _find_reports(_path('solution_dir', sln_dir=sln_dir, solution=sln), top_func_name, full_report)
def _parse_project_script(path):
prj_dir = None
top_func_name = None
+ backend_name = 'vivado'
- project_path = path + '/project.tcl'
+ project_path = _path('project_tcl', hls_dir=path)
with open(project_path) as f:
for line in f.readlines():
@@ -49,46 +138,46 @@ def _parse_project_script(path):
if 'accelerator' in backend_name:
top_func_name += '_axi'
- return prj_dir, top_func_name
+ return prj_dir, top_func_name, backend_name
def _find_solutions(sln_dir):
solutions = []
- if os.path.isfile(sln_dir + '/vivado_hls.app'):
+ if os.path.isfile(_path('vivado_hls_app', sln_dir=sln_dir)):
sln_file = 'vivado_hls.app'
- elif os.path.isfile(sln_dir + '/hls.app'):
+ elif os.path.isfile(_path('hls_app', sln_dir=sln_dir)):
sln_file = 'hls.app'
else:
return solutions
- with open(sln_dir + '/' + sln_file) as f:
+ with open(_path('vivado_hls_app' if sln_file == 'vivado_hls.app' else 'hls_app', sln_dir=sln_dir)) as f:
# Get rid of namespaces (workaround to support two types of vivado_hls.app files)
xmlstring = re.sub(' xmlns="[^"]+"', '', f.read(), count=1)
root = ET.fromstring(xmlstring)
for sln_tag in root.findall('solutions/solution'):
sln_name = sln_tag.get('name')
- if sln_name is not None and os.path.isdir(sln_dir + '/' + sln_name):
+ if sln_name is not None and os.path.isdir(_path('solution_dir', sln_dir=sln_dir, solution=sln_name)):
solutions.append(sln_name)
return solutions
def _find_reports(sln_dir, top_func_name, full_report=False):
- csim_file = sln_dir + f'/csim/report/{top_func_name}_csim.log'
+ csim_file = _path('csim_log', sln_dir=sln_dir, top=top_func_name)
if os.path.isfile(csim_file):
_show_csim_report(csim_file)
else:
print('C simulation report not found.')
- syn_file = sln_dir + f'/syn/report/{top_func_name}_csynth.rpt'
+ syn_file = _path('csynth_rpt', sln_dir=sln_dir, top=top_func_name)
if os.path.isfile(syn_file):
_show_synth_report(syn_file, full_report)
else:
print('Synthesis report not found.')
- cosim_file = sln_dir + f'/sim/report/{top_func_name}_cosim.rpt'
+ cosim_file = _path('cosim_rpt', sln_dir=sln_dir, top=top_func_name)
if os.path.isfile(cosim_file):
_show_cosim_report(cosim_file)
else:
@@ -104,8 +193,8 @@ def _show_csim_report(csim_file):
def _show_synth_report(synth_file, full_report=False):
with open(synth_file) as f:
print('SYNTHESIS REPORT:')
- for line in f.readlines()[2:]:
- if not full_report and '* DSP48' in line:
+ for line in f.readlines()[SYNTH_HEADER_LINES:]:
+ if not full_report and SYNTH_TRUNCATE_MARKER in line:
break
print(line, end='')
@@ -120,6 +209,210 @@ def _get_abs_and_percentage_values(unparsed_cell):
return int(unparsed_cell.split('(')[0]), float(unparsed_cell.split('(')[1].replace('%', '').replace(')', ''))
+def _parse_csim_results(hls_dir):
+ """Parse C simulation results from tb_data/csim_results.log."""
+ sim_file = _path('csim_results', hls_dir=hls_dir)
+ if not os.path.isfile(sim_file):
+ return None
+ with open(sim_file) as f:
+ return [[r for r in line.split()] for line in f.readlines()]
+
+
+def _parse_rtl_cosim_results(hls_dir):
+ """Parse RTL cosimulation results from tb_data/rtl_cosim_results.log."""
+ sim_file = _path('rtl_cosim_results', hls_dir=hls_dir)
+ if not os.path.isfile(sim_file):
+ return None
+ with open(sim_file) as f:
+ return [[r for r in line.split()] for line in f.readlines()]
+
+
+def _parse_csynthesis_report(sln_dir, solution, top_func_name):
+ """Parse C synthesis XML report."""
+ syn_file = _path('csynth_xml', sln_dir=sln_dir, solution=solution, top=top_func_name)
+ if not os.path.isfile(syn_file):
+ return None
+ root = ET.parse(syn_file).getroot()
+ c_synth_report = {}
+ perf_node = root.find('./PerformanceEstimates')
+ c_synth_report['TargetClockPeriod'] = root.find('./UserAssignments/TargetClockPeriod').text
+ c_synth_report['EstimatedClockPeriod'] = perf_node.find('./SummaryOfTimingAnalysis/EstimatedClockPeriod').text
+ c_synth_report['BestLatency'] = perf_node.find('./SummaryOfOverallLatency/Best-caseLatency').text
+ c_synth_report['WorstLatency'] = perf_node.find('./SummaryOfOverallLatency/Worst-caseLatency').text
+ c_synth_report['IntervalMin'] = perf_node.find('./SummaryOfOverallLatency/Interval-min').text
+ c_synth_report['IntervalMax'] = perf_node.find('./SummaryOfOverallLatency/Interval-max').text
+ area_node = root.find('./AreaEstimates')
+ for child in area_node.find('./Resources'):
+ if child.tag == 'DSP48E':
+ child.tag = 'DSP'
+ c_synth_report[child.tag] = child.text
+ for child in area_node.find('./AvailableResources'):
+ if child.tag == 'DSP48E':
+ child.tag = 'DSP'
+ c_synth_report['Available' + child.tag] = child.text
+ return c_synth_report
+
+
+def _parse_vivado_synth_report(hls_dir):
+ """Parse Vivado synthesis report (vivado_synth.rpt)."""
+ vivado_syn_file = _path('vivado_synth', hls_dir=hls_dir)
+ if not os.path.isfile(vivado_syn_file):
+ return None
+ vivado_synth_rpt = {}
+ with open(vivado_syn_file) as f:
+ section = 0
+ for line in f.readlines():
+ match = re.match(r'^(\d)\.', line)
+ if match:
+ section = int(match.group(1))
+ if '|' in line:
+ if ('CLB LUTs' in line or 'Slice LUTs' in line) and section == VIVADO_SECTION_CLB:
+ vivado_synth_rpt['LUT'] = line.split('|')[VIVADO_COLUMN_INDEX].strip()
+ elif ('CLB Registers' in line or 'Slice Registers' in line) and section == VIVADO_SECTION_CLB:
+ vivado_synth_rpt['FF'] = line.split('|')[VIVADO_COLUMN_INDEX].strip()
+ elif 'Block RAM Tile' in line and section == VIVADO_SECTION_RAM:
+ vivado_synth_rpt['BRAM_18K'] = line.split('|')[VIVADO_COLUMN_INDEX].strip()
+ elif 'URAM' in line and section == VIVADO_SECTION_RAM:
+ vivado_synth_rpt['URAM'] = line.split('|')[VIVADO_COLUMN_INDEX].strip()
+ elif 'DSPs' in line and section == VIVADO_SECTION_DSP:
+ vivado_synth_rpt['DSP48E'] = line.split('|')[VIVADO_COLUMN_INDEX].strip()
+ return vivado_synth_rpt
+
+
+def _parse_transaction_file(sln_dir, solution, rtl, top_func_name):
+ """Parse transaction file for detailed latency/interval stats. Returns dict to merge into CosimReport."""
+ transaction_file = _path('transaction_file', sln_dir=sln_dir, solution=solution, rtl=rtl.lower(), top=top_func_name)
+ if not os.path.isfile(transaction_file):
+ return None
+ cosim_transactions = {
+ 'InitiationInterval': {'max': 0, 'min': sys.maxsize, 'avg': 0.0},
+ 'Latency': {'max': 0, 'min': sys.maxsize, 'avg': 0.0},
+ }
+ with open(transaction_file) as f:
+ i = 1
+ for line in f.readlines():
+ if re.search('transaction', line):
+ result = line.split()
+ if result[TX_INTERVAL_IDX] != 'x':
+ cosim_transactions['InitiationInterval']['min'] = min(
+ int(result[TX_INTERVAL_IDX]), cosim_transactions['InitiationInterval']['min']
+ )
+ cosim_transactions['InitiationInterval']['max'] = max(
+ int(result[TX_INTERVAL_IDX]), cosim_transactions['InitiationInterval']['max']
+ )
+ cosim_transactions['InitiationInterval']['avg'] += float(
+ (int(result[TX_INTERVAL_IDX]) - cosim_transactions['InitiationInterval']['avg']) / i
+ )
+ cosim_transactions['Latency']['min'] = min(int(result[TX_LATENCY_IDX]), cosim_transactions['Latency']['min'])
+ cosim_transactions['Latency']['max'] = max(int(result[TX_LATENCY_IDX]), cosim_transactions['Latency']['max'])
+ cosim_transactions['Latency']['avg'] += float(
+ (int(result[TX_LATENCY_IDX]) - cosim_transactions['Latency']['avg']) / i
+ )
+ i += 1
+ return {
+ 'LatencyMin': cosim_transactions['Latency']['min'],
+ 'LatencyMax': cosim_transactions['Latency']['max'],
+ 'LatencyAvg': cosim_transactions['Latency']['avg'],
+ 'IntervalMin': cosim_transactions['InitiationInterval']['min'],
+ 'IntervalMax': cosim_transactions['InitiationInterval']['max'],
+ 'IntervalAvg': cosim_transactions['InitiationInterval']['avg'],
+ }
+
+
+def _parse_implementation_report(hls_dir, is_vivado_accelerator, percentage_columns=True):
+ """Parse post-route utilization report.
+
+ Args:
+ hls_dir: project directory.
+ is_vivado_accelerator: select the system-level report instead of the top-level one.
+ percentage_columns (bool, optional): whether each cell carries a percentage next to the
+ absolute value. Vivado's hierarchical utilization report writes both, but reports
+ produced by other flows carry the absolute count only. Defaults to True.
+ """
+ util_rpt_path = 'util_rpt_system' if is_vivado_accelerator else 'util_rpt_vivado'
+ post_route_util_file = _path(util_rpt_path, hls_dir=hls_dir)
+ if not os.path.isfile(post_route_util_file):
+ return None
+ implementation_report = {}
+ with open(post_route_util_file) as f:
+ for line in f.readlines():
+ if re.search(r'\(top\)', line):
+ cells = line.replace('|', '').split()[UTIL_SKIP_CELLS:]
+ if percentage_columns:
+ results = [_get_abs_and_percentage_values(elem) for elem in cells]
+ else:
+ results = [(int(elem), None) for elem in cells]
+
+ columns = [
+ ('TotLUTs', UTIL_COL_TOTLUTS),
+ ('LogicLUTs', UTIL_COL_LOGICLUTS),
+ ('LUTRAMs', UTIL_COL_LUTRAMS),
+ ('SRLs', UTIL_COL_SRLS),
+ ('FFs', UTIL_COL_FFS),
+ ('RAMB36s', UTIL_COL_RAMB36),
+ ('RAMB18s', UTIL_COL_RAMB18),
+ ]
+ if len(results) == UTIL_COLS_WITH_URAM:
+ columns += [('URAMs', UTIL_COL_URAM), ('DSPs', UTIL_COL_DSP)]
+ else:
+ columns += [('DSPs', UTIL_COL_DSP - 1)]
+
+ for name, col in columns:
+ implementation_report[name] = results[col][0]
+ if percentage_columns:
+ implementation_report[f'{name}%'] = results[col][1]
+ break
+ return implementation_report if implementation_report else None
+
+
+def _parse_timing_report(hls_dir, is_vivado_accelerator):
+ """Parse post-route timing summary report."""
+ timing_rpt_path = 'timing_summary_system' if is_vivado_accelerator else 'timing_summary_vivado'
+ timing_report_file = _path(timing_rpt_path, hls_dir=hls_dir)
+ if not os.path.isfile(timing_report_file):
+ return None
+ with open(timing_report_file) as f:
+ while not re.search('WNS', next(f)):
+ pass
+ next(f)
+ result = next(f).split()
+ return {
+ 'WNS': float(result[TIMING_COL_WNS]),
+ 'TNS': float(result[TIMING_COL_TNS]),
+ 'WHS': float(result[TIMING_COL_WHS]),
+ 'THS': float(result[TIMING_COL_THS]),
+ 'WPWS': float(result[TIMING_COL_WPWS]),
+ 'TPWS': float(result[TIMING_COL_TPWS]),
+ }
+
+
+def _parse_power_report(hls_dir, is_vivado_accelerator):
+ """Parse post-route power report."""
+ power_rpt_path = 'power_rpt_system' if is_vivado_accelerator else 'power_rpt_vivado'
+ power_report_file = _path(power_rpt_path, hls_dir=hls_dir)
+ if not os.path.isfile(power_report_file):
+ return None
+ power_report = {}
+ power_keys = {
+ 'Total On-Chip Power (W)': 'TotalOnChipPower',
+ 'Dynamic (W)': 'Dynamic',
+ 'Device Static (W)': 'Static',
+ }
+ with open(power_report_file) as f:
+ for line in f:
+ if '|' not in line:
+ continue
+ parts = [p.strip() for p in line.split('|')]
+ if len(parts) < 3:
+ continue
+ label, value = parts[1], parts[2]
+ for key_pattern, report_key in power_keys.items():
+ if key_pattern in label:
+ power_report[report_key] = value
+ break
+ return power_report if power_report else None
+
+
def parse_vivado_report(hls_dir):
if not os.path.exists(hls_dir):
print(f'Path {hls_dir} does not exist. Exiting.')
@@ -128,247 +421,77 @@ def parse_vivado_report(hls_dir):
prj_dir = None
top_func_name = None
- if os.path.isfile(hls_dir + '/project.tcl'):
- prj_dir, top_func_name = _parse_project_script(hls_dir)
+ if os.path.isfile(_path('project_tcl', hls_dir=hls_dir)):
+ prj_dir, top_func_name, backend_name = _parse_project_script(hls_dir)
+ else:
+ prj_dir, top_func_name, backend_name = None, None, 'vivado'
if prj_dir is None or top_func_name is None:
print('Unable to read project data. Exiting.')
return
- sln_dir = hls_dir + '/' + prj_dir
+ sln_dir = _path('sln_dir', hls_dir=hls_dir, prj_dir=prj_dir)
if not os.path.exists(sln_dir):
- print(f'Project {prj_dir} does not exist. Rerun "hls4ml build -p {hls_dir}".')
+ print(f'Project {prj_dir} does not exist. Rerun `model_hls.build(...)`')
return
solutions = _find_solutions(sln_dir)
if len(solutions) > 1:
print(f'WARNING: Found {len(solutions)} solution(s) in {sln_dir}. Using the first solution.')
+ is_vivado_accelerator = 'vivadoaccelerator' == backend_name
+
+ solution = solutions[0]
report = {}
- sim_file = hls_dir + '/tb_data/csim_results.log'
- if os.path.isfile(sim_file):
- csim_results = []
- with open(sim_file) as f:
- for line in f.readlines():
- csim_results.append([r for r in line.split()])
+ csim_results = _parse_csim_results(hls_dir)
+ if csim_results is not None:
report['CSimResults'] = csim_results
- sim_file = hls_dir + '/tb_data/rtl_cosim_results.log'
- if os.path.isfile(sim_file):
- cosim_results = []
- with open(sim_file) as f:
- for line in f.readlines():
- cosim_results.append([r for r in line.split()])
+ cosim_results = _parse_rtl_cosim_results(hls_dir)
+ if cosim_results is not None:
report['CosimResults'] = cosim_results
- syn_file = sln_dir + '/' + solutions[0] + f'/syn/report/{top_func_name}_csynth.xml'
- c_synth_report = {}
- if os.path.isfile(syn_file):
- root = ET.parse(syn_file).getroot()
-
- # Performance
- perf_node = root.find('./PerformanceEstimates')
- c_synth_report['TargetClockPeriod'] = root.find('./UserAssignments/TargetClockPeriod').text
- c_synth_report['EstimatedClockPeriod'] = perf_node.find('./SummaryOfTimingAnalysis/EstimatedClockPeriod').text
- c_synth_report['BestLatency'] = perf_node.find('./SummaryOfOverallLatency/Best-caseLatency').text
- c_synth_report['WorstLatency'] = perf_node.find('./SummaryOfOverallLatency/Worst-caseLatency').text
- c_synth_report['IntervalMin'] = perf_node.find('./SummaryOfOverallLatency/Interval-min').text
- c_synth_report['IntervalMax'] = perf_node.find('./SummaryOfOverallLatency/Interval-max').text
- # Area
- area_node = root.find('./AreaEstimates')
- for child in area_node.find('./Resources'):
- # DSPs are called 'DSP48E' in Vivado and just 'DSP' in Vitis. Overriding here to have consistent keys
- if child.tag == 'DSP48E':
- child.tag = 'DSP'
- c_synth_report[child.tag] = child.text
- for child in area_node.find('./AvailableResources'):
- if child.tag == 'DSP48E':
- child.tag = 'DSP'
- c_synth_report['Available' + child.tag] = child.text
+ c_synth_report = _parse_csynthesis_report(sln_dir, solution, top_func_name)
+ if c_synth_report is not None:
report['CSynthesisReport'] = c_synth_report
else:
print('CSynthesis report not found.')
- vivado_syn_file = hls_dir + '/vivado_synth.rpt'
- if os.path.isfile(vivado_syn_file):
- vivado_synth_rpt = {}
- with open(vivado_syn_file) as f:
- section = 0
- for line in f.readlines():
- match = re.match(r'^(\d)\.', line)
- if match:
- section = int(match.group(1))
- # Sometimes, phrases such as 'CLB Registers' can show up in the non-tabular sections of the report
- if '|' in line:
- # CLB (2019.X) vs. Slice (2020.X)
- if ('CLB LUTs' in line or 'Slice LUTs' in line) and section == 1:
- vivado_synth_rpt['LUT'] = line.split('|')[2].strip()
- elif ('CLB Registers' in line or 'Slice Registers' in line) and section == 1:
- vivado_synth_rpt['FF'] = line.split('|')[2].strip()
- elif 'Block RAM Tile' in line and section == 2:
- vivado_synth_rpt['BRAM_18K'] = line.split('|')[2].strip()
- elif 'URAM' in line and section == 2:
- vivado_synth_rpt['URAM'] = line.split('|')[2].strip()
- elif 'DSPs' in line and section == 3:
- vivado_synth_rpt['DSP48E'] = line.split('|')[2].strip()
+ vivado_synth_rpt = _parse_vivado_synth_report(hls_dir)
+ if vivado_synth_rpt is not None:
report['VivadoSynthReport'] = vivado_synth_rpt
else:
print('Vivado synthesis report not found.')
- cosim_file = sln_dir + '/' + solutions[0] + f'/sim/report/{top_func_name}_cosim.rpt'
- if os.path.isfile(cosim_file):
- cosim_report = {}
- with open(cosim_file) as f:
- for line in f.readlines():
- if re.search('VHDL', line) or re.search('Verilog', line):
- result = line[1:].split() # [1:] skips the leading '|'
- result = [res[:-1] if res[-1] == '|' else res for res in result]
- # RTL, Status, Latency-min, Latency-avg, Latency-max, Interval-min, Interval-avg, Interval-max
- if result[1] == 'NA':
- continue
- else:
- cosim_report['RTL'] = result[0]
- cosim_report['Status'] = result[1]
- cosim_report['LatencyMin'] = result[2]
- cosim_report['LatencyMax'] = result[4]
- cosim_report['IntervalMin'] = result[5]
- cosim_report['IntervalMax'] = result[7]
- report['CosimReport'] = cosim_report
+ transaction_data = _parse_transaction_file(sln_dir, solution, 'verilog', top_func_name)
+ if transaction_data is not None:
+ report['CosimReport'] = {
+ 'RTL': 'Verilog',
+ 'Status': 'PASS',
+ **transaction_data,
+ }
else:
print('Cosim report not found.')
- if os.path.isfile(cosim_file):
- transaction_file = (
- sln_dir
- + '/'
- + solutions[0]
- + '/sim/'
- + report['CosimReport']['RTL'].lower()
- + '/'
- + top_func_name
- + '.performance.result.transaction.xml'
- )
- if os.path.isfile(transaction_file):
- cosim_transactions = {
- 'InitiationInterval': {'max': 0, 'min': sys.maxsize, 'avg': 0.0},
- 'Latency': {'max': 0, 'min': sys.maxsize, 'avg': 0.0},
- }
- with open(transaction_file) as f:
- i = 1
- for line in f.readlines():
- if re.search('transaction', line):
- result = line.split()
- # update min
- if result[3] != 'x':
- cosim_transactions['InitiationInterval']['min'] = (
- int(result[3])
- if int(result[3]) < cosim_transactions['InitiationInterval']['min']
- else cosim_transactions['InitiationInterval']['min']
- )
- cosim_transactions['Latency']['min'] = (
- int(result[2])
- if int(result[2]) < cosim_transactions['Latency']['min']
- else cosim_transactions['Latency']['min']
- )
- # update max
- if result[3] != 'x':
- cosim_transactions['InitiationInterval']['max'] = (
- int(result[3])
- if int(result[3]) > cosim_transactions['InitiationInterval']['max']
- else cosim_transactions['InitiationInterval']['max']
- )
- cosim_transactions['Latency']['max'] = (
- int(result[2])
- if int(result[2]) > cosim_transactions['Latency']['max']
- else cosim_transactions['Latency']['max']
- )
- # update avg
- if result[3] != 'x':
- cosim_transactions['InitiationInterval']['avg'] = cosim_transactions['InitiationInterval'][
- 'avg'
- ] + float((int(result[3]) - cosim_transactions['InitiationInterval']['avg']) / i)
- cosim_transactions['Latency']['avg'] = cosim_transactions['Latency']['avg'] + float(
- (int(result[2]) - cosim_transactions['Latency']['avg']) / i
- )
- i += 1
-
- report['CosimReport']['LatencyMin'] = cosim_transactions['Latency']['min']
- report['CosimReport']['LatencyMax'] = cosim_transactions['Latency']['max']
- report['CosimReport']['LatencyAvg'] = cosim_transactions['Latency']['avg']
-
- report['CosimReport']['IntervalMin'] = cosim_transactions['InitiationInterval']['min']
- report['CosimReport']['IntervalMax'] = cosim_transactions['InitiationInterval']['max']
- report['CosimReport']['IntervalAvg'] = cosim_transactions['InitiationInterval']['avg']
-
- util_rpt_file = hls_dir + '/util.rpt'
- if os.path.isfile(util_rpt_file):
- implementation_report = {}
- with open(util_rpt_file) as f:
- for line in f.readlines():
- if re.search(r'\(top\)', line):
- # Total LUTs | Logic LUTs | LUTRAMs | SRLs | FFs | RAMB36 | RAMB18 (| URAM )| DSP48 Blocks
- # skipping the first 2 unuseful cells with [:2]
- results = [_get_abs_and_percentage_values(elem) for elem in line.replace('|', '').split()[2:]]
- implementation_report['TotLUTs'] = results[0][0]
- implementation_report['TotLUTs%'] = results[0][1]
-
- implementation_report['LogicLUTs'] = results[1][0]
- implementation_report['LogicLUTs%'] = results[1][1]
-
- implementation_report['LUTRAMs'] = results[2][0]
- implementation_report['LUTRAMs%'] = results[2][1]
-
- implementation_report['SRLs'] = results[3][0]
- implementation_report['SRLs%'] = results[3][1]
-
- implementation_report['FFs'] = results[4][0]
- implementation_report['FFs%'] = results[4][1]
-
- implementation_report['RAMB36s'] = results[5][0]
- implementation_report['RAMB36s%'] = results[5][1]
-
- implementation_report['RAMB18s'] = results[6][0]
- implementation_report['RAMB18s%'] = results[6][1]
-
- if len(results) == 9:
- implementation_report['URAMs'] = results[7][0]
- implementation_report['URAMs%'] = results[7][1]
-
- implementation_report['DSPs'] = results[8][0]
- implementation_report['DSPs%'] = results[8][1]
- else:
- implementation_report['DSPs'] = results[7][0]
- implementation_report['DSPs%'] = results[7][1]
- report['ImplementationReport'] = implementation_report
- else:
- print('Implementation report not found.')
-
- timing_report_file = (
- hls_dir
- + '/'
- + prj_dir.split('_')[0]
- + '_vivado_accelerator/project_1.runs/impl_1/design_1_wrapper_timing_summary_routed.rpt'
- )
- if os.path.isfile(timing_report_file):
- timing_report = {}
- with open(timing_report_file) as f:
- while not re.search('WNS', next(f)):
- pass
- # skip the successive line
- next(f)
- result = next(f).split()
-
- timing_report['WNS'] = float(result[0])
- timing_report['TNS'] = float(result[1])
- timing_report['WHS'] = float(result[4])
- timing_report['THS'] = float(result[5])
- timing_report['WPWS'] = float(result[8])
- timing_report['TPWS'] = float(result[9])
+ implementation_report = _parse_implementation_report(hls_dir, is_vivado_accelerator)
+ if implementation_report is not None:
+ report['ImplementationReport'] = implementation_report
+ else:
+ print('Implementation report not found.')
+ timing_report = _parse_timing_report(hls_dir, is_vivado_accelerator)
+ if timing_report is not None:
report['TimingReport'] = timing_report
else:
print('Timing report not found.')
+
+ power_report = _parse_power_report(hls_dir, is_vivado_accelerator)
+ if power_report is not None:
+ report['PowerReport'] = power_report
+ else:
+ print('Power report not found.')
+
return report
@@ -538,7 +661,7 @@ def _make_report_body(report_dict, make_table_template, make_header_template):
csynth_report = report_dict['CSynthesisReport']
target_clock = float(csynth_report['TargetClockPeriod'])
best_latency = int(csynth_report['BestLatency'])
- worst_latency = int(csynth_report['BestLatency'])
+ worst_latency = int(csynth_report['WorstLatency'])
bram = int(csynth_report['BRAM_18K'])
avail_bram = int(csynth_report['AvailableBRAM_18K'])
dsp = int(csynth_report['DSP'])
@@ -671,6 +794,24 @@ def _make_report_body(report_dict, make_table_template, make_header_template):
body = body.format(**params)
+ if 'PowerReport' in report_dict:
+ body += make_header_template('Power report')
+ perf_rows = {
+ 'Total On-Chip Power (W)': 'total',
+ 'Dynamic (W)': 'dynamic',
+ 'Device Static (W)': 'static',
+ }
+ body += make_table_template('Power', perf_rows)
+
+ power_report = report_dict['PowerReport']
+
+ params = {}
+ params['total'] = power_report.get('TotalOnChipPower', 'N/A')
+ params['dynamic'] = power_report.get('Dynamic', 'N/A')
+ params['static'] = power_report.get('Static', 'N/A')
+
+ body = body.format(**params)
+
return body
diff --git a/hls4ml/templates/bambu/ac_types b/hls4ml/templates/bambu/ac_types
new file mode 160000
index 0000000000..abb773b4d5
--- /dev/null
+++ b/hls4ml/templates/bambu/ac_types
@@ -0,0 +1 @@
+Subproject commit abb773b4d5152dd96ed8a3da1efde82d66991cdc
diff --git a/hls4ml/templates/bambu/build_bambu.sh b/hls4ml/templates/bambu/build_bambu.sh
new file mode 100644
index 0000000000..2b2e735632
--- /dev/null
+++ b/hls4ml/templates/bambu/build_bambu.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+# HLS4ML insert_bambu_command BEGIN
+# HLS4ML insert_bambu_command END
+
+# HLS4ML insert_final_report_copying BEGIN
+# HLS4ML insert_final_report_copying END
diff --git a/hls4ml/templates/bambu/build_lib.sh b/hls4ml/templates/bambu/build_lib.sh
new file mode 100755
index 0000000000..d606579aa5
--- /dev/null
+++ b/hls4ml/templates/bambu/build_lib.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+APPIMAGE="bambu"
+
+TMPINFO=""
+MOUNT_PID=""
+MOUNT_DIR=""
+
+cleanup() {
+ if [ -n "${MOUNT_PID:-}" ]; then kill "$MOUNT_PID" 2>/dev/null || true; fi
+ if [ -n "${TMPINFO:-}" ]; then rm -f "$TMPINFO" || true; fi
+}
+trap cleanup EXIT
+
+# 1. Use BAMBU_SQUASHFS_ROOT if set by setup-bambu.sh (extracted AppImage)
+if [ -n "${BAMBU_SQUASHFS_ROOT:-}" ] && [ -d "$BAMBU_SQUASHFS_ROOT" ]; then
+ MOUNT_DIR="$BAMBU_SQUASHFS_ROOT"
+fi
+
+# 2. Try --appimage-mount (actual AppImage on PATH)
+if [ -z "$MOUNT_DIR" ] && command -v "$APPIMAGE" >/dev/null 2>&1; then
+ TMPINFO="$(mktemp)"
+ "$APPIMAGE" --appimage-mount >"$TMPINFO" 2>&1 &
+ MOUNT_PID=$!
+
+ for _ in {1..100}; do
+ if [ -s "$TMPINFO" ]; then
+ MOUNT_DIR=$(sed -n '1p' "$TMPINFO" | tr -d '\r\n')
+ if [ -d "$MOUNT_DIR" ]; then break; fi
+ fi
+ sleep 0.05
+ done
+
+ if [ ! -d "$MOUNT_DIR" ]; then
+ MOUNT_DIR=""
+ MOUNT_PID=""
+ fi
+fi
+
+# 3. Derive from extracted squashfs via which bambu → up 3 dirs
+if [ -z "$MOUNT_DIR" ]; then
+ BIN_PATH="$(which "$APPIMAGE" 2>/dev/null || true)"
+ if [ -n "$BIN_PATH" ]; then
+ APPDIR="$(dirname "$(dirname "$(dirname "$BIN_PATH")")")"
+ if [ -x "$APPDIR/usr/bin/clang++-16" ] || \
+ [ -x "$APPDIR/usr/compilers/clang-16/bin/clang++-16" ]; then
+ MOUNT_DIR="$APPDIR"
+ fi
+ fi
+fi
+
+# Locate clang++-16 — required, no fallback to g++
+CC=""
+if [ -n "$MOUNT_DIR" ]; then
+ for candidate in \
+ "$MOUNT_DIR/usr/compilers/clang-16/bin/clang++-16" \
+ "$MOUNT_DIR/usr/bin/clang++-16"
+ do
+ if [ -x "$candidate" ]; then CC="$candidate"; break; fi
+ done
+fi
+if [ -z "$CC" ]; then
+ echo "ERROR: Bambu clang++-16 not found. Set BAMBU_SQUASHFS_ROOT or ensure bambu AppImage is on PATH." >&2
+ exit 1
+fi
+
+echo "Using compiler: $($CC --version | head -n1)"
+
+CFLAGS="-O3 -fPIC"
+
+# Include -std=c++23 if the compiler supports it (enables half and bfloat16 types, errors otherwise)
+if echo "" | $CC -Werror -fsyntax-only -std=c++23 -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -std=c++23"
+else
+ CFLAGS+=" -std=c++14"
+fi
+
+# Include -fno-gnu-unique if it is there
+if echo "" | $CC -Werror -fsyntax-only -fno-gnu-unique -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -fno-gnu-unique"
+fi
+
+LDFLAGS=""
+INCFLAGS="-isystem ${MOUNT_DIR}/usr/include/panda"
+PROJECT="myproject"
+LIB_STAMP="mystamp"
+BASEDIR="$(cd "$(dirname "$0")" && pwd)"
+WEIGHTS_DIR="\"${BASEDIR}/firmware/weights\""
+
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c firmware/${PROJECT}.cpp -o ${PROJECT}.o
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c ${PROJECT}_bridge.cpp -o ${PROJECT}_bridge.o
+$CC $CFLAGS $INCFLAGS -shared ${PROJECT}.o ${PROJECT}_bridge.o -o firmware/${PROJECT}-${LIB_STAMP}.so
+
+rm -f *.o
+
+echo "Done."
diff --git a/hls4ml/templates/bambu/build_lib_float.sh b/hls4ml/templates/bambu/build_lib_float.sh
new file mode 100644
index 0000000000..4521bf1c6d
--- /dev/null
+++ b/hls4ml/templates/bambu/build_lib_float.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+APPIMAGE="bambu"
+
+TMPINFO=""
+MOUNT_PID=""
+MOUNT_DIR=""
+
+cleanup() {
+ if [ -n "${MOUNT_PID:-}" ]; then kill "$MOUNT_PID" 2>/dev/null || true; fi
+ if [ -n "${TMPINFO:-}" ]; then rm -f "$TMPINFO" || true; fi
+}
+trap cleanup EXIT
+
+# 1. Use BAMBU_SQUASHFS_ROOT if set by setup-bambu.sh (extracted AppImage)
+if [ -n "${BAMBU_SQUASHFS_ROOT:-}" ] && [ -d "$BAMBU_SQUASHFS_ROOT" ]; then
+ MOUNT_DIR="$BAMBU_SQUASHFS_ROOT"
+fi
+
+# 2. Try --appimage-mount (actual AppImage on PATH)
+if [ -z "$MOUNT_DIR" ] && command -v "$APPIMAGE" >/dev/null 2>&1; then
+ TMPINFO="$(mktemp)"
+ "$APPIMAGE" --appimage-mount >"$TMPINFO" 2>&1 &
+ MOUNT_PID=$!
+
+ for _ in {1..100}; do
+ if [ -s "$TMPINFO" ]; then
+ MOUNT_DIR=$(sed -n '1p' "$TMPINFO" | tr -d '\r\n')
+ if [ -d "$MOUNT_DIR" ]; then break; fi
+ fi
+ sleep 0.05
+ done
+
+ if [ ! -d "$MOUNT_DIR" ]; then
+ MOUNT_DIR=""
+ MOUNT_PID=""
+ fi
+fi
+
+# 3. Derive from extracted squashfs via which bambu → up 3 dirs
+if [ -z "$MOUNT_DIR" ]; then
+ BIN_PATH="$(which "$APPIMAGE" 2>/dev/null || true)"
+ if [ -n "$BIN_PATH" ]; then
+ APPDIR="$(dirname "$(dirname "$(dirname "$BIN_PATH")")")"
+ if [ -x "$APPDIR/usr/bin/clang++-16" ] || \
+ [ -x "$APPDIR/usr/compilers/clang-16/bin/clang++-16" ]; then
+ MOUNT_DIR="$APPDIR"
+ fi
+ fi
+fi
+
+# Locate clang++-16 — required, no fallback to g++
+CC=""
+if [ -n "$MOUNT_DIR" ]; then
+ for candidate in \
+ "$MOUNT_DIR/usr/compilers/clang-16/bin/clang++-16" \
+ "$MOUNT_DIR/usr/bin/clang++-16"
+ do
+ if [ -x "$candidate" ]; then CC="$candidate"; break; fi
+ done
+fi
+if [ -z "$CC" ]; then
+ echo "ERROR: Bambu clang++-16 not found. Set BAMBU_SQUASHFS_ROOT or ensure bambu AppImage is on PATH." >&2
+ exit 1
+fi
+
+echo "Using compiler: $($CC --version | head -n1)"
+
+CFLAGS="-O3 -fPIC"
+
+# Include -std=c++23 if the compiler supports it (enables half and bfloat16 types, errors otherwise)
+if echo "" | $CC -Werror -fsyntax-only -std=c++23 -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -std=c++23"
+else
+ CFLAGS+=" -std=c++14"
+fi
+
+# Include -fno-gnu-unique if it is there
+if echo "" | $CC -Werror -fsyntax-only -fno-gnu-unique -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -fno-gnu-unique"
+fi
+
+LDFLAGS=""
+INCFLAGS="-isystem ${MOUNT_DIR}/usr/include/panda"
+PROJECT="myproject"
+LIB_STAMP="mystamp"
+BASEDIR="$(cd "$(dirname "$0")" && pwd)"
+WEIGHTS_DIR="\"${BASEDIR}/firmware/weights\""
+
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c firmware/${PROJECT}.cpp -o ${PROJECT}.o
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c firmware/${PROJECT}_float.cpp -o ${PROJECT}_float.o
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c ${PROJECT}_bridge.cpp -o ${PROJECT}_bridge.o
+$CC $CFLAGS $INCFLAGS -shared ${PROJECT}.o ${PROJECT}_float.o ${PROJECT}_bridge.o -o firmware/${PROJECT}-${LIB_STAMP}.so
+
+rm -f *.o
+
+echo "Done."
diff --git a/hls4ml/templates/bambu/build_lib_multigraph.sh b/hls4ml/templates/bambu/build_lib_multigraph.sh
new file mode 100644
index 0000000000..3884f581a3
--- /dev/null
+++ b/hls4ml/templates/bambu/build_lib_multigraph.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+set -e
+
+CC=g++
+if [[ "$OSTYPE" == "linux-gnu" ]]; then
+ CFLAGS="-O3 -fPIC -std=c++11 -fno-gnu-unique"
+elif [[ "$OSTYPE" == "darwin"* ]]; then
+ CFLAGS="-O3 -fPIC -std=c++11"
+fi
+
+graph_project_names=(mygraph_name_list)
+
+LDFLAGS=
+ORIGINAL_PROJECT=myproject
+PROJECT=myproject_stitched
+LIB_STAMP=mystamp
+BASEDIR="$(cd "$(dirname "$0")" && cd .. && pwd)"
+INCFLAGS=""
+OUTPUT_DIR="${BASEDIR}/stitched/firmware"
+WEIGHTS_DIR="\"${BASEDIR}/stitched/firmware/weights\""
+
+mkdir -p "${OUTPUT_DIR}"
+
+# Compile all graphs in parallel
+OBJECT_FILES=()
+PIDS=()
+
+for g in "${graph_project_names[@]}"; do
+ SRC_FILE="${g}/firmware/${ORIGINAL_PROJECT}_${g}.cpp"
+ OBJ_FILE="${ORIGINAL_PROJECT}_${g}.o"
+ AC_TYPES_PATH="-I${BASEDIR}/${g}/firmware/ac_types/include"
+ (
+ ${CC} ${CFLAGS} ${AC_TYPES_PATH} -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c "${BASEDIR}/${SRC_FILE}" -o "${OBJ_FILE}"
+ ) &
+ PIDS+=($!)
+ OBJECT_FILES+=("${OBJ_FILE}")
+ INCFLAGS+="-I${BASEDIR}/${g}/ "
+done
+
+for pid in "${PIDS[@]}"; do
+ wait $pid
+done
+
+AC_TYPES_PATH="-I${BASEDIR}/${graph_project_names[@]: -1}/firmware/ac_types/include"
+
+${CC} ${CFLAGS} ${INCFLAGS} ${AC_TYPES_PATH} -c "${PROJECT}_bridge.cpp" -o ${PROJECT}_bridge.o
+${CC} ${CFLAGS} ${INCFLAGS} ${AC_TYPES_PATH} -shared "${OBJECT_FILES[@]}" ${PROJECT}_bridge.o -o "${OUTPUT_DIR}/${PROJECT}-${LIB_STAMP}.so"
+
+rm -f "${OBJECT_FILES[@]}"
+rm -f ${PROJECT}_bridge.o
diff --git a/hls4ml/templates/bambu/build_tb_exe.sh b/hls4ml/templates/bambu/build_tb_exe.sh
new file mode 100644
index 0000000000..68430104ea
--- /dev/null
+++ b/hls4ml/templates/bambu/build_tb_exe.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+APPIMAGE="bambu"
+
+TMPINFO=""
+MOUNT_PID=""
+MOUNT_DIR=""
+
+cleanup() {
+ if [ -n "${MOUNT_PID:-}" ]; then kill "$MOUNT_PID" 2>/dev/null || true; fi
+ if [ -n "${TMPINFO:-}" ]; then rm -f "$TMPINFO" || true; fi
+}
+trap cleanup EXIT
+
+# 1. Use BAMBU_SQUASHFS_ROOT if set by setup-bambu.sh (extracted AppImage)
+if [ -n "${BAMBU_SQUASHFS_ROOT:-}" ] && [ -d "$BAMBU_SQUASHFS_ROOT" ]; then
+ MOUNT_DIR="$BAMBU_SQUASHFS_ROOT"
+fi
+
+# 2. Try --appimage-mount (actual AppImage on PATH)
+if [ -z "$MOUNT_DIR" ] && command -v "$APPIMAGE" >/dev/null 2>&1; then
+ TMPINFO="$(mktemp)"
+ "$APPIMAGE" --appimage-mount >"$TMPINFO" 2>&1 &
+ MOUNT_PID=$!
+
+ for _ in {1..100}; do
+ if [ -s "$TMPINFO" ]; then
+ MOUNT_DIR=$(sed -n '1p' "$TMPINFO" | tr -d '\r\n')
+ if [ -d "$MOUNT_DIR" ]; then break; fi
+ fi
+ sleep 0.05
+ done
+
+ if [ ! -d "$MOUNT_DIR" ]; then
+ MOUNT_DIR=""
+ MOUNT_PID=""
+ fi
+fi
+
+# 3. Derive from extracted squashfs via which bambu → up 3 dirs
+if [ -z "$MOUNT_DIR" ]; then
+ BIN_PATH="$(which "$APPIMAGE" 2>/dev/null || true)"
+ if [ -n "$BIN_PATH" ]; then
+ APPDIR="$(dirname "$(dirname "$(dirname "$BIN_PATH")")")"
+ if [ -x "$APPDIR/usr/bin/clang++-16" ] || \
+ [ -x "$APPDIR/usr/compilers/clang-16/bin/clang++-16" ]; then
+ MOUNT_DIR="$APPDIR"
+ fi
+ fi
+fi
+
+# Locate clang++-16 — required, no fallback to g++
+CC=""
+if [ -n "$MOUNT_DIR" ]; then
+ for candidate in \
+ "$MOUNT_DIR/usr/compilers/clang-16/bin/clang++-16" \
+ "$MOUNT_DIR/usr/bin/clang++-16"
+ do
+ if [ -x "$candidate" ]; then CC="$candidate"; break; fi
+ done
+fi
+if [ -z "$CC" ]; then
+ echo "ERROR: Bambu clang++-16 not found. Set BAMBU_SQUASHFS_ROOT or ensure bambu AppImage is on PATH." >&2
+ exit 1
+fi
+
+echo "Using compiler: $($CC --version | head -n1)"
+
+CFLAGS="-O3 -fPIC"
+
+# Include -std=c++23 if the compiler supports it (enables half and bfloat16 types, errors otherwise)
+if echo "" | $CC -Werror -fsyntax-only -std=c++23 -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -std=c++23"
+else
+ CFLAGS+=" -std=c++14"
+fi
+
+# Include -fno-gnu-unique if it is there
+if echo "" | $CC -Werror -fsyntax-only -fno-gnu-unique -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -fno-gnu-unique"
+fi
+
+LDFLAGS=""
+INCFLAGS="-isystem ${MOUNT_DIR}/usr/include/panda"
+PROJECT="myproject"
+LIB_STAMP="mystamp"
+BASEDIR="$(cd "$(dirname "$0")" && pwd)"
+WEIGHTS_DIR="\"${BASEDIR}/firmware/weights\""
+
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c firmware/${PROJECT}.cpp -o ${PROJECT}.o
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c ${PROJECT}_test.cpp -o ${PROJECT}_test.o
+$CC ${PROJECT}.o ${PROJECT}_test.o -o ${PROJECT}-${LIB_STAMP}_tb.exe
+
+rm -f *.o
+
+echo "Executable built: ${PROJECT}-${LIB_STAMP}_tb.exe"
diff --git a/hls4ml/templates/bambu/build_tb_float_exe.sh b/hls4ml/templates/bambu/build_tb_float_exe.sh
new file mode 100644
index 0000000000..5778e89def
--- /dev/null
+++ b/hls4ml/templates/bambu/build_tb_float_exe.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+APPIMAGE="bambu"
+
+TMPINFO=""
+MOUNT_PID=""
+MOUNT_DIR=""
+
+cleanup() {
+ if [ -n "${MOUNT_PID:-}" ]; then kill "$MOUNT_PID" 2>/dev/null || true; fi
+ if [ -n "${TMPINFO:-}" ]; then rm -f "$TMPINFO" || true; fi
+}
+trap cleanup EXIT
+
+# 1. Use BAMBU_SQUASHFS_ROOT if set by setup-bambu.sh (extracted AppImage)
+if [ -n "${BAMBU_SQUASHFS_ROOT:-}" ] && [ -d "$BAMBU_SQUASHFS_ROOT" ]; then
+ MOUNT_DIR="$BAMBU_SQUASHFS_ROOT"
+fi
+
+# 2. Try --appimage-mount (actual AppImage on PATH)
+if [ -z "$MOUNT_DIR" ] && command -v "$APPIMAGE" >/dev/null 2>&1; then
+ TMPINFO="$(mktemp)"
+ "$APPIMAGE" --appimage-mount >"$TMPINFO" 2>&1 &
+ MOUNT_PID=$!
+
+ for _ in {1..100}; do
+ if [ -s "$TMPINFO" ]; then
+ MOUNT_DIR=$(sed -n '1p' "$TMPINFO" | tr -d '\r\n')
+ if [ -d "$MOUNT_DIR" ]; then break; fi
+ fi
+ sleep 0.05
+ done
+
+ if [ ! -d "$MOUNT_DIR" ]; then
+ MOUNT_DIR=""
+ MOUNT_PID=""
+ fi
+fi
+
+# 3. Derive from extracted squashfs via which bambu → up 3 dirs
+if [ -z "$MOUNT_DIR" ]; then
+ BIN_PATH="$(which "$APPIMAGE" 2>/dev/null || true)"
+ if [ -n "$BIN_PATH" ]; then
+ APPDIR="$(dirname "$(dirname "$(dirname "$BIN_PATH")")")"
+ if [ -x "$APPDIR/usr/bin/clang++-16" ] || \
+ [ -x "$APPDIR/usr/compilers/clang-16/bin/clang++-16" ]; then
+ MOUNT_DIR="$APPDIR"
+ fi
+ fi
+fi
+
+# Locate clang++-16 — required, no fallback to g++
+CC=""
+if [ -n "$MOUNT_DIR" ]; then
+ for candidate in \
+ "$MOUNT_DIR/usr/compilers/clang-16/bin/clang++-16" \
+ "$MOUNT_DIR/usr/bin/clang++-16"
+ do
+ if [ -x "$candidate" ]; then CC="$candidate"; break; fi
+ done
+fi
+if [ -z "$CC" ]; then
+ echo "ERROR: Bambu clang++-16 not found. Set BAMBU_SQUASHFS_ROOT or ensure bambu AppImage is on PATH." >&2
+ exit 1
+fi
+
+echo "Using compiler: $($CC --version | head -n1)"
+
+CFLAGS="-O3 -fPIC"
+
+# Include -std=c++23 if the compiler supports it (enables half and bfloat16 types, errors otherwise)
+if echo "" | $CC -Werror -fsyntax-only -std=c++23 -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -std=c++23"
+else
+ CFLAGS+=" -std=c++14"
+fi
+
+# Include -fno-gnu-unique if it is there
+if echo "" | $CC -Werror -fsyntax-only -fno-gnu-unique -xc++ - -o /dev/null &>/dev/null; then
+ CFLAGS+=" -fno-gnu-unique"
+fi
+
+LDFLAGS=""
+INCFLAGS="-isystem ${MOUNT_DIR}/usr/include/panda"
+PROJECT="myproject"
+LIB_STAMP="mystamp"
+BASEDIR="$(cd "$(dirname "$0")" && pwd)"
+WEIGHTS_DIR="\"${BASEDIR}/firmware/weights\""
+
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c firmware/${PROJECT}.cpp -o ${PROJECT}.o
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c firmware/${PROJECT}_float.cpp -o ${PROJECT}_float.o
+$CC $CFLAGS $INCFLAGS -D WEIGHTS_DIR="${WEIGHTS_DIR}" -c ${PROJECT}_float_test.cpp -o ${PROJECT}_float_test.o
+$CC ${PROJECT}.o ${PROJECT}_float.o ${PROJECT}_float_test.o -o ${PROJECT}-${LIB_STAMP}_float_tb.exe
+
+rm -f *.o
+
+echo "Executable built: ${PROJECT}-${LIB_STAMP}_float_tb.exe"
diff --git a/hls4ml/templates/bambu/firmware/defines.h b/hls4ml/templates/bambu/firmware/defines.h
new file mode 100644
index 0000000000..8013d891a7
--- /dev/null
+++ b/hls4ml/templates/bambu/firmware/defines.h
@@ -0,0 +1,23 @@
+#ifndef DEFINES_H_
+#define DEFINES_H_
+
+#include "ap_fixed.h"
+#include "ap_int.h"
+#include "nnet_utils/nnet_types.h"
+#include
+#include
+#include
+#include
+// hls-fpga-machine-learning insert headers
+
+// hls-fpga-machine-learning insert namespace-start
+
+// hls-fpga-machine-learning insert numbers
+
+// hls-fpga-machine-learning insert layer-precision
+
+// hls-fpga-machine-learning insert emulator-defines
+
+// hls-fpga-machine-learning insert namespace-end
+
+#endif
diff --git a/hls4ml/templates/bambu/firmware/myproject.cpp b/hls4ml/templates/bambu/firmware/myproject.cpp
new file mode 100644
index 0000000000..5ba7f118ba
--- /dev/null
+++ b/hls4ml/templates/bambu/firmware/myproject.cpp
@@ -0,0 +1,23 @@
+#include
+
+#include "myproject.h"
+#include "parameters.h"
+
+// hls-fpga-machine-learning insert namespace-start
+
+void myproject(
+ // hls-fpga-machine-learning insert header
+) {
+
+ // hls-fpga-machine-learning insert IO
+
+ // hls-fpga-machine-learning insert load weights
+
+ // ****************************************
+ // NETWORK INSTANTIATION
+ // ****************************************
+
+ // hls-fpga-machine-learning insert layers
+}
+
+// hls-fpga-machine-learning insert namespace-end
diff --git a/hls4ml/templates/bambu/firmware/myproject.h b/hls4ml/templates/bambu/firmware/myproject.h
new file mode 100644
index 0000000000..899b85b484
--- /dev/null
+++ b/hls4ml/templates/bambu/firmware/myproject.h
@@ -0,0 +1,21 @@
+#ifndef MYPROJECT_H_
+#define MYPROJECT_H_
+
+#include "ap_fixed.h"
+#include "ap_int.h"
+#include "hls_stream.h"
+
+#include "defines.h"
+
+// hls-fpga-machine-learning insert namespace-start
+
+// Prototype of top level function for C-synthesis
+void myproject(
+ // hls-fpga-machine-learning insert header
+);
+
+// hls-fpga-machine-learning insert emulator-defines
+
+// hls-fpga-machine-learning insert namespace-end
+
+#endif
diff --git a/hls4ml/templates/bambu/firmware/myproject_float.h b/hls4ml/templates/bambu/firmware/myproject_float.h
new file mode 100644
index 0000000000..89e4278a8f
--- /dev/null
+++ b/hls4ml/templates/bambu/firmware/myproject_float.h
@@ -0,0 +1,11 @@
+#ifndef MYPROJECT_FLOAT_H_
+#define MYPROJECT_FLOAT_H_
+
+#include "ac_int.h"
+// hls-fpga-machine-learning insert float-includes
+
+// hls-fpga-machine-learning insert definitions
+
+// hls-fpga-machine-learning insert float-signature
+
+#endif
diff --git a/hls4ml/templates/bambu/firmware/parameters.h b/hls4ml/templates/bambu/firmware/parameters.h
new file mode 100644
index 0000000000..614020ddea
--- /dev/null
+++ b/hls4ml/templates/bambu/firmware/parameters.h
@@ -0,0 +1,19 @@
+#ifndef PARAMETERS_H_
+#define PARAMETERS_H_
+
+#include "ap_fixed.h"
+#include "ap_int.h"
+
+#include "nnet_utils/nnet_code_gen.h"
+#include "nnet_utils/nnet_helpers.h"
+// hls-fpga-machine-learning insert includes
+
+// hls-fpga-machine-learning insert weights
+
+// hls-fpga-machine-learning insert namespace-start
+
+// hls-fpga-machine-learning insert layer-config
+
+// hls-fpga-machine-learning insert namespace-end
+
+#endif
diff --git a/hls4ml/templates/bambu/ip_stitcher.tcl b/hls4ml/templates/bambu/ip_stitcher.tcl
new file mode 100644
index 0000000000..879ad44ffe
--- /dev/null
+++ b/hls4ml/templates/bambu/ip_stitcher.tcl
@@ -0,0 +1,658 @@
+# ======================================================
+# The script connects the output ports of each subgraph IP
+# instance to the input ports of the next one in sequence,
+# and makes important signals as external
+#
+# Run this script from the base directory containing the
+# subgraph project folders (e.g., {proj_name}_graph1, etc.)
+# ======================================================
+
+puts "###########################################################"
+
+array set opt {
+ stitch_design 1
+ sim_design 0
+ export_design 0
+ stitch_project_name ""
+ original_project_name ""
+ sim_verilog_file ""
+}
+
+foreach arg $::argv {
+ if {[regexp {([^=]+)=(.*)} $arg -> key value]} {
+ if {[info exists opt($key)]} {
+ set opt($key) $value
+ } else {
+ puts "Warning: Unknown option $key"
+ }
+ } else {
+ puts "Warning: Ignoring argument $arg"
+ }
+}
+
+set stitch_design [expr {$opt(stitch_design)}]
+set sim_design [expr {$opt(sim_design)}]
+set export_design [expr {$opt(export_design)}]
+set sim_verilog_file $opt(sim_verilog_file)
+set stitch_project_name $opt(stitch_project_name)
+set original_project_name $opt(original_project_name)
+
+# Project base dir
+set base_dir [pwd]
+set original_project_path "$base_dir/../../"
+puts $base_dir
+# Name of the block design
+set bd_name "stitched_design"
+
+# Find a directory that ends with "graph1", "graph2", etc. in the parent project folder
+set project_dirs [glob -nocomplain -directory $original_project_path *graph[0-9]]
+
+# Check if a matching directory is found
+if {[llength $project_dirs] == 0} {
+ puts "Error: No project directory ending with 'graph{id}' found in $original_project_path"
+} else {
+ # Get the first matching directory
+ set project_dir [lindex $project_dirs 0]
+ set project_tcl_file [file join $project_dir project.tcl]
+
+ # Check if project.tcl exists and source it
+ if {[file exists $project_tcl_file]} {
+ puts "Sourcing $project_tcl_file from $project_dir"
+ source $project_tcl_file
+ } else {
+ puts "Error: project.tcl not found in $project_dir"
+ exit 1
+ }
+}
+
+# Procedure for stitching the project
+proc stitch_procedure {base_dir stitch_project_name original_project_name bd_name part} {
+
+ puts "###########################################################"
+ puts "# Starting the IP connection process... "
+ puts "###########################################################"
+
+
+ # Create New Vivado Project
+ create_project $stitch_project_name . -part $part -force
+
+ # Add repositories
+ # Initialize the repo count
+ set repo_count 0
+ # Loop through potential project directories
+ for {set i 1} {[file exists "$base_dir/graph$i/${original_project_name}_graph${i}_prj"]} {incr i} {
+ set repo_path "$base_dir/graph$i/${original_project_name}_graph${i}_prj/solution1/impl/ip"
+ # Check if the repository path exists
+ if {[file isdirectory $repo_path]} {
+ # Add repository path to current project's IP repository paths
+ set_property ip_repo_paths [concat [get_property ip_repo_paths [current_project]] $repo_path] [current_project]
+
+ # Increment the repo count
+ incr repo_count
+
+ puts "Added IP repository path: $repo_path"
+ } else {
+ puts "Directory does not exist: $repo_path"
+ }
+ }
+
+ if { $repo_count == 0 } {
+ puts "No IP repositories were found in the specified directories."
+ } else {
+ puts "Total IP repositories added: $repo_count"
+ }
+ # Rescan repositories
+ update_ip_catalog
+
+ create_bd_design $bd_name
+
+ # Add IPs to block design
+ for {set i 1} {$i <= $repo_count} {incr i} {
+ set vlnv "xilinx.com:hls:${original_project_name}_graph${i}:1.0"
+ create_bd_cell -type ip -vlnv $vlnv "${original_project_name}_graph${i}_0"
+ }
+
+ # Collect all IP instance names in a list
+ set ip_instances {}
+ for {set i 1} {$i <= $repo_count} {incr i} {
+ set ip_name "${original_project_name}_graph${i}_0"
+ lappend ip_instances $ip_name
+ }
+
+ # Collect 'ap_clk' and 'ap_rst' signals from all IPs
+ set ap_clk_ports {}
+ set ap_rst_ports {}
+
+ foreach ip $ip_instances {
+ set ip_cell [get_bd_cells $ip]
+ set ip_pins [get_bd_pins -of $ip_cell]
+ foreach pin $ip_pins {
+ set pin_name [get_property NAME $pin]
+ if {[string match "ap_clk*" $pin_name]} {
+ lappend ap_clk_ports $pin
+ } elseif {[string match "ap_rst*" $pin_name]} {
+ lappend ap_rst_ports $pin
+ }
+ }
+ }
+
+ # Create external ports for 'ap_clk' and 'ap_rst'
+ # ap_clk
+ if {[llength $ap_clk_ports] > 0} {
+ set clk_freq [get_property CONFIG.FREQ_HZ [lindex $ap_clk_ports 0]]
+
+ # Warn if modules are synthesized with different clk
+ foreach clk_pin $ap_clk_ports {
+ if {[get_property CONFIG.FREQ_HZ $clk_pin] ne $clk_freq} {
+ puts "Warning: Inconsistent CONFIG.FREQ_HZ for ap_clk ports."
+ break
+ }
+ }
+ # NOTE: Probably we will need the lowest clock frequency among all IPs here
+ create_bd_port -dir I -type clk -freq_hz 100000000 ap_clk
+ set ap_clk_port [get_bd_ports ap_clk]
+ # Connect all 'ap_clk' pins to the 'ap_clk' port
+ foreach clk_pin $ap_clk_ports {
+ connect_bd_net $ap_clk_port $clk_pin
+ }
+ }
+
+ # ap_rst
+ if {[llength $ap_rst_ports] > 0} {
+ # Get the CONFIG.POLARITY property from one of the IP's 'ap_rst' pins
+ set sample_rst_pin [lindex $ap_rst_ports 0]
+ set rst_polarity [get_property CONFIG.POLARITY $sample_rst_pin]
+
+ foreach ap_rst_port $ap_rst_ports {
+ # All ports should have the same polarity
+ if {[get_property CONFIG.POLARITY $ap_rst_port] ne $rst_polarity} {
+ puts "Error: Inconsistent CONFIG.POLARITY for ap_rst ports. Aborting."
+ exit 1
+ }
+ }
+
+ # Only proceed if the polarity is defined
+ if {$rst_polarity ne ""} {
+ # Create the 'ap_rst' port
+ set rst_port_name "ap_rst"
+ create_bd_port -dir I -type rst $rst_port_name
+ set ap_rst_port [get_bd_ports ap_rst]
+
+ # Set the CONFIG.POLARITY property of the 'ap_rst' port based on the retrieved polarity
+ set_property CONFIG.POLARITY $rst_polarity $ap_rst_port
+
+ # Rename the port based on polarity
+ if {$rst_polarity eq "ACTIVE_LOW"} {
+ set rst_port_name "ap_rst_n"
+ set_property NAME $rst_port_name $ap_rst_port
+ puts "Setting reset port ap_rst_n (ACTIVE_LOW)."
+ } else {
+ puts "Setting reset port ap_rst (ACTIVE_HIGH)."
+ }
+ # Connect all 'ap_rst' pins to the 'ap_rst' port
+ foreach rst_pin $ap_rst_ports {
+ connect_bd_net $ap_rst_port $rst_pin
+ }
+ } else {
+ # Fallback: Undefined polarity, no port created
+ puts "Warning: CONFIG.POLARITY of ap_rst is undefined. No reset port created."
+ }
+ } else {
+ puts "Error: No reset ports found."
+ }
+
+ # Determine interface type
+ set first_ip [lindex $ip_instances 0]
+ set first_ip_cell [get_bd_cells $first_ip]
+ set first_ip_pins [get_bd_pins -of $first_ip_cell]
+
+ set interface_type "unknown"
+ foreach port $first_ip_pins {
+ set port_name [get_property NAME $port]
+ if {[string match "*_TDATA" $port_name]} {
+ set interface_type "axi_stream"
+ break
+ } elseif {[regexp {^layer(?:\d+_)?out_(\d+)$} $port_name]} {
+ set interface_type "partition"
+ break
+ }
+ }
+
+ if {$interface_type == "unknown"} {
+ puts "Error: Could not determine interface type."
+ exit 1
+ } else {
+ puts "Interface type detected: $interface_type"
+ }
+
+ # Collect 'ap_start' signals from all IPs
+ set ap_start_ports {}
+ foreach ip $ip_instances {
+ set ip_cell [get_bd_cells $ip]
+ set ip_pins [get_bd_pins -of $ip_cell]
+ foreach pin $ip_pins {
+ set pin_name [get_property NAME $pin]
+ if {[string match "ap_start" $pin_name]} {
+ lappend ap_start_ports $pin
+ }
+ }
+ }
+
+ # Loop over IP instances to connect outputs to inputs
+ for {set i 0} {$i < [expr {[llength $ip_instances] - 1}]} {incr i} {
+ # Get current IP and next IP
+ set ip_i [lindex $ip_instances $i]
+ set ip_i_plus1 [lindex $ip_instances [expr {$i + 1}]]
+
+ # Get bd_cells for each IP
+ set ip_i_cell [get_bd_cells $ip_i]
+ set ip_i_plus1_cell [get_bd_cells $ip_i_plus1]
+
+ if {$interface_type == "partition"} {
+ # Existing partitioned interface connection logic
+ # Get all output pins from ip_i
+ set output_ports [get_bd_pins -of $ip_i_cell]
+
+ # Initialize arrays for output ports
+ array unset layer_out_ports_by_index
+ array unset layer_out_vld_ports_by_index
+
+ # Filter output ports and extract indices
+ foreach port $output_ports {
+ set port_name [get_property NAME $port]
+ # Match 'layer_out_' or 'layer_out_'
+ if {[regexp {^layer(?:\d+_)?out_(\d+)$} $port_name all index]} {
+ set layer_out_ports_by_index($index) $port
+ } elseif {[regexp {^layer(?:\d+_)?out_(\d+)_ap_vld$} $port_name all index]} {
+ set layer_out_vld_ports_by_index($index) $port
+ } else {
+ # NOTE: We expect data ports to follow the previous naming pattern
+ # NOTE: This is not treated as an error because it might be a valid control port or non-standard signal
+ }
+ }
+
+ # Get all input pins from ip_i_plus1
+ set input_ports [get_bd_pins -of $ip_i_plus1_cell]
+
+ # Initialize arrays for input ports
+ array unset input_ports_by_index
+ array unset input_vld_ports_by_index
+
+ # Filter input ports and extract indices
+ foreach port $input_ports {
+ set port_name [get_property NAME $port]
+ # Match '{name}_input_{index}'
+ if {[regexp {^\w+_input_(\d+)$} $port_name all index]} {
+ set input_ports_by_index($index) $port
+ } elseif {[regexp {^\w+_input_(\d+)_ap_vld$} $port_name all index]} {
+ set input_vld_ports_by_index($index) $port
+ }
+ }
+
+ # Connect data signals
+ foreach index [array names layer_out_ports_by_index] {
+ set out_port $layer_out_ports_by_index($index)
+ if {[info exists input_ports_by_index($index)]} {
+ set in_port $input_ports_by_index($index)
+ # Connect the ports
+ connect_bd_net $out_port $in_port
+ } else {
+ puts "Warning: No matching input port found for output [get_property NAME $out_port]"
+ }
+ }
+
+ # Connect ap_vld signals
+ foreach index [array names layer_out_vld_ports_by_index] {
+ set out_vld_port $layer_out_vld_ports_by_index($index)
+ if {[info exists input_vld_ports_by_index($index)]} {
+ set in_vld_port $input_vld_ports_by_index($index)
+ # Connect the ports
+ connect_bd_net $out_vld_port $in_vld_port
+ } else {
+ puts "Error: No matching input ap_vld port found for output [get_property NAME $out_vld_port]"
+ exit 1
+ }
+ }
+
+ # Connect 'ap_done' of ip_i to 'ap_start' of ip_i_plus1
+ # Get 'ap_done' pin of ip_i
+ set ip_i_pins [get_bd_pins -of $ip_i_cell]
+ set ap_done_pin ""
+ foreach pin $ip_i_pins {
+ set pin_name [get_property NAME $pin]
+ if {[string match "ap_done" $pin_name]} {
+ set ap_done_pin $pin
+ break
+ }
+ }
+
+ # Get 'ap_start' pin of ip_i_plus1
+ set ip_i_plus1_pins [get_bd_pins -of $ip_i_plus1_cell]
+ set ap_start_pin ""
+ foreach pin $ip_i_plus1_pins {
+ set pin_name [get_property NAME $pin]
+ if {[string match "ap_start" $pin_name]} {
+ set ap_start_pin $pin
+ break
+ }
+ }
+
+ # Connect 'ap_done' of ip_i to 'ap_start' of ip_i_plus1
+ if {[string length $ap_done_pin] > 0 && [string length $ap_start_pin] > 0} {
+ connect_bd_net $ap_done_pin $ap_start_pin
+ puts "Connected 'ap_done' of $ip_i to 'ap_start' of $ip_i_plus1"
+ } else {
+ puts "Warning: Could not find 'ap_done' or 'ap_start' pin for IPs $ip_i and $ip_i_plus1"
+ }
+ } elseif {$interface_type == "axi_stream"} {
+ # Get AXI Stream interface pins from ip_i and ip_i_plus1
+ set ip_i_intf_pins [get_bd_intf_pins -of $ip_i_cell]
+ set ip_i_plus1_intf_pins [get_bd_intf_pins -of $ip_i_plus1_cell]
+ set ip_i_axis_master ""
+ set ip_i_plus1_axis_slave ""
+
+ # Identify the Master (output) AXI Stream interface of ip_i
+ foreach intf_pin $ip_i_intf_pins {
+ set pin_name [get_property NAME $intf_pin]
+ # Assuming output interfaces have names ending with 'out'
+ if {[string match "*out" $pin_name]} {
+ set ip_i_axis_master $intf_pin
+ break
+ }
+ }
+
+ # Identify the Slave (input) AXI Stream interface of ip_i_plus1
+ foreach intf_pin $ip_i_plus1_intf_pins {
+ set pin_name [get_property NAME $intf_pin]
+ # Assuming input interfaces have names ending with 'input'
+ if {[string match "*input" $pin_name]} {
+ set ip_i_plus1_axis_slave $intf_pin
+ break
+ }
+ }
+
+ # Check if both interfaces are found
+ if {[string length $ip_i_axis_master] > 0 && [string length $ip_i_plus1_axis_slave] > 0} {
+ # Connect the AXI Stream interfaces
+ connect_bd_intf_net $ip_i_axis_master $ip_i_plus1_axis_slave
+ puts "Connected AXI Stream interface between $ip_i and $ip_i_plus1"
+ } else {
+ puts "Warning: Could not find matching AXI Stream interfaces for $ip_i and $ip_i_plus1"
+ }
+ }
+ }
+
+ if {$interface_type == "axi_stream"} {
+ # Create external port for 'ap_start' and connect all 'ap_start' pins
+ # ap_start in streaming IPs needs to be constantly high
+ if {[llength $ap_start_ports] > 0} {
+ create_bd_port -dir I ap_start
+ set ap_start_port [get_bd_ports ap_start]
+ foreach start_pin $ap_start_ports {
+ connect_bd_net $ap_start_port $start_pin
+ }
+ }
+
+ # Make external all input interfaces of the first IP
+ set first_ip_cell [get_bd_cells [lindex $ip_instances 0]]
+ if {[string length $first_ip_cell] == 0} {
+ puts "Error: Could not find the first IP cell."
+ return
+ }
+ set first_ip_intf_pins [get_bd_intf_pins -of $first_ip_cell]
+ set input_pin_names {}
+ foreach intf_pin $first_ip_intf_pins {
+ set intf_mode [get_property MODE $intf_pin]
+ set vlnv [get_property VLNV $intf_pin]
+ if {$intf_mode eq "Slave" && [string match "*:axis_rtl:*" $vlnv]} {
+ # Make the interface pin external
+ make_bd_intf_pins_external $intf_pin
+ set pin_name [get_property NAME $intf_pin]
+ # Retrieve the external interface port
+ set external_intf_port [get_bd_intf_ports -filter "NAME =~ \"${pin_name}*\""]
+ # Change name to base_name
+ set_property NAME $pin_name $external_intf_port
+ lappend input_pin_names $pin_name
+ }
+ }
+ if {[llength $input_pin_names] == 0} {
+ puts "Error: Could not find any input AXI Stream interfaces for first IP."
+ return
+ }
+
+ # Make external all output interfaces of the last IP
+ set last_ip_cell [get_bd_cells [lindex $ip_instances end]]
+ if {[string length $last_ip_cell] == 0} {
+ puts "Error: Could not find the last IP cell."
+ return
+ }
+ set last_ip_intf_pins [get_bd_intf_pins -of $last_ip_cell]
+ set output_pin_names {}
+ foreach intf_pin $last_ip_intf_pins {
+ set intf_mode [get_property MODE $intf_pin]
+ set vlnv [get_property VLNV $intf_pin]
+ if {$intf_mode eq "Master" && [string match "*:axis_rtl:*" $vlnv]} {
+ # Make the interface pin external
+ make_bd_intf_pins_external $intf_pin
+ set pin_name [get_property NAME $intf_pin]
+ # Retrieve the external interface port and change name to base name
+ set external_intf_port [get_bd_intf_ports -filter "NAME =~ \"${pin_name}*\""]
+ set_property NAME $pin_name $external_intf_port
+ lappend output_pin_names $pin_name
+ }
+ }
+ if {[llength $output_pin_names] == 0} {
+ puts "Error: Could not find any output AXI Stream interfaces for last IP."
+ return
+ }
+
+ # Associate input, output, and ap_rst to run at 'ap_clk'
+ # Join interface names with colons to match the required format
+ set associated_busif [join [concat $input_pin_names $output_pin_names] ":"]
+ set_property CONFIG.ASSOCIATED_BUSIF {$associated_busif} [get_bd_ports /ap_clk]
+ set_property CONFIG.ASSOCIATED_RESET $rst_port_name [get_bd_ports /ap_clk]
+
+ # Make external the 'ap_done' signal of the last IP
+ set last_ip_pins [get_bd_pins -of $last_ip_cell]
+ set last_ap_done_pin ""
+ foreach pin $last_ip_pins {
+ set pin_name [get_property NAME $pin]
+ if {[string match "ap_done" $pin_name]} {
+ set last_ap_done_pin $pin
+ break
+ }
+ }
+ if {[string length $last_ap_done_pin] > 0} {
+ create_bd_port -dir O ap_done
+ set ap_done_port [get_bd_ports ap_done]
+ connect_bd_net $ap_done_port $last_ap_done_pin
+ } else {
+ puts "Warning: Could not find 'ap_done' pin for last IP"
+ }
+
+ } elseif {$interface_type == "partition"} {
+ # Make 'ap_start' of the first IP external
+ set first_ip_cell [get_bd_cells [lindex $ip_instances 0]]
+ if {[string length $first_ip_cell] == 0} {
+ puts "Error: Could not find the first IP cell."
+ return
+ }
+ set first_ip_pins [get_bd_pins -of $first_ip_cell]
+ set first_ap_start_pin ""
+ foreach pin $first_ip_pins {
+ set pin_name [get_property NAME $pin]
+ if {[string match "ap_start" $pin_name]} {
+ set first_ap_start_pin $pin
+ break
+ }
+ }
+ if {[string length $first_ap_start_pin] > 0} {
+ create_bd_port -dir I ap_start
+ set ap_start_port [get_bd_ports ap_start]
+ connect_bd_net $ap_start_port $first_ap_start_pin
+ } else {
+ puts "Warning: Could not find 'ap_start' pin for first IP"
+ }
+
+ # Make 'ap_done' of the last IP external
+ set last_ip_cell [get_bd_cells [lindex $ip_instances end]]
+ if {[string length $last_ip_cell] == 0} {
+ puts "Error: Could not find the last IP cell."
+ return
+ }
+ set last_ip_pins [get_bd_pins -of $last_ip_cell]
+ set last_ap_done_pin ""
+ foreach pin $last_ip_pins {
+ set pin_name [get_property NAME $pin]
+ if {[string match "ap_done" $pin_name]} {
+ set last_ap_done_pin $pin
+ break
+ }
+ }
+ if {[string length $last_ap_done_pin] > 0} {
+ create_bd_port -dir O ap_done
+ set ap_done_port [get_bd_ports ap_done]
+ connect_bd_net $ap_done_port $last_ap_done_pin
+ } else {
+ puts "Warning: Could not find 'ap_done' pin for last IP"
+ }
+
+ set control_pins {ap_clk ap_rst ap_start ap_done ap_idle ap_ready}
+
+ # Make external all inputs of the first IP (including 'vld' signals)
+ set input_pin_names {}
+ foreach pin $first_ip_pins {
+ set pin_name [get_property NAME $pin]
+ set pin_dir [get_property DIR $pin]
+ # Match patterns for inputs and input valid pins
+ if {$pin_dir eq "I" && [lsearch -exact $control_pins $pin_name] == -1} {
+ puts "Found NN model input pin: $pin_name"
+
+ # Make the pin external
+ make_bd_pins_external $pin
+ # Retrieve the external port and change name to base name
+ set external_port [get_bd_ports -filter "NAME =~ \"${pin_name}*\""]
+ set_property NAME $pin_name $external_port
+ lappend input_pin_names $pin_name
+ }
+ }
+ if {[llength $input_pin_names] == 0} {
+ puts "Error: Could not find any input pins for first IP."
+ return
+ }
+
+ # Make external all outputs of the last IP (including 'vld' signals)
+ set output_pin_names {}
+ foreach pin $last_ip_pins {
+ set pin_name [get_property NAME $pin]
+ set pin_dir [get_property DIR $pin]
+ # Match patterns for outputs and output valid pins
+ if {$pin_dir eq "O" && [lsearch -exact $control_pins $pin_name] == -1} {
+ puts "Found NN model output pin: $pin_name"
+ # Make the pin external
+ make_bd_pins_external $pin
+ # Retrieve the external port and change name to base name
+ set external_port [get_bd_ports -filter "NAME =~ \"${pin_name}*\""]
+ set_property NAME $pin_name $external_port
+ lappend output_pin_names $pin_name
+ }
+ }
+ if {[llength $output_pin_names] == 0} {
+ puts "Error: Could not find any output pins for last IP."
+ return
+ }
+ }
+
+ validate_bd_design
+
+ regenerate_bd_layout
+
+ save_bd_design
+
+ puts "###########################################################"
+ puts "# Successfully connected the ports of each IP instance "
+ puts "# A total of $repo_count IPs were connected. "
+ puts "###########################################################"
+
+}
+
+if {$stitch_design} {
+ set start_time [clock seconds]
+ stitch_procedure $original_project_path $stitch_project_name $original_project_name $bd_name $part
+ set end_time [clock seconds]
+ set elapsed_time [expr {$end_time - $start_time}]
+ puts "====================================================="
+ puts "\[Stitch\] Elapsed Time : $elapsed_time seconds"
+ puts "====================================================="
+} else {
+ #set existing_stitch_project_name [file join $stitch_project_name "$stitch_project_name.xpr"]
+ if {[file exists "$stitch_project_name.xpr"]} {
+ puts "Opening existing project: $stitch_project_name.xpr"
+ open_project "$stitch_project_name.xpr"
+ } else {
+ puts "Error: Project file "$stitch_project_name.xpr" does not exist."
+ exit 1
+ }
+}
+
+if {$export_design} {
+ set start_time [clock seconds]
+ puts "Exporting stitched IP..."
+ set stitched_ip_dir "ip_repo"
+ ipx::package_project -root_dir $stitched_ip_dir \
+ -vendor user.org -library user -taxonomy /UserIP -module $bd_name \
+ -import_files
+ set_property description "This IP core integrates all NN subgraph IPs into one." [ipx::find_open_core user.org:user:stitched_design:1.0]
+ set_property core_revision 2 [ipx::find_open_core user.org:user:stitched_design:1.0]
+ ipx::create_xgui_files [ipx::find_open_core user.org:user:stitched_design:1.0]
+ ipx::update_checksums [ipx::find_open_core user.org:user:stitched_design:1.0]
+ ipx::check_integrity [ipx::find_open_core user.org:user:stitched_design:1.0]
+ ipx::save_core [ipx::find_open_core user.org:user:stitched_design:1.0]
+ puts "Stitched IP has been exported to '$stitched_ip_dir' folder"
+ puts "====================================================="
+ puts "\[Export\] Elapsed Time : $elapsed_time seconds"
+ puts "====================================================="
+}
+
+if {$sim_design} {
+ set start_time [clock seconds]
+ if {$sim_verilog_file == ""} {
+ puts "Error: sim_verilog_file not provided."
+ exit 1
+ }
+ if {![file exists "$base_dir/$sim_verilog_file"]} {
+ puts "Error: Simulation file not found: $base_dir/$sim_verilog_file"
+ exit 1
+ }
+ if {[llength [get_filesets sim_1]] == 0} {
+ create_fileset -simset sim_1
+ }
+ set_property SOURCE_SET sources_1 [get_filesets sim_1]
+ add_files -fileset sim_1 -norecurse -scan_for_includes "$base_dir/$sim_verilog_file"
+ update_compile_order -fileset sim_1
+ puts "Simulation Verilog file added: $base_dir/$sim_verilog_file"
+ set_property top tb_design_1_wrapper [get_filesets sim_1]
+ set_property -name {xsim.simulate.runtime} -value {1000000ns} -objects [get_filesets sim_1]
+
+ # Check if snapshot already exists
+ set snapshot_name "tb_design_1_wrapper_behav"
+ set xsim_folder_path "${base_dir}/vivado_stitched_design.sim/sim_1/behav/xsim"
+ puts "##########################"
+ puts "# Running Simulation... #"
+ puts "##########################"
+ if {[file exists "${xsim_folder_path}/${snapshot_name}.wdb"]} {
+ puts "Using existing snapshot..."
+ cd $xsim_folder_path
+ exec xsim $snapshot_name -R
+ } else {
+ launch_simulation
+ }
+ set end_time [clock seconds]
+ set elapsed_time [expr {$end_time - $start_time}]
+ puts "====================================================="
+ puts "\[Simulation\] Elapsed Time : $elapsed_time seconds"
+ puts "====================================================="
+}
+
+
+close_project
diff --git a/hls4ml/templates/bambu/myproject_bridge.cpp b/hls4ml/templates/bambu/myproject_bridge.cpp
new file mode 100644
index 0000000000..8aa76a703b
--- /dev/null
+++ b/hls4ml/templates/bambu/myproject_bridge.cpp
@@ -0,0 +1,71 @@
+#ifndef MYPROJECT_BRIDGE_H_
+#define MYPROJECT_BRIDGE_H_
+
+#include "firmware/myproject.h"
+#include "firmware/nnet_utils/nnet_helpers.h"
+#include
+#include