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 + +// hls-fpga-machine-learning insert bram + +namespace nnet { +bool trace_enabled = false; +std::map *trace_outputs = NULL; +size_t trace_type_size = sizeof(double); +} // namespace nnet + +extern "C" { + +struct trace_data { + const char *name; + void *data; +}; + +void allocate_trace_storage(size_t element_size) { + nnet::trace_enabled = true; + nnet::trace_outputs = new std::map; + nnet::trace_type_size = element_size; + // hls-fpga-machine-learning insert trace_outputs +} + +void free_trace_storage() { + for (std::map::iterator i = nnet::trace_outputs->begin(); i != nnet::trace_outputs->end(); i++) { + void *ptr = i->second; + free(ptr); + } + nnet::trace_outputs->clear(); + delete nnet::trace_outputs; + nnet::trace_outputs = NULL; + nnet::trace_enabled = false; +} + +void collect_trace_output(struct trace_data *c_trace_outputs) { + int ii = 0; + for (std::map::iterator i = nnet::trace_outputs->begin(); i != nnet::trace_outputs->end(); i++) { + c_trace_outputs[ii].name = i->first.c_str(); + c_trace_outputs[ii].data = i->second; + ii++; + } +} + +// hls-fpga-machine-learning insert tb_input_writer + +// Wrapper of top level function for Python bridge +void myproject_float( + // hls-fpga-machine-learning insert header #float +) { + // hls-fpga-machine-learning insert namespace + + // hls-fpga-machine-learning insert wrapper #float +} + +void myproject_double( + // hls-fpga-machine-learning insert header #double +) { + // hls-fpga-machine-learning insert namespace + + // hls-fpga-machine-learning insert wrapper #double +} +} + +#endif diff --git a/hls4ml/templates/bambu/myproject_float_test.cpp b/hls4ml/templates/bambu/myproject_float_test.cpp new file mode 100644 index 0000000000..22cf332db2 --- /dev/null +++ b/hls4ml/templates/bambu/myproject_float_test.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "firmware/myproject_float.h" + +#ifdef __BAMBU__ +#include +#endif + +#define CHECKPOINT 5000 + +int main(int argc, char **argv) { + // load input data from text file + std::ifstream fin("tb_data/tb_input_features.dat"); + // load predictions from text file + std::ifstream fpr("tb_data/tb_output_predictions.dat"); + +#ifdef RTL_SIM + std::string RESULTS_LOG = "tb_data/rtl_cosim_results.log"; +#else + std::string RESULTS_LOG = "tb_data/csim_results.log"; +#endif + std::ofstream fout(RESULTS_LOG); + + std::string iline; + std::string pline; + int e = 0; + + if (fin.is_open() && fpr.is_open()) { + while (std::getline(fin, iline) && std::getline(fpr, pline)) { + if (e % CHECKPOINT == 0) + std::cout << "Processing input " << e << std::endl; + char *cstr = const_cast(iline.c_str()); + char *current; + std::vector in; + current = strtok(cstr, " "); + while (current != NULL) { + in.push_back(atof(current)); + current = strtok(NULL, " "); + } + cstr = const_cast(pline.c_str()); + std::vector pr; + current = strtok(cstr, " "); + while (current != NULL) { + pr.push_back(atof(current)); + current = strtok(NULL, " "); + } + + // hls-fpga-machine-learning insert float-data + + // hls-fpga-machine-learning insert float-top-level-function + + e++; + + // hls-fpga-machine-learning insert float-tb-output + } + fin.close(); + fpr.close(); + } else { + std::cout << "INFO: Unable to open input/predictions file, using default input." << std::endl; + const unsigned NUM_TEST_SAMPLES = 5; + for (unsigned i = 0; i < NUM_TEST_SAMPLES; i++) { + // hls-fpga-machine-learning insert float-zero + + // hls-fpga-machine-learning insert float-top-level-function + + // hls-fpga-machine-learning insert float-output + + // hls-fpga-machine-learning insert float-tb-output + } + } + + fout.close(); + std::cout << "INFO: Saved inference results to file: " << RESULTS_LOG << std::endl; + + return 0; +} diff --git a/hls4ml/templates/bambu/myproject_test.cpp b/hls4ml/templates/bambu/myproject_test.cpp new file mode 100644 index 0000000000..864f5b9c8a --- /dev/null +++ b/hls4ml/templates/bambu/myproject_test.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "firmware/myproject.h" +#include "firmware/nnet_utils/nnet_helpers.h" + +#ifdef __BAMBU__ +#include +#endif +// hls-fpga-machine-learning insert bram + +#define CHECKPOINT 5000 + +namespace nnet { +bool trace_enabled = true; +std::map *trace_outputs = NULL; +size_t trace_type_size = sizeof(double); +} // namespace nnet + +int main(int argc, char **argv) { + // hls-fpga-machine-learning insert namespace + + // load input data from text file + std::ifstream fin("tb_data/tb_input_features.dat"); + // load predictions from text file + std::ifstream fpr("tb_data/tb_output_predictions.dat"); + +#ifdef RTL_SIM + std::string RESULTS_LOG = "tb_data/rtl_cosim_results.log"; +#else + std::string RESULTS_LOG = "tb_data/csim_results.log"; +#endif + std::ofstream fout(RESULTS_LOG); + + std::string iline; + std::string pline; + int e = 0; + + if (fin.is_open() && fpr.is_open()) { + while (std::getline(fin, iline) && std::getline(fpr, pline)) { + if (e % CHECKPOINT == 0) + std::cout << "Processing input " << e << std::endl; + char *cstr = const_cast(iline.c_str()); + char *current; + std::vector in; + current = strtok(cstr, " "); + while (current != NULL) { + in.push_back(atof(current)); + current = strtok(NULL, " "); + } + cstr = const_cast(pline.c_str()); + std::vector pr; + current = strtok(cstr, " "); + while (current != NULL) { + pr.push_back(atof(current)); + current = strtok(NULL, " "); + } + + // hls-fpga-machine-learning insert data + + // hls-fpga-machine-learning insert top-level-function + + if (e % CHECKPOINT == 0) { + std::cout << "Predictions" << std::endl; + // hls-fpga-machine-learning insert predictions + std::cout << "Quantized predictions" << std::endl; + // hls-fpga-machine-learning insert quantized + } + e++; + + // hls-fpga-machine-learning insert tb-output + } + fin.close(); + fpr.close(); + } else { + std::cout << "INFO: Unable to open input/predictions file, using default input." << std::endl; + const unsigned NUM_TEST_SAMPLES = 5; + for (unsigned i = 0; i < NUM_TEST_SAMPLES; i++) { + // hls-fpga-machine-learning insert zero + + // hls-fpga-machine-learning insert top-level-function + + // hls-fpga-machine-learning insert output + + // hls-fpga-machine-learning insert tb-output + } + } + + fout.close(); + std::cout << "INFO: Saved inference results to file: " << RESULTS_LOG << std::endl; + + return 0; +} diff --git a/hls4ml/templates/bambu/nnet_utils/gcem b/hls4ml/templates/bambu/nnet_utils/gcem new file mode 160000 index 0000000000..012ae73c6d --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/gcem @@ -0,0 +1 @@ +Subproject commit 012ae73c6d0a2cb09ffe86475f5c6fba3926e200 diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_activation.h b/hls4ml/templates/bambu/nnet_utils/nnet_activation.h new file mode 100644 index 0000000000..f97b74fa6f --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_activation.h @@ -0,0 +1,1158 @@ +#ifndef NNET_ACTIVATION_H_ +#define NNET_ACTIVATION_H_ + +#include "ap_fixed.h" +#include "gcem/include/gcem.hpp" +#include "nnet_common.h" +#include +#include +#include + +namespace nnet { + +struct activ_config { + // IO size + static const unsigned n_in = 10; + + // Internal info + static const unsigned table_size = 1024; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; + + // Internal data type definitions + typedef ap_fixed<18, 8> table_t; +}; + +// ************************************************* +// LINEAR Activation -- See Issue 53 +// ************************************************* +template void linear(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + res[ii] = data[ii]; + } +} + +// ************************************************* +// RELU Activation +// ************************************************* +template void relu(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + data_T datareg; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + if (datareg > 0) + res[ii] = datareg; + else + res[ii] = 0; + } +} + +template +void relu_max(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + data_T datareg; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + if (datareg < 0) + res[ii] = 0; + else if (datareg > MAX_INT) + res[ii] = MAX_INT; + else + res[ii] = datareg; + } +} + +template void relu6(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + relu_max(data, res); +} + +template void relu1(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + relu_max(data, res); +} + +constexpr inline float exp_with_clamp_fcn_float(float input) { + // Keep constexpr table generation finite for large-magnitude inputs. + // Without this clamp, exp() can overflow to Inf and break ac_fixed conversion. + constexpr float max_exp_input = 80.0f; + constexpr float min_exp_input = -80.0f; + const float clamped_input = (input > max_exp_input) ? max_exp_input : ((input < min_exp_input) ? min_exp_input : input); + using gcem::exp; + return exp(clamped_input); +} + +// ************************************************* +// Sigmoid Activation +// ************************************************* +constexpr inline float sigmoid_fcn_float(float input) { return 1.0 / (1 + exp_with_clamp_fcn_float(-input)); } +#ifdef OLD_SIGMOID +template void init_sigmoid_table(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Default logistic sigmoid function: + // result = 1/(1+e^(-x)) + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = 2 * 8.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = sigmoid_fcn_float(in_val); + // std::cout << "Lookup table In Value: " << in_val << " Result: " << real_val << std::endl; + table_out[ii] = real_val; + } +} +#else +template +constexpr typename CONFIG_T::table_t compute_sigmoid_fcn_float_index(size_t ii) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = 2 * 8.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = sigmoid_fcn_float(in_val); + return real_val; +} + +template +constexpr static std::array init_sigmoid_table(std::index_sequence) { + return std::array{compute_sigmoid_fcn_float_index(I)...}; +} + +template constexpr static std::array init_sigmoid_table() { + return init_sigmoid_table(std::make_index_sequence{}); +} +#endif + +template +void sigmoid(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + // Initialize the lookup table +#ifdef OLD_SIGMOID +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t sigmoid_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t sigmoid_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_sigmoid_table(sigmoid_table); + initialized = true; + } +#else + static constexpr const ::std::array sigmoid_table = + init_sigmoid_table(); +#endif + //#pragma HLS PIPELINE + + // Index into the lookup table based on data + int data_round; + int index; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + data_round = data[ii] * CONFIG_T::table_size / 16; + index = data_round + 8 * CONFIG_T::table_size / 16; + if (index < 0) + index = 0; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + res[ii] = (res_T)sigmoid_table[index]; + } +} + +// ************************************************* +// Softmax Activation +// ************************************************* + +enum class softmax_implementation { latency = 0, legacy = 1, stable = 2, argmax = 3 }; + +constexpr inline float exp_fcn_float(float input) { return exp_with_clamp_fcn_float(input); } + +template constexpr inline float softmax_real_val_from_idx(unsigned i) { + // Treat the index as the top N bits + constexpr int N = ceillog2(table_size); // number of address bits for table + data_T x(0); + x(x.width - 1, x.width - N) = i; + return (float)x; +} + +template constexpr inline unsigned softmax_idx_from_real_val(data_T x) { + // Slice the top N bits to get an index into the table + constexpr int N = ceillog2(table_size); // number of address bits for table + ap_uint y = x(x.width - 1, x.width - N); // slice the top N bits of input + return (unsigned)y(N - 1, 0); +} + +#ifdef OLD_EXP +template +void init_exp_table(typename CONFIG_T::exp_table_t table_out[CONFIG_T::exp_table_size], bool negative = false) { + // The template data_T is the data type used to address the table + for (unsigned i = 0; i < CONFIG_T::exp_table_size; i++) { + // Slicing bits for address is going to round towards 0, so take the central value + float x = softmax_real_val_from_idx(i) * CONFIG_T::exp_scale; + if (negative) { + // for normalized inputs, we keep the normalization values positive (x_bar = x_max - x) + // so we need to negate the input (exp(-x_bar) = exp(x - x_max)) + x = -x; + } + typename CONFIG_T::exp_table_t exp_x = exp_fcn_float(x); + table_out[i] = exp_x; + } +} +#else +template +constexpr typename CONFIG_T::exp_table_t compute_exp_index(size_t i) { + float x = softmax_real_val_from_idx(i) * CONFIG_T::exp_scale; + if (negative) { + // for normalized inputs, we keep the normalization values positive (x_bar = x_max - x) + // so we need to negate the input (exp(-x_bar) = exp(x - x_max)) + x = -x; + } + typename CONFIG_T::exp_table_t exp_x = exp_fcn_float(x); + return exp_x; +} + +template +constexpr static std::array init_exp_table(std::index_sequence) { + return std::array{compute_exp_index(I)...}; +} + +template +constexpr static std::array init_exp_table() { + return init_exp_table(std::make_index_sequence{}); +} + +#endif + +#ifdef OLD_INVERT +template +void init_invert_table(typename CONFIG_T::inv_table_t table_out[CONFIG_T::inv_table_size]) { + // The template data_T is the data type used to address the table + for (unsigned i = 0; i < CONFIG_T::inv_table_size; i++) { + float x = softmax_real_val_from_idx(i); + typename CONFIG_T::inv_table_t inv_x = 1 / x; + table_out[i] = inv_x; + } +} +#else +template constexpr typename CONFIG_T::inv_table_t compute_inv_index(size_t i) { + float x = softmax_real_val_from_idx(i); + float safe_x = (x == 0.0f) ? std::numeric_limits::min() : x; + typename CONFIG_T::inv_table_t inv_x = 1 / safe_x; + return inv_x; +} + +template +constexpr static std::array init_inv_table(std::index_sequence) { + return std::array{compute_inv_index(I)...}; +} + +template +constexpr static std::array init_inv_table() { + return init_inv_table(std::make_index_sequence{}); +} + +#endif + +template +void softmax_latency(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice]) { + //#pragma HLS pipeline + // Initialize the lookup tables +#ifdef OLD_EXP +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + +#endif + if (!initialized) { + // Note we are exponentiating the inputs, which have type data_T + init_exp_table(exp_table); + initialized = true; + } +#else + static constexpr const ::std::array exp_table = + init_exp_table(); +#endif +#ifdef OLD_INVERT +#ifdef __HLS_SYN__ + bool initializedinv = false; + typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; +#else + static bool initializedinv = false; + static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; + +#endif + if (!initializedinv) { + // Note we are inverting the exponentials, which have type exp_table_t + init_invert_table(invert_table); + initializedinv = true; + } +#else + static constexpr const ::std::array invert_table = + init_inv_table(); +#endif + // Calculate all the e^x's + typename CONFIG_T::accum_t exp_res[CONFIG_T::n_slice]; + #pragma HLS array_partition variable=exp_res complete + typename CONFIG_T::inv_inp_t exp_sum(0); + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { + unsigned x = softmax_idx_from_real_val(data[i]); + exp_res[i] = exp_table[x]; + } + + // Explicitly sum the results with an adder tree. + // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing + Op_add op_add; + exp_sum = reduce>(exp_res, op_add); + + typename CONFIG_T::inv_table_t inv_exp_sum = + invert_table[softmax_idx_from_real_val(exp_sum)]; + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { + res[i] = exp_res[i] * inv_exp_sum; + } +} + +template +void softmax_stable(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice]) { + //#pragma HLS pipeline + // Initialize the lookup tables +#ifdef OLD_EXP +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + +#endif + if (!initialized) { + // Note we are exponentiating the inputs, which have type data_T + init_exp_table(exp_table, true); + initialized = true; + } +#else + static constexpr const ::std::array exp_table = + init_exp_table(); +#endif +#ifdef OLD_INVERT +#ifdef __HLS_SYN__ + bool initializedinv = false; + typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; +#else + static bool initializedinv = false; + static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; + +#endif + if (!initializedinv) { + // Note we are inverting the exponentials, which have type exp_table_t + init_invert_table(invert_table); + initializedinv = true; + } +#else + static constexpr const ::std::array invert_table = + init_inv_table(); +#endif + + // Find the max and compute all delta(x_i, x_max) + Op_max op_max; + data_T x_max = reduce>(data, op_max); + + typename CONFIG_T::inp_norm_t d_xi_xmax[CONFIG_T::n_slice]; + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { + d_xi_xmax[i] = x_max - data[i]; + } + + // Calculate all the e^x's + typename CONFIG_T::accum_t exp_res[CONFIG_T::n_slice]; + #pragma HLS array_partition variable=exp_res complete + typename CONFIG_T::inv_inp_t exp_sum(0); + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { + unsigned x = softmax_idx_from_real_val(d_xi_xmax[i]); + exp_res[i] = exp_table[x]; + } + + // Explicitly sum the results with an adder tree. + // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing + Op_add op_add; + exp_sum = reduce>(exp_res, op_add); + + typename CONFIG_T::inv_table_t inv_exp_sum = + invert_table[softmax_idx_from_real_val(exp_sum)]; + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { + res[i] = exp_res[i] * inv_exp_sum; + } +} + +// Compile-time constexpr helpers for softmax_legacy (default path) +#ifndef OLD_SOFTMAX_LEGACY +template +constexpr typename CONFIG_T::table_t compute_exp_fcn_float_index_legacy(size_t ii) { + float in_val = 2 * 8.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); + typename CONFIG_T::table_t real_val = exp_fcn_float(in_val); + return real_val; +} + +template +constexpr static std::array init_exp_table_legacy(std::index_sequence) { + return std::array{compute_exp_fcn_float_index_legacy(I)...}; +} + +template +constexpr static std::array init_exp_table_legacy() { + return init_exp_table_legacy(std::make_index_sequence{}); +} + +template +constexpr typename CONFIG_T::table_t compute_invert_fcn_float_index_legacy(size_t ii) { + float in_val = 64.0 * ii / float(N_TABLE); + typename CONFIG_T::table_t real_val = (in_val > 0.0) ? (1.0 / in_val) : 0.0; + return real_val; +} + +template +constexpr static std::array init_invert_table_legacy(std::index_sequence) { + return std::array{compute_invert_fcn_float_index_legacy(I)...}; +} + +template +constexpr static std::array init_invert_table_legacy() { + return init_invert_table_legacy(std::make_index_sequence{}); +} +#endif + +// Runtime init functions (for backward compatibility with OLD_SOFTMAX_LEGACY) +#ifdef OLD_SOFTMAX_LEGACY +template void init_exp_table_legacy(typename CONFIG_T::table_t table_out[N_TABLE]) { + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = 2 * 8.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = exp_fcn_float(in_val); + // std::cout << "Lookup table In Value: " << in_val << " Result: " << real_val << std::endl; + table_out[ii] = real_val; + } +} + +template void init_invert_table_legacy(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Inversion function: + // result = 1/x + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range 0 to +64) + float in_val = 64.0 * ii / float(N_TABLE); + // Next, compute lookup table function + if (in_val > 0.0) + table_out[ii] = 1.0 / in_val; + else + table_out[ii] = 0.0; + } +} +#endif + +template +void softmax_legacy(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice]) { + // Initialize the lookup tables +#ifdef OLD_SOFTMAX_LEGACY + // Runtime initialization for backward compatibility +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t exp_table[CONFIG_T::exp_table_size]; + typename CONFIG_T::table_t invert_table[CONFIG_T::inv_table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t exp_table[CONFIG_T::exp_table_size]; + static typename CONFIG_T::table_t invert_table[CONFIG_T::inv_table_size]; +#endif + if (!initialized) { + init_exp_table_legacy(exp_table); + init_invert_table_legacy(invert_table); + initialized = true; + } +#else + // Compile-time initialization (default) + static constexpr const ::std::array exp_table = + init_exp_table_legacy(); + static constexpr const ::std::array invert_table = + init_invert_table_legacy(); +#endif + + //#pragma HLS PIPELINE + + // [rest of softmax_legacy implementation remains the same] + typename CONFIG_T::table_t exp_res[CONFIG_T::n_slice]; + typename CONFIG_T::table_t exp_diff_res; + data_T data_cache[CONFIG_T::n_slice]; + int data_round; + int index; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_slice; ii++) { + data_cache[ii] = data[ii]; + exp_res[ii] = 0; + } + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_slice; ii++) { + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_slice; jj++) { + if (ii == jj) + exp_diff_res = 1; + else { + data_round = (data_cache[jj] - data_cache[ii]) * CONFIG_T::exp_table_size / 16; + index = data_round + 8 * CONFIG_T::exp_table_size / 16; + if (index < 0) + index = 0; + if (index > CONFIG_T::exp_table_size - 1) + index = CONFIG_T::exp_table_size - 1; + exp_diff_res = exp_table[index]; + } + exp_res[ii] += exp_diff_res; + } + } + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_slice; ii++) { + int exp_res_index = exp_res[ii] * CONFIG_T::inv_table_size / 64; + if (exp_res_index < 0) + exp_res_index = 0; + if (exp_res_index > CONFIG_T::inv_table_size - 1) + exp_res_index = CONFIG_T::inv_table_size - 1; + res[ii] = (res_T)invert_table[exp_res_index]; + } +} + +template +void softmax_argmax(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice]) { + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::n_slice; i++) { + res[i] = (res_T)0; + } + + data_T maximum = data[0]; + int idx = 0; + + for (int i = 1; i < CONFIG_T::n_slice; i++) { + //#pragma HLS PIPELINE + if (data[i] > maximum) { + maximum = data[i]; + idx = i; + } + } + + res[idx] = (res_T)1; +} + +template +void softmax(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice]) { + #pragma HLS inline + switch (CONFIG_T::implementation) { + case softmax_implementation::latency: + softmax_latency(data, res); + break; + case softmax_implementation::stable: + softmax_stable(data, res); + break; + case softmax_implementation::legacy: + softmax_legacy(data, res); + break; + case softmax_implementation::argmax: + softmax_argmax(data, res); + break; + } +} + +template +void softmax_multidim(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + #pragma HLS inline + //#pragma HLS allocation instances = softmax limit = CONFIG_T::parallelization_factor function + data_T buffer_in[CONFIG_T::n_slice]; + res_T buffer_out[CONFIG_T::n_slice]; + #pragma clang loop unroll(full) + for (signed i = 0; i < CONFIG_T::n_outer; i++) { + //#pragma HLS UNROLL + #pragma clang loop unroll(full) + for (signed k = 0; k < CONFIG_T::n_inner; k++) { + //#pragma HLS UNROLL + #pragma clang loop unroll(full) + for (signed j = 0; j < CONFIG_T::n_slice; j++) { + //#pragma HLS UNROLL + buffer_in[j] = data[i * CONFIG_T::n_slice * CONFIG_T::n_inner + j * CONFIG_T::n_inner + k]; + } + softmax(buffer_in, buffer_out); + #pragma clang loop unroll(full) + for (signed j = 0; j < CONFIG_T::n_slice; j++) { + //#pragma HLS UNROLL + res[i * CONFIG_T::n_slice * CONFIG_T::n_inner + j * CONFIG_T::n_inner + k] = buffer_out[j]; + } + } + } +} + +// ************************************************* +// TanH Activation +// ************************************************* + +constexpr inline float tanh_fcn_float(float input) { + using gcem::tanh; + return tanh(input); +} +template void init_tanh_table(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Implement tanh lookup + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range -4 to +4) + float in_val = 2 * 4.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = tanh(in_val); + table_out[ii] = real_val; + } +} + +template +constexpr typename CONFIG_T::table_t compute_tanh_fcn_float_index(size_t ii) { + float in_val = 2 * 4.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); + // Compute lookup table function + typename CONFIG_T::table_t real_val = tanh_fcn_float(in_val); + return real_val; +} + +template +constexpr static std::array init_tanh_table(std::index_sequence) { + return std::array{compute_tanh_fcn_float_index(I)...}; +} + +template constexpr static std::array init_tanh_table() { + return init_tanh_table(std::make_index_sequence{}); +} + +template void tanh(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + // Initialize the lookup table at compile time +#ifdef OLD_TANH + // Keep old runtime initialization for backwards compatibility +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t tanh_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t tanh_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_tanh_table(tanh_table); + initialized = true; + } +#else + // Compile-time initialization + static constexpr const ::std::array tanh_table = + init_tanh_table(); +#endif + + int data_round; + int index; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + data_round = data[ii] * CONFIG_T::table_size / 8; + index = data_round + 4 * CONFIG_T::table_size / 8; + if (index < 0) + index = 0; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + res[ii] = (res_T)tanh_table[index]; + } +} + +// ************************************************* +// UnaryLUT Activation +// ************************************************* +template inline unsigned get_index_unary_lut(data_T x) { + // Slice the top N bits to get an index into the table + static constexpr int N = ceillog2(table_size); + return (unsigned)(x(x.width - 1, 0)); +} + +template +void unary_lut(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in], + typename CONFIG_T::table_t table[CONFIG_T::table_size]) { + //#pragma HLS function_instantiate variable=table + #pragma HLS ARRAY_PARTITION variable=table + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + unsigned index = get_index_unary_lut(data[ii]); + res[ii] = (res_T)table[index]; + } +} + +// ************************************************* +// Hard sigmoid Activation +// ************************************************* +template +void hard_sigmoid(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + auto datareg = CONFIG_T::slope * data[ii] + CONFIG_T::shift; + if (datareg > 1) + datareg = 1; + else if (datareg < 0) + datareg = 0; + res[ii] = datareg; + } +} + +template +void hard_tanh(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + if (CONFIG_T::io_type == io_parallel) { + //#pragma HLS PIPELINE + /// TO BE RECONSIDERED FF + } + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + auto sigmoid = CONFIG_T::slope * data[ii] + CONFIG_T::shift; + if (sigmoid > 1) + sigmoid = 1; + else if (sigmoid < 0) + sigmoid = 0; + res[ii] = 2 * sigmoid - 1; + } +} + +// ************************************************* +// Leaky RELU Activation +// ************************************************* +template +void leaky_relu(data_T data[CONFIG_T::n_in], param_T alpha, res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + data_T datareg; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + if (datareg > 0) + res[ii] = datareg; + else + res[ii] = alpha * datareg; + } +} + +// ************************************************* +// Thresholded RELU Activation +// ************************************************* +template +void thresholded_relu(data_T data[CONFIG_T::n_in], param_T theta, res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + data_T datareg; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + if (datareg > theta) + res[ii] = datareg; + else + res[ii] = 0; + } +} + +// ************************************************* +// Softplus Activation +// ************************************************* +constexpr inline float softplus_fcn_float(float input) { + using gcem::log; + return log(exp_with_clamp_fcn_float(input) + 1.); +} + +#ifdef OLD_SOFTPLUS + +template void init_softplus_table(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Default softplus function: + // result = log(exp(x) + 1) + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = 2 * 8.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = softplus_fcn_float(in_val); + // std::cout << "Lookup table In Value: " << in_val << " Result: " << real_val << std::endl; + table_out[ii] = real_val; + } +} +#else +template +constexpr typename CONFIG_T::table_t compute_softplus_fcn_float_index(std::size_t ii) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = 2 * 8.0f * (static_cast(ii) - float(N_TABLE) / 2.0f) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = softplus_fcn_float(in_val); + return real_val; +} + +template +constexpr static std::array init_softplus_table(std::index_sequence) { + return std::array{compute_softplus_fcn_float_index(I)...}; +} + +template +constexpr static std::array init_softplus_table() { + return init_softplus_table(std::make_index_sequence{}); +} +#endif +template +void softplus(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + // Initialize the lookup table +#ifdef OLD_SOFTPLUS +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t softplus_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t softplus_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_softplus_table(softplus_table); + initialized = true; + } +#else + static const ::std::array softplus_table = + init_softplus_table(); +#endif + //#pragma HLS PIPELINE + + // Index into the lookup table based on data + int data_round; + int index; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + data_round = data[ii] * CONFIG_T::table_size / 16; + index = data_round + 8 * CONFIG_T::table_size / 16; + if (index < 0) + index = 0; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + res[ii] = (res_T)softplus_table[index]; + } +} + +// ************************************************* +// Softsign Activation +// ************************************************* +constexpr inline float softsign_fcn_float(float input) { + using gcem::abs; + return input / (abs(input) + 1.0f); +} +#ifdef OLD_SOFTSIGN +template void init_softsign_table(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Default softsign function: + // result = x / (abs(x) + 1) + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = 2 * 8.0f * (ii - float(N_TABLE) / 2.0f) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = softsign_fcn_float(in_val); + table_out[ii] = real_val; + } +} +#else +template +constexpr typename CONFIG_T::table_t compute_softsign_fcn_float_index(std::size_t ii) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = 2 * 8.0f * (static_cast(ii) - float(N_TABLE) / 2.0f) / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = softsign_fcn_float(in_val); + return real_val; +} + +template +constexpr static std::array init_softsign_table(std::index_sequence) { + return std::array{compute_softsign_fcn_float_index(I)...}; +} + +template +constexpr static std::array init_softsign_table() { + return init_softsign_table(std::make_index_sequence{}); +} +#endif // OLD_SOFTSIGN + +template +void softsign(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + // Initialize the lookup table +#ifdef OLD_SOFTSIGN +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t softsign_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t softsign_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_softsign_table(softsign_table); + initialized = true; + } +#else + static const ::std::array softsign_table = + init_softsign_table(); +#endif + + // Index into the lookup table based on data + int data_round; + int index; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + data_round = data[ii] * CONFIG_T::table_size / 16; + index = data_round + 8 * CONFIG_T::table_size / 16; + if (index < 0) + index = 0; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + res[ii] = (res_T)softsign_table[index]; + } +} + +// ************************************************* +// ELU Activation +// ************************************************* +constexpr inline float elu_fcn_float(float input) { return exp_with_clamp_fcn_float(input) - 1.; } + +#ifdef OLD_ELU +template void init_elu_table(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Default ELU function: + // result = alpha * (e^(x) - 1) + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range -8 to 0) + float in_val = -8.0 * ii / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = elu_fcn_float(in_val); + // std::cout << "Lookup table In Value: " << in_val << " Result: " << real_val << std::endl; + table_out[ii] = real_val; + } +} +#else +template +constexpr typename CONFIG_T::table_t compute_elu_fcn_float_index(size_t ii) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = -8.0 * ii / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = elu_fcn_float(in_val); + return real_val; +} + +template +constexpr static std::array init_elu_table(std::index_sequence) { + return std::array{compute_elu_fcn_float_index(I)...}; +} + +template constexpr static std::array init_elu_table() { + return init_elu_table(std::make_index_sequence{}); +} +#endif + +template +void elu(data_T data[CONFIG_T::n_in], const param_T alpha, res_T res[CONFIG_T::n_in]) { + // Initialize the lookup table +#ifdef OLD_ELU +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t elu_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t elu_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_elu_table(elu_table); + initialized = true; + } +#else + static constexpr const ::std::array elu_table = + init_elu_table(); +#endif + //#pragma HLS PIPELINE + + data_T datareg; + // Index into the lookup table based on data + int index; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + if (datareg >= 0) { + res[ii] = datareg; + } else { + index = datareg * CONFIG_T::table_size / -8; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + res[ii] = alpha * elu_table[index]; + } + } +} + +template void elu(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + elu, res_T, CONFIG_T>(data, 1.0, res); +} + +// ************************************************* +// SELU Activation +// ************************************************* +constexpr inline float selu_fcn_float(float input) { + return 1.0507009873554804934193349852946 * (1.6732632423543772848170429916717 * (exp_with_clamp_fcn_float(input) - 1.)); +} + +#ifdef OLD_SELU +template void init_selu_table(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Default SELU function: + // result = 1.05 * (1.673 * (e^(x) - 1)) + for (int ii = 0; ii < N_TABLE; ii++) { + // First, convert from table index to X-value (signed 8-bit, range -8 to 0) + float in_val = -8.0 * ii / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = selu_fcn_float(in_val); + // std::cout << "Lookup table In Value: " << in_val << " Result: " << real_val << std::endl; + table_out[ii] = real_val; + } +} +#else +template +constexpr typename CONFIG_T::table_t compute_selu_fcn_float_index(size_t ii) { + // First, convert from table index to X-value (signed 8-bit, range -8 to +8) + float in_val = -8.0 * ii / float(N_TABLE); + // Next, compute lookup table function + typename CONFIG_T::table_t real_val = selu_fcn_float(in_val); + return real_val; +} + +template +constexpr static std::array init_selu_table(std::index_sequence) { + return std::array{compute_selu_fcn_float_index(I)...}; +} + +template constexpr static std::array init_selu_table() { + return init_selu_table(std::make_index_sequence{}); +} +#endif + +template void selu(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + // Initialize the lookup table +#ifdef OLD_SELU +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t selu_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t selu_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_selu_table(selu_table); + initialized = true; + } +#else + static constexpr const ::std::array selu_table = + init_selu_table(); +#endif + + //#pragma HLS PIPELINE + + typedef ap_ufixed<16, 1> selu_const_t; + constexpr const selu_const_t lambda = 1.0507009873554805; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + data_T datareg = data[ii]; + + if (datareg >= 0) { + // Positive branch y = λ · x + res[ii] = lambda * datareg; + } else { + // Negative branch y = table(x) + int index = datareg * CONFIG_T::table_size / -8; + + // clamp index to [0, table_size-1] + if (index < 0) + index = 0; + else if (index > CONFIG_T::table_size - 1) { + index = CONFIG_T::table_size - 1; + } + + res[ii] = selu_table[index]; + } + } +} + +// ************************************************* +// PReLU Activation +// ************************************************* +template +void prelu(data_T data[CONFIG_T::n_in], param_T alpha[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + data_T datareg; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + if (datareg > 0) + res[ii] = datareg; + else + res[ii] = alpha[ii] * datareg; + } +} + +template +inline typename std::enable_if<(!std::is_same>::value), res_T>::type binary_cast(data_T data) { + return static_cast(data); +} + +// should choose this via function overloading +template +inline typename std::enable_if<(std::is_same>::value), res_T>::type binary_cast(data_T data) { + return (data > 0) ? static_cast(data) : static_cast(0); +} + +// ************************************************* +// Binary TanH Activation +// ************************************************* +template +void binary_tanh(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + using cache_T = ap_int<2>; + data_T datareg; + cache_T cache; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + if (datareg >= 0) + cache = 1; + else + cache = -1; + + res[ii] = binary_cast(cache); + } +} + +// ************************************************* +// Ternary TanH Activation +// ************************************************* +template +void ternary_tanh(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + //#pragma HLS PIPELINE + + data_T datareg; + res_T cache; + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = 2 * data[ii]; + if (datareg > 1) + cache = 1; + else if (datareg > -1 && datareg <= 1) + cache = 0; + else + cache = -1; + + res[ii] = (res_T)cache; + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_activation_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_activation_stream.h new file mode 100644 index 0000000000..73627f642b --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_activation_stream.h @@ -0,0 +1,905 @@ +#ifndef NNET_ACTIVATION_STREAM_H_ +#define NNET_ACTIVATION_STREAM_H_ + +#include "ap_fixed.h" +#include "hls_stream.h" +#include "nnet_activation.h" +#include "nnet_common.h" +#include "nnet_stream.h" +#include "nnet_types.h" +#include + +namespace nnet { + +// ************************************************* +// LINEAR Activation +// ************************************************* +template void linear(hls::stream &data, hls::stream &res) { +LinearActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + LinearPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + out_data[j] = in_data[j]; + } + + res.write(out_data); + } +} + +// ************************************************* +// RELU Activation +// ************************************************* +template void relu(hls::stream &data, hls::stream &res) { +ReLUActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ReLUPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + if (in_data[j] > 0) + out_data[j] = in_data[j]; + else + out_data[j] = 0; + } + + res.write(out_data); + } +} + +// ************************************************* +// Sigmoid Activation +// ************************************************* + +template void sigmoid(hls::stream &data, hls::stream &res) { + // Initialize the lookup table +#ifdef OLD_SIGMOID +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t sigmoid_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t sigmoid_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_sigmoid_table(sigmoid_table); + initialized = true; + } +#else + static constexpr const ::std::array sigmoid_table = + init_sigmoid_table(); +#endif +SigmoidActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + SigmoidPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + int data_round = in_data[j] * CONFIG_T::table_size / 16; + int index = data_round + 8 * CONFIG_T::table_size / 16; + if (index < 0) + index = 0; + else if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + out_data[j] = sigmoid_table[index]; + } + + res.write(out_data); + } +} + +// ************************************************* +// Softmax Activation +// ************************************************* + +template +void softmax_latency(hls::stream &data, hls::stream &res) { + // Initialize the lookup tables +#ifdef OLD_EXP +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + +#endif + if (!initialized) { + // Note we are exponentiating the inputs, which have type data_T + init_exp_table(exp_table); + initialized = true; + } +#else + static constexpr const ::std::array exp_table = + init_exp_table(); +#endif + +#ifdef OLD_INVERT +#ifdef __HLS_SYN__ + bool initializedinv = false; + typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; +#else + static bool initializedinv = false; + static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; + +#endif + if (!initializedinv) { + // Note we are inverting the exponentials, which have type exp_table_t + init_invert_table(invert_table); + initializedinv = true; + } +#else + static constexpr const ::std::array invert_table = + init_inv_table(); +#endif + constexpr unsigned multiplier_limit = DIV_ROUNDUP(data_T::size, CONFIG_T::reuse_factor); + constexpr unsigned ii = data_T::size / multiplier_limit; + + // Calculate all the e^x's + typename CONFIG_T::accum_t exp_res[data_T::size]; + #pragma HLS array_partition variable=exp_res complete + typename CONFIG_T::inv_inp_t exp_sum(0); +SoftmaxExpLoop: + for (unsigned i = 0; i < CONFIG_T::n_in / data_T::size; i++) { + //#pragma HLS PIPELINE II=ii + + data_T in_pack = data.read(); + SoftmaxExpPackLoop: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < data_T::size; j++) { + unsigned x = softmax_idx_from_real_val(in_pack[j]); + exp_res[j] = exp_table[x]; + } + + // Explicitly sum the results with an adder tree. + // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing + Op_add op_add; + exp_sum = reduce>(exp_res, op_add); + + typename CONFIG_T::inv_table_t inv_exp_sum = + invert_table[softmax_idx_from_real_val(exp_sum)]; + + res_T out_pack; + PRAGMA_DATA_PACK(out_pack) + + SoftmaxInvPackLoop: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + //#pragma HLS ALLOCATION operation instances=mul limit=multiplier_limit + out_pack[j] = exp_res[j] * inv_exp_sum; + } + res.write(out_pack); + } +} + +template +void softmax_stable(hls::stream &data, hls::stream &res) { + // Initialize the lookup tables +#ifdef OLD_EXP +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + +#endif + if (!initialized) { + // Note we are exponentiating the inputs, which have type data_T + init_exp_table(exp_table, true); + initialized = true; + } +#else + static constexpr const ::std::array exp_table = + init_exp_table(); +#endif +#ifdef OLD_INVERT +#ifdef __HLS_SYN__ + bool initializedinv = false; + typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; +#else + static bool initializedinv = false; + static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; + +#endif + if (!initializedinv) { + // Note we are inverting the exponentials, which have type exp_table_t + init_invert_table(invert_table); + initializedinv = true; + } +#else + static constexpr const ::std::array invert_table = + init_inv_table(); +#endif + + constexpr unsigned multiplier_limit = DIV_ROUNDUP(data_T::size, CONFIG_T::reuse_factor); + constexpr unsigned ii = data_T::size / multiplier_limit; + + typename data_T::value_type data_array[data_T::size]; +#pragma HLS ARRAY_PARTITION variable=data_array complete +SoftmaxArrayLoop: + for (unsigned i = 0; i < CONFIG_T::n_in / data_T::size; i++) { + //#pragma HLS PIPELINE II=ii + + data_T in_pack = data.read(); + SoftmaxArrayPackLoop: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < data_T::size; j++) { + data_array[j] = in_pack[j]; + } + + // Find the max and compute all delta(x_i, x_max) + Op_max op_max; + typename data_T::value_type x_max = + reduce>(data_array, op_max); + + typename CONFIG_T::inp_norm_t d_xi_xmax[data_T::size]; + #pragma clang loop unroll(full) + for (unsigned j = 0; j < data_T::size; j++) { + d_xi_xmax[j] = x_max - data_array[j]; + } + + // Calculate all the e^x's + typename CONFIG_T::accum_t exp_res[data_T::size]; + #pragma HLS ARRAY_PARTITION variable=exp_res complete + typename CONFIG_T::inv_inp_t exp_sum(0); + #pragma clang loop unroll(full) + for (unsigned j = 0; j < data_T::size; j++) { + unsigned x = softmax_idx_from_real_val(d_xi_xmax[j]); + exp_res[j] = exp_table[x]; + } + + // Explicitly sum the results with an adder tree. + // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing + Op_add op_add; + exp_sum = reduce>(exp_res, op_add); + + typename CONFIG_T::inv_table_t inv_exp_sum = + invert_table[softmax_idx_from_real_val(exp_sum)]; + + res_T out_pack; + PRAGMA_DATA_PACK(out_pack) + + SoftmaxInvPackLoop: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + //#pragma HLS ALLOCATION operation instances=mul limit=multiplier_limit + out_pack[j] = exp_res[j] * inv_exp_sum; + } + res.write(out_pack); + } +} + +template +void softmax_legacy(hls::stream &data, hls::stream &res) { + // Initialize the lookup tables +#ifdef OLD_SOFTMAX_LEGACY + // Keep old runtime initialization for backwards compatibility +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t exp_table[CONFIG_T::table_size]; + typename CONFIG_T::table_t invert_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t exp_table[CONFIG_T::table_size]; + static typename CONFIG_T::table_t invert_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_exp_table_legacy(exp_table); + init_invert_table_legacy(invert_table); + initialized = true; + } +#else + // Compile-time initialization (default) + static constexpr const ::std::array exp_table = + init_exp_table_legacy(); + static constexpr const ::std::array invert_table = + init_invert_table_legacy(); +#endif + + // Index into the lookup table based on data for exponentials + typename CONFIG_T::table_t exp_res[data_T::size]; + typename CONFIG_T::table_t exp_diff_res; + typename data_T::value_type data_cache[data_T::size]; + +SoftmaxInitLoop: + for (unsigned s = 0; s < CONFIG_T::n_in / data_T::size; s++) { + //#pragma HLS PIPELINE + data_T in_pack = data.read(); + SoftmaxInitPackLoop: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + data_cache[j] = in_pack[j]; + exp_res[j] = 0; + } + + SoftmaxExpLoop: + #pragma clang loop unroll(full) + for (int i = 0; i < data_T::size; i++) { + //#pragma HLS UNROLL + SoftmaxExpInner: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + + if (i == j) { + exp_diff_res = 1; + } else { + int data_round = (data_cache[j] - data_cache[i]) * CONFIG_T::table_size / 16; + int index = data_round + 8 * CONFIG_T::table_size / 16; + if (index < 0) + index = 0; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + exp_diff_res = exp_table[index]; + } + + exp_res[i] += exp_diff_res; + } + } + + res_T out_pack; + PRAGMA_DATA_PACK(out_pack) + + SoftmaxInvPackLoop: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + + int exp_res_index = exp_res[j] * CONFIG_T::table_size / 64; + if (exp_res_index < 0) + exp_res_index = 0; + if (exp_res_index > CONFIG_T::table_size - 1) + exp_res_index = CONFIG_T::table_size - 1; + + out_pack[j] = (typename res_T::value_type)invert_table[exp_res_index]; + } + res.write(out_pack); + } +} + +template +void softmax_argmax(hls::stream &data, hls::stream &res) { + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + data_T in_data = data.read(); + res_T out_data; + + #pragma clang loop unroll(full) + for (int i = 0; i < res_T::size; i++) { + //#pragma HLS UNROLL + out_data[i] = (typename res_T::value_type)0; + } + + typename data_T::value_type maximum = in_data[0]; + int idx = 0; + + for (int i = 1; i < res_T::size; i++) { + //#pragma HLS PIPELINE + if (in_data[i] > maximum) { + maximum = in_data[i]; + idx = i; + } + } + + out_data[idx] = (typename res_T::value_type)1; + res.write(out_data); + } +} + +template void softmax(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::axis == -1); + + switch (CONFIG_T::implementation) { + case softmax_implementation::latency: + softmax_latency(data, res); + break; + case softmax_implementation::stable: + softmax_stable(data, res); + break; + case softmax_implementation::legacy: + softmax_legacy(data, res); + break; + case softmax_implementation::argmax: + softmax_argmax(data, res); + break; + } +} + +// ************************************************* +// TanH Activation +// ************************************************* + +template void tanh(hls::stream &data, hls::stream &res) { + // Initialize the lookup table at compile time +#ifdef OLD_TANH + // Keep old runtime initialization for backwards compatibility +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t tanh_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t tanh_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_tanh_table(tanh_table); + initialized = true; + } +#else + // Compile-time initialization + static constexpr const ::std::array tanh_table = + init_tanh_table(); +#endif + +TanHActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + TanHPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + int data_round = in_data[j] * CONFIG_T::table_size / 8; + int index = data_round + 4 * CONFIG_T::table_size / 8; + if (index < 0) + index = 0; + else if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + out_data[j] = tanh_table[index]; + } + + res.write(out_data); + } +} + +// ************************************************* +// UnaryLUT Activation +// ************************************************* + +template +void unary_lut(hls::stream &data, hls::stream &res, typename CONFIG_T::table_t table[CONFIG_T::table_size]) { + //#pragma HLS function_instantiate variable=table + #pragma HLS ARRAY_PARTITION variable=table complete + +UnaryLUTActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor rewind + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + UnaryLUTPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + unsigned index = get_index_unary_lut(in_data[j].V); + out_data[j] = table[index]; + } + + res.write(out_data); + } +} + +// ************************************************* +// Hard sigmoid Activation +// ************************************************* + +template +void hard_sigmoid(hls::stream &data, hls::stream &res) { + +HardSigmoidActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + HardSigmoidPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + auto datareg = CONFIG_T::slope * in_data[j] + CONFIG_T::shift; + if (datareg > 1) + datareg = 1; + else if (datareg < 0) + datareg = 0; + out_data[j] = datareg; + } + + res.write(out_data); + } +} + +template void hard_tanh(hls::stream &data, hls::stream &res) { + +HardSigmoidActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + HardSigmoidPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + auto sigmoid = CONFIG_T::slope * in_data[j] + CONFIG_T::shift; + if (sigmoid > 1) + sigmoid = 1; + else if (sigmoid < 0) + sigmoid = 0; + out_data[j] = 2 * sigmoid - 1; + } + + res.write(out_data); + } +} + +// ************************************************* +// Leaky RELU Activation +// ************************************************* + +template +void leaky_relu(hls::stream &data, param_T alpha, hls::stream &res) { +LeakyReLUActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + LeakyReLUPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + if (in_data[j] > 0) + out_data[j] = in_data[j]; + else + out_data[j] = alpha * in_data[j]; + } + res.write(out_data); + } +} + +// ************************************************* +// Thresholded RELU Activation +// ************************************************* + +template +void thresholded_relu(hls::stream &data, param_T theta, hls::stream &res) { +ThresholdedReLUActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ThresholdedReLUPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + if (in_data[j] > theta) + out_data[j] = in_data[j]; + else + out_data[j] = 0; + } + + res.write(out_data); + } +} + +// ************************************************* +// Softplus Activation +// ************************************************* + +template void softplus(hls::stream &data, hls::stream &res) { + // Initialize the lookup table +#ifdef OLD_SOFTPLUS +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t softplus_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t softplus_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_softplus_table(softplus_table); + initialized = true; + } +#else + static const ::std::array softplus_table = + init_softplus_table(); +#endif + +SoftplusActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + SoftplusPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + int data_round = in_data[j] * CONFIG_T::table_size / 16; + int index = data_round + 8 * CONFIG_T::table_size / 16; + if (index < 0) + index = 0; + else if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + out_data[j] = softplus_table[index]; + } + res.write(out_data); + } +} + +// ************************************************* +// Softsign Activation +// ************************************************* + +template void softsign(hls::stream &data, hls::stream &res) { + // Initialize the lookup table +#ifdef OLD_SOFTSIGN +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t softsign_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t softsign_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_softsign_table(softsign_table); + initialized = true; + } +#else + static const ::std::array softsign_table = + init_softsign_table(); +#endif +SoftsignActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + SoftsignPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + int data_round = in_data[j] * CONFIG_T::table_size / 16; + int index = data_round + 8 * CONFIG_T::table_size / 16; + if (index < 0) + index = 0; + else if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + // funziona sia per C-array che per std::array + out_data[j] = softsign_table[index]; + } + + res.write(out_data); + } +} + +// ************************************************* +// ELU Activation +// ************************************************* +template +void elu(hls::stream &data, param_T alpha, hls::stream &res) { + // Initialize the lookup table +#ifdef OLD_ELU +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t elu_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t elu_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_elu_table(elu_table); + initialized = true; + } +#else + static constexpr const ::std::array elu_table = + init_elu_table(); +#endif + +EluActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + EluPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + + typename data_T::value_type datareg = in_data[j]; + if (datareg >= 0) { + out_data[j] = datareg; + } else { + int index = datareg * CONFIG_T::table_size / -8; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + out_data[j] = alpha * elu_table[index]; + } + } + res.write(out_data); + } +} + +template void elu(hls::stream &data, hls::stream &res) { + elu, res_T, CONFIG_T>(data, 1.0, res); +} + +// ************************************************* +// SELU Activation +// ************************************************* + +template void selu(hls::stream &data, hls::stream &res) { + // Initialize the lookup table +#ifdef OLD_SELU +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t selu_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t selu_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_selu_table(selu_table); + initialized = true; + } +#else + static constexpr const ::std::array selu_table = + init_selu_table(); +#endif + +SeluActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + SeluPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + + typename data_T::value_type datareg = in_data[j]; + if (datareg >= 0) { + out_data[j] = (typename data_T::value_type)1.0507009873554804934193349852946 * datareg; + } else { + int index = datareg * CONFIG_T::table_size / -8; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + out_data[j] = selu_table[index]; + } + } + res.write(out_data); + } +} + +// ************************************************* +// PReLU Activation +// ************************************************* + +template +void prelu(hls::stream &data, const param_T alpha[CONFIG_T::n_in], hls::stream &res) { +PReLUActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + PReLUPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + if (in_data[j] > 0) + out_data[j] = in_data[j]; + else + out_data[j] = alpha[i * res_T::size + j] * in_data[j]; + } + res.write(out_data); + } +} + +// ************************************************* +// Binary TanH Activation +// ************************************************* +template +void binary_tanh(hls::stream &data, hls::stream &res) { + using cache_T = ap_int<2>; +PReLUActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + cache_T cache; + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + PReLUPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + if (in_data[j] >= 0) + cache = 1; + else + cache = -1; + + out_data[j] = binary_cast(cache); + } + res.write(out_data); + } +} + +// ************************************************* +// Ternary TanH Activation +// ************************************************* +template +void ternary_tanh(hls::stream &data, hls::stream &res) { +PReLUActLoop: + for (int i = 0; i < CONFIG_T::n_in / res_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + PReLUPackLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + if (in_data[j] > 1) + out_data[j] = (typename res_T::value_type)1; + else if (in_data[j] <= -1) + out_data[j] = (typename res_T::value_type) - 1; + else + out_data[j] = (typename res_T::value_type)0; + } + res.write(out_data); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_batchnorm.h b/hls4ml/templates/bambu/nnet_utils/nnet_batchnorm.h new file mode 100644 index 0000000000..a283bd3b2a --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_batchnorm.h @@ -0,0 +1,124 @@ +#ifndef NNET_BATCHNORM_H_ +#define NNET_BATCHNORM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_dense.h" +#include + +namespace nnet { + +struct batchnorm_config { + // Internal data type definitions + typedef float bias_t; + typedef float scale_t; + + // Layer Sizes + static const unsigned n_in = 10; + static const unsigned n_filt = -1; + static const unsigned n_scale_bias = 10; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; + static const bool store_weights_in_bram = false; + static const unsigned n_zeros = 0; + // partitioning arrays cyclically to go with roll factors? + template using product = nnet::product::mult; +}; + +template +void normalize(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in], + typename CONFIG_T::scale_t scale[CONFIG_T::n_scale_bias], + typename CONFIG_T::bias_t bias[CONFIG_T::n_scale_bias]) { + data_T cache; + + // Use a function_instantiate in case it helps to explicitly optimize unchanging weights/biases + //#pragma HLS function_instantiate variable=scale,bias + + // For parallel inputs: + // - completely partition arrays -- target fabric + // - if we have an unroll factor, limit number of multipliers + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + // #pragma HLS ARRAY_PARTITION variable=weights complete // remove this line for now, it breaks compression sometimes + #pragma HLS ARRAY_PARTITION variable=scale complete + #pragma HLS ARRAY_PARTITION variable=bias complete + + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::multiplier_limit + +// Calcuate result +Result: + for (int ires = 0; ires < CONFIG_T::n_in; ires++) { + if (CONFIG_T::n_filt == -1) { + res[ires] = CONFIG_T::template product::product(data[ires], scale[ires]) + + bias[ires]; + } else { + int norm_index = ires % CONFIG_T::n_filt; + res[ires] = + CONFIG_T::template product::product(data[ires], scale[norm_index]) + + bias[norm_index]; + } + } +} + +// **************************************************** +// Merged Batch Normalization and Quantized Tanh +// **************************************************** +struct batchnorm_quantized_tanh_config { + // Layer Sizes + static const unsigned n_in = 10; + static const unsigned n_filt = -1; + static const unsigned n_scale_bias = 10; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; + static const unsigned n_zeros = 0; +}; + +template +void normalize_binary_tanh(data_T data[CONFIG_T::n_in], ap_uint<1> res[CONFIG_T::n_in], + data_T threshold[CONFIG_T::n_scale_bias]) { + //#pragma HLS PIPELINE + #pragma HLS ARRAY_PARTITION variable=res complete + + data_T datareg; + ap_uint<1> cache; + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + int norm_index = CONFIG_T::n_filt == -1 ? ii : ii % CONFIG_T::n_filt; + if (datareg >= threshold[norm_index]) + cache = 1; + else + cache = 0; + + res[ii] = cache; + } +} + +template +void normalize_ternary_tanh(data_T data[CONFIG_T::n_in], ap_int<2> res[CONFIG_T::n_in], + data_T threshold_hi[CONFIG_T::n_scale_bias], data_T threshold_lo[CONFIG_T::n_scale_bias]) { + //#pragma HLS PIPELINE + #pragma HLS ARRAY_PARTITION variable=res complete + + data_T datareg; + ap_int<2> cache; + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + datareg = data[ii]; + int norm_index = CONFIG_T::n_filt == -1 ? ii : ii % CONFIG_T::n_filt; + if (datareg > threshold_hi[norm_index]) + cache = 1; + else if (datareg <= threshold_lo[norm_index]) + cache = -1; + else + cache = 0; + + res[ii] = cache; + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_batchnorm_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_batchnorm_stream.h new file mode 100644 index 0000000000..b14c48e7aa --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_batchnorm_stream.h @@ -0,0 +1,126 @@ +#ifndef NNET_BATCHNORM_STREAM_H_ +#define NNET_BATCHNORM_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_mult.h" +#include "nnet_types.h" + +namespace nnet { + +// **************************************************** +// Streaming Batch Normalization +// **************************************************** + +template +void normalize(hls::stream &data, hls::stream &res, typename CONFIG_T::scale_t scale[CONFIG_T::n_scale_bias], + typename CONFIG_T::bias_t bias[CONFIG_T::n_scale_bias]) { + #pragma HLS ARRAY_PARTITION variable=scale complete + #pragma HLS ARRAY_PARTITION variable=bias complete + + constexpr unsigned ii = CONFIG_T::n_in / CONFIG_T::multiplier_limit; + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::multiplier_limit + +BatchNormLoop: + for (int i = 0; i < CONFIG_T::n_in / data_T::size; i++) { + //#pragma HLS PIPELINE II=ii + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + BatchNormpack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + int norm_index; + if (CONFIG_T::n_filt == -1) { + norm_index = i * data_T::size + j; + } else { + norm_index = j % CONFIG_T::n_filt; + } + out_data[j] = CONFIG_T::template product::product( + in_data[j], scale[norm_index]) + + bias[norm_index]; + } + + res.write(out_data); + } +} + +// **************************************************** +// Merged Batch Normalization and Quantized Tanh +// **************************************************** +template +void normalize_binary_tanh(hls::stream &data, hls::stream, CONFIG_T::n_scale_bias>> &res, + typename data_T::value_type threshold[CONFIG_T::n_scale_bias]) { + #pragma HLS ARRAY_PARTITION variable=threshold complete + +BinaryNormLoop: + for (int i = 0; i < CONFIG_T::n_in / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + nnet::array, CONFIG_T::n_scale_bias> out_data; + PRAGMA_DATA_PACK(out_data) + + BatchNormPack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + int norm_index; + if (CONFIG_T::n_filt == -1) { + norm_index = i * data_T::size + j; + } else { + norm_index = j % CONFIG_T::n_filt; + } + out_data[j] = (in_data[j] >= threshold[norm_index]) ? 1 : 0; + } + + res.write(out_data); + } +} + +template +void normalize_ternary_tanh(hls::stream &data, hls::stream, CONFIG_T::n_scale_bias>> &res, + typename data_T::value_type threshold_hi[CONFIG_T::n_scale_bias], + typename data_T::value_type threshold_lo[CONFIG_T::n_scale_bias]) { + #pragma HLS ARRAY_PARTITION variable=threshold_hi complete + #pragma HLS ARRAY_PARTITION variable=threshold_lo complete + +TernaryNormLoop: + for (int i = 0; i < CONFIG_T::n_in / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + nnet::array, CONFIG_T::n_scale_bias> out_data; + PRAGMA_DATA_PACK(out_data) + + BatchNormPack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + + int norm_index; + if (CONFIG_T::n_filt == -1) { + norm_index = i * data_T::size + j; + } else { + norm_index = j % CONFIG_T::n_filt; + } + + if (in_data[j] > threshold_hi[norm_index]) { + out_data[j] = 1; + } else if (in_data[j] <= threshold_lo[norm_index]) { + out_data[j] = -1; + } else { + out_data[j] = 0; + } + } + + res.write(out_data); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_code_gen.h b/hls4ml/templates/bambu/nnet_utils/nnet_code_gen.h new file mode 100644 index 0000000000..6011e20cca --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_code_gen.h @@ -0,0 +1,28 @@ +#ifndef NNET_INSTR_GEN_H_ +#define NNET_INSTR_GEN_H_ + +#include "nnet_conv1d_latency.h" +#include "nnet_helpers.h" + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_function_stubs.h" +#include "nnet_mult.h" + +namespace nnet { + +template class PointwiseConv1D { + public: + static void pointwise_conv(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + // To be implemented in subclasses + } +}; + +// hls4ml insert code + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_common.h b/hls4ml/templates/bambu/nnet_utils/nnet_common.h new file mode 100644 index 0000000000..f73ffb05f9 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_common.h @@ -0,0 +1,76 @@ +#ifndef NNET_COMMON_H_ +#define NNET_COMMON_H_ + +#include "ap_fixed.h" +#include "nnet_helpers.h" + +// This is a substitute for "ceil(n/(float)d)". +#define DIV_ROUNDUP(n, d) ((n + d - 1) / d) +#define MIN(n, d) (n > d ? d : n) +#define MAX(n, d) (n > d ? n : d) + +#define STRINGIFY(x) #x +#define EXPAND_STRING(x) STRINGIFY(x) + +#ifndef __BAMBU__ +#define DATA_PACK_TXT HLS DATA_PACK variable = +#define DATA_PACK_PRAGMA(variable) DATA_PACK_TXT variable +#define PRAGMA_DATA_PACK(variable) _Pragma(EXPAND_STRING(DATA_PACK_PRAGMA(variable))) +#else +#define PRAGMA_DATA_PACK(variable) +#endif + +namespace nnet { + +// Common type definitions +enum io_type { io_parallel = 0, io_stream }; +enum strategy { latency, resource, resource_unrolled, distributed_arithmetic }; + +/* --- + * Balanced tree reduce implementation. + * For use in scenarios where Vivado cannot expression balance + * Reduces an array of inputs to a single value using the template binary operator 'Op', + * for example summing all elements with Op_add, or finding the maximum with Op_max + * Use only when the input array is fully unrolled. Or, slice out a fully unrolled section + * before applying and accumulate the result over the rolled dimension. + * --- */ +template T reduce(const T *x, Op op) { + static constexpr int leftN = pow2(floorlog2(N - 1)) > 0 ? pow2(floorlog2(N - 1)) : 0; + static constexpr int rightN = N - leftN > 0 ? N - leftN : 0; + if (N == 1) { + return x[0]; + } + if (N == 2) { + return op(x[0], x[1]); + } + return op(reduce(x, op), reduce(x + leftN, op)); +} + +template class Op_add { + public: + T operator()(T a, T b) { return a + b; } +}; + +template class Op_and { + public: + T operator()(T a, T b) { return a && b; } +}; + +template class Op_or { + public: + T operator()(T a, T b) { return a || b; } +}; + +template class Op_max { + public: + T operator()(T a, T b) { return a >= b ? a : b; } +}; + +template class Op_min { + public: + T operator()(T a, T b) { return a <= b ? a : b; } +}; + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv1d.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d.h new file mode 100644 index 0000000000..7f52d5beef --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d.h @@ -0,0 +1,118 @@ +#ifndef NNET_CONV1D_H_ +#define NNET_CONV1D_H_ + +#include "nnet_common.h" +#include "nnet_conv1d_latency.h" +#include "nnet_conv1d_resource.h" +#include "nnet_function_stubs.h" +#include + +namespace nnet { + +struct conv1d_config { + // Internal data type definitions + typedef float bias_t; + typedef float weight_t; + typedef float accum_t; + + // Convolutional parameters + static const unsigned pad_left = 0; + static const unsigned pad_right = 0; + static const unsigned in_width = 10; + static const unsigned n_chan = 0; + static const unsigned filt_width = 1; + static const unsigned kernel_size = filt_width; + static const unsigned n_filt = 1; + static const unsigned stride_width = 1; + static const unsigned dilation = 1; + static const unsigned out_width = 10; //(N_IN + PAD_LEFT * PAD_RIGHT - (DILATION * (FILT_WIDTH - 1) + 1)) / STRIDE + 1 + + static const unsigned reuse_factor = 1; + static const bool store_weights_in_bram = false; + static const unsigned n_zeros = 0; // not used yet +}; + +template +void conv_1d_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + //#pragma HLS INLINE region + + CONFIG_T::template conv_kernel::conv(data, res, weights, biases); +} + +template +void pointwise_conv_1d_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::filt_width == 1); + + //#pragma HLS INLINE region + + CONFIG_T::template conv_kernel::conv(data, res, weights, biases); +} + +template class Conv1DLatency : public Conv1DKernel { + public: + static void conv(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + //#pragma HLS INLINE region + conv_1d_latency_cl(data, res, weights, biases); + } +}; + +template class Conv1DResource : public Conv1DKernel { + public: + static void conv(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + //#pragma HLS INLINE region + conv_1d_resource_cl(data, res, weights, biases); + } +}; + +template +class BatchedDenseForConv1D : public nnet::Conv1DKernel { + public: + static void conv(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + + //#pragma HLS PIPELINE II = 1 + //#pragma HLS INLINE RECURSIVE + data_T data_tmp[CONFIG_T::n_partitions][CONFIG_T::in_width * CONFIG_T::n_chan / CONFIG_T::n_partitions]; + #pragma HLS ARRAY_PARTITION variable=data_tmp complete dim=0 + res_T res_tmp[CONFIG_T::n_partitions][CONFIG_T::out_width * CONFIG_T::n_filt / CONFIG_T::n_partitions]; + #pragma HLS ARRAY_PARTITION variable=res_tmp complete dim=0 + + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_partitions; jj++) { + //#pragma HLS UNROLL + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::in_width * CONFIG_T::n_chan / CONFIG_T::n_partitions; ii++) { + //#pragma HLS UNROLL + data_tmp[jj][ii] = data[jj * CONFIG_T::in_width * CONFIG_T::n_chan / CONFIG_T::n_partitions + ii]; + } + } + + for (int jj = 0; jj < CONFIG_T::n_partitions; jj++) { + nnet::pointwise_conv_1d_latency_cl(data_tmp[jj], res_tmp[jj], weights, biases); + } + + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_partitions; jj++) { + //#pragma HLS UNROLL + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::out_width * CONFIG_T::n_filt / CONFIG_T::n_partitions; ii++) { + //#pragma HLS UNROLL + res[jj * CONFIG_T::out_width * CONFIG_T::n_filt / CONFIG_T::n_partitions + ii] = res_tmp[jj][ii]; + } + } + } +}; + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_latency.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_latency.h new file mode 100644 index 0000000000..ce867dde18 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_latency.h @@ -0,0 +1,171 @@ +#ifndef NNET_CONV1D_LATENCY_H_ +#define NNET_CONV1D_LATENCY_H_ + +#include "nnet_common.h" +#include "nnet_mult.h" +#include + +namespace nnet { + +template +void conv_1d_latency_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + constexpr unsigned mult_n_in = CONFIG_T::filt_width * CONFIG_T::n_chan; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=data_buf complete dim=0 + + typename CONFIG_T::accum_t mult[mult_n_in * mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=mult complete + + typename CONFIG_T::accum_t acc[mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + + #pragma HLS ARRAY_PARTITION variable=weights complete + #pragma HLS ARRAY_PARTITION variable=biases complete + + // Limit multipliers to control parallelization + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::mult_config::multiplier_limit + +PartitionLoop: + for (int i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor rewind + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + + data_T cache; + + // Do the matrix-multiply + Product1: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_in; i_in++) { + cache = data_buf[i_pxl][i_in]; + Product2: + #pragma clang loop unroll(full) + for (int i_out = 0; i_out < mult_n_out; i_out++) { + mult[i_in * mult_n_out + i_out] = + CONFIG_T::mult_config::template product::product( + cache, weights[i_in * mult_n_out + i_out]); + } + } + + // Initialize accumulator with input biases + ResetAccum: + #pragma clang loop unroll(full) + for (int i_acc = 0; i_acc < mult_n_out; i_acc++) { + acc[i_acc] = (typename CONFIG_T::accum_t)biases[i_acc]; + } + + // Accumulate multiplication result + Accum1: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_in; i_in++) { + Accum2: + #pragma clang loop unroll(full) + for (int i_out = 0; i_out < mult_n_out; i_out++) { + acc[i_out] += mult[i_in * mult_n_out + i_out]; + } + } + + // Cast to "res_t" type + Result: + #pragma clang loop unroll(full) + for (int i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + res[(i_part * CONFIG_T::n_pixels + i_pxl) * mult_n_out + i_res] = + cast(acc[i_res]); + } + } + } +} + +template +void pointwise_conv_1d_latency_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan / CONFIG_T::n_partitions], + res_T res[CONFIG_T::out_width * CONFIG_T::n_filt / CONFIG_T::n_partitions], + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::filt_width == 1); + + typename CONFIG_T::accum_t mult[CONFIG_T::out_width * CONFIG_T::n_filt * CONFIG_T::n_chan / CONFIG_T::n_partitions]; + typename CONFIG_T::accum_t acc[CONFIG_T::out_width / CONFIG_T::n_partitions][CONFIG_T::n_filt]; + + #pragma HLS ARRAY_PARTITION variable=mult complete dim=0 + #pragma HLS ARRAY_PARTITION variable=acc complete dim=0 + + // Use a function_instantiate in case it helps to explicitly optimize unchanging weights/biases + //#pragma HLS function_instantiate variable=weights,biases + + // Parallel mode + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + #pragma HLS ARRAY_PARTITION variable=weights complete dim=0 + #pragma HLS ARRAY_PARTITION variable=biases complete dim=0 + + // Limit multipliers to control parallelization + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::mult_config::multiplier_limit + +// Convolve, saving all multiplication results to accumulate later +ConvOut: + for (int ii = 0; ii < CONFIG_T::out_width / CONFIG_T::n_partitions; ii++) { + ConvFilt: + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + ConvChan: + #pragma clang loop unroll(full) + for (int cc = 0; cc < CONFIG_T::n_chan; cc++) { + //#pragma HLS UNROLL + int index_mult = ii * CONFIG_T::n_filt * CONFIG_T::n_chan + ff * CONFIG_T::n_chan + cc; + int index_weight = cc * CONFIG_T::n_filt + ff; + int index_data = (ii * CONFIG_T::stride_width - CONFIG_T::pad_left) * CONFIG_T::n_chan + cc; + + if ((ii * CONFIG_T::stride_width) < CONFIG_T::pad_left || + (ii * CONFIG_T::stride_width) >= (CONFIG_T::pad_left + CONFIG_T::in_width)) { + mult[index_mult] = 0; + } else { + mult[index_mult] = CONFIG_T::mult_config::template product::product( + data[index_data], weights[index_weight]); + } + } // end channel loop + } // end filter loop + } // end output loop + + // Initialize accumulator with input biases + for (int ii = 0; ii < CONFIG_T::out_width / CONFIG_T::n_partitions; ii++) { + #pragma clang loop unroll(full) + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + //#pragma HLS UNROLL + acc[ii][ff] = biases[ff]; + } + } + +// Accumulate multiplication result +AccumOut: + for (int ii = 0; ii < CONFIG_T::out_width / CONFIG_T::n_partitions; ii++) { + AccumFilt: + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + // Do "dot product" sum within filter and sum over channels + AccumChan: + for (int cc = 0; cc < CONFIG_T::n_chan; cc++) { + int index_mult = ii * CONFIG_T::n_filt * CONFIG_T::n_chan + ff * CONFIG_T::n_chan + cc; + acc[ii][ff] += mult[index_mult]; + } // end channel loop + } // end filter loop + } // end output loop + + // Cast to "res_t" type + for (int ii = 0; ii < CONFIG_T::out_width / CONFIG_T::n_partitions; ii++) { + #pragma clang loop unroll(full) + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + //#pragma HLS UNROLL + res[ii * CONFIG_T::n_filt + ff] = cast(acc[ii][ff]); + } + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_resource.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_resource.h new file mode 100644 index 0000000000..14aa6aa5bb --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_resource.h @@ -0,0 +1,123 @@ +#ifndef NNET_CONV1D_RESOURCE_H_ +#define NNET_CONV1D_RESOURCE_H_ + +#include "nnet_common.h" +#include "nnet_dense.h" + +namespace nnet { + +template +void conv_1d_resource_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + constexpr unsigned mult_n_in = CONFIG_T::filt_width * CONFIG_T::n_chan; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + constexpr unsigned block_factor = DIV_ROUNDUP(mult_n_in * mult_n_out, CONFIG_T::reuse_factor); + constexpr unsigned multscale = block_factor / mult_n_out; + + assert((block_factor % mult_n_out == 0 || CONFIG_T::reuse_factor >= mult_n_in) && + "The current Reuse Factor is not allowed"); + assert((CONFIG_T::reuse_factor <= CONFIG_T::filt_width * CONFIG_T::n_chan) && + "This function is correct only for RF <= FILT_WIDTH * N_CHAN"); + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=data_buf complete dim=0 + + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + #pragma HLS ARRAY_PARTITION variable=biases complete + + typename CONFIG_T::accum_t acc[CONFIG_T::n_pixels][mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete dim=0 + +PartitionLoop: + //#pragma clang loop unroll(full) We don't want this loop unrolled + for (unsigned i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelInitAccumLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + InitAccumLoop: + #pragma clang loop unroll(full) + for (unsigned i_acc = 0; i_acc < mult_n_out; i_acc++) { + //#pragma HLS UNROLL + acc[i_pxl][i_acc] = (typename CONFIG_T::accum_t)biases[i_acc]; + } + } + + ReuseLoop: + for (unsigned i_rf = 0; i_rf < CONFIG_T::reuse_factor; i_rf++) { + //#pragma HLS PIPELINE II=1 rewind + + unsigned i_in = i_rf; + unsigned i_out = 0; + unsigned i_acc = 0; + unsigned i_w = i_rf; + + MultLoop: + #pragma clang loop unroll(full) + for (unsigned i_blk = 0; i_blk < block_factor; i_blk++) { + //#pragma HLS UNROLL + + PixelMultLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + acc[i_pxl][i_out] += static_cast( + CONFIG_T::mult_config::template product::product( + data_buf[i_pxl][i_in], weights[i_w])); + } + + // Increment i_w + i_w += CONFIG_T::reuse_factor; + // Increment i_in + i_in += CONFIG_T::reuse_factor; + if (i_in >= mult_n_in) { + i_in = i_rf; + } + // Increment i_out + if (i_acc + 1 >= multscale) { + i_acc = 0; + i_out++; + } else { + i_acc++; + } + } + } + + // PixelResultLoop: + // #pragma clang loop unroll(full) + // for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + // //#pragma HLS UNROLL + // // Cast to "res_t" type + // ResultLoop: + // #pragma clang loop unroll(full) + // for (unsigned i_res = 0; i_res < mult_n_out; i_res++) { + // //#pragma HLS UNROLL + // *(res++) = cast(acc[i_pxl][i_res]); + // } + // } + + PixelResultLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + // Cast to "res_t" type + + ResultLoop: + #pragma clang loop unroll(full) + for (unsigned i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + res[(i_part * CONFIG_T::n_pixels + i_pxl) * mult_n_out + i_res] = + cast(acc[i_pxl][i_res]); + } + } + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_stream.h new file mode 100644 index 0000000000..533867eeaf --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv1d_stream.h @@ -0,0 +1,97 @@ +#ifndef NNET_CONV1D_STREAM_H_ +#define NNET_CONV1D_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_conv_stream.h" + +namespace nnet { + +template +void compute_scaled_indices_1d(const unsigned w_idx, ap_uint *pixel_idx) { + unsigned wp_idx = w_idx * (data_T::size / CONFIG_T::n_chan); + +ComputeIndex: + #pragma clang loop unroll(full) + for (unsigned p = 0; p < data_T::size / CONFIG_T::n_chan; p++) { + //#pragma HLS UNROLL + unsigned sw_idx = + CONFIG_T::template scale_index::scale_index( + wp_idx + p); + pixel_idx[p] = CONFIG_T::pixels[sw_idx]; + } +} + +template +void conv_1d_encoded_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + + hls::stream data_window[CONFIG_T::filt_width * CONFIG_T::n_chan]; + const int win_depth = CONFIG_T::out_width; + for (unsigned i_out = 0; i_out < CONFIG_T::filt_width * CONFIG_T::n_chan; i_out++) { + // TO BE ANALYZED + //#pragma HLS STREAM variable=data_window[i_out] depth=win_depth + } + + const ap_uint(&pixels)[CONFIG_T::min_width] = CONFIG_T::pixels; + #pragma HLS ARRAY_PARTITION variable=pixels complete + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + unsigned outputs_ready = 0; + + ap_uint pixel_idx[data_T::size / CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=pixel_idx complete + +ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (data_T::size / CONFIG_T::n_chan); i_iw++) { + //#pragma HLS LOOP_FLATTEN + if ((CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) && + data_T::size / CONFIG_T::n_chan == 1) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + compute_scaled_indices_1d(i_iw, pixel_idx); + compute_output_encoded(data.read(), data_window, res, res_pack, outputs_ready, weights, + biases, pixel_idx); + } +} + +template +void conv_1d_buffer_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + + if (CONFIG_T::strategy == nnet::resource_unrolled && CONFIG_T::reuse_factor > 1) { + //#pragma HLS allocation instances=compute_output_buffer_1d limit=1 function + } + +ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width; i_iw++) { + //#pragma HLS LOOP_FLATTEN + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + compute_output_buffer_1d(data.read(), res, weights, biases); + } +} + +template +void conv_1d_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + #pragma HLS inline recursive + switch (CONFIG_T::implementation) { + case conv_implementation::linebuffer: + conv_1d_buffer_cl(data, res, weights, biases); + break; + case conv_implementation::encoded: + conv_1d_encoded_cl(data, res, weights, biases); + break; + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv2d.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d.h new file mode 100644 index 0000000000..a36c09b49a --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d.h @@ -0,0 +1,80 @@ +#ifndef NNET_CONV2D_H_ +#define NNET_CONV2D_H_ + +#include "nnet_common.h" +#include "nnet_conv2d_latency.h" +#include "nnet_conv2d_resource.h" +#include +#include + +namespace nnet { + +struct conv2d_config { + // Internal data type definitions + typedef float bias_t; + typedef float weight_t; + typedef float accum_t; + + // Convolutional parameters + static const unsigned pad_top = 0; + static const unsigned pad_bottom = 0; + static const unsigned pad_left = 0; + static const unsigned pad_right = 0; + static const unsigned in_height = 10; + static const unsigned in_width = 10; + static const unsigned n_chan = 1; + static const unsigned filt_height = 1; + static const unsigned filt_width = 1; + static const unsigned kernel_size = filt_height * filt_width; + static const unsigned n_filt = 1; + static const unsigned stride_height = 1; + static const unsigned stride_width = 1; + static const unsigned out_height = 10; + static const unsigned out_width = 10; + static const unsigned dilation_height = 1; + static const unsigned dilation_width = 1; + + static const unsigned reuse_factor = 1; + static const bool store_weights_in_bram = false; + static const unsigned n_zeros = 0; // not used yet +}; + +template +void conv_2d_cl( + data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + //#pragma HLS INLINE region + + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + conv_2d_latency_cl(data, res, weights, biases); + } else if (CONFIG_T::strategy == nnet::resource || CONFIG_T::strategy == nnet::resource_unrolled) { + conv_2d_resource_cl(data, res, weights, biases); + } else { + assert(false && "Invalid strategy for conv_2d_cl"); + } +} + +template +void pointwise_conv_2d_cl(data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::filt_width == 1); + + //#pragma HLS INLINE region + + // Nothing special to be done for io_parallel implementation + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + conv_2d_latency_cl(data, res, weights, biases); + } else if (CONFIG_T::strategy == nnet::resource || CONFIG_T::strategy == nnet::resource_unrolled) { + conv_2d_resource_cl(data, res, weights, biases); + } else { + assert(false && "Invalid strategy for pointwise_conv_2d_cl"); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_latency.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_latency.h new file mode 100644 index 0000000000..b09074a836 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_latency.h @@ -0,0 +1,96 @@ +#ifndef NNET_CONV2D_LATENCY_H_ +#define NNET_CONV2D_LATENCY_H_ + +#include "nnet_common.h" +#include "nnet_mult.h" +#include + +namespace nnet { + +template +void conv_2d_latency_cl( + data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + constexpr unsigned mult_n_in = CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=data_buf complete dim=0 + + typename CONFIG_T::accum_t mult[mult_n_in * mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=mult complete + + typename CONFIG_T::accum_t acc[mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + + #pragma HLS ARRAY_PARTITION variable=weights complete + #pragma HLS ARRAY_PARTITION variable=biases complete + + // Limit multipliers to control parallelization + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::mult_config::multiplier_limit + +PartitionLoop: + for (int i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor rewind + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + data_T cache; + + // Do the matrix-multiply + Product1: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_in; i_in++) { + //#pragma HLS UNROLL + cache = data_buf[i_pxl][i_in]; + Product2: + #pragma clang loop unroll(full) + for (int i_out = 0; i_out < mult_n_out; i_out++) { + //#pragma HLS UNROLL + mult[i_in * mult_n_out + i_out] = + CONFIG_T::mult_config::template product::product( + cache, weights[i_in * mult_n_out + i_out]); + } + } + + // Initialize accumulator with input biases + ResetAccum: + #pragma clang loop unroll(full) + for (int i_acc = 0; i_acc < mult_n_out; i_acc++) { + //#pragma HLS UNROLL + acc[i_acc] = (typename CONFIG_T::accum_t)biases[i_acc]; + } + + // Accumulate multiplication result + Accum1: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_in; i_in++) { + //#pragma HLS UNROLL + Accum2: + #pragma clang loop unroll(full) + for (int i_out = 0; i_out < mult_n_out; i_out++) { + //#pragma HLS UNROLL + acc[i_out] += mult[i_in * mult_n_out + i_out]; + } + } + + // Cast to "res_t" type + Result: + #pragma clang loop unroll(full) + for (int i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + *(res++) = cast(acc[i_res]); + } + } + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_resource.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_resource.h new file mode 100644 index 0000000000..5e2023d05e --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_resource.h @@ -0,0 +1,112 @@ +#ifndef NNET_CONV2D_RESOURCE_H_ +#define NNET_CONV2D_RESOURCE_H_ + +#include "nnet_common.h" +#include "nnet_dense.h" + +namespace nnet { + +template +void conv_2d_resource_cl( + data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + constexpr unsigned mult_n_in = CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + constexpr unsigned block_factor = DIV_ROUNDUP(mult_n_in * mult_n_out, CONFIG_T::reuse_factor); + + constexpr unsigned multscale = block_factor / mult_n_out; + + assert((block_factor % mult_n_out == 0 || CONFIG_T::reuse_factor >= mult_n_in) && + "The current Reuse Factor is not allowed"); + assert((CONFIG_T::reuse_factor <= CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan) && + "This function is correct only for RF <= FILT_HEIGHT * FILT_WIDTH * N_CHAN"); + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=data_buf complete dim=0 + + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + #pragma HLS ARRAY_PARTITION variable=biases complete + + typename CONFIG_T::accum_t acc[CONFIG_T::n_pixels][mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete dim=0 + +PartitionLoop: + //#pragma clang loop unroll(full) + for (unsigned i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + //#pragma HLS UNROLL // We don't want this loop unrolled + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelInitAccumLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + InitAccumLoop: + #pragma clang loop unroll(full) + for (unsigned i_acc = 0; i_acc < mult_n_out; i_acc++) { + //#pragma HLS UNROLL + acc[i_pxl][i_acc] = (typename CONFIG_T::accum_t)biases[i_acc]; + } + } + + ReuseLoop: + for (unsigned i_rf = 0; i_rf < CONFIG_T::reuse_factor; i_rf++) { + //#pragma HLS PIPELINE II=1 rewind + + unsigned i_w = i_rf; + unsigned i_in = i_rf; + unsigned i_out = 0; + unsigned i_acc = 0; + + MultLoop: + #pragma clang loop unroll(full) + for (unsigned i_blk = 0; i_blk < block_factor; i_blk++) { + //#pragma HLS UNROLL + + PixelMultLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + acc[i_pxl][i_out] += static_cast( + CONFIG_T::mult_config::template product::product( + data_buf[i_pxl][i_in], weights[i_w])); + } + + // Increment i_w + i_w += CONFIG_T::reuse_factor; + // Increment i_in + i_in += CONFIG_T::reuse_factor; + if (i_in >= mult_n_in) { + i_in = i_rf; + } + // Increment i_out + if (i_acc + 1 >= multscale) { + i_acc = 0; + i_out++; + } else { + i_acc++; + } + } + } + + PixelResultLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + // Cast to "res_t" type + ResultLoop: + #pragma clang loop unroll(full) + for (unsigned i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + *(res++) = cast(acc[i_pxl][i_res]); + } + } + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_stream.h new file mode 100644 index 0000000000..361e68e688 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv2d_stream.h @@ -0,0 +1,119 @@ +#ifndef NNET_CONV2D_STREAM_H_ +#define NNET_CONV2D_STREAM_H_ + +#include "ap_shift_reg.h" +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_conv_stream.h" + +namespace nnet { + +template +void compute_scaled_indices_2d(const unsigned h_idx, const unsigned w_idx, + ap_uint *pixel_idx) { + const unsigned sh_idx = CONFIG_T::template scale_index_height::scale_index(h_idx); + unsigned wp_idx = w_idx * (data_T::size / CONFIG_T::n_chan); + +ComputeIndex: + #pragma clang loop unroll(full) + for (unsigned p = 0; p < data_T::size / CONFIG_T::n_chan; p++) { + //#pragma HLS UNROLL + + unsigned sw_idx = CONFIG_T::template scale_index_width::scale_index(wp_idx + p); + pixel_idx[p] = CONFIG_T::pixels[sh_idx * CONFIG_T::min_width + sw_idx]; + } +} + +template +void conv_2d_encoded_cl( + hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::filt_height == CONFIG_T::filt_width); + + hls::stream data_window[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan]; + const int win_depth = CONFIG_T::filt_height * CONFIG_T::out_width; + for (unsigned i_out = 0; i_out < CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan; i_out++) { + //#pragma HLS STREAM variable=data_window[i_out] depth=win_depth + } + + #pragma HLS ARRAY_PARTITION variable=CONFIG_T::pixels complete + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + unsigned outputs_ready = 0; + + ap_uint pixel_idx[data_T::size / CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=pixel_idx complete + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (data_T::size / CONFIG_T::n_chan); i_iw++) { + //#pragma HLS LOOP_FLATTEN + if ((CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) && + data_T::size / CONFIG_T::n_chan == 1) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + compute_scaled_indices_2d(i_ih, i_iw, pixel_idx); + compute_output_encoded(data.read(), data_window, res, res_pack, outputs_ready, weights, + biases, pixel_idx); + } + } +} + +// Line Buffer +template +void conv_2d_buffer_cl( + hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + + static ap_shift_reg line_buffer[MAX(CONFIG_T::filt_height - 1, 1)] + [CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable = line_buffer complete dim = 2 + + if (CONFIG_T::strategy == nnet::resource_unrolled && CONFIG_T::reuse_factor > 1) { + //#pragma HLS allocation instances=compute_output_buffer_1d limit=1 function + //#pragma HLS allocation instances=compute_output_buffer_2d limit=1 function + } + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width; i_iw++) { + //#pragma HLS LOOP_FLATTEN + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + if (CONFIG_T::filt_height > 1) { + compute_output_buffer_2d(data.read(), line_buffer, res, weights, biases); + } else { + compute_output_buffer_1d(data.read(), res, weights, biases); + } + } + } +} + +template +void conv_2d_cl( + hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + #pragma HLS inline recursive + switch (CONFIG_T::implementation) { + case conv_implementation::linebuffer: + conv_2d_buffer_cl(data, res, weights, biases); + break; + case conv_implementation::encoded: + conv_2d_encoded_cl(data, res, weights, biases); + break; + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_conv_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_conv_stream.h new file mode 100644 index 0000000000..88804c0bfd --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_conv_stream.h @@ -0,0 +1,392 @@ +#ifndef NNET_CONV_STREAM_H_ +#define NNET_CONV_STREAM_H_ + +#include "ap_shift_reg.h" +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_dense.h" + +namespace nnet { + +enum class conv_implementation { linebuffer = 0, encoded = 1 }; + +// ************************************************* +// Encoded Implementation (Vlad's) +// ************************************************* +template unsigned scale_index_K_gte_S(const unsigned idx) { + #pragma HLS INLINE + + if (idx < K - S) { + return idx; + } + + constexpr unsigned nW = ((W - K) / S) * S + K; // Nearest W without unused pixels on the right + constexpr unsigned sW = (DIV_ROUNDUP(K, S) - 1) * S + K; // Scaled W that behaves like original W + if (idx >= nW) { + return sW; + } + + const unsigned r = nW - idx; + if (r <= K - S) { + return sW - r; + } + + return K - S + (idx - (K - S)) % S; +} + +template unsigned scale_index_K_lt_S(const unsigned idx) { + #pragma HLS INLINE + + if (idx < S - K) { + return idx; + } + + constexpr unsigned nW = ((W - K) / S) * S + K; // Nearest W without unused pixels on the right + constexpr unsigned sW = (DIV_ROUNDUP(S, K) - 1) * S + K; // Scaled W that behaves like original W + if (idx >= nW) { + return sW; + } + + const unsigned r = nW - idx; + if (r <= S - K) { + return sW - r; + } + + return S - K + (idx - (S - K)) % S; +} + +template class scale_index_regular { + public: + static unsigned scale_index(const unsigned idx) { + #pragma HLS INLINE + + if (K >= S) { + return scale_index_K_gte_S(idx); + } else { + return scale_index_K_lt_S(idx); + } + } +}; + +template class scale_index_unscaled { + public: + static unsigned scale_index(const unsigned idx) { + #pragma HLS INLINE + return idx; + } +}; + +template +void mult_buffer(hls::stream data_window[CONFIG_T::kernel_size * CONFIG_T::n_chan], + res_T &res_pack, hls::stream &res_stream, unsigned &outputs_ready, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + #pragma HLS INLINE + + typename data_T::value_type data[CONFIG_T::kernel_size * CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable = data complete + typename res_T::value_type res[CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable = res complete + +InitData: + #pragma clang loop unroll(full) + for (int id = 0; id < CONFIG_T::kernel_size * CONFIG_T::n_chan; id++) { + //#pragma HLS UNROLL + data[id] = data_window[id].read(); + } + + //#pragma HLS INLINE recursive + CONFIG_T::mult_config::template kernel::dense(data, res, weights, biases); + +CastLoop: + #pragma clang loop unroll(full) + for (unsigned jj = 0; jj < CONFIG_T::n_filt; jj++) { + //#pragma HLS UNROLL + if (res_T::size / CONFIG_T::n_filt == 1) { + res_pack[jj] = res[jj]; + } else { + res_pack[outputs_ready * CONFIG_T::n_filt + jj] = res[jj]; + } + } + + if (res_T::size / CONFIG_T::n_filt == 1) { + res_stream.write(res_pack); + } else { + if (outputs_ready == (res_T::size / CONFIG_T::n_filt) - 1) { + res_stream.write(res_pack); + outputs_ready = 0; + } else { + outputs_ready++; + } + } +} + +template +void compute_output_encoded(const data_T &in_elem, + hls::stream data_window[CONFIG_T::kernel_size * CONFIG_T::n_chan], + hls::stream &res, res_T &res_pack, unsigned &outputs_ready, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt], ap_uint *pixel_idx) { + #pragma HLS INLINE + +MultLoop: + for (unsigned p = 0; p < data_T::size / CONFIG_T::n_chan; p++) { + //#pragma HLS PIPELINE II = CONFIG_T::reuse_factor + CopyDataFilt: + #pragma clang loop unroll(full) + for (unsigned f = 0; f < CONFIG_T::kernel_size; f++) { + //#pragma HLS UNROLL + CopyDataChan: + #pragma clang loop unroll(full) + for (unsigned c = 0; c < CONFIG_T::n_chan; c++) { + //#pragma HLS UNROLL + if (pixel_idx[p][f]) + data_window[f * CONFIG_T::n_chan + c].write(in_elem[p * CONFIG_T::n_chan + c]); + } + } + if (pixel_idx[p][CONFIG_T::kernel_size - 1]) { + mult_buffer(data_window, res_pack, res, outputs_ready, weights, biases); + } + } +} + +// ************************************************* +// Line Buffer Implementation (Phil's) +// ************************************************* +template +void kernel_shift_1d(const data_T &in_elem, + typename data_T::value_type kernel_window[CONFIG_T::filt_width * CONFIG_T::n_chan]) { + #pragma HLS inline + + // Shift kernel_window by one step to the left (manual shift operation) + static const int filt_width = CONFIG_T::filt_width - 1; +KernelShiftWidth: + for (int i_iw = 0; i_iw < filt_width; i_iw++) { + //#pragma HLS PIPELINE II = 1 + KernelShiftChannel: + #pragma clang loop unroll(full) + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_chan; i_ic++) { + //#pragma HLS UNROLL + // Shift every element in kernel_window to the left + kernel_window[i_iw * CONFIG_T::n_chan + i_ic] = kernel_window[(i_iw + 1) * CONFIG_T::n_chan + i_ic]; + } + } + + // Insert shift_buffer column into right-most column of kernel + static const int lastheight = (CONFIG_T::filt_width - 1) * CONFIG_T::n_chan; +KernelPushChannel: + #pragma clang loop unroll(full) + for (int i_ic = 0; i_ic < CONFIG_T::n_chan; i_ic++) { + //#pragma HLS UNROLL + kernel_window[lastheight + i_ic] = in_elem[i_ic]; + } +} + +template +void kernel_shift_2d( + typename data_T::value_type shift_buffer[CONFIG_T::filt_height][CONFIG_T::n_chan], + typename data_T::value_type kernel_window[CONFIG_T::filt_width * CONFIG_T::filt_height * CONFIG_T::n_chan]) { + #pragma HLS inline + + // Shift kernel_window by one step to the left (manual shift operation) + static const int filt_width = CONFIG_T::filt_width - 1; +KernelShiftWidth: + for (int i_iw = 0; i_iw < filt_width; i_iw++) { + //#pragma HLS PIPELINE II = 1 + KernelShiftHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::filt_height; i_ih++) { + KernelShiftChannel: + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_chan; i_ic++) { + // Shift every element in kernel_window to the left + kernel_window[i_ih * CONFIG_T::filt_width * CONFIG_T::n_chan + i_iw * CONFIG_T::n_chan + i_ic] = + kernel_window[i_ih * CONFIG_T::filt_width * CONFIG_T::n_chan + (i_iw + 1) * CONFIG_T::n_chan + i_ic]; + } + } + } + + // Insert shift_buffer column into right-most column of kernel + static const int lastheight = (CONFIG_T::filt_width - 1) * CONFIG_T::n_chan; +KernelPushHeight: + #pragma clang loop unroll(full) + for (int i_ih = 0; i_ih < CONFIG_T::filt_height; i_ih++) { + //#pragma HLS UNROLL + KernelPushChannel: + for (int i_ic = 0; i_ic < CONFIG_T::n_chan; i_ic++) { + kernel_window[lastheight + i_ih * CONFIG_T::filt_width * CONFIG_T::n_chan + i_ic] = shift_buffer[i_ih][i_ic]; + } + } +} + +template +void shift_line_buffer( + const data_T &in_elem, + ap_shift_reg line_buffer[MAX(CONFIG_T::filt_height - 1, 1)] + [CONFIG_T::n_chan], + typename data_T::value_type kernel_window[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan]) { + + //#pragma HLS PIPELINE + + // Temporary buffer for popped (shifted) elements + typename data_T::value_type shift_buffer[CONFIG_T::filt_height][CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable = shift_buffer complete dim = 0 + +UpdateBuffer: + #pragma clang loop unroll(full) + for (int i_ic = 0; i_ic < CONFIG_T::n_chan; i_ic++) { + //#pragma HLS UNROLL + + // Insert pixel(s) at end of shift buffer + shift_buffer[CONFIG_T::filt_height - 1][i_ic] = in_elem[i_ic]; + } + +LineBufferDataIn: + for (int i_ic = 0; i_ic < CONFIG_T::n_chan; i_ic++) { + // Shift the shift buffer into the line buffer + LineBufferShift: + #pragma clang loop unroll(full) + for (unsigned i_ih = 1; i_ih < CONFIG_T::filt_height; i_ih++) { + //#pragma HLS UNROLL + typename data_T::value_type pop_elem = line_buffer[i_ih - 1][i_ic].shift( + shift_buffer[CONFIG_T::filt_height - i_ih][i_ic]); // Shift the line buffer, return the popped pixel + shift_buffer[CONFIG_T::filt_height - i_ih - 1][i_ic] = + pop_elem; // Popped element placed back into shift_buffer, one row up. + } + } + kernel_shift_2d(shift_buffer, kernel_window); +} + +template +void compute_output_buffer_2d( + const data_T &in_elem, + ap_shift_reg line_buffer[MAX(CONFIG_T::filt_height - 1, 1)] + [CONFIG_T::n_chan], + hls::stream &res_stream, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + #pragma HLS INLINE OFF + + // Thresholds + const static int lShiftX = CONFIG_T::filt_width - 1; + const static int lShiftY = CONFIG_T::filt_height - 1; + + // Counters + static int pX = 0; // Pixel X + static int pY = 0; // Pixel Y + + static int sX = 0; // Stride X + static int sY = 0; // Stride Y + + static typename data_T::value_type kernel_data[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable = kernel_data complete + + typename res_T::value_type res_out[CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable = res_out complete dim = 0 + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + + // Add pixel to buffer + nnet::shift_line_buffer(in_elem, line_buffer, kernel_data); + + // Check to see if we have a full kernel + if ((sX - lShiftX) == 0 && (sY - lShiftY) == 0 && pY > lShiftY - 1 && pX > lShiftX - 1) { + + // Dense multiply + // #pragma HLS INLINE recursive + CONFIG_T::mult_config::template kernel::dense(kernel_data, res_out, weights, biases); + + // Pack output + CastLoop: + #pragma clang loop unroll(full) + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_filt; i_ic++) { + //#pragma HLS UNROLL + res_pack[i_ic] = res_out[i_ic]; + } + + // Write output to stream when output ready + res_stream.write(res_pack); + } + + // Counter Housekeeping + if (pX + 1 == CONFIG_T::in_width) // Includes padding, end of line (padded) + { + pX = 0; + sX = 0; + if (pY + 1 == CONFIG_T::in_height) { // Reached bottom of image + pY = 0; + sY = 0; + } else { + pY = pY + 1; + // Update stride (threshold) ? subtract stride : increment stride + sY = ((sY - lShiftY) == 0) ? sY - CONFIG_T::stride_height + 1 : sY + 1; + } + } else { + pX = pX + 1; + // Update stride (threshold) ? subtract stride : increment stride + sX = ((sX - lShiftX) == 0) ? sX - CONFIG_T::stride_width + 1 : sX + 1; + } +} + +// Conv 1D compute output +template +void compute_output_buffer_1d( + const data_T &in_elem, hls::stream &res_stream, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + #pragma HLS INLINE OFF + + // Thresholds + const static int lShiftX = CONFIG_T::filt_width - 1; + + // Counters + static int pX = 0; // pixel counter + static int sX = 0; // stride counter + + static typename data_T::value_type kernel_data[CONFIG_T::filt_width * CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable = kernel_data complete + + typename res_T::value_type res_out[CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable = res_out complete dim = 0 + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + + // Add pixel to buffer + nnet::kernel_shift_1d(in_elem, kernel_data); + + // Check to see if we have a full kernel + if ((sX - lShiftX) == 0 && pX > lShiftX - 1) { + + // Dense multiply + // #pragma HLS INLINE recursive + CONFIG_T::mult_config::template kernel::dense(kernel_data, res_out, weights, biases); + + // Pack output + CastLoop: + #pragma clang loop unroll(full) + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_filt; i_ic++) { + //#pragma HLS UNROLL + res_pack[i_ic] = res_out[i_ic]; + } + + // Write output to stream when output ready + res_stream.write(res_pack); + } + + // Counter Housekeeping + if (pX + 1 == CONFIG_T::in_width) // Includes padding, end of line (padded) + { + pX = 0; + sX = 0; + } else { + pX = pX + 1; + // Update stride (threshold) ? subtract stride : increment stride + sX = ((sX - lShiftX) == 0) ? sX - CONFIG_T::stride_width + 1 : sX + 1; + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_cropping.h b/hls4ml/templates/bambu/nnet_utils/nnet_cropping.h new file mode 100644 index 0000000000..13c704c2b2 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_cropping.h @@ -0,0 +1,92 @@ +#ifndef NNET_CROPPING_H_ +#define NNET_CROPPING_H_ + +#include + +namespace nnet { + +struct cropping1d_config { + static const unsigned n_chan = 10; + static const unsigned in_width = 10; + static const unsigned out_width = 10; + static const unsigned crop_left = 0; + static const unsigned crop_right = 0; +}; + +// no need for channel first for 1D cropping (no keras equivalent) +template +void cropping1d_cl(data_T data[CONFIG_T::n_chan * CONFIG_T::in_width], res_T res[CONFIG_T::n_chan * CONFIG_T::out_width]) { + //#pragma HLS PIPELINE + + // Skip cropped input from left + data += CONFIG_T::crop_left * CONFIG_T::n_chan; + + // Fill upto out_width (implicit cropping from right) + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::out_width; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_chan; j++) { + *(res++) = (res_T) * (data++); + } + } +} + +struct cropping2d_config { + static const unsigned n_chan = 10; + static const unsigned in_height = 10; + static const unsigned in_width = 10; + static const unsigned out_height = 10; + static const unsigned out_width = 10; + static const unsigned crop_top = 0; + static const unsigned crop_bottom = 0; + static const unsigned crop_left = 0; + static const unsigned crop_right = 0; +}; + +template +void cropping2d_cf(data_T data[CONFIG_T::n_chan * CONFIG_T::in_height * CONFIG_T::in_width], + res_T res[CONFIG_T::n_chan * CONFIG_T::out_height * CONFIG_T::out_width]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { // channels first + // Skip current channel data from top and left + data_T *data_ptr = data + k * CONFIG_T::in_height * CONFIG_T::in_width + CONFIG_T::crop_top * CONFIG_T::in_width + + CONFIG_T::crop_left; + + // Fill upto out_height and out_width + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::out_height; i++) { + data_T *row_ptr = data_ptr + i * CONFIG_T::in_width; + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::out_width; j++) { + *(res++) = (res_T) * (row_ptr++); + } + } + } +} + +template +void cropping2d_cl(data_T data[CONFIG_T::n_chan * CONFIG_T::in_height * CONFIG_T::in_width], + res_T res[CONFIG_T::n_chan * CONFIG_T::out_height * CONFIG_T::out_width]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::out_height; i++) { + int in_row = i + CONFIG_T::crop_top; + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::out_width; j++) { + int in_col = j + CONFIG_T::crop_left; + + data_T *data_ptr = data + (in_row * CONFIG_T::in_width + in_col) * CONFIG_T::n_chan; + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { // channels last + *(res++) = (res_T) * (data_ptr++); + } + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_cropping_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_cropping_stream.h new file mode 100644 index 0000000000..120159c6ed --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_cropping_stream.h @@ -0,0 +1,75 @@ +#ifndef NNET_CROPPING_STREAM_H_ +#define NNET_CROPPING_STREAM_H_ + +#include "nnet_padding_stream.h" // fill_data function +#include + +namespace nnet { + +template +void cropping1d_cl(hls::stream &data, hls::stream &res) { + //#pragma HLS PIPELINE + + // Discard left + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::crop_left; i++) { + data.read(); + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::out_width; i++) { + fill_data(data, res); + } + + // Discard right + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::crop_right; i++) { + data.read(); + } +} + +template +void cropping2d_cl(hls::stream &data, hls::stream &res) { + //#pragma HLS PIPELINE + + // Discard top rows + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::crop_top; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::in_width; j++) { + data.read(); + } + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::out_height; i++) { + // Discard left columns + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::crop_left; j++) { + data.read(); + } + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::out_width; j++) { + fill_data(data, res); + } + + // Discard right columns + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::crop_right; j++) { + data.read(); + } + } + + // Discard bottom rows + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::crop_bottom; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::in_width; j++) { + data.read(); + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_da_wrappers.h b/hls4ml/templates/bambu/nnet_utils/nnet_da_wrappers.h new file mode 100644 index 0000000000..523927c38a --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_da_wrappers.h @@ -0,0 +1,127 @@ +#ifndef NNET_UNROLLED__H_ +#define NNET_UNROLLED__H_ + +#include "nnet_common.h" +#include "nnet_helpers.h" + +namespace nnet { + +template +typename std::enable_if::type +dense(hls::stream &data_stream, hls::stream &res_stream) { + typename data_T::value_type data[CONFIG_T::n_in]; + #pragma HLS ARRAY_PARTITION variable=data complete + + typename res_T::value_type res[CONFIG_T::n_out]; + #pragma HLS ARRAY_PARTITION variable=res complete + +DataPrepare: + for (int i_in = 0; i_in < CONFIG_T::n_in / data_T::size; i_in++) { + if (CONFIG_T::n_in / data_T::size > 1) { + //#pragma HLS PIPELINE + } + data_T data_pack = data_stream.read(); + DataPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < data_T::size; i_pack++) { + //#pragma HLS UNROLL + data[i_in * data_T::size + i_pack] = data_pack[i_pack]; + } + } + + CONFIG_T::dense_da(data, res); + +ResWrite: + for (unsigned i_out = 0; i_out < CONFIG_T::n_out / res_T::size; i_out++) { + if (CONFIG_T::n_out / res_T::size > 1) { + //#pragma HLS PIPELINE + } + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + ResPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = res[i_out * res_T::size + i_pack]; + } + res_stream.write(res_pack); + } +} + +template +inline typename std::enable_if::type +conv1d_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], res_T res[CONFIG_T::out_width * CONFIG_T::n_filt]) { + constexpr unsigned mult_n_in = CONFIG_T::n_chan * CONFIG_T::filt_width; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable = data_buf complete dim = 0 + #pragma HLS inline + + res_T out_buf[mult_n_out]; + +PartitionLoop: + for (int i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + //#pragma HLS PIPELINE II = 1 rewind + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + // Do the matrix-multiply + CONFIG_T::dense_da(data_buf[i_pxl], out_buf); + + Result: + #pragma clang loop unroll(full) + for (int i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + *(res++) = out_buf[i_res]; + } + } + } +} + +template +inline typename std::enable_if::type +conv2d_cl(data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt]) { + constexpr unsigned mult_n_in = CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=data_buf complete dim = 0 + #pragma HLS inline + + res_T out_buf[mult_n_out]; + +PartitionLoop: + for (int i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + //#pragma HLS PIPELINE II=1 rewind + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + data_T cache; + + CONFIG_T::dense_da(data_buf[i_pxl], out_buf); + + Result: + #pragma clang loop unroll(full) + for (int i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + *(res++) = out_buf[i_res]; + } + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_dense.h b/hls4ml/templates/bambu/nnet_utils/nnet_dense.h new file mode 100644 index 0000000000..e9e469a5a6 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_dense.h @@ -0,0 +1,92 @@ +#ifndef NNET_DENSE_H_ +#define NNET_DENSE_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_dense_latency.h" +#include "nnet_dense_resource.h" +#include "nnet_function_stubs.h" +#include "nnet_helpers.h" +#include "nnet_mult.h" +#include + +namespace nnet { + +struct dense_config { + // Internal data type definitions + typedef float bias_t; + typedef float weight_t; + typedef float accum_t; + + // Layer Sizes + static const unsigned n_in = 10; + static const unsigned n_out = 10; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned strategy = latency; + static const unsigned reuse_factor = 1; + static const bool store_weights_in_bram = false; + static const unsigned n_zeros = 0; + + template using kernel = nnet::DenseKernel; + + // Partitioning arrays cyclically to go with roll factors? + + // Product function to use + template using product = nnet::product::mult; +}; + +template +void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + #pragma HLS inline + CONFIG_T::template kernel::dense(data, res, weights, biases); +} + +// Two-argument overload: weights/biases come from CONFIG_T (compile-time +// resolved class members). The Bambu backend emitter calls this form so +// the wrapper's DATAFLOW scope doesn't pass weights as runtime pointer +// parameters — those bind to `DF_bambu_*FO0` interfaces that read zero +// at runtime. +template void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out]) { + #pragma HLS inline + CONFIG_T::template kernel::dense(data, res, CONFIG_T::weights, CONFIG_T::biases); +} + +template class DenseLatency : public DenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + //#pragma HLS INLINE + dense_latency(data, res, weights, biases); + } +}; + +template +class DenseResource_rf_leq_nin : public DenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + //#pragma HLS INLINE + dense_resource_rf_leq_nin(data, res, weights, biases); + } +}; + +template +class DenseResource_rf_gt_nin_rem0 : public DenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + //#pragma HLS INLINE + dense_resource_rf_gt_nin_rem0(data, res, weights, biases); + } +}; + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_dense_compressed.h b/hls4ml/templates/bambu/nnet_utils/nnet_dense_compressed.h new file mode 100644 index 0000000000..02df23776f --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_dense_compressed.h @@ -0,0 +1,95 @@ +#ifndef NNET_COMPRESSED_LAYER_H_ +#define NNET_COMPRESSED_LAYER_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_dense.h" +#include + +namespace nnet { + +template +void fill_mult(typename CONFIG_T::index_t index, typename CONFIG_T::accum_t mult[CONFIG_T::n_out], + typename CONFIG_T::accum_t weight) { + #pragma clang loop unroll(full) + for (unsigned k = 0; k < CONFIG_T::n_out; k++) { + //#pragma HLS UNROLL + if (k == index) + mult[k] += weight; + } +} + +template +void dense_compressed(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_nonzeros], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + const int multiplier_limit = DIV_ROUNDUP(CONFIG_T::n_nonzeros, CONFIG_T::reuse_factor); + + typename CONFIG_T::accum_t acc[CONFIG_T::n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + #pragma HLS ARRAY_PARTITION variable=biases complete + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=multiplier_limit + +#ifdef __VITIS_HLS__ + //#pragma HLS AGGREGATE variable=weights +#else + //#pragma HLS data_pack variable=weights struct_level +#endif + +InitAccum: + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_out; i++) { + //#pragma HLS UNROLL + acc[i] = (typename CONFIG_T::accum_t)(biases[i]); + } + + // Do the compressed matrix-multiply + const int rufactor = CONFIG_T::reuse_factor; +ReuseLoop: + for (unsigned ir = 0; ir < rufactor; ir++) { + //#pragma HLS PIPELINE II=1 rewind + + typename CONFIG_T::accum_t mult[CONFIG_T::n_out]; + #pragma HLS ARRAY_PARTITION variable=mult complete + + ResetMult: + #pragma clang loop unroll(full) + for (int imult = 0; imult < CONFIG_T::n_out; imult++) { + //#pragma HLS UNROLL + mult[imult] = 0; + } + + CompressedMultLoop: + #pragma clang loop unroll(full) + for (unsigned im = 0; im < multiplier_limit; im++) { + //#pragma HLS UNROLL + unsigned w = im * rufactor + ir; + auto row = weights[w].row_index; + auto col = weights[w].col_index; + auto weight_cache = weights[w].weight; + data_T data_cache = data[row]; + // mult[col] += weight_cache * data_cache; + typename CONFIG_T::accum_t prod = + CONFIG_T::template product::product(data_cache, weight_cache); + fill_mult(col, mult, prod); + } + + for (int im = 0; im < CONFIG_T::n_out; im++) { + acc[im] += mult[im]; + } + } + +// Cast to "res_t" type +ResultLoop: + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_out; i++) { + //#pragma HLS UNROLL + // res[i] = (res_T) (acc[i]); + res[i] = cast(acc[i]); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_dense_latency.h b/hls4ml/templates/bambu/nnet_utils/nnet_dense_latency.h new file mode 100644 index 0000000000..8e88028e93 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_dense_latency.h @@ -0,0 +1,78 @@ +#ifndef NNET_DENSE_LATENCY_H_ +#define NNET_DENSE_LATENCY_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_helpers.h" +#include "nnet_mult.h" +#include + +namespace nnet { + +template +void dense_latency(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + data_T cache; + typename CONFIG_T::accum_t mult[CONFIG_T::n_in * CONFIG_T::n_out]; + typename CONFIG_T::accum_t acc[CONFIG_T::n_out]; + + // Use a function_instantiate in case it helps to explicitly optimize unchanging weights/biases + //#pragma HLS function_instantiate variable=weights,biases + + // For parallel inputs: + // - completely partition arrays -- target fabric + // - if we have an unroll factor, limit number of multipliers + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + // #pragma HLS ARRAY_PARTITION variable=weights complete // remove this line for now, it breaks compression sometimes + #pragma HLS ARRAY_PARTITION variable=biases complete + #pragma HLS ARRAY_PARTITION variable=mult complete + #pragma HLS ARRAY_PARTITION variable=acc complete + + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::multiplier_limit + +// Do the matrix-multiply +Product1: + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + cache = data[ii]; + Product2: + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_out; jj++) { + int index = ii * CONFIG_T::n_out + jj; + mult[index] = CONFIG_T::template product::product(cache, weights[index]); + } + } + +// Initialize accumulator with input biases +ResetAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < CONFIG_T::n_out; iacc++) { + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + +// Accumulate multiplication result +Accum1: + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + Accum2: + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_out; jj++) { + int index = ii * CONFIG_T::n_out + jj; + acc[jj] += mult[index]; + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < CONFIG_T::n_out; ires++) { + // res[ires] = (res_T) (acc[ires]); + res[ires] = cast(acc[ires]); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_dense_resource.h b/hls4ml/templates/bambu/nnet_utils/nnet_dense_resource.h new file mode 100644 index 0000000000..bfd7d73068 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_dense_resource.h @@ -0,0 +1,284 @@ +#ifndef NNET_DENSE_RESOURCE_H_ +#define NNET_DENSE_RESOURCE_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_mult.h" +#include +#include + +namespace nnet { + +template +void dense_resource_rf_leq_nin(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + const int rufactor = CONFIG_T::reuse_factor; + const int multfactor = MIN(CONFIG_T::n_in, CONFIG_T::reuse_factor); + const int multiplier_limit = DIV_ROUNDUP(CONFIG_T::n_in * CONFIG_T::n_out, multfactor); + const int block_factor = DIV_ROUNDUP(CONFIG_T::n_in * CONFIG_T::n_out, CONFIG_T::reuse_factor); + const int multscale = multiplier_limit / CONFIG_T::n_out; + const int nin = CONFIG_T::n_in; + const int nout = CONFIG_T::n_out; + + assert((multiplier_limit % nout == 0 || rufactor >= nin) && "The current Reuse Factor is not allowed"); + assert((multiplier_limit == block_factor) && "This function is correct only for RF <= N_IN"); + + //#pragma HLS function_instantiate variable=weights,biases + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + #pragma HLS ARRAY_PARTITION variable=biases complete + + if (CONFIG_T::reuse_factor > 1) { + //#pragma HLS RESOURCE variable=weights core=ROM_nP_BRAM + } + + typename CONFIG_T::accum_t acc[CONFIG_T::n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + +InitAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < nout; iacc++) { + //#pragma HLS UNROLL + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + +ReuseLoop: + for (int ir = 0; ir < rufactor; ir++) { + //#pragma HLS PIPELINE II=1 rewind + + int w_index = ir; + int in_index = ir; + int out_index = 0; + int acc_step = 0; + + MultLoop: + #pragma clang loop unroll(full) + for (int im = 0; im < block_factor; im++) { + //#pragma HLS UNROLL + + acc[out_index] += static_cast( + CONFIG_T::template product::product(data[in_index], weights[w_index])); + + // Increment w_index + w_index += rufactor; + // Increment in_index + in_index += rufactor; + if (in_index >= nin) { + in_index = ir; + } + // Increment out_index + if (acc_step + 1 >= multscale) { + acc_step = 0; + out_index++; + } else { + acc_step++; + } + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < CONFIG_T::n_out; ires++) { + //#pragma HLS UNROLL + res[ires] = cast(acc[ires]); + } +} + +template +void dense_resource_rf_gt_nin_rem0(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + const int rufactor = MIN(CONFIG_T::reuse_factor, CONFIG_T::n_in * CONFIG_T::n_out); + const int multfactor = MIN(CONFIG_T::n_in, CONFIG_T::reuse_factor); + const int multiplier_limit = DIV_ROUNDUP(CONFIG_T::n_in * CONFIG_T::n_out, multfactor); + const int block_factor = DIV_ROUNDUP(CONFIG_T::n_in * CONFIG_T::n_out, CONFIG_T::reuse_factor); + const int multscale = multiplier_limit / CONFIG_T::n_out; + const int nin = CONFIG_T::n_in; + const int nout = CONFIG_T::n_out; + + assert((multiplier_limit % nout == 0 || rufactor >= nin) && "The current Reuse Factor is not allowed"); + assert((rufactor > nin && rufactor % nin == 0) && "This function is correct only for RF > N_IN && RF % N_IN == 0"); + + //#pragma HLS function_instantiate variable=weights,biases + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + #pragma HLS ARRAY_PARTITION variable=biases complete + + if (CONFIG_T::reuse_factor > 1) { + //#pragma HLS RESOURCE variable=weights core=ROM_nP_BRAM + } + + typename CONFIG_T::accum_t acc[CONFIG_T::n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + +InitAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < nout; iacc++) { + //#pragma HLS UNROLL + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + + int w_index; + int in_index = 0; + int out_index; + int outstep = 0; + const int outscale = rufactor / nin; + + int outidx[rufactor]; +IndexLoop: + for (int ir = 0; ir < rufactor; ir++) { + outidx[ir] = outstep; + if ((ir + 1) % nin == 0) { + outstep++; + } + } + +ReuseLoop: + for (int ir = 0; ir < rufactor; ir++) { + //#pragma HLS PIPELINE II=1 rewind + + w_index = ir; + out_index = outidx[ir] /*outstep*/; + + MultLoop: + #pragma clang loop unroll(full) + for (int im = 0; im < block_factor; im++) { + //#pragma HLS UNROLL + acc[out_index] += static_cast( + CONFIG_T::template product::product(data[in_index], weights[w_index])); + + w_index += rufactor; + if (w_index >= CONFIG_T::n_in * CONFIG_T::n_out) + break; // check out of bounds + out_index += outscale; + } + + in_index++; + if (in_index >= nin) { + in_index = 0; + // outstep++; // This causes a huge increase in scheduling and RTL generation times, hence the above workaround. + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < CONFIG_T::n_out; ires++) { + //#pragma HLS UNROLL + res[ires] = cast(acc[ires]); + } +} + +template +void dense_resource_rf_gt_nin(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + const int rufactor = CONFIG_T::reuse_factor; + const int multfactor = MIN(CONFIG_T::n_in, CONFIG_T::reuse_factor); + const int multiplier_limit = DIV_ROUNDUP(CONFIG_T::n_in * CONFIG_T::n_out, multfactor); + const int block_factor = DIV_ROUNDUP(CONFIG_T::n_in * CONFIG_T::n_out, CONFIG_T::reuse_factor); + const int multscale = multiplier_limit / CONFIG_T::n_out; + const int nin = CONFIG_T::n_in; + const int nout = CONFIG_T::n_out; + + assert((multiplier_limit % nout == 0 || rufactor >= nin) && "The current Reuse Factor is not allowed"); + assert((rufactor > nin) && "This function is correct only for RF > N_IN"); + + //#pragma HLS function_instantiate variable=weights,biases + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + #pragma HLS ARRAY_PARTITION variable=biases complete + + if (CONFIG_T::reuse_factor > 1) { + //#pragma HLS RESOURCE variable=weights core=ROM_nP_BRAM + } + + typename CONFIG_T::accum_t acc[CONFIG_T::n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + +InitAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < nout; iacc++) { + //#pragma HLS UNROLL + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + +ReuseLoop: + for (int ir = 0; ir < rufactor; ir++) { + //#pragma HLS PIPELINE II=1 rewind + typename CONFIG_T::accum_t tmpmult[block_factor]; + #pragma HLS ARRAY_PARTITION variable=tmpmult complete + + MultLoop: + #pragma clang loop unroll(full) + for (int im = 0; im < block_factor; im++) { + //#pragma HLS UNROLL + int w_index = ir + rufactor * im; + int in_index = w_index % nin; + if (w_index >= CONFIG_T::n_in * CONFIG_T::n_out) + continue; // check out of bounds + tmpmult[im] = + CONFIG_T::template product::product(data[in_index], weights[w_index]); + } + + typename CONFIG_T::accum_t mult[multiplier_limit]; + #pragma HLS ARRAY_PARTITION variable=mult complete + + ResetMult: + #pragma clang loop unroll(full) + for (int imult = 0; imult < multiplier_limit; imult++) { + //#pragma HLS UNROLL + mult[imult] = 0; + } + + AccumLoop1: + #pragma clang loop unroll(full) + for (int im = 0; im < block_factor; im++) { + //#pragma HLS UNROLL + int w_index = ir + rufactor * im; + int out_index = w_index / multfactor; + if (out_index >= multiplier_limit) + continue; // check out of bounds + mult[out_index] += tmpmult[im]; + } + + AccumLoop2: + #pragma clang loop unroll(full) + for (int im = 0; im < multiplier_limit; im++) { + //#pragma HLS UNROLL + // int out_index = im/multscale; // This is the general case + // acc[out_index] += mult[im]; + acc[im] += mult[im]; // If RF > N_IN then multiplier_limit == n_out + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < CONFIG_T::n_out; ires++) { + //#pragma HLS UNROLL + res[ires] = cast(acc[ires]); + } +} + +template +void dense_resource(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + #pragma HLS inline recursive + + if (CONFIG_T::reuse_factor <= CONFIG_T::n_in) { + dense_resource_rf_leq_nin(data, res, weights, biases); + } else if (CONFIG_T::reuse_factor % CONFIG_T::n_in == 0) { + dense_resource_rf_gt_nin_rem0(data, res, weights, biases); + } else { + dense_resource_rf_gt_nin(data, res, weights, biases); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_dense_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_dense_stream.h new file mode 100644 index 0000000000..0885417ca1 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_dense_stream.h @@ -0,0 +1,73 @@ +#ifndef NNET_DENSE_STREAM_H_ +#define NNET_DENSE_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_types.h" +#include +#include + +namespace nnet { + +template +void dense_wrapper(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + #pragma HLS inline recursive + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + CONFIG_T::template kernel::dense(data, res, weights, biases); +} + +// Weights/biases are reached through `CONFIG_T` rather than taken as +// pointer-array parameters: Bambu's DATAFLOW scheduler binds array-pointer +// parameters of sub-functions to internal `DF_bambu_*FO0` interfaces that +// read zero at runtime, regardless of what the .mem init file contains. +// CONFIG_T::weights / ::biases are compile-time-resolved class members, +// so the callee inlines them correctly. +template +void dense(hls::stream &data_stream, hls::stream &res_stream) { + typename data_T::value_type data[CONFIG_T::n_in]; + #pragma HLS ARRAY_PARTITION variable=data complete + + typename res_T::value_type res[CONFIG_T::n_out]; + #pragma HLS ARRAY_PARTITION variable=res complete + +DataPrepare: + for (int i_in = 0; i_in < CONFIG_T::n_in / data_T::size; i_in++) { + if (CONFIG_T::n_in / data_T::size > 1) { + //#pragma HLS PIPELINE + } + data_T data_pack = data_stream.read(); + DataPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < data_T::size; i_pack++) { + //#pragma HLS UNROLL + data[i_in * data_T::size + i_pack] = data_pack[i_pack]; + } + } + + dense_wrapper(data, res, CONFIG_T::weights, + CONFIG_T::biases); + +ResWrite: + for (unsigned i_out = 0; i_out < CONFIG_T::n_out / res_T::size; i_out++) { + if (CONFIG_T::n_out / res_T::size > 1) { + //#pragma HLS PIPELINE + } + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + ResPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = res[i_out * res_T::size + i_pack]; + } + res_stream.write(res_pack); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_depthwise_product.h b/hls4ml/templates/bambu/nnet_utils/nnet_depthwise_product.h new file mode 100644 index 0000000000..9baadaa8d2 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_depthwise_product.h @@ -0,0 +1,312 @@ +#ifndef NNET_DEPTHWISE_PRODUCT_H_ +#define NNET_DEPTHWISE_PRODUCT_H_ + +namespace nnet { + +template +void depthwise_product_latency(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + //#pragma HLS INLINE + + typename CONFIG_T::accum_t mult[CONFIG_T::n_in]; + typename CONFIG_T::accum_t acc[CONFIG_T::n_out]; + + // Use a function_instantiate in case it helps to explicitly optimize unchanging weights/biases + //#pragma HLS function_instantiate variable=weights + + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + #pragma HLS ARRAY_PARTITION variable=mult complete + + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::multiplier_limit + +// Do the matrix-multiply +Product: + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_in; ii++) { + //#pragma HLS UNROLL + mult[ii] = CONFIG_T::template product::product(data[ii], weights[ii]); + } + +// Initialize accumulator with input biases +ResetAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < CONFIG_T::n_out; iacc++) { + //#pragma HLS UNROLL + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + +// Accumulate multiplication result +Accum1: + for (int ii = 0; ii < CONFIG_T::n_in / CONFIG_T::n_out; ii++) { + Accum2: + for (int jj = 0; jj < CONFIG_T::n_out; jj++) { + int index = ii * CONFIG_T::n_out + jj; + acc[jj] += mult[index]; + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < CONFIG_T::n_out; ires++) { + //#pragma HLS UNROLL + res[ires] = cast(acc[ires]); + } +} + +template +void depthwise_product_resource_rf_leq_nout(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + const int nin = CONFIG_T::n_in; + const int nout = CONFIG_T::n_out; + const int rufactor = CONFIG_T::reuse_factor; + const int multfactor = MIN(CONFIG_T::n_in, rufactor); + const int multiplier_limit = DIV_ROUNDUP(nin, multfactor); + const int block_factor = DIV_ROUNDUP(nin, rufactor); + + assert((multiplier_limit == block_factor) && "This function is correct only for RF <= N_CHAN"); + + //#pragma HLS function_instantiate variable=weights,biases + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + //#pragma HLS ARRAY_RESHAPE variable=data block factor=block_factor + + #pragma HLS ARRAY_PARTITION variable=biases complete + + typename CONFIG_T::accum_t acc[nout]; + #pragma HLS ARRAY_PARTITION variable=acc complete + +InitAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < nout; iacc++) { + //#pragma HLS UNROLL + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + +ReuseLoop: + for (int ir = 0; ir < rufactor; ir++) { + //#pragma HLS PIPELINE II=1 rewind + + int in_index = ir; + int out_index = ir; + + MultLoop: + #pragma clang loop unroll(full) + for (int im = 0; im < block_factor; im++) { + //#pragma HLS UNROLL + + acc[out_index] += static_cast( + CONFIG_T::template product::product(data[in_index], weights[in_index])); + + in_index += rufactor; + out_index += rufactor; + + if (out_index >= nout) { + out_index -= nout; + } + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < nout; ires++) { + //#pragma HLS UNROLL + res[ires] = cast(acc[ires]); + } +} + +template +void depthwise_product_resource_rf_gt_nout_rem0(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + const int nin = CONFIG_T::n_in; + const int nout = CONFIG_T::n_out; + const int rufactor = MIN(CONFIG_T::reuse_factor, nin); + const int multfactor = MIN(nin, rufactor); + const int multiplier_limit = DIV_ROUNDUP(nin, multfactor); + const int block_factor = DIV_ROUNDUP(nin, rufactor); + + assert((rufactor >= nout && rufactor % nout == 0) && + "This function is correct only for RF >= N_CHAN && RF % N_CHAN == 0"); + + //#pragma HLS function_instantiate variable=weights,biases + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + //#pragma HLS ARRAY_RESHAPE variable=data block factor=block_factor + + #pragma HLS ARRAY_PARTITION variable=biases complete + + typename CONFIG_T::accum_t acc[nout]; + #pragma HLS ARRAY_PARTITION variable=acc complete + +InitAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < nout; iacc++) { + //#pragma HLS UNROLL + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + + int outidx[rufactor]; + int outstep = 0; +IndexLoop: + for (int ir = 0; ir < rufactor; ir++) { + outidx[ir] = outstep; + outstep++; + if (outstep == nout) { + outstep = 0; + } + } + + int out_index = 0; + +ReuseLoop: + for (int ir = 0; ir < rufactor; ir++) { + //#pragma HLS PIPELINE II=1 rewind + + int in_index = ir; + out_index = outidx[ir]; + + MultLoop: + #pragma clang loop unroll(full) + for (int im = 0; im < block_factor; im++) { + //#pragma HLS UNROLL + + acc[out_index] += static_cast( + CONFIG_T::template product::product(data[in_index], weights[in_index])); + + in_index += rufactor; + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < nout; ires++) { + //#pragma HLS UNROLL + res[ires] = cast(acc[ires]); + } +} + +template +void depthwise_product_resource_gt_nout(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + + const int nin = CONFIG_T::n_in; + const int nout = CONFIG_T::n_out; + const int rufactor = MIN(CONFIG_T::reuse_factor, nin); + const int block_factor = DIV_ROUNDUP(nin, rufactor); + assert((rufactor > nout) && "This function is correct only for RF > N_CHAN"); + + //#pragma HLS function_instantiate variable=weights,biases + //#pragma HLS ARRAY_RESHAPE variable=weights block factor=block_factor + //#pragma HLS ARRAY_RESHAPE variable=data block factor=block_factor + + #pragma HLS ARRAY_PARTITION variable=biases complete + + typename CONFIG_T::accum_t acc[nout]; + #pragma HLS ARRAY_PARTITION variable=acc complete + +InitAccum: + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < nout; iacc++) { + //#pragma HLS UNROLL + acc[iacc] = (typename CONFIG_T::accum_t)biases[iacc]; + } + + const int remainder = CONFIG_T::reuse_factor % nout; + + int outidx[rufactor]; + int outstep = 0; +IndexLoop: + for (int ir = 0; ir < rufactor; ir++) { + outidx[ir] = outstep; + outstep++; + if (outstep == nout) { + outstep = 0; + } + } + +ReuseLoop: + for (int ir = 0; ir < rufactor; ir++) { + //#pragma HLS PIPELINE II=1 rewind + + int in_index = ir; + int out_index = outidx[ir]; + + MultLoop: + #pragma clang loop unroll(full) + for (int im = 0; im < block_factor; im++) { + //#pragma HLS UNROLL + + // out_index = in_index % nout; + acc[out_index] += static_cast( + CONFIG_T::template product::product(data[in_index], weights[in_index])); + + in_index += rufactor; + out_index += remainder; + if (out_index >= nout) { + out_index -= nout; + } + } + } + +// Cast to "res_t" type +Result: + #pragma clang loop unroll(full) + for (int ires = 0; ires < nout; ires++) { + //#pragma HLS UNROLL + res[ires] = cast(acc[ires]); + } +} + +template +class DepthwiseDenseLatency : public DepthwiseDenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + #pragma HLS inline + depthwise_product_latency(data, res, weights, biases); + } +}; + +template +class DepthwiseDenseResource_rf_leq_nout : public DepthwiseDenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + #pragma HLS inline + depthwise_product_resource_rf_leq_nout(data, res, weights, biases); + } +}; + +template +class DepthwiseDenseResource_rf_gt_nout_rem0 : public DepthwiseDenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + #pragma HLS inline + depthwise_product_resource_rf_gt_nout_rem0(data, res, weights, biases); + } +}; + +template +class DepthwiseDenseResource_rf_gt_nout : public DepthwiseDenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + #pragma HLS inline + depthwise_product_resource_gt_nout(data, res, weights, biases); + } +}; + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_einsum.h b/hls4ml/templates/bambu/nnet_utils/nnet_einsum.h new file mode 100644 index 0000000000..e90e7cc533 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_einsum.h @@ -0,0 +1,86 @@ +#ifndef NNET_EINSUM_H_ +#define NNET_EINSUM_H_ + +#include "nnet_common.h" +#include "nnet_mult.h" +#include "nnet_transpose.h" + +namespace nnet { + +struct config_einsum { + typedef void tpose_inp0_config; + typedef void tpose_inp1_config; + typedef void tpose_out_conf; + + // Layer Sizes + static const unsigned n_free0; + static const unsigned n_free1; + static const unsigned n_contract; + static const unsigned n_inplace; + + // Resource reuse info + static const unsigned io_type; + static const unsigned strategy; + static const unsigned reuse_factor; + static const unsigned multiplier_limit; + + template using product = nnet::product::mult; +}; + +template +void einsum(const data0_T data0[CONFIG_T::tpose_inp0_config::N], const data1_T data1[CONFIG_T::tpose_inp1_config::N], + res_T res[CONFIG_T::tpose_out_conf::N]) { + + //#pragma HLS PIPELINE II = CONFIG_T::reuse_factor + //#pragma HLS ALLOCATION operation instances = mul limit = CONFIG_T::multiplier_limit + + data0_T tpose_i0[CONFIG_T::tpose_inp0_config::N]; + data1_T tpose_i1[CONFIG_T::tpose_inp1_config::N]; + res_T tpose_o[CONFIG_T::tpose_out_conf::N]; + + #pragma HLS ARRAY_PARTITION variable = tpose_i0 complete + #pragma HLS ARRAY_PARTITION variable = tpose_i1 complete + #pragma HLS ARRAY_PARTITION variable = tpose_o complete + + nnet::transpose(data0, tpose_i0); + nnet::transpose(data1, tpose_i1); + + // for l0 in range(L0): + // for i in range(I): + // output[(i*L0+l0)*L1:(i*L0+l0+1)*L1] = input1[i*L1*C:(i+1)*L1*C].reshape((L1,C)) @ + // input0[(i*L0+l0)*C:(i*L0+l0+1)*C] + + constexpr unsigned L0 = CONFIG_T::n_free0; + constexpr unsigned L1 = CONFIG_T::n_free1; + constexpr unsigned C = CONFIG_T::n_contract; + constexpr unsigned I = CONFIG_T::n_inplace; + + typename CONFIG_T::accum_t accum_buf; + #pragma clang loop unroll(full) + for (unsigned i = 0; i < I; i++) { + //#pragma HLS UNROLL + #pragma clang loop unroll(full) + for (unsigned l0 = 0; l0 < L0; l0++) { + //#pragma HLS UNROLL + #pragma clang loop unroll(full) + for (unsigned l1 = 0; l1 < L1; l1++) { + //#pragma HLS UNROLL + accum_buf = 0; + #pragma clang loop unroll(full) + for (unsigned c = 0; c < C; c++) { + //#pragma HLS UNROLL + data0_T a = tpose_i0[(i * L0 + l0) * C + c]; + data1_T b = tpose_i1[i * L1 * C + l1 * C + c]; + accum_buf += CONFIG_T::template product::product(a, b); + } + tpose_o[(i * L0 + l0) * L1 + l1] = accum_buf; + } + } + } + + nnet::transpose(tpose_o, res); +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_einsum_dense.h b/hls4ml/templates/bambu/nnet_utils/nnet_einsum_dense.h new file mode 100644 index 0000000000..1df4336b99 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_einsum_dense.h @@ -0,0 +1,114 @@ +#ifndef NNET_EINSUM_DENSE_H_ +#define NNET_EINSUM_DENSE_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_dense.h" +#include "nnet_dense_latency.h" +#include "nnet_dense_resource.h" +#include "nnet_function_stubs.h" +#include "nnet_helpers.h" +#include "nnet_mult.h" +#include "nnet_transpose.h" + +namespace nnet { + +struct einsum_dense_config { + // Internal data type definitions + + typedef void tpose_inp_conf; + typedef void tpose_out_conf; + typedef void dense_conf; + + // Layer Sizes + static const unsigned n_free_data = 1; + static const unsigned n_free_kernel = 1; + static const unsigned n_contract = 1; + static const unsigned n_inplace = 1; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned strategy = latency; + static const unsigned reuse_factor = 1; + static const unsigned parallelization_factor = 1000; // Only useful when n_inplace > 1 + + // Product function to use + template using product = nnet::product::mult; +}; + +template +void einsum_dense( + data_T data[CONFIG_T::n_free_data * CONFIG_T::n_contract * CONFIG_T::n_inplace], + res_T res[CONFIG_T::n_free_data * CONFIG_T::n_free_kernel * CONFIG_T::n_inplace], + typename CONFIG_T::dense_conf::weight_t weights[CONFIG_T::n_free_kernel * CONFIG_T::n_contract * CONFIG_T::n_inplace], + typename CONFIG_T::dense_conf::bias_t biases[CONFIG_T::n_free_data * CONFIG_T::n_free_kernel * CONFIG_T::n_inplace]) { + data_T inp_tpose[CONFIG_T::n_free_data * CONFIG_T::n_contract * CONFIG_T::n_inplace]; + res_T out_tpose[CONFIG_T::n_free_data * CONFIG_T::n_free_kernel * CONFIG_T::n_inplace]; + res_T out_buffer[CONFIG_T::n_free_kernel]; + #pragma HLS ARRAY_PARTITION variable = inp_tpose complete + #pragma HLS ARRAY_PARTITION variable = out_tpose complete + + nnet::transpose(data, inp_tpose); + + constexpr unsigned L0 = CONFIG_T::n_free_data; + constexpr unsigned L1 = CONFIG_T::n_free_kernel; + constexpr unsigned C = CONFIG_T::n_contract; + constexpr unsigned I = CONFIG_T::n_inplace; + + #pragma clang loop unroll_count(CONFIG_T::parallelization_factor) + for (unsigned l0 = 0; l0 < L0; l0++) { + //#pragma HLS UNROLL factor = CONFIG_T::parallelization_factor + #pragma clang loop unroll(full) + for (unsigned i = 0; i < I; i++) { + //#pragma HLS UNROLL + // even w/o explicit distributed arithmetic optimization, latency kernels are partially implemented as such + // so reusing the same multiplier for different weights doesn't really help... only full unrolling for now + dense(&inp_tpose[(i * L0 + l0) * C], out_buffer, + &weights[(i * L1 * C)], &biases[((i * L0 + l0) * L1)]); + #pragma clang loop unroll(full) + for (unsigned j = 0; j < L1; j++) { + //#pragma HLS UNROLL + out_tpose[(i * L0 + l0) * L1 + j] = out_buffer[j]; + } + } + } + + nnet::transpose(out_tpose, res); +} + +template +typename std::enable_if::type +einsum_dense(data_T data[CONFIG_T::n_free_data * CONFIG_T::n_contract * CONFIG_T::n_inplace], + res_T res[CONFIG_T::n_free_data * CONFIG_T::n_free_kernel * CONFIG_T::n_inplace], + typename CONFIG_T::bias_t biases[CONFIG_T::n_free_data * CONFIG_T::n_free_kernel * CONFIG_T::n_inplace]) { + + data_T inp_tpose[CONFIG_T::n_free_data * CONFIG_T::n_contract * CONFIG_T::n_inplace]; + typename CONFIG_T::accum_t out_tpose[CONFIG_T::n_free_data * CONFIG_T::n_free_kernel * CONFIG_T::n_inplace]; + + #pragma HLS ARRAY_PARTITION variable = inp_tpose complete + #pragma HLS ARRAY_PARTITION variable = out_tpose complete + + nnet::transpose(data, inp_tpose); + + constexpr unsigned L0 = CONFIG_T::n_free_data; + constexpr unsigned L1 = CONFIG_T::n_free_kernel; + constexpr unsigned C = CONFIG_T::n_contract; + constexpr unsigned I = CONFIG_T::n_inplace; + + #pragma clang loop unroll(full) + for (unsigned l0 = 0; l0 < L0; l0++) { + //#pragma HLS UNROLL factor = CONFIG_T::parallelization_factor + CONFIG_T::da_kernel(inp_tpose, out_tpose, l0); + } + #pragma clang loop unroll(full) + for (unsigned ii = 0; ii < (L0 * L1 * I); ii++) { + //#pragma HLS UNROLL + out_tpose[ii] = out_tpose[ii] + biases[ii]; + } + + nnet::transpose(out_tpose, res); +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_embed.h b/hls4ml/templates/bambu/nnet_utils/nnet_embed.h new file mode 100644 index 0000000000..1153ac138d --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_embed.h @@ -0,0 +1,47 @@ +#ifndef NNET_EMBED_H_ +#define NNET_EMBED_H_ + +#include "nnet_common.h" +#include "nnet_helpers.h" + +namespace nnet { + +struct embed_config { + // Internal data type definitions + typedef float embeddings_t; + + // Layer Sizes + static const unsigned n_in = 10; + static const unsigned n_out = 16; + static const unsigned vocab_size = 50; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; +}; + +template +void embedding(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in * CONFIG_T::n_out], + typename CONFIG_T::embeddings_t embeddings[CONFIG_T::vocab_size * CONFIG_T::n_out]) { + + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + // This can save a few cycles, but it will create a large multiplexer due to + // non-constant access pattern, so let's leave it out + #pragma HLS ARRAY_PARTITION variable=embeddings complete + +InputSequence: + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_in; j++) { + //#pragma HLS UNROLL + DenseEmbedding: + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::n_out; i++) { + //#pragma HLS UNROLL + res[j * CONFIG_T::n_out + i] = embeddings[data[j] * CONFIG_T::n_out + i]; + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_embed_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_embed_stream.h new file mode 100644 index 0000000000..f3c55d79fa --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_embed_stream.h @@ -0,0 +1,34 @@ +#ifndef NNET_EMBED_STREAM_H_ +#define NNET_EMBED_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_helpers.h" + +namespace nnet { + +template +void embedding(hls::stream &data, hls::stream &res, + typename CONFIG_T::embeddings_t embeddings[CONFIG_T::vocab_size * CONFIG_T::n_out]) { + data_T in_data = data.read(); + +InputSequence: + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + + DenseEmbedding: + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::n_out; i++) { + //#pragma HLS UNROLL + res_pack[i] = embeddings[in_data[j] * CONFIG_T::n_out + i]; + } + res.write(res_pack); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_function_stubs.h b/hls4ml/templates/bambu/nnet_utils/nnet_function_stubs.h new file mode 100644 index 0000000000..7b4fa75bc3 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_function_stubs.h @@ -0,0 +1,89 @@ +#ifndef NNET_FUNCTION_STUBS_H_ +#define NNET_FUNCTION_STUBS_H_ + +#include "nnet_helpers.h" + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_mult.h" + +namespace nnet { + +template class FillConv1DBuffer { + public: + static void fill_buffer(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + data_T buffer[CONFIG_T::n_pixels][CONFIG_T::filt_width * CONFIG_T::n_chan], + const unsigned partition) { + // To be implemented in subclasses + } +}; + +template class FillConv2DBuffer { + public: + static void + fill_buffer(data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + data_T buffer[CONFIG_T::n_pixels][CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan], + const unsigned partition) { + // To be implemented in subclasses + } +}; + +template class DenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + // To be implemented in subclasses + } +}; + +template class DepthwiseDenseKernel { + public: + static void dense(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_out], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_in * CONFIG_T::n_out], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_out]) { + // To be implemented in subclasses + } +}; + +template class Conv1DKernel { + public: + static void conv(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + const typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + const typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + // To be implemented in subclasses + } +}; + +template ap_fixed bit_shift(ap_fixed x) { + #pragma HLS inline + ap_fixed r; + r.range() = x.range(); + return r; +}; + +template ap_ufixed bit_shift(ap_ufixed x) { + #pragma HLS inline + ap_ufixed r; + r.range() = x.range(); + return r; +}; + +template ap_fixed bit_shift(ap_int x) { + #pragma HLS inline + ap_fixed r; + r.range() = x.range(); + return r; +}; + +template ap_ufixed bit_shift(ap_uint x) { + #pragma HLS inline + ap_ufixed r; + r.range() = x.range(); + return r; +}; +// hls4ml insert code + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_garnet.h b/hls4ml/templates/bambu/nnet_utils/nnet_garnet.h new file mode 100644 index 0000000000..79f6cd9265 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_garnet.h @@ -0,0 +1,826 @@ +#ifndef NNET_GARNET_H_ +#define NNET_GARNET_H_ + +#include "hls_math.h" +#include "hls_stream.h" +#include "nnet_common.h" + +namespace nnet { +namespace garnet_utils { + +template +inline typename std::enable_if::value>::type +initialize_edge_weights_table(typename CONFIG_T::edge_weight_t edge_weights_table[]) { + typedef ap_uint index_t; + + unsigned const table_size = (1 << CONFIG_T::distance_width); + + index_t index; + typename CONFIG_T::distance_t distance; + + // edge_weight_t is ap_ufixed with 0 iwidth -> let index 0 be a saturated version of 1 + edge_weights_table[0] = ap_ufixed(1.); + + for (unsigned iw = 1; iw < table_size; ++iw) { + index = iw; + distance.range(CONFIG_T::distance_width - 1, 0) = index.range(CONFIG_T::distance_width - 1, 0); + edge_weights_table[iw] = hls::exp(-distance * distance); + } +} + +template +inline typename std::enable_if::value>::type +initialize_edge_weights_table(typename CONFIG_T::edge_weight_t edge_weights_table[]) { + unsigned const table_size = (1 << CONFIG_T::distance_width); + double const step = 64. / table_size; + + typename CONFIG_T::distance_t v = -32.; + for (unsigned iw = 0; iw < table_size; ++iw) { + edge_weights_table[iw] = std::exp(-v * v); + v += step; + } +} + +template +inline typename std::enable_if::value, typename CONFIG_T::edge_weight_t>::type +get_edge_weight(typename CONFIG_T::distance_t distance, typename CONFIG_T::edge_weight_t edge_weights_table[]) { + typedef ap_uint index_t; + + index_t index(distance.range(CONFIG_T::distance_width - 1, 0)); + + return edge_weights_table[index]; +} + +template +inline + typename std::enable_if::value, typename CONFIG_T::edge_weight_t>::type + get_edge_weight(typename CONFIG_T::distance_t distance, typename CONFIG_T::edge_weight_t edge_weights_table[]) { + unsigned const table_size = (1 << CONFIG_T::distance_width); + double const step = 64. / table_size; + + int index = (distance + 32.) / step; + if (index < 0) + index = 0; + else if (index >= table_size) + index = table_size - 1; + + return edge_weights_table[index]; +} + +template typename CONFIG_T::edge_weight_t compute_edge_weight(typename CONFIG_T::distance_t distance) { + if (CONFIG_T::is_stack) { + //#pragma HLS INLINE OFF + } +#ifdef __SYNTHESIS__ + typename CONFIG_T::edge_weight_t edge_weights_table[1 << CONFIG_T::distance_width]; + // unsigned const reshape_factor = CONFIG_T::n_aggregators * CONFIG_T::n_in_features * (CONFIG_T::n_vertices / + // CONFIG_T::reuse_factor); + // #pragma HLS ARRAY_RESHAPE variable=edge_weights_table cyclic factor=reshape_factor dim=1 + bool initialized = false; +#else + static typename CONFIG_T::edge_weight_t edge_weights_table[1 << CONFIG_T::distance_width]; + static bool initialized = false; +#endif + if (not initialized) { + initialize_edge_weights_table(edge_weights_table); + initialized = true; + } + + return get_edge_weight(distance, edge_weights_table); +} + +template +inline typename std::enable_if::value, dividend_T>::type normalize_log2(dividend_T dividend, + exponent_T exponent) { + #pragma HLS inline + return dividend >> exponent; +} + +template +inline typename std::enable_if::value, dividend_T>::type normalize_log2(dividend_T dividend, + exponent_T exponent) { + #pragma HLS inline + return dividend / std::pow(2., exponent); +} + +template struct Means { + typedef E edge_weight_t; + + edge_weight_t edge_weight_mean[CONFIG_T::n_aggregators]; + typename CONFIG_T::aggr_t weighted_feature_mean[CONFIG_T::n_aggregators * CONFIG_T::n_in_features]; + + Means() { + #pragma HLS inline + #pragma HLS ARRAY_PARTITION variable=edge_weight_mean complete + #pragma HLS ARRAY_PARTITION variable=weighted_feature_mean complete + //#pragma HLS UNROLL region + + Aggregators: + #pragma clang loop unroll(full) + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + edge_weight_mean[ia] = 0.; + + InFeatures: + #pragma clang loop unroll(full) + for (unsigned ix = 0; ix < CONFIG_T::n_in_features; ++ix) { + unsigned const iax = ia * CONFIG_T::n_in_features + ix; + weighted_feature_mean[iax] = 0.; + } + } + } + + void set_weight(unsigned, edge_weight_t const &) { + #pragma HLS inline + } + + void add_means_normalized(Means const &local) { + #pragma HLS inline + // Always called within a pipelined region - no UNROLL needed + + unsigned const log2_unroll_factor = CONFIG_T::n_vertices_width - CONFIG_T::log2_reuse_factor; + + Aggregators: + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + edge_weight_mean[ia] += normalize_log2(local.edge_weight_mean[ia], log2_unroll_factor); + + InFeatures: + for (unsigned ix = 0; ix < CONFIG_T::n_in_features; ++ix) { + unsigned const iax = ia * CONFIG_T::n_in_features + ix; + weighted_feature_mean[iax] += normalize_log2(local.weighted_feature_mean[iax], log2_unroll_factor); + } + } + } + + template + typename std::enable_if::type set_means_normalized(nvtx_T const nvtx, arrays_T const &accum) { + #pragma HLS inline + //#pragma HLS UNROLL region + + // accum comes divided by unroll factor + typename T::norm_t nvtx_norm = (T::n_vertices / T::reuse_factor) / nvtx; + + Aggregators: + #pragma clang loop unroll(full) + for (unsigned ia = 0; ia < T::n_aggregators; ++ia) { + edge_weight_mean[ia] = accum.edge_weight_mean[ia] * nvtx_norm; + + InFeatures: + #pragma clang loop unroll(full) + for (unsigned ix = 0; ix < T::n_in_features; ++ix) { + unsigned const iax = ia * T::n_in_features + ix; + + weighted_feature_mean[iax] = accum.weighted_feature_mean[iax] * nvtx_norm; + } + } + } + + template + typename std::enable_if::type set_means_normalized(nvtx_T const nvtx, arrays_T const &accum) { + #pragma HLS inline + //#pragma HLS UNROLL region + + Aggregators: + #pragma clang loop unroll(full) + for (unsigned ia = 0; ia < T::n_aggregators; ++ia) { + + edge_weight_mean[ia] = normalize_log2(accum.edge_weight_mean[ia], T::log2_reuse_factor); + + InFeatures: + #pragma clang loop unroll(full) + for (unsigned ix = 0; ix < T::n_in_features; ++ix) { + unsigned const iax = ia * T::n_in_features + ix; + + weighted_feature_mean[iax] = normalize_log2(accum.weighted_feature_mean[iax], T::log2_reuse_factor); + } + } + } +}; + +template struct WeightsAndMeans : public Means { + typedef E edge_weight_t; + + edge_weight_t edge_weights[CONFIG_T::n_vertices * CONFIG_T::n_aggregators]; + + WeightsAndMeans() : Means() { + #pragma HLS inline + unsigned const reshape_factor = CONFIG_T::n_aggregators * (CONFIG_T::n_vertices / CONFIG_T::reuse_factor); + #pragma HLS ARRAY_PARTITION variable=edge_weights cyclic factor=reshape_factor + } + + void set_weight(unsigned iva, edge_weight_t const &weight) { + #pragma HLS inline + edge_weights[iva] = weight; + } +}; + +template struct OutputBiasNormalizer; + +template +struct OutputBiasNormalizer::type> { + typedef typename CONFIG_T::output_transform_biases_t biases_t; + + biases_t const (&output_biases)[CONFIG_T::n_out_features]; + + OutputBiasNormalizer(nvtx_T const) : output_biases{CONFIG_T::output_transform_biases} { + #pragma HLS inline + } +}; + +template +struct OutputBiasNormalizer::type> { + typedef typename CONFIG_T::output_transform_biases_t biases_t; + + biases_t output_biases[CONFIG_T::n_out_features]; + + OutputBiasNormalizer(nvtx_T const nvtx) { + #pragma HLS ARRAY_PARTITION variable=output_biases complete + //#pragma HLS UNROLL region + + // Cannot add a loop label here due to a Vivado HLS bug, apparently + #pragma clang loop unroll(full) + for (unsigned io = 0; io < CONFIG_T::n_out_features; ++io) { + typename CONFIG_T::aggr_t bias = CONFIG_T::output_transform_biases[io]; + bias *= nvtx; + output_biases[io] = normalize_log2(bias, CONFIG_T::n_vertices_width); + } + } +}; + +template struct InputDataGetter { + typedef data_T data_t; + + data_T const *dataref; + + InputDataGetter(data_T const *d) : dataref{d} { + #pragma HLS inline + } + data_T const &get(unsigned iv, unsigned ix) const { + #pragma HLS inline + unsigned const ivx = iv * CONFIG_T::n_in_features + ix; + return dataref[ivx]; + } +}; + +template struct SingleVertexDataGetter { + typedef data_T data_t; + + data_T const (&dataref)[CONFIG_T::n_in_features]; + + SingleVertexDataGetter(data_T const (&d)[CONFIG_T::n_in_features]) : dataref{d} { + #pragma HLS inline + } + data_T const &get(unsigned, unsigned ix) const { + #pragma HLS inline + return dataref[ix]; + } +}; + +template struct OutputResSetter { + typedef res_T res_t; + + res_T *resref; + + OutputResSetter(res_T *r) : resref{r} { + #pragma HLS inline + } + void set(unsigned iv, unsigned io, res_T const &acc) { + #pragma HLS inline + unsigned const ivo = iv * CONFIG_T::n_out_features + io; + resref[ivo] = acc; + } +}; + +template struct SingleVertexResSetter { + typedef res_T res_t; + + res_T (&resref)[CONFIG_T::n_out_features]; + + SingleVertexResSetter(res_T (&r)[CONFIG_T::n_out_features]) : resref{r} { + #pragma HLS inline + } + void set(unsigned, unsigned io, res_T const &acc) { + #pragma HLS inline + resref[io] = acc; + } +}; + +template +inline void compute_weights_aggregates(data_getter_T const &data_getter, unsigned iv, arrays_local_T &arrays_local, + arrays_T &arrays) { + #pragma HLS inline + +Aggregators: + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + typename CONFIG_T::distance_t distance = CONFIG_T::aggregator_distance_biases[ia]; + + InFeatures1: + for (unsigned ix = 0; ix < CONFIG_T::n_in_features; ++ix) { + unsigned const iax = ia * CONFIG_T::n_in_features + ix; + + typename CONFIG_T::distance_t incr = data_getter.get(iv, ix) * CONFIG_T::aggregator_distance_weights[iax]; + + distance += incr; + } + + typename CONFIG_T::edge_weight_t edge_weight = + garnet_utils::compute_edge_weight(distance); + + arrays_local.edge_weight_mean[ia] += edge_weight; + + InFeatures2: + for (unsigned ix = 0; ix < CONFIG_T::n_in_features; ++ix) { + unsigned const iax = ia * CONFIG_T::n_in_features + ix; + + typename data_getter_T::data_t incr = data_getter.get(iv, ix) * edge_weight; + + arrays_local.weighted_feature_mean[iax] += incr; + } + + unsigned const iva = iv * CONFIG_T::n_aggregators + ia; + arrays.set_weight(iva, edge_weight); + } +} + +template +inline typename CONFIG_T::aggr_t compute_output_base_core(arrays_T const &arrays, unsigned io, unsigned ia) { + #pragma HLS inline + //#pragma HLS UNROLL region + + unsigned const ioa = io * CONFIG_T::n_aggregators + ia; + typename CONFIG_T::aggr_t aggr = arrays.edge_weight_mean[ia] * CONFIG_T::input_transform_biases[ioa]; + +InFeatures: + #pragma clang loop unroll(full) + for (unsigned ix = 0; ix < CONFIG_T::n_in_features; ++ix) { + unsigned const ioax = ioa * CONFIG_T::n_in_features + ix; + unsigned const iax = ia * CONFIG_T::n_in_features + ix; + + aggr += arrays.weighted_feature_mean[iax] * CONFIG_T::input_transform_weights[ioax]; + } + + return aggr; +} + +template +inline void compute_output_base(arrays_T const &arrays, + typename CONFIG_T::aggr_t output_base[CONFIG_T::n_out_features * CONFIG_T::n_aggregators]) { + #pragma HLS inline + //#pragma HLS UNROLL region + +OutFeatures: + #pragma clang loop unroll(full) + for (unsigned io = 0; io < CONFIG_T::n_out_features; ++io) { + Aggregators: + #pragma clang loop unroll(full) + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + unsigned const ioa = io * CONFIG_T::n_aggregators + ia; + + output_base[ioa] = compute_output_base_core(arrays, io, ia); + } + } +} + +template +inline void +compute_vertex_output(arrays_T const &arrays, unsigned iv, + typename CONFIG_T::aggr_t const output_base[CONFIG_T::n_out_features * CONFIG_T::n_aggregators], + res_setter_T &res_setter) { + #pragma HLS inline + + typename arrays_T::edge_weight_t edge_weights[CONFIG_T::n_aggregators]; + #pragma HLS ARRAY_PARTITION variable=edge_weights complete + +Aggregators1: + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + unsigned const iva = iv * CONFIG_T::n_aggregators + ia; + + edge_weights[ia] = arrays.edge_weights[iva]; + } + +OutFeatures: + for (unsigned io = 0; io < CONFIG_T::n_out_features; ++io) { + typename res_setter_T::res_t acc = CONFIG_T::output_transform_biases[io]; + + Aggregators2: + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + unsigned const ioa = io * CONFIG_T::n_aggregators + ia; + + typename res_setter_T::res_t incr = edge_weights[ia] * output_base[ioa]; + acc += incr; + } + + res_setter.set(iv, io, acc); + } +} + +template +void aggregate(data_T const data[CONFIG_T::n_vertices * CONFIG_T::n_in_features], nvtx_T const nvtx, arrays_T &arrays) { + InputDataGetter data_getter(data); + + unsigned const unroll_factor = CONFIG_T::n_vertices >> CONFIG_T::log2_reuse_factor; + + Means means_accum; + +VerticesOuter: + for (unsigned ivv = 0; ivv < CONFIG_T::reuse_factor; ++ivv) { + //#pragma HLS PIPELINE + + if (ivv * unroll_factor >= nvtx) + break; + + Means means_local; + + VerticesInner: + for (unsigned ir = 0; ir < unroll_factor; ++ir) { + unsigned iv = ivv * unroll_factor + ir; + + if (iv == nvtx) + break; + + compute_weights_aggregates(data_getter, iv, means_local, arrays); + } + + means_accum.add_means_normalized(means_local); + } + + arrays.set_means_normalized(nvtx, means_accum); +} + +template +void distribute(nvtx_T const nvtx, arrays_T const &arrays, res_T res[CONFIG_T::n_vertices * CONFIG_T::n_out_features]) { + OutputResSetter res_setter(res); + + typename CONFIG_T::aggr_t output_base[CONFIG_T::n_out_features * CONFIG_T::n_aggregators]; + #pragma HLS ARRAY_PARTITION variable=output_base complete + + compute_output_base(arrays, output_base); + + unsigned const unroll_factor = CONFIG_T::n_vertices >> CONFIG_T::log2_reuse_factor; + +VerticesOuter: + for (unsigned ivv = 0; ivv < CONFIG_T::reuse_factor; ++ivv) { + //#pragma HLS PIPELINE + + if (ivv * unroll_factor >= nvtx) + break; + + VerticesInner: + for (unsigned ir = 0; ir < unroll_factor; ++ir) { + unsigned iv = ivv * unroll_factor + ir; + + if (iv == nvtx) + break; + + compute_vertex_output(arrays, iv, output_base, res_setter); + } + } +} + +template +void set_output(output_biases_T const &output_transform_biases, arrays_T const &arrays, + res_T res[CONFIG_T::n_out_features]) { + //#pragma HLS PIPELINE + +OutFeatures: + for (unsigned io = 0; io < CONFIG_T::n_out_features; ++io) { + res_T acc = output_transform_biases.output_biases[io]; + + Aggregators: + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + typename CONFIG_T::aggr_t aggr = compute_output_base_core(arrays, io, ia); + + acc += arrays.edge_weight_mean[ia] * aggr; + } + + res[io] = acc; + } +} + +template +void distribute_aggregate(nvtx_T const nvtx, prev_arrays_T const &prev_arrays, current_arrays_T ¤t_arrays) { + typedef typename prev_layer_t::output_t data_T; + + typename prev_layer_t::aggr_t prev_output_base[prev_layer_t::n_out_features * prev_layer_t::n_aggregators]; + #pragma HLS ARRAY_PARTITION variable=prev_output_base complete + + compute_output_base(prev_arrays, prev_output_base); + + unsigned const unroll_factor = current_layer_t::n_vertices >> current_layer_t::log2_reuse_factor; + + Means means_accum; + +VerticesOuter: + for (unsigned ivv = 0; ivv < current_layer_t::reuse_factor; ++ivv) { + //#pragma HLS PIPELINE + + if (ivv * unroll_factor >= nvtx) + break; + + Means means_local; + + VerticesInner: + for (unsigned ir = 0; ir < unroll_factor; ++ir) { + unsigned iv = ivv * unroll_factor + ir; + + if (iv == nvtx) + break; + + data_T data[prev_layer_t::n_out_features]; + #pragma HLS ARRAY_PARTITION variable=data complete + + SingleVertexResSetter res_setter(data); + + compute_vertex_output(prev_arrays, iv, prev_output_base, res_setter); + + SingleVertexDataGetter data_getter(data); + + compute_weights_aggregates(data_getter, iv, means_local, current_arrays); + } + + means_accum.add_means_normalized(means_local); + } + + current_arrays.set_means_normalized(nvtx, means_accum); +} + +template +inline typename std::enable_if::value>::type +sublayer(nvtx_T const nvtx, prev_arrays_T const &prev_arrays, last_arrays_T &last_arrays) { + #pragma HLS inline + + distribute_aggregate(nvtx, prev_arrays, last_arrays); +} + +template +inline typename std::enable_if::value>::type +sublayer(nvtx_T const nvtx, prev_arrays_T const &prev_arrays, last_arrays_T &last_arrays) { + #pragma HLS inline + + WeightsAndMeans current_arrays; + + distribute_aggregate(nvtx, prev_arrays, current_arrays); + + sublayer(nvtx, current_arrays, last_arrays); +} +} // namespace garnet_utils + +struct garnet_config { + // Layer specs + static const unsigned n_vertices_width = 8; + static const unsigned n_vertices = (1 << n_vertices_width); + static const unsigned n_in_features = 4; + static const unsigned n_propagate = 4; + static const unsigned n_aggregators = 4; + static const unsigned n_out_features = 4; + static const unsigned distance_width = 12; + + // Internal data type definitions + typedef float input_transform_weights_t; + typedef float input_transform_biases_t; + typedef float output_transform_weights_t; + typedef float output_transform_biases_t; + typedef float aggregator_distance_weights_t; + typedef float aggregator_distance_biases_t; + + typedef float norm_t; + typedef float distance_t; + typedef float edge_weight_t; + typedef float edge_weight_aggr_t; + typedef float aggr_t; + typedef float output_t; + + /* static const input_transform_weights_t (&input_transform_weights)[n_out_features * n_aggregators * n_in_features]; */ + /* static const input_transform_biases_t (&input_transform_biases)[n_out_features * n_aggregators]; */ + /* static const aggregator_distance_weights_t (&aggregator_distance_weights)[n_aggregators * n_in_features]; */ + /* static const aggregator_distance_biases_t (&aggregator_distance_biases)[n_aggregators]; */ + /* static const output_transform_biases_t (&output_transform_biases)[n_out_features]; */ + + enum OutputCollapse { no_collapse, collapse_mean, collapse_max }; + + static const unsigned output_collapse = no_collapse; + + static const bool mean_by_nvert = false; + static const bool is_stack = false; + + // Optimization specs + static const unsigned reuse_factor = 64; + static const unsigned log2_reuse_factor = 6; +}; + +// vertices -> vertices +template +typename std::enable_if::type +garnet(data_T const data[CONFIG_T::n_vertices * CONFIG_T::n_in_features], nvtx_T const nvtx[1], + res_T res[CONFIG_T::n_vertices * CONFIG_T::n_out_features]) { + //#pragma HLS DATAFLOW + + garnet_utils::WeightsAndMeans arrays; + + garnet_utils::aggregate(data, nvtx[0], arrays); + + garnet_utils::distribute(nvtx[0], arrays, res); +} + +// vertices -> out features +template +typename std::enable_if::type +garnet(data_T const data[CONFIG_T::n_vertices * CONFIG_T::n_in_features], nvtx_T const nvtx[1], + res_T res[CONFIG_T::n_out_features]) { + //#pragma HLS DATAFLOW + + garnet_utils::Means arrays; + + garnet_utils::aggregate(data, nvtx[0], arrays); + + garnet_utils::OutputBiasNormalizer normalize_bias(nvtx[0]); + + garnet_utils::set_output(normalize_bias, arrays, res); +} + +// vertices -> vertices +template +typename std::enable_if::type +garnet_stack(data_T const data[CONFIG_T::n_vertices * CONFIG_T::n_in_features], nvtx_T const nvtx[1], + res_T res[CONFIG_T::n_vertices * CONFIG_T::n_out_features]) { + //#pragma HLS DATAFLOW + + typedef typename CONFIG_T::template sublayer_t<0> first_layer_t; + unsigned const ilast = CONFIG_T::n_sublayers - 1; + typedef typename CONFIG_T::template sublayer_t last_layer_t; + + garnet_utils::WeightsAndMeans arrays_first; + garnet_utils::Means arrays_last; + + garnet_utils::aggregate(data, nvtx[0], arrays_first); + + garnet_utils::sublayer(nvtx[0], arrays_first, + arrays_last); + + garnet_utils::distribute(nvtx[0], arrays_last, res); +} + +// vertices -> out features +template +typename std::enable_if::type +garnet_stack(data_T const data[CONFIG_T::n_vertices * CONFIG_T::n_in_features], nvtx_T const nvtx[1], + res_T res[CONFIG_T::n_out_features]) { + //#pragma HLS DATAFLOW + + typedef typename CONFIG_T::template sublayer_t<0> first_layer_t; + unsigned const ilast = CONFIG_T::n_sublayers - 1; + typedef typename CONFIG_T::template sublayer_t last_layer_t; + + garnet_utils::WeightsAndMeans arrays_first; + garnet_utils::Means arrays_last; + + garnet_utils::aggregate(data, nvtx[0], arrays_first); + + garnet_utils::sublayer(nvtx[0], arrays_first, + arrays_last); + + garnet_utils::OutputBiasNormalizer normalize_bias(nvtx[0]); + + garnet_utils::set_output(normalize_bias, arrays_last, res); +} + +/* Reference (dumb) implementation returning (Vertices, Features) */ +template +typename std::enable_if::type +garnet_ref(data_T const data[CONFIG_T::n_vertices * CONFIG_T::n_in_features], nvtx_T const nvtx[1], + res_T res[CONFIG_T::n_vertices * CONFIG_T::n_out_features]) { + typename CONFIG_T::edge_weight_t edge_weights[CONFIG_T::n_vertices * CONFIG_T::n_aggregators]; + typename CONFIG_T::aggr_t propagated_features[CONFIG_T::n_vertices * CONFIG_T::n_propagate]; + + for (unsigned iv = 0; iv < CONFIG_T::n_vertices; ++iv) { + if (iv == nvtx[0]) + break; + + for (unsigned ip = 0; ip < CONFIG_T::n_propagate; ++ip) { + unsigned const ivp = iv * CONFIG_T::n_propagate + ip; + + propagated_features[ivp] = CONFIG_T::input_transform_biases[ip]; + + for (unsigned ix = 0; ix < CONFIG_T::n_in_features; ++ix) { + unsigned const ivx = iv * CONFIG_T::n_in_features + ix; + unsigned const ipx = ip * CONFIG_T::n_in_features + ix; + + propagated_features[ivp] += data[ivx] * CONFIG_T::input_transform_weights[ipx]; + } + } + + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + unsigned const iva = iv * CONFIG_T::n_aggregators + ia; + + typename CONFIG_T::aggr_t distance = CONFIG_T::aggregator_distance_biases[ia]; + + for (unsigned ix = 0; ix < CONFIG_T::n_in_features; ++ix) { + unsigned const ivx = iv * CONFIG_T::n_in_features + ix; + unsigned const iax = ia * CONFIG_T::n_in_features + ix; + + distance += data[ivx] * CONFIG_T::aggregator_distance_weights[iax]; + } + + edge_weights[iva] = garnet_utils::compute_edge_weight(distance); + } + } + + typename CONFIG_T::aggr_t aggregated_features[CONFIG_T::n_aggregators * CONFIG_T::n_propagate]; + + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + for (unsigned ip = 0; ip < CONFIG_T::n_propagate; ++ip) { + unsigned const iap = ia * CONFIG_T::n_propagate + ip; + + aggregated_features[iap] = 0.; + + for (unsigned iv = 0; iv < CONFIG_T::n_vertices; ++iv) { + if (iv == nvtx[0]) + break; + + unsigned const iva = iv * CONFIG_T::n_aggregators + ia; + unsigned const ivp = iv * CONFIG_T::n_propagate + ip; + + aggregated_features[iap] += edge_weights[iva] * propagated_features[ivp]; + } + } + } + + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + for (unsigned ip = 0; ip < CONFIG_T::n_propagate; ++ip) { + unsigned const iap = ia * CONFIG_T::n_propagate + ip; + + if (CONFIG_T::mean_by_nvert) + aggregated_features[iap] /= nvtx[0]; + else { + // Not using right shift in case aggr_t is float or double + aggregated_features[iap] /= CONFIG_T::n_vertices; + } + } + } + + for (unsigned iv = 0; iv < CONFIG_T::n_vertices; ++iv) { + if (iv == nvtx[0]) + break; + + for (unsigned io = 0; io < CONFIG_T::n_out_features; ++io) { + unsigned const ivo = iv * CONFIG_T::n_out_features + io; + + typename CONFIG_T::aggr_t acc = CONFIG_T::output_transform_biases[io]; + + for (unsigned ia = 0; ia < CONFIG_T::n_aggregators; ++ia) { + unsigned const iva = iv * CONFIG_T::n_aggregators + ia; + unsigned const ioa = io * CONFIG_T::n_aggregators + ia; + + typename CONFIG_T::aggr_t aggr = 0.; + + for (unsigned ip = 0; ip < CONFIG_T::n_propagate; ++ip) { + unsigned const iap = ia * CONFIG_T::n_propagate + ip; + unsigned const ioap = ioa * CONFIG_T::n_propagate + ip; + + aggr += CONFIG_T::output_transform_weights[ioap] * aggregated_features[iap]; + } + + acc += edge_weights[iva] * aggr; + } + + res[ivo] = acc; + } + } +} + +/* Reference (dumb) implementation returning (Features) - output averaged over vertices already */ +template +typename std::enable_if::type +garnet_ref(data_T const data[CONFIG_T::n_vertices * CONFIG_T::n_in_features], nvtx_T const nvtx[1], + res_T res[CONFIG_T::n_out_features]) { + typename CONFIG_T::aggr_t vertex_res[CONFIG_T::n_vertices * CONFIG_T::n_out_features]; + + garnet_ref(data, nvtx, vertex_res); + + for (unsigned io = 0; io < CONFIG_T::n_out_features; ++io) { + typename CONFIG_T::aggr_t acc = 0.; + + for (unsigned iv = 0; iv < CONFIG_T::n_vertices; ++iv) { + if (iv == nvtx[0]) + break; + + unsigned const ivo = iv * CONFIG_T::n_out_features + io; + + acc += vertex_res[ivo]; + } + + if (CONFIG_T::mean_by_nvert) + acc /= nvtx[0]; + else { + // Not using right shift in case aggr_t is float or double + acc /= CONFIG_T::n_vertices; + } + + res[io] = acc; + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_helpers.h b/hls4ml/templates/bambu/nnet_utils/nnet_helpers.h new file mode 100644 index 0000000000..fad9eb808f --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_helpers.h @@ -0,0 +1,384 @@ +#ifndef NNET_HELPERS_H +#define NNET_HELPERS_H + +#include "hls_stream.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nnet { + +#ifndef __BAMBU__ + +#ifndef WEIGHTS_DIR +#define WEIGHTS_DIR "weights" +#endif + +template void load_weights_from_txt(T *w, const char *fname) { + + std::string full_path = std::string(WEIGHTS_DIR) + "/" + std::string(fname); + std::ifstream infile(full_path.c_str(), std::ios::binary); + + if (infile.fail()) { + std::cerr << "ERROR: file " << std::string(full_path) << " does not exist" << std::endl; + exit(1); + } + + std::string line; + if (std::getline(infile, line)) { + std::istringstream iss(line); + std::string token; + + size_t i = 0; + while (std::getline(iss, token, ',')) { + std::istringstream(token) >> w[i]; + i++; + } + + if (SIZE != i) { + std::cerr << "ERROR: Expected " << SIZE << " values"; + std::cerr << " but read only " << i << " values" << std::endl; + } + } +} + +template void load_compressed_weights_from_txt(T *w, const char *fname) { + + std::string full_path = std::string(WEIGHTS_DIR) + "/" + std::string(fname); + std::ifstream infile(full_path.c_str(), std::ios::binary); + + if (infile.fail()) { + std::cerr << "ERROR: file " << std::string(fname) << " does not exist" << std::endl; + exit(1); + } + + std::string line; + if (std::getline(infile, line)) { + std::istringstream iss(line); + std::string token; + std::string extra_chars = "} "; + + size_t i = 0; + while (std::getline(iss, token, '{')) { + if (token.length() == 0) { + continue; + } + for (char c : extra_chars) { + token.erase(std::remove(token.begin(), token.end(), c), token.end()); + } + if (token.back() == ',') { + token.erase(token.end() - 1); + } + + std::replace(token.begin(), token.end(), ',', ' '); + std::istringstream structss(token); + + if (!(structss >> w[i].row_index >> w[i].col_index >> w[i].weight)) { + std::cerr << "ERROR: Unable to parse file " << std::string(fname); + exit(1); + } + i++; + } + + if (SIZE != i) { + std::cerr << "ERROR: Expected " << SIZE << " values"; + std::cerr << " but read only " << i << " values" << std::endl; + } + } +} + +template void load_exponent_weights_from_txt(T *w, const char *fname) { + + std::string full_path = std::string(WEIGHTS_DIR) + "/" + std::string(fname); + std::ifstream infile(full_path.c_str(), std::ios::binary); + + if (infile.fail()) { + std::cerr << "ERROR: file " << std::string(fname) << " does not exist" << std::endl; + exit(1); + } + + std::string line; + if (std::getline(infile, line)) { + std::istringstream iss(line); + std::string token; + std::string extra_chars = "} "; + + size_t i = 0; + while (std::getline(iss, token, '{')) { + if (token.length() == 0) { + continue; + } + for (char c : extra_chars) { + token.erase(std::remove(token.begin(), token.end(), c), token.end()); + } + if (token.back() == ',') { + token.erase(token.end() - 1); + } + + std::replace(token.begin(), token.end(), ',', ' '); + std::istringstream structss(token); + + if (!(structss >> w[i].sign >> w[i].weight)) { + std::cerr << "ERROR: Unable to parse file " << std::string(fname); + exit(1); + } + i++; + } + + if (SIZE != i) { + std::cerr << "ERROR: Expected " << SIZE << " values"; + std::cerr << " but read only " << i << " values" << std::endl; + } + } +} +template void convert_data(srcType *src, dstType *dst) { + for (size_t i = 0; i < SIZE; i++) { + dst[i] = dstType(src[i]); + } +} + +template void convert_data(srcType *src, hls::stream &dst) { + for (size_t i = 0; i < SIZE / dstType::size; i++) { + dstType ctype; + for (size_t j = 0; j < dstType::size; j++) { + ctype[j] = typename dstType::value_type(src[i * dstType::size + j]); + } + dst.write(ctype); + } +} + +template void convert_data(hls::stream &src, dstType *dst) { + for (size_t i = 0; i < SIZE / srcType::size; i++) { + srcType ctype = src.read(); + for (size_t j = 0; j < srcType::size; j++) { + dst[i * srcType::size + j] = dstType(ctype[j]); + } + } +} + +extern bool trace_enabled; +extern std::map *trace_outputs; +extern size_t trace_type_size; + +template void save_output_array(data_T *data, save_T *ptr, size_t layer_size) { + for (int i = 0; i < layer_size; i++) { + ptr[i] = save_T(data[i]); + } +} + +template void save_output_array(hls::stream &data, save_T *ptr, size_t layer_size) { + for (size_t i = 0; i < layer_size / data_T::size; i++) { + data_T ctype = data.read(); + for (size_t j = 0; j < data_T::size; j++) { + ptr[i * data_T::size + j] = save_T(ctype[j]); + } + data.write(ctype); + } +} + +// We don't want to include save_T in this function because it will be inserted into myproject.cpp +// so a workaround with element size is used +template void save_layer_output(data_T *data, const char *layer_name, size_t layer_size) { + if (!trace_enabled) + return; + + if (trace_outputs) { + if (trace_outputs->count(layer_name) > 0) { + if (trace_type_size == 4) { + save_output_array(data, (float *)(*trace_outputs)[layer_name], layer_size); + } else if (trace_type_size == 8) { + save_output_array(data, (double *)(*trace_outputs)[layer_name], layer_size); + } else { + std::cout << "Unknown trace type!" << std::endl; + } + } else { + std::cout << "Layer name: " << layer_name << " not found in debug storage!" << std::endl; + } + } else { + std::ostringstream filename; + filename << "./tb_data/" << layer_name << "_output.log"; // TODO if run as a shared lib, path should be ../tb_data + std::fstream out; + out.open(filename.str(), std::ios::app); + assert(out.is_open()); + for (int i = 0; i < layer_size; i++) { + out << float(data[i]) << " "; // We don't care about precision in text files + } + out << std::endl; + out.close(); + } +} + +template void save_layer_output(hls::stream &data, const char *layer_name, size_t layer_size) { + if (!trace_enabled) + return; + + if (trace_outputs) { + if (trace_outputs->count(layer_name) > 0) { + if (trace_type_size == 4) { + save_output_array(data, (float *)(*trace_outputs)[layer_name], layer_size); + } else if (trace_type_size == 8) { + save_output_array(data, (double *)(*trace_outputs)[layer_name], layer_size); + } else { + std::cout << "Unknown trace type!" << std::endl; + } + } else { + std::cout << "Layer name: " << layer_name << " not found in debug storage!" << std::endl; + } + } else { + std::ostringstream filename; + filename << "./tb_data/" << layer_name << "_output.log"; // TODO if run as a shared lib, path should be ../tb_data + std::fstream out; + out.open(filename.str(), std::ios::app); + assert(out.is_open()); + for (size_t i = 0; i < layer_size / data_T::size; i++) { + data_T ctype = data.read(); + for (size_t j = 0; j < data_T::size; j++) { + out << float(ctype[j]) << " "; // We don't care about precision in text files + } + data.write(ctype); + } + out << std::endl; + out.close(); + } +} + +#endif + +template void copy_data(std::vector src, dst_T dst[SIZE]) { + typename std::vector::const_iterator in_begin = src.cbegin() + OFFSET; + typename std::vector::const_iterator in_end = in_begin + SIZE; + std::copy(in_begin, in_end, dst); +} + +template +void copy_data(std::vector src, hls::stream &dst) { + typename std::vector::const_iterator in_begin = src.cbegin() + OFFSET; + typename std::vector::const_iterator in_end = in_begin + SIZE; + + size_t i_pack = 0; + dst_T dst_pack; + for (typename std::vector::const_iterator i = in_begin; i != in_end; ++i) { + dst_pack[i_pack++] = typename dst_T::value_type(*i); + if (i_pack == dst_T::size) { + i_pack = 0; + dst.write(dst_pack); + } + } +} + +template void copy_data_axi(std::vector src, dst_T dst[SIZE]) { + for (auto i = 0; i < SIZE; i++) + if (i == SIZE - 1) { + dst[i].data = src[i]; + dst[i].last = 1; + } else { + dst[i].data = src[i]; + dst[i].last = 0; + } +} + +template void print_result(res_T result[SIZE], std::ostream &out, bool keep = false) { + for (int i = 0; i < SIZE; i++) { + out << result[i] << " "; + } + out << std::endl; +} + +template void print_result(hls::stream &result, std::ostream &out, bool keep = false) { + for (int i = 0; i < SIZE / res_T::size; i++) { + res_T res_pack = result.read(); + for (int j = 0; j < res_T::size; j++) { + out << res_pack[j] << " "; + } + if (keep) + result.write(res_pack); + } + out << std::endl; +} + +template void fill_zero(data_T data[SIZE]) { std::fill_n(data, SIZE, 0.); } + +template void fill_zero(hls::stream &data) { + for (int i = 0; i < SIZE / data_T::size; i++) { + data_T data_pack; + for (int j = 0; j < data_T::size; j++) { + data_pack[j] = 0.; + } + data.write(data_pack); + } +} + +template int read_file_1D(const char *filename, dataType data[nrows]) { + FILE *fp; + fp = fopen(filename, "r"); + if (fp == 0) { + return -1; + } + // Read data from file + float newval; + for (int ii = 0; ii < nrows; ii++) { + if (fscanf(fp, "%f\n", &newval) != 0) { + data[ii] = newval; + } else { + return -2; + } + } + fclose(fp); + return 0; +} + +template +int read_file_2D(const char *filename, dataType data[nrows][ncols]) { + FILE *fp; + fp = fopen(filename, "r"); + if (fp == 0) { + return -1; + } + // Read data from file + float newval; + for (int ii = 0; ii < nrows; ii++) { + for (int jj = 0; jj < ncols; jj++) { + if (fscanf(fp, "%f\n", &newval) != 0) { + data[ii][jj] = newval; + } else { + return -2; + } + } + } + fclose(fp); + return 0; +} + +template void change_type(hls::stream &in, hls::stream &out) { + in_T datareg; + hls::stream input_trunc; + for (int ii = 0; ii < N_IN; ii++) { + out << (out_T)in.read(); + } +} + +template void hls_stream_debug(hls::stream &data, hls::stream &res) { + data_T datareg; + for (int ii = 0; ii < N_IN; ii++) { + datareg = data.read(); + std::cout << "[" << ii << "]: " << datareg << std::endl; + res << datareg; + } +} + +constexpr int ceillog2(int x) { return (x <= 2) ? 1 : 1 + ceillog2((x + 1) / 2); } + +constexpr int floorlog2(int x) { return (x < 2) ? 0 : 1 + floorlog2(x / 2); } + +constexpr int pow2(int x) { return x == 0 ? 1 : 2 * pow2(x - 1); } + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_image.h b/hls4ml/templates/bambu/nnet_utils/nnet_image.h new file mode 100644 index 0000000000..326fdc0534 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_image.h @@ -0,0 +1,41 @@ +#ifndef NNET_IMAGE_H_ +#define NNET_IMAGE_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include + +namespace nnet { + +struct resize_config { + static const unsigned height = 10; + static const unsigned width = 10; + static const unsigned n_chan = 10; + static const unsigned new_height = 10; + static const unsigned new_width = 10; +}; + +template +void resize_nearest(data_T image[CONFIG_T::height * CONFIG_T::width * CONFIG_T::n_chan], + data_T resized[CONFIG_T::new_height * CONFIG_T::new_width * CONFIG_T::n_chan]) { + int y_ratio = (int)((CONFIG_T::height << 16) / CONFIG_T::new_height) + 1; + int x_ratio = (int)((CONFIG_T::width << 16) / CONFIG_T::new_width) + 1; + int x2, y2; + + //#pragma HLS PIPELINE + + for (int i = 0; i < CONFIG_T::new_height; i++) { + for (int j = 0; j < CONFIG_T::new_width; j++) { + x2 = ((j * x_ratio) >> 16); + y2 = ((i * y_ratio) >> 16); + for (int k = 0; k < CONFIG_T::n_chan; k++) { + resized[(i * CONFIG_T::new_width * CONFIG_T::n_chan) + j * CONFIG_T::n_chan + k] = + image[(y2 * CONFIG_T::width * CONFIG_T::n_chan) + x2 * CONFIG_T::n_chan + k]; + } + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_image_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_image_stream.h new file mode 100644 index 0000000000..fd166afc1e --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_image_stream.h @@ -0,0 +1,72 @@ +#ifndef NNET_IMAGE_STREAM_H_ +#define NNET_IMAGE_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" + +namespace nnet { + +template void resize_nearest(hls::stream &image, hls::stream &resized) { + assert(CONFIG_T::new_height % CONFIG_T::height == 0); + assert(CONFIG_T::new_width % CONFIG_T::width == 0); + constexpr unsigned ratio_height = CONFIG_T::new_height / CONFIG_T::height; + constexpr unsigned ratio_width = CONFIG_T::new_width / CONFIG_T::width; + +ImageHeight: + for (unsigned h = 0; h < CONFIG_T::height; h++) { + //#pragma HLS PIPELINE + + data_T data_in_row[CONFIG_T::width]; + + ImageWidth: + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::width; i++) { + //#pragma HLS UNROLL + + data_T in_data = image.read(); + + ImageChan: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < CONFIG_T::n_chan; j++) { + //#pragma HLS UNROLL + + data_in_row[i][j] = in_data[j]; + } + } + + ResizeHeight: + #pragma clang loop unroll(full) + for (unsigned i = 0; i < ratio_height; i++) { + //#pragma HLS UNROLL + + ImageWidth2: + #pragma clang loop unroll(full) + for (unsigned l = 0; l < CONFIG_T::width; l++) { + //#pragma HLS UNROLL + + ResizeWidth: + #pragma clang loop unroll(full) + for (unsigned j = 0; j < ratio_width; j++) { + //#pragma HLS UNROLL + + data_T out_data; + PRAGMA_DATA_PACK(out_data) + + ResizeChan: + #pragma clang loop unroll(full) + for (unsigned k = 0; k < CONFIG_T::n_chan; k++) { + //#pragma HLS UNROLL + + out_data[k] = data_in_row[l][k]; + } + + resized.write(out_data); + } + } + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_layernorm.h b/hls4ml/templates/bambu/nnet_utils/nnet_layernorm.h new file mode 100644 index 0000000000..5e68fcf449 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_layernorm.h @@ -0,0 +1,142 @@ +#ifndef NNET_LAYERNORM_H_ +#define NNET_LAYERNORM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_dense.h" +#include + +#include "hls_math.h" + +namespace nnet { + +struct layernorm_config { + // Internal data type definitions + typedef float bias_t; + typedef float scale_t; + typedef float accum_t; + typedef float table_t; + + // Layer Sizes + static const unsigned n_in = 20; + static const unsigned seq_len = 4; + static const unsigned axis = 2; + static const unsigned epsilon_power_of_10 = 3; + static const unsigned table_range_power2 = 0; + static const unsigned table_size = 1024; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; + + template using product = nnet::product::mult; +}; + +template void init_invert_sqr_table(typename CONFIG_T::table_t table_out[N_TABLE]) { + // Inversion function: + // result = 1/sqrt(x) + float min_val = pow(10.0f, -(int)CONFIG_T::epsilon_power_of_10); + float max_val = pow(2.0f, -(int)CONFIG_T::table_range_power2); + float step = max_val / (float)(N_TABLE); + for (int ii = 0; ii < N_TABLE; ii++) { + float in_val = min_val + step * ii; + table_out[ii] = (typename CONFIG_T::table_t)(1.0 / sqrt(in_val)); + } +} + +template +void layernorm_1d(data_T data[CONFIG_T::n_in / CONFIG_T::seq_len], res_T res[CONFIG_T::n_in / CONFIG_T::seq_len], + typename CONFIG_T::scale_t scale[CONFIG_T::n_in / CONFIG_T::seq_len], + typename CONFIG_T::bias_t bias[CONFIG_T::n_in / CONFIG_T::seq_len]) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor /// to be checked again + #pragma HLS ARRAY_PARTITION variable=data complete + #pragma HLS ARRAY_PARTITION variable=res complete + int inv_range_inv = (int)1 << CONFIG_T::table_range_power2; + typename CONFIG_T::table_t deno_inver = 0; +#ifdef __HLS_SYN__ + bool initialized = false; + typename CONFIG_T::table_t invert_sqr_table[CONFIG_T::table_size]; +#else + static bool initialized = false; + static typename CONFIG_T::table_t invert_sqr_table[CONFIG_T::table_size]; +#endif + if (!initialized) { + init_invert_sqr_table(invert_sqr_table); + initialized = true; + } + + static const unsigned dim = CONFIG_T::n_in / CONFIG_T::seq_len; + typename CONFIG_T::accum_t sum_cache = 0; + typename CONFIG_T::accum_t sum_cache2 = 0; + typename CONFIG_T::accum_t var, mean, diff; + typename CONFIG_T::accum_t data_diff[dim]; + + #pragma HLS ARRAY_PARTITION variable=data_diff complete + + const typename CONFIG_T::accum_t k_inv = 1.0 / dim; + +LAYERNORM_1D_SUM: + #pragma clang loop unroll(full) + for (int i = 0; i < dim; ++i) { + sum_cache += static_cast(data[i]); + } + mean = CONFIG_T::template product::product(sum_cache, k_inv); + +LAYERNORM_1D_VAR: + #pragma clang loop unroll(full) + for (int i = 0; i < dim; ++i) { + data_diff[i] = static_cast(data[i]) - mean; + diff = data_diff[i] * data_diff[i]; + sum_cache2 += diff; + } + var = CONFIG_T::template product::product(sum_cache2, k_inv); + + int index = (var) * (CONFIG_T::table_size)*inv_range_inv; + if (index < 0) + index = 0; + if (index > CONFIG_T::table_size - 1) + index = CONFIG_T::table_size - 1; + deno_inver = invert_sqr_table[index]; + +LAYERNORM_1D_RESULT: + #pragma clang loop unroll(full) + for (int i = 0; i < dim; ++i) { + res[i] = data_diff[i] * deno_inver * scale[i] + bias[i]; + } +} + +template +void layernormalize(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in], + typename CONFIG_T::scale_t scale[CONFIG_T::n_in / CONFIG_T::seq_len], + typename CONFIG_T::bias_t bias[CONFIG_T::n_in / CONFIG_T::seq_len]) { + static const unsigned dim = CONFIG_T::n_in / CONFIG_T::seq_len; + data_T in_val[dim]; + res_T outval[dim]; + + #pragma HLS ARRAY_PARTITION variable=scale complete + #pragma HLS ARRAY_PARTITION variable=bias complete + #pragma HLS ARRAY_PARTITION variable=in_val complete + #pragma HLS ARRAY_PARTITION variable=outval complete + +LAYERNORM_SEQ_LOOP: + for (int j = 0; j < CONFIG_T::seq_len; ++j) { + //#pragma HLS PIPELINE + LAYERNORM_LOAD: + #pragma clang loop unroll(full) + for (int i = 0; i < dim; ++i) { + //#pragma HLS UNROLL + in_val[i] = data[j * dim + i]; + } + layernorm_1d(in_val, outval, scale, bias); + LAYERNORM_STORE: + #pragma clang loop unroll(full) + for (int i = 0; i < dim; ++i) { + //#pragma HLS UNROLL + res[j * dim + i] = outval[i]; + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_math.h b/hls4ml/templates/bambu/nnet_utils/nnet_math.h new file mode 100644 index 0000000000..bbc5dc41e8 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_math.h @@ -0,0 +1,178 @@ +#ifndef NNET_MATH_H_ +#define NNET_MATH_H_ + +#include "hls_math.h" + +namespace nnet { + +// This header defines the functions that return type different from the input +// For example, hls::sin(x) returns ap_fixed +// By ensuring we return the same type we can avoid casting issues in expressions + +template T sin(T x) { return (T)hls::sin(x); }; + +template T cos(T x) { return (T)hls::cos(x); }; + +template T asin(T x) { return (T)hls::asin(x); }; + +template T acos(T x) { return (T)hls::acos(x); }; + +template T atan(T x) { return (T)hls::atan(x); }; + +template T atan2(T x, T y) { return (T)hls::atan2(x, y); }; + +template void init_sincos_table(T table[1 << (W - I - 3)][2]) { + unsigned int NTE = 1 << (W - I - 3); // No of table entries + double step = M_PI / (4 * NTE); // Interval between angles + double y = 0; + // double scaled_angle = 0; + + for (unsigned int i = 0; i < NTE; i++) { + table[i][0] = std::cos(y); + table[i][1] = std::sin(y); + y += step; + // scaled_angle = y/(2*M_PI); + // printf("cos(%f) = %23.22f, sin(%f) = %23.22f index = %d, scaled angle = %13.12f \n", y, cos(y), y, sin(y), i, + // scaled_angle); + } +} + +template void sincos_lut(const T &input, T output[2]) { + + #pragma HLS inline + + // This implementation is based on ac_sincos_lut.h from AC math library + + static bool flag = true; + if (flag && T::width - T::iwidth > 12) { +#if !defined(__SYNTHESIS__) && defined(SINCOS_LUT_DEBUG) + std::cout << "FILE : " << __FILE__ << ", LINE : " << __LINE__ << std::endl; + std::cout << "Warning: The output of sincos_lut will not be accurate" << std::endl; +#endif + flag = false; + } + // Datatype for lookup table entries + typedef ap_ufixed luttype; + // Datatype for posinput which is used to handle negative inputs + typedef ap_ufixed posinputtype; + + typedef ap_uint<9> lutindextype; // 9 bits required for indexing into 512 entry table + typedef ap_uint<3> octanttype; // 3 bits required for octant value range of 0 thru 7 + T outputtemp[2]; + lutindextype luTdex = 0; + posinputtype posinput = input; + + // Initialize the lookup table +#ifdef __SYNTHESIS__ + bool initialized = false; + luttype sincos[512][2]; +#else + static bool initialized = false; + static luttype sincos[512][2]; +#endif + if (!initialized) { + init_sincos_table(sincos); + initialized = true; + } + + // Leaving this commented out makes the table to to BRAM + #pragma HLS ARRAY_PARTITION variable=sincos complete dim=0 + + typedef ap_uint lutindextype1; + // Extracting (MSB-3:LSB) bits of scaled input to determine the lookup table index + lutindextype1 luTdex1 = posinput.range(AP_MAX(T::width - T::iwidth - 3, 1), 0); // Extracting the lookup table index + + if (T::width - T::iwidth >= 4 && T::width - T::iwidth <= 12) { + luTdex(8, 12 - (T::width - T::iwidth)) = luTdex1; // stride + } + // Approximation for the scaled inputs whose number of bits are greater than 12 + else if (T::width - T::iwidth > 12) { + // Lookup table index for the scaled inputs whose number of bits are greater than 12 + luTdex = luTdex1 / (1 << (AP_MAX(T::width - T::iwidth - 12, 0))); + if ((luTdex1 % (1 << (AP_MAX(T::width - T::iwidth - 12, 0)))) > (1 << (AP_MAX(T::width - T::iwidth - 13, 0)))) { + luTdex = luTdex + 1; + } + typedef ap_ufixed + datatype; + datatype x = (datatype)luTdex1; + x = x >> AP_MAX(T::width - T::iwidth - 12, 0); + if (x > 511.5) { + luTdex = 511; + } + if (luTdex1 <= 1 << (AP_MAX(T::width - T::iwidth - 13, 0)) && luTdex1 != 0) { + luTdex = 1; + } + } + + if (T::width - T::iwidth >= 3) { + // Getting the octant 0-7 by extracting the first 3 bits from MSB side of scaled input where + // octant 0 corresponds to [0-PI/4), + // octant 1 corresponds to [PI/4-2PI/4), + // octant 2 corresponds to [2PI/4-3PI/4) and so on + // octanttype octant = posinput.template slc<3>(T::width-T::iwidth-3); + octanttype octant = posinput(T::width - T::iwidth - 1, T::width - T::iwidth - 3); + luTdex = (octant[0] == 1) ? (lutindextype)(512 - luTdex) : (lutindextype)(luTdex); + // imaginary part is sine + outputtemp[1] = ((octant == 0) | (octant == 3)) ? (T)sincos[luTdex][1] + : ((octant == 2) | (octant == 1)) ? (T)sincos[luTdex][0] + : ((octant == 7) | (octant == 4)) ? (T)-sincos[luTdex][1] + : (T)-sincos[luTdex][0]; + // real part is cosine + outputtemp[0] = ((octant == 6) | (octant == 1)) ? (T)sincos[luTdex][1] + : ((octant == 3) | (octant == 4)) ? (T)-sincos[luTdex][0] + : ((octant == 2) | (octant == 5)) ? (T)-sincos[luTdex][1] + : (T)sincos[luTdex][0]; + // Below two are the cases when the output corresponds to + or - (0 or 1) for which there is no entry in the lookup + // table + output[1] = ((posinput == 0.125) | (posinput == 0.375)) ? T(0.7071067811865475244008) + : ((posinput == 0.625) | (posinput == 0.875)) ? T(-0.7071067811865475244008) + : outputtemp[1]; + output[0] = ((posinput == 0.125) | (posinput == 0.875)) ? T(0.7071067811865475244008) + : ((posinput == 0.375) | (posinput == 0.625)) ? T(-0.7071067811865475244008) + : outputtemp[0]; + } + + if (T::width - T::iwidth <= 2) { + output[1] = (posinput == 0) ? (T)0 + : (posinput == 0.25) ? (T)1 + : (posinput == 0.5) ? (T)0 + : (posinput == 0.75) ? (T)-1 + : outputtemp[1]; + output[0] = (posinput == 0) ? (T)1 + : (posinput == 0.25) ? (T)0 + : (posinput == 0.5) ? (T)-1 + : (posinput == 0.75) ? (T)0 + : outputtemp[0]; + } + +#if !defined(__SYNTHESIS__) && defined(SINCOS_LUT_DEBUG) + std::cout << "FILE : " << __FILE__ << ", LINE : " << __LINE__ << std::endl; + std::cout << "============AP_FIXED SINCOS======================" << std::endl; + std::cout << "positive input is = " << posinput << std::endl; + std::cout << "lut index is = " << luTdex << std::endl; + std::cout << "sin value is = " << output[1] << std::endl; + std::cout << "cos value is = " << output[0] << std::endl; + std::cout << "=================================================" << std::endl; +#endif +} + +template T sin_lut(const T input) { + #pragma HLS inline + T sincos_res[2]; + T scaled_input = input * ap_ufixed<16, 0>(0.15915494309); // 1/(2*pi) + sincos_lut(scaled_input, sincos_res); + return sincos_res[1]; +} + +template T cos_lut(const T input) { + #pragma HLS inline + T sincos_res[2]; + T scaled_input = input * ap_ufixed<16, 0>(0.15915494309); // 1/(2*pi) + sincos_lut(scaled_input, sincos_res); + return sincos_res[0]; +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_merge.h b/hls4ml/templates/bambu/nnet_utils/nnet_merge.h new file mode 100644 index 0000000000..25f6f701d8 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_merge.h @@ -0,0 +1,283 @@ +#ifndef NNET_MERGE_H_ +#define NNET_MERGE_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_mult.h" +#include + +namespace nnet { + +struct merge_config { + static const unsigned n_elem = 10; + static const unsigned reuse_factor = 1; +}; + +struct dot_config { + static const unsigned n_in = 10; + static const unsigned n_out = 1; + static const unsigned reuse_factor = 1; + typedef float accum_t; + // Product function to use + template using product = nnet::product::mult; +}; + +struct concat_config { + static const unsigned n_elem1_0 = 10; + static const unsigned n_elem1_1 = 10; + static const unsigned n_elem1_2 = 10; + static const unsigned n_elem2_0 = 10; + static const unsigned n_elem2_1 = 10; + static const unsigned n_elem2_2 = 10; + + static const unsigned axis = -1; +}; + +template +void add(input1_T data1[CONFIG_T::n_elem], input2_T data2[CONFIG_T::n_elem], res_T res[CONFIG_T::n_elem]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem; ii++) { + res[ii] = data1[ii] + data2[ii]; + } +} + +template +void subtract(input1_T data1[CONFIG_T::n_elem], input2_T data2[CONFIG_T::n_elem], res_T res[CONFIG_T::n_elem]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem; ii++) { + res[ii] = data1[ii] - data2[ii]; + } +} + +template +void multiply(input1_T data1[CONFIG_T::n_elem], input2_T data2[CONFIG_T::n_elem], res_T res[CONFIG_T::n_elem]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem; ii++) { + res[ii] = data1[ii] * data2[ii]; + } +} + +template +void average(input1_T data1[CONFIG_T::n_elem], input2_T data2[CONFIG_T::n_elem], res_T res[CONFIG_T::n_elem]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem; ii++) { + res[ii] = (data1[ii] + data2[ii]) * ap_ufixed<1, 0>(0.5); + } +} + +template +void maximum(input1_T data1[CONFIG_T::n_elem], input2_T data2[CONFIG_T::n_elem], res_T res[CONFIG_T::n_elem]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem; ii++) { + res[ii] = (data1[ii] > data2[ii]) ? static_cast(data1[ii]) : static_cast(data2[ii]); + } +} + +template +void minimum(input1_T data1[CONFIG_T::n_elem], input2_T data2[CONFIG_T::n_elem], res_T res[CONFIG_T::n_elem]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem; ii++) { + res[ii] = (data1[ii] < data2[ii]) ? static_cast(data1[ii]) : static_cast(data2[ii]); + } +} + +template +void dot1d(input1_T data1[CONFIG_T::n_in], input2_T data2[CONFIG_T::n_in], res_T res[CONFIG_T::n_out]) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor /// to be checked again + + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::multiplier_limit + + typename CONFIG_T::accum_t mult[CONFIG_T::n_in]; + #pragma HLS ARRAY_PARTITION variable=mult complete + typename CONFIG_T::accum_t acc = 0; + +Product: + #pragma clang loop unroll(full) + for (int i_mult = 0; i_mult < CONFIG_T::n_in; i_mult++) { + //#pragma HLS UNROLL + mult[i_mult] = CONFIG_T::template product::product(data1[i_mult], data2[i_mult]); + } + +Accum: + #pragma clang loop unroll(full) + for (int i_acc = 0; i_acc < CONFIG_T::n_in; i_acc++) { + //#pragma HLS UNROLL + acc += mult[i_acc]; + } + + res[0] = cast(acc); +} + +template +void concatenate1d(input1_T data1[CONFIG_T::n_elem1_0], input2_T data2[CONFIG_T::n_elem2_0], + res_T res[CONFIG_T::n_elem1_0 + CONFIG_T::n_elem2_0]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem1_0; ii++) { + res[ii] = data1[ii]; + } + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem2_0; ii++) { + res[CONFIG_T::n_elem1_0 + ii] = data2[ii]; + } +} + +template +void concatenate2d_0(input1_T data1[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1], + input2_T data2[CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1], + res_T res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 + CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1; ii++) { + res[ii] = data1[ii]; + } + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1; ii++) { + res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 + ii] = data2[ii]; + } +} + +template +void concatenate2d_1(input1_T data1[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1], + input2_T data2[CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1], + res_T res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 + CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem1_0; ii++) { + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_elem1_1; jj++) { + res[ii * (CONFIG_T::n_elem1_1 + CONFIG_T::n_elem2_1) + jj] = data1[ii * CONFIG_T::n_elem1_1 + jj]; + } + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_elem2_1; jj++) { + res[ii * (CONFIG_T::n_elem1_1 + CONFIG_T::n_elem2_1) + CONFIG_T::n_elem1_1 + jj] = + data2[ii * CONFIG_T::n_elem2_1 + jj]; + } + } +} + +template +void concatenate2d(input1_T data1[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1], + input2_T data2[CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1], + res_T res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 + CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1]) { + #pragma HLS inline + + if (CONFIG_T::axis == 2 || CONFIG_T::axis == -1) { + concatenate2d_1(data1, data2, res); + } else { + concatenate2d_0(data1, data2, res); + } +} + +template +void concatenate3d_0(input1_T data1[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2], + input2_T data2[CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2], + res_T res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2 + + CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2; ii++) { + res[ii] = data1[ii]; + } + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2; ii++) { + res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2 + ii] = data2[ii]; + } +} + +template +void concatenate3d_1(input1_T data1[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2], + input2_T data2[CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2], + res_T res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2 + + CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem1_0; ii++) { + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_elem1_1; jj++) { + #pragma clang loop unroll(full) + for (int kk = 0; kk < CONFIG_T::n_elem1_2; kk++) { + int res_idx = + ii * (CONFIG_T::n_elem1_1 + CONFIG_T::n_elem2_1) * CONFIG_T::n_elem1_2 + jj * CONFIG_T::n_elem1_2 + kk; + int data_idx = ii * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2 + jj * CONFIG_T::n_elem1_2 + kk; + res[res_idx] = data1[data_idx]; + } + } + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_elem2_1; jj++) { + #pragma clang loop unroll(full) + for (int kk = 0; kk < CONFIG_T::n_elem2_2; kk++) { + int res_idx = ii * (CONFIG_T::n_elem1_1 + CONFIG_T::n_elem2_1) * CONFIG_T::n_elem1_2 + + (jj + CONFIG_T::n_elem1_1) * CONFIG_T::n_elem1_2 + kk; + int data_idx = ii * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2 + jj * CONFIG_T::n_elem2_2 + kk; + res[res_idx] = data2[data_idx]; + } + } + } +} + +template +void concatenate3d_2(input1_T data1[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2], + input2_T data2[CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2], + res_T res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2 + + CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_elem1_0; ii++) { + #pragma clang loop unroll(full) + for (int jj = 0; jj < CONFIG_T::n_elem1_1; jj++) { + #pragma clang loop unroll(full) + for (int kk = 0; kk < CONFIG_T::n_elem1_2; kk++) { + int res_idx = ii * CONFIG_T::n_elem1_1 * (CONFIG_T::n_elem1_2 + CONFIG_T::n_elem2_2) + + jj * (CONFIG_T::n_elem1_2 + CONFIG_T::n_elem2_2) + kk; + int data_idx = ii * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2 + jj * CONFIG_T::n_elem1_2 + kk; + res[res_idx] = data1[data_idx]; + } + #pragma clang loop unroll(full) + for (int kk = 0; kk < CONFIG_T::n_elem2_2; kk++) { + int res_idx = ii * CONFIG_T::n_elem1_1 * (CONFIG_T::n_elem1_2 + CONFIG_T::n_elem2_2) + + jj * (CONFIG_T::n_elem1_2 + CONFIG_T::n_elem2_2) + kk + CONFIG_T::n_elem1_2; + int data_idx = ii * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2 + jj * CONFIG_T::n_elem2_2 + kk; + res[res_idx] = data2[data_idx]; + } + } + } +} + +template +void concatenate3d(input1_T data1[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2], + input2_T data2[CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2], + res_T res[CONFIG_T::n_elem1_0 * CONFIG_T::n_elem1_1 * CONFIG_T::n_elem1_2 + + CONFIG_T::n_elem2_0 * CONFIG_T::n_elem2_1 * CONFIG_T::n_elem2_2]) { + #pragma HLS inline + + if (CONFIG_T::axis == 3 || CONFIG_T::axis == -1) { + concatenate3d_2(data1, data2, res); + } else if (CONFIG_T::axis == 2 || CONFIG_T::axis == -2) { + concatenate3d_1(data1, data2, res); + } else { + concatenate3d_0(data1, data2, res); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_merge_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_merge_stream.h new file mode 100644 index 0000000000..9f6a8b9b08 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_merge_stream.h @@ -0,0 +1,390 @@ +#ifndef NNET_MERGE_STREAM_H_ +#define NNET_MERGE_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include + +namespace nnet { + +template +void add(hls::stream &data1, hls::stream &data2, hls::stream &res) { + assert(input1_T::size == input2_T::size && input1_T::size == res_T::size); + +AddLoop: + for (int i = 0; i < CONFIG_T::n_elem / input1_T::size; i++) { + //#pragma HLS PIPELINE + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + AddPack: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = in_data1[j] + in_data2[j]; + } + + res.write(out_data); + } +} + +template +void subtract(hls::stream &data1, hls::stream &data2, hls::stream &res) { + assert(input1_T::size == input2_T::size && input1_T::size == res_T::size); + +SubtractLoop: + for (int i = 0; i < CONFIG_T::n_elem / input1_T::size; i++) { + //#pragma HLS PIPELINE + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + SubtractPack: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = in_data1[j] - in_data2[j]; + } + + res.write(out_data); + } +} + +template +void multiply(hls::stream &data1, hls::stream &data2, hls::stream &res) { + assert(input1_T::size == input2_T::size && input1_T::size == res_T::size); + +MultiplyLoop: + for (int i = 0; i < CONFIG_T::n_elem / input1_T::size; i++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor /// to be checked again + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + MultiplyPack: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = in_data1[j] * in_data2[j]; + } + + res.write(out_data); + } +} + +template +void average(hls::stream &data1, hls::stream &data2, hls::stream &res) { + assert(input1_T::size == input2_T::size && input1_T::size == res_T::size); + +AverageLoop: + for (int i = 0; i < CONFIG_T::n_elem / input1_T::size; i++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor /// to be checked again + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + AveragePack: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = (in_data1[j] + in_data2[j]) * ap_ufixed<1, 0>(0.5); + } + + res.write(out_data); + } +} + +template +void maximum(hls::stream &data1, hls::stream &data2, hls::stream &res) { + assert(input1_T::size == input2_T::size && input1_T::size == res_T::size); + +MaximumLoop: + for (int i = 0; i < CONFIG_T::n_elem / input1_T::size; i++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor /// to be checked again + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + MaximumPack: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = (in_data1[j] > in_data2[j]) ? static_cast(in_data1[j]) + : static_cast(in_data2[j]); + } + + res.write(out_data); + } +} + +template +void minimum(hls::stream &data1, hls::stream &data2, hls::stream &res) { + assert(input1_T::size == input2_T::size && input1_T::size == res_T::size); + +MinimumLoop: + for (int i = 0; i < CONFIG_T::n_elem / input1_T::size; i++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor /// to be checked again + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + MinimumPack: + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = (in_data1[j] < in_data2[j]) ? static_cast(in_data1[j]) + : static_cast(in_data2[j]); + } + + res.write(out_data); + } +} + +template +void concatenate3d_0(hls::stream &data1, hls::stream &data2, hls::stream &res) { +ConcatLoopHeight1: + for (int i = 0; i < CONFIG_T::n_elem1_0; i++) { + ConcatLoopWidth1: + for (int j = 0; j < CONFIG_T::n_elem1_1; j++) { + //#pragma HLS PIPELINE II=1 + + input1_T in_data1 = data1.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput1: + #pragma clang loop unroll(full) + for (int k = 0; k < input1_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data1[k]; + } + + res.write(out_data); + } + } +ConcatLoopHeight2: + for (int i = 0; i < CONFIG_T::n_elem2_0; i++) { + ConcatLoopWidth2: + for (int j = 0; j < CONFIG_T::n_elem2_1; j++) { + //#pragma HLS PIPELINE II=1 + + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput2: + #pragma clang loop unroll(full) + for (int k = 0; k < input2_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data2[k]; + } + + res.write(out_data); + } + } +} + +template +void concatenate3d_1(hls::stream &data1, hls::stream &data2, hls::stream &res) { +ConcatLoopHeight: + for (int i = 0; i < CONFIG_T::n_elem1_0; i++) { + ConcatLoopWidth1: + for (int j = 0; j < CONFIG_T::n_elem1_1; j++) { + //#pragma HLS PIPELINE II=1 + + input1_T in_data1 = data1.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput1: + #pragma clang loop unroll(full) + for (int k = 0; k < input1_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data1[k]; + } + + res.write(out_data); + } + ConcatLoopWidth2: + for (int j = 0; j < CONFIG_T::n_elem2_1; j++) { + //#pragma HLS PIPELINE II=1 + + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput2: + #pragma clang loop unroll(full) + for (int k = 0; k < input2_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data2[k]; + } + + res.write(out_data); + } + } +} + +template +void concatenate3d_2(hls::stream &data1, hls::stream &data2, hls::stream &res) { +ConcatLoopHeight: + for (int i = 0; i < CONFIG_T::n_elem1_0; i++) { + ConcatLoopWidth: + for (int j = 0; j < CONFIG_T::n_elem1_1; j++) { + //#pragma HLS PIPELINE II=1 + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput1: + #pragma clang loop unroll(full) + for (int k = 0; k < input1_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data1[k]; + } + + ConcatPackInput2: + #pragma clang loop unroll(full) + for (int k = 0; k < input2_T::size; k++) { + //#pragma HLS UNROLL + out_data[input1_T::size + k] = in_data2[k]; + } + + res.write(out_data); + } + } +} + +template +void concatenate3d(hls::stream &data1, hls::stream &data2, hls::stream &res) { + if (CONFIG_T::axis == 3 || CONFIG_T::axis == -1) { + concatenate3d_2(data1, data2, res); + } else if (CONFIG_T::axis == 2 || CONFIG_T::axis == -2) { + concatenate3d_1(data1, data2, res); + } else { + concatenate3d_0(data1, data2, res); + } +} + +template +void concatenate2d_0(hls::stream &data1, hls::stream &data2, hls::stream &res) { +ConcatLoopHeight1: + for (int i = 0; i < CONFIG_T::n_elem1_0; i++) { + //#pragma HLS PIPELINE II=1 + + input1_T in_data1 = data1.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput1: + #pragma clang loop unroll(full) + for (int k = 0; k < input1_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data1[k]; + } + + res.write(out_data); + } +ConcatLoopHeight2: + for (int i = 0; i < CONFIG_T::n_elem2_0; i++) { + //#pragma HLS PIPELINE II=1 + + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput2: + #pragma clang loop unroll(full) + for (int k = 0; k < input2_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data2[k]; + } + + res.write(out_data); + } +} + +template +void concatenate2d_1(hls::stream &data1, hls::stream &data2, hls::stream &res) { +ConcatLoopHeight: + for (int i = 0; i < CONFIG_T::n_elem1_0; i++) { + //#pragma HLS PIPELINE II=1 + + input1_T in_data1 = data1.read(); + input2_T in_data2 = data2.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + ConcatPackInput1: + #pragma clang loop unroll(full) + for (int k = 0; k < input1_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data1[k]; + } + + ConcatPackInput2: + #pragma clang loop unroll(full) + for (int k = 0; k < input2_T::size; k++) { + //#pragma HLS UNROLL + out_data[input1_T::size + k] = in_data2[k]; + } + + res.write(out_data); + } +} + +template +void concatenate2d(hls::stream &data1, hls::stream &data2, hls::stream &res) { + if (CONFIG_T::axis == 2 || CONFIG_T::axis == -1) { + concatenate2d_1(data1, data2, res); + } else { + concatenate2d_0(data1, data2, res); + } +} + +template +void concatenate1d(hls::stream &data1, hls::stream &data2, hls::stream &res) { + res_T out_data; + PRAGMA_DATA_PACK(out_data) +ConcatLoop1: + for (int i = 0; i < CONFIG_T::n_elem1_0 / input1_T::size; i++) { + //#pragma HLS PIPELINE + input1_T in_data1 = data1.read(); + ConcatPack1: + #pragma clang loop unroll(full) + for (int j = 0; j < input1_T::size; j++) { + //#pragma HLS UNROLL + out_data[j + (i * input1_T::size)] = in_data1[j]; + } + } +ConcatLoop2: + for (int i = 0; i < CONFIG_T::n_elem2_0 / input2_T::size; i++) { + //#pragma HLS PIPELINE + input2_T in_data2 = data2.read(); + ConcatPack2: + #pragma clang loop unroll(full) + for (int j = 0; j < input2_T::size; j++) { + //#pragma HLS UNROLL + out_data[j + (i * input2_T::size) + (CONFIG_T::n_elem1_0)] = in_data2[j]; + } + } + res.write(out_data); +} +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_mult.h b/hls4ml/templates/bambu/nnet_utils/nnet_mult.h new file mode 100644 index 0000000000..62f9379dbb --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_mult.h @@ -0,0 +1,116 @@ +#ifndef NNET_MULT_H_ +#define NNET_MULT_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_helpers.h" +#include +#include + +namespace nnet { + +namespace product { + +/* --- + * different methods to perform the product of input and weight, depending on the + * types of each. + * --- */ + +class Product {}; + +template class both_binary : public Product { + public: + static x_T product(x_T a, w_T w) { + // specialisation for 1-bit weights and incoming data + #pragma HLS inline + return a == w; + } +}; + +template class weight_binary : public Product { + public: + static auto product(x_T a, w_T w) -> decltype(-a) { + // Specialisation for 1-bit weights, arbitrary data + #pragma HLS inline + if (w == 0) + return -a; + else + return a; + } +}; + +template class data_binary : public Product { + public: + static auto product(x_T a, w_T w) -> decltype(-w) { + // Specialisation for 1-bit data, arbitrary weight + #pragma HLS inline + if (a == 0) + return -w; + else + return w; + } +}; + +template class weight_ternary : public Product { + public: + static auto product(x_T a, w_T w) -> decltype(-a) { + // Specialisation for 2-bit weights, arbitrary data + #pragma HLS inline + if (w == 0) + return 0; + else if (w == -1) + return -a; + else + return a; // if(w == 1) + } +}; + +template class mult : public Product { + public: + static auto product(x_T a, w_T w) -> decltype(a * w) { + // 'Normal' product + #pragma HLS inline + return a * w; + } +}; + +template class weight_exponential : public Product { + public: + using r_T = ap_fixed<2 * (decltype(w_T::weight)::width + x_T::width), (decltype(w_T::weight)::width + x_T::width)>; + static r_T product(x_T a, w_T w) { + // Shift product for exponential weights + #pragma HLS inline + + // Shift by the exponent. Negative weights shift right + r_T y = static_cast(a) << w.weight; + + // Negate or not depending on weight sign + return w.sign == 1 ? y : static_cast(-y); + } +}; + +} // namespace product + +template +inline typename std::enable_if>::value && + std::is_same>::value, + ap_int>::type +cast(typename CONFIG_T::accum_t x) { + return static_cast>(x * 2 - CONFIG_T::n_in); +} + +template +inline typename std::enable_if< + std::is_same>::value && !std::is_same>::value, res_T>::type +cast(typename CONFIG_T::accum_t x) { + return (res_T)x; +} + +template +inline typename std::enable_if<(!std::is_same>::value), res_T>::type cast(typename CONFIG_T::accum_t x) { + return (res_T)x; +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_padding.h b/hls4ml/templates/bambu/nnet_utils/nnet_padding.h new file mode 100644 index 0000000000..200ec54fb4 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_padding.h @@ -0,0 +1,176 @@ +#ifndef NNET_PADDING_H_ +#define NNET_PADDING_H_ + +#include + +namespace nnet { + +struct padding1d_config { + static const unsigned n_chan = 10; + static const unsigned in_width = 10; + static const unsigned out_width = 10; + static const unsigned pad_left = 0; + static const unsigned pad_right = 0; +}; + +template +void zeropad1d_cf(data_T data[CONFIG_T::n_chan * CONFIG_T::in_width], data_T res[CONFIG_T::n_chan * CONFIG_T::out_width]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_chan; j++) { + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_left; i++) { + *(res++) = 0; + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_width; i++) { + *(res++) = (res_T) * (data++); + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_right; i++) { + *(res++) = 0; + } + } +} + +template +void zeropad1d_cl(data_T data[CONFIG_T::n_chan * CONFIG_T::in_width], res_T res[CONFIG_T::n_chan * CONFIG_T::out_width]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_left; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_chan; j++) { + *(res++) = 0; + } + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_width; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_chan; j++) { + *(res++) = (res_T) * (data++); + } + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_right; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_chan; j++) { + *(res++) = 0; + } + } +} + +struct padding2d_config { + static const unsigned n_chan = 10; + static const unsigned in_height = 10; + static const unsigned in_width = 10; + static const unsigned out_height = 10; + static const unsigned out_width = 10; + static const unsigned pad_top = 0; + static const unsigned pad_bottom = 0; + static const unsigned pad_left = 0; + static const unsigned pad_right = 0; +}; + +template +void zeropad2d_cf(data_T data[CONFIG_T::n_chan * CONFIG_T::in_height * CONFIG_T::in_width], + data_T res[CONFIG_T::n_chan * CONFIG_T::out_height * CONFIG_T::out_width]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_top; i++) { + for (int j = 0; j < CONFIG_T::out_width; j++) { + *(res++) = 0; + } + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_height; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::pad_left; j++) { + *(res++) = 0; + } + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::in_width; j++) { + *(res++) = (res_T) * (data++); + } + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::pad_right; j++) { + *(res++) = 0; + } + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_bottom; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::out_width; j++) { + *(res++) = 0; + } + } + } +} + +template +void zeropad2d_cl(data_T data[CONFIG_T::n_chan * CONFIG_T::in_height * CONFIG_T::in_width], + res_T res[CONFIG_T::n_chan * CONFIG_T::out_height * CONFIG_T::out_width]) { + //#pragma HLS PIPELINE + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_top; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::out_width; j++) { + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + *(res++) = 0; + } + } + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_height; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::pad_left; j++) { + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + *(res++) = 0; + } + } + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::in_width; j++) { + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + *(res++) = (res_T) * (data++); + } + } + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::pad_right; j++) { + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + *(res++) = 0; + } + } + } + + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::pad_bottom; i++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::out_width; j++) { + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + *(res++) = 0; + } + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_padding_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_padding_stream.h new file mode 100644 index 0000000000..413d91bba6 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_padding_stream.h @@ -0,0 +1,87 @@ +#ifndef NNET_PADDING_STREAM_H_ +#define NNET_PADDING_STREAM_H_ + +#include + +namespace nnet { + +template void fill_zero(hls::stream &res) { + #pragma HLS inline + res_T res_part; + #pragma clang loop unroll(full) + for (int c = 0; c < CONFIG_T::n_chan; c++) { + //#pragma HLS UNROLL + res_part[c] = 0; + } + res.write(res_part); +} + +template void fill_data(hls::stream &data, hls::stream &res) { + #pragma HLS inline + data_T data_part = data.read(); + res_T res_part; + #pragma clang loop unroll(full) + for (int c = 0; c < CONFIG_T::n_chan; c++) { + //#pragma HLS UNROLL + res_part[c] = data_part[c]; + } + res.write(res_part); +} + +template +void zeropad1d_cl(hls::stream &data, hls::stream &res) { +PadLeft: + for (int i = 0; i < CONFIG_T::pad_left; i++) { + fill_zero(res); + } + +CopyMain: + for (int i = 0; i < CONFIG_T::in_width; i++) { + fill_data(data, res); + } + +PadRight: + for (int i = 0; i < CONFIG_T::pad_right; i++) { + fill_zero(res); + } +} + +template +void zeropad2d_cl(hls::stream &data, hls::stream &res) { + +PadTop: + for (int i = 0; i < CONFIG_T::pad_top; i++) { + PadTopWidth: + for (int j = 0; j < CONFIG_T::out_width; j++) { + fill_zero(res); + } + } + +PadMain: + for (int i = 0; i < CONFIG_T::in_height; i++) { + PadLeft: + for (int j = 0; j < CONFIG_T::pad_left; j++) { + fill_zero(res); + } + CopyMain: + for (int j = 0; j < CONFIG_T::in_width; j++) { + fill_data(data, res); + } + PadRight: + for (int j = 0; j < CONFIG_T::pad_right; j++) { + fill_zero(res); + } + } + +PadBottom: + for (int i = 0; i < CONFIG_T::pad_bottom; i++) { + PadBottomWidth: + for (int j = 0; j < CONFIG_T::out_width; j++) { + fill_zero(res); + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_pooling.h b/hls4ml/templates/bambu/nnet_utils/nnet_pooling.h new file mode 100644 index 0000000000..845187c539 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_pooling.h @@ -0,0 +1,316 @@ +#ifndef NNET_POOLING_H_ +#define NNET_POOLING_H_ + +#include "nnet_helpers.h" +#include + +namespace nnet { + +// Return the maximum value from an array +template accum_t max(T x[N]) { + T y = x[0]; + for (int i = 1; i < N; i++) { + y = x[i] > y ? x[i] : y; + } + return y; +} + +// Return the mean value of an array +template accum_t avg(T (&x)[N], unsigned length) { + accum_t y = 0; + for (int i = 0; i < N; i++) { + y += x[i]; + } + y /= length; + return y; +} + +// Enumeration for pooling operation (max, avg, l2norm pooling) +enum Pool_Op { Max, Average }; // L2Norm }; +template accum_t pool_op(T (&x)[N], unsigned length) { + switch (op) { + case Max: + return max(x); + case Average: + return avg(x, length); + // case L2Norm: return l2norm(x); + } +} + +template accum_t pool_op(T (&x)[N]) { + return pool_op(x, N); +} + +template T pad_val() { + /*--- + *- In Tensorflow, pooling ignores the value in the padded cells + *- For Avg pooling, return 0 (the divisior is modified to the + *- area overlapping the unpadded image. + *- For max pooling, return the most negative value for the type. + *- TODO this is not really generic, it assumes fixed point or integer T + ---*/ + switch (op) { + case Max: { + T x = 0; + x[x.width - 1] = 1; + return x; + break; + } + case Average: + return 0; + } +} + +struct pooling1d_config { + // IO size + static const unsigned n_in = 10; + static const unsigned pool_width = 2; + static const unsigned stride_width = 2; + static const unsigned n_out = (n_in - pool_width) / stride_width + 1; + static const unsigned pad_left = 0; + static const unsigned pad_right = 0; + static const bool count_pad = false; + // Pooling function + static const Pool_Op pool_op = Max; +}; + +template constexpr int pool_op_limit_1d() { + return CONFIG_T::n_in * CONFIG_T::n_filt / CONFIG_T::reuse_factor; +} + +template +void pooling1d_cl(data_T data[CONFIG_T::n_in * CONFIG_T::n_filt], res_T res[CONFIG_T::n_out * CONFIG_T::n_filt]) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + // TODO partition the arrays according to the reuse factor + const int limit = pool_op_limit_1d(); + //#pragma HLS ALLOCATION function instances=CONFIG_T::pool_op limit=limit + // Add any necessary padding + + // Add padding and reduce input width to area covered by pooling function + static constexpr int full_padded_width = CONFIG_T::n_in + CONFIG_T::pad_left + CONFIG_T::pad_right; + static constexpr int restricted_padded_width = + (full_padded_width - CONFIG_T::pool_width) / CONFIG_T::stride_width * CONFIG_T::stride_width + 1; + + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + // Loop over input image x in steps of stride + for (int ii = 0; ii < restricted_padded_width; ii += CONFIG_T::stride_width) { + unsigned overlap_pixel = 0; + data_T pool[CONFIG_T::pool_width]; + #pragma HLS ARRAY_PARTITION variable=pool complete dim=0 + + for (int jj = 0; jj < CONFIG_T::pool_width; jj++) { + if (ii + jj >= CONFIG_T::pad_left && ii + jj < CONFIG_T::n_in + CONFIG_T::pad_left) { + pool[jj] = data[(ii + jj - CONFIG_T::pad_left) * CONFIG_T::n_filt + ff]; + overlap_pixel++; + } else + pool[jj] = pad_val(); + } + + int patch_size = CONFIG_T::count_pad ? CONFIG_T::pool_width : overlap_pixel; + + res[(ii / CONFIG_T::stride_width) * CONFIG_T::n_filt + ff] = + pool_op(pool, patch_size); + } + } +} + +template +void global_pooling1d_cl(data_T data[CONFIG_T::n_in * CONFIG_T::n_filt], res_T res[CONFIG_T::n_filt]) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::pool_width == CONFIG_T::stride_width); + + // TODO partition the arrays according to the reuse factor + const int limit = pool_op_limit_1d(); + //#pragma HLS ALLOCATION function instances=CONFIG_T::pool_op limit=limit + + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + data_T pool[CONFIG_T::n_in]; + #pragma HLS ARRAY_PARTITION variable=pool complete dim=0 + for (int jj = 0; jj < CONFIG_T::n_in; jj++) { + pool[jj] = data[jj * CONFIG_T::n_filt + ff]; + } + // do the pooling + res[ff] = pool_op(pool); + } +} + +struct pooling2d_config { + // IO size + static const unsigned in_height = 10; + static const unsigned in_width = 10; + static const unsigned n_filt = 4; + static const unsigned stride_height = 2; + static const unsigned stride_width = 2; + static const unsigned pool_height = 2; + static const unsigned pool_width = 2; + static const unsigned out_height = (in_height - pool_height) / stride_height + 1; + static const unsigned out_width = (in_width - pool_width) / stride_width + 1; + // Padding + static const unsigned pad_top = 0; + static const unsigned pad_bottom = 0; + static const unsigned pad_left = 0; + static const unsigned pad_right = 0; + static const bool count_pad = false; + // Pooling function + static const Pool_Op pool_op = Max; + // Reuse factor + static const unsigned reuse_factor = 1; + + // Internal data type definitions + typedef float accum_t; +}; + +template constexpr int pool_op_limit() { + return (CONFIG_T::out_height * CONFIG_T::out_width) * CONFIG_T::n_filt / CONFIG_T::reuse_factor; +} + +template +void pooling2d_cl(data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_filt], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt]) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + // TODO partition the arrays according to the reuse factor + const int limit = pool_op_limit(); + //#pragma HLS ALLOCATION function instances=CONFIG_T::pool_op limit=limit + + // Add padding and reduce input width to area covered by pooling function + static constexpr int full_padded_width = CONFIG_T::in_width + CONFIG_T::pad_left + CONFIG_T::pad_right; + static constexpr int full_padded_height = CONFIG_T::in_height + CONFIG_T::pad_top + CONFIG_T::pad_bottom; + static constexpr int restricted_padded_width = + (full_padded_width - CONFIG_T::pool_width) / CONFIG_T::stride_width * CONFIG_T::stride_width + 1; + static constexpr int restricted_padded_height = + (full_padded_height - CONFIG_T::pool_height) / CONFIG_T::stride_height * CONFIG_T::stride_height + 1; + + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + + // Loop over input image y in steps of stride + for (int ii = 0; ii < restricted_padded_height; ii += CONFIG_T::stride_height) { + // Loop over input image x in steps of stride + for (int jj = 0; jj < restricted_padded_width; jj += CONFIG_T::stride_width) { + data_T pool[CONFIG_T::pool_height * CONFIG_T::pool_width]; + #pragma HLS ARRAY_PARTITION variable=pool complete dim=0 + + unsigned overlap_pixel = 0; + + // Loop over pool window y + for (int kk = 0; kk < CONFIG_T::pool_height; kk++) { + // Loop over pool window x + for (int ll = 0; ll < CONFIG_T::pool_width; ll++) { + bool cond1 = ii + kk >= CONFIG_T::pad_top && ii + kk < CONFIG_T::in_height + CONFIG_T::pad_top; + bool cond2 = jj + ll >= CONFIG_T::pad_left && jj + ll < CONFIG_T::in_width + CONFIG_T::pad_left; + if (cond1 && cond2) { + unsigned data_idx = + ((ii + kk - CONFIG_T::pad_top) * CONFIG_T::in_width + (jj + ll - CONFIG_T::pad_left)) * + CONFIG_T::n_filt + + ff; + pool[kk * CONFIG_T::pool_width + ll] = data[data_idx]; + overlap_pixel++; + } else + pool[kk * CONFIG_T::pool_width + ll] = pad_val(); + } + } + + int patch_size = CONFIG_T::count_pad ? CONFIG_T::pool_width * CONFIG_T::pool_height : overlap_pixel; + + res[(ii / CONFIG_T::stride_height) * CONFIG_T::out_width * CONFIG_T::n_filt + + (jj / CONFIG_T::stride_width) * CONFIG_T::n_filt + ff] = + pool_op(pool, patch_size); + } + } + } +} + +template +void pooling2d_cf(data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_filt], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt]) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + // TODO partition the arrays according to the reuse factor + const int limit = pool_op_limit(); + //#pragma HLS ALLOCATION function instances=CONFIG_T::pool_op limit=limit + // Add padding and reduce input width to area covered by pooling function + static constexpr int full_padded_width = CONFIG_T::in_width + CONFIG_T::pad_left + CONFIG_T::pad_right; + static constexpr int full_padded_height = CONFIG_T::in_height + CONFIG_T::pad_top + CONFIG_T::pad_bottom; + static constexpr int restricted_padded_width = full_padded_width / CONFIG_T::stride_width * CONFIG_T::stride_width; + static constexpr int restricted_padded_height = full_padded_height / CONFIG_T::stride_height * CONFIG_T::stride_height; + + for (int ff = 0; ff < CONFIG_T::n_filt; ff++) { + // Loop over input image y in steps of stride + for (int ii = 0; ii < restricted_padded_height; ii += CONFIG_T::stride_height) { + // Loop over input image x in steps of stride + for (int jj = 0; jj < restricted_padded_width; jj += CONFIG_T::stride_width) { + data_T pool[CONFIG_T::pool_height * CONFIG_T::pool_width]; + #pragma HLS ARRAY_PARTITION variable=pool complete dim=0 + // Keep track of number of pixels in image vs padding region + unsigned img_overlap = 0; + // Loop over pool window y + for (int kk = 0; kk < CONFIG_T::stride_height; kk++) { + // Loop over pool window x + for (int ll = 0; ll < CONFIG_T::stride_width; ll++) { + if (ii + kk < CONFIG_T::pad_top || ii + kk >= (full_padded_height - CONFIG_T::pad_bottom) || + jj + ll < CONFIG_T::pad_left || jj + ll >= (full_padded_width - CONFIG_T::pad_right)) { + // Add padding + pool[kk * CONFIG_T::stride_width + ll] = pad_val(); + if (CONFIG_T::count_pad) + img_overlap++; + } else { + pool[kk * CONFIG_T::stride_width + ll] = + data[(ii + kk - CONFIG_T::pad_top) * CONFIG_T::in_width + + ff * CONFIG_T::in_width * CONFIG_T::in_height + ll + jj - CONFIG_T::pad_left]; + img_overlap++; + } + } + } + // do the pooling + // TODO in the case of average pooling, need to reduce height * width to area of pool window + // not overlapping padding region + res[(ii / CONFIG_T::stride_height) * CONFIG_T::out_width + (jj / CONFIG_T::stride_width) + + ff * CONFIG_T::out_height * CONFIG_T::out_width] = + pool_op(pool); + // If the pool op is Average, the zero-padding needs to be removed from the results + if (CONFIG_T::pool_op == Average) { + data_T rescale = + static_cast(CONFIG_T::pool_height) * static_cast(CONFIG_T::pool_width) / img_overlap; + res[(ii / CONFIG_T::stride_height) * CONFIG_T::out_width + (jj / CONFIG_T::stride_width) + + ff * CONFIG_T::out_height * CONFIG_T::out_width] *= rescale; + } + } + } + } +} + +template +void global_pooling2d_cl(data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_filt], + res_T res[CONFIG_T::n_filt]) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0); + assert(CONFIG_T::pool_width == CONFIG_T::stride_width); + assert(CONFIG_T::pool_height == CONFIG_T::stride_height); + + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + + const int limit = pool_op_limit(); + //#pragma HLS ALLOCATION instances=pool_op limit=limit function + +FiltLoop: + for (int filt = 0; filt < CONFIG_T::n_filt; filt++) { + data_T pool[CONFIG_T::in_height * CONFIG_T::in_width]; + + InputLoop: + for (int i = 0; i < CONFIG_T::in_height * CONFIG_T::in_width; i++) { + pool[i] = data[i * CONFIG_T::n_filt + filt]; + } + + res[filt] = static_cast( + pool_op(pool)); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_pooling_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_pooling_stream.h new file mode 100644 index 0000000000..a989acb4bd --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_pooling_stream.h @@ -0,0 +1,618 @@ +#ifndef NNET_POOLING_STREAM_H_ +#define NNET_POOLING_STREAM_H_ + +#include "ap_shift_reg.h" +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_conv_stream.h" +#include "nnet_pooling.h" +#include "utils/x_hls_utils.h" + +namespace nnet { + +// ************************************************* +// Max/average pooling +// ************************************************* + +template T reduce_pool(T x[N]) { + #pragma HLS inline + if (CONFIG_T::pool_op == Max) { + Op_max op_max; + return reduce>(x, op_max); + } else { + Op_add op_add; + T sum = reduce>(x, op_add); + return sum / N; + } +} + +template void init_pool_table(unsigned table[TABLE_SIZE]) { + for (unsigned ii = 0; ii < TABLE_SIZE; ii++) { + table[ii] = ii % POOL_SIZE; + } +} + +template +void compute_pool_encoded_2d( + const unsigned h_idx, const unsigned w_idx, const data_T &in_elem, + hls::stream data_window[CONFIG_T::pool_height * CONFIG_T::pool_width * CONFIG_T::n_filt], + hls::stream &res, res_T &res_pack, unsigned &outputs_ready) { + // Nearest H without unused pixels on the right + constexpr unsigned nH = + ((CONFIG_T::in_height - CONFIG_T::pool_height) / CONFIG_T::stride_height) * CONFIG_T::stride_height + + CONFIG_T::pool_height; + // Scaled H that behaves like original H + constexpr unsigned sH = + (DIV_ROUNDUP(CONFIG_T::pool_height, CONFIG_T::stride_height) - 1) * CONFIG_T::stride_height + CONFIG_T::pool_height; + // Nearest W without unused pixels on the right + constexpr unsigned nW = ((CONFIG_T::in_width - CONFIG_T::pool_width) / CONFIG_T::stride_width) * CONFIG_T::stride_width + + CONFIG_T::pool_width; + // Scaled W that behaves like original W + constexpr unsigned sW = + (DIV_ROUNDUP(CONFIG_T::pool_width, CONFIG_T::stride_width) - 1) * CONFIG_T::stride_width + CONFIG_T::pool_width; + +#ifdef __SYNTHESIS__ + bool initialized = false; + unsigned pool_table_height[CONFIG_T::in_height]; + unsigned pool_table_width[CONFIG_T::in_width]; +#else + static bool initialized = false; + static unsigned pool_table_height[CONFIG_T::in_height]; + static unsigned pool_table_width[CONFIG_T::in_width]; +#endif + if (!initialized) { + init_pool_table(pool_table_height); + init_pool_table(pool_table_width); + initialized = true; + } + + #pragma HLS inline + + if (data_T::size / CONFIG_T::n_filt > 1) { + #pragma HLS ARRAY_PARTITION variable=pool_table_height complete + #pragma HLS ARRAY_PARTITION variable=pool_table_width complete + } + + typename CONFIG_T::accum_t pool_window[CONFIG_T::pool_height * CONFIG_T::pool_width]; + #pragma HLS ARRAY_PARTITION variable=pool_window complete + + const unsigned sh_idx = pool_table_height[h_idx] * CONFIG_T::pool_width; + const unsigned wp_idx = w_idx * (data_T::size / CONFIG_T::n_filt); + +PixelLoop: + for (unsigned p = 0; p < data_T::size / CONFIG_T::n_filt; p++) { + //#pragma HLS PIPELINE + + ap_uint filt_mask = 0; + if ((h_idx < nH) && (wp_idx + p < nW)) { + filt_mask = sh_idx + pool_table_width[wp_idx + p] + 1; + } + + CopyDataFilt: + for (unsigned c = 0; c < CONFIG_T::n_filt; c++) { + if (filt_mask > 0) + data_window[c * CONFIG_T::pool_height * CONFIG_T::pool_width + filt_mask.to_uint() - 1].write( + in_elem[p * CONFIG_T::n_filt + c]); + } + + if (filt_mask == CONFIG_T::pool_height * CONFIG_T::pool_width) { + FiltLoop: + for (unsigned c = 0; c < CONFIG_T::n_filt; c++) { + PoolLoop: + for (unsigned f = 0; f < CONFIG_T::pool_height * CONFIG_T::pool_width; f++) { + pool_window[f] = data_window[c * CONFIG_T::pool_height * CONFIG_T::pool_width + f].read(); + } + if (res_T::size / CONFIG_T::n_filt == + 1) { // Saves resources if we don't pack output, compiler will remove the else branch + res_pack[c] = + reduce_pool( + pool_window); + } else { + res_pack[outputs_ready * CONFIG_T::n_filt + c] = + reduce_pool( + pool_window); + } + } + if (res_T::size / CONFIG_T::n_filt == + 1) { // Saves resources if we don't pack output, compiler will remove the else branch + res.write(res_pack); + } else { + if (outputs_ready == (res_T::size / CONFIG_T::n_filt) - 1) { + res.write(res_pack); + outputs_ready = 0; + } else { + outputs_ready++; + } + } + } + } +} + +template +void pooling2d_encoded_cl(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::pool_height == CONFIG_T::stride_height && CONFIG_T::pool_width == CONFIG_T::stride_width); + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + unsigned outputs_ready = 0; + + hls::stream data_window[CONFIG_T::pool_height * CONFIG_T::pool_width * CONFIG_T::n_filt]; + constexpr int win_depth = CONFIG_T::pool_height * CONFIG_T::out_width; + for (unsigned i_out = 0; i_out < CONFIG_T::pool_height * CONFIG_T::pool_width * CONFIG_T::n_filt; i_out++) { + //#pragma HLS STREAM variable=data_window[i_out] depth=win_depth + } + + constexpr int pack_factor = data_T::size / CONFIG_T::n_filt; + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (pack_factor); i_iw++) { + //#pragma HLS LOOP_FLATTEN + if (res_T::size / CONFIG_T::n_filt == 1) { + //#pragma HLS PIPELINE II=pack_factor + } + compute_pool_encoded_2d(i_ih, i_iw, data.read(), data_window, res, res_pack, + outputs_ready); + } + } +} + +// ************************************************* +// Line Buffer Implementation (Phil's) +// ************************************************* +template +void compute_pool_buffer_2d(const data_T &in_elem, + ap_shift_reg + line_buffer[MAX(CONFIG_T::pool_height - 1, 1)][CONFIG_T::n_filt], + hls::stream &res) { + #pragma HLS inline + const static int lShiftX = CONFIG_T::pool_width - 1; + const static int lShiftY = CONFIG_T::pool_height - 1; + static int pX = 0; // pixel X + static int pY = 0; // pixel Y + static int sX = 0; // stride X + static int sY = 0; // stride Y + + typename CONFIG_T::accum_t pool_window[CONFIG_T::pool_height * CONFIG_T::pool_width]; + #pragma HLS ARRAY_PARTITION variable=pool_window complete + + static typename data_T::value_type kernel_data[CONFIG_T::pool_height * CONFIG_T::pool_width * CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable = kernel_data complete dim = 0 + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + + // Add pixel into line buffer, return pooling kernels + nnet::shift_line_buffer(in_elem, line_buffer, kernel_data); + + // Can compute pooling output + if ((sX - lShiftX) == 0 && (sY - lShiftY) == 0 && pY > lShiftY - 1 && pX > lShiftX - 1) { + FiltLoop: + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_filt; i_ic++) { + //#pragma HLS PIPELINE + + // Retrieve data for current channel + PoolLoop: + for (unsigned i_ihw = 0; i_ihw < CONFIG_T::pool_height * CONFIG_T::pool_width; i_ihw++) { + pool_window[i_ihw] = kernel_data[i_ihw * CONFIG_T::n_filt + i_ic]; + } + + // Compute Pooling + res_pack[i_ic] = + reduce_pool(pool_window); + } + + // Write to output + res.write(res_pack); + } + + // Counter Housekeeping + if (pX + 1 == CONFIG_T::in_width) // Includes padding, end of line (padded) + { + pX = 0; + sX = 0; + if (pY + 1 == CONFIG_T::in_height) { // Reached bottom of image + pY = 0; + sY = 0; + } else { // Next line + pY = pY + 1; + // Update stride (threshold) ? subtract stride : increment stride + sY = ((sY - lShiftY) == 0) ? sY - CONFIG_T::stride_height + 1 : sY + 1; + } + } else { + pX = pX + 1; + // Update stride (threshold) ? subtract stride : increment stride + sX = ((sX - lShiftX) == 0) ? sX - CONFIG_T::stride_width + 1 : sX + 1; + } +} + +template +void pooling2d_buffer_cl(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::pool_height == CONFIG_T::stride_height && CONFIG_T::pool_width == CONFIG_T::stride_width); + + static ap_shift_reg line_buffer[MAX(CONFIG_T::pool_height - 1, 1)] + [CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable = line_buffer complete dim = 2 + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width; i_iw++) { + //#pragma HLS LOOP_FLATTEN + //#pragma HLS PIPELINE + + compute_pool_buffer_2d(data.read(), line_buffer, res); + } + } +} + +template +void pooling2d_cl(hls::stream &data, hls::stream &res) { + #pragma HLS inline recursive + switch (CONFIG_T::implementation) { + case conv_implementation::linebuffer: + pooling2d_buffer_cl(data, res); + break; + case conv_implementation::encoded: + pooling2d_encoded_cl(data, res); + break; + } +} + +// ************************************************* +// Pooling 1D +// ************************************************* + +template +void compute_pool_encoded_1d(const unsigned w_idx, const data_T &in_elem, + hls::stream data_window[CONFIG_T::pool_width * CONFIG_T::n_filt], + hls::stream &res, res_T &res_pack, unsigned &outputs_ready) { + // Nearest W without unused pixels on the right + constexpr unsigned nW = + ((CONFIG_T::n_in - CONFIG_T::pool_width) / CONFIG_T::stride_width) * CONFIG_T::stride_width + CONFIG_T::pool_width; + // Scaled W that behaves like original W + constexpr unsigned sW = + (DIV_ROUNDUP(CONFIG_T::pool_width, CONFIG_T::stride_width) - 1) * CONFIG_T::stride_width + CONFIG_T::pool_width; + +#ifdef __BAMBU__ + bool initialized = false; + unsigned pool_table_width[CONFIG_T::n_in]; +#else + static bool initialized = false; + static unsigned pool_table_width[CONFIG_T::n_in]; +#endif + if (!initialized) { + init_pool_table(pool_table_width); + initialized = true; + } + + #pragma HLS inline + + if (data_T::size / CONFIG_T::n_filt > 1) { + #pragma HLS ARRAY_PARTITION variable=pool_table_width complete + } + + typename CONFIG_T::accum_t pool_window[CONFIG_T::pool_width]; + #pragma HLS ARRAY_PARTITION variable=pool_window complete + + const unsigned wp_idx = w_idx * (data_T::size / CONFIG_T::n_filt); + +PixelLoop: + for (unsigned p = 0; p < data_T::size / CONFIG_T::n_filt; p++) { + //#pragma HLS PIPELINE + + ap_uint filt_mask = 0; + if (wp_idx + p < nW) { + filt_mask = pool_table_width[wp_idx + p] + 1; + } + + CopyDataFilt: + for (unsigned c = 0; c < CONFIG_T::n_filt; c++) { + if (filt_mask > 0) + data_window[c * CONFIG_T::pool_width + filt_mask.to_uint() - 1].write(in_elem[p * CONFIG_T::n_filt + c]); + } + + if (filt_mask == CONFIG_T::pool_width) { + FiltLoop: + for (unsigned c = 0; c < CONFIG_T::n_filt; c++) { + PoolLoop: + for (unsigned f = 0; f < CONFIG_T::pool_width; f++) { + pool_window[f] = data_window[c * CONFIG_T::pool_width + f].read(); + } + if (res_T::size / CONFIG_T::n_filt == + 1) { // Saves resources if we don't pack output, compiler will remove the else branch + res_pack[c] = reduce_pool(pool_window); + } else { + res_pack[outputs_ready * CONFIG_T::n_filt + c] = + reduce_pool(pool_window); + } + } + if (res_T::size / CONFIG_T::n_filt == + 1) { // Saves resources if we don't pack output, compiler will remove the else branch + res.write(res_pack); + } else { + if (outputs_ready == (res_T::size / CONFIG_T::n_filt) - 1) { + res.write(res_pack); + outputs_ready = 0; + } else { + outputs_ready++; + } + } + } + } +} + +template +void pooling1d_encoded_cl(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::pool_width == CONFIG_T::stride_width); + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + unsigned outputs_ready = 0; + + hls::stream data_window[CONFIG_T::pool_width * CONFIG_T::n_filt]; + constexpr int win_depth = CONFIG_T::n_out; + for (unsigned i_out = 0; i_out < CONFIG_T::pool_width * CONFIG_T::n_filt; i_out++) { + //#pragma HLS STREAM variable=data_window[i_out] depth=win_depth + } + + constexpr int pack_factor = data_T::size / CONFIG_T::n_filt; + +ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::n_in / (pack_factor); i_iw++) { + //#pragma HLS LOOP_FLATTEN + if (res_T::size / CONFIG_T::n_filt == 1) { + //#pragma HLS PIPELINE II=pack_factor + } + compute_pool_encoded_1d(i_iw, data.read(), data_window, res, res_pack, outputs_ready); + } +} + +// ************************************************* +// Line Buffer Implementation (Phil's) 1D +// ************************************************* +template +void compute_pool_buffer_1d(const data_T &in_elem, hls::stream &res) { + #pragma HLS inline + const static int lShiftX = CONFIG_T::pool_width - 1; + // Counters + static int pX = 0; + static int sX = 0; + + typename CONFIG_T::accum_t pool_window[CONFIG_T::pool_width]; + #pragma HLS ARRAY_PARTITION variable=pool_window complete + + static typename data_T::value_type kernel_data[CONFIG_T::pool_width * CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable = kernel_data complete dim = 0 + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + + // Add pixel into line buffer, return pooling kernels + // 1D case line buffer not necessary. Put directly into the kernel_data buffer + nnet::kernel_shift_1d(in_elem, kernel_data); + + // Can compute pooling output + if ((sX - lShiftX) == 0 && pX > lShiftX - 1) { + FiltLoop: + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_filt; i_ic++) { + //#pragma HLS PIPELINE + + // Retrieve data for current channel + PoolLoop: + for (unsigned i_iw = 0; i_iw < CONFIG_T::pool_width; i_iw++) { + pool_window[i_iw] = kernel_data[i_iw * CONFIG_T::n_filt + i_ic]; + } + + // Compute Pooling + res_pack[i_ic] = reduce_pool(pool_window); + } + + // Write to output + res.write(res_pack); + } + + // Counter Housekeeping + if (pX + 1 == CONFIG_T::n_in) // Includes padding, end of line (padded) + { + pX = 0; + sX = 0; + } else { + pX = pX + 1; + // Update stride (threshold) ? subtract stride : increment stride + sX = ((sX - lShiftX) == 0) ? sX - CONFIG_T::stride_width + 1 : sX + 1; + } +} + +template +void pooling1d_buffer_cl(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + +ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::n_in; i_iw++) { + //#pragma HLS LOOP_FLATTEN + //#pragma HLS PIPELINE + compute_pool_buffer_1d(data.read(), res); + } +} + +template +void pooling1d_cl(hls::stream &data, hls::stream &res) { + #pragma HLS inline recursive + switch (CONFIG_T::implementation) { + case conv_implementation::linebuffer: + pooling1d_buffer_cl(data, res); + break; + case conv_implementation::encoded: + pooling1d_encoded_cl(data, res); + break; + } +} + +// ************************************************* +// Global max/average pooling +// ************************************************* + +template T reduce_global_pool(T x, T y[N]) { + #pragma HLS inline + if (CONFIG_T::pool_op == Max) { + Op_max op_max; + T y_max = reduce>(y, op_max); + return (x > y_max) ? x : y_max; + } else { + Op_add op_add; + T y_sum = reduce>(y, op_add); + return x + y_sum; + } +} + +template +void compute_global_pool(const data_T &in_elem, typename CONFIG_T::accum_t data_window[CONFIG_T::n_filt]) { +PoolFilt: + #pragma clang loop unroll(full) + for (unsigned c = 0; c < CONFIG_T::n_filt; c++) { + //#pragma HLS UNROLL + + typename CONFIG_T::accum_t data_pack[data_T::size / CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable=data_pack complete dim=0 + + PixelLoop: + #pragma clang loop unroll(full) + for (unsigned p = 0; p < data_T::size / CONFIG_T::n_filt; p++) { + //#pragma HLS UNROLL + data_pack[p] = in_elem[p * CONFIG_T::n_filt + c]; + } + data_window[c] = reduce_global_pool( + data_window[c], data_pack); + } +} + +template +void global_pooling2d_cl(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::pool_height == CONFIG_T::stride_height && CONFIG_T::pool_width == CONFIG_T::stride_width); + + typename CONFIG_T::accum_t data_window[CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable=data_window complete + + typename CONFIG_T::accum_t init = 0; + if (CONFIG_T::pool_op == Max) { + init = hls::numeric_limits::min(); + } + +PoolInitLoop: + #pragma clang loop unroll(full) + for (unsigned i_init = 0; i_init < CONFIG_T::n_filt; i_init++) { + //#pragma HLS UNROLL + data_window[i_init] = init; + } + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + #pragma clang loop unroll(full) + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (data_T::size / CONFIG_T::n_filt); i_iw++) { + //#pragma HLS LOOP_FLATTEN + compute_global_pool(data.read(), data_window); + } + } + + if (CONFIG_T::pool_op == Max) { + MaxPoolRes: + for (unsigned i_res = 0; i_res < CONFIG_T::n_filt / res_T::size; i_res++) { + //#pragma HLS PIPELINE + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + MaxPoolPack: + #pragma clang loop unroll(full) + for (unsigned i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = data_window[i_pack]; + } + res.write(res_pack); + } + } else { + AvgPoolRes: + for (unsigned i_res = 0; i_res < CONFIG_T::n_filt / res_T::size; i_res++) { + //#pragma HLS PIPELINE + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + AvgPoolPack: + #pragma clang loop unroll(full) + for (unsigned i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = data_window[i_pack] / (CONFIG_T::in_height * CONFIG_T::in_width); + } + res.write(res_pack); + } + } +} + +template +void global_pooling1d_cl(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::pool_width == CONFIG_T::stride_width); + + typename CONFIG_T::accum_t data_window[CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable=data_window complete + + typename CONFIG_T::accum_t init = 0; + if (CONFIG_T::pool_op == Max) { + init = hls::numeric_limits::min(); + } + +PoolInitLoop: + #pragma clang loop unroll(full) + for (unsigned i_init = 0; i_init < CONFIG_T::n_filt; i_init++) { + //#pragma HLS UNROLL + data_window[i_init] = init; + } + +ReadInput: + for (unsigned i_iw = 0; i_iw < CONFIG_T::n_in / (data_T::size / CONFIG_T::n_filt); i_iw++) { + //#pragma HLS LOOP_FLATTEN + compute_global_pool(data.read(), data_window); + } + + if (CONFIG_T::pool_op == Max) { + MaxPoolRes: + for (unsigned i_res = 0; i_res < CONFIG_T::n_filt / res_T::size; i_res++) { + //#pragma HLS PIPELINE + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + MaxPoolPack: + #pragma clang loop unroll(full) + for (unsigned i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = data_window[i_pack]; + } + res.write(res_pack); + } + } else { + AvgPoolRes: + for (unsigned i_res = 0; i_res < CONFIG_T::n_filt / res_T::size; i_res++) { + //#pragma HLS PIPELINE + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + AvgPoolPack: + #pragma clang loop unroll(full) + for (unsigned i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = data_window[i_pack] / CONFIG_T::n_in; + } + res.write(res_pack); + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_recr_activations.h b/hls4ml/templates/bambu/nnet_utils/nnet_recr_activations.h new file mode 100644 index 0000000000..f68d80663b --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_recr_activations.h @@ -0,0 +1,56 @@ +#ifndef NNET_RECR_ACTIVATION_H_ +#define NNET_RECR_ACTIVATION_H_ + +#include "hls_stream.h" +#include "nnet_activation.h" +#include "nnet_common.h" +#include "nnet_helpers.h" +#include + +namespace nnet { + +namespace activation { + +template class Activation { + public: + // ************************************************* + // Blank Activation + // ************************************************* + static void activation(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) {} // Nothing to do here +}; + +template class relu : public Activation { + public: + // ************************************************* + // Relu Activation + // ************************************************* + static void activation(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + nnet::relu(data, res); + } +}; + +template class sigmoid : public Activation { + public: + // ************************************************* + // Sigmoid Activation + // ************************************************* + static void activation(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + nnet::sigmoid(data, res); + } +}; + +template class tanh : public Activation { + public: + // ************************************************* + // TanH Activation + // ************************************************* + static void activation(data_T data[CONFIG_T::n_in], res_T res[CONFIG_T::n_in]) { + nnet::tanh(data, res); + } +}; + +} // namespace activation + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_recurrent.h b/hls4ml/templates/bambu/nnet_utils/nnet_recurrent.h new file mode 100644 index 0000000000..c98cba3ec2 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_recurrent.h @@ -0,0 +1,881 @@ +#ifndef NNET_RECURSIVE_H_ +#define NNET_RECURSIVE_H_ + +#include "hls_stream.h" +#include "nnet_activation.h" +#include "nnet_common.h" +#include "nnet_dense.h" +#include "nnet_recr_activations.h" + +namespace nnet { + +// Struct for the LSTM template + +struct lstm_config { + // Internal data type definitions + typedef float weight_t; + typedef float recurrent_weight_t; + typedef float bias_t; + typedef float recurrent_bias_t; + typedef float accum_t; + + // Layer Sizes + static const unsigned n_in = 2; + static const unsigned n_parts = 20; + static const unsigned n_out = 2; + static const unsigned n_state = 2; + static const unsigned n_4state = 8; + static const unsigned table_size = 1024; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; + static const unsigned n_zeros = 0; + static const bool store_weights_in_bram = false; + static const bool use_static = true; + + template using activation_recr = nnet::activation::relu; + template using activation = nnet::activation::relu; +}; + +// Long Short term Memory NN (LSTM) +// Resources: +// https://github.com/nicodjimenez/lstm/blob/master/lstm.py +// https://github.com/llSourcell/LSTM_Networks/blob/master/LSTM%20Demo.ipynb +// https://en.wikipedia.org/wiki/Long_short-term_memory +// Notes: +// - LSTM naming conventions adopted from the above links +// - s_newstate = activation(U*input + W*state) +// - h_output = activation(U*input + W*state)*activation(s_newstate) +// - If softmax is needed on output, perform *outside* this operations +// Originall had a version allows for the state in each layer to be saved, moved this to above (this requires are LARGE +// dense network at the end) +template +void lstm(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_newstate[CONFIG_T::n_state], + res_T s_newstate[CONFIG_T::n_state], typename CONFIG_T::weight_t param[CONFIG_T::n_state * 4 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_r[CONFIG_T::n_state * 4 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 4], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 4]) { + // Initialize the state variable -- will maintain state between function calls + + typename CONFIG_T::accum_t tmpres[CONFIG_T::n_state * 4]; + typename CONFIG_T::accum_t tmpres_state[CONFIG_T::n_state * 4]; + typename CONFIG_T::accum_t tmpres_ifo[CONFIG_T::n_state * 3]; // activated i,f,o matrices (keras notation) + typename CONFIG_T::accum_t tmpres_c[CONFIG_T::n_state]; // activated c-matrix (keras notation) + typename CONFIG_T::accum_t inputacc_ifo[CONFIG_T::n_state * 3]; // i,f,o matrices (keras notation) + typename CONFIG_T::accum_t inputacc_c[CONFIG_T::n_state]; // c-matrix (keras notation) + typename CONFIG_T::accum_t s_actstate[CONFIG_T::n_state]; + + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=s_newstate complete + #pragma HLS ARRAY_PARTITION variable=tmpres complete + #pragma HLS ARRAY_PARTITION variable=tmpres_state complete + #pragma HLS ARRAY_PARTITION variable=tmpres_ifo complete + #pragma HLS ARRAY_PARTITION variable=tmpres_c complete + #pragma HLS ARRAY_PARTITION variable=inputacc_ifo complete + #pragma HLS ARRAY_PARTITION variable=inputacc_c complete + #pragma HLS ARRAY_PARTITION variable=s_actstate complete + + nnet::dense(data, tmpres, param, param_b); + nnet::dense(h_newstate, tmpres_state, param_r, param_br); + + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (3 * CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc; + if (iacc > 2 * CONFIG_T::n_state - 1) + index = iacc + CONFIG_T::n_state; + inputacc_ifo[iacc] = tmpres[index] + tmpres_state[index]; + } + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc + CONFIG_T::n_state * 2; + inputacc_c[iacc] = tmpres[index] + tmpres_state[index]; + } + + CONFIG_T::template activation_recr::activation(inputacc_ifo, tmpres_ifo); + + // Now for the confusion matrix + CONFIG_T::template activation::activation(inputacc_c, tmpres_c); + + // Operation: s=g*i+sold*f (update state with buffer to avoid timing issues) + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + s_newstate[iacc] = tmpres_c[iacc] * tmpres_ifo[iacc] + s_newstate[iacc] * tmpres_ifo[iacc + (CONFIG_T::n_state)]; + } + // Operation: h=act(s)*o + CONFIG_T::template activation::activation( + s_newstate, s_actstate); + + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < CONFIG_T::n_state; iacc++) { + //#pragma HLS UNROLL + h_newstate[iacc] = tmpres_ifo[iacc + 2 * (CONFIG_T::n_state)] * s_actstate[iacc]; + } +} + +template +void lstm_static(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_newstate[CONFIG_T::n_state], + res_T s_newstate[CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 4 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_r[CONFIG_T::n_state * 4 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 4], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 4]) { + static res_T h_state[CONFIG_T::n_state]; + static res_T s_state[CONFIG_T::n_state]; + // Initialize the state variable -- will maintain state between function calls + typename CONFIG_T::accum_t tmpres[CONFIG_T::n_state * 4]; + typename CONFIG_T::accum_t tmpres_state[CONFIG_T::n_state * 4]; + typename CONFIG_T::accum_t tmpres_ifo[CONFIG_T::n_state * 3]; // activated i,f,o matrices (keras notation) + typename CONFIG_T::accum_t tmpres_c[CONFIG_T::n_state]; // activated c-matrix (keras notation) + typename CONFIG_T::accum_t inputacc_ifo[CONFIG_T::n_state * 3]; // i,f,o matrices (keras notation) + typename CONFIG_T::accum_t inputacc_c[CONFIG_T::n_state]; // c-matrix (keras notation) + typename CONFIG_T::accum_t s_actstate[CONFIG_T::n_state]; + + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=s_newstate complete + #pragma HLS ARRAY_PARTITION variable=h_state complete + #pragma HLS ARRAY_PARTITION variable=s_state complete + #pragma HLS ARRAY_PARTITION variable=tmpres complete + #pragma HLS ARRAY_PARTITION variable=tmpres_state complete + #pragma HLS ARRAY_PARTITION variable=tmpres_ifo complete + #pragma HLS ARRAY_PARTITION variable=tmpres_c complete + #pragma HLS ARRAY_PARTITION variable=inputacc_ifo complete + #pragma HLS ARRAY_PARTITION variable=inputacc_c complete + #pragma HLS ARRAY_PARTITION variable=s_actstate complete + + if (reset_state) { + #pragma clang loop unroll(full) + for (int i_state = 0; i_state < (CONFIG_T::n_state); i_state++) { + //#pragma HLS UNROLL + s_state[i_state] = 0; + h_state[i_state] = 0; + } + } + nnet::dense(data, tmpres, param, param_b); + nnet::dense(h_state, tmpres_state, param_r, + param_br); + + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (3 * CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc; + if (iacc > 2 * CONFIG_T::n_state - 1) + index = iacc + CONFIG_T::n_state; + inputacc_ifo[iacc] = tmpres[index] + tmpres_state[index]; + } + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc + CONFIG_T::n_state * 2; + inputacc_c[iacc] = tmpres[index] + tmpres_state[index]; + } + + CONFIG_T::template activation_recr::activation(inputacc_ifo, tmpres_ifo); + + // Now for the confusion matrix + CONFIG_T::template activation::activation(inputacc_c, tmpres_c); + + // Operation: s=g*i+sold*f (update state with buffer to avoid timing issues) + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + s_state[iacc] = tmpres_c[iacc] * tmpres_ifo[iacc] + s_state[iacc] * tmpres_ifo[iacc + (CONFIG_T::n_state)]; + s_newstate[iacc] = s_state[iacc]; + } + // Operation: h=act(s)*o + CONFIG_T::template activation::activation( + s_state, s_actstate); + + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < CONFIG_T::n_state; iacc++) { + //#pragma HLS UNROLL + h_state[iacc] = tmpres_ifo[iacc + 2 * (CONFIG_T::n_state)] * s_actstate[iacc]; + h_newstate[iacc] = h_state[iacc]; + } +} + +template class lstm_class { + public: + static void apply(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_total[2 * CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 4 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_r[CONFIG_T::n_state * 4 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 4], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 4]) { + res_T *h_newstate = h_total; + res_T *s_newstate = h_newstate + CONFIG_T::n_state; + nnet::lstm(reset_state, data, h_newstate, s_newstate, param, param_r, param_b, param_br); + }; +}; + +template class lstm_class_static { + public: + static void apply(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_total[2 * CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 4 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_r[CONFIG_T::n_state * 4 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 4], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 4]) { + res_T *h_newstate = h_total; + res_T *s_newstate = h_newstate + CONFIG_T::n_state; + nnet::lstm_static(reset_state, data, h_newstate, s_newstate, param, param_r, + param_b, param_br); + }; +}; + +template +void lstm_stack(data_T data[CONFIG_T::n_sequence * CONFIG_T::n_in], res_T res[CONFIG_T::n_sequence_out * CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 4 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_r[CONFIG_T::n_state * 4 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 4], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 4]) { + + res_T h_newstate[CONFIG_T::n_state]; + res_T s_newstate[CONFIG_T::n_state]; + data_T data_in[CONFIG_T::n_in]; + bool reset_state = true; + + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=s_newstate complete + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_state; ii++) { + //#pragma HLS UNROLL + h_newstate[ii] = 0; + s_newstate[ii] = 0; + } + for (int iloop = 0; iloop < CONFIG_T::n_sequence; iloop++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_in; j++) { + //#pragma HLS UNROLL + data_in[j] = data[j + iloop * CONFIG_T::n_in]; + } + if (CONFIG_T::use_static) + nnet::lstm_static(reset_state, data_in, h_newstate, s_newstate, param, param_r, param_b, + param_br); + else + nnet::lstm(reset_state, data_in, h_newstate, s_newstate, param, param_r, param_b, + param_br); + if (CONFIG_T::n_sequence_out > 1) + #pragma clang loop unroll(full) + for (int i = CONFIG_T::n_state * iloop, j = 0; i < (CONFIG_T::n_state * (iloop + 1)); i++, j++) { + //#pragma HLS UNROLL + res[i] = h_newstate[j]; + } + reset_state = false; + } + if (CONFIG_T::n_sequence_out == 1) + #pragma clang loop unroll(full) + for (int i = 0; i < (CONFIG_T::n_state); i++) { + //#pragma HLS UNROLL + res[i] = h_newstate[i]; + } +} + +template +void lstm_stack(data_T data[CONFIG_T::n_sequence * CONFIG_T::n_in], h_T h_newstate[CONFIG_T::n_state], + s_T s_newstate[CONFIG_T::n_state], res_T res[CONFIG_T::n_sequence_out * CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 4 * CONFIG_T::n_in], + typename CONFIG_T::weight_t param_r[CONFIG_T::n_state * 4 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 4], + typename CONFIG_T::bias_t param_br[CONFIG_T::n_state * 4]) { + + data_T data_in[CONFIG_T::n_in]; + bool reset_state = false; + + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=s_newstate complete + + for (int iloop = 0; iloop < CONFIG_T::n_sequence; iloop++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_in; j++) { + //#pragma HLS UNROLL + data_in[j] = data[j + iloop * CONFIG_T::n_in]; + } + + nnet::lstm(reset_state, data_in, h_newstate, s_newstate, param, param_r, param_b, param_br); + if (CONFIG_T::n_sequence_out > 1) + #pragma clang loop unroll(full) + for (int i = CONFIG_T::n_state * iloop, j = 0; i < (CONFIG_T::n_state * (iloop + 1)); i++, j++) { + //#pragma HLS UNROLL + res[i] = h_newstate[j]; + } + reset_state = false; + } + if (CONFIG_T::n_sequence_out == 1) + #pragma clang loop unroll(full) + for (int i = 0; i < (CONFIG_T::n_state); i++) { + //#pragma HLS UNROLL + res[i] = h_newstate[i]; + } +} + +template +void lstm_stack(hls::stream &data_stream, hls::stream &res_stream, + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 4 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_r[CONFIG_T::n_state * 4 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 4], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 4]) { + + typename res_T::value_type h_newstate[CONFIG_T::n_state]; + typename res_T::value_type s_newstate[CONFIG_T::n_state]; + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=s_newstate complete + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_state; ii++) { + //#pragma HLS UNROLL + h_newstate[ii] = 0; + s_newstate[ii] = 0; + } + + typename data_T::value_type data_in[CONFIG_T::n_in]; + bool reset_state = true; + +DataPropagation: + for (int i_in = 0; i_in < CONFIG_T::n_sequence * CONFIG_T::n_in / data_T::size; i_in++) { + if (CONFIG_T::n_sequence * CONFIG_T::n_in / data_T::size > 1) { + // #pragma HLS PIPELINE + } + data_T data_pack = data_stream.read(); + DataPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < data_T::size; i_pack++) { + //#pragma HLS UNROLL + data_in[i_pack] = data_pack[i_pack]; + } + if (CONFIG_T::use_static) + nnet::lstm_static( + reset_state, data_in, h_newstate, s_newstate, param, param_r, param_b, param_br); + else + nnet::lstm( + reset_state, data_in, h_newstate, s_newstate, param, param_r, param_b, param_br); + if (CONFIG_T::n_sequence_out > 1) { + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + ResPack_sequences: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = h_newstate[i_pack]; + } + res_stream.write(res_pack); + } + reset_state = false; + } + + if (CONFIG_T::n_sequence_out == 1) { + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + ResPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = h_newstate[i_pack]; + } + res_stream.write(res_pack); + } +} + +// Struct for the GRU template + +struct gru_config { + // Internal data type definitions + typedef float weight_t; + typedef float recurrent_weight_t; + typedef float bias_t; + typedef float recurrent_bias_t; + typedef float accum_t; + + // Layer Sizes + static const unsigned n_in = 2; + static const unsigned n_out = 2; + static const unsigned n_state = 2; + static const unsigned n_sequence = 2; + static const unsigned n_4state = 8; + static const unsigned table_size = 1024; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; + static const bool store_weights_in_bram = false; + static const bool use_static = true; + static const bool pytorch_order = false; + static const unsigned n_zeros = 0; + + template using activation_recr = nnet::activation::relu; + template using activation = nnet::activation::relu; +}; + +template +void gru(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_newstate[CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 3 * CONFIG_T::n_in], // TODO - Check the layout of the param + // weights - refer page in copy!! + typename CONFIG_T::recurrent_weight_t param_zr[CONFIG_T::n_state * 3 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 3], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 3]) { + // Initialize the state variable -- will maintain state between function calls + typename CONFIG_T::accum_t tmpres[CONFIG_T::n_state * 3]; + typename CONFIG_T::accum_t tmpres_state_zr[CONFIG_T::n_state * 3]; + typename CONFIG_T::accum_t tmpres_state_h[CONFIG_T::n_state]; + typename CONFIG_T::accum_t tmpres_zr[CONFIG_T::n_state * 2]; // activated i,f,o matrices (keras notation) + typename CONFIG_T::accum_t tmpres_h[CONFIG_T::n_state]; // activated c-matrix (keras notation) + typename CONFIG_T::accum_t inputacc_zr[CONFIG_T::n_state * 2]; // i,f,o matrices (keras notation) + typename CONFIG_T::accum_t inputacc_h[CONFIG_T::n_state]; // c-matrix (keras notation) + + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=tmpres complete + #pragma HLS ARRAY_PARTITION variable=tmpres_state_zr complete + #pragma HLS ARRAY_PARTITION variable=tmpres_state_h complete + #pragma HLS ARRAY_PARTITION variable=tmpres_zr complete + #pragma HLS ARRAY_PARTITION variable=tmpres_h complete + #pragma HLS ARRAY_PARTITION variable=inputacc_zr complete + #pragma HLS ARRAY_PARTITION variable=inputacc_h complete + + nnet::dense(data, tmpres, param, param_b); + nnet::dense(h_newstate, tmpres_state_zr, param_zr, + param_br); + // Adding the individual vectors from the multiplication of tmpres = Wx*x(t); tmpres_state_zr = Wh*h(t-1); tmpres + // initialized with biases -- DONE + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (2 * CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc; + inputacc_zr[iacc] = tmpres[index] + tmpres_state_zr[index]; + } + + // Activation function Sub layer -- START + CONFIG_T::template activation_recr::activation(inputacc_zr, tmpres_zr); + + // Activation function Sub layer -- END + + // Hadamrd product of r(t) = inputacc_zr[2*n_state:n_state] and h(t-1) = h_newstate + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + if (CONFIG_T::pytorch_order) + tmpres_state_h[iacc] = tmpres_zr[iacc] * tmpres_state_zr[iacc + (2 * CONFIG_T::n_state)]; + else + tmpres_state_h[iacc] = tmpres_zr[iacc + (CONFIG_T::n_state)] * tmpres_state_zr[iacc + (2 * CONFIG_T::n_state)]; + } + + // Assuming reset_after is false + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc + CONFIG_T::n_state * 2; + inputacc_h[iacc] = tmpres[index] + tmpres_state_h[iacc]; + } + + // Now run the activation on this guy + CONFIG_T::template activation::activation(inputacc_h, tmpres_h); + + // Mix the stat with the previous state + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + if (CONFIG_T::pytorch_order) + h_newstate[iacc] = (res_T)(tmpres_h[iacc] * (1 - tmpres_zr[iacc + (CONFIG_T::n_state)]) + + h_newstate[iacc] * tmpres_zr[iacc + (CONFIG_T::n_state)]); + else + h_newstate[iacc] = (res_T)(tmpres_h[iacc] * (1 - tmpres_zr[iacc]) + h_newstate[iacc] * tmpres_zr[iacc]); + } +} + +template +void gru_static(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_newstate[CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 3 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_zr[CONFIG_T::n_state * 3 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 3], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 3]) { + static res_T h_state[CONFIG_T::n_state]; + // Initialize the state variable -- will maintain state between function calls + typename CONFIG_T::accum_t tmpres[CONFIG_T::n_state * 3]; + typename CONFIG_T::accum_t tmpres_state_zr[CONFIG_T::n_state * 3]; + typename CONFIG_T::accum_t tmpres_state_h[CONFIG_T::n_state]; + typename CONFIG_T::accum_t tmpres_zr[CONFIG_T::n_state * 2]; // activated i,f,o matrices (keras notation) + typename CONFIG_T::accum_t tmpres_h[CONFIG_T::n_state]; // activated c-matrix (keras notation) + typename CONFIG_T::accum_t inputacc_zr[CONFIG_T::n_state * 2]; // i,f,o matrices (keras notation) + typename CONFIG_T::accum_t inputacc_h[CONFIG_T::n_state]; // c-matrix (keras notation) + + #pragma HLS ARRAY_PARTITION variable=h_state complete + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=tmpres complete + #pragma HLS ARRAY_PARTITION variable=tmpres_state_zr complete + #pragma HLS ARRAY_PARTITION variable=tmpres_state_h complete + #pragma HLS ARRAY_PARTITION variable=tmpres_zr complete + #pragma HLS ARRAY_PARTITION variable=tmpres_h complete + #pragma HLS ARRAY_PARTITION variable=inputacc_zr complete + #pragma HLS ARRAY_PARTITION variable=inputacc_h complete + + if (reset_state) { + #pragma clang loop unroll(full) + for (int i_h_state = 0; i_h_state < (CONFIG_T::n_state); i_h_state++) { + //#pragma HLS UNROLL + h_state[i_h_state] = 0; + } + } + + nnet::dense(data, tmpres, param, param_b); + nnet::dense(h_state, tmpres_state_zr, param_zr, + param_br); + + // Adding the individual vectors from the multiplication of tmpres = Wx*x(t); tmpres_state_zr = Wh*h(t-1); tmpres + // initialized with biases -- DONE + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (2 * CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc; + inputacc_zr[iacc] = tmpres[index] + tmpres_state_zr[index]; + } + + // Activation function Sub layer -- START + CONFIG_T::template activation_recr::activation(inputacc_zr, tmpres_zr); + + // Activation function Sub layer -- END + + // Hadamrd product of r(t) = inputacc_zr[2*n_state:n_state] and h(t-1) = h_newstate + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + if (CONFIG_T::pytorch_order) + tmpres_state_h[iacc] = tmpres_zr[iacc] * tmpres_state_zr[iacc + (2 * CONFIG_T::n_state)]; + else + tmpres_state_h[iacc] = tmpres_zr[iacc + (CONFIG_T::n_state)] * tmpres_state_zr[iacc + (2 * CONFIG_T::n_state)]; + } + + // Assuming reset_after is false + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + int index = iacc + CONFIG_T::n_state * 2; + inputacc_h[iacc] = tmpres[index] + tmpres_state_h[iacc]; + } + + // Now run the activation on this guy + CONFIG_T::template activation::activation(inputacc_h, tmpres_h); + + // Mix the stat with the previous state + #pragma clang loop unroll(full) + for (int iacc = 0; iacc < (CONFIG_T::n_state); iacc++) { + //#pragma HLS UNROLL + if (CONFIG_T::pytorch_order) + h_state[iacc] = (res_T)(tmpres_h[iacc] * (1 - tmpres_zr[iacc + (CONFIG_T::n_state)]) + + h_state[iacc] * tmpres_zr[iacc + (CONFIG_T::n_state)]); + else + h_state[iacc] = (res_T)(tmpres_h[iacc] * (1 - tmpres_zr[iacc]) + h_state[iacc] * tmpres_zr[iacc]); + h_newstate[iacc] = h_state[iacc]; + } +} + +template struct gru_class { + static void apply(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_state[CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 3 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_zr[CONFIG_T::n_state * 3 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 3], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 3]) { + nnet::gru(reset_state, data, h_state, param, param_zr, param_b, param_br); + }; +}; + +template struct gru_class_static { + static void apply(bool reset_state, data_T data[CONFIG_T::n_in], res_T h_state[CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 3 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_zr[CONFIG_T::n_state * 3 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 3], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 3]) { + nnet::gru_static(reset_state, data, h_state, param, param_zr, param_b, param_br); + }; +}; + +template +void gru_stack(data_T data[CONFIG_T::n_sequence * CONFIG_T::n_in], res_T res[CONFIG_T::n_sequence_out * CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 3 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_zr[CONFIG_T::n_state * 3 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 3], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 3]) { + + res_T h_state[CONFIG_T::n_state]; + data_T data_in[CONFIG_T::n_in]; + bool reset_state = true; + + #pragma HLS ARRAY_PARTITION variable=h_state complete + #pragma HLS ARRAY_PARTITION variable=data_in complete + + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_state; ii++) { + //#pragma HLS UNROLL + h_state[ii] = 0; + } + for (int iloop = 0; iloop < CONFIG_T::n_sequence; iloop++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_in; j++) { + //#pragma HLS UNROLL + data_in[j] = data[j + iloop * CONFIG_T::n_in]; + } + if (CONFIG_T::use_static) + nnet::gru_static(reset_state, data_in, h_state, param, param_zr, param_b, param_br); + else + nnet::gru(reset_state, data_in, h_state, param, param_zr, param_b, param_br); + if (CONFIG_T::n_sequence_out > 1) + #pragma clang loop unroll(full) + for (int i = CONFIG_T::n_state * iloop, j = 0; i < (CONFIG_T::n_state * (iloop + 1)); i++, j++) { + //#pragma HLS UNROLL + res[i] = h_state[j]; + } + reset_state = false; + } + if (CONFIG_T::n_sequence_out == 1) + #pragma clang loop unroll(full) + for (int i = 0; i < (CONFIG_T::n_state); i++) { + //#pragma HLS UNROLL + res[i] = h_state[i]; + } +} + +template +void gru_stack(data_T data[CONFIG_T::n_sequence * CONFIG_T::n_in], h_T h_state[CONFIG_T::n_state], + res_T res[CONFIG_T::n_sequence_out * CONFIG_T::n_state], + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 3 * CONFIG_T::n_in], + typename CONFIG_T::weight_t param_zr[CONFIG_T::n_state * 3 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 3], + typename CONFIG_T::bias_t param_br[CONFIG_T::n_state * 3]) { + + data_T data_in[CONFIG_T::n_in]; + bool reset_state = false; + + #pragma HLS ARRAY_PARTITION variable=h_state complete + #pragma HLS ARRAY_PARTITION variable=data_in complete + for (int iloop = 0; iloop < CONFIG_T::n_sequence; iloop++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_in; j++) { + //#pragma HLS UNROLL + data_in[j] = data[j + iloop * CONFIG_T::n_in]; + } + nnet::gru(reset_state, data_in, h_state, param, param_zr, param_b, param_br); + + if (CONFIG_T::n_sequence_out > 1) + #pragma clang loop unroll(full) + for (int i = CONFIG_T::n_state * iloop, j = 0; i < (CONFIG_T::n_state * (iloop + 1)); i++, j++) { + //#pragma HLS UNROLL + res[i] = h_state[j]; + } + reset_state = false; + } + + if (CONFIG_T::n_sequence_out == 1) + #pragma clang loop unroll(full) + for (int i = 0; i < (CONFIG_T::n_state); i++) { + //#pragma HLS UNROLL + res[i] = h_state[i]; + } +} + +template +void gru_stack(hls::stream &data_stream, hls::stream &res_stream, + typename CONFIG_T::weight_t param[CONFIG_T::n_state * 3 * CONFIG_T::n_in], + typename CONFIG_T::recurrent_weight_t param_zr[CONFIG_T::n_state * 3 * CONFIG_T::n_state], + typename CONFIG_T::bias_t param_b[CONFIG_T::n_state * 3], + typename CONFIG_T::recurrent_bias_t param_br[CONFIG_T::n_state * 3]) { + + typename res_T::value_type h_newstate[CONFIG_T::n_state]; + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma clang loop unroll(full) + for (int ii = 0; ii < CONFIG_T::n_state; ii++) { + //#pragma HLS UNROLL + h_newstate[ii] = 0; + } + + typename data_T::value_type data_in[CONFIG_T::n_in]; + bool reset_state = true; + +DataPropagation: + for (int i_in = 0; i_in < CONFIG_T::n_sequence * CONFIG_T::n_in / data_T::size; i_in++) { + if (CONFIG_T::n_sequence * CONFIG_T::n_in / data_T::size > 1) { + // #pragma HLS PIPELINE + } + data_T data_pack = data_stream.read(); + DataPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < data_T::size; i_pack++) { + //#pragma HLS UNROLL + data_in[i_pack] = data_pack[i_pack]; + } + if (CONFIG_T::use_static) + nnet::gru_static( + reset_state, data_in, h_newstate, param, param_zr, param_b, param_br); + else + nnet::gru(reset_state, data_in, h_newstate, + param, param_zr, param_b, param_br); + if (CONFIG_T::n_sequence_out > 1) { + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + ResPack_sequences: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = h_newstate[i_pack]; + } + res_stream.write(res_pack); + } + reset_state = false; + } + + if (CONFIG_T::n_sequence_out == 1) { + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + ResPack: + #pragma clang loop unroll(full) + for (int i_pack = 0; i_pack < res_T::size; i_pack++) { + //#pragma HLS UNROLL + res_pack[i_pack] = h_newstate[i_pack]; + } + res_stream.write(res_pack); + } +} + +// Struct for the Bidirectional template + +struct single_layer_config { + // Internal data type definitions + typedef float weight_t; + typedef float recurrent_weight_t; + typedef float bias_t; + typedef float recurrent_bias_t; + typedef float accum_t; + + // Layer Sizes + static const unsigned n_in = 2; + static const unsigned n_state = 2; + static const unsigned n_mult = 3; + static const unsigned table_size = 1024; + + template using activation_recr = nnet::activation::relu; + template using activation = nnet::activation::relu; +}; + +struct bidirectional_config { + // Layer Sizes + static const unsigned n_in = 2; + static const unsigned n_parts = 20; + static const unsigned n_out = 2; + static const unsigned table_size = 1024; + + // Resource reuse info + static const unsigned io_type = io_parallel; + static const unsigned reuse_factor = 1; + static const unsigned n_zeros = 0; + static const bool store_weights_in_bram = false; + static const bool use_static = true; + + // Layers info + + template + using RNNfunc_forward = nnet::lstm_class; + template + using RNNfunc_backward = nnet::lstm_class; +}; + +template +void bidirectional_stack( + data_T data[CONFIG_T::n_sequence * CONFIG_T::n_in], res_T res[CONFIG_T::n_sequence_out * CONFIG_T::n_out], + typename CONFIG_T::FORWARD_CONFIG::weight_t + param[CONFIG_T::FORWARD_CONFIG::n_state * CONFIG_T::FORWARD_CONFIG::n_mult * CONFIG_T::n_in], + typename CONFIG_T::FORWARD_CONFIG::recurrent_weight_t + param_r[CONFIG_T::FORWARD_CONFIG::n_state * CONFIG_T::FORWARD_CONFIG::n_mult * CONFIG_T::FORWARD_CONFIG::n_state], + typename CONFIG_T::FORWARD_CONFIG::bias_t param_b[CONFIG_T::FORWARD_CONFIG::n_state * CONFIG_T::FORWARD_CONFIG::n_mult], + typename CONFIG_T::FORWARD_CONFIG::recurrent_bias_t + param_br[CONFIG_T::FORWARD_CONFIG::n_state * CONFIG_T::FORWARD_CONFIG::n_mult], + typename CONFIG_T::BACKWARD_CONFIG::weight_t + param_back[CONFIG_T::BACKWARD_CONFIG::n_state * CONFIG_T::BACKWARD_CONFIG::n_mult * CONFIG_T::n_in], + typename CONFIG_T::BACKWARD_CONFIG::recurrent_weight_t + param_r_back[CONFIG_T::BACKWARD_CONFIG::n_state * CONFIG_T::BACKWARD_CONFIG::n_mult * + CONFIG_T::BACKWARD_CONFIG::n_state], + typename CONFIG_T::BACKWARD_CONFIG::bias_t + param_b_back[CONFIG_T::BACKWARD_CONFIG::n_state * CONFIG_T::BACKWARD_CONFIG::n_mult], + typename CONFIG_T::BACKWARD_CONFIG::recurrent_bias_t + param_br_back[CONFIG_T::BACKWARD_CONFIG::n_state * CONFIG_T::BACKWARD_CONFIG::n_mult]) { + + res_T h_newstate[(CONFIG_T::FORWARD_CONFIG::n_mult - 2) * CONFIG_T::FORWARD_CONFIG::n_state]; + res_T h_newstate_back[(CONFIG_T::BACKWARD_CONFIG::n_mult - 2) * CONFIG_T::BACKWARD_CONFIG::n_state]; + data_T data_in[CONFIG_T::n_in]; + data_T data_in_back[CONFIG_T::n_in]; + bool reset_state = true; + + #pragma HLS ARRAY_PARTITION variable=h_newstate complete + #pragma HLS ARRAY_PARTITION variable=h_newstate_back complete + + #pragma clang loop unroll(full) + for (int ii = 0; ii < (CONFIG_T::FORWARD_CONFIG::n_mult - 2) * CONFIG_T::FORWARD_CONFIG::n_state; ii++) { + //#pragma HLS UNROLL + h_newstate[ii] = 0; + } + #pragma clang loop unroll(full) + for (int ii = 0; ii < (CONFIG_T::BACKWARD_CONFIG::n_mult - 2) * CONFIG_T::BACKWARD_CONFIG::n_state; ii++) { + //#pragma HLS UNROLL + h_newstate_back[ii] = 0; + } + + for (int iloop = 0; iloop < CONFIG_T::n_sequence; iloop++) { + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_in; j++) { + //#pragma HLS UNROLL + data_in[j] = data[j + iloop * CONFIG_T::n_in]; + data_in_back[j] = data[j + (CONFIG_T::n_sequence - iloop - 1) * CONFIG_T::n_in]; + } + + CONFIG_T::template RNNfunc_forward::apply( + reset_state, data_in, h_newstate, param, param_r, param_b, param_br); + CONFIG_T::template RNNfunc_backward::apply( + reset_state, data_in_back, h_newstate_back, param_back, param_r_back, param_b_back, param_br_back); + + if (CONFIG_T::n_sequence_out > 1) { + #pragma clang loop unroll(full) + for (int i = (CONFIG_T::FORWARD_CONFIG::n_state + CONFIG_T::BACKWARD_CONFIG::n_state) * iloop, j = 0; + i < (CONFIG_T::FORWARD_CONFIG::n_state + CONFIG_T::BACKWARD_CONFIG::n_state) * iloop + + CONFIG_T::FORWARD_CONFIG::n_state; + i++, j++) { + //#pragma HLS UNROLL + res[i] = h_newstate[j]; + } + #pragma clang loop unroll(full) + for (int i = (CONFIG_T::FORWARD_CONFIG::n_state + CONFIG_T::BACKWARD_CONFIG::n_state) * + (CONFIG_T::n_sequence - iloop) - + CONFIG_T::BACKWARD_CONFIG::n_state, + j = 0; + i < + (CONFIG_T::FORWARD_CONFIG::n_state + CONFIG_T::BACKWARD_CONFIG::n_state) * (CONFIG_T::n_sequence - iloop); + i++, j++) { + //#pragma HLS UNROLL + res[i] = h_newstate_back[j]; + } + } + reset_state = false; + } + + if (CONFIG_T::n_sequence_out == 1) { + #pragma clang loop unroll(full) + for (int i = 0; i < (CONFIG_T::FORWARD_CONFIG::n_state); i++) { + //#pragma HLS UNROLL + res[i] = h_newstate[i]; + } + #pragma clang loop unroll(full) + for (int i = 0; i < (CONFIG_T::BACKWARD_CONFIG::n_state); i++) { + //#pragma HLS UNROLL + res[i + CONFIG_T::FORWARD_CONFIG::n_state] = h_newstate_back[i]; + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d.h b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d.h new file mode 100644 index 0000000000..3407a0ed31 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d.h @@ -0,0 +1,46 @@ +#ifndef NNET_SEPARABLE_CONV1D_H_ +#define NNET_SEPARABLE_CONV1D_H_ + +#include "nnet_common.h" +#include "nnet_conv1d.h" +#include "nnet_sepconv1d_latency.h" +//#include "nnet_sepconv1d_resource.h" +#include + +namespace nnet { + +template +void depthwise_conv_1d_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + #pragma HLS inline recursive + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + depthwise_conv_1d_latency_cl(data, res, weights, biases); + } else { + assert("Resource strategy for DepthwiseConv1D is not supported." && false); + } +} + +template +void separable_conv_1d_cl(data_T data[CONFIG_T::depthwise_config::in_width * CONFIG_T::depthwise_config::n_chan], + res_T res[CONFIG_T::pointwise_config::out_width * CONFIG_T::pointwise_config::n_filt], + typename CONFIG_T::depthwise_config::weight_t + depthwise_weights[CONFIG_T::depthwise_config::filt_width * CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::weight_t + pointwise_weights[CONFIG_T::pointwise_config::n_chan * CONFIG_T::pointwise_config::n_filt], + typename CONFIG_T::depthwise_config::bias_t depthwise_biases[CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::bias_t pointwise_biases[CONFIG_T::pointwise_config::n_filt]) { + #pragma HLS inline recursive + + dw_res_T depthwise_res[CONFIG_T::depthwise_config::out_width * CONFIG_T::depthwise_config::n_filt]; + + depthwise_conv_1d_cl(data, depthwise_res, depthwise_weights, + depthwise_biases); + pointwise_conv_1d_cl(depthwise_res, res, pointwise_weights, + pointwise_biases); +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_latency.h b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_latency.h new file mode 100644 index 0000000000..047fd7f18e --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_latency.h @@ -0,0 +1,94 @@ +#ifndef NNET_SEPARABLE_CONV2D_LATENCY_H_ +#define NNET_SEPARABLE_CONV2D_LATENCY_H_ + +#include "nnet_common.h" +#include "nnet_mult.h" +#include + +namespace nnet { + +template +void depthwise_conv_1d_latency_cl(data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + + constexpr unsigned mult_n_in = CONFIG_T::filt_width * CONFIG_T::n_chan; + constexpr unsigned mult_n_acc = CONFIG_T::filt_width; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=data_buf complete dim=0 + + typename CONFIG_T::accum_t mult[mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=mult complete + + typename CONFIG_T::accum_t acc[mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + + #pragma HLS ARRAY_PARTITION variable=weights complete + #pragma HLS ARRAY_PARTITION variable=biases complete + + // Limit multipliers to control parallelization + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::mult_config::multiplier_limit + + assert((CONFIG_T::n_filt == CONFIG_T::n_chan) && "only a depth multiplier of 1 is currently supported"); + +PartitionLoop: + for (int i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor rewind + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + data_T cache; + + // Do the matrix-multiply + Product: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_in; i_in++) { + //#pragma HLS UNROLL + cache = data_buf[i_pxl][i_in]; + mult[i_in] = + CONFIG_T::mult_config::template product::product( + cache, weights[i_in]); + } + + // Initialize accumulator with input biases + ResetAccum: + #pragma clang loop unroll(full) + for (int i_acc = 0; i_acc < mult_n_out; i_acc++) { + //#pragma HLS UNROLL + acc[i_acc] = (typename CONFIG_T::accum_t)biases[i_acc]; + } + + // Accumulate multiplication result + Accum1: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_acc; i_in++) { + //#pragma HLS UNROLL + Accum2: + #pragma clang loop unroll(full) + for (int i_out = 0; i_out < mult_n_out; i_out++) { + //#pragma HLS UNROLL + acc[i_out] += mult[i_in * mult_n_out + i_out]; + } + } + + // Cast to "res_t" type + Result: + #pragma clang loop unroll(full) + for (int i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + *(res++) = cast(acc[i_res]); + } + } + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_stream.h new file mode 100644 index 0000000000..30b66ec30e --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_stream.h @@ -0,0 +1,124 @@ +#ifndef NNET_SEPARABLE_CONV1D_STREAM_H_ +#define NNET_SEPARABLE_CONV1D_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_conv1d_stream.h" +#include "nnet_sepconv_stream.h" + +namespace nnet { + +template +void depthwise_conv_1d_encoded_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + + hls::stream data_window[CONFIG_T::filt_width * CONFIG_T::n_chan]; + const int win_depth = CONFIG_T::out_width; + for (unsigned i_out = 0; i_out < CONFIG_T::filt_width * CONFIG_T::n_chan; i_out++) { + //#pragma HLS STREAM variable=data_window[i_out] depth=win_depth + } + + #pragma HLS ARRAY_PARTITION variable=CONFIG_T::pixels complete + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + unsigned outputs_ready = 0; + + ap_uint pixel_idx[data_T::size / CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=pixel_idx complete + +ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (data_T::size / CONFIG_T::n_chan); i_iw++) { + //#pragma HLS LOOP_FLATTEN + if ((CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) && + data_T::size / CONFIG_T::n_chan == 1) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + compute_scaled_indices_1d(i_iw, pixel_idx); + compute_depthwise_output_encoded(data.read(), data_window, res, res_pack, outputs_ready, + weights, biases, pixel_idx); + } +} + +template +void depthwise_conv_1d_buffer_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + +ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width; i_iw++) { + //#pragma HLS LOOP_FLATTEN + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + compute_depthwise_output_buffer_1d(data.read(), res, weights, biases); + } +} + +template +void depthwise_conv_1d_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + + assert((CONFIG_T::n_filt == CONFIG_T::n_chan) && "only a depth multiplier of 1 is currently supported"); + + #pragma HLS inline recursive + switch (CONFIG_T::implementation) { + case conv_implementation::linebuffer: + depthwise_conv_1d_buffer_cl(data, res, weights, biases); + break; + case conv_implementation::encoded: + depthwise_conv_1d_encoded_cl(data, res, weights, biases); + break; + } +} + +template +void pointwise_conv_1d_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::filt_width == 1); + + #pragma HLS ARRAY_PARTITION variable=weights complete + #pragma HLS ARRAY_PARTITION variable=biases complete + +ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (data_T::size / CONFIG_T::n_chan); i_iw++) { + if ((CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) && + data_T::size / CONFIG_T::n_chan == 1) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + if (i_iw % CONFIG_T::stride_width == 0) { + pointwise_mult_buffer(data.read(), res, weights, biases); + } else { + data.read(); + } + } +} + +template +void separable_conv_1d_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::depthwise_config::weight_t + depthwise_weights[CONFIG_T::depthwise_config::filt_width * CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::weight_t + pointwise_weights[CONFIG_T::pointwise_config::n_chan * CONFIG_T::pointwise_config::n_filt], + typename CONFIG_T::depthwise_config::bias_t depthwise_biases[CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::bias_t pointwise_biases[CONFIG_T::pointwise_config::n_filt]) { + //#pragma HLS DATAFLOW + + hls::stream depthwise_res; + constexpr unsigned res_depth = CONFIG_T::depthwise_config::out_width; + //#pragma HLS STREAM variable=depthwise_res depth=res_depth + + depthwise_conv_1d_cl(data, depthwise_res, depthwise_weights, + depthwise_biases); + pointwise_conv_1d_cl(depthwise_res, res, pointwise_weights, + pointwise_biases); +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d.h b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d.h new file mode 100644 index 0000000000..700785583d --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d.h @@ -0,0 +1,51 @@ +#ifndef NNET_SEPARABLE_CONV2D_H_ +#define NNET_SEPARABLE_CONV2D_H_ + +#include "nnet_common.h" +#include "nnet_conv2d.h" +#include "nnet_sepconv2d_latency.h" +//#include "nnet_sepconv2d_resource.h" +#include + +namespace nnet { + +template +void depthwise_conv_2d_cl( + data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + #pragma HLS inline recursive + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + depthwise_conv_2d_latency_cl(data, res, weights, biases); + } else { + assert("Resource strategy for DepthwiseConv2D is not supported." && false); + } +} + +template +void separable_conv_2d_cl(data_T data[CONFIG_T::depthwise_config::in_height * CONFIG_T::depthwise_config::in_width * + CONFIG_T::depthwise_config::n_chan], + res_T res[CONFIG_T::pointwise_config::out_height * CONFIG_T::pointwise_config::out_width * + CONFIG_T::pointwise_config::n_filt], + typename CONFIG_T::depthwise_config::weight_t + depthwise_weights[CONFIG_T::depthwise_config::filt_height * + CONFIG_T::depthwise_config::filt_width * CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::weight_t + pointwise_weights[CONFIG_T::pointwise_config::n_chan * CONFIG_T::pointwise_config::n_filt], + typename CONFIG_T::depthwise_config::bias_t depthwise_biases[CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::bias_t pointwise_biases[CONFIG_T::pointwise_config::n_filt]) { + #pragma HLS inline recursive + + dw_res_T depthwise_res[CONFIG_T::depthwise_config::out_height * CONFIG_T::depthwise_config::out_width * + CONFIG_T::depthwise_config::n_filt]; + + depthwise_conv_2d_cl(data, depthwise_res, depthwise_weights, + depthwise_biases); + pointwise_conv_2d_cl(depthwise_res, res, pointwise_weights, + pointwise_biases); +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_latency.h b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_latency.h new file mode 100644 index 0000000000..524c3c758f --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_latency.h @@ -0,0 +1,95 @@ +#ifndef NNET_SEPARABLE_CONV2D_LATENCY_H_ +#define NNET_SEPARABLE_CONV2D_LATENCY_H_ + +#include "nnet_common.h" +#include "nnet_mult.h" +#include + +namespace nnet { + +template +void depthwise_conv_2d_latency_cl( + data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + res_T res[CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::n_filt], + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + + constexpr unsigned mult_n_in = CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan; + constexpr unsigned mult_n_acc = CONFIG_T::filt_height * CONFIG_T::filt_width; + constexpr unsigned mult_n_out = CONFIG_T::n_filt; + + data_T data_buf[CONFIG_T::n_pixels][mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=data_buf complete dim=0 + + typename CONFIG_T::accum_t mult[mult_n_in]; + #pragma HLS ARRAY_PARTITION variable=mult complete + + typename CONFIG_T::accum_t acc[mult_n_out]; + #pragma HLS ARRAY_PARTITION variable=acc complete + + #pragma HLS ARRAY_PARTITION variable=weights complete + #pragma HLS ARRAY_PARTITION variable=biases complete + + // Limit multipliers to control parallelization + //#pragma HLS ALLOCATION operation instances=mul limit=CONFIG_T::mult_config::multiplier_limit + + assert((CONFIG_T::n_filt == CONFIG_T::n_chan) && "only a depth multiplier of 1 is currently supported"); + +PartitionLoop: + for (int i_part = 0; i_part < CONFIG_T::n_partitions; i_part++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor rewind + + CONFIG_T::template fill_buffer::fill_buffer(data, data_buf, i_part); + + PixelLoop: + #pragma clang loop unroll(full) + for (unsigned i_pxl = 0; i_pxl < CONFIG_T::n_pixels; i_pxl++) { + //#pragma HLS UNROLL + + data_T cache; + + // Do the matrix-multiply + Product: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_in; i_in++) { + //#pragma HLS UNROLL + cache = data_buf[i_pxl][i_in]; + mult[i_in] = + CONFIG_T::mult_config::template product::product( + cache, weights[i_in]); + } + + // Initialize accumulator with input biases + ResetAccum: + #pragma clang loop unroll(full) + for (int i_acc = 0; i_acc < mult_n_out; i_acc++) { + //#pragma HLS UNROLL + acc[i_acc] = (typename CONFIG_T::accum_t)biases[i_acc]; + } + + // Accumulate multiplication result + Accum1: + #pragma clang loop unroll(full) + for (int i_in = 0; i_in < mult_n_acc; i_in++) { + //#pragma HLS UNROLL + Accum2: + #pragma clang loop unroll(full) + for (int i_out = 0; i_out < mult_n_out; i_out++) { + //#pragma HLS UNROLL + acc[i_out] += mult[i_in * mult_n_out + i_out]; + } + } + + // Cast to "res_t" type + Result: + #pragma clang loop unroll(full) + for (int i_res = 0; i_res < mult_n_out; i_res++) { + //#pragma HLS UNROLL + *(res++) = cast(acc[i_res]); + } + } + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_stream.h new file mode 100644 index 0000000000..588c60c0ca --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_stream.h @@ -0,0 +1,148 @@ +#ifndef NNET_SEPARABLE_CONV2D_STREAM_H_ +#define NNET_SEPARABLE_CONV2D_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_conv2d_stream.h" +#include "nnet_sepconv_stream.h" +#include "nnet_types.h" + +namespace nnet { + +template +void depthwise_conv_2d_encoded_cl( + hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::filt_height == CONFIG_T::filt_width); + + hls::stream data_window[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan]; + const int win_depth = CONFIG_T::filt_height * CONFIG_T::out_width; + for (unsigned i_out = 0; i_out < CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan; i_out++) { + //#pragma HLS STREAM variable=data_window[i_out] depth=win_depth + } + + #pragma HLS ARRAY_PARTITION variable=CONFIG_T::pixels complete + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + unsigned outputs_ready = 0; + + ap_uint pixel_idx[data_T::size / CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=pixel_idx complete + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (data_T::size / CONFIG_T::n_chan); i_iw++) { + //#pragma HLS LOOP_FLATTEN + if ((CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) && + data_T::size / CONFIG_T::n_chan == 1) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + compute_scaled_indices_2d(i_ih, i_iw, pixel_idx); + compute_depthwise_output_encoded(data.read(), data_window, res, res_pack, outputs_ready, + weights, biases, pixel_idx); + } + } +} + +// Line Buffer Implementation (Phil's) +template +void depthwise_conv_2d_buffer_cl( + hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + + static ap_shift_reg line_buffer[CONFIG_T::filt_height - 1] + [CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable = line_buffer complete dim = 2 + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width; i_iw++) { + //#pragma HLS LOOP_FLATTEN + if (CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + if (CONFIG_T::filt_height > 1) { + compute_depthwise_output_buffer_2d(data.read(), line_buffer, res, weights, biases); + } else { + compute_depthwise_output_buffer_1d(data.read(), res, weights, biases); + } + } + } +} + +template +void depthwise_conv_2d_cl( + hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + + assert((CONFIG_T::n_filt == CONFIG_T::n_chan) && "only a depth multiplier of 1 is currently supported"); + + #pragma HLS inline recursive + switch (CONFIG_T::implementation) { + case conv_implementation::linebuffer: + depthwise_conv_2d_buffer_cl(data, res, weights, biases); + break; + case conv_implementation::encoded: + depthwise_conv_2d_encoded_cl(data, res, weights, biases); + break; + } +} + +template +void pointwise_conv_2d_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + assert(CONFIG_T::pad_top == 0 && CONFIG_T::pad_bottom == 0 && CONFIG_T::pad_left == 0 && CONFIG_T::pad_right == 0); + assert(CONFIG_T::filt_height == 1 && CONFIG_T::filt_width == 1); + + #pragma HLS ARRAY_PARTITION variable=weights complete + #pragma HLS ARRAY_PARTITION variable=biases complete + +ReadInputHeight: + for (unsigned i_ih = 0; i_ih < CONFIG_T::in_height; i_ih++) { + ReadInputWidth: + for (unsigned i_iw = 0; i_iw < CONFIG_T::in_width / (data_T::size / CONFIG_T::n_chan); i_iw++) { + if ((CONFIG_T::strategy == nnet::latency || CONFIG_T::strategy == nnet::distributed_arithmetic) && + data_T::size / CONFIG_T::n_chan == 1) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + } + if (i_ih % CONFIG_T::stride_height == 0 && i_iw % CONFIG_T::stride_width == 0) { + pointwise_mult_buffer(data.read(), res, weights, biases); + } else { + data.read(); + } + } + } +} + +template +void separable_conv_2d_cl(hls::stream &data, hls::stream &res, + typename CONFIG_T::depthwise_config::weight_t + depthwise_weights[CONFIG_T::depthwise_config::filt_height * + CONFIG_T::depthwise_config::filt_width * CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::weight_t + pointwise_weights[CONFIG_T::pointwise_config::n_chan * CONFIG_T::pointwise_config::n_filt], + typename CONFIG_T::depthwise_config::bias_t depthwise_biases[CONFIG_T::depthwise_config::n_chan], + typename CONFIG_T::pointwise_config::bias_t pointwise_biases[CONFIG_T::pointwise_config::n_filt]) { + //#pragma HLS DATAFLOW + + hls::stream depthwise_res; + constexpr unsigned res_depth = CONFIG_T::depthwise_config::out_height * CONFIG_T::depthwise_config::out_width; + //#pragma HLS STREAM variable=depthwise_res depth=res_depth + + depthwise_conv_2d_cl(data, depthwise_res, depthwise_weights, + depthwise_biases); + pointwise_conv_2d_cl(depthwise_res, res, pointwise_weights, + pointwise_biases); +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_sepconv_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv_stream.h new file mode 100644 index 0000000000..c98846e24f --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_sepconv_stream.h @@ -0,0 +1,248 @@ +#ifndef NNET_SEPARABLE_CONV_STREAM_H_ +#define NNET_SEPARABLE_CONV_STREAM_H_ + +#include "hls_stream.h" +#include "nnet_common.h" +#include "nnet_conv_stream.h" +#include "nnet_depthwise_product.h" + +namespace nnet { + +template +void depthwise_mult_buffer(hls::stream data_window[CONFIG_T::kernel_size * CONFIG_T::n_chan], + res_T &res_pack, hls::stream &res_stream, unsigned &outputs_ready, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + #pragma HLS inline + + typename data_T::value_type data[CONFIG_T::kernel_size * CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=data complete + typename res_T::value_type res[CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=res complete + +InitData: + #pragma clang loop unroll(full) + for (int id = 0; id < CONFIG_T::kernel_size * CONFIG_T::n_chan; id++) { + //#pragma HLS UNROLL + data[id] = data_window[id].read(); + } + + #pragma HLS inline recursive + CONFIG_T::mult_config::template kernel::dense(data, res, weights, biases); + +CastLoop: + #pragma clang loop unroll(full) + for (unsigned jj = 0; jj < CONFIG_T::n_chan; jj++) { + //#pragma HLS UNROLL + if (res_T::size / CONFIG_T::n_chan == 1) { + res_pack[jj] = res[jj]; + } else { + res_pack[outputs_ready * CONFIG_T::n_chan + jj] = res[jj]; + } + } + + if (res_T::size / CONFIG_T::n_chan == 1) { + res_stream.write(res_pack); + } else { + if (outputs_ready == (res_T::size / CONFIG_T::n_chan) - 1) { + res_stream.write(res_pack); + outputs_ready = 0; + } else { + outputs_ready++; + } + } +} + +template +void compute_depthwise_output_encoded( + const data_T &in_elem, hls::stream data_window[CONFIG_T::kernel_size * CONFIG_T::n_chan], + hls::stream &res, res_T &res_pack, unsigned &outputs_ready, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan], ap_uint *pixel_idx) { + #pragma HLS inline + +MultLoop: + for (unsigned p = 0; p < data_T::size / CONFIG_T::n_chan; p++) { + //#pragma HLS PIPELINE II=CONFIG_T::reuse_factor + CopyDataFilt: + #pragma clang loop unroll(full) + for (unsigned f = 0; f < CONFIG_T::kernel_size; f++) { + //#pragma HLS UNROLL + CopyDataChan: + #pragma clang loop unroll(full) + for (unsigned c = 0; c < CONFIG_T::n_chan; c++) { + //#pragma HLS UNROLL + if (pixel_idx[p][f]) + data_window[f * CONFIG_T::n_chan + c].write(in_elem[p * CONFIG_T::n_chan + c]); + } + } + if (pixel_idx[p][CONFIG_T::kernel_size - 1]) { + depthwise_mult_buffer(data_window, res_pack, res, outputs_ready, weights, biases); + } + } +} + +template +void pointwise_mult_buffer(const data_T &data_pack, hls::stream &res_stream, + typename CONFIG_T::weight_t weights[CONFIG_T::n_chan * CONFIG_T::n_filt], + typename CONFIG_T::bias_t biases[CONFIG_T::n_filt]) { + #pragma HLS inline + + typename data_T::value_type data[CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=data complete + + typename res_T::value_type res[CONFIG_T::n_filt]; + #pragma HLS ARRAY_PARTITION variable=res complete + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + +InitData: + #pragma clang loop unroll(full) + for (int id = 0; id < CONFIG_T::n_chan; id++) { + //#pragma HLS UNROLL + data[id] = data_pack[id]; + } + + #pragma HLS inline recursive + CONFIG_T::mult_config::template kernel::dense(data, res, weights, biases); + +CastLoop: + #pragma clang loop unroll(full) + for (unsigned jj = 0; jj < CONFIG_T::n_filt; jj++) { + //#pragma HLS UNROLL + res_pack[jj] = res[jj]; + } + + res_stream.write(res_pack); +} + +// Line Buffer Implementation (Phil's) +template +void compute_depthwise_output_buffer_1d(const data_T &in_elem, hls::stream &res_stream, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + #pragma HLS inline + + // Thresholds + const static int lShiftX = CONFIG_T::filt_width - 1; + + // Counters + static int pX = 0; + static int sX = 0; + + static typename data_T::value_type kernel_data[CONFIG_T::filt_width * CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=kernel_data complete + + typename res_T::value_type res_out[CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=res_out complete dim = 0 + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + + // Add pixel to buffer + nnet::kernel_shift_1d(in_elem, kernel_data); + + // Check to see if we have a full kernel + if ((sX - lShiftX) == 0 && pX > lShiftX - 1) { + // Dense multiply + #pragma HLS inline recursive + CONFIG_T::mult_config::template kernel::dense(kernel_data, res_out, weights, biases); + + // Pack output + CastLoop: + #pragma clang loop unroll(full) + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_filt; i_ic++) { + //#pragma HLS UNROLL + res_pack[i_ic] = res_out[i_ic]; + } + + // Write output to stream when output ready + res_stream.write(res_pack); + } + + // Pointer Housekeeping + if (pX + 1 == CONFIG_T::in_width) // Includes padding, end of line (padded) + { + pX = 0; + sX = 0; + } else { + pX = pX + 1; + sX = ((sX - lShiftX) == 0) ? sX - CONFIG_T::stride_width + 1 : sX + 1; + } +} + +template +void compute_depthwise_output_buffer_2d(const data_T &in_elem, + ap_shift_reg + line_buffer[MAX(CONFIG_T::filt_height - 1, 1)][CONFIG_T::n_chan], + hls::stream &res_stream, + typename CONFIG_T::weight_t weights[CONFIG_T::kernel_size * CONFIG_T::n_chan], + typename CONFIG_T::bias_t biases[CONFIG_T::n_chan]) { + #pragma HLS inline + + // Thresholds + const static int lShiftX = CONFIG_T::filt_width - 1; + const static int lShiftY = CONFIG_T::filt_height - 1; + + // counters + static int pX = 0; // pixel X + static int pY = 0; // pixel Y + + static int sX = 0; // stride X + static int sY = 0; // stride Y + + static typename data_T::value_type kernel_data[CONFIG_T::filt_height * CONFIG_T::filt_width * CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=kernel_data complete + + typename res_T::value_type res_out[CONFIG_T::n_chan]; + #pragma HLS ARRAY_PARTITION variable=res_out complete dim = 0 + + res_T res_pack; + PRAGMA_DATA_PACK(res_pack) + + // Add pixel to buffer + nnet::shift_line_buffer(in_elem, line_buffer, kernel_data); + + // Check to see if we have a full kernel + if ((sX - lShiftX) == 0 && (sY - lShiftY) == 0 && pY > lShiftY - 1 && pX > lShiftX - 1) { + // Dense multiply + #pragma HLS inline recursive + CONFIG_T::mult_config::template kernel::dense(kernel_data, res_out, weights, biases); + + // Pack output + CastLoop: + #pragma clang loop unroll(full) + for (unsigned i_ic = 0; i_ic < CONFIG_T::n_filt; i_ic++) { + //#pragma HLS UNROLL + res_pack[i_ic] = res_out[i_ic]; + } + + // Write output to stream when output ready + res_stream.write(res_pack); + } + + // Pointer Housekeeping + if (pX + 1 == CONFIG_T::in_width) // Includes padding, end of line (padded) + { + pX = 0; + sX = 0; + if (pY + 1 == CONFIG_T::in_height) { // Reached bottom of image + pY = 0; + sY = 0; + } else { + pY = pY + 1; + sY = ((sY - lShiftY) == 0) ? sY - CONFIG_T::stride_height + 1 : sY + 1; + } + } else { + pX = pX + 1; + sX = ((sX - lShiftX) == 0) ? sX - CONFIG_T::stride_width + 1 : sX + 1; + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_stream.h new file mode 100644 index 0000000000..cfee9cafa4 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_stream.h @@ -0,0 +1,349 @@ + +#ifndef NNET_STREAM_H +#define NNET_STREAM_H + +#include "hls_stream.h" +#include "nnet_common.h" + +namespace nnet { + +struct broadcast_config { + static const unsigned in_height = 1; + static const unsigned in_width = 1; + static const unsigned in_chan = 3; + static const unsigned out_height = 2; + static const unsigned out_width = 2; + static const unsigned out_chan = 3; +}; + +template +void clone_stream(hls::stream &data, hls::stream &res1, hls::stream &res2) { +CloneLoop: + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data1; + res_T out_data2; + PRAGMA_DATA_PACK(out_data1) + PRAGMA_DATA_PACK(out_data2) + + ClonePack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + out_data1[j] = in_data[j]; + out_data2[j] = in_data[j]; + } + + res1.write(out_data1); + res2.write(out_data2); + } +} + +template +void clone_stream(hls::stream &data, hls::stream &res1, hls::stream &res2, hls::stream &res3) { +CloneLoop: + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data1; + res_T out_data2; + res_T out_data3; + PRAGMA_DATA_PACK(out_data1) + PRAGMA_DATA_PACK(out_data2) + PRAGMA_DATA_PACK(out_data3) + + ClonePack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + out_data1[j] = in_data[j]; + out_data2[j] = in_data[j]; + out_data3[j] = in_data[j]; + } + + res1.write(out_data1); + res2.write(out_data2); + res3.write(out_data3); + } +} + +template +void clone_stream(hls::stream &data, hls::stream &res1, hls::stream &res2, hls::stream &res3, + hls::stream &res4) { +CloneLoop: + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data1; + res_T out_data2; + res_T out_data3; + res_T out_data4; + PRAGMA_DATA_PACK(out_data1) + PRAGMA_DATA_PACK(out_data2) + PRAGMA_DATA_PACK(out_data3) + PRAGMA_DATA_PACK(out_data4) + + ClonePack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + out_data1[j] = in_data[j]; + out_data2[j] = in_data[j]; + out_data3[j] = in_data[j]; + out_data4[j] = in_data[j]; + } + + res1.write(out_data1); + res2.write(out_data2); + res3.write(out_data3); + res4.write(out_data4); + } +} + +template +void clone_stream(hls::stream &data, hls::stream &res1, hls::stream &res2, hls::stream &res3, + hls::stream &res4, hls::stream &res5) { +CloneLoop: + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data1; + res_T out_data2; + res_T out_data3; + res_T out_data4; + res_T out_data5; + PRAGMA_DATA_PACK(out_data1) + PRAGMA_DATA_PACK(out_data2) + PRAGMA_DATA_PACK(out_data3) + PRAGMA_DATA_PACK(out_data4) + PRAGMA_DATA_PACK(out_data5) + + ClonePack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + out_data1[j] = in_data[j]; + out_data2[j] = in_data[j]; + out_data3[j] = in_data[j]; + out_data4[j] = in_data[j]; + out_data5[j] = in_data[j]; + } + + res1.write(out_data1); + res2.write(out_data2); + res3.write(out_data3); + res4.write(out_data4); + res5.write(out_data5); + } +} + +template +void clone_stream(hls::stream &data, hls::stream &res1, hls::stream &res2, hls::stream &res3, + hls::stream &res4, hls::stream &res5, hls::stream &res6) { +CloneLoop: + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data1; + res_T out_data2; + res_T out_data3; + res_T out_data4; + res_T out_data5; + res_T out_data6; + PRAGMA_DATA_PACK(out_data1) + PRAGMA_DATA_PACK(out_data2) + PRAGMA_DATA_PACK(out_data3) + PRAGMA_DATA_PACK(out_data4) + PRAGMA_DATA_PACK(out_data5) + PRAGMA_DATA_PACK(out_data6) + + ClonePack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + out_data1[j] = in_data[j]; + out_data2[j] = in_data[j]; + out_data3[j] = in_data[j]; + out_data4[j] = in_data[j]; + out_data5[j] = in_data[j]; + out_data6[j] = in_data[j]; + } + + res1.write(out_data1); + res2.write(out_data2); + res3.write(out_data3); + res4.write(out_data4); + res5.write(out_data5); + res6.write(out_data6); + } +} + +template +void clone_stream(hls::stream &data, hls::stream &res1, hls::stream &res2, hls::stream &res3, + hls::stream &res4, hls::stream &res5, hls::stream &res6, hls::stream &res7) { +CloneLoop: + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data1; + res_T out_data2; + res_T out_data3; + res_T out_data4; + res_T out_data5; + res_T out_data6; + res_T out_data7; + PRAGMA_DATA_PACK(out_data1) + PRAGMA_DATA_PACK(out_data2) + PRAGMA_DATA_PACK(out_data3) + PRAGMA_DATA_PACK(out_data4) + PRAGMA_DATA_PACK(out_data5) + PRAGMA_DATA_PACK(out_data6) + PRAGMA_DATA_PACK(out_data7) + + ClonePack: + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + out_data1[j] = in_data[j]; + out_data2[j] = in_data[j]; + out_data3[j] = in_data[j]; + out_data4[j] = in_data[j]; + out_data5[j] = in_data[j]; + out_data6[j] = in_data[j]; + out_data7[j] = in_data[j]; + } + + res1.write(out_data1); + res2.write(out_data2); + res3.write(out_data3); + res4.write(out_data4); + res5.write(out_data5); + res6.write(out_data6); + res7.write(out_data7); + } +} + +template void repack_stream(hls::stream &data, hls::stream &res) { + if (data_T::size == res_T::size) { + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = in_data[j]; + } + + res.write(out_data); + } + } else if (data_T::size > res_T::size) { + constexpr unsigned pack_diff = data_T::size / res_T::size; + for (int i = 0; i < N / data_T::size; i++) { + if (N / data_T::size > 1) { + //#pragma HLS PIPELINE + } + + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + + for (int j = 0; j < pack_diff; j++) { + //#pragma HLS PIPELINE + + res_T out_data; + #pragma clang loop unroll(full) + for (int k = 0; k < res_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data[j * res_T::size + k]; + } + res.write(out_data); + } + } + } else { // data_T::size < res_T::size + res_T out_data; + constexpr unsigned pack_diff = res_T::size / data_T::size; + unsigned pack_cnt = 0; + for (int i = 0; i < N / data_T::size; i++) { + //#pragma HLS PIPELINE + + data_T in_data = data.read(); + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + out_data[pack_cnt * data_T::size + j] = in_data[j]; + } + + if (pack_cnt == pack_diff - 1) { + res.write(out_data); + pack_cnt = 0; + } else { + pack_cnt++; + } + } + } +} + +template +void broadcast_stream_1x1xC(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::in_height == 1 && CONFIG_T::in_width == 1 && CONFIG_T::in_chan == CONFIG_T::out_chan); + int n_dupl = (CONFIG_T::out_height * CONFIG_T::out_width * CONFIG_T::out_chan) / + (CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::in_chan); +BroadcastLoop: + for (int i = 0; i < CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::in_chan / data_T::size; i++) { + //#pragma HLS PIPELINE + data_T in_data = data.read(); + for (int j = 0; j < n_dupl; j++) { + //#pragma HLS PIPELINE + res_T out_data; + PRAGMA_DATA_PACK(out_data) + #pragma clang loop unroll(full) + for (int k = 0; k < res_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data[k]; + } + res.write(out_data); + } + } +} + +template +void broadcast_stream_HxWx1(hls::stream &data, hls::stream &res) { + assert(CONFIG_T::in_chan == 1 && CONFIG_T::in_height == CONFIG_T::out_height && + CONFIG_T::in_width == CONFIG_T::out_width); +BroadcastLoop: + for (int i = 0; i < CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::in_chan / data_T::size; i++) { + //#pragma HLS PIPELINE + data_T in_data = data.read(); + res_T out_data; + PRAGMA_DATA_PACK(out_data) + #pragma clang loop unroll(full) + for (int k = 0; k < res_T::size; k++) { + //#pragma HLS UNROLL + out_data[k] = in_data[0]; + } + res.write(out_data); + } +} + +template +void broadcast_stream(hls::stream &data, hls::stream &res) { + if (CONFIG_T::in_height == 1 && CONFIG_T::in_width == 1 && CONFIG_T::in_chan == CONFIG_T::out_chan) { + broadcast_stream_1x1xC(data, res); + } else if (CONFIG_T::in_chan == 1 && CONFIG_T::in_height == CONFIG_T::out_height && + CONFIG_T::in_width == CONFIG_T::out_width) { + broadcast_stream_HxWx1(data, res); + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_time_distributed.h b/hls4ml/templates/bambu/nnet_utils/nnet_time_distributed.h new file mode 100644 index 0000000000..ad4461d86a --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_time_distributed.h @@ -0,0 +1,121 @@ +#ifndef NNET_TIME_DISTRIBUTED_H_ +#define NNET_TIME_DISTRIBUTED_H_ + +#include + +namespace nnet { + +struct time_distributed_config { + static const unsigned dim = 2; + + static const unsigned n_time_steps = 10; + static const unsigned in_height = 10; + static const unsigned in_width = 10; + static const unsigned n_chan = 10; +}; + +template +void read_time_step_2d(unsigned time_step, data_T data[CONFIG_T::n_time_steps * CONFIG_T::n_chan], + data_T res[CONFIG_T::n_chan]) { + //#pragma HLS PIPELINE + +ChannelLoop: + for (unsigned i = 0; i < CONFIG_T::n_chan; i++) { + res[i] = data[time_step * CONFIG_T::n_chan + i]; + } +} + +template +void read_time_step_3d(unsigned time_step, data_T data[CONFIG_T::n_time_steps * CONFIG_T::in_width * CONFIG_T::n_chan], + data_T res[CONFIG_T::in_width * CONFIG_T::n_chan]) { + //#pragma HLS PIPELINE + +WidthLoop: + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_width; i++) { + ChannelLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_chan; j++) { + res[i * CONFIG_T::n_chan + j] = + data[time_step * CONFIG_T::in_width * CONFIG_T::n_chan + i * CONFIG_T::n_chan + j]; + } + } +} + +template +void read_time_step_4d(unsigned time_step, + data_T data[CONFIG_T::n_time_steps * CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + data_T res[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan]) { + //#pragma HLS PIPELINE + +HeightLoop: + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_height; i++) { + WidthLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::in_width; j++) { + ChannelLoop: + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + res[i * CONFIG_T::in_width * CONFIG_T::n_chan + j * CONFIG_T::n_chan + k] = + data[time_step * CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan + + i * CONFIG_T::in_width * CONFIG_T::n_chan + j * CONFIG_T::n_chan + k]; + } + } + } +} + +template +void write_time_step_2d(unsigned time_step, data_T data[CONFIG_T::n_chan], + data_T res[CONFIG_T::n_time_steps * CONFIG_T::n_chan]) { + //#pragma HLS PIPELINE + +ChannelLoop: + #pragma clang loop unroll(full) + for (unsigned i = 0; i < CONFIG_T::n_chan; i++) { + res[time_step * CONFIG_T::n_chan + i] = data[i]; + } +} + +template +void write_time_step_3d(unsigned time_step, data_T data[CONFIG_T::in_width * CONFIG_T::n_chan], + data_T res[CONFIG_T::n_time_steps * CONFIG_T::in_width * CONFIG_T::n_chan]) { + //#pragma HLS PIPELINE + +WidthLoop: + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_width; i++) { + ChannelLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::n_chan; j++) { + res[time_step * CONFIG_T::in_width * CONFIG_T::n_chan + i * CONFIG_T::n_chan + j] = + data[i * CONFIG_T::n_chan + j]; + } + } +} + +template +void write_time_step_4d(unsigned time_step, data_T data[CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan], + data_T res[CONFIG_T::n_time_steps * CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan]) { + //#pragma HLS PIPELINE + +HeightLoop: + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::in_height; i++) { + WidthLoop: + #pragma clang loop unroll(full) + for (int j = 0; j < CONFIG_T::in_width; j++) { + ChannelLoop: + #pragma clang loop unroll(full) + for (int k = 0; k < CONFIG_T::n_chan; k++) { + res[time_step * CONFIG_T::in_height * CONFIG_T::in_width * CONFIG_T::n_chan + + i * CONFIG_T::in_width * CONFIG_T::n_chan + j * CONFIG_T::n_chan + k] = + data[i * CONFIG_T::in_width * CONFIG_T::n_chan + j * CONFIG_T::n_chan + k]; + } + } + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_transpose.h b/hls4ml/templates/bambu/nnet_utils/nnet_transpose.h new file mode 100644 index 0000000000..fab0e54962 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_transpose.h @@ -0,0 +1,40 @@ +#ifndef NNET_PERMUTE_H_ +#define NNET_PERMUTE_H_ + +namespace nnet { + +struct transpose_config { + static const unsigned dims; + static const unsigned N; + // vivado/vitis hls can't index constexpr array for some reason + // and vivado hls don't like template recursion either (vitis is fine) + // thus this appears to be the only workaround (or overkill it with codegen) + static const unsigned *const from_shape; + static const unsigned *const to_shape; + static const unsigned *const perm; + static const unsigned *const perm_strides; +}; + +template unsigned transfer_idx(int index) { + // Given output idx in c-order flat array, return input idx + int idx = 0; + for (int i = CONFIG_T::dims - 1; i >= 0; i--) { + idx += (index % CONFIG_T::to_shape[i]) * CONFIG_T::perm_strides[i]; + index /= CONFIG_T::to_shape[i]; + } + return idx; +} + +template +void transpose(const data_T data[CONFIG_T::N], res_T res[CONFIG_T::N]) { + #pragma clang loop unroll(full) + for (int i = 0; i < CONFIG_T::N; i++) { + //#pragma HLS UNROLL + int idx = transfer_idx(i); + res[i] = data[idx]; + } +} + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_transpose_stream.h b/hls4ml/templates/bambu/nnet_utils/nnet_transpose_stream.h new file mode 100644 index 0000000000..59a910a08a --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_transpose_stream.h @@ -0,0 +1,71 @@ +#ifndef NNET_TRANSPOSE_STREAM_H +#define NNET_TRANSPOSE_STREAM_H + +#include "hls_stream.h" +#include "nnet_transpose.h" +#include + +namespace nnet { + +template +typename std::enable_if::type transpose(hls::stream &data, hls::stream &res) { + #pragma HLS inline recursive + typename data_T::value_type data_array[CONFIG_T::N]; + #pragma HLS ARRAY_PARTITION variable=data_array complete + + for (int i = 0; i < CONFIG_T::N / data_T::size; i++) { + //#pragma HLS PIPELINE + data_T in_data = data.read(); + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + data_array[i * data_T::size + j] = typename data_T::value_type(in_data[j]); + } + } + + for (int i = 0; i < CONFIG_T::N / res_T::size; i++) { + //#pragma HLS PIPELINE + res_T out_data; + PRAGMA_DATA_PACK(out_data) + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = typename res_T::value_type(data_array[j * CONFIG_T::from_shape[1] + i]); + } + res.write(out_data); + } +} + +// This sfinae is for vivado_hls, which has some overhead using the transfer_idx in io_stream. +// In vitis both performs exactly the same, thus this is not removed out of convenience. +template +typename std::enable_if::type transpose(hls::stream &data, hls::stream &res) { + #pragma HLS inline recursive + typename data_T::value_type data_array[CONFIG_T::N]; + #pragma HLS ARRAY_PARTITION variable=data_array complete + + for (int i = 0; i < CONFIG_T::N / data_T::size; i++) { + //#pragma HLS PIPELINE + data_T in_data = data.read(); + #pragma clang loop unroll(full) + for (int j = 0; j < data_T::size; j++) { + //#pragma HLS UNROLL + data_array[i * data_T::size + j] = typename data_T::value_type(in_data[j]); + } + } + + for (int i = 0; i < CONFIG_T::N / res_T::size; i++) { + //#pragma HLS PIPELINE + res_T out_data; + PRAGMA_DATA_PACK(out_data) + #pragma clang loop unroll(full) + for (int j = 0; j < res_T::size; j++) { + //#pragma HLS UNROLL + out_data[j] = typename res_T::value_type(data_array[transfer_idx(i * res_T::size + j)]); + } + res.write(out_data); + } +} + +} // namespace nnet +#endif diff --git a/hls4ml/templates/bambu/nnet_utils/nnet_types.h b/hls4ml/templates/bambu/nnet_utils/nnet_types.h new file mode 100644 index 0000000000..b170c61141 --- /dev/null +++ b/hls4ml/templates/bambu/nnet_utils/nnet_types.h @@ -0,0 +1,83 @@ +#ifndef NNET_TYPES_H_ +#define NNET_TYPES_H_ + +#include +#include +#include +/// include required to work around a cosimulation problem TOBEFIXED +#include "ap_fixed.h" + +namespace nnet { + +// Fixed-size array +template struct array { + typedef T value_type; + static const unsigned size = N; + + T data[N]; + + T &operator[](size_t pos) { return data[pos]; } + + const T &operator[](size_t pos) const { return data[pos]; } + + array &operator=(const array &other) { + if (&other == this) + return *this; + + assert(N == other.size && "Array sizes must match."); + + #pragma clang loop unroll(full) + for (unsigned i = 0; i < N; i++) { + //#pragma HLS UNROLL + data[i] = other[i]; + } + return *this; + } + + bool operator==(const array &other) const { + if (N != other.size) { + return false; + } + + for (unsigned i = 0; i < N; i++) { + if (data[i] != other[i]) { + return false; + } + } + + return true; + } + + bool operator!=(const array &other) const { return !(*this == other); } +}; + +// Generic lookup-table implementation, for use in approximations of math functions +template class lookup_table { + public: + lookup_table(T from, T to) : range_start(from), range_end(to), base_div(ap_uint<16>(N) / T(to - from)) { + T step = (range_end - range_start) / ap_uint<16>(N); + for (size_t i = 0; i < N; i++) { + T num = range_start + ap_uint<16>(i) * step; + T sample = func(num); + samples[i] = sample; + } + } + + T operator()(T n) const { + int index = (n - range_start) * base_div; + if (index < 0) + index = 0; + else if (index > N - 1) + index = N - 1; + return samples[index]; + } + + private: + T samples[N]; + const T range_start, range_end; + ap_fixed<20, 16> base_div; +}; + +} // namespace nnet + +#endif diff --git a/hls4ml/templates/bambu_accelerator/rtl/AXISlaveParallel.v b/hls4ml/templates/bambu_accelerator/rtl/AXISlaveParallel.v new file mode 100644 index 0000000000..2f980b5b66 --- /dev/null +++ b/hls4ml/templates/bambu_accelerator/rtl/AXISlaveParallel.v @@ -0,0 +1,1212 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Filename: rtl/demofull.v +// {{{ +// Project: WB2AXIPSP: bus bridges and other odds and ends +// +// Purpose: Demonstrate a formally verified AXI4 core with a (basic) +// interface. This interface is explained below. +// +// Performance: This core has been designed for a total throughput of one beat +// per clock cycle. Both read and write channels can achieve +// this. The write channel will also introduce two clocks of latency, +// assuming no other latency from the master. This means it will take +// a minimum of 3+AWLEN clock cycles per transaction of (1+AWLEN) beats, +// including both address and acknowledgment cycles. The read channel +// will introduce a single clock of latency, requiring 2+ARLEN cycles +// per transaction of 1+ARLEN beats. +// +// Creator: Dan Gisselquist, Ph.D. +// Gisselquist Technology, LLC +// +//////////////////////////////////////////////////////////////////////////////// +// }}} +// Copyright (C) 2019-2025, Gisselquist Technology, LLC +// {{{ +// This file is part of the WB2AXIP project. +// +// The WB2AXIP project contains free software and gateware, licensed under the +// Apache License, Version 2.0 (the "License"). You may not use this project, +// or this file, except in compliance with the License. You may obtain a copy +// of the License at +// }}} +// http://www.apache.org/licenses/LICENSE-2.0 +// {{{ +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +//////////////////////////////////////////////////////////////////////////////// +// +`default_nettype none +// }}} +module AXISlaveParallel #( + // {{{ + parameter integer C_S_AXI_ID_WIDTH = 12, + parameter integer C_S_AXI_DATA_WIDTH = 128, + parameter integer C_S_AXI_ADDR_WIDTH = 40, + parameter [0:0] OPT_LOCK = 1'b0, + parameter [0:0] OPT_LOCKID = 1'b1, + parameter [0:0] OPT_LOWPOWER = 1'b0, + parameter HLS_IN_DATA_W = 16, + parameter HLS_OUT_DATA_W = 16, + parameter HLS_IN_N_WORDS = 8, + parameter HLS_OUT_N_WORDS = 8, + parameter HLS_IN_ADDR_W = 3, + parameter HLS_OUT_ADDR_W = 3 + // }}} + ) ( + // {{{ + // User ports + // {{{ + // A very basic protocol-independent peripheral interface + // 1. A value will be written any time o_we is true + // 2. A value will be read any time o_rd is true + // 3. Such a slave might just as easily be written as: + // + // always @(posedge S_AXI_ACLK) + // if (o_we) + // begin + // for(k=0; k= lock_start + && o_waddr <= lock_end + && o_wstrb != 0); + // }}} + + // lock_valid + // {{{ + initial lock_valid = 0; + always @(posedge S_AXI_ACLK) + if (!S_AXI_ARESETN || !OPT_LOCK) + lock_valid <= 0; + else begin + if (S_AXI_ARVALID && S_AXI_ARREADY + && S_AXI_ARLOCK && S_AXI_ARID== lock_id) + lock_valid <= 0; + if (w_cancel_lock) + lock_valid <= 0; + if (w_valid_lock_request) + lock_valid <= 1; + end + // }}} + + // lock_start, lock_end, lock_len, lock_size, lock_id + // {{{ + always @(posedge S_AXI_ACLK) + if (w_valid_lock_request) + begin + lock_start <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB]; + lock_end <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB] + + ((S_AXI_ARBURST == 2'b00) ? 0 : S_AXI_ARLEN[3:0]); + lock_len <= S_AXI_ARLEN[3:0]; + lock_burst <= S_AXI_ARBURST; + lock_size <= S_AXI_ARSIZE; + lock_id <= S_AXI_ARID; + end + // }}} + + // w_write_lock_valid + // {{{ + always @(*) + begin + w_write_lock_valid = returned_lock_valid; + if (!m_awvalid || !m_awready || !m_awlock || !lock_valid) + w_write_lock_valid = 0; + if (m_awaddr[C_S_AXI_ADDR_WIDTH-1:LSB] != lock_start) + w_write_lock_valid = 0; + if (m_awid != lock_id) + w_write_lock_valid = 0; + if (m_awlen[3:0] != lock_len) // MAX transfer size is 16 beats + w_write_lock_valid = 0; + if (m_awburst != 2'b01 && lock_len != 0) + w_write_lock_valid = 0; + if (m_awsize != lock_size) + w_write_lock_valid = 0; + end + // }}} + + assign write_lock_valid = w_write_lock_valid; + // }}} + end else if (OPT_LOCK) // && OPT_LOCKID + begin : EXCLUSIVE_ACCESS_PER_ID + // {{{ + + genvar gk; + wire [(1<= lock_start + && o_waddr <= lock_end + && o_wstrb != 0); + // }}} + + // lock_valid + // {{{ + initial lock_valid = 0; + always @(posedge S_AXI_ACLK) + if (!S_AXI_ARESETN || !OPT_LOCK) + lock_valid <= 0; + else begin + if (S_AXI_ARVALID && S_AXI_ARREADY + && S_AXI_ARLOCK + && S_AXI_ARID == gk[IW-1:0]) + lock_valid <= 0; + if (w_cancel_lock) + lock_valid <= 0; + if (w_valid_lock_request) + lock_valid <= 1; + end + // }}} + + // lock_start, lock_end, lock_len, lock_size + // {{{ + always @(posedge S_AXI_ACLK) + if (w_valid_lock_request) + begin + lock_start <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB]; + // Verilator lint_off WIDTH + lock_end <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB] + + ((S_AXI_ARBURST == 2'b00) ? 4'h0 : S_AXI_ARLEN[3:0]); + // Verilator lint_on WIDTH + lock_len <= S_AXI_ARLEN[3:0]; + lock_size <= S_AXI_ARSIZE; + lock_burst <= S_AXI_ARBURST; + end + // }}} + + // w_write_lock_valid + // {{{ + always @(*) + begin + w_write_lock_valid = returned_lock_valid; + if (!m_awvalid || !m_awready || !m_awlock || !lock_valid) + w_write_lock_valid = 0; + if (m_awaddr[C_S_AXI_ADDR_WIDTH-1:LSB] != lock_start) + w_write_lock_valid = 0; + if (m_awid[IW-1:0] != gk[IW-1:0]) + w_write_lock_valid = 0; + if (m_awlen[3:0] != lock_len) // MAX transfer size is 16 beats + w_write_lock_valid = 0; + if (m_awburst != 2'b01 && lock_len != 0) + w_write_lock_valid = 0; + if (m_awsize != lock_size) + w_write_lock_valid = 0; + end + // }}} + + assign write_lock_valid_per_id[gk]= w_write_lock_valid; + // }}} + end + + assign write_lock_valid = |write_lock_valid_per_id; + // }}} + end else begin : NO_LOCKING + // {{{ + + assign write_lock_valid = 1'b0; + // Verilator lint_off UNUSED + wire unused_lock; + assign unused_lock = &{ 1'b0, S_AXI_ARLOCK, S_AXI_AWLOCK }; + // Verilator lint_on UNUSED + // }}} + end endgenerate + // }}} + + // Make Verilator happy + // {{{ + // Verilator lint_off UNUSED + wire unused; + assign unused = &{ 1'b0, S_AXI_AWCACHE, S_AXI_AWPROT, S_AXI_AWQOS, + S_AXI_ARCACHE, S_AXI_ARPROT, S_AXI_ARQOS }; + // Verilator lint_on UNUSED + // }}} + + // ========================================== + // HLS BRAM INTERFACE + CONTROL FSM + // ========================================== + localparam WORDS_PER_IN_BEAT = C_S_AXI_DATA_WIDTH / HLS_IN_DATA_W; + localparam WORDS_PER_OUT_BEAT = C_S_AXI_DATA_WIDTH / HLS_OUT_DATA_W; + localparam N_BEATS_IN = (HLS_IN_N_WORDS + WORDS_PER_IN_BEAT - 1) / WORDS_PER_IN_BEAT; + localparam N_BEATS_OUT = (HLS_OUT_N_WORDS + WORDS_PER_OUT_BEAT - 1) / WORDS_PER_OUT_BEAT; + localparam N_BEATS_CYCLES = 1; + localparam N_BEATS_TOTAL = N_BEATS_OUT + N_BEATS_CYCLES; + + // ---- Generate-based per-word registers (avoids NanoXplore rfb inference) ---- + // Each word is its own reg in its own generate instance, with constant-index + // address decode. Works for arbitrary HLS_IN_N_WORDS / HLS_OUT_N_WORDS. + + // Flat buses collecting all individual word registers + wire [HLS_IN_N_WORDS*HLS_IN_DATA_W-1:0] input_flat; + wire [HLS_OUT_N_WORDS*HLS_OUT_DATA_W-1:0] output_flat; + + // Beat counter for multi-beat inputs + reg [$clog2(N_BEATS_IN+1)-1:0] beat_cnt; + + // Beat counter for multi-beat outputs + reg [$clog2(N_BEATS_TOTAL+1)-1:0] rd_beat_cnt; + + // FSM + localparam ST_IDLE = 2'd0; + localparam ST_RUNNING = 2'd1; + localparam ST_DRAIN = 2'd2; + + reg [1:0] state; + + // --- Inference cycle counter --- + reg counting; + reg [31:0] cycle_counter; + reg [31:0] inference_cycles; + wire hls_in_read = hls_in_ce0 || hls_in_ce1; + wire hls_out_write = (hls_out_ce0 && hls_out_we0) + || (hls_out_ce1 && hls_out_we1); + + // --- Input registers (DMA write → HLS read) --- + // Each word lives in its own generate block so synthesis sees N independent + // flip-flops, never an addressable register-file. + genvar gi; + generate + for (gi = 0; gi < HLS_IN_N_WORDS; gi = gi + 1) begin : gen_in + localparam BEAT_IDX = gi / WORDS_PER_IN_BEAT; // which AXI beat + localparam WORD_IDX = gi % WORDS_PER_IN_BEAT; // position in beat + reg [HLS_IN_DATA_W-1:0] r; + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) + r <= {HLS_IN_DATA_W{1'b0}}; + else if (o_we && (state == ST_IDLE) && (beat_cnt == BEAT_IDX)) + r <= o_wdata[WORD_IDX*HLS_IN_DATA_W +: HLS_IN_DATA_W]; + end + assign input_flat[gi*HLS_IN_DATA_W +: HLS_IN_DATA_W] = r; + end + endgenerate + + // --- Output registers (HLS write → DMA read) --- + // Dual write ports (ce0/we0, ce1/we1) each get constant-decoded enables. + // Port 0 has priority when both write the same word in the same cycle. + generate + for (gi = 0; gi < HLS_OUT_N_WORDS; gi = gi + 1) begin : gen_out + reg [HLS_OUT_DATA_W-1:0] r; + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) + r <= {HLS_OUT_DATA_W{1'b0}}; + else if (hls_out_ce0 && hls_out_we0 && (hls_out_addr0 == gi[HLS_OUT_ADDR_W-1:0])) + r <= hls_out_d0; + else if (hls_out_ce1 && hls_out_we1 && (hls_out_addr1 == gi[HLS_OUT_ADDR_W-1:0])) + r <= hls_out_d1; + end + assign output_flat[gi*HLS_OUT_DATA_W +: HLS_OUT_DATA_W] = r; + end + endgenerate + + // --- FSM (unchanged logic, no per-word references) --- + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) begin + state <= ST_IDLE; + start_out <= 1'b0; + result_valid <= 1'b0; + beat_cnt <= 0; + end else begin + start_out <= 1'b0; + case (state) + ST_IDLE: begin + if (o_we) begin + // Input capture handled by gen_in always blocks above + if (beat_cnt == N_BEATS_IN - 1) begin + beat_cnt <= 0; + result_valid <= 1'b0; + start_out <= 1'b1; + state <= ST_RUNNING; + end else begin + beat_cnt <= beat_cnt + 1'b1; + end + end + end + ST_RUNNING: begin + if (done_in) state <= ST_DRAIN; + end + ST_DRAIN: begin + result_valid <= 1'b1; + state <= ST_IDLE; + end + endcase + end + end + + // --- Cycle counter: counts from first HLS input read to last HLS output write --- + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) begin + counting <= 1'b0; + cycle_counter <= 32'd0; + inference_cycles <= 32'd0; + end else if (start_out) begin + counting <= 1'b0; + cycle_counter <= 32'd0; + end else begin + if (!counting && (state == ST_RUNNING) && hls_in_read) begin + counting <= 1'b1; + cycle_counter <= 32'd1; + end else if (counting && (state == ST_RUNNING)) begin + cycle_counter <= cycle_counter + 1'b1; + end + if (counting && hls_out_write) + inference_cycles <= cycle_counter; + if (state == ST_DRAIN) + counting <= 1'b0; + end + end + + // --- Synchronous input reads (HLS ports 0/1 → MUX from flat bus) --- + always @(posedge S_AXI_ACLK) begin + if (hls_in_ce0) + hls_in_q0 <= input_flat[hls_in_addr0*HLS_IN_DATA_W +: HLS_IN_DATA_W]; + if (hls_in_ce1) + hls_in_q1 <= input_flat[hls_in_addr1*HLS_IN_DATA_W +: HLS_IN_DATA_W]; + end + + // --- rd_beat_cnt: advances through output beats on each o_rd --- + // Reset to 0 at the end of each computation (ST_DRAIN) so reads always + // start from beat 0, even if the previous cycle read a partial burst. + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) + rd_beat_cnt <= 0; + else if (state == ST_DRAIN) + rd_beat_cnt <= 0; + else if (o_rd) begin + if (rd_beat_cnt == N_BEATS_TOTAL - 1) + rd_beat_cnt <= 0; + else + rd_beat_cnt <= rd_beat_cnt + 1'b1; + end + end + + // --- output_flat_padded: zero-extend to a whole number of AXI beats --- + // When HLS_OUT_N_WORDS is not a multiple of WORDS_PER_BEAT the plain + // output_flat wire is narrower than N_BEATS_OUT*C_S_AXI_DATA_WIDTH. + // Variable part-selects beyond the wire's declared width return X in + // Icarus, so we pad the MSBs with zeros here. + localparam OUT_FLAT_W = HLS_OUT_N_WORDS * HLS_OUT_DATA_W; + localparam OUT_PAD_W = N_BEATS_OUT * C_S_AXI_DATA_WIDTH; + wire [OUT_PAD_W-1:0] output_flat_padded; + generate + if (OUT_PAD_W > OUT_FLAT_W) begin : gen_out_pad + assign output_flat_padded = + {{(OUT_PAD_W - OUT_FLAT_W){1'b0}}, output_flat}; + end else begin : gen_out_nopad + assign output_flat_padded = output_flat; + end + endgenerate + + // --- i_rdata: registered, only changes on o_rd --- + wire [C_S_AXI_DATA_WIDTH-1:0] cycles_beat = + { {(C_S_AXI_DATA_WIDTH-32){1'b0}}, inference_cycles }; + + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) + i_rdata <= {C_S_AXI_DATA_WIDTH{1'b0}}; + else if (o_rd) begin + if (!result_valid) + i_rdata <= {(C_S_AXI_DATA_WIDTH/32){32'hDEADBEEF}}; + else if (rd_beat_cnt == N_BEATS_OUT) + i_rdata <= cycles_beat; + else + i_rdata <= output_flat_padded[rd_beat_cnt * C_S_AXI_DATA_WIDTH +: C_S_AXI_DATA_WIDTH]; + end + end + + assign o_interrupt = result_valid; + +endmodule +`ifndef YOSYS +`default_nettype wire +`endif diff --git a/hls4ml/templates/bambu_accelerator/rtl/AXISlaveStream.v b/hls4ml/templates/bambu_accelerator/rtl/AXISlaveStream.v new file mode 100644 index 0000000000..bf2777987b --- /dev/null +++ b/hls4ml/templates/bambu_accelerator/rtl/AXISlaveStream.v @@ -0,0 +1,1298 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Filename: rtl/demofull.v +// {{{ +// Project: WB2AXIPSP: bus bridges and other odds and ends +// +// Purpose: Demonstrate a formally verified AXI4 core with a (basic) +// interface. This interface is explained below. +// +// Performance: This core has been designed for a total throughput of one beat +// per clock cycle. Both read and write channels can achieve +// this. The write channel will also introduce two clocks of latency, +// assuming no other latency from the master. This means it will take +// a minimum of 3+AWLEN clock cycles per transaction of (1+AWLEN) beats, +// including both address and acknowledgment cycles. The read channel +// will introduce a single clock of latency, requiring 2+ARLEN cycles +// per transaction of 1+ARLEN beats. +// +// Creator: Dan Gisselquist, Ph.D. +// Gisselquist Technology, LLC +// +//////////////////////////////////////////////////////////////////////////////// +// }}} +// Copyright (C) 2019-2025, Gisselquist Technology, LLC +// {{{ +// This file is part of the WB2AXIP project. +// +// The WB2AXIP project contains free software and gateware, licensed under the +// Apache License, Version 2.0 (the "License"). You may not use this project, +// or this file, except in compliance with the License. You may obtain a copy +// of the License at +// }}} +// http://www.apache.org/licenses/LICENSE-2.0 +// {{{ +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +//////////////////////////////////////////////////////////////////////////////// +// +`default_nettype none +// }}} +module AXISlaveStream #( + // {{{ + parameter integer C_S_AXI_ID_WIDTH = 12, + parameter integer C_S_AXI_DATA_WIDTH = 128, + parameter integer C_S_AXI_ADDR_WIDTH = 40, + parameter [0:0] OPT_LOCK = 1'b0, + parameter [0:0] OPT_LOCKID = 1'b1, + parameter [0:0] OPT_LOWPOWER = 1'b0, + parameter HLS_IN_DATA_W = 16, + parameter HLS_OUT_DATA_W = 16, + parameter HLS_IN_N_WORDS = 8, + parameter HLS_OUT_N_WORDS = 8, + parameter LGFLEN_IN = 4, + parameter LGFLEN_OUT = 4 + // }}} + ) ( + // {{{ + // User ports + // {{{ + // A very basic protocol-independent peripheral interface + // 1. A value will be written any time o_we is true + // 2. A value will be read any time o_rd is true + // 3. Such a slave might just as easily be written as: + // + // always @(posedge S_AXI_ACLK) + // if (o_we) + // begin + // for(k=0; k= lock_start + && o_waddr <= lock_end + && o_wstrb != 0); + // }}} + + // lock_valid + // {{{ + initial lock_valid = 0; + always @(posedge S_AXI_ACLK) + if (!S_AXI_ARESETN || !OPT_LOCK) + lock_valid <= 0; + else begin + if (S_AXI_ARVALID && S_AXI_ARREADY + && S_AXI_ARLOCK && S_AXI_ARID== lock_id) + lock_valid <= 0; + if (w_cancel_lock) + lock_valid <= 0; + if (w_valid_lock_request) + lock_valid <= 1; + end + // }}} + + // lock_start, lock_end, lock_len, lock_size, lock_id + // {{{ + always @(posedge S_AXI_ACLK) + if (w_valid_lock_request) + begin + lock_start <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB]; + lock_end <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB] + + ((S_AXI_ARBURST == 2'b00) ? 0 : S_AXI_ARLEN[3:0]); + lock_len <= S_AXI_ARLEN[3:0]; + lock_burst <= S_AXI_ARBURST; + lock_size <= S_AXI_ARSIZE; + lock_id <= S_AXI_ARID; + end + // }}} + + // w_write_lock_valid + // {{{ + always @(*) + begin + w_write_lock_valid = returned_lock_valid; + if (!m_awvalid || !m_awready || !m_awlock || !lock_valid) + w_write_lock_valid = 0; + if (m_awaddr[C_S_AXI_ADDR_WIDTH-1:LSB] != lock_start) + w_write_lock_valid = 0; + if (m_awid != lock_id) + w_write_lock_valid = 0; + if (m_awlen[3:0] != lock_len) // MAX transfer size is 16 beats + w_write_lock_valid = 0; + if (m_awburst != 2'b01 && lock_len != 0) + w_write_lock_valid = 0; + if (m_awsize != lock_size) + w_write_lock_valid = 0; + end + // }}} + + assign write_lock_valid = w_write_lock_valid; + // }}} + end else if (OPT_LOCK) // && OPT_LOCKID + begin : EXCLUSIVE_ACCESS_PER_ID + // {{{ + + genvar gk; + wire [(1<= lock_start + && o_waddr <= lock_end + && o_wstrb != 0); + // }}} + + // lock_valid + // {{{ + initial lock_valid = 0; + always @(posedge S_AXI_ACLK) + if (!S_AXI_ARESETN || !OPT_LOCK) + lock_valid <= 0; + else begin + if (S_AXI_ARVALID && S_AXI_ARREADY + && S_AXI_ARLOCK + && S_AXI_ARID == gk[IW-1:0]) + lock_valid <= 0; + if (w_cancel_lock) + lock_valid <= 0; + if (w_valid_lock_request) + lock_valid <= 1; + end + // }}} + + // lock_start, lock_end, lock_len, lock_size + // {{{ + always @(posedge S_AXI_ACLK) + if (w_valid_lock_request) + begin + lock_start <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB]; + // Verilator lint_off WIDTH + lock_end <= S_AXI_ARADDR[C_S_AXI_ADDR_WIDTH-1:LSB] + + ((S_AXI_ARBURST == 2'b00) ? 4'h0 : S_AXI_ARLEN[3:0]); + // Verilator lint_on WIDTH + lock_len <= S_AXI_ARLEN[3:0]; + lock_size <= S_AXI_ARSIZE; + lock_burst <= S_AXI_ARBURST; + end + // }}} + + // w_write_lock_valid + // {{{ + always @(*) + begin + w_write_lock_valid = returned_lock_valid; + if (!m_awvalid || !m_awready || !m_awlock || !lock_valid) + w_write_lock_valid = 0; + if (m_awaddr[C_S_AXI_ADDR_WIDTH-1:LSB] != lock_start) + w_write_lock_valid = 0; + if (m_awid[IW-1:0] != gk[IW-1:0]) + w_write_lock_valid = 0; + if (m_awlen[3:0] != lock_len) // MAX transfer size is 16 beats + w_write_lock_valid = 0; + if (m_awburst != 2'b01 && lock_len != 0) + w_write_lock_valid = 0; + if (m_awsize != lock_size) + w_write_lock_valid = 0; + end + // }}} + + assign write_lock_valid_per_id[gk]= w_write_lock_valid; + // }}} + end + + assign write_lock_valid = |write_lock_valid_per_id; + // }}} + end else begin : NO_LOCKING + // {{{ + + assign write_lock_valid = 1'b0; + // Verilator lint_off UNUSED + wire unused_lock; + assign unused_lock = &{ 1'b0, S_AXI_ARLOCK, S_AXI_AWLOCK }; + // Verilator lint_on UNUSED + // }}} + end endgenerate + // }}} + + // Make Verilator happy + // {{{ + // Verilator lint_off UNUSED + wire unused; + assign unused = &{ 1'b0, S_AXI_AWCACHE, S_AXI_AWPROT, S_AXI_AWQOS, + S_AXI_ARCACHE, S_AXI_ARPROT, S_AXI_ARQOS }; + // Verilator lint_on UNUSED + // }}} + + // ========================================== + // HLS AXIS FIFO INTERFACE + CONTROL FSM + // ========================================== + localparam WORDS_PER_IN_BEAT = C_S_AXI_DATA_WIDTH / HLS_IN_DATA_W; + localparam WORDS_PER_OUT_BEAT = C_S_AXI_DATA_WIDTH / HLS_OUT_DATA_W; + + // Per-inference beat counts and the valid-word count of the LAST beat. + // hls4ml io_stream networks often have N_IN/N_OUT that don't align to the + // AXI beat width (e.g. N_IN=10 with WORDS_PER_IN_BEAT=8 → 2 beats, last + // beat carries only 2 valid words). We track the beat index within the + // current inference to drop padding words on write and flush partial + // beats on read. + localparam N_BEATS_IN = (HLS_IN_N_WORDS + WORDS_PER_IN_BEAT - 1) / WORDS_PER_IN_BEAT; + localparam N_BEATS_OUT = (HLS_OUT_N_WORDS + WORDS_PER_OUT_BEAT - 1) / WORDS_PER_OUT_BEAT; + localparam LAST_BEAT_IN_VALID = HLS_IN_N_WORDS - (N_BEATS_IN - 1) * WORDS_PER_IN_BEAT; + localparam LAST_BEAT_OUT_VALID = HLS_OUT_N_WORDS - (N_BEATS_OUT - 1) * WORDS_PER_OUT_BEAT; + + // FSM + localparam ST_IDLE = 2'd0; + localparam ST_STREAMING = 2'd1; + localparam ST_DONE = 2'd2; + + reg [1:0] state; + + // --- Inference cycle counter --- + reg [31:0] cycle_counter; + reg [31:0] inference_cycles; + + // Note on OPT_ASYNC_READ=0 below: + // NxMap auto-maps small sfifo memories (16x16 bits) into NX_RFB + // register-file primitives, which have a SYNCHRONOUS read port. + // The async-read variant of sfifo assumes combinational memory + // output, so on silicon it reads mem[rd_addr-1] one cycle late — + // the entire input stream is silently corrupted. The sync-read + // path includes a bypass register that matches NX_RFB behaviour + // and preserves correctness at the cost of one cycle of read + // latency. + + // --- Input FIFO (AXI W → HLS AXIS in) --- + wire in_fifo_i_wr; + wire [HLS_IN_DATA_W-1:0] in_fifo_i_data; + wire in_fifo_o_empty; + wire in_fifo_o_full; + wire [HLS_IN_DATA_W-1:0] in_fifo_o_data; + wire in_fifo_i_rd; + + sfifo #(.BW(HLS_IN_DATA_W), .LGFLEN(LGFLEN_IN), .OPT_ASYNC_READ(1'b0)) + u_in_fifo ( + .i_clk(S_AXI_ACLK), .i_reset(!S_AXI_ARESETN), + .i_wr(in_fifo_i_wr), .i_data(in_fifo_i_data), + .o_full(in_fifo_o_full), .o_fill(), + .i_rd(in_fifo_i_rd), .o_data(in_fifo_o_data), + .o_empty(in_fifo_o_empty) + ); + + // --- Output FIFO (HLS AXIS out → AXI R) --- + wire out_fifo_i_wr; + wire [HLS_OUT_DATA_W-1:0] out_fifo_i_data; + wire out_fifo_o_empty; + wire out_fifo_o_full; + wire [HLS_OUT_DATA_W-1:0] out_fifo_o_data; + wire out_fifo_i_rd; + + sfifo #(.BW(HLS_OUT_DATA_W), .LGFLEN(LGFLEN_OUT), .OPT_ASYNC_READ(1'b0)) + u_out_fifo ( + .i_clk(S_AXI_ACLK), .i_reset(!S_AXI_ARESETN), + .i_wr(out_fifo_i_wr), .i_data(out_fifo_i_data), + .o_full(out_fifo_o_full), .o_fill(), + .i_rd(out_fifo_i_rd), .o_data(out_fifo_o_data), + .o_empty(out_fifo_o_empty) + ); + + // --- Write path: unpack one AXI beat into WORDS_PER_IN_BEAT FIFO entries --- + // + // A dedicated pipeline register (wr_stage) sits between the skidbuffer + // output (o_wdata, registered) and the unpacker's wr_hold latch. The + // direct o_wdata → wr_hold path is short enough on NG-Ultra that clock + // skew between distant flops exceeds data-path delay, producing marginal + // hold-time violations (~57 ps) that NxMap's STA cannot absorb. Running + // the data through an explicit register stage turns the critical path + // into flop-to-flop-to-flop, which always meets hold by construction. + // The cost is one extra cycle of AXI-write-to-FIFO latency. + reg wr_unpacking; + reg wr_stage_valid; + reg [C_S_AXI_DATA_WIDTH-1:0] wr_stage; + reg [$clog2(WORDS_PER_IN_BEAT)-1:0] wr_word_idx; + reg [C_S_AXI_DATA_WIDTH-1:0] wr_hold; + + // Which beat of the current inference we are unpacking. Wraps back to 0 + // once the last beat of the inference is fully consumed so the next + // AXI-queued inference starts fresh. + reg [$clog2(N_BEATS_IN > 1 ? N_BEATS_IN : 2)-1:0] wr_beat_idx; + wire wr_is_last_beat = (wr_beat_idx == N_BEATS_IN - 1); + // How many words in THIS beat actually carry payload (the rest are padding + // zeros the testbench/DMA wrote to fill out the AXI beat). + wire [$clog2(WORDS_PER_IN_BEAT+1)-1:0] wr_valid_words_this_beat = + wr_is_last_beat ? LAST_BEAT_IN_VALID[$clog2(WORDS_PER_IN_BEAT+1)-1:0] + : WORDS_PER_IN_BEAT[$clog2(WORDS_PER_IN_BEAT+1)-1:0]; + wire wr_word_valid = ({1'b0, wr_word_idx} < wr_valid_words_this_beat); + + // Stage 1: absorb an AXI beat into wr_stage the cycle o_we fires. + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) begin + wr_stage_valid <= 1'b0; + wr_stage <= 0; + end else if (o_we) begin + wr_stage_valid <= 1'b1; + wr_stage <= o_wdata; + end else if (!wr_unpacking) begin + // Consumed by stage 2 below; clear only when stage 2 has latched. + wr_stage_valid <= 1'b0; + end + end + + // Stage 2: move wr_stage into wr_hold when the unpacker is idle. + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) begin + wr_unpacking <= 1'b0; + wr_word_idx <= 0; + wr_hold <= 0; + wr_beat_idx <= 0; + end else if (wr_stage_valid && !wr_unpacking) begin + wr_hold <= wr_stage; + wr_unpacking <= 1'b1; + wr_word_idx <= 0; + end else if (wr_unpacking && !in_fifo_o_full) begin + if (wr_word_idx == WORDS_PER_IN_BEAT - 1) begin + wr_unpacking <= 1'b0; + wr_word_idx <= 0; + wr_beat_idx <= wr_is_last_beat ? {$clog2(N_BEATS_IN > 1 ? N_BEATS_IN : 2){1'b0}} + : (wr_beat_idx + 1'b1); + end else begin + wr_word_idx <= wr_word_idx + 1'b1; + end + end + end + + // Only push words that carry real payload. The last beat of an + // inference with misaligned N_IN silently drops its padding slots. + assign in_fifo_i_wr = wr_unpacking && !in_fifo_o_full && wr_word_valid; + assign in_fifo_i_data = wr_hold[wr_word_idx * HLS_IN_DATA_W +: HLS_IN_DATA_W]; + // Back-pressure when stage 1 already holds an unconsumed beat, when the + // narrow FIFO is full, or when a beat is being accepted this cycle. + // Note: wr_unpacking alone no longer back-pressures — stage 1 (wr_stage) + // absorbs the next AXI beat concurrently with stage 2's word-by-word push. + assign wr_full = wr_stage_valid || in_fifo_o_full || o_we; + + // --- AXIS input wiring (input FIFO → HLS IP) --- + assign hls_in_tdata = in_fifo_o_data; + assign hls_in_tvalid = !in_fifo_o_empty; + assign in_fifo_i_rd = hls_in_tready && !in_fifo_o_empty; + + // --- AXIS output wiring (HLS IP → output FIFO) --- + // NOTE: the per-inference capture counter (out_words_captured) was tried + // here but the corresponding bitstream reproduced the NG-Ultra config + // fault. Reverted for now; if spurious pre/post IP outputs turn out to + // pollute the FIFO boundary, we'll re-introduce it with a different + // synthesis seed or rewrite it to share state with the FSM. + assign out_fifo_i_wr = hls_out_tvalid && !out_fifo_o_full; + assign out_fifo_i_data = hls_out_tdata; + assign hls_out_tready = !out_fifo_o_full; + + // --- Read path: pack FIFO entries into AXI beats --- + // For the LAST output beat of an inference (when N_OUT isn't a multiple + // of WORDS_PER_OUT_BEAT) we flush with fewer than WORDS_PER_OUT_BEAT + // words so the reader never hangs waiting on padding that will never come. + reg [$clog2(WORDS_PER_OUT_BEAT)-1:0] rd_pack_idx; + reg rd_beat_ready; + reg [C_S_AXI_DATA_WIDTH-1:0] rd_data_assembled; + + // Which output beat of the current inference we are assembling. + reg [$clog2(N_BEATS_OUT > 1 ? N_BEATS_OUT : 2)-1:0] rd_beat_idx; + wire rd_is_last_beat = (rd_beat_idx == N_BEATS_OUT - 1); + wire [$clog2(WORDS_PER_OUT_BEAT+1)-1:0] rd_pack_target = + rd_is_last_beat ? LAST_BEAT_OUT_VALID[$clog2(WORDS_PER_OUT_BEAT+1)-1:0] + : WORDS_PER_OUT_BEAT[$clog2(WORDS_PER_OUT_BEAT+1)-1:0]; + + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) begin + rd_pack_idx <= 0; + rd_beat_ready <= 1'b0; + rd_data_assembled <= 0; + rd_beat_idx <= 0; + end else if (rd_beat_ready && o_rd) begin + rd_beat_ready <= 1'b0; + rd_pack_idx <= 0; + // Clear so padding slots in the next (possibly short) beat read + // back as zero rather than leaking bits from the prior beat. + rd_data_assembled <= 0; + rd_beat_idx <= rd_is_last_beat + ? {$clog2(N_BEATS_OUT > 1 ? N_BEATS_OUT : 2){1'b0}} + : (rd_beat_idx + 1'b1); + end else if (!rd_beat_ready && !out_fifo_o_empty) begin + rd_data_assembled[rd_pack_idx * HLS_OUT_DATA_W +: HLS_OUT_DATA_W] <= out_fifo_o_data; + // Flush when we've filled the target for this beat — either a + // full beat or the short last-beat of an inference. + if (({1'b0, rd_pack_idx} + 1'b1) == rd_pack_target) begin + rd_beat_ready <= 1'b1; + rd_pack_idx <= 0; + end else begin + rd_pack_idx <= rd_pack_idx + 1'b1; + end + end + end + + assign out_fifo_i_rd = !rd_beat_ready && !out_fifo_o_empty; + + // --- rd_phase: RD_DATA serves output beats; RD_CYCLES serves cycle counter --- + localparam RD_DATA = 1'b0; + localparam RD_CYCLES = 1'b1; + reg rd_phase; + reg cycles_served; + + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN || state == ST_IDLE) begin + rd_phase <= RD_DATA; + cycles_served <= 1'b0; + end else begin + if (rd_phase == RD_DATA && state == ST_DONE + && out_fifo_o_empty && !rd_beat_ready) + rd_phase <= RD_CYCLES; + else if (rd_phase == RD_CYCLES && o_rd) begin + rd_phase <= RD_DATA; + cycles_served <= 1'b1; + end + end + end + + assign rd_empty = (rd_phase == RD_DATA) ? !rd_beat_ready : 1'b0; + + // --- FSM --- + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) begin + state <= ST_IDLE; + start_out <= 1'b0; + end else begin + start_out <= 1'b0; + case (state) + ST_IDLE: begin + // Start when either an AXI write is in flight or the + // input FIFO already holds data from a previously + // back-pressured inference. + if (o_we || !in_fifo_o_empty) begin + start_out <= 1'b1; + state <= ST_STREAMING; + end + end + ST_STREAMING: begin + if (done_in) state <= ST_DONE; + end + ST_DONE: begin + if (cycles_served) state <= ST_IDLE; + end + endcase + end + end + + // --- Cycle counter: starts on start_out, captured on done_in --- + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) begin + cycle_counter <= 32'd0; + inference_cycles <= 32'd0; + end else if (start_out) begin + cycle_counter <= 32'd0; + end else if (state == ST_STREAMING) begin + cycle_counter <= cycle_counter + 1'b1; + if (done_in) + inference_cycles <= cycle_counter; + end + end + + // --- i_rdata: registered, only changes on o_rd --- + wire [C_S_AXI_DATA_WIDTH-1:0] cycles_beat = + { {(C_S_AXI_DATA_WIDTH-32){1'b0}}, inference_cycles }; + + always @(posedge S_AXI_ACLK) begin + if (!S_AXI_ARESETN) + i_rdata <= {C_S_AXI_DATA_WIDTH{1'b0}}; + else if (o_rd) begin + if (rd_phase == RD_CYCLES) + i_rdata <= cycles_beat; + else + i_rdata <= rd_data_assembled; + end + end + + assign o_interrupt = (state == ST_DONE); + +endmodule +`ifndef YOSYS +`default_nettype wire +`endif diff --git a/hls4ml/templates/bambu_accelerator/rtl/axi_addr.v b/hls4ml/templates/bambu_accelerator/rtl/axi_addr.v new file mode 100644 index 0000000000..f2c9027d22 --- /dev/null +++ b/hls4ml/templates/bambu_accelerator/rtl/axi_addr.v @@ -0,0 +1,235 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Filename: rtl/axi_addr.v +// {{{ +// Project: WB2AXIPSP: bus bridges and other odds and ends +// +// Purpose: The AXI (full) standard has some rather complicated addressing +// modes, where the address can either be FIXED, INCRementing, or +// even where it can WRAP around some boundary. When in either INCR or +// WRAP modes, the next address must always be aligned. In WRAP mode, +// the next address calculation needs to wrap around a given value, and +// that value is dependent upon the burst size (i.e. bytes per beat) and +// length (total numbers of beats). Since this calculation can be +// non-trivial, and since it needs to be done multiple times, the logic +// below captures it for every time it might be needed. +// +// 20200918 - modified to accommodate (potential) AXI3 burst lengths +// +// Creator: Dan Gisselquist, Ph.D. +// Gisselquist Technology, LLC +// +//////////////////////////////////////////////////////////////////////////////// +// }}} +// Copyright (C) 2019-2025, Gisselquist Technology, LLC +// {{{ +// This file is part of the WB2AXIP project. +// +// The WB2AXIP project contains free software and gateware, licensed under the +// Apache License, Version 2.0 (the "License"). You may not use this project, +// or this file, except in compliance with the License. You may obtain a copy +// of the License at +// }}} +// http://www.apache.org/licenses/LICENSE-2.0 +// {{{ +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +//////////////////////////////////////////////////////////////////////////////// +// +`default_nettype none +// }}} +module axi_addr #( + // {{{ + parameter AW = 32, + DW = 32, + // parameter [0:0] OPT_AXI3 = 1'b0, + parameter LENB = 8 + // }}} + ) ( + // {{{ + input wire [AW-1:0] i_last_addr, + input wire [2:0] i_size, // 1b, 2b, 4b, 8b, etc + input wire [1:0] i_burst, // fixed, incr, wrap, reserved + input wire [LENB-1:0] i_len, + output wire [AW-1:0] o_next_addr + // }}} + ); + + // Parameter/register declarations + // {{{ + localparam DSZ = $clog2(DW)-3; + localparam [1:0] FIXED = 2'b00; + // localparam [1:0] INCREMENT = 2'b01; + // localparam [1:0] WRAP = 2'b10; + localparam IN_AW = (AW >= 12) ? 12 : AW; + localparam [IN_AW-1:0] ONE = 1; + + reg [IN_AW-1:0] wrap_mask, increment; + reg [IN_AW-1:0] crossblk_addr, aligned_addr, unaligned_addr; + // }}} + + // Address increment + // {{{ + always @(*) + if (DSZ == 0) + increment = 1; + else if (DSZ == 1) + increment = (i_size[0]) ? 2 : 1; + else if (DSZ == 2) + increment = (i_size[1]) ? 4 : ((i_size[0]) ? 2 : 1); + else if (DSZ == 3) + case(i_size[1:0]) + 2'b00: increment = 1; + 2'b01: increment = 2; + 2'b10: increment = 4; + 2'b11: increment = 8; + endcase + else + increment = (ONE<1) ? 1 : (AW-1):0]= 0; + 2'b11: aligned_addr[(AW-1>2) ? 2 : (AW-1):0]= 0; + endcase + // }}} + end else begin + // {{{ + // Align any subsequent address + case(i_size) + 3'b001: aligned_addr[ 0] = 0; + 3'b010: aligned_addr[(AW-1>1) ? 1 : (AW-1):0]=0; + 3'b011: aligned_addr[(AW-1>2) ? 2 : (AW-1):0]=0; + 3'b100: aligned_addr[(AW-1>3) ? 3 : (AW-1):0]=0; + 3'b101: aligned_addr[(AW-1>4) ? 4 : (AW-1):0]=0; + 3'b110: aligned_addr[(AW-1>5) ? 5 : (AW-1):0]=0; + 3'b111: aligned_addr[(AW-1>6) ? 6 : (AW-1):0]=0; + default: aligned_addr = unaligned_addr; + endcase + // }}} + end + // }}} + end else + aligned_addr = i_last_addr[IN_AW-1:0]; + // }}} + + // crossblk_addr from aligned_addr, for WRAP addressing + // {{{ + always @(*) + if (i_burst[1]) + begin + // WRAP! + crossblk_addr[IN_AW-1:0] = (i_last_addr[IN_AW-1:0] & ~wrap_mask) + | (aligned_addr & wrap_mask); + end else + crossblk_addr[IN_AW-1:0] = aligned_addr; + // }}} + + // o_next_addr: Guarantee only the bottom 12 bits change + // {{{ + // This is really a logic simplification. AXI bursts aren't allowed + // to cross 4kB boundaries. Given that's the case, we don't have to + // suffer from the propagation across all AW bits, and can limit any + // address propagation to just the lower 12 bits + generate if (AW > 12) + begin : WIDE_ADDRESS + assign o_next_addr = { i_last_addr[AW-1:12], + crossblk_addr[11:0] }; + end else begin : NARROW_ADDRESS + assign o_next_addr = crossblk_addr[AW-1:0]; + end endgenerate + // }}} + + // Make Verilator happy + // {{{ + // Verilator lint_off UNUSED + wire unused; + assign unused = (LENB <= 4) ? &{1'b0, i_len[0] } + : &{ 1'b0, i_len[LENB-1:4], i_len[0] }; + // Verilator lint_on UNUSED + // }}} +endmodule +`default_nettype wire diff --git a/hls4ml/templates/bambu_accelerator/rtl/sfifo.v b/hls4ml/templates/bambu_accelerator/rtl/sfifo.v new file mode 100644 index 0000000000..21b70135c6 --- /dev/null +++ b/hls4ml/templates/bambu_accelerator/rtl/sfifo.v @@ -0,0 +1,241 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Filename: sfifo.v +// {{{ +// Project: WB2AXIPSP: bus bridges and other odds and ends +// +// Purpose: A synchronous data FIFO. +// +// Creator: Dan Gisselquist, Ph.D. +// Gisselquist Technology, LLC +// +//////////////////////////////////////////////////////////////////////////////// +// +// Written and distributed by Gisselquist Technology, LLC +// }}} +// This design is hereby granted to the public domain. +// {{{ +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +//////////////////////////////////////////////////////////////////////////////// +// +`default_nettype none +// }}} +module sfifo #( + // {{{ + parameter BW=8, // Byte/data width + parameter LGFLEN=4, + parameter [0:0] OPT_ASYNC_READ = 1'b1, + parameter [0:0] OPT_WRITE_ON_FULL = 1'b0, + parameter [0:0] OPT_READ_ON_EMPTY = 1'b0 + // }}} + ) ( + // {{{ + input wire i_clk, + input wire i_reset, + // + // Write interface + input wire i_wr, + input wire [(BW-1):0] i_data, + output wire o_full, + output reg [LGFLEN:0] o_fill, + // + // Read interface + input wire i_rd, + output reg [(BW-1):0] o_data, + output wire o_empty // True if FIFO is empty + // }}} + ); + + // Register/net declarations + // {{{ + localparam FLEN=(1< !i_valid; + endproperty + + property IDATA_HELD_WHEN_NOT_READY; + @(posedge i_clk) disable iff (i_reset) + i_valid && !o_ready |=> i_valid && $stable(i_data); + endproperty + +`ifdef SKIDBUFFER + assume property (IDATA_HELD_WHEN_NOT_READY); +`else + assert property (IDATA_HELD_WHEN_NOT_READY); +`endif +`endif + // }}} + //////////////////////////////////////////////////////////////////////// + // + // Outgoing stream properties / assumptions + // {{{ + //////////////////////////////////////////////////////////////////////// + // + + generate if (!OPT_PASSTHROUGH) + begin + + always @(posedge i_clk) + if (!f_past_valid) // || $past(i_reset)) + begin + // Following any reset, valid must be deasserted + assert(!o_valid || !OPT_INITIAL); + end else if ($past(o_valid && !i_ready && !i_reset) && !i_reset) + // Following any stall, valid must remain high and + // data must be preserved + assert(o_valid && $stable(o_data)); + + end endgenerate + // }}} + //////////////////////////////////////////////////////////////////////// + // + // Other properties + // {{{ + //////////////////////////////////////////////////////////////////////// + // + // + generate if (!OPT_PASSTHROUGH) + begin + // Rule #1: + // If registered, then following any reset we should be + // ready for a new request + // {{{ + always @(posedge i_clk) + if (f_past_valid && $past(OPT_OUTREG && i_reset)) + assert(o_ready); + // }}} + + // Rule #2: + // All incoming data must either go directly to the + // output port, or into the skid buffer + // {{{ +`ifndef VERIFIC + always @(posedge i_clk) + if (f_past_valid && !$past(i_reset) && $past(i_valid && o_ready + && (!OPT_OUTREG || o_valid) && !i_ready)) + assert(!o_ready && w_data == $past(i_data)); +`else + assert property (@(posedge i_clk) + disable iff (i_reset) + (i_valid && o_ready + && (!OPT_OUTREG || o_valid) && !i_ready) + |=> (!o_ready && w_data == $past(i_data))); +`endif + // }}} + + // Rule #3: + // After the last transaction, o_valid should become idle + // {{{ + if (!OPT_OUTREG) + begin + // {{{ + always @(posedge i_clk) + if (f_past_valid && !$past(i_reset) && !i_reset + && $past(i_ready)) + begin + assert(o_valid == i_valid); + assert(!i_valid || (o_data == i_data)); + end + // }}} + end else begin + // {{{ + always @(posedge i_clk) + if (f_past_valid && !$past(i_reset)) + begin + if ($past(i_valid && o_ready)) + assert(o_valid); + + if ($past(!i_valid && o_ready && i_ready)) + assert(!o_valid); + end + // }}} + end + // }}} + + // Rule #4 + // Same thing, but this time for o_ready + // {{{ + always @(posedge i_clk) + if (f_past_valid && $past(!o_ready && i_ready)) + assert(o_ready); + // }}} + + // If OPT_LOWPOWER is set, o_data and w_data both need to be + // zero any time !o_valid or !r_valid respectively + // {{{ + if (OPT_LOWPOWER) + begin + always @(*) + if ((OPT_OUTREG || !i_reset) && !o_valid) + assert(o_data == 0); + + always @(*) + if (o_ready) + assert(w_data == 0); + + end + // }}} + end endgenerate + // }}} + + always @(posedge i_clk) + if (!OPT_PASSTHROUGH && !i_reset && !o_ready) + assert(o_valid); + + //////////////////////////////////////////////////////////////////////// + // + // Cover checks + // {{{ + //////////////////////////////////////////////////////////////////////// + // + // +`ifdef SKIDBUFFER + generate if (!OPT_PASSTHROUGH) + begin + reg f_changed_data; + + initial f_changed_data = 0; + always @(posedge i_clk) + if (i_reset) + f_changed_data <= 1; + else if (i_valid && $past(!i_valid || o_ready)) + begin + if (i_data != $past(i_data + 1)) + f_changed_data <= 0; + end else if (!i_valid && i_data != 0) + f_changed_data <= 0; + + +`ifndef VERIFIC + reg [3:0] cvr_steps, cvr_hold; + + always @(posedge i_clk) + if (i_reset) + begin + cvr_steps <= 0; + cvr_hold <= 0; + end else begin + cvr_steps <= cvr_steps + 1; + cvr_hold <= cvr_hold + 1; + case(cvr_steps) + 0: if (o_valid || i_valid) + cvr_steps <= 0; + 1: if (!i_valid || !i_ready) + cvr_steps <= 0; + 2: if (!i_valid || !i_ready) + cvr_steps <= 0; + 3: if (!i_valid || !i_ready) + cvr_steps <= 0; + 4: if (!i_valid || i_ready) + cvr_steps <= 0; + 5: if (!i_valid || !i_ready) + cvr_steps <= 0; + 6: if (!i_valid || !i_ready) + cvr_steps <= 0; + 7: if (!i_valid || i_ready) + cvr_steps <= 0; + 8: if (!i_valid || i_ready) + cvr_steps <= 0; + 9: if (!i_valid || !i_ready) + cvr_steps <= 0; + 10: if (!i_valid || !i_ready) + cvr_steps <= 0; + 11: if (!i_valid || !i_ready) + cvr_steps <= 0; + 12: begin + cvr_steps <= cvr_steps; + cover(!o_valid && !i_valid && f_changed_data); + if (!o_valid || !i_ready) + cvr_steps <= 0; + else + cvr_hold <= cvr_hold + 1; + end + default: assert(0); + endcase + end + +`else + // Cover test + cover property (@(posedge i_clk) + disable iff (i_reset) + (!o_valid && !i_valid) + ##1 i_valid && i_ready [*3] + ##1 i_valid && !i_ready + ##1 i_valid && i_ready [*2] + ##1 i_valid && !i_ready [*2] + ##1 i_valid && i_ready [*3] + // Wait for the design to clear + ##1 o_valid && i_ready [*0:5] + ##1 (!o_valid && !i_valid && f_changed_data)); +`endif + end endgenerate +`endif // SKIDBUFFER + // }}} +`endif +// }}} +endmodule +`default_nettype wire diff --git a/hls4ml/templates/bambu_accelerator/rtl/top_parallel.v b/hls4ml/templates/bambu_accelerator/rtl/top_parallel.v new file mode 100644 index 0000000000..19982ea7b3 --- /dev/null +++ b/hls4ml/templates/bambu_accelerator/rtl/top_parallel.v @@ -0,0 +1,698 @@ +`default_nettype none + +module top ( + input wire clk_i, + input wire rstn_i, + output reg led_0, + output reg led_1, + output reg led_2, + output reg led_3 +); + +// HLS IP parameters — defaults are dense-ioparallel: 16 input slots × 16-bit, +// 4 output slots × 64-bit (ap_fixed<36,16> in low 36 bits of each container). +// build() rewrites the marked region to match the actual model. N_WORDS is the +// BRAM DEPTH (2**address_port_width), not the element count: measured on +// NG-ULTRA, a non-power-of-two value makes NxMap delete the whole datapath +// (144 LUT4 / 0 carry at 5 outputs, vs 3329 / 7794 at the 8-slot depth). +// HLS4ML PARAMS BEGIN (autogenerated; do not edit inside) +localparam HLS_IN_DATA_W = 16; +localparam HLS_OUT_DATA_W = 64; +localparam HLS_IN_N_WORDS = 16; +localparam HLS_OUT_N_WORDS = 4; +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; +// HLS4ML PARAMS END + +// Clock/reset fabric +wire [9:0] clk_nic_fabric; +wire [9:0] rstn_nic_fabric; +wire [119:0] fpga_interrupt_in_i_int; + +// PLL output +wire clk_50_0mhz; +wire locked_1; +wire dma_interrupt; +wire rstn_50mhz; + +// HLS BRAM connections +wire [HLS_IN_DATA_W-1:0] hls_in_q0, hls_in_q1; +wire [HLS_IN_ADDR_W-1:0] hls_in_addr0, hls_in_addr1; +wire hls_in_ce0, hls_in_ce1; +wire [HLS_OUT_DATA_W-1:0] hls_out_d0, hls_out_d1; +wire [HLS_OUT_ADDR_W-1:0] hls_out_addr0, hls_out_addr1; +wire hls_out_ce0, hls_out_ce1; +wire hls_out_we0, hls_out_we1; +wire hls_start, hls_done; + +// AXI S1 signals +wire [39:0] s1_araddr, s1_awaddr; +wire [1:0] s1_arburst, s1_awburst; +wire [3:0] s1_arcache, s1_awcache; +wire [11:0] s1_arid, s1_awid; +wire [7:0] s1_arlen, s1_awlen; +wire s1_arlock, s1_awlock; +wire [2:0] s1_arprot, s1_awprot; +wire [3:0] s1_arqos, s1_awqos; +wire [3:0] s1_arregion, s1_awregion; +wire [2:0] s1_arsize, s1_awsize; +wire s1_arvalid, s1_awvalid; +wire s1_arready, s1_awready; + +wire s1_bready, s1_bvalid; +wire [1:0] s1_bresp; +wire [11:0] s1_bid; + +wire s1_rready, s1_rvalid, s1_rlast; +wire [127:0] s1_rdata; +wire [11:0] s1_rid; +wire [1:0] s1_rresp; + +wire [127:0] s1_wdata; +wire s1_wlast, s1_wvalid, s1_wready; +wire [15:0] s1_wstrb; + +// Continuous assignments +assign rstn_50mhz = rstn_i && locked_1; +assign clk_nic_fabric = {10{clk_50_0mhz}}; +assign rstn_nic_fabric = {10{rstn_i}}; +assign fpga_interrupt_in_i_int = {119'b0, dma_interrupt}; + +// NX SoC Interface +NX_SOC_INTERFACE_WRAP i_NX_SOC_INTERFACE_WRAP ( + .fabric_lowskew_o (), + .fabric_lowskew_i ({clk_nic_fabric, 7'b0}), + .fabric_fpga_nic_rstn_i (rstn_nic_fabric), + .fabric_fpga_pmrstn_i (1'b1), + .fabric_fpga_sysrstn_i (1'b1), + .fabric_fpga_trigger_in_o (), + .fabric_fpga_trigger_out_i (8'b0), + .fabric_fpga_interrupt_in_i (fpga_interrupt_in_i_int), + .fabric_sysc_hold_on_debug_i (1'b0), + .fabric_fpga_events60_i (60'b0), + // S1 outputs (fabric -> FPGA) + .fabric_fpga_araddr_axi_s1_o (s1_araddr), + .fabric_fpga_arburst_axi_s1_o (s1_arburst), + .fabric_fpga_arcache_axi_s1_o (s1_arcache), + .fabric_fpga_arid_axi_s1_o (s1_arid), + .fabric_fpga_arlen_axi_s1_o (s1_arlen), + .fabric_fpga_arlock_axi_s1_o (s1_arlock), + .fabric_fpga_arprot_axi_s1_o (s1_arprot), + .fabric_fpga_arqos_axi_s1_o (s1_arqos), + .fabric_fpga_arregion_axi_s1_o (s1_arregion), + .fabric_fpga_arsize_axi_s1_o (s1_arsize), + .fabric_fpga_arvalid_axi_s1_o (s1_arvalid), + .fabric_fpga_awaddr_axi_s1_o (s1_awaddr), + .fabric_fpga_awburst_axi_s1_o (s1_awburst), + .fabric_fpga_awcache_axi_s1_o (s1_awcache), + .fabric_fpga_awid_axi_s1_o (s1_awid), + .fabric_fpga_awlen_axi_s1_o (s1_awlen), + .fabric_fpga_awlock_axi_s1_o (s1_awlock), + .fabric_fpga_awprot_axi_s1_o (s1_awprot), + .fabric_fpga_awqos_axi_s1_o (s1_awqos), + .fabric_fpga_awregion_axi_s1_o (s1_awregion), + .fabric_fpga_awsize_axi_s1_o (s1_awsize), + .fabric_fpga_awvalid_axi_s1_o (s1_awvalid), + .fabric_fpga_bready_axi_s1_o (s1_bready), + .fabric_fpga_rready_axi_s1_o (s1_rready), + .fabric_fpga_wdata_axi_s1_o (s1_wdata), + .fabric_fpga_wlast_axi_s1_o (s1_wlast), + .fabric_fpga_wstrb_axi_s1_o (s1_wstrb), + .fabric_fpga_wvalid_axi_s1_o (s1_wvalid), + // S1 inputs (FPGA -> fabric) + .fabric_fpga_arready_axi_s1_i (s1_arready), + .fabric_fpga_awready_axi_s1_i (s1_awready), + .fabric_fpga_bid_axi_s1_i (s1_bid), + .fabric_fpga_bresp_axi_s1_i (s1_bresp), + .fabric_fpga_bvalid_axi_s1_i (s1_bvalid), + .fabric_fpga_rdata_axi_s1_i (s1_rdata), + .fabric_fpga_rid_axi_s1_i (s1_rid), + .fabric_fpga_rlast_axi_s1_i (s1_rlast), + .fabric_fpga_rresp_axi_s1_i (s1_rresp), + .fabric_fpga_rvalid_axi_s1_i (s1_rvalid), + .fabric_fpga_wready_axi_s1_i (s1_wready), + // S2 (unused) + .fabric_fpga_araddr_axi_s2_o (), + .fabric_fpga_arburst_axi_s2_o (), + .fabric_fpga_arcache_axi_s2_o (), + .fabric_fpga_arid_axi_s2_o (), + .fabric_fpga_arlen_axi_s2_o (), + .fabric_fpga_arlock_axi_s2_o (), + .fabric_fpga_arprot_axi_s2_o (), + .fabric_fpga_arqos_axi_s2_o (), + .fabric_fpga_arregion_axi_s2_o (), + .fabric_fpga_arsize_axi_s2_o (), + .fabric_fpga_arvalid_axi_s2_o (), + .fabric_fpga_awaddr_axi_s2_o (), + .fabric_fpga_awburst_axi_s2_o (), + .fabric_fpga_awcache_axi_s2_o (), + .fabric_fpga_awid_axi_s2_o (), + .fabric_fpga_awlen_axi_s2_o (), + .fabric_fpga_awlock_axi_s2_o (), + .fabric_fpga_awprot_axi_s2_o (), + .fabric_fpga_awqos_axi_s2_o (), + .fabric_fpga_awregion_axi_s2_o (), + .fabric_fpga_awsize_axi_s2_o (), + .fabric_fpga_awvalid_axi_s2_o (), + .fabric_fpga_bready_axi_s2_o (), + .fabric_fpga_rready_axi_s2_o (), + .fabric_fpga_wdata_axi_s2_o (), + .fabric_fpga_wlast_axi_s2_o (), + .fabric_fpga_wstrb_axi_s2_o (), + .fabric_fpga_wvalid_axi_s2_o (), + .fabric_fpga_arready_axi_s2_i (1'b0), + .fabric_fpga_awready_axi_s2_i (1'b0), + .fabric_fpga_bid_axi_s2_i (12'b0), + .fabric_fpga_bresp_axi_s2_i (2'b0), + .fabric_fpga_bvalid_axi_s2_i (1'b0), + .fabric_fpga_rdata_axi_s2_i (128'b0), + .fabric_fpga_rid_axi_s2_i (12'b0), + .fabric_fpga_rlast_axi_s2_i (1'b0), + .fabric_fpga_rresp_axi_s2_i (2'b0), + .fabric_fpga_rvalid_axi_s2_i (1'b0), + .fabric_fpga_wready_axi_s2_i (1'b0), + // M1 (unused) + .fabric_fpga_arready_axi_m1_o (), + .fabric_fpga_awready_axi_m1_o (), + .fabric_fpga_bid_axi_m1_o (), + .fabric_fpga_bresp_axi_m1_o (), + .fabric_fpga_bvalid_axi_m1_o (), + .fabric_fpga_dma_ack_m1_o (), + .fabric_fpga_dma_finish_m1_o (), + .fabric_fpga_rdata_axi_m1_o (), + .fabric_fpga_rid_axi_m1_o (), + .fabric_fpga_rlast_axi_m1_o (), + .fabric_fpga_rresp_axi_m1_o (), + .fabric_fpga_rvalid_axi_m1_o (), + .fabric_fpga_wready_axi_m1_o (), + .fabric_fpga_araddr_axi_m1_i (40'b0), + .fabric_fpga_arburst_axi_m1_i (2'b0), + .fabric_fpga_arcache_axi_m1_i (4'b0), + .fabric_fpga_arid_axi_m1_i (5'b0), + .fabric_fpga_arlen_axi_m1_i (8'b0), + .fabric_fpga_arlock_axi_m1_i (1'b0), + .fabric_fpga_arprot_axi_m1_i (3'b0), + .fabric_fpga_arqos_axi_m1_i (4'b0), + .fabric_fpga_arsize_axi_m1_i (3'b0), + .fabric_fpga_arvalid_axi_m1_i (1'b0), + .fabric_fpga_awaddr_axi_m1_i (40'b0), + .fabric_fpga_awburst_axi_m1_i (2'b0), + .fabric_fpga_awcache_axi_m1_i (4'b0), + .fabric_fpga_awid_axi_m1_i (5'b0), + .fabric_fpga_awlen_axi_m1_i (8'b0), + .fabric_fpga_awlock_axi_m1_i (1'b0), + .fabric_fpga_awprot_axi_m1_i (3'b0), + .fabric_fpga_awqos_axi_m1_i (4'b0), + .fabric_fpga_awsize_axi_m1_i (3'b0), + .fabric_fpga_awvalid_axi_m1_i (1'b0), + .fabric_fpga_bready_axi_m1_i (1'b0), + .fabric_fpga_dma_last_m1_i (6'b0), + .fabric_fpga_dma_req_m1_i (6'b0), + .fabric_fpga_dma_single_m1_i (6'b0), + .fabric_fpga_rready_axi_m1_i (1'b0), + .fabric_fpga_wdata_axi_m1_i (128'b0), + .fabric_fpga_wlast_axi_m1_i (1'b0), + .fabric_fpga_wstrb_axi_m1_i (16'b0), + .fabric_fpga_wvalid_axi_m1_i (1'b0), + // M2 (unused) + .fabric_fpga_arready_axi_m2_o (), + .fabric_fpga_awready_axi_m2_o (), + .fabric_fpga_bid_axi_m2_o (), + .fabric_fpga_bresp_axi_m2_o (), + .fabric_fpga_bvalid_axi_m2_o (), + .fabric_fpga_dma_ack_m2_o (), + .fabric_fpga_dma_finish_m2_o (), + .fabric_fpga_rdata_axi_m2_o (), + .fabric_fpga_rid_axi_m2_o (), + .fabric_fpga_rlast_axi_m2_o (), + .fabric_fpga_rresp_axi_m2_o (), + .fabric_fpga_rvalid_axi_m2_o (), + .fabric_fpga_wready_axi_m2_o (), + .fabric_fpga_araddr_axi_m2_i (40'b0), + .fabric_fpga_arburst_axi_m2_i (2'b0), + .fabric_fpga_arcache_axi_m2_i (4'b0), + .fabric_fpga_arid_axi_m2_i (5'b0), + .fabric_fpga_arlen_axi_m2_i (8'b0), + .fabric_fpga_arlock_axi_m2_i (1'b0), + .fabric_fpga_arprot_axi_m2_i (3'b0), + .fabric_fpga_arqos_axi_m2_i (4'b0), + .fabric_fpga_arsize_axi_m2_i (3'b0), + .fabric_fpga_arvalid_axi_m2_i (1'b0), + .fabric_fpga_awaddr_axi_m2_i (40'b0), + .fabric_fpga_awburst_axi_m2_i (2'b0), + .fabric_fpga_awcache_axi_m2_i (4'b0), + .fabric_fpga_awid_axi_m2_i (5'b0), + .fabric_fpga_awlen_axi_m2_i (8'b0), + .fabric_fpga_awlock_axi_m2_i (1'b0), + .fabric_fpga_awprot_axi_m2_i (3'b0), + .fabric_fpga_awqos_axi_m2_i (4'b0), + .fabric_fpga_awsize_axi_m2_i (3'b0), + .fabric_fpga_awvalid_axi_m2_i (1'b0), + .fabric_fpga_bready_axi_m2_i (1'b0), + .fabric_fpga_dma_last_m2_i (6'b0), + .fabric_fpga_dma_req_m2_i (6'b0), + .fabric_fpga_dma_single_m2_i (6'b0), + .fabric_fpga_rready_axi_m2_i (1'b0), + .fabric_fpga_wdata_axi_m2_i (128'b0), + .fabric_fpga_wlast_axi_m2_i (1'b0), + .fabric_fpga_wstrb_axi_m2_i (16'b0), + .fabric_fpga_wvalid_axi_m2_i (1'b0), + // DDR0 (unused) + .fabric_fpga_ddr0_arready_o (), + .fabric_fpga_ddr0_awready_o (), + .fabric_fpga_ddr0_bid_o (), + .fabric_fpga_ddr0_bresp_o (), + .fabric_fpga_ddr0_bvalid_o (), + .fabric_fpga_ddr0_rdata_o (), + .fabric_fpga_ddr0_rid_o (), + .fabric_fpga_ddr0_rlast_o (), + .fabric_fpga_ddr0_rresp_o (), + .fabric_fpga_ddr0_rvalid_o (), + .fabric_fpga_ddr0_wready_o (), + .fabric_fpga_ddr0_araddr_i (40'b0), + .fabric_fpga_ddr0_arburst_i (2'b0), + .fabric_fpga_ddr0_arcache_i (4'b0), + .fabric_fpga_ddr0_arid_i (5'b0), + .fabric_fpga_ddr0_arlen_i (8'b0), + .fabric_fpga_ddr0_arlock_i (1'b0), + .fabric_fpga_ddr0_arprot_i (3'b0), + .fabric_fpga_ddr0_arqos_i (4'b0), + .fabric_fpga_ddr0_arsize_i (3'b0), + .fabric_fpga_ddr0_arvalid_i (1'b0), + .fabric_fpga_ddr0_awaddr_i (40'b0), + .fabric_fpga_ddr0_awburst_i (2'b0), + .fabric_fpga_ddr0_awcache_i (4'b0), + .fabric_fpga_ddr0_awid_i (5'b0), + .fabric_fpga_ddr0_awlen_i (8'b0), + .fabric_fpga_ddr0_awlock_i (1'b0), + .fabric_fpga_ddr0_awprot_i (3'b0), + .fabric_fpga_ddr0_awqos_i (4'b0), + .fabric_fpga_ddr0_awsize_i (3'b0), + .fabric_fpga_ddr0_awvalid_i (1'b0), + .fabric_fpga_ddr0_bready_i (1'b0), + .fabric_fpga_ddr0_rready_i (1'b0), + .fabric_fpga_ddr0_wdata_i (128'b0), + .fabric_fpga_ddr0_wlast_i (1'b0), + .fabric_fpga_ddr0_wstrb_i (16'b0), + .fabric_fpga_ddr0_wvalid_i (1'b0), + // APB (unused) + .fabric_fpga_paddr_apb_o (), + .fabric_fpga_penable_apb_o (), + .fabric_fpga_psel_apb_o (), + .fabric_fpga_pwdata_apb_o (), + .fabric_fpga_pwrite_apb_o (), + .fabric_fpga_prdata_apb_i (32'b0), + .fabric_fpga_pready_apb_i (1'b0), + .fabric_fpga_pslverr_apb_i (1'b0), + // LLPP0 (unused) + .fabric_llpp0_araddr_s_o (), + .fabric_llpp0_arburst_s_o (), + .fabric_llpp0_arcache_s_o (), + .fabric_llpp0_arid_s_o (), + .fabric_llpp0_arlen_s_o (), + .fabric_llpp0_arlock_s_o (), + .fabric_llpp0_arprot_s_o (), + .fabric_llpp0_arqos_s_o (), + .fabric_llpp0_arsize_s_o (), + .fabric_llpp0_arvalid_s_o (), + .fabric_llpp0_awaddr_s_o (), + .fabric_llpp0_awburst_s_o (), + .fabric_llpp0_awcache_s_o (), + .fabric_llpp0_awid_s_o (), + .fabric_llpp0_awlen_s_o (), + .fabric_llpp0_awlock_s_o (), + .fabric_llpp0_awprot_s_o (), + .fabric_llpp0_awqos_s_o (), + .fabric_llpp0_awsize_s_o (), + .fabric_llpp0_awvalid_s_o (), + .fabric_llpp0_bready_s_o (), + .fabric_llpp0_rready_s_o (), + .fabric_llpp0_wdata_s_o (), + .fabric_llpp0_wlast_s_o (), + .fabric_llpp0_wstrb_s_o (), + .fabric_llpp0_wvalid_s_o (), + .fabric_llpp0_arready_s_i (1'b0), + .fabric_llpp0_awready_s_i (1'b0), + .fabric_llpp0_bid_s_i (12'b0), + .fabric_llpp0_bresp_s_i (2'b0), + .fabric_llpp0_bvalid_s_i (1'b0), + .fabric_llpp0_rdata_s_i (32'b0), + .fabric_llpp0_rid_s_i (12'b0), + .fabric_llpp0_rlast_s_i (1'b0), + .fabric_llpp0_rresp_s_i (2'b0), + .fabric_llpp0_rvalid_s_i (1'b0), + .fabric_llpp0_wready_s_i (1'b0), + // LLPP1 (unused) + .fabric_llpp1_araddr_s_o (), + .fabric_llpp1_arburst_s_o (), + .fabric_llpp1_arcache_s_o (), + .fabric_llpp1_arid_s_o (), + .fabric_llpp1_arlen_s_o (), + .fabric_llpp1_arlock_s_o (), + .fabric_llpp1_arprot_s_o (), + .fabric_llpp1_arqos_s_o (), + .fabric_llpp1_arsize_s_o (), + .fabric_llpp1_arvalid_s_o (), + .fabric_llpp1_awaddr_s_o (), + .fabric_llpp1_awburst_s_o (), + .fabric_llpp1_awcache_s_o (), + .fabric_llpp1_awid_s_o (), + .fabric_llpp1_awlen_s_o (), + .fabric_llpp1_awlock_s_o (), + .fabric_llpp1_awprot_s_o (), + .fabric_llpp1_awqos_s_o (), + .fabric_llpp1_awsize_s_o (), + .fabric_llpp1_awvalid_s_o (), + .fabric_llpp1_bready_s_o (), + .fabric_llpp1_rready_s_o (), + .fabric_llpp1_wdata_s_o (), + .fabric_llpp1_wlast_s_o (), + .fabric_llpp1_wstrb_s_o (), + .fabric_llpp1_wvalid_s_o (), + .fabric_llpp1_arready_s_i (1'b0), + .fabric_llpp1_awready_s_i (1'b0), + .fabric_llpp1_bid_s_i (12'b0), + .fabric_llpp1_bresp_s_i (2'b0), + .fabric_llpp1_bvalid_s_i (1'b0), + .fabric_llpp1_rdata_s_i (32'b0), + .fabric_llpp1_rid_s_i (12'b0), + .fabric_llpp1_rlast_s_i (1'b0), + .fabric_llpp1_rresp_s_i (2'b0), + .fabric_llpp1_rvalid_s_i (1'b0), + .fabric_llpp1_wready_s_i (1'b0), + // LLPP2 (unused) + .fabric_llpp2_araddr_s_o (), + .fabric_llpp2_arburst_s_o (), + .fabric_llpp2_arcache_s_o (), + .fabric_llpp2_arid_s_o (), + .fabric_llpp2_arlen_s_o (), + .fabric_llpp2_arlock_s_o (), + .fabric_llpp2_arprot_s_o (), + .fabric_llpp2_arqos_s_o (), + .fabric_llpp2_arsize_s_o (), + .fabric_llpp2_arvalid_s_o (), + .fabric_llpp2_awaddr_s_o (), + .fabric_llpp2_awburst_s_o (), + .fabric_llpp2_awcache_s_o (), + .fabric_llpp2_awid_s_o (), + .fabric_llpp2_awlen_s_o (), + .fabric_llpp2_awlock_s_o (), + .fabric_llpp2_awprot_s_o (), + .fabric_llpp2_awqos_s_o (), + .fabric_llpp2_awsize_s_o (), + .fabric_llpp2_awvalid_s_o (), + .fabric_llpp2_bready_s_o (), + .fabric_llpp2_rready_s_o (), + .fabric_llpp2_wdata_s_o (), + .fabric_llpp2_wlast_s_o (), + .fabric_llpp2_wstrb_s_o (), + .fabric_llpp2_wvalid_s_o (), + .fabric_llpp2_arready_s_i (1'b0), + .fabric_llpp2_awready_s_i (1'b0), + .fabric_llpp2_bid_s_i (12'b0), + .fabric_llpp2_bresp_s_i (2'b0), + .fabric_llpp2_bvalid_s_i (1'b0), + .fabric_llpp2_rdata_s_i (32'b0), + .fabric_llpp2_rid_s_i (12'b0), + .fabric_llpp2_rlast_s_i (1'b0), + .fabric_llpp2_rresp_s_i (2'b0), + .fabric_llpp2_rvalid_s_i (1'b0), + .fabric_llpp2_wready_s_i (1'b0), + // LLPP3 (unused) + .fabric_llpp3_araddr_s_o (), + .fabric_llpp3_arburst_s_o (), + .fabric_llpp3_arcache_s_o (), + .fabric_llpp3_arid_s_o (), + .fabric_llpp3_arlen_s_o (), + .fabric_llpp3_arlock_s_o (), + .fabric_llpp3_arprot_s_o (), + .fabric_llpp3_arqos_s_o (), + .fabric_llpp3_arsize_s_o (), + .fabric_llpp3_arvalid_s_o (), + .fabric_llpp3_awaddr_s_o (), + .fabric_llpp3_awburst_s_o (), + .fabric_llpp3_awcache_s_o (), + .fabric_llpp3_awid_s_o (), + .fabric_llpp3_awlen_s_o (), + .fabric_llpp3_awlock_s_o (), + .fabric_llpp3_awprot_s_o (), + .fabric_llpp3_awqos_s_o (), + .fabric_llpp3_awsize_s_o (), + .fabric_llpp3_awvalid_s_o (), + .fabric_llpp3_bready_s_o (), + .fabric_llpp3_rready_s_o (), + .fabric_llpp3_wdata_s_o (), + .fabric_llpp3_wlast_s_o (), + .fabric_llpp3_wstrb_s_o (), + .fabric_llpp3_wvalid_s_o (), + .fabric_llpp3_arready_s_i (1'b0), + .fabric_llpp3_awready_s_i (1'b0), + .fabric_llpp3_bid_s_i (12'b0), + .fabric_llpp3_bresp_s_i (2'b0), + .fabric_llpp3_bvalid_s_i (1'b0), + .fabric_llpp3_rdata_s_i (32'b0), + .fabric_llpp3_rid_s_i (12'b0), + .fabric_llpp3_rlast_s_i (1'b0), + .fabric_llpp3_rresp_s_i (2'b0), + .fabric_llpp3_rvalid_s_i (1'b0), + .fabric_llpp3_wready_s_i (1'b0), + // QoS (unused) + .fabric_qos_pprdata_o (), + .fabric_qos_ppready_o (), + .fabric_qos_ppslverr_o (), + .fabric_qos_ppaddr_i (32'b0), + .fabric_qos_ppenable_i (1'b0), + .fabric_qos_ppwdata_i (32'b0), + .fabric_qos_ppwrite_i (1'b0), + .fabric_qos_presetn_i (1'b0), + .fabric_qos_psel_i (1'b0), + // TND (unused) + .fabric_tnd_hssl_flushin_o (), + .fabric_tnd_hssl_trigin_o (), + .fabric_tnd_fpga_apb_master_paddr_o (), + .fabric_tnd_fpga_apb_master_penable_o (), + .fabric_tnd_fpga_apb_master_psel_o (), + .fabric_tnd_fpga_apb_master_pwdata_o (), + .fabric_tnd_fpga_apb_master_pwrite_o (), + .fabric_tnd_fpga_atb_master_afvalid_o (), + .fabric_tnd_fpga_atb_master_atready_o (), + .fabric_tnd_fpga_atb_master_syncreq_o (), + .fabric_tnd_hssl_apb_master_paddr_o (), + .fabric_tnd_hssl_apb_master_penable_o (), + .fabric_tnd_hssl_apb_master_psel_o (), + .fabric_tnd_hssl_apb_master_pwdata_o (), + .fabric_tnd_hssl_apb_master_pwrite_o (), + .fabric_tnd_hssl_atb_master_afready_o (), + .fabric_tnd_hssl_atb_master_atbytes_o (), + .fabric_tnd_hssl_atb_master_atdata_o (), + .fabric_tnd_hssl_atb_master_atid_o (), + .fabric_tnd_hssl_atb_master_atvalid_o (), + .fabric_tnd_trace_clk_traceoutportintf_o (), + .fabric_tnd_trace_ctl_traceoutportintf_o (), + .fabric_tnd_trace_data_traceoutportintf_o (), + .fabric_tsvalue_tsgen_fpga_o (), + .fabric_tnd_fpga_apb_master_prdata_i (32'b0), + .fabric_tnd_fpga_apb_master_pready_i (1'b0), + .fabric_tnd_fpga_apb_master_pslverr_i (1'b0), + .fabric_tnd_fpga_atb_master_afready_i (1'b0), + .fabric_tnd_fpga_atb_master_atbytes_i (4'b0), + .fabric_tnd_fpga_atb_master_atdata_i (128'b0), + .fabric_tnd_fpga_atb_master_atid_i (7'b0), + .fabric_tnd_fpga_atb_master_atvalid_i (1'b0), + .fabric_tnd_hssl_apb_master_prdata_i (32'b0), + .fabric_tnd_hssl_apb_master_pready_i (1'b0), + .fabric_tnd_hssl_apb_master_pslverr_i (1'b0), + .fabric_tnd_hssl_atb_master_afvalid_i (1'b0), + .fabric_tnd_hssl_atb_master_atready_i (1'b0), + .fabric_tnd_hssl_atb_master_syncreq_i (1'b0), + // Watchdog (unused) + .fabric_watchdog0_signal_0_o (), + .fabric_watchdog0_signal_1_o (), + .fabric_watchdog1_signal_0_o (), + .fabric_watchdog1_signal_1_o (), + .fabric_watchdog2_signal_0_o (), + .fabric_watchdog2_signal_1_o (), + .fabric_watchdog3_signal_0_o (), + .fabric_watchdog3_signal_1_o (), + .fabric_tst_pll_lock_o (), + .fabric_soc_mon_sensor_alarm_o (), + // EROM (unused) + .fabric_erom_fpga_cpu0_dbgen_i (1'b0), + .fabric_erom_fpga_cpu0_hiden_i (1'b0), + .fabric_erom_fpga_cpu0_hniden_i (1'b0), + .fabric_erom_fpga_cpu0_niden_i (1'b0), + .fabric_erom_fpga_cpu1_dbgen_i (1'b0), + .fabric_erom_fpga_cpu1_hiden_i (1'b0), + .fabric_erom_fpga_cpu1_hniden_i (1'b0), + .fabric_erom_fpga_cpu1_niden_i (1'b0), + .fabric_erom_fpga_cpu2_dbgen_i (1'b0), + .fabric_erom_fpga_cpu2_hiden_i (1'b0), + .fabric_erom_fpga_cpu2_hniden_i (1'b0), + .fabric_erom_fpga_cpu2_niden_i (1'b0), + .fabric_erom_fpga_cpu3_dbgen_i (1'b0), + .fabric_erom_fpga_cpu3_hiden_i (1'b0), + .fabric_erom_fpga_cpu3_hniden_i (1'b0), + .fabric_erom_fpga_cpu3_niden_i (1'b0), + .fabric_erom_fpga_cs_dbgen_i (1'b0), + .fabric_erom_fpga_cs_niden_i (1'b0), + .fabric_erom_fpga_cs_deviceen_i (1'b0), + .fabric_erom_fpga_cs_rst_n_i (1'b0), + .fabric_erom_fpga_debug_en_i (1'b0), + .fabric_enable_TMR_i ({3{1'b1}}), + .fabric_spw_interrupts_toggle_o (), + .fabric_spw_interrupts_o (), + .fabric_fpga_dma_hs_rstn_i (6'b0) +); + +// HLS4ML PLL BEGIN (autogenerated for ClockPeriod; do not edit inside) +// PLL 1: VCO=750.0MHz (PFD=25.0MHz), generates 50.0 MHz +NX_PLL_U #( + .location (""), + .ref_osc_on (1'b1), + .use_pll (1'b1), + .ext_fbk_on (1'b0), + .fbk_delay_on (1'b0), + .fbk_delay (6'd0), + .ref_intdiv (5'd15), + .fbk_intdiv (7'd14), + .clk_outdiv1 (3'd6), + .clk_outdiv2 (3'd0), + .clk_outdiv3 (3'd0), + .clk_outdiv4 (3'd0), + .clk_outdivd1 (4'd0), + .clk_outdivd2 (4'd0), + .clk_outdivd3 (4'd0), + .clk_outdivd4 (4'd0), + .clk_outdivd5 (4'd0) +) PLL_1 ( + .REF (1'b0), + .FBK (1'b0), + .R (~rstn_i), + .VCO (), + .LDFO (), + .REFO (), + .OSC (), + .CAL_LOCKED(), + .PLL_LOCKED(locked_1), + .CLK_DIV1 (clk_50_0mhz), + .CLK_DIV2 (), + .CLK_DIV3 (), + .CLK_DIV4 (), + .CLK_DIVD1 (), + .CLK_DIVD2 (), + .CLK_DIVD3 (), + .CLK_DIVD4 (), + .CLK_DIVD5 () +); +// HLS4ML PLL END + +// AXI parallel slave +AXISlaveParallel #( + .C_S_AXI_ID_WIDTH (12), + .C_S_AXI_DATA_WIDTH (128), + .C_S_AXI_ADDR_WIDTH (40), + .HLS_IN_DATA_W (HLS_IN_DATA_W), + .HLS_OUT_DATA_W (HLS_OUT_DATA_W), + .HLS_IN_N_WORDS (HLS_IN_N_WORDS), + .HLS_OUT_N_WORDS (HLS_OUT_N_WORDS), + .HLS_IN_ADDR_W (HLS_IN_ADDR_W), + .HLS_OUT_ADDR_W (HLS_OUT_ADDR_W) +) u_AXISlaveParallel ( + .S_AXI_ACLK (clk_50_0mhz), + .S_AXI_ARESETN (rstn_50mhz), + .S_AXI_AWVALID (s1_awvalid), + .S_AXI_AWREADY (s1_awready), + .S_AXI_AWID (s1_awid), + .S_AXI_AWADDR (s1_awaddr), + .S_AXI_AWLEN (s1_awlen), + .S_AXI_AWSIZE (s1_awsize), + .S_AXI_AWBURST (s1_awburst), + .S_AXI_AWLOCK (s1_awlock), + .S_AXI_AWCACHE (s1_awcache), + .S_AXI_AWPROT (s1_awprot), + .S_AXI_AWQOS (s1_awqos), + .S_AXI_WVALID (s1_wvalid), + .S_AXI_WREADY (s1_wready), + .S_AXI_WDATA (s1_wdata), + .S_AXI_WSTRB (s1_wstrb), + .S_AXI_WLAST (s1_wlast), + .S_AXI_BVALID (s1_bvalid), + .S_AXI_BREADY (s1_bready), + .S_AXI_BID (s1_bid), + .S_AXI_BRESP (s1_bresp), + .S_AXI_ARVALID (s1_arvalid), + .S_AXI_ARREADY (s1_arready), + .S_AXI_ARID (s1_arid), + .S_AXI_ARADDR (s1_araddr), + .S_AXI_ARLEN (s1_arlen), + .S_AXI_ARSIZE (s1_arsize), + .S_AXI_ARBURST (s1_arburst), + .S_AXI_ARLOCK (s1_arlock), + .S_AXI_ARCACHE (s1_arcache), + .S_AXI_ARPROT (s1_arprot), + .S_AXI_ARQOS (s1_arqos), + .S_AXI_RVALID (s1_rvalid), + .S_AXI_RREADY (s1_rready), + .S_AXI_RID (s1_rid), + .S_AXI_RDATA (s1_rdata), + .S_AXI_RRESP (s1_rresp), + .S_AXI_RLAST (s1_rlast), + .o_interrupt (dma_interrupt), + .hls_in_q0 (hls_in_q0), + .hls_in_q1 (hls_in_q1), + .hls_in_addr0 (hls_in_addr0), + .hls_in_addr1 (hls_in_addr1), + .hls_in_ce0 (hls_in_ce0), + .hls_in_ce1 (hls_in_ce1), + .hls_out_d0 (hls_out_d0), + .hls_out_d1 (hls_out_d1), + .hls_out_addr0 (hls_out_addr0), + .hls_out_addr1 (hls_out_addr1), + .hls_out_ce0 (hls_out_ce0), + .hls_out_ce1 (hls_out_ce1), + .hls_out_we0 (hls_out_we0), + .hls_out_we1 (hls_out_we1), + .start_out (hls_start), + .done_in (hls_done) +); + +// HLS IP (myproject wrapper) +myproject u_myproject ( + .clock (clk_50_0mhz), + .reset (rstn_50mhz), + .start_port (hls_start), + .done_port (hls_done), + .input_q0 (hls_in_q0), + .input_q1 (hls_in_q1), + .input_address0 (hls_in_addr0), + .input_address1 (hls_in_addr1), + .input_ce0 (hls_in_ce0), + .input_ce1 (hls_in_ce1), + .output_address0 (hls_out_addr0), + .output_address1 (hls_out_addr1), + .output_ce0 (hls_out_ce0), + .output_ce1 (hls_out_ce1), + .output_we0 (hls_out_we0), + .output_we1 (hls_out_we1), + .output_d0 (hls_out_d0), + .output_d1 (hls_out_d1) +); + +// Drive LEDs with AXI activity indicators +always @(posedge clk_50_0mhz) begin + if (!rstn_i) begin + led_0 <= 1'b0; + led_1 <= 1'b0; + led_2 <= 1'b0; + led_3 <= 1'b0; + end else begin + if (s1_awvalid) led_0 <= 1'b1; + if (s1_wvalid) led_1 <= 1'b1; + if (s1_arvalid) led_2 <= 1'b1; + if (s1_rvalid) led_3 <= 1'b1; + end +end + +endmodule + +`default_nettype wire diff --git a/hls4ml/templates/bambu_accelerator/rtl/top_stream.v b/hls4ml/templates/bambu_accelerator/rtl/top_stream.v new file mode 100644 index 0000000000..053f0baf8b --- /dev/null +++ b/hls4ml/templates/bambu_accelerator/rtl/top_stream.v @@ -0,0 +1,684 @@ +`default_nettype none + +// Synthesis top for AXI-Stream (io_stream) HLS IPs. Derived from top.v; +// differs only in the HLS-side wiring: AXISlaveStream replaces AXISlaveDMA, +// and the `myproject` wrapper is instantiated with AXIS ports +// (hls_in_t*/hls_out_t*). Everything else — NX SoC interface, PLL, S1 AXI +// plumbing, LEDs — is identical to top.v. + +module top ( + input wire clk_i, + input wire rstn_i, + output reg led_0, + output reg led_1, + output reg led_2, + output reg led_3 +); + +// HLS IP parameters (dense-stream defaults). build() rewrites the marked +// region to match the actual model. No ADDR_W here — AXISlaveStream has no +// address bus, and N_WORDS is the element count (beats), not a BRAM depth. +// HLS4ML PARAMS BEGIN (autogenerated; do not edit inside) +localparam HLS_IN_DATA_W = 16; +localparam HLS_OUT_DATA_W = 16; +localparam HLS_IN_N_WORDS = 10; +localparam HLS_OUT_N_WORDS = 3; +// HLS4ML PARAMS END + +// Clock/reset fabric +wire [9:0] clk_nic_fabric; +wire [9:0] rstn_nic_fabric; +wire [119:0] fpga_interrupt_in_i_int; + +// PLL output +wire clk_50_0mhz; +wire locked_1; +wire dma_interrupt; +wire rstn_50mhz; + +// HLS AXIS connections +wire [HLS_IN_DATA_W-1:0] hls_in_tdata; +wire hls_in_tvalid; +wire hls_in_tready; +wire [HLS_OUT_DATA_W-1:0] hls_out_tdata; +wire hls_out_tvalid; +wire hls_out_tready; +wire hls_start; +wire hls_done; + +// AXI S1 signals +wire [39:0] s1_araddr, s1_awaddr; +wire [1:0] s1_arburst, s1_awburst; +wire [3:0] s1_arcache, s1_awcache; +wire [11:0] s1_arid, s1_awid; +wire [7:0] s1_arlen, s1_awlen; +wire s1_arlock, s1_awlock; +wire [2:0] s1_arprot, s1_awprot; +wire [3:0] s1_arqos, s1_awqos; +wire [3:0] s1_arregion, s1_awregion; +wire [2:0] s1_arsize, s1_awsize; +wire s1_arvalid, s1_awvalid; +wire s1_arready, s1_awready; + +wire s1_bready, s1_bvalid; +wire [1:0] s1_bresp; +wire [11:0] s1_bid; + +wire s1_rready, s1_rvalid, s1_rlast; +wire [127:0] s1_rdata; +wire [11:0] s1_rid; +wire [1:0] s1_rresp; + +wire [127:0] s1_wdata; +wire s1_wlast, s1_wvalid, s1_wready; +wire [15:0] s1_wstrb; + +// Continuous assignments +assign rstn_50mhz = rstn_i && locked_1; +// assign rstn_50mhz = rstn_i; +assign clk_nic_fabric = {10{clk_50_0mhz}}; +assign rstn_nic_fabric = {10{rstn_i}}; +assign fpga_interrupt_in_i_int = {119'b0, dma_interrupt}; + +// NX SoC Interface +NX_SOC_INTERFACE_WRAP i_NX_SOC_INTERFACE_WRAP ( + .fabric_lowskew_o (), + .fabric_lowskew_i ({clk_nic_fabric, 7'b0}), + .fabric_fpga_nic_rstn_i (rstn_nic_fabric), + .fabric_fpga_pmrstn_i (1'b1), + .fabric_fpga_sysrstn_i (1'b1), + .fabric_fpga_trigger_in_o (), + .fabric_fpga_trigger_out_i (8'b0), + .fabric_fpga_interrupt_in_i (fpga_interrupt_in_i_int), + .fabric_sysc_hold_on_debug_i (1'b0), + .fabric_fpga_events60_i (60'b0), + // S1 outputs (fabric -> FPGA) + .fabric_fpga_araddr_axi_s1_o (s1_araddr), + .fabric_fpga_arburst_axi_s1_o (s1_arburst), + .fabric_fpga_arcache_axi_s1_o (s1_arcache), + .fabric_fpga_arid_axi_s1_o (s1_arid), + .fabric_fpga_arlen_axi_s1_o (s1_arlen), + .fabric_fpga_arlock_axi_s1_o (s1_arlock), + .fabric_fpga_arprot_axi_s1_o (s1_arprot), + .fabric_fpga_arqos_axi_s1_o (s1_arqos), + .fabric_fpga_arregion_axi_s1_o (s1_arregion), + .fabric_fpga_arsize_axi_s1_o (s1_arsize), + .fabric_fpga_arvalid_axi_s1_o (s1_arvalid), + .fabric_fpga_awaddr_axi_s1_o (s1_awaddr), + .fabric_fpga_awburst_axi_s1_o (s1_awburst), + .fabric_fpga_awcache_axi_s1_o (s1_awcache), + .fabric_fpga_awid_axi_s1_o (s1_awid), + .fabric_fpga_awlen_axi_s1_o (s1_awlen), + .fabric_fpga_awlock_axi_s1_o (s1_awlock), + .fabric_fpga_awprot_axi_s1_o (s1_awprot), + .fabric_fpga_awqos_axi_s1_o (s1_awqos), + .fabric_fpga_awregion_axi_s1_o (s1_awregion), + .fabric_fpga_awsize_axi_s1_o (s1_awsize), + .fabric_fpga_awvalid_axi_s1_o (s1_awvalid), + .fabric_fpga_bready_axi_s1_o (s1_bready), + .fabric_fpga_rready_axi_s1_o (s1_rready), + .fabric_fpga_wdata_axi_s1_o (s1_wdata), + .fabric_fpga_wlast_axi_s1_o (s1_wlast), + .fabric_fpga_wstrb_axi_s1_o (s1_wstrb), + .fabric_fpga_wvalid_axi_s1_o (s1_wvalid), + // S1 inputs (FPGA -> fabric) + .fabric_fpga_arready_axi_s1_i (s1_arready), + .fabric_fpga_awready_axi_s1_i (s1_awready), + .fabric_fpga_bid_axi_s1_i (s1_bid), + .fabric_fpga_bresp_axi_s1_i (s1_bresp), + .fabric_fpga_bvalid_axi_s1_i (s1_bvalid), + .fabric_fpga_rdata_axi_s1_i (s1_rdata), + .fabric_fpga_rid_axi_s1_i (s1_rid), + .fabric_fpga_rlast_axi_s1_i (s1_rlast), + .fabric_fpga_rresp_axi_s1_i (s1_rresp), + .fabric_fpga_rvalid_axi_s1_i (s1_rvalid), + .fabric_fpga_wready_axi_s1_i (s1_wready), + // S2 (unused) + .fabric_fpga_araddr_axi_s2_o (), + .fabric_fpga_arburst_axi_s2_o (), + .fabric_fpga_arcache_axi_s2_o (), + .fabric_fpga_arid_axi_s2_o (), + .fabric_fpga_arlen_axi_s2_o (), + .fabric_fpga_arlock_axi_s2_o (), + .fabric_fpga_arprot_axi_s2_o (), + .fabric_fpga_arqos_axi_s2_o (), + .fabric_fpga_arregion_axi_s2_o (), + .fabric_fpga_arsize_axi_s2_o (), + .fabric_fpga_arvalid_axi_s2_o (), + .fabric_fpga_awaddr_axi_s2_o (), + .fabric_fpga_awburst_axi_s2_o (), + .fabric_fpga_awcache_axi_s2_o (), + .fabric_fpga_awid_axi_s2_o (), + .fabric_fpga_awlen_axi_s2_o (), + .fabric_fpga_awlock_axi_s2_o (), + .fabric_fpga_awprot_axi_s2_o (), + .fabric_fpga_awqos_axi_s2_o (), + .fabric_fpga_awregion_axi_s2_o (), + .fabric_fpga_awsize_axi_s2_o (), + .fabric_fpga_awvalid_axi_s2_o (), + .fabric_fpga_bready_axi_s2_o (), + .fabric_fpga_rready_axi_s2_o (), + .fabric_fpga_wdata_axi_s2_o (), + .fabric_fpga_wlast_axi_s2_o (), + .fabric_fpga_wstrb_axi_s2_o (), + .fabric_fpga_wvalid_axi_s2_o (), + .fabric_fpga_arready_axi_s2_i (1'b0), + .fabric_fpga_awready_axi_s2_i (1'b0), + .fabric_fpga_bid_axi_s2_i (12'b0), + .fabric_fpga_bresp_axi_s2_i (2'b0), + .fabric_fpga_bvalid_axi_s2_i (1'b0), + .fabric_fpga_rdata_axi_s2_i (128'b0), + .fabric_fpga_rid_axi_s2_i (12'b0), + .fabric_fpga_rlast_axi_s2_i (1'b0), + .fabric_fpga_rresp_axi_s2_i (2'b0), + .fabric_fpga_rvalid_axi_s2_i (1'b0), + .fabric_fpga_wready_axi_s2_i (1'b0), + // M1 (unused) + .fabric_fpga_arready_axi_m1_o (), + .fabric_fpga_awready_axi_m1_o (), + .fabric_fpga_bid_axi_m1_o (), + .fabric_fpga_bresp_axi_m1_o (), + .fabric_fpga_bvalid_axi_m1_o (), + .fabric_fpga_dma_ack_m1_o (), + .fabric_fpga_dma_finish_m1_o (), + .fabric_fpga_rdata_axi_m1_o (), + .fabric_fpga_rid_axi_m1_o (), + .fabric_fpga_rlast_axi_m1_o (), + .fabric_fpga_rresp_axi_m1_o (), + .fabric_fpga_rvalid_axi_m1_o (), + .fabric_fpga_wready_axi_m1_o (), + .fabric_fpga_araddr_axi_m1_i (40'b0), + .fabric_fpga_arburst_axi_m1_i (2'b0), + .fabric_fpga_arcache_axi_m1_i (4'b0), + .fabric_fpga_arid_axi_m1_i (5'b0), + .fabric_fpga_arlen_axi_m1_i (8'b0), + .fabric_fpga_arlock_axi_m1_i (1'b0), + .fabric_fpga_arprot_axi_m1_i (3'b0), + .fabric_fpga_arqos_axi_m1_i (4'b0), + .fabric_fpga_arsize_axi_m1_i (3'b0), + .fabric_fpga_arvalid_axi_m1_i (1'b0), + .fabric_fpga_awaddr_axi_m1_i (40'b0), + .fabric_fpga_awburst_axi_m1_i (2'b0), + .fabric_fpga_awcache_axi_m1_i (4'b0), + .fabric_fpga_awid_axi_m1_i (5'b0), + .fabric_fpga_awlen_axi_m1_i (8'b0), + .fabric_fpga_awlock_axi_m1_i (1'b0), + .fabric_fpga_awprot_axi_m1_i (3'b0), + .fabric_fpga_awqos_axi_m1_i (4'b0), + .fabric_fpga_awsize_axi_m1_i (3'b0), + .fabric_fpga_awvalid_axi_m1_i (1'b0), + .fabric_fpga_bready_axi_m1_i (1'b0), + .fabric_fpga_dma_last_m1_i (6'b0), + .fabric_fpga_dma_req_m1_i (6'b0), + .fabric_fpga_dma_single_m1_i (6'b0), + .fabric_fpga_rready_axi_m1_i (1'b0), + .fabric_fpga_wdata_axi_m1_i (128'b0), + .fabric_fpga_wlast_axi_m1_i (1'b0), + .fabric_fpga_wstrb_axi_m1_i (16'b0), + .fabric_fpga_wvalid_axi_m1_i (1'b0), + // M2 (unused) + .fabric_fpga_arready_axi_m2_o (), + .fabric_fpga_awready_axi_m2_o (), + .fabric_fpga_bid_axi_m2_o (), + .fabric_fpga_bresp_axi_m2_o (), + .fabric_fpga_bvalid_axi_m2_o (), + .fabric_fpga_dma_ack_m2_o (), + .fabric_fpga_dma_finish_m2_o (), + .fabric_fpga_rdata_axi_m2_o (), + .fabric_fpga_rid_axi_m2_o (), + .fabric_fpga_rlast_axi_m2_o (), + .fabric_fpga_rresp_axi_m2_o (), + .fabric_fpga_rvalid_axi_m2_o (), + .fabric_fpga_wready_axi_m2_o (), + .fabric_fpga_araddr_axi_m2_i (40'b0), + .fabric_fpga_arburst_axi_m2_i (2'b0), + .fabric_fpga_arcache_axi_m2_i (4'b0), + .fabric_fpga_arid_axi_m2_i (5'b0), + .fabric_fpga_arlen_axi_m2_i (8'b0), + .fabric_fpga_arlock_axi_m2_i (1'b0), + .fabric_fpga_arprot_axi_m2_i (3'b0), + .fabric_fpga_arqos_axi_m2_i (4'b0), + .fabric_fpga_arsize_axi_m2_i (3'b0), + .fabric_fpga_arvalid_axi_m2_i (1'b0), + .fabric_fpga_awaddr_axi_m2_i (40'b0), + .fabric_fpga_awburst_axi_m2_i (2'b0), + .fabric_fpga_awcache_axi_m2_i (4'b0), + .fabric_fpga_awid_axi_m2_i (5'b0), + .fabric_fpga_awlen_axi_m2_i (8'b0), + .fabric_fpga_awlock_axi_m2_i (1'b0), + .fabric_fpga_awprot_axi_m2_i (3'b0), + .fabric_fpga_awqos_axi_m2_i (4'b0), + .fabric_fpga_awsize_axi_m2_i (3'b0), + .fabric_fpga_awvalid_axi_m2_i (1'b0), + .fabric_fpga_bready_axi_m2_i (1'b0), + .fabric_fpga_dma_last_m2_i (6'b0), + .fabric_fpga_dma_req_m2_i (6'b0), + .fabric_fpga_dma_single_m2_i (6'b0), + .fabric_fpga_rready_axi_m2_i (1'b0), + .fabric_fpga_wdata_axi_m2_i (128'b0), + .fabric_fpga_wlast_axi_m2_i (1'b0), + .fabric_fpga_wstrb_axi_m2_i (16'b0), + .fabric_fpga_wvalid_axi_m2_i (1'b0), + // DDR0 (unused) + .fabric_fpga_ddr0_arready_o (), + .fabric_fpga_ddr0_awready_o (), + .fabric_fpga_ddr0_bid_o (), + .fabric_fpga_ddr0_bresp_o (), + .fabric_fpga_ddr0_bvalid_o (), + .fabric_fpga_ddr0_rdata_o (), + .fabric_fpga_ddr0_rid_o (), + .fabric_fpga_ddr0_rlast_o (), + .fabric_fpga_ddr0_rresp_o (), + .fabric_fpga_ddr0_rvalid_o (), + .fabric_fpga_ddr0_wready_o (), + .fabric_fpga_ddr0_araddr_i (40'b0), + .fabric_fpga_ddr0_arburst_i (2'b0), + .fabric_fpga_ddr0_arcache_i (4'b0), + .fabric_fpga_ddr0_arid_i (5'b0), + .fabric_fpga_ddr0_arlen_i (8'b0), + .fabric_fpga_ddr0_arlock_i (1'b0), + .fabric_fpga_ddr0_arprot_i (3'b0), + .fabric_fpga_ddr0_arqos_i (4'b0), + .fabric_fpga_ddr0_arsize_i (3'b0), + .fabric_fpga_ddr0_arvalid_i (1'b0), + .fabric_fpga_ddr0_awaddr_i (40'b0), + .fabric_fpga_ddr0_awburst_i (2'b0), + .fabric_fpga_ddr0_awcache_i (4'b0), + .fabric_fpga_ddr0_awid_i (5'b0), + .fabric_fpga_ddr0_awlen_i (8'b0), + .fabric_fpga_ddr0_awlock_i (1'b0), + .fabric_fpga_ddr0_awprot_i (3'b0), + .fabric_fpga_ddr0_awqos_i (4'b0), + .fabric_fpga_ddr0_awsize_i (3'b0), + .fabric_fpga_ddr0_awvalid_i (1'b0), + .fabric_fpga_ddr0_bready_i (1'b0), + .fabric_fpga_ddr0_rready_i (1'b0), + .fabric_fpga_ddr0_wdata_i (128'b0), + .fabric_fpga_ddr0_wlast_i (1'b0), + .fabric_fpga_ddr0_wstrb_i (16'b0), + .fabric_fpga_ddr0_wvalid_i (1'b0), + // APB (unused) + .fabric_fpga_paddr_apb_o (), + .fabric_fpga_penable_apb_o (), + .fabric_fpga_psel_apb_o (), + .fabric_fpga_pwdata_apb_o (), + .fabric_fpga_pwrite_apb_o (), + .fabric_fpga_prdata_apb_i (32'b0), + .fabric_fpga_pready_apb_i (1'b0), + .fabric_fpga_pslverr_apb_i (1'b0), + // LLPP0 (unused) + .fabric_llpp0_araddr_s_o (), + .fabric_llpp0_arburst_s_o (), + .fabric_llpp0_arcache_s_o (), + .fabric_llpp0_arid_s_o (), + .fabric_llpp0_arlen_s_o (), + .fabric_llpp0_arlock_s_o (), + .fabric_llpp0_arprot_s_o (), + .fabric_llpp0_arqos_s_o (), + .fabric_llpp0_arsize_s_o (), + .fabric_llpp0_arvalid_s_o (), + .fabric_llpp0_awaddr_s_o (), + .fabric_llpp0_awburst_s_o (), + .fabric_llpp0_awcache_s_o (), + .fabric_llpp0_awid_s_o (), + .fabric_llpp0_awlen_s_o (), + .fabric_llpp0_awlock_s_o (), + .fabric_llpp0_awprot_s_o (), + .fabric_llpp0_awqos_s_o (), + .fabric_llpp0_awsize_s_o (), + .fabric_llpp0_awvalid_s_o (), + .fabric_llpp0_bready_s_o (), + .fabric_llpp0_rready_s_o (), + .fabric_llpp0_wdata_s_o (), + .fabric_llpp0_wlast_s_o (), + .fabric_llpp0_wstrb_s_o (), + .fabric_llpp0_wvalid_s_o (), + .fabric_llpp0_arready_s_i (1'b0), + .fabric_llpp0_awready_s_i (1'b0), + .fabric_llpp0_bid_s_i (12'b0), + .fabric_llpp0_bresp_s_i (2'b0), + .fabric_llpp0_bvalid_s_i (1'b0), + .fabric_llpp0_rdata_s_i (32'b0), + .fabric_llpp0_rid_s_i (12'b0), + .fabric_llpp0_rlast_s_i (1'b0), + .fabric_llpp0_rresp_s_i (2'b0), + .fabric_llpp0_rvalid_s_i (1'b0), + .fabric_llpp0_wready_s_i (1'b0), + // LLPP1 (unused) + .fabric_llpp1_araddr_s_o (), + .fabric_llpp1_arburst_s_o (), + .fabric_llpp1_arcache_s_o (), + .fabric_llpp1_arid_s_o (), + .fabric_llpp1_arlen_s_o (), + .fabric_llpp1_arlock_s_o (), + .fabric_llpp1_arprot_s_o (), + .fabric_llpp1_arqos_s_o (), + .fabric_llpp1_arsize_s_o (), + .fabric_llpp1_arvalid_s_o (), + .fabric_llpp1_awaddr_s_o (), + .fabric_llpp1_awburst_s_o (), + .fabric_llpp1_awcache_s_o (), + .fabric_llpp1_awid_s_o (), + .fabric_llpp1_awlen_s_o (), + .fabric_llpp1_awlock_s_o (), + .fabric_llpp1_awprot_s_o (), + .fabric_llpp1_awqos_s_o (), + .fabric_llpp1_awsize_s_o (), + .fabric_llpp1_awvalid_s_o (), + .fabric_llpp1_bready_s_o (), + .fabric_llpp1_rready_s_o (), + .fabric_llpp1_wdata_s_o (), + .fabric_llpp1_wlast_s_o (), + .fabric_llpp1_wstrb_s_o (), + .fabric_llpp1_wvalid_s_o (), + .fabric_llpp1_arready_s_i (1'b0), + .fabric_llpp1_awready_s_i (1'b0), + .fabric_llpp1_bid_s_i (12'b0), + .fabric_llpp1_bresp_s_i (2'b0), + .fabric_llpp1_bvalid_s_i (1'b0), + .fabric_llpp1_rdata_s_i (32'b0), + .fabric_llpp1_rid_s_i (12'b0), + .fabric_llpp1_rlast_s_i (1'b0), + .fabric_llpp1_rresp_s_i (2'b0), + .fabric_llpp1_rvalid_s_i (1'b0), + .fabric_llpp1_wready_s_i (1'b0), + // LLPP2 (unused) + .fabric_llpp2_araddr_s_o (), + .fabric_llpp2_arburst_s_o (), + .fabric_llpp2_arcache_s_o (), + .fabric_llpp2_arid_s_o (), + .fabric_llpp2_arlen_s_o (), + .fabric_llpp2_arlock_s_o (), + .fabric_llpp2_arprot_s_o (), + .fabric_llpp2_arqos_s_o (), + .fabric_llpp2_arsize_s_o (), + .fabric_llpp2_arvalid_s_o (), + .fabric_llpp2_awaddr_s_o (), + .fabric_llpp2_awburst_s_o (), + .fabric_llpp2_awcache_s_o (), + .fabric_llpp2_awid_s_o (), + .fabric_llpp2_awlen_s_o (), + .fabric_llpp2_awlock_s_o (), + .fabric_llpp2_awprot_s_o (), + .fabric_llpp2_awqos_s_o (), + .fabric_llpp2_awsize_s_o (), + .fabric_llpp2_awvalid_s_o (), + .fabric_llpp2_bready_s_o (), + .fabric_llpp2_rready_s_o (), + .fabric_llpp2_wdata_s_o (), + .fabric_llpp2_wlast_s_o (), + .fabric_llpp2_wstrb_s_o (), + .fabric_llpp2_wvalid_s_o (), + .fabric_llpp2_arready_s_i (1'b0), + .fabric_llpp2_awready_s_i (1'b0), + .fabric_llpp2_bid_s_i (12'b0), + .fabric_llpp2_bresp_s_i (2'b0), + .fabric_llpp2_bvalid_s_i (1'b0), + .fabric_llpp2_rdata_s_i (32'b0), + .fabric_llpp2_rid_s_i (12'b0), + .fabric_llpp2_rlast_s_i (1'b0), + .fabric_llpp2_rresp_s_i (2'b0), + .fabric_llpp2_rvalid_s_i (1'b0), + .fabric_llpp2_wready_s_i (1'b0), + // LLPP3 (unused) + .fabric_llpp3_araddr_s_o (), + .fabric_llpp3_arburst_s_o (), + .fabric_llpp3_arcache_s_o (), + .fabric_llpp3_arid_s_o (), + .fabric_llpp3_arlen_s_o (), + .fabric_llpp3_arlock_s_o (), + .fabric_llpp3_arprot_s_o (), + .fabric_llpp3_arqos_s_o (), + .fabric_llpp3_arsize_s_o (), + .fabric_llpp3_arvalid_s_o (), + .fabric_llpp3_awaddr_s_o (), + .fabric_llpp3_awburst_s_o (), + .fabric_llpp3_awcache_s_o (), + .fabric_llpp3_awid_s_o (), + .fabric_llpp3_awlen_s_o (), + .fabric_llpp3_awlock_s_o (), + .fabric_llpp3_awprot_s_o (), + .fabric_llpp3_awqos_s_o (), + .fabric_llpp3_awsize_s_o (), + .fabric_llpp3_awvalid_s_o (), + .fabric_llpp3_bready_s_o (), + .fabric_llpp3_rready_s_o (), + .fabric_llpp3_wdata_s_o (), + .fabric_llpp3_wlast_s_o (), + .fabric_llpp3_wstrb_s_o (), + .fabric_llpp3_wvalid_s_o (), + .fabric_llpp3_arready_s_i (1'b0), + .fabric_llpp3_awready_s_i (1'b0), + .fabric_llpp3_bid_s_i (12'b0), + .fabric_llpp3_bresp_s_i (2'b0), + .fabric_llpp3_bvalid_s_i (1'b0), + .fabric_llpp3_rdata_s_i (32'b0), + .fabric_llpp3_rid_s_i (12'b0), + .fabric_llpp3_rlast_s_i (1'b0), + .fabric_llpp3_rresp_s_i (2'b0), + .fabric_llpp3_rvalid_s_i (1'b0), + .fabric_llpp3_wready_s_i (1'b0), + // QoS (unused) + .fabric_qos_pprdata_o (), + .fabric_qos_ppready_o (), + .fabric_qos_ppslverr_o (), + .fabric_qos_ppaddr_i (32'b0), + .fabric_qos_ppenable_i (1'b0), + .fabric_qos_ppwdata_i (32'b0), + .fabric_qos_ppwrite_i (1'b0), + .fabric_qos_presetn_i (1'b0), + .fabric_qos_psel_i (1'b0), + // TND (unused) + .fabric_tnd_hssl_flushin_o (), + .fabric_tnd_hssl_trigin_o (), + .fabric_tnd_fpga_apb_master_paddr_o (), + .fabric_tnd_fpga_apb_master_penable_o (), + .fabric_tnd_fpga_apb_master_psel_o (), + .fabric_tnd_fpga_apb_master_pwdata_o (), + .fabric_tnd_fpga_apb_master_pwrite_o (), + .fabric_tnd_fpga_atb_master_afvalid_o (), + .fabric_tnd_fpga_atb_master_atready_o (), + .fabric_tnd_fpga_atb_master_syncreq_o (), + .fabric_tnd_hssl_apb_master_paddr_o (), + .fabric_tnd_hssl_apb_master_penable_o (), + .fabric_tnd_hssl_apb_master_psel_o (), + .fabric_tnd_hssl_apb_master_pwdata_o (), + .fabric_tnd_hssl_apb_master_pwrite_o (), + .fabric_tnd_hssl_atb_master_afready_o (), + .fabric_tnd_hssl_atb_master_atbytes_o (), + .fabric_tnd_hssl_atb_master_atdata_o (), + .fabric_tnd_hssl_atb_master_atid_o (), + .fabric_tnd_hssl_atb_master_atvalid_o (), + .fabric_tnd_trace_clk_traceoutportintf_o (), + .fabric_tnd_trace_ctl_traceoutportintf_o (), + .fabric_tnd_trace_data_traceoutportintf_o (), + .fabric_tsvalue_tsgen_fpga_o (), + .fabric_tnd_fpga_apb_master_prdata_i (32'b0), + .fabric_tnd_fpga_apb_master_pready_i (1'b0), + .fabric_tnd_fpga_apb_master_pslverr_i (1'b0), + .fabric_tnd_fpga_atb_master_afready_i (1'b0), + .fabric_tnd_fpga_atb_master_atbytes_i (4'b0), + .fabric_tnd_fpga_atb_master_atdata_i (128'b0), + .fabric_tnd_fpga_atb_master_atid_i (7'b0), + .fabric_tnd_fpga_atb_master_atvalid_i (1'b0), + .fabric_tnd_hssl_apb_master_prdata_i (32'b0), + .fabric_tnd_hssl_apb_master_pready_i (1'b0), + .fabric_tnd_hssl_apb_master_pslverr_i (1'b0), + .fabric_tnd_hssl_atb_master_afvalid_i (1'b0), + .fabric_tnd_hssl_atb_master_atready_i (1'b0), + .fabric_tnd_hssl_atb_master_syncreq_i (1'b0), + // Watchdog (unused) + .fabric_watchdog0_signal_0_o (), + .fabric_watchdog0_signal_1_o (), + .fabric_watchdog1_signal_0_o (), + .fabric_watchdog1_signal_1_o (), + .fabric_watchdog2_signal_0_o (), + .fabric_watchdog2_signal_1_o (), + .fabric_watchdog3_signal_0_o (), + .fabric_watchdog3_signal_1_o (), + .fabric_tst_pll_lock_o (), + .fabric_soc_mon_sensor_alarm_o (), + // EROM (unused) + .fabric_erom_fpga_cpu0_dbgen_i (1'b0), + .fabric_erom_fpga_cpu0_hiden_i (1'b0), + .fabric_erom_fpga_cpu0_hniden_i (1'b0), + .fabric_erom_fpga_cpu0_niden_i (1'b0), + .fabric_erom_fpga_cpu1_dbgen_i (1'b0), + .fabric_erom_fpga_cpu1_hiden_i (1'b0), + .fabric_erom_fpga_cpu1_hniden_i (1'b0), + .fabric_erom_fpga_cpu1_niden_i (1'b0), + .fabric_erom_fpga_cpu2_dbgen_i (1'b0), + .fabric_erom_fpga_cpu2_hiden_i (1'b0), + .fabric_erom_fpga_cpu2_hniden_i (1'b0), + .fabric_erom_fpga_cpu2_niden_i (1'b0), + .fabric_erom_fpga_cpu3_dbgen_i (1'b0), + .fabric_erom_fpga_cpu3_hiden_i (1'b0), + .fabric_erom_fpga_cpu3_hniden_i (1'b0), + .fabric_erom_fpga_cpu3_niden_i (1'b0), + .fabric_erom_fpga_cs_dbgen_i (1'b0), + .fabric_erom_fpga_cs_niden_i (1'b0), + .fabric_erom_fpga_cs_deviceen_i (1'b0), + .fabric_erom_fpga_cs_rst_n_i (1'b0), + .fabric_erom_fpga_debug_en_i (1'b0), + .fabric_enable_TMR_i ({3{1'b1}}), + .fabric_spw_interrupts_toggle_o (), + .fabric_spw_interrupts_o (), + .fabric_fpga_dma_hs_rstn_i (6'b0) +); + +// HLS4ML PLL BEGIN (autogenerated for ClockPeriod; do not edit inside) +// PLL 1: VCO=750.0MHz (PFD=25.0MHz), generates 50.0 MHz +NX_PLL_U #( + .location (""), + .ref_osc_on (1'b1), + .use_pll (1'b1), + .ext_fbk_on (1'b0), + .fbk_delay_on (1'b0), + .fbk_delay (6'd0), + .ref_intdiv (5'd15), + .fbk_intdiv (7'd14), + .clk_outdiv1 (3'd6), + .clk_outdiv2 (3'd0), + .clk_outdiv3 (3'd0), + .clk_outdiv4 (3'd0), + .clk_outdivd1 (4'd0), + .clk_outdivd2 (4'd0), + .clk_outdivd3 (4'd0), + .clk_outdivd4 (4'd0), + .clk_outdivd5 (4'd0) +) PLL_1 ( + .REF (1'b0), + .FBK (1'b0), + .R (~rstn_i), + .VCO (), + .LDFO (), + .REFO (), + .OSC (), + .CAL_LOCKED(), + .PLL_LOCKED(locked_1), + .CLK_DIV1 (clk_50_0mhz), + .CLK_DIV2 (), + .CLK_DIV3 (), + .CLK_DIV4 (), + .CLK_DIVD1 (), + .CLK_DIVD2 (), + .CLK_DIVD3 (), + .CLK_DIVD4 (), + .CLK_DIVD5 () +); +// HLS4ML PLL END + +// AXI Slave <-> AXI-Stream bridge +AXISlaveStream #( + .C_S_AXI_ID_WIDTH (12), + .C_S_AXI_DATA_WIDTH (128), + .C_S_AXI_ADDR_WIDTH (40), + .HLS_IN_DATA_W (HLS_IN_DATA_W), + .HLS_OUT_DATA_W (HLS_OUT_DATA_W), + .HLS_IN_N_WORDS (HLS_IN_N_WORDS), + .HLS_OUT_N_WORDS (HLS_OUT_N_WORDS), + .LGFLEN_IN (4), + .LGFLEN_OUT (4) +) u_AXISlaveStream ( + .S_AXI_ACLK (clk_50_0mhz), + .S_AXI_ARESETN (rstn_50mhz), + .S_AXI_AWVALID (s1_awvalid), + .S_AXI_AWREADY (s1_awready), + .S_AXI_AWID (s1_awid), + .S_AXI_AWADDR (s1_awaddr), + .S_AXI_AWLEN (s1_awlen), + .S_AXI_AWSIZE (s1_awsize), + .S_AXI_AWBURST (s1_awburst), + .S_AXI_AWLOCK (s1_awlock), + .S_AXI_AWCACHE (s1_awcache), + .S_AXI_AWPROT (s1_awprot), + .S_AXI_AWQOS (s1_awqos), + .S_AXI_WVALID (s1_wvalid), + .S_AXI_WREADY (s1_wready), + .S_AXI_WDATA (s1_wdata), + .S_AXI_WSTRB (s1_wstrb), + .S_AXI_WLAST (s1_wlast), + .S_AXI_BVALID (s1_bvalid), + .S_AXI_BREADY (s1_bready), + .S_AXI_BID (s1_bid), + .S_AXI_BRESP (s1_bresp), + .S_AXI_ARVALID (s1_arvalid), + .S_AXI_ARREADY (s1_arready), + .S_AXI_ARID (s1_arid), + .S_AXI_ARADDR (s1_araddr), + .S_AXI_ARLEN (s1_arlen), + .S_AXI_ARSIZE (s1_arsize), + .S_AXI_ARBURST (s1_arburst), + .S_AXI_ARLOCK (s1_arlock), + .S_AXI_ARCACHE (s1_arcache), + .S_AXI_ARPROT (s1_arprot), + .S_AXI_ARQOS (s1_arqos), + .S_AXI_RVALID (s1_rvalid), + .S_AXI_RREADY (s1_rready), + .S_AXI_RID (s1_rid), + .S_AXI_RDATA (s1_rdata), + .S_AXI_RRESP (s1_rresp), + .S_AXI_RLAST (s1_rlast), + .o_interrupt (dma_interrupt), + .hls_in_tdata (hls_in_tdata), + .hls_in_tvalid (hls_in_tvalid), + .hls_in_tready (hls_in_tready), + .hls_out_tdata (hls_out_tdata), + .hls_out_tvalid(hls_out_tvalid), + .hls_out_tready(hls_out_tready), + .start_out (hls_start), + .done_in (hls_done) +); + +// HLS IP (myproject wrapper from gen_myproject_wrapper.py, AXIS mode) +myproject u_myproject ( + .clock (clk_50_0mhz), + .reset (rstn_50mhz), + .start_port (hls_start), + .done_port (hls_done), + .hls_in_tdata (hls_in_tdata), + .hls_in_tvalid (hls_in_tvalid), + .hls_in_tready (hls_in_tready), + .hls_out_tdata (hls_out_tdata), + .hls_out_tvalid (hls_out_tvalid), + .hls_out_tready (hls_out_tready) +); + +// Drive LEDs with AXI activity indicators +always @(posedge clk_50_0mhz) begin + if (!rstn_i) begin + led_0 <= 1'b0; + led_1 <= 1'b0; + led_2 <= 1'b0; + led_3 <= 1'b0; + end else begin + if (s1_awvalid) led_0 <= 1'b1; + if (s1_wvalid) led_1 <= 1'b1; + if (s1_arvalid) led_2 <= 1'b1; + if (s1_rvalid) led_3 <= 1'b1; + end +end + +endmodule + +`default_nettype wire diff --git a/hls4ml/templates/vitis/build_opt.tcl b/hls4ml/templates/vitis/build_opt.tcl index 30b9bc96b1..8cced5d5f7 100644 --- a/hls4ml/templates/vitis/build_opt.tcl +++ b/hls4ml/templates/vitis/build_opt.tcl @@ -7,4 +7,5 @@ array set opt { export 0 vsynth 0 fifo_opt 0 + pnr 0 } diff --git a/hls4ml/templates/vitis/build_prj.tcl b/hls4ml/templates/vitis/build_prj.tcl index d0fc5c1c1a..8daa393cfd 100644 --- a/hls4ml/templates/vitis/build_prj.tcl +++ b/hls4ml/templates/vitis/build_prj.tcl @@ -99,6 +99,89 @@ proc add_vcd_instructions_tcl {} { file rename -force $temp $filename } +# Generate RTL simulation JSON report from transaction file (latency and II in clock cycles) +proc generate_rtl_sim_report { project_name } { + set transaction_file ${project_name}_prj/solution1/sim/verilog/${project_name}.performance.result.transaction.xml + file mkdir vivado_reports + set report_json vivado_reports/rtl_sim_${project_name}_report.json + if {![file exists $transaction_file]} { + puts "WARNING: Transaction file not found: $transaction_file (skipping RTL sim report)" + return + } + set latency_min 0 + set latency_max 0 + set latency_sum 0 + set latency_count 0 + set ii_min 0 + set ii_max 0 + set ii_sum 0 + set ii_count 0 + set first_latency 1 + set first_ii 1 + set fh [open $transaction_file r] + while {[gets $fh line] >= 0} { + if {[regexp {transaction\s+\d+:\s+(\d+)\s+(\d+|x)} $line -> lat_val ii_val]} { + if {[string is integer -strict $lat_val]} { + set lat [expr {int($lat_val)}] + if $first_latency { + set latency_min $lat + set latency_max $lat + set latency_sum $lat + set latency_count 1 + set first_latency 0 + } else { + if {$lat < $latency_min} { set latency_min $lat } + if {$lat > $latency_max} { set latency_max $lat } + set latency_sum [expr {$latency_sum + $lat}] + incr latency_count + } + } + if {$ii_val != "x" && [string is integer -strict $ii_val]} { + set ii [expr {int($ii_val)}] + if $first_ii { + set ii_min $ii + set ii_max $ii + set ii_sum $ii + set ii_count 1 + set first_ii 0 + } else { + if {$ii < $ii_min} { set ii_min $ii } + if {$ii > $ii_max} { set ii_max $ii } + set ii_sum [expr {$ii_sum + $ii}] + incr ii_count + } + } + } + } + close $fh + set latency_avg 0 + if {$latency_count > 0} { + set latency_avg [expr {double($latency_sum) / $latency_count}] + } + set ii_avg 0 + if {$ii_count > 0} { + set ii_avg [expr {double($ii_sum) / $ii_count}] + } + set ofile [open $report_json w] + puts $ofile "\{" + puts $ofile " \"transaction_count\": $latency_count," + puts $ofile " \"latency\": \{" + puts $ofile " \"min\": $latency_min," + puts $ofile " \"max\": $latency_max," + puts $ofile " \"avg\": $latency_avg" + puts $ofile " \}," + puts $ofile " \"initiation_interval\": \{" + puts $ofile " \"min\": $ii_min," + puts $ofile " \"max\": $ii_max," + puts $ofile " \"avg\": $ii_avg" + puts $ofile " \}" + puts $ofile "\}" + flush $ofile + close $ofile + puts "INFO: RTL sim report written to $report_json" + return $report_json +} + proc report_time { op_name time_start time_end } { set time_taken [expr $time_end - $time_start] set time_s [expr ($time_taken / 1000) % 60] @@ -196,9 +279,11 @@ if {$opt(cosim)} { set time_end [clock clicks -milliseconds] puts "INFO:" if {[string equal "$backend" "vivadoaccelerator"]} { - puts [read [open ${project_name}_prj/solution1/sim/report/${project_name}_axi_cosim.rpt r]] + set report_path [generate_rtl_sim_report ${project_name}_axi] + puts [read [open $report_path r]] } else { - puts [read [open ${project_name}_prj/solution1/sim/report/${project_name}_cosim.rpt r]] + set report_path [generate_rtl_sim_report ${project_name}] + puts [read [open $report_path r]] } report_time "C/RTL SIMULATION" $time_start $time_end } @@ -227,7 +312,7 @@ if {$opt(vsynth)} { puts "***** VIVADO SYNTHESIS *****" if {[file exist ${project_name}_prj/solution1/syn/verilog]} { set time_start [clock clicks -milliseconds] - exec vivado -mode batch -source vivado_synth.tcl >@ stdout + exec vivado -mode batch -source vivado_synth.tcl -tclargs $opt(pnr) >@ stdout set time_end [clock clicks -milliseconds] report_time "VIVADO SYNTHESIS" $time_start $time_end } else { diff --git a/hls4ml/templates/vivado/build_opt.tcl b/hls4ml/templates/vivado/build_opt.tcl new file mode 100644 index 0000000000..8cced5d5f7 --- /dev/null +++ b/hls4ml/templates/vivado/build_opt.tcl @@ -0,0 +1,11 @@ +array set opt { + reset 0 + csim 1 + synth 1 + cosim 1 + validation 1 + export 0 + vsynth 0 + fifo_opt 0 + pnr 0 +} diff --git a/hls4ml/templates/vivado/build_prj.tcl b/hls4ml/templates/vivado/build_prj.tcl index 888c5f4c95..d787b655f5 100644 --- a/hls4ml/templates/vivado/build_prj.tcl +++ b/hls4ml/templates/vivado/build_prj.tcl @@ -1,19 +1,12 @@ ################# # HLS4ML ################# -array set opt { - reset 0 - csim 1 - synth 1 - cosim 1 - validation 1 - export 0 - vsynth 0 - fifo_opt 0 -} set tcldir [file dirname [info script]] source [file join $tcldir project.tcl] +# Vivado makefiles treat DEBUG as compiler flags; drop shell DEBUG=release, etc. +if {[info exists ::env(DEBUG)]} { unset ::env(DEBUG) } +source [file join $tcldir build_opt.tcl] proc remove_recursive_log_wave {} { set tcldir [file dirname [info script]] @@ -108,10 +101,88 @@ proc add_vcd_instructions_tcl {} { file rename -force $temp $filename } -foreach arg $::argv { - foreach o [lsort [array names opt]] { - regexp "$o=+(\\w+)" $arg unused opt($o) +# Generate RTL simulation JSON report from transaction file (latency and II in clock cycles) +# top_name: top function name (for VivadoAccelerator use ${project_name}_axi, otherwise ${project_name}) +proc generate_rtl_sim_report { top_name project_name } { + set transaction_file ${project_name}_prj/solution1/sim/verilog/${top_name}.performance.result.transaction.xml + file mkdir vivado_reports + set report_json vivado_reports/rtl_sim_${project_name}_report.json + if {![file exists $transaction_file]} { + puts "WARNING: Transaction file not found: $transaction_file (skipping RTL sim report)" + return + } + set latency_min 0 + set latency_max 0 + set latency_sum 0 + set latency_count 0 + set ii_min 0 + set ii_max 0 + set ii_sum 0 + set ii_count 0 + set first_latency 1 + set first_ii 1 + set fh [open $transaction_file r] + while {[gets $fh line] >= 0} { + if {[regexp {transaction\s+\d+:\s+(\d+)\s+(\d+|x)} $line -> lat_val ii_val]} { + if {[string is integer -strict $lat_val]} { + set lat [expr {int($lat_val)}] + if $first_latency { + set latency_min $lat + set latency_max $lat + set latency_sum $lat + set latency_count 1 + set first_latency 0 + } else { + if {$lat < $latency_min} { set latency_min $lat } + if {$lat > $latency_max} { set latency_max $lat } + set latency_sum [expr {$latency_sum + $lat}] + incr latency_count + } + } + if {$ii_val != "x" && [string is integer -strict $ii_val]} { + set ii [expr {int($ii_val)}] + if $first_ii { + set ii_min $ii + set ii_max $ii + set ii_sum $ii + set ii_count 1 + set first_ii 0 + } else { + if {$ii < $ii_min} { set ii_min $ii } + if {$ii > $ii_max} { set ii_max $ii } + set ii_sum [expr {$ii_sum + $ii}] + incr ii_count + } + } + } + } + close $fh + set latency_avg 0 + if {$latency_count > 0} { + set latency_avg [expr {double($latency_sum) / $latency_count}] + } + set ii_avg 0 + if {$ii_count > 0} { + set ii_avg [expr {double($ii_sum) / $ii_count}] } + set ofile [open $report_json w] + puts $ofile "\{" + puts $ofile " \"transaction_count\": $latency_count," + puts $ofile " \"latency\": \{" + puts $ofile " \"min\": $latency_min," + puts $ofile " \"max\": $latency_max," + puts $ofile " \"avg\": $latency_avg" + puts $ofile " \}," + puts $ofile " \"initiation_interval\": \{" + puts $ofile " \"min\": $ii_min," + puts $ofile " \"max\": $ii_max," + puts $ofile " \"avg\": $ii_avg" + puts $ofile " \}" + puts $ofile "\}" + flush $ofile + close $ofile + puts "INFO: RTL sim report written to $report_json" + return $report_json } proc report_time { op_name time_start time_end } { @@ -151,9 +222,10 @@ if {$opt(reset)} { } else { open_project ${project_name}_prj } +set common_cflags "-std=c++0x -DHLS_NO_XIL_FPO_LIB" set_top ${project_name} -add_files firmware/${project_name}.cpp -cflags "-std=c++0x" -add_files -tb ${project_name}_test.cpp -cflags "-std=c++0x" +add_files firmware/${project_name}.cpp -cflags $common_cflags +add_files -tb ${project_name}_test.cpp -cflags $common_cflags add_files -tb firmware/weights add_files -tb tb_data if {$opt(reset)} { @@ -189,7 +261,7 @@ if {$opt(synth)} { if {$opt(cosim)} { puts "***** C/RTL SIMULATION *****" # TODO: This is a workaround (Xilinx defines __RTL_SIMULATION__ only for SystemC testbenches). - add_files -tb ${project_name}_test.cpp -cflags "-std=c++0x -DRTL_SIM" + add_files -tb ${project_name}_test.cpp -cflags "$common_cflags -DRTL_SIM" set time_start [clock clicks -milliseconds] cosim_design -trace_level all -setup @@ -211,9 +283,13 @@ if {$opt(cosim)} { set time_end [clock clicks -milliseconds] puts "INFO:" if {[string equal "$backend" "vivadoaccelerator"]} { - puts [read [open ${project_name}_prj/solution1/sim/report/${project_name}_axi_cosim.rpt r]] + set report_path [generate_rtl_sim_report ${project_name}_axi $project_name] + puts [read [open $report_path r]] + #puts [read [open ${project_name}_prj/solution1/sim/report/${project_name}_axi_cosim.rpt r]] } else { - puts [read [open ${project_name}_prj/solution1/sim/report/${project_name}_cosim.rpt r]] + set report_path [generate_rtl_sim_report ${project_name} $project_name] + puts [read [open $report_path r]] + # puts [read [open ${project_name}_prj/solution1/sim/report/${project_name}_cosim.rpt r]] } report_time "C/RTL SIMULATION" $time_start $time_end } @@ -242,7 +318,7 @@ if {$opt(vsynth)} { puts "***** VIVADO SYNTHESIS *****" if {[file exist ${project_name}_prj/solution1/syn/verilog]} { set time_start [clock clicks -milliseconds] - exec vivado -mode batch -source vivado_synth.tcl >@ stdout + exec vivado -mode batch -source vivado_synth.tcl -tclargs $opt(pnr) >@ stdout set time_end [clock clicks -milliseconds] report_time "VIVADO SYNTHESIS" $time_start $time_end } else { diff --git a/hls4ml/templates/vivado/statistics.tcl b/hls4ml/templates/vivado/statistics.tcl new file mode 100644 index 0000000000..87d113f8d0 --- /dev/null +++ b/hls4ml/templates/vivado/statistics.tcl @@ -0,0 +1,72 @@ +# Shared dump_statistics procedure for Vivado accelerator and regular Vivado design scripts. +# Parses utilization, power, and timing reports into JSON. + +proc dump_statistics { outputDir reportBase stage_name} { + set reportJson [file join $outputDir ${stage_name}_${reportBase}.json] + set util_rpt [report_utilization -return_string] + set SliceRegisters 0 + set Slice 0 + set SliceLUTs 0 + set BRAMFIFO36 0 + set BRAMFIFO18 0 + set BRAMFIFO36_star 0 + set BRAMFIFO18_star 0 + set BRAM18 0 + set BRAMFIFO 0 + set DRAM 0 + set BIOB 0 + set DSPs 0 + set TotPower 0 + set DynamicPower 0 + set StaticPower 0 + set design_slack 0 + set design_req 0 + regexp -- {\s*Slice Registers\s*\|\s*([^[:blank:]]+)} $util_rpt ignore SliceRegisters + regexp -- {\s*Slice\s*\|\s*([^[:blank:]]+)} $util_rpt ignore Slice + regexp -- {\s*LUT as Logic\s*\|\s*([^[:blank:]]+)} $util_rpt ignore SliceLUTs + regexp -- {\s*RAMB36/FIFO36\s*\|\s*([^[:blank:]]+)} $util_rpt ignore BRAMFIFO36 + regexp -- {\s*RAMB18/FIFO18\s*\|\s*([^[:blank:]]+)} $util_rpt ignore BRAMFIFO18 + regexp -- {\s*RAMB36/FIFO\*\s*\|\s*([^[:blank:]]+)} $util_rpt ignore BRAMFIFO36_star + regexp -- {\s*RAMB18/FIFO\*\s*\|\s*([^[:blank:]]+)} $util_rpt ignore BRAMFIFO18_star + regexp -- {\s*RAMB18\s*\|\s*([^[:blank:]]+)} $util_rpt ignore BRAM18 + set BRAMFIFO [expr {(2 *$BRAMFIFO36) + $BRAMFIFO18 + (2*$BRAMFIFO36_star) + $BRAMFIFO18_star + $BRAM18}] + regexp -- {\s*LUT as Memory\s*\|\s*([^[:blank:]]+)} $util_rpt ignore DRAM + regexp -- {\s*Bonded IOB\s*\|\s*([^[:blank:]]+)} $util_rpt ignore BIOB + regexp -- {\s*DSPs\s*\|\s*([^[:blank:]]+)} $util_rpt ignore DSPs + set power_rpt [report_power -return_string] + regexp -- {\s*Total On-Chip Power \(W\)\s*\|\s*([^[:blank:]]+)} $power_rpt ignore TotPower + regexp -- {\s*Dynamic \(W\)\s*\|\s*([^[:blank:]]+)} $power_rpt ignore DynamicPower + regexp -- {\s*Device Static \(W\)\s*\|\s*([^[:blank:]]+)} $power_rpt ignore StaticPower + set Timing_Paths [get_timing_paths -max_paths 1 -nworst 1 -setup] + if { [expr {$Timing_Paths == ""}] } { + set design_slack 0 + set design_req 0 + } else { + set design_slack [get_property SLACK $Timing_Paths] + set design_req [get_property REQUIREMENT $Timing_Paths] + } + if { [expr {$design_slack == ""}] } { + set design_slack 0 + } + if { [expr {$design_req == ""}] } { + set design_req 0 + } + set ofile_json [open $reportJson w] + puts $ofile_json "\{" + puts $ofile_json " \"XILINX_SYNTHESIS_SUMMARY\": \{" + puts $ofile_json " \"XILINX_SLICE\": \"$Slice\"," + puts $ofile_json " \"XILINX_SLICE_REGISTERS\": \"$SliceRegisters\"," + puts $ofile_json " \"XILINX_SLICE_LUTS\": \"$SliceLUTs\"," + puts $ofile_json " \"XILINX_BLOCK_RAMFIFO\": \"$BRAMFIFO\"," + puts $ofile_json " \"XILINX_DRAM\": \"$DRAM\"," + puts $ofile_json " \"XILINX_IOPIN\": \"$BIOB\"," + puts $ofile_json " \"XILINX_DSPS\": \"$DSPs\"," + puts $ofile_json " \"XILINX_POWER\": \"$TotPower\"," + puts $ofile_json " \"XILINX_POWER_DYNAMIC\": \"$DynamicPower\"," + puts $ofile_json " \"XILINX_POWER_STATIC\": \"$StaticPower\"," + puts $ofile_json " \"XILINX_CLOCK_SLACK\": \"$design_slack\"" + puts $ofile_json " \}" + puts $ofile_json "\}" + flush $ofile_json + close $ofile_json +} diff --git a/hls4ml/templates/vivado/vivado_synth.tcl b/hls4ml/templates/vivado/vivado_synth.tcl index 342b1e6740..b4518d8e9a 100644 --- a/hls4ml/templates/vivado/vivado_synth.tcl +++ b/hls4ml/templates/vivado/vivado_synth.tcl @@ -1,7 +1,62 @@ set tcldir [file dirname [info script]] source [file join $tcldir project.tcl] +source [file join $tcldir statistics.tcl] +# From build_prj.tcl: vivado ... -tclargs $opt(pnr). 1 = full implementation to post_route; 0 = post_synth only. +if {[llength $argv] >= 1} { + set pnr [lindex $argv 0] +} else { + set pnr 0 +} + +set outputDir vivado_reports +set reportBase ${project_name}_report + +set_param general.maxThreads 1 +file mkdir $outputDir +create_project ${project_name}_vsynth -part $part -force add_files ${project_name}_prj/solution1/syn/verilog -synth_design -top ${project_name} -part $part -opt_design -retarget -propconst -sweep -bram_power_opt -shift_register_opt -report_utilization -file vivado_synth.rpt +update_compile_order -fileset sources_1 + +set sdcFile ${outputDir}/${project_name}.xdc +set ofile_xdc [open $sdcFile w] +puts $ofile_xdc "create_clock -period $clock_period -name default \[get_ports ap_clk\]" +puts $ofile_xdc "set_property HD.CLK_SRC BUFGCTRL_X0Y0 \[get_ports ap_clk\]" +close $ofile_xdc +read_xdc $sdcFile + +synth_design -mode out_of_context -no_iobuf -top ${project_name} -part $part +write_checkpoint -force $outputDir/post_synth.dcp +report_timing_summary -file $outputDir/post_synth_timing_summary.rpt +report_utilization -file $outputDir/post_synth_util.rpt +report_utilization -hierarchical -hierarchical_percentages -file $outputDir/post_synth_util_hier.rpt +dump_statistics $outputDir $reportBase "post_synth" +if {$pnr} { + opt_design + dump_statistics $outputDir $reportBase "post_opt_design" + report_utilization -file $outputDir/post_opt_design_util.rpt + report_utilization -hierarchical -hierarchical_percentages -file $outputDir/post_opt_design_util_hier.rpt + place_design -directive Explore + report_clock_utilization -file $outputDir/clock_util.rpt + set timing_paths [get_timing_paths -max_paths 1 -nworst 1 -setup] + if {$timing_paths != "" && [get_property SLACK $timing_paths] < 0.5} { + puts "Found setup timing violations => running physical optimization" + phys_opt_design + } + write_checkpoint -force $outputDir/post_place.dcp + report_utilization -file $outputDir/post_place_util.rpt + report_utilization -hierarchical -hierarchical_percentages -file $outputDir/post_place_util_hier.rpt + report_timing_summary -file $outputDir/post_place_timing_summary.rpt + dump_statistics $outputDir $reportBase "post_place" + route_design -directive Explore + write_checkpoint -force $outputDir/post_route.dcp + report_route_status -file $outputDir/post_route_status.rpt + report_timing_summary -file $outputDir/post_route_timing_summary.rpt + report_power -file $outputDir/post_route_power.rpt + report_drc -file $outputDir/post_imp_drc.rpt + report_utilization -file $outputDir/post_route_util.rpt + report_utilization -hierarchical -hierarchical_percentages -file $outputDir/post_route_util_hier.rpt + dump_statistics $outputDir $reportBase "post_route" +} +close_design +close_project diff --git a/hls4ml/templates/vivado_accelerator/build_opt.tcl b/hls4ml/templates/vivado_accelerator/build_opt.tcl new file mode 100644 index 0000000000..7fe2fe9d16 --- /dev/null +++ b/hls4ml/templates/vivado_accelerator/build_opt.tcl @@ -0,0 +1,12 @@ +array set opt { + reset 0 + csim 1 + synth 1 + cosim 1 + validation 1 + export 0 + vsynth 0 + fifo_opt 0 + bitfile 0 + pnr 0 +} diff --git a/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_lite_design.tcl b/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_lite_design.tcl index c14aafb8cb..946cd0b2e3 100644 --- a/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_lite_design.tcl +++ b/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_lite_design.tcl @@ -1,5 +1,14 @@ set tcldir [file dirname [info script]] source [file join $tcldir project.tcl] +source [file join $tcldir statistics.tcl] + +set outputDir vivado_reports +set reportBase ${project_name}_report +set implJobs 4 +if {![catch {set implJobs [exec nproc]}]} { + if {$implJobs < 1} { set implJobs 1 } +} +file mkdir $outputDir create_project project_1 ${project_name}_vivado_accelerator -part xc7z020clg400-1 -force @@ -19,8 +28,17 @@ add_files -norecurse ./${project_name}_vivado_accelerator/project_1.srcs/sources reset_run impl_1 reset_run synth_1 -launch_runs impl_1 -to_step write_bitstream -jobs 6 +launch_runs impl_1 -to_step write_bitstream -jobs $implJobs wait_on_run -timeout 360 impl_1 open_run impl_1 -report_utilization -file util.rpt -hierarchical -hierarchical_percentages +write_checkpoint -force $outputDir/post_route_system.dcp +report_route_status -file $outputDir/post_route_status_system.rpt +report_timing_summary -file $outputDir/post_route_timing_summary_system.rpt +report_power -file $outputDir/post_route_power_system.rpt +report_drc -file $outputDir/post_imp_drc_system.rpt +report_utilization -file $outputDir/post_route_util_system.rpt +report_utilization -hierarchical -hierarchical_percentages -file $outputDir/post_route_util_hier_system.rpt +dump_statistics $outputDir $reportBase "post_route_system" +close_design +close_project diff --git a/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_stream_design.tcl b/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_stream_design.tcl index c5549dc256..c2ee8d80a7 100644 --- a/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_stream_design.tcl +++ b/hls4ml/templates/vivado_accelerator/pynq-z2/tcl_scripts/axi_stream_design.tcl @@ -1,6 +1,15 @@ #@todo: try to remove startgroup and endgroup and see if it work set tcldir [file dirname [info script]] source [file join $tcldir project.tcl] +source [file join $tcldir statistics.tcl] + +set outputDir vivado_reports +set reportBase ${project_name}_report +set implJobs 4 +if {![catch {set implJobs [exec nproc]}]} { + if {$implJobs < 1} { set implJobs 1 } +} +file mkdir $outputDir create_project project_1 ${project_name}_vivado_accelerator -part xc7z020clg400-1 -force @@ -52,8 +61,17 @@ add_files -norecurse ./${project_name}_vivado_accelerator/project_1.srcs/sources reset_run impl_1 reset_run synth_1 -launch_runs impl_1 -to_step write_bitstream -jobs 6 +launch_runs impl_1 -to_step write_bitstream -jobs $implJobs wait_on_run -timeout 360 impl_1 open_run impl_1 -report_utilization -file util.rpt -hierarchical -hierarchical_percentages +write_checkpoint -force $outputDir/post_route_system.dcp +report_route_status -file $outputDir/post_route_status_system.rpt +report_timing_summary -file $outputDir/post_route_timing_summary_system.rpt +report_power -file $outputDir/post_route_power_system.rpt +report_drc -file $outputDir/post_imp_drc_system.rpt +report_utilization -file $outputDir/post_route_util_system.rpt +report_utilization -hierarchical -hierarchical_percentages -file $outputDir/post_route_util_hier_system.rpt +dump_statistics $outputDir $reportBase "post_route_system" +close_design +close_project diff --git a/hls4ml/templates/vivado_accelerator/zcu102/tcl_scripts/axi_stream_design.tcl b/hls4ml/templates/vivado_accelerator/zcu102/tcl_scripts/axi_stream_design.tcl index 5d886c6f25..8b0bebea6f 100644 --- a/hls4ml/templates/vivado_accelerator/zcu102/tcl_scripts/axi_stream_design.tcl +++ b/hls4ml/templates/vivado_accelerator/zcu102/tcl_scripts/axi_stream_design.tcl @@ -1,6 +1,14 @@ -#@todo: try to remove startgroup and endgroup and see if it work set tcldir [file dirname [info script]] source [file join $tcldir project.tcl] +source [file join $tcldir statistics.tcl] + +set outputDir vivado_reports +set reportBase ${project_name}_report +set implJobs 4 +if {![catch {set implJobs [exec nproc]}]} { + if {$implJobs < 1} { set implJobs 1 } +} +file mkdir $outputDir create_project project_1 ${project_name}_vivado_accelerator -part xczu9eg-ffvb1156-2-e -force @@ -51,8 +59,17 @@ add_files -norecurse ./${project_name}_vivado_accelerator/project_1.srcs/sources reset_run impl_1 reset_run synth_1 -launch_runs impl_1 -to_step write_bitstream -jobs 6 +launch_runs impl_1 -to_step write_bitstream -jobs $implJobs wait_on_run -timeout 360 impl_1 open_run impl_1 -report_utilization -file util.rpt -hierarchical -hierarchical_percentages +write_checkpoint -force $outputDir/post_route_system.dcp +report_route_status -file $outputDir/post_route_status_system.rpt +report_timing_summary -file $outputDir/post_route_timing_summary_system.rpt +report_power -file $outputDir/post_route_power_system.rpt +report_drc -file $outputDir/post_imp_drc_system.rpt +report_utilization -file $outputDir/post_route_util_system.rpt +report_utilization -hierarchical -hierarchical_percentages -file $outputDir/post_route_util_hier_system.rpt +dump_statistics $outputDir $reportBase "post_route_system" +close_design +close_project diff --git a/hls4ml/utils/symbolic_utils.py b/hls4ml/utils/symbolic_utils.py index 9bafedc053..a5acb60362 100644 --- a/hls4ml/utils/symbolic_utils.py +++ b/hls4ml/utils/symbolic_utils.py @@ -1,3 +1,4 @@ +import os import subprocess import tempfile @@ -166,8 +167,20 @@ def generate_operator_complexity( hls_libs_path=hls_libs_path, ) hls_model.write() + build_opts = ( + 'array set opt {\n' + ' reset 1\n' + ' csim 0\n' + ' synth 1\n' + ' cosim 0\n' + ' validation 0\n' + ' export 0\n' + '}\n' + ) + with open(os.path.join(tmp_dir, 'build_opt.tcl'), 'w') as f: + f.write(build_opts) subprocess.run( - ['vivado_hls', '-f', 'build_prj.tcl', '"reset=1 synth=1 csim=0 cosim=0 validation=0 export=0"'], + ['vivado_hls', '-f', 'build_prj.tcl'], cwd=tmp_dir, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, @@ -196,8 +209,20 @@ def generate_operator_complexity( hls_libs_path=hls_libs_path, ) hls_model.write() + build_opts = ( + 'array set opt {\n' + ' reset 1\n' + ' csim 0\n' + ' synth 1\n' + ' cosim 0\n' + ' validation 0\n' + ' export 0\n' + '}\n' + ) + with open(os.path.join(tmp_dir, 'build_opt.tcl'), 'w') as f: + f.write(build_opts) subprocess.run( - ['vivado_hls', '-f', 'build_prj.tcl', '"reset=1 synth=1 csim=0 cosim=0 validation=0 export=0"'], + ['vivado_hls', '-f', 'build_prj.tcl'], cwd=tmp_dir, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, diff --git a/hls4ml/writer/__init__.py b/hls4ml/writer/__init__.py index 8c48f79d2d..650365e4ec 100644 --- a/hls4ml/writer/__init__.py +++ b/hls4ml/writer/__init__.py @@ -1,3 +1,6 @@ +from hls4ml.writer.bambu_writer import BambuWriter + +from hls4ml.writer.bambu_accelerator_writer import BambuAcceleratorWriter # isort: skip from hls4ml.writer.catapult_writer import CatapultWriter from hls4ml.writer.libero_writer import LiberoWriter from hls4ml.writer.oneapi_writer import OneAPIWriter @@ -16,3 +19,5 @@ register_writer('Catapult', CatapultWriter) register_writer('Libero', LiberoWriter) register_writer('SymbolicExpression', SymbolicExpressionWriter) +register_writer('Bambu', BambuWriter) +register_writer('BambuAccelerator', BambuAcceleratorWriter) diff --git a/hls4ml/writer/bambu_accelerator_writer.py b/hls4ml/writer/bambu_accelerator_writer.py new file mode 100644 index 0000000000..0bbc9c4226 --- /dev/null +++ b/hls4ml/writer/bambu_accelerator_writer.py @@ -0,0 +1,438 @@ +import os +import stat +from pathlib import Path + +from hls4ml.writer.bambu_writer import BambuWriter + + +class BambuAcceleratorWriter(BambuWriter): + """Extends BambuWriter with an integer-container wrapper around the ap_fixed HLS core. + + Each input/output element is packed into the smallest standard unsigned integer + (8/16/32/64-bit) that fits the fixed-point width, keeping the AXI bus aligned + to 128 bits without introducing any IEEE 754 hardware. + + Supports both io_parallel (flat C-array interface) and io_stream (AXIS of + scalars, one unsigned-container beat per ap_fixed scalar, packed into + nnet::array structs to feed the core IP's hls::stream interface). + + The wrapper INLINES the layer pipeline directly — it does NOT call + `myproject()` from the core. Inlining is what lets the io_stream path + work: Bambu's DATAFLOW scheduler binds sub-function array-pointer + parameters to BRAMs that read zero at runtime, so all layer calls must + sit inside one DATAFLOW function. We use the same wrapper layout for + io_parallel for symmetry. + """ + + # The wrapper myproject_float owns the top-level interface. The inner + # myproject becomes a sub-function (its definition is still emitted but + # unused on the BambuAccelerator path) and must NOT emit its own + # `#pragma HLS interface` lines — Bambu's InterfaceInfer rejects two + # competing declarations on the same port. + _emit_core_interface_pragmas = False + + @staticmethod + def _container_width(precision_width): + """Smallest power-of-2 width (8/16/32/64) that fits precision_width bits.""" + if precision_width <= 8: + return 8 + if precision_width <= 16: + return 16 + if precision_width <= 32: + return 32 + return 64 + + @staticmethod + def _var_precision(v): + """Return (total_bits, frac_bits) for a model variable. + + Works for both io_parallel (NamedType) and io_stream (PackedType) + because PackedType inherits from NamedType and stores the scalar + ap_fixed precision in the same .type.precision attribute. + """ + try: + total = v.type.precision.width + integer = v.type.precision.integer + return total, total - integer + except AttributeError: + return 32, 0 + + def _max_container_width(self, variables): + """Return the container width needed for the widest variable in the list.""" + max_w = max((self._var_precision(v)[0] for v in variables), default=32) + return self._container_width(max_w) + + def _compute_io_sizes(self, model): + """Return total flattened input and output sizes (Python ints).""" + n_in = sum(v.size() for v in model.get_input_variables()) + n_out = sum(v.size() for v in model.get_output_variables()) + return n_in, n_out + + def _n_elem(self, v): + """Number of scalars packed per stream beat (= shape[-1] for io_stream, else 1).""" + try: + return v.type.n_elem + except AttributeError: + return 1 + + def write_float_header(self, model): + """Write firmware/_float.h""" + filedir = os.path.dirname(os.path.abspath(__file__)) + proj = model.config.get_project_name() + io_type = model.config.get_config_value('IOType') + + n_in, n_out = self._compute_io_sizes(model) + in_cw = self._max_container_width(model.get_input_variables()) + out_cw = self._max_container_width(model.get_output_variables()) + + tmpl = open(os.path.join(filedir, '../templates/bambu/firmware/myproject_float.h')) + fout = open(f'{model.config.get_output_dir()}/firmware/{proj}_float.h', 'w') + + for line in tmpl.readlines(): + if 'MYPROJECT' in line: + line = line.replace('MYPROJECT', proj.upper()) + if 'myproject' in line: + line = line.replace('myproject', proj) + + if '// hls-fpga-machine-learning insert float-includes' in line: + newline = line + if io_type == 'io_stream': + newline += '#include "hls_stream.h"\n' + + elif '// hls-fpga-machine-learning insert definitions' in line: + newline = line + newline += f'#define IN_CONTAINER_WIDTH {in_cw}\n' + newline += f'#define OUT_CONTAINER_WIDTH {out_cw}\n' + newline += 'typedef ac_int in_container_t;\n' + newline += 'typedef ac_int out_container_t;\n' + newline += f'static const unsigned N_IN = {n_in};\n' + newline += f'static const unsigned N_OUT = {n_out};\n' + + elif '// hls-fpga-machine-learning insert float-signature' in line: + newline = line + if io_type == 'io_stream': + newline += f'void {proj}_float(hls::stream &input_stream, hls::stream &output_stream);\n' # noqa: E501 + else: + newline += f'void {proj}_float(in_container_t input[N_IN], out_container_t output[N_OUT]);\n' + + else: + newline = line + + fout.write(newline) + + tmpl.close() + fout.close() + + # Wrapper-side emission helpers. The accelerator owns the + # container-packing layer and composes with the layer-pipeline helpers + # inherited from BambuWriter (`_emit_core_*`, `_emit_load_weights_block`, + # `_emit_internal_stream_decls`, `_emit_layer_calls`). + + def _emit_ingest_helper(self, inp, io_type): + """Static decode function: container -> core input variable.""" + total, _ = self._var_precision(inp) + if io_type == 'io_stream': + n_elem = self._n_elem(inp) + n_beats = inp.size() // n_elem + return ( + f'static void ingest_{inp.name}(' + f'hls::stream &input_stream, ' + f'hls::stream<{inp.type.name}> &{inp.name}) {{\n' + f' for (int i = 0; i < {n_beats}; i++) {{\n' + f' {inp.type.name} pack;\n' + f' #pragma clang loop unroll(full)\n' + f' for (int j = 0; j < {inp.type.name}::size; j++) {{\n' + f' in_container_t raw = input_stream.read();\n' + f' pack[j].set_slc(0, raw.slc<{total}>(0));\n' + f' }}\n' + f' {inp.name}.write(pack);\n' + f' }}\n' + f'}}\n\n' + ) + return ( + f'static void ingest_{inp.name}(' + f'in_container_t input[N_IN], ' + f'{inp.type.name} {inp.name}[{inp.size()}]) {{\n' + f' #pragma clang loop unroll(full)\n' + f' for (int i = 0; i < {inp.size()}; i++)\n' + f' {inp.name}[i].set_slc(0, input[i].slc<{total}>(0));\n' + f'}}\n\n' + ) + + def _emit_egress_helper(self, out_var, io_type): + """Static encode function: core output variable -> container.""" + total, _ = self._var_precision(out_var) + if io_type == 'io_stream': + n_elem = self._n_elem(out_var) + n_beats = out_var.size() // n_elem + return ( + f'static void egress_{out_var.name}(' + f'hls::stream<{out_var.type.name}> &{out_var.name}, ' + f'hls::stream &output_stream) {{\n' + f' for (int i = 0; i < {n_beats}; i++) {{\n' + f' {out_var.type.name} pack = {out_var.name}.read();\n' + f' #pragma clang loop unroll(full)\n' + f' for (int j = 0; j < {out_var.type.name}::size; j++) {{\n' + f' out_container_t raw = 0;\n' + f' raw.set_slc(0, pack[j].slc<{total}>(0));\n' + f' output_stream.write(raw);\n' + f' }}\n' + f' }}\n' + f'}}\n\n' + ) + return ( + f'static void egress_{out_var.name}(' + f'{out_var.type.name} {out_var.name}[{out_var.size()}], ' + f'out_container_t output[N_OUT]) {{\n' + f' #pragma clang loop unroll(full)\n' + f' for (int i = 0; i < {out_var.size()}; i++) {{\n' + f' output[i] = 0;\n' + f' output[i].set_slc(0, {out_var.name}[i].slc<{total}>(0));\n' + f' }}\n' + f'}}\n\n' + ) + + def _emit_ingest_helpers(self, model, io_type): + return ''.join(self._emit_ingest_helper(inp, io_type) for inp in model.get_input_variables()) + + def _emit_egress_helpers(self, model, io_type): + return ''.join(self._emit_egress_helper(o, io_type) for o in model.get_output_variables()) + + def _emit_ingest_calls(self, model, io_type, indent=' '): + port = 'input_stream' if io_type == 'io_stream' else 'input' + return ''.join(f'{indent}ingest_{inp.name}({port}, {inp.name});\n' for inp in model.get_input_variables()) + + def _emit_egress_calls(self, model, io_type, indent=' '): + port = 'output_stream' if io_type == 'io_stream' else 'output' + return ''.join(f'{indent}egress_{o.name}({o.name}, {port});\n' for o in model.get_output_variables()) + + def _emit_wrapper_interface_pragmas(self, io_type): + """File-scope AXIS pragmas for the wrapper top-level (io_stream only). + + io_parallel emits no interface pragma — Bambu's + `--generate-interface=INFER` derives it from the typed array + parameters in the wrapper signature. + """ + if io_type != 'io_stream': + return '' + return '#pragma HLS interface mode=axis port=input_stream\n#pragma HLS interface mode=axis port=output_stream\n' + + def _emit_wrapper_signature(self, proj, io_type): + if io_type == 'io_stream': + return ( + f'void {proj}_float(' + f'hls::stream &input_stream, ' + f'hls::stream &output_stream)\n' + ) + return f'void {proj}_float(in_container_t input[N_IN], out_container_t output[N_OUT])\n' + + def write_float_wrapper(self, model): + """Write firmware/_float.cpp — ingest/egress helpers wrapping + the inlined layer pipeline.""" + proj = model.config.get_project_name() + io_type = model.config.get_config_value('IOType') + indent = ' ' + + # Bambu DATAFLOW (active for io_stream) requires every local stream + # to be declared before any sub-function call; declarations first, + # then calls in pipeline order. The same layout works for io_parallel. + parts = [ + f'#include "{proj}_float.h"\n', + f'#include "{proj}.h"\n', + '#include "parameters.h"\n', + '\n', + self._emit_ingest_helpers(model, io_type), + self._emit_egress_helpers(model, io_type), + self._emit_wrapper_interface_pragmas(io_type), + self._emit_wrapper_signature(proj, io_type), + '{\n', + self._emit_core_io_pragma(model, indent=indent), + self._emit_core_input_declarations(model, indent=indent), + self._emit_core_output_declarations(model, indent=indent), + self._emit_internal_stream_decls(model, indent=indent), + self._emit_ingest_calls(model, io_type, indent=indent), + self._emit_load_weights_block(model, indent=indent), + # `_emit_load_weights_block` ends with `#endif` (no trailing + # newline — kept byte-faithful to BambuBackend's existing + # emission, where the next template line is blank and provides + # the separator). Composing strings here, we add the newline + # explicitly so the `nnet::dense(...)` call below isn't gobbled + # as garbage at end of #endif. + '\n' if model.config.get_writer_config()['WriteWeightsTxt'] else '', + self._emit_layer_calls(model, indent=indent), + self._emit_egress_calls(model, io_type, indent=indent), + '}\n', + ] + + with open(f'{model.config.get_output_dir()}/firmware/{proj}_float.cpp', 'w') as fout: + fout.write(''.join(parts)) + + def write_float_test_bench(self, model): + """Write _float_test.cpp""" + filedir = os.path.dirname(os.path.abspath(__file__)) + proj = model.config.get_project_name() + io_type = model.config.get_config_value('IOType') + + model_inputs = model.get_input_variables() + model_outputs = model.get_output_variables() + + tmpl = open(os.path.join(filedir, '../templates/bambu/myproject_float_test.cpp')) + fout = open(f'{model.config.get_output_dir()}/{proj}_float_test.cpp', 'w') + + for line in tmpl.readlines(): + indent = ' ' * (len(line) - len(line.lstrip(' '))) + + if 'myproject' in line and '// hls-fpga-machine-learning' not in line: + newline = line.replace('myproject', proj) + + elif '// hls-fpga-machine-learning insert float-data' in line: + newline = line + if io_type == 'io_stream': + newline += f'{indent}hls::stream input_stream("input_stream");\n' + in_offset = 0 + for inp in model_inputs: + total, frac = self._var_precision(inp) + scale = 1 << frac + max_val = (1 << (total - 1)) - 1 + min_val = -(1 << (total - 1)) + newline += f'{indent}for(int i = 0; i < {inp.size()}; i++) {{\n' + newline += f'{indent} long long s = (long long)floor((double)in[{in_offset} + i] * {scale}LL);\n' + newline += f'{indent} if (s > {max_val}LL) s = {max_val}LL;\n' + newline += f'{indent} if (s < {min_val}LL) s = {min_val}LL;\n' + newline += f'{indent} input_stream.write((in_container_t)(long long)s);\n' + newline += f'{indent}}}\n' + in_offset += inp.size() + newline += f'{indent}hls::stream output_stream("output_stream");\n' + else: + newline += f'{indent}in_container_t input[N_IN];\n' + in_offset = 0 + for inp in model_inputs: + total, frac = self._var_precision(inp) + scale = 1 << frac + max_val = (1 << (total - 1)) - 1 + min_val = -(1 << (total - 1)) + newline += f'{indent}for(int i = 0; i < {inp.size()}; i++) {{\n' + newline += f'{indent} long long s = (long long)floor((double)in[{in_offset} + i] * {scale}LL);\n' + newline += f'{indent} if (s > {max_val}LL) s = {max_val}LL;\n' + newline += f'{indent} if (s < {min_val}LL) s = {min_val}LL;\n' + newline += f'{indent} input[{in_offset} + i] = (in_container_t)(long long)s;\n' + newline += f'{indent}}}\n' + in_offset += inp.size() + newline += f'{indent}out_container_t output[N_OUT];\n' + + elif '// hls-fpga-machine-learning insert float-zero' in line: + newline = line + if io_type == 'io_stream': + newline += f'{indent}hls::stream input_stream("input_stream");\n' + newline += f'{indent}for(int i = 0; i < N_IN; i++) input_stream.write((in_container_t)0);\n' + newline += f'{indent}hls::stream output_stream("output_stream");\n' + else: + # `in_container_t input[N_IN] = {};` trips ac_int's explicit + # default constructor under clang; zero the array explicitly. + newline += f'{indent}in_container_t input[N_IN];\n' + newline += f'{indent}for(int i = 0; i < N_IN; i++) input[i] = (in_container_t)0;\n' + newline += f'{indent}out_container_t output[N_OUT];\n' + + elif '// hls-fpga-machine-learning insert float-top-level-function' in line: + newline = line + if io_type == 'io_stream': + newline += '#ifdef __BAMBU__\n' + newline += f'{indent}m_param_alloc(0, sizeof(input_stream));\n' + newline += f'{indent}m_param_alloc(1, sizeof(output_stream));\n' + newline += '#endif\n' + newline += f'{indent}{proj}_float(input_stream, output_stream);\n' + else: + newline += '#ifdef __BAMBU__\n' + newline += f'{indent}m_param_alloc(0, sizeof(input));\n' + newline += f'{indent}m_param_alloc(1, sizeof(output));\n' + newline += '#endif\n' + newline += f'{indent}{proj}_float(input, output);\n' + + elif '// hls-fpga-machine-learning insert float-tb-output' in line: + newline = line + out_offset = 0 + for out in model_outputs: + total, frac = self._var_precision(out) + scale = 1 << frac + if io_type == 'io_stream': + newline += f'{indent}for(int i = 0; i < {out.size()}; i++) {{\n' + newline += f'{indent} out_container_t raw_i = output_stream.read();\n' + newline += f'{indent} long long raw = ((long long)raw_i.to_uint64() << (64 - {total})) >> (64 - {total});\n' # noqa: E501 + newline += f'{indent} fout << (double)raw / {scale}.0 << " ";\n' + newline += f'{indent}}}\n' + else: + newline += f'{indent}for(int i = 0; i < {out.size()}; i++) {{\n' + newline += f'{indent} long long raw = ((long long)output[{out_offset} + i].to_uint64() << (64 - {total})) >> (64 - {total});\n' # noqa: E501 + newline += f'{indent} fout << (double)raw / {scale}.0 << " ";\n' + newline += f'{indent}}}\n' + out_offset += out.size() + newline += f'{indent}fout << "\\n";\n' + + elif '// hls-fpga-machine-learning insert float-output' in line: + newline = line + out_offset = 0 + for out in model_outputs: + total, frac = self._var_precision(out) + scale = 1 << frac + if io_type == 'io_stream': + newline += f'{indent}for(int i = 0; i < {out.size()}; i++) {{\n' + newline += f'{indent} out_container_t raw_i = output_stream.read();\n' + newline += f'{indent} long long raw = ((long long)raw_i.to_uint64() << (64 - {total})) >> (64 - {total});\n' # noqa: E501 + newline += f'{indent} std::cout << (double)raw / {scale}.0 << " ";\n' + # Write the value straight back so the immediately + # following '// hls-fpga-machine-learning insert + # float-tb-output' block (which reads output_stream + # again for the results file) doesn't drain an + # already-empty FIFO. Mirrors the `keep` idiom + # nnet::print_result already uses for the same + # read-stream-twice situation (nnet_helpers.h). + newline += f'{indent} output_stream.write(raw_i);\n' + newline += f'{indent}}}\n' + else: + newline += f'{indent}for(int i = 0; i < {out.size()}; i++) {{\n' + newline += f'{indent} long long raw = ((long long)output[{out_offset} + i].to_uint64() << (64 - {total})) >> (64 - {total});\n' # noqa: E501 + newline += f'{indent} std::cout << (double)raw / {scale}.0 << " ";\n' + newline += f'{indent}}}\n' + out_offset += out.size() + newline += f'{indent}std::cout << std::endl;\n' + + else: + newline = line + + fout.write(newline) + + tmpl.close() + fout.close() + + def write_float_build_scripts(self, model): + """Write build_tb_float_exe.sh and overwrite build_lib.sh with float-aware version.""" + filedir = Path(__file__).parent + + # build_tb_float_exe.sh + tb_float_src = (filedir / '../templates/bambu/build_tb_float_exe.sh').resolve() + tb_float_dst = Path(f'{model.config.get_output_dir()}/build_tb_float_exe.sh').resolve() + with open(tb_float_src) as src, open(tb_float_dst, 'w') as dst: + for line in src.readlines(): + line = line.replace('myproject', model.config.get_project_name()) + line = line.replace('mystamp', model.config.get_config_value('Stamp')) + dst.write(line) + tb_float_dst.chmod(tb_float_dst.stat().st_mode | stat.S_IEXEC) + + # Overwrite build_lib.sh with float-aware version that links both + # myproject.o and myproject_float.o into the bridge .so. + build_lib_float_src = (filedir / '../templates/bambu/build_lib_float.sh').resolve() + build_lib_dst = Path(f'{model.config.get_output_dir()}/build_lib.sh').resolve() + with open(build_lib_float_src) as src, open(build_lib_dst, 'w') as dst: + for line in src.readlines(): + line = line.replace('myproject', model.config.get_project_name()) + line = line.replace('mystamp', model.config.get_config_value('Stamp')) + dst.write(line) + build_lib_dst.chmod(build_lib_dst.stat().st_mode | stat.S_IEXEC) + + def write_hls(self, model, is_multigraph=False): + super().write_hls(model, is_multigraph=is_multigraph) + if not is_multigraph: + self.write_float_header(model) + self.write_float_wrapper(model) + self.write_float_test_bench(model) + self.write_float_build_scripts(model) diff --git a/hls4ml/writer/bambu_writer.py b/hls4ml/writer/bambu_writer.py new file mode 100644 index 0000000000..87345c22b3 --- /dev/null +++ b/hls4ml/writer/bambu_writer.py @@ -0,0 +1,1295 @@ +import glob +import logging +import os +import re +import stat +import tarfile +from collections import OrderedDict +from pathlib import Path +from shutil import copyfile, copytree, rmtree + +import numpy as np +import yaml + +from hls4ml.writer.writers import Writer + +config_filename = 'hls4ml_config.yml' + + +class BambuWriter(Writer): + _ARRAY_PARTITION_PRAGMA_RE = re.compile(r'^(\s*)(?://\s*)?(#pragma\s+HLS\s+array_partition\b.*)$', re.IGNORECASE) + + # Whether the top-level core function should emit its own + # `#pragma HLS interface` lines. A subclass that wraps this core inside + # a different top-level (and owns the AXI/AXIS interface there) sets + # this to False so InterfaceInfer doesn't see two competing + # declarations on the same port. + _emit_core_interface_pragmas = True + + @staticmethod + def _env_flag_enabled(name, default): + value = os.environ.get(name) + if value is None: + return default + return str(value).strip().lower() not in ('0', 'false', 'no', 'off') + + @classmethod + def _should_emit_array_partition_pragma(cls): + # Default to enabled to preserve behavior unless explicitly disabled. + return cls._env_flag_enabled('USE_BAMBU_ARRAY_PARTITION', True) + + def print_array_to_cpp(self, var, odir, namespace=None, write_txt_file=True): + """Write a weights array to C++ header files. + + Args: + var (WeightVariable): Weight to write + odir (str): Output directory + namespace (str, optional): Writes a namespace for the weights to avoid clashes with global variables. + write_txt_file (bool, optional): Write txt files in addition to .h files. Defaults to True. + """ + + h_file = open(f'{odir}/firmware/weights/{var.name}.h', 'w') + if write_txt_file: + txt_file = open(f'{odir}/firmware/weights/{var.name}.txt', 'w') + + # meta data + h_file.write(f'//Numpy array shape {var.shape}\n') + h_file.write(f'//Min {np.min(var.min):.12f}\n') + h_file.write(f'//Max {np.max(var.max):.12f}\n') + h_file.write(f'//Number of zeros {var.nzeros}\n') + h_file.write('\n') + + h_file.write(f'#ifndef {var.name.upper()}_H_\n') + h_file.write(f'#define {var.name.upper()}_H_\n') + h_file.write('\n') + + if namespace is not None: + h_file.write(f'namespace {namespace} {{\n\n') + + if write_txt_file: + h_file.write('#ifndef __SYNTHESIS__\n') + # `static` (internal linkage) so each translation unit that + # includes this weight header gets its own private copy. With + # the BambuAccelerator wrapper inlining the layer pipeline, + # both myproject.cpp and myproject_float.cpp include + # parameters.h; without `static` they collide at link time + # ("multiple definition of `w2'"). + h_file.write('static ' + var.definition_cpp() + ';\n') + h_file.write('#else\n') + h_file.write('static const ' + var.definition_cpp() + ' = {') + else: + h_file.write(var.definition_cpp() + ' = {') + + # fill c++ array. + # not including internal brackets for multidimensional case + sep = '' + for x in var: + h_file.write(sep + x) + if write_txt_file: + txt_file.write(sep + x) + sep = ', ' + h_file.write('};\n\n') + + if write_txt_file: + h_file.write('#endif\n') + txt_file.close() + + if namespace is not None: + h_file.write('}\n\n') + + h_file.write('\n#endif\n') + h_file.close() + + def write_project_dir(self, model): + """Write the base project directory + + Args: + model (ModelGraph): the hls4ml model. + """ + if not os.path.isdir(f'{model.config.get_output_dir()}/firmware/weights'): + os.makedirs(f'{model.config.get_output_dir()}/firmware/weights') + + @staticmethod + def _make_array_pragma(variable): + """ + Layers in hls_model.py can specify output array partitioning through the `pragma` attribute. + If `pragma` is a string: options are 'partition', 'reshape', or 'stream'. + If `pragma` is a tuple: (mode, type, factor) where mode is 'partition' or 'reshape', type is + 'complete', 'cyclic', or 'block', and factor is an integer only used when the type is not 'complete'. + + Bambu does not support ARRAY_RESHAPE, so reshape requests are emitted + as ARRAY_PARTITION instead. ARRAY_PARTITION is emitted live for both + internal arrays and top-level arguments to match the requested pragma + policy, even if some Bambu versions still fail on top-level partitions. + """ + + config = variable.pragma + if type(config) is tuple: + mode = config[0] + if mode in ['partition', 'reshape']: + typ = config[1] + if typ != 'complete': + factor = config[2] + elif mode == 'stream': + depth = config[1] + else: + mode = config + typ = 'complete' + factor = 0 + + if mode in ['partition', 'reshape']: + mode = 'partition' + if typ == 'complete': + template = '#pragma HLS ARRAY_{mode} variable={name} {type} dim={dim}' + else: + template = '#pragma HLS ARRAY_{mode} variable={name} {type} factor={factor} dim={dim}' + + return template.format( + mode=mode.upper(), + name=variable.name, + type=typ, + factor=factor, + dim=0, + ) + + elif mode == 'stream': + # STREAM pragmas stay commented while the io_stream path is blocked + # upstream by Bambu's InterfaceInfer pass (ac_channel<>::fifo::_read + # not supported). + return f'//#pragma HLS STREAM variable={variable.name} depth={depth}' + + # weights/biases are `static const` (ROMs) since the const-propagation + # change; Bambu's `__bambu_csroa_partition__(void*, ...)` intrinsic has no + # const overload, so an active ARRAY_PARTITION on them is a hard compile + # error. Keep those commented regardless of USE_BAMBU_ARRAY_PARTITION. + _CONST_ARRAY_VARIABLES_RE = re.compile(r'variable\s*=\s*(weights|biases)\b', re.IGNORECASE) + + @classmethod + def _rewrite_array_partition_pragmas(cls, header_path): + enable_array_partition = cls._should_emit_array_partition_pragma() + + rewritten = [] + with open(header_path) as header: + for line in header: + match = cls._ARRAY_PARTITION_PRAGMA_RE.match(line) + if match: + indent, pragma = match.groups() + if enable_array_partition and not cls._CONST_ARRAY_VARIABLES_RE.search(pragma): + rewritten.append(f'{indent}{pragma}\n') + else: + rewritten.append(f'{indent}//{pragma}\n') + else: + rewritten.append(line) + + with open(header_path, 'w') as header: + header.writelines(rewritten) + + # Helpers reused by `write_project_cpp` and (in a forthcoming subclass) + # by a wrapper writer that emits a different top-level function around + # this core. The first three are consumed below; the last three + # (`_emit_core_*`) are forward-looking infrastructure for the wrapper. + + def _emit_load_weights_block(self, model, indent=' '): + """Emit the `#ifndef __BAMBU__` weight-loading block; '' if disabled.""" + if not model.config.get_writer_config()['WriteWeightsTxt']: + return '' + out = '#ifndef __BAMBU__\n' + out += f'{indent}static bool loaded_weights = false;\n' + out += f'{indent}if (!loaded_weights) {{\n' + for layer in model.get_layers(): + for w in layer.get_weights(): + if w.weight_class == 'CompressedWeightVariable': + out += indent + ' nnet::load_compressed_weights_from_txt<{}, {}>({}, "{}.txt");\n'.format( + w.type.name, w.nonzeros, w.name, w.name + ) + elif w.weight_class == 'ExponentWeightVariable': + out += indent + ' nnet::load_exponent_weights_from_txt<{}, {}>({}, "{}.txt");\n'.format( + w.type.name, w.data_length, w.name, w.name + ) + else: + out += indent + ' nnet::load_weights_from_txt<{}, {}>({}, "{}.txt");\n'.format( + w.type.name, w.data_length, w.name, w.name + ) + out += ' loaded_weights = true;' + out += ' }\n' + out += '#endif' + return out + + def _emit_internal_stream_decls(self, model, indent=' '): + """Emit declarations for every non-input/output per-layer variable. + + Bambu's DATAFLOW requires every local stream to be declared before + any sub-function call in the top-function body, so they live in the + same block as the layer calls. + """ + model_inputs = model.get_input_variables() + model_outputs = model.get_output_variables() + out = '' + for layer in model.get_layers(): + for var in layer.get_variables(): + if var in model_inputs or var in model_outputs: + continue + def_cpp = var.definition_cpp() + if def_cpp is None: + continue + out += f'{indent}{def_cpp};\n' + if var.pragma: + if self._should_emit_array_partition_pragma(): + out += f'{indent}{self._make_array_pragma(var)}\n' + out += '\n' + return out + + def _emit_layer_calls(self, model, indent=' '): + """Emit the per-layer `function_cpp` calls.""" + out = '' + for layer in model.get_layers(): + func = layer.get_attr('function_cpp', None) + if not func: + continue + if not isinstance(func, (list, set)): + func = [func] + if len(func) == 1: + out += f'{indent}{func[0]} // {layer.name}\n' + else: + out += f'{indent}// {layer.name}\n' + for entry in func: + out += f'{indent}{entry}\n' + if model.config.trace_output and layer.get_attr('trace', False): + out += '#ifndef __SYNTHESIS__\n' + for var in layer.get_variables(): + out += '{}nnet::save_layer_output<{}>({}, "{}", {});\n'.format( + indent, var.type.name, var.name, layer.name, var.size_cpp() + ) + out += '#endif\n' + out += '\n' + return out + + def _emit_core_io_pragma(self, model, indent=' '): + """Return `#pragma HLS DATAFLOW` for io_stream, empty otherwise. + + Used by a wrapper top-level that calls into this core: the wrapper's + DATAFLOW pragma is what makes the per-layer streams flow concurrently. + """ + io_type = model.config.get_config_value('IOType') + if io_type != 'io_stream': + return '' + return f'{indent}#pragma HLS DATAFLOW\n' + + def _emit_core_input_declarations(self, model, indent=' '): + """Emit local declarations for each model input variable.""" + out = '' + for inp in model.get_input_variables(): + out += f'{indent}{inp.definition_cpp()};\n' + return out + + def _emit_core_output_declarations(self, model, indent=' '): + """Emit local declarations for each model output variable.""" + out = '' + for o in model.get_output_variables(): + out += f'{indent}{o.definition_cpp()};\n' + return out + + def write_project_cpp(self, model): + """Write the main architecture source file (myproject.cpp) + + Args: + model (ModelGraph): the hls4ml model. + """ + + filedir = os.path.dirname(os.path.abspath(__file__)) + + f = open(os.path.join(filedir, '../templates/bambu/firmware/myproject.cpp')) + fout = open(f'{model.config.get_output_dir()}/firmware/{model.config.get_project_name()}.cpp', 'w') + + model_inputs = model.get_input_variables() + model_outputs = model.get_output_variables() + model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram'] + + indent = ' ' + + for line in f.readlines(): + # Add headers to weights and biases + if 'myproject' in line: + newline = line.replace('myproject', model.config.get_project_name()) + + elif '// hls-fpga-machine-learning insert header' in line: + inputs_str = ', '.join([i.definition_cpp(as_reference=True) for i in model_inputs]) + outputs_str = ', '.join([o.definition_cpp(as_reference=True) for o in model_outputs]) + brams_str = ', \n'.join([indent + b.definition_cpp(as_reference=False) for b in model_brams]) + + newline = '' + newline += indent + inputs_str + ',\n' + newline += indent + outputs_str + if len(model_brams) > 0: + newline += ',\n' + brams_str + newline += '\n' + + elif '// hls-fpga-machine-learning insert namespace-start' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += f'namespace {namespace} {{\n' + + elif '// hls-fpga-machine-learning insert namespace-end' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += '}\n' + + elif '// hls-fpga-machine-learning insert load weights' in line: + newline = line + self._emit_load_weights_block(model) + + # Add input/output type + elif '// hls-fpga-machine-learning insert IO' in line: + newline = line + all_brams = [b.name for b in model_brams] + io_type = model.config.get_config_value('IOType') + + pipeline_style = model.config.pipeline_style + pipeline_ii = model.config.pipeline_ii + # PIPELINE stays commented out: Bambu's default II=1 isn't + # achievable for the layer pipeline (`Function pipelining + # not possible with II=1`, observed minII=2 maxII=4). + # DATAFLOW (io_stream) IS activated — io_stream layers need + # it to flow concurrently as separate tasks. + pragma_prefix = '//' if pipeline_style == 'pipeline' else '' + pipeline_pragma = indent + f'{pragma_prefix}#pragma HLS {pipeline_style.upper()}' + if pipeline_style == 'pipeline' and pipeline_ii is not None: + pipeline_pragma += f' II={pipeline_ii}\n' + else: + pipeline_pragma += '\n' + + # Per-port `#pragma HLS interface` directives. Two changes + # vs. the old comma-list form: + # - io_parallel emits NOTHING. Current Bambu rejects + # `mode=valid` as "Invalid HLS interface mode"; the + # valid-handshake interface is derived by + # `--generate-interface=INFER` from the typed array + # parameters in the function signature. + # - io_stream emits one `mode=axis` line per port (Bambu + # requires explicit AXIS pragmas; the comma-list form + # is rejected by current InterfaceInfer). + # `_emit_core_interface_pragmas = False` suppresses the + # io_stream emission for a subclass that wraps this core + # in a different top-level and owns the interface there. + interface_pragmas = '' + if self._emit_core_interface_pragmas and io_type == 'io_stream': + for port in [i.name for i in model_inputs] + [o.name for o in model_outputs]: + interface_pragmas += f'{indent}#pragma HLS interface mode=axis port={port}\n' + + if io_type == 'io_parallel': + for i in model_inputs: + if self._should_emit_array_partition_pragma(): + newline += indent + self._make_array_pragma(i) + '\n' + for o in model_outputs: + if self._should_emit_array_partition_pragma(): + newline += indent + self._make_array_pragma(o) + '\n' + newline += pipeline_pragma + + if io_type == 'io_stream': + newline += interface_pragmas + if all_brams: + newline += indent + '//#pragma HLS INTERFACE bram port={} \n'.format(','.join(all_brams)) + newline += pipeline_pragma + + elif '// hls-fpga-machine-learning insert layers' in line: + newline = line + '\n' + newline += self._emit_internal_stream_decls(model) + newline += self._emit_layer_calls(model) + + # Just copy line + else: + newline = line + + fout.write(newline) + + f.close() + fout.close() + + def write_project_header(self, model): + """Write the main architecture header file (myproject.h) + + Args: + model (ModelGraph): the hls4ml model. + """ + + filedir = os.path.dirname(os.path.abspath(__file__)) + f = open(os.path.join(filedir, '../templates/bambu/firmware/myproject.h')) + fout = open(f'{model.config.get_output_dir()}/firmware/{model.config.get_project_name()}.h', 'w') + + model_inputs = model.get_input_variables() + model_outputs = model.get_output_variables() + model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram'] + + indent = ' ' + + for line in f.readlines(): + if 'MYPROJECT' in line: + newline = line.replace('MYPROJECT', format(model.config.get_project_name().upper())) + + elif 'myproject' in line: + newline = line.replace('myproject', model.config.get_project_name()) + + elif '// hls-fpga-machine-learning insert header' in line: + inputs_str = ', '.join([i.definition_cpp(as_reference=True) for i in model_inputs]) + outputs_str = ', '.join([o.definition_cpp(as_reference=True) for o in model_outputs]) + brams_str = ', \n'.join([indent + b.definition_cpp(as_reference=False) for b in model_brams]) + + newline = '' + newline += indent + inputs_str + ',\n' + newline += indent + outputs_str + if len(model_brams) > 0: + newline += ',\n' + brams_str + newline += '\n' + + elif '// hls-fpga-machine-learning insert namespace-start' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += f'namespace {namespace} {{\n' + + elif '// hls-fpga-machine-learning insert namespace-end' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += '}\n' + + elif '// hls-fpga-machine-learning insert emulator-defines' in line: + newline = line + + if model.config.get_writer_config().get('WriteEmulationConstants', False): + brams_def_str = ', '.join([b.definition_cpp(as_reference=False) for b in model_brams]) + brams_call_str = ', '.join([b.name for b in model_brams]) + + if model.config.get_config_value('IOType') == 'io_stream': + input_call_str = ', '.join([f'std::get<{n}>(inputs)' for n in range(len(model_inputs))]) + output_call_str = ', '.join([f'std::get<{n}>(outputs)' for n in range(len(model_outputs))]) + else: + input_call_str = ', '.join([f'std::get<{n}>(inputs).data()' for n in range(len(model_inputs))]) + output_call_str = ', '.join([f'std::get<{n}>(outputs).data()' for n in range(len(model_outputs))]) + + newline += ( + f'\ninline void {model.config.get_project_name()}_emulator(' + 'inputs_t& inputs, outputs_t& outputs' # the inputs_t should ideally be const + ) + if len(model_brams) > 0: + newline += ',\n' + brams_def_str + newline += ') {\n' + newline += indent + model.config.get_project_name() + '(\n' + newline += indent + indent + input_call_str + ',\n' + newline += indent + indent + output_call_str + if len(model_brams) > 0: + newline += ',\n' + indent + indent + brams_call_str + newline += '\n' + indent + ');\n}\n' + + else: + newline = line + fout.write(newline) + + f.close() + fout.close() + + def write_defines(self, model): + """Write the C++ type definitions file (defines.h) + + Args: + model (ModelGraph): the hls4ml model. + """ + filedir = os.path.dirname(os.path.abspath(__file__)) + f = open(os.path.join(filedir, '../templates/bambu/firmware/defines.h')) + fout = open(f'{model.config.get_output_dir()}/firmware/defines.h', 'w') + + for line in f.readlines(): + if '// hls-fpga-machine-learning insert headers' in line: + uses_stdfloat = False + uses_apfloat = False + for layer in model.get_layers(): + layer_precision = layer.get_layer_precision() + for type_var in layer_precision.values(): + cpp = type_var.definition_cpp() + if 'std::' in cpp: + uses_stdfloat = True + break + if 'ap_float' in cpp: + uses_apfloat = True + break + if uses_stdfloat: + newline = line + '#include \n' + if uses_apfloat: + newline = line + '#include "ap_float.h"\n' + + elif '// hls-fpga-machine-learning insert layer-precision' in line: + newline = line + all_precision = OrderedDict() + for layer in model.get_layers(): + layer_precision = layer.get_layer_precision() + for type_name, type_var in layer_precision.items(): + # Ensure that layer's types doesn't override existing types + # This can happen in case of InplaceVariable types + if type_name not in all_precision: + all_precision[type_name] = type_var + for used_type in all_precision.values(): + newline += used_type.definition_cpp() + + elif '// hls-fpga-machine-learning insert namespace-start' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += f'namespace {namespace} {{\n' + + elif '// hls-fpga-machine-learning insert namespace-end' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += '}\n' + + elif '// hls-fpga-machine-learning insert emulator-defines' in line: + newline = line + + if model.config.get_writer_config().get('WriteEmulationConstants', False): + if model.config.get_config_value('IOType') == 'io_stream': + input_types = [f'hls::stream<{v.type.name}>' for v in model.get_input_variables()] + output_types = [f'hls::stream<{v.type.name}>' for v in model.get_output_variables()] + else: + input_types = [f'std::array<{v.type.name}, {v.size_cpp()}>' for v in model.get_input_variables()] + output_types = [f'std::array<{v.type.name}, {v.size_cpp()}>' for v in model.get_output_variables()] + input_types_str = ', '.join(input_types) + output_types_str = ', '.join(output_types) + newline += '\n' + f'using inputs_t = std::tuple<{input_types_str}>;' + newline += '\n' + f'using outputs_t = std::tuple<{output_types_str}>;\n' + else: + newline = line + fout.write(newline) + f.close() + fout.close() + + def write_parameters(self, model): + """Write the C++ layer config file (parameters.h) + + Args: + model (ModelGraph): the hls4ml model. + """ + filedir = os.path.dirname(os.path.abspath(__file__)) + f = open(os.path.join(filedir, '../templates/bambu/firmware/parameters.h')) + fout = open(f'{model.config.get_output_dir()}/firmware/parameters.h', 'w') + + for line in f.readlines(): + if '// hls-fpga-machine-learning insert includes' in line: + newline = line + for include in sorted(set(sum((layer.get_attr('include_header', []) for layer in model.get_layers()), []))): + newline += '#include "%s"\n' % include + + elif '// hls-fpga-machine-learning insert weights' in line: + newline = line + for layer in model.get_layers(): + for w in layer.get_weights(): + if w.storage.lower() != 'bram': + newline += f'#include "weights/{w.name}.h"\n' + + elif '// hls-fpga-machine-learning insert layer-config' in line: + newline = line + for layer in model.get_layers(): + config = layer.get_attr('config_cpp', None) + if config: + newline += '// ' + layer.name + '\n' + newline += config + '\n' + + elif '// hls-fpga-machine-learning insert namespace-start' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += f'namespace {namespace} {{\n' + + elif '// hls-fpga-machine-learning insert namespace-end' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += '}\n' + + else: + newline = line + fout.write(newline) + f.close() + fout.close() + + def write_weights(self, model): + """Write the weights into header files + + Args: + model (ModelGraph): the hls4ml model. + """ + namespace = model.config.get_writer_config().get('Namespace', None) + write_txt = model.config.get_writer_config().get('WriteWeightsTxt', True) + for layer in model.get_layers(): + for weights in layer.get_weights(): + self.print_array_to_cpp( + weights, model.config.get_output_dir(), namespace=namespace, write_txt_file=write_txt + ) + + def write_multigraph_weights(self, model): + """Write the weights into header files + + Args: + model (MultiModelGraph): the hls4ml multigraph model. + """ + namespace = model.config.get_writer_config().get('Namespace', None) + write_txt = model.config.get_writer_config().get('WriteWeightsTxt', True) + for g in model.graphs: + for layer in g.get_layers(): + for weights in layer.get_weights(): + self.print_array_to_cpp( + weights, model.config.get_output_dir(), namespace=namespace, write_txt_file=write_txt + ) + + def __make_dat_file(self, original_path, project_path): + """ + Convert other input/output data types into a dat file, which is + a text file with the falttened matrix printed out. Note that ' ' is + assumed to be the delimiter. + """ + + # Take in data from current supported data files + if original_path[-3:] == 'npy': + data = np.load(original_path) + else: + raise Exception('Unsupported input/output data files.') + + # Faltten data, just keep first dimension + data = data.reshape(data.shape[0], -1) + + def print_data(f): + for i in range(data.shape[0]): + for j in range(data.shape[1]): + f.write(str(data[i][j]) + ' ') + f.write('\n') + + # Print out in dat file + with open(project_path, 'w') as f: + print_data(f) + + def write_test_bench(self, model): + """Write the testbench files (myproject_test.cpp and input/output .dat files) + + Args: + model (ModelGraph): the hls4ml model. + """ + + filedir = os.path.dirname(os.path.abspath(__file__)) + + if not os.path.exists(f'{model.config.get_output_dir()}/tb_data/'): + os.mkdir(f'{model.config.get_output_dir()}/tb_data/') + + input_data = model.config.get_config_value('InputData') + output_predictions = model.config.get_config_value('OutputPredictions') + + if input_data: + if input_data[-3:] == 'dat': + copyfile(input_data, f'{model.config.get_output_dir()}/tb_data/tb_input_features.dat') + else: + self.__make_dat_file(input_data, f'{model.config.get_output_dir()}/tb_data/tb_input_features.dat') + + if output_predictions: + if output_predictions[-3:] == 'dat': + copyfile(output_predictions, f'{model.config.get_output_dir()}/tb_data/tb_output_predictions.dat') + else: + self.__make_dat_file( + output_predictions, f'{model.config.get_output_dir()}/tb_data/tb_output_predictions.dat' + ) + + f = open(os.path.join(filedir, '../templates/bambu/myproject_test.cpp')) + fout = open(f'{model.config.get_output_dir()}/{model.config.get_project_name()}_test.cpp', 'w') + + model_inputs = model.get_input_variables() + model_outputs = model.get_output_variables() + model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram'] + + for line in f.readlines(): + indent = ' ' * (len(line) - len(line.lstrip(' '))) + + # Insert numbers + if 'myproject' in line: + newline = line.replace('myproject', model.config.get_project_name()) + + elif '// hls-fpga-machine-learning insert bram' in line: + newline = line + for bram in model_brams: + newline += f'#include "firmware/weights/{bram.name}.h"\n' + + elif '// hls-fpga-machine-learning insert data' in line: + newline = line + offset = 0 + for inp in model_inputs: + newline += ' ' + inp.definition_cpp() + ';\n' + newline += ' nnet::copy_data(in, {});\n'.format( + inp.type.name, offset, inp.size_cpp(), inp.name + ) + offset += inp.size() + for out in model_outputs: + newline += ' ' + out.definition_cpp() + ';\n' + + elif '// hls-fpga-machine-learning insert zero' in line: + newline = line + for inp in model_inputs: + newline += indent + inp.definition_cpp() + ';\n' + newline += indent + f'nnet::fill_zero<{inp.type.name}, {inp.size_cpp()}>({inp.name});\n' + for out in model_outputs: + newline += indent + out.definition_cpp() + ';\n' + + elif '// hls-fpga-machine-learning insert top-level-function' in line: + newline = line + + input_vars = ','.join([i.name for i in model_inputs]) + output_vars = ','.join([o.name for o in model_outputs]) + bram_vars = ','.join([b.name for b in model_brams]) + + # Concatenate the input, output, and bram variables. Filter out empty/null values + all_vars = ','.join(filter(None, [input_vars, output_vars, bram_vars])) + + top_level = '#ifdef __BAMBU__\n' + for i, v in enumerate(model_inputs): + top_level += indent + f'm_param_alloc({i}, sizeof({v.name}));\n' + for i, v in enumerate(model_outputs): + top_level += indent + f'm_param_alloc({i + len(model_inputs)}, sizeof({v.name}));\n' + # not sure if this is needed + # for i,v in enumerate(bram_vars): + # top_level += f'm_param_alloc({i}, sizeof({v}));\n' + top_level += '#endif\n' + top_level += indent + f'{model.config.get_project_name()}({all_vars});\n' + + newline += top_level + + elif '// hls-fpga-machine-learning insert predictions' in line: + newline = line + for out in model_outputs: + newline += indent + f'for(int i = 0; i < {out.size_cpp()}; i++) {{\n' + newline += indent + ' std::cout << pr[i] << " ";\n' + newline += indent + '}\n' + newline += indent + 'std::cout << std::endl;\n' + + elif '// hls-fpga-machine-learning insert tb-output' in line: + newline = line + tb_stream = model.config.get_writer_config().get('TBOutputStream', 'both') + if tb_stream != 'stdout': + for out in model_outputs: + newline += indent + 'nnet::print_result<{}, {}>({}, fout);\n'.format( + out.type.name, out.size_cpp(), out.name + ) # TODO enable this + + elif ( + '// hls-fpga-machine-learning insert output' in line + or '// hls-fpga-machine-learning insert quantized' in line + ): + newline = line + tb_stream = model.config.get_writer_config().get('TBOutputStream', 'both') + keep_output = str(tb_stream != 'stdout').lower() # We keep output if we need to write it to file too. + if tb_stream != 'file': + for out in model_outputs: + newline += indent + 'nnet::print_result<{}, {}>({}, std::cout, {});\n'.format( + out.type.name, out.size_cpp(), out.name, keep_output + ) + + elif '// hls-fpga-machine-learning insert namespace' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += indent + f'using namespace {namespace};\n' + + else: + newline = line + fout.write(newline) + f.close() + fout.close() + + def write_bridge(self, model): + """Write the Python-C++ bridge (myproject_bridge.cpp) + + Args: + model (ModelGraph): the hls4ml model. + """ + + filedir = os.path.dirname(os.path.abspath(__file__)) + f = open(os.path.join(filedir, '../templates/bambu/myproject_bridge.cpp')) + fout = open(f'{model.config.get_output_dir()}/{model.config.get_project_name()}_bridge.cpp', 'w') + + model_inputs = model.get_input_variables() + model_outputs = model.get_output_variables() + model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram'] + + indent = ' ' + + for line in f.readlines(): + if 'MYPROJECT' in line: + newline = line.replace('MYPROJECT', format(model.config.get_project_name().upper())) + + elif 'myproject' in line: + newline = line.replace('myproject', format(model.config.get_project_name())) + + elif '// hls-fpga-machine-learning insert bram' in line: + newline = line + for bram in model_brams: + newline += f'#include "firmware/weights/{bram.name}.h"\n' + + elif '// hls-fpga-machine-learning insert header' in line: + dtype = line.split('#', 1)[1].strip() + inputs_str = ', '.join([f'{dtype} *{i.name}' for i in model_inputs]) + outputs_str = ', '.join([f'{dtype} *{o.name}' for o in model_outputs]) + + newline = '' + newline += indent + inputs_str + ',\n' + newline += indent + outputs_str + '\n' + + elif '// hls-fpga-machine-learning insert wrapper' in line: + dtype = line.split('#', 1)[1].strip() + newline = '' + for i in model_inputs: + newline += indent + '{var};\n'.format(var=i.definition_cpp(name_suffix='_ap')) + newline += indent + 'nnet::convert_data<{}, {}, {}>({}, {}_ap);\n'.format( + dtype, i.type.name, i.size_cpp(), i.name, i.name + ) + newline += '\n' + + for o in model_outputs: + newline += indent + '{var};\n'.format(var=o.definition_cpp(name_suffix='_ap')) + + newline += '\n' + + input_vars = ','.join([i.name + '_ap' for i in model_inputs]) + bram_vars = ','.join([b.name for b in model_brams]) + output_vars = ','.join([o.name + '_ap' for o in model_outputs]) + + # Concatenate the input, output, and bram variables. Filter out empty/null values + all_vars = ','.join(filter(None, [input_vars, output_vars, bram_vars])) + + top_level = indent + f'{model.config.get_project_name()}({all_vars});\n' + newline += top_level + + newline += '\n' + + for o in model_outputs: + newline += indent + 'nnet::convert_data<{}, {}, {}>({}_ap, {});\n'.format( + o.type.name, dtype, o.size_cpp(), o.name, o.name + ) + + elif '// hls-fpga-machine-learning insert trace_outputs' in line: + newline = '' + for layer in model.get_layers(): + func = layer.get_attr('function_cpp', None) + if func and model.config.trace_output and layer.get_attr('trace', False): + vars = layer.get_variables() + for var in vars: + newline += ( + indent + + 'nnet::trace_outputs->insert(std::pair(' + + f'"{layer.name}", (void *) malloc({var.size_cpp()} * element_size)));\n' + ) + + elif '// hls-fpga-machine-learning insert namespace' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += indent + f'using namespace {namespace};\n' + + else: + newline = line + fout.write(newline) + + f.close() + fout.close() + + def write_bridge_multigraph(self, model): + """Write the Python-C++ bridge (myproject_stitched_bridge.cpp) + Args: + model (MultiModelGraph): the hls4ml multigraph model. + """ + + filedir = os.path.dirname(os.path.abspath(__file__)) + f = open(os.path.join(filedir, '../templates/bambu/myproject_bridge.cpp')) + fout = open(f'{model.config.get_output_dir()}/{model.config.get_project_name()}_bridge.cpp', 'w') + model_inputs = model.graphs[0].get_input_variables() + model_outputs = model.graphs[-1].get_output_variables() + model_brams = [var for var in model.graphs[0].get_weight_variables() if var.storage.lower() == 'bram'] + + indent = ' ' + + for line in f.readlines(): + newline = '' + if 'MYPROJECT' in line: + newline = line.replace('MYPROJECT', format(model.config.get_project_name().upper())) + elif 'firmware/myproject' in line: + for graph_idx, g in enumerate(model.graphs): + newline += '#undef DEFINES_H_\n' + if len(g.outputs) == 1: + newline += '#define result_t ' + 'result_graph' + str(graph_idx + 1) + '_t\n' + newline += line.replace('myproject', format(model.graphs[graph_idx].config.get_project_name())) + if len(g.outputs) == 1: + newline += ( + 'typedef result_graph' + str(graph_idx + 1) + '_t graph' + str(graph_idx + 1) + '_result_t;\n' + ) + newline += '#undef result_t\n\n' if graph_idx < len(model.graphs) - 1 else '\n' + newline += '\n' + elif 'myproject' in line: + newline = line.replace('myproject', format(model.config.get_project_name())) + + elif '// hls-fpga-machine-learning insert bram' in line: + newline = line + for bram in model_brams: + newline += f'#include "firmware/weights/{bram.name}.h"\n' + + elif '// hls-fpga-machine-learning insert header' in line: + dtype = line.split('#', 1)[1].strip() + inputs_str = ', '.join([f'{dtype} {i.name}[{i.size_cpp()}]' for i in model_inputs]) + outputs_str = ', '.join([f'{dtype} {o.name}[{o.size_cpp()}]' for o in model_outputs]) + + newline = '' + newline += indent + inputs_str + ',\n' + newline += indent + outputs_str + '\n' + + elif '// hls-fpga-machine-learning insert wrapper' in line: + dtype = line.split('#', 1)[1].strip() + newline = '' + for i in model_inputs: + newline += indent + '{var};\n'.format(var=i.definition_cpp(name_suffix='_ap')) + newline += indent + 'nnet::convert_data<{}, {}, {}>({}, {}_ap);\n'.format( + dtype, i.type.name, i.size_cpp(), i.name, i.name + ) + newline += '\n' + + for idx, g in enumerate(model.graphs): + for o in g.get_output_variables(): + definition = o.definition_cpp(name_suffix='_ap') + if len(g.outputs) == 1: + parts = definition.split(' ', 1) + datatype = 'graph' + str(idx + 1) + '_result_t' + if parts[0].startswith('hls::stream'): + modified_definition = 'hls::stream<' + datatype + '> ' + parts[1] + else: + modified_definition = datatype + ' ' + parts[1] + newline += indent + f'{modified_definition};\n' + else: + newline += indent + f'{definition};\n' + + newline += '\n' + + top_level = '' + output_vars = '' + for idx, g in enumerate(model.graphs): + if idx == 0: + input_vars = ','.join([i.name + '_ap' for i in g.get_input_variables()]) + else: + input_vars = output_vars + bram_vars = ','.join( + [b.name for b in [var for var in g.get_weight_variables() if var.storage.lower() == 'bram']] + ) + output_vars = ','.join([o.name + '_ap' for o in g.get_output_variables()]) + # Concatenate the input, output, and bram variables. Filter out empty/null values + all_vars = ','.join(filter(None, [input_vars, output_vars, bram_vars])) + top_level += indent + f'{g.config.get_project_name()}({all_vars});\n' + newline += top_level + + newline += '\n' + + for o in model_outputs: + if len(model.graphs[-1].outputs) == 1: + newline += indent + 'nnet::convert_data<{}, {}, {}>({}_ap, {});\n'.format( + datatype, dtype, o.size_cpp(), o.name, o.name + ) + else: + newline += indent + 'nnet::convert_data<{}, {}, {}>({}_ap, {});\n'.format( + o.type.name, dtype, o.size_cpp(), o.name, o.name + ) + + elif '// hls-fpga-machine-learning insert trace_outputs' in line: + newline = '' + for layer in model.get_layers(): + func = layer.get_attr('function_cpp', None) + if func and model.config.trace_output and layer.get_attr('trace', False): + vars = layer.get_variables() + for var in vars: + newline += ( + indent + + 'nnet::trace_outputs->insert(std::pair(' + + f'"{layer.name}", (void *) malloc({var.size_cpp()} * element_size)));\n' + ) + + elif '// hls-fpga-machine-learning insert namespace' in line: + newline = '' + + namespace = model.config.get_writer_config().get('Namespace', None) + if namespace is not None: + newline += indent + f'using namespace {namespace};\n' + + elif '// hls-fpga-machine-learning insert tb_input_writer' in line: + funcs = [ + ('float', 'dump_tb_inputs_float'), + ('double', 'dump_tb_inputs_double'), + ] + newline = '' + for dtype, funcname in funcs: + newline += f'void {funcname}(\n' + newline += ' const char* output_path' + for inp in model_inputs: + newline += f',\n {dtype} {inp.name}[{inp.size_cpp()}]' + newline += '\n) {\n\n' + + for inp in model_inputs: + decl = inp.definition_cpp(name_suffix='_ap').strip() + ap = inp.name + '_ap' + if decl.startswith('hls::stream'): + newline += f' {decl};\n' + else: + newline += f' {inp.type.name} {ap}[{inp.size_cpp()}];\n' + newline += f' nnet::convert_data<{dtype}, {inp.type.name}, {inp.size_cpp()}>({inp.name}, {ap});\n' + newline += '\n' + newline += f' std::ofstream fout(std::string(output_path) + "/{inp.name}_input_data.txt");\n' + + for inp in model_inputs: + decl = inp.definition_cpp(name_suffix='_ap').strip() + shape = inp.shape + + if decl.startswith('hls::stream'): + if len(shape) == 1: + N = shape[0] + newline += f' for(int i = 0; i < {N}; i++) {{\n' + newline += f' auto temp = {inp.name}_ap.read();\n' + newline += f' ap_uint<{inp.type.name}::value_type::width> bits = temp[0].range();\n' + newline += f" fout << bits.to_uint() << (i+1<{N} ? ' ' : '\\n');\n" + newline += ' }\n' + else: + inputs_list = model.nn_config['inputs'] + fifo_depth = next((e['fifo_depth'] for e in inputs_list if e['name'] == inp.name), None) + batch_size = next((e['batch_size'] for e in inputs_list if e['name'] == inp.name), None) + newline += f' for(int r = 0; r < {fifo_depth}; r++) {{\n' + newline += f' auto temp = {inp.name}_ap.read();\n' + newline += f' for(int c = 0; c < {batch_size}; c++) {{\n' + newline += ( + f' ap_uint<{inp.type.name}::value_type::width> bits = temp[c].range();\n' + ) + newline += f" fout << bits.to_uint() << (c+1<{batch_size} ? ' ' : '\\n');\n" + newline += ' }\n' + newline += ' }\n' + else: + ap = inp.name + '_ap' + N = inp.size_cpp() + newline += f' for(int i = 0; i < {N}; i++) {{\n' + newline += f' ap_uint<{inp.type.name}::width> bits = {ap}[i].range();\n' + newline += f" fout << bits.to_uint() << (i+1<{N} ? ' ' : '\\n');\n" + newline += ' }\n' + newline += ' fout.close();\n' + newline += '}\n' + else: + newline = line + fout.write(newline) + + f.close() + fout.close() + + def write_build_script(self, model): + """Write the Shell build scripts (build_lib.sh, build_tb_exe.sh) + + Args: + model (ModelGraph): the hls4ml model. + """ + + filedir = Path(__file__).parent + + # build_bambu.sh + build_bambu_src = (filedir / '../templates/bambu/build_bambu.sh').resolve() + build_bambu_dst = Path(f'{model.config.get_output_dir()}/build_bambu.sh').resolve() + with open(build_bambu_src) as src, open(build_bambu_dst, 'w') as dst: + for line in src.readlines(): + dst.write(line) + build_bambu_dst.chmod(build_bambu_dst.stat().st_mode | stat.S_IEXEC) + + # build_lib.sh + build_lib_src = (filedir / '../templates/bambu/build_lib.sh').resolve() + build_lib_dst = Path(f'{model.config.get_output_dir()}/build_lib.sh').resolve() + with open(build_lib_src) as src, open(build_lib_dst, 'w') as dst: + for line in src.readlines(): + line = line.replace('myproject', model.config.get_project_name()) + line = line.replace('mystamp', model.config.get_config_value('Stamp')) + + dst.write(line) + build_lib_dst.chmod(build_lib_dst.stat().st_mode | stat.S_IEXEC) + + # build_tb_exe.sh + build_tb_src = (filedir / '../templates/bambu/build_tb_exe.sh').resolve() + build_tb_dst = Path(f'{model.config.get_output_dir()}/build_tb_exe.sh').resolve() + with open(build_tb_src) as src, open(build_tb_dst, 'w') as dst: + for line in src.readlines(): + line = line.replace('myproject', model.config.get_project_name()) + line = line.replace('mystamp', model.config.get_config_value('Stamp')) + + dst.write(line) + build_tb_dst.chmod(build_tb_dst.stat().st_mode | stat.S_IEXEC) + + def write_build_script_multigraph(self, model): + """Write the build script (build_lib.sh) for stitched multigraph project + Args: + model (MultiModelGraph): the hls4ml multigraph model. + """ + filedir = Path(__file__).parent + os.makedirs(model.config.get_output_dir(), exist_ok=True) + build_lib_src = (filedir / '../templates/bambu/build_lib_multigraph.sh').resolve() + build_lib_dst = Path(f'{model.config.get_output_dir()}/build_lib.sh').resolve() + graph_project_names = ' '.join(f'"{g.config.get_output_dir().split("/")[-1]}"' for g in model.graphs) + + with open(build_lib_src) as src, open(build_lib_dst, 'w') as dst: + for line in src.readlines(): + line = line.replace('myproject', model.config.config['OriginalProjectName']) + line = line.replace('myproject_stitched', model.config.config['ProjectName']) + line = line.replace('mystamp', model.config.config['Stamp']) + line = line.replace('mygraph_name_list', graph_project_names) + dst.write(line) + os.chmod(build_lib_dst, os.stat(build_lib_dst).st_mode | stat.S_IEXEC) + + def write_nnet_utils(self, model): + """Copy the nnet_utils, AP types headers and any custom source to the project output directory + + Args: + model (ModelGraph): the hls4ml model. + """ + + # nnet_utils + filedir = os.path.dirname(os.path.abspath(__file__)) + + srcpath = os.path.join(filedir, '../templates/bambu/nnet_utils/') + dstpath = f'{model.config.get_output_dir()}/firmware/nnet_utils/' + + if not os.path.exists(dstpath): + os.mkdir(dstpath) + + headers = [os.path.basename(h) for h in glob.glob(srcpath + '*.h')] + + # Check if gcem folder is present in nnet_utils, and copy it if so + gcem_srcpath = os.path.join(srcpath, 'gcem') + gcem_dstpath = os.path.join(dstpath, 'gcem') + if os.path.isdir(gcem_srcpath): + if os.path.exists(gcem_dstpath): + rmtree(gcem_dstpath) + copytree(gcem_srcpath, gcem_dstpath) + else: + logging.warning( + "The 'gcem' folder was not found in nnet_utils. " + 'If you need it, make sure to initialize this submodule:\n' + ' git submodule update --init hls4ml/templates/bambu/nnet_utils/gcem' + ) + for h in headers: + copyfile(srcpath + h, dstpath + h) + self._rewrite_array_partition_pragmas(dstpath + h) + + # ac_types (ap_fixed headers) — always copied; only the legacy + # USE_HLS4ML_AC_TYPES=1 path (see BambuBackend.build) compiles against it. + filedir = os.path.dirname(os.path.abspath(__file__)) + + srcpath = os.path.join(filedir, '../templates/bambu/ac_types/') + dstpath = f'{model.config.get_output_dir()}/firmware/ac_types/' + + if os.path.exists(dstpath): + rmtree(dstpath) + + copytree(srcpath, dstpath) + + # custom source + filedir = os.path.dirname(os.path.abspath(__file__)) + + custom_source = model.config.backend.get_custom_source() + for dst, srcpath in custom_source.items(): + dstpath = f'{model.config.get_output_dir()}/firmware/{dst}' + copyfile(srcpath, dstpath) + + def write_generated_code(self, model): + """Write the generated code (nnet_code_gen.h) + + Args: + model (ModelGraph): the hls4ml model. + """ + path = f'{model.config.get_output_dir()}/firmware/nnet_utils/nnet_code_gen.h' + f = open(path) + contents = f.readlines() + f.close() + f = open(path, 'w') + namespace = model.config.get_writer_config().get('Namespace', None) + + for line in contents: + if '// hls4ml insert code' in line: + newline = line + for layer in model.get_layers(): + for generated_code in layer.code.values(): + newline += str(generated_code) + else: + newline = line + if namespace is not None: + if 'namespace nnet' in newline: + newline = newline.replace('namespace nnet', f'namespace {namespace}') + f.write(newline) + f.close() + + def write_yml(self, model): + """Write the config to the YAML file + + Args: + model (ModelGraph): the hls4ml model. + """ + + def keras_model_representer(dumper, keras_model): + model_path = model.config.get_output_dir() + '/keras_model.keras' + keras_model.save(model_path) + return dumper.represent_scalar('!keras_model', model_path) + + try: + import keras + + KerasModel = keras.models.Model + + yaml.add_multi_representer(KerasModel, keras_model_representer) + except Exception: + pass + + with open(model.config.get_output_dir() + '/' + config_filename, 'w') as file: + yaml.dump(model.config.config, file) + + def write_tar(self, model): + """Write the generated project as a .tar.gz archive + + Args: + model (ModelGraph): the hls4ml model. + """ + + write_tar = model.config.get_writer_config().get('WriteTar', False) + if write_tar: + tar_path = model.config.get_output_dir() + '.tar.gz' + if os.path.exists(tar_path): + os.remove(tar_path) + with tarfile.open(tar_path, mode='w:gz') as archive: + archive.add(model.config.get_output_dir(), recursive=True, arcname='') + + def write_hls(self, model, is_multigraph=False): + if not is_multigraph: + self.write_project_dir(model) + self.write_project_cpp(model) + self.write_project_header(model) + self.write_weights(model) + self.write_defines(model) + self.write_parameters(model) + self.write_test_bench(model) + self.write_bridge(model) + self.write_build_script(model) + self.write_nnet_utils(model) + self.write_generated_code(model) + self.write_yml(model) + self.write_tar(model) + else: + self.write_project_dir(model) + self.write_build_script_multigraph(model) + self.write_bridge_multigraph(model) + self.write_multigraph_weights(model) diff --git a/hls4ml/writer/vivado_accelerator_writer.py b/hls4ml/writer/vivado_accelerator_writer.py index 164648539a..d677e2e7a2 100644 --- a/hls4ml/writer/vivado_accelerator_writer.py +++ b/hls4ml/writer/vivado_accelerator_writer.py @@ -243,10 +243,42 @@ def modify_build_script(self, model): f = open(oldfile) fout = open(newfile, 'w') + bitfile_block = """ +if {$opt(bitfile)} { + puts "***** BITFILE / XCLBIN GENERATION *****" + set time_start [clock clicks -milliseconds] + set abs_path_dir [file normalize [pwd]] + if {[string match "alveo*" $board]} { + file mkdir xo_files + if {[catch {exec vivado -mode batch -source design.tcl >@ stdout} err]} { + puts "ERROR: Something went wrong running design.tcl. Check the Vivado logs." + exit 1 + } + set ip_repo_path ${abs_path_dir}/${project_name}_prj/solution1/impl/ip + file mkdir xclbin_files + cd xclbin_files + if {[catch {exec v++ -t hw --platform $platform --link ../xo_files/${project_name}_kernel.xo -o \ + ${project_name}_kernel.xclbin --user_ip_repo_paths $ip_repo_path >@ stdout} err]} { + puts "ERROR: Something went wrong running v++. Check the Vitis/Vivado logs." + cd $abs_path_dir + exit 1 + } + cd $abs_path_dir + } else { + if {[catch {exec vivado -mode batch -source design.tcl >@ stdout} err]} { + puts "ERROR: Something went wrong running design.tcl. Check the Vivado logs." + exit 1 + } + } + set time_end [clock clicks -milliseconds] + report_time "BITFILE / XCLBIN GENERATION" $time_start $time_end +} + +""" for line in f.readlines(): if 'set_top' in line: newline = line[:-1] + '_axi\n' # remove the newline from the line end and append _axi for the new top - newline += f'add_files firmware/{model.config.get_project_name()}_axi.cpp -cflags "-std=c++0x"\n' + newline += f'add_files firmware/{model.config.get_project_name()}_axi.cpp -cflags $common_cflags\n' elif f'{model.config.get_project_name()}_cosim' in line: newline = line.replace( f'{model.config.get_project_name()}_cosim', @@ -254,6 +286,8 @@ def modify_build_script(self, model): ) elif '${project_name}.tcl' in line: newline = line.replace('${project_name}.tcl', '${project_name}_axi.tcl') + elif line.strip() == 'exit': + newline = bitfile_block + line else: newline = line fout.write(newline) @@ -358,6 +392,14 @@ def write_wrapper_test(self, model): fout.close() os.rename(newfile, oldfile) + def write_build_opt_override(self, model): + """Overwrite build_opt.tcl with VivadoAccelerator template (includes bitfile option).""" + filedir = os.path.dirname(os.path.abspath(__file__)) + copyfile( + os.path.join(filedir, '../templates/vivado_accelerator/build_opt.tcl'), + f'{model.config.get_output_dir()}/build_opt.tcl', + ) + def write_board_script(self, model): """ Write the tcl scripts and kernel sources to create a Vivado IPI project for the VivadoAccelerator @@ -391,6 +433,11 @@ def write_board_script(self, model): f.write('set version "{}"\n'.format(model.config.get_config_value('Version', '1.0.0'))) f.write('variable maximum_size\n') f.write('set maximum_size {}\n'.format(model.config.get_config_value('MaximumSize', '4096'))) + f.write('variable board\n') + f.write(f'set board "{self.vivado_accelerator_config.get_board()}"\n') + if self.vivado_accelerator_config.get_board().startswith('alveo'): + f.write('variable platform\n') + f.write(f'set platform "{self.vivado_accelerator_config.get_platform()}"\n') if self.vivado_accelerator_config.get_interface() == 'axi_stream': in_bit, out_bit = self.vivado_accelerator_config.get_io_bitwidth() f.write(f'set bit_width_hls_output {in_bit}\n') @@ -422,6 +469,7 @@ def write_hls(self, model): ) super().write_hls(model) self.write_board_script(model) + self.write_build_opt_override(model) self.write_driver(model) self.write_wrapper_test(model) self.write_axi_wrapper(model) diff --git a/hls4ml/writer/vivado_writer.py b/hls4ml/writer/vivado_writer.py index dc5556cb33..fdc972b6de 100644 --- a/hls4ml/writer/vivado_writer.py +++ b/hls4ml/writer/vivado_writer.py @@ -979,11 +979,21 @@ def write_build_script(self, model): dstpath = f'{model.config.get_output_dir()}/build_prj.tcl' copyfile(srcpath, dstpath) + # build_opt.tcl + srcpath = (filedir / '../templates/vivado/build_opt.tcl').resolve() + dstpath = f'{model.config.get_output_dir()}/build_opt.tcl' + copyfile(srcpath, dstpath) + # vivado_synth.tcl srcpath = (filedir / '../templates/vivado/vivado_synth.tcl').resolve() dstpath = f'{model.config.get_output_dir()}/vivado_synth.tcl' copyfile(srcpath, dstpath) + # statistics.tcl + srcpath = (filedir / '../templates/vivado/statistics.tcl').resolve() + dstpath = f'{model.config.get_output_dir()}/statistics.tcl' + copyfile(srcpath, dstpath) + # build_lib.sh build_lib_src = (filedir / '../templates/vivado/build_lib.sh').resolve() build_lib_dst = Path(f'{model.config.get_output_dir()}/build_lib.sh').resolve() diff --git a/pyproject.toml b/pyproject.toml index 542e1caa6e..6b3cba1904 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ optional-dependencies.doc = [ optional-dependencies.hgq = [ "hgq>=0.2.3" ] optional-dependencies.hgq2 = [ "hgq2>=0.1.7" ] optional-dependencies.keras-v3 = [ "keras>=3.10" ] +optional-dependencies.nanoxplore = [ "ortools" ] optional-dependencies.onnx = [ "onnx>=1.4" ] optional-dependencies.optimization = [ "keras-tuner==1.1.3", diff --git a/test/build-prj.sh b/test/build-prj.sh index f83bd02180..001307a1b8 100755 --- a/test/build-prj.sh +++ b/test/build-prj.sh @@ -7,13 +7,14 @@ hlsver=2020.1 hlscommand=vivado_hls parallel=1 -csim="csim=0" -synth="synth=0" -cosim="cosim=0" -validation="validation=0" -vsynth="vsynth=0" -export="export=0" -reset="reset=0" +csim=0 +synth=0 +cosim=0 +validation=0 +vsynth=0 +export=0 +reset=0 +fifo_opt=0 function print_usage { echo "Usage: `basename $0` [OPTION]" @@ -52,10 +53,29 @@ function print_usage { function run_hls { hlscommand=$1 dir=$2 - opt=$3 - echo "Building project in ${dir} with options: ${opt}" + reset=$3 + csim=$4 + synth=$5 + cosim=$6 + validation=$7 + vsynth=$8 + export=$9 + fifo_opt=${10} + echo "Building project in ${dir} with options: reset=${reset} csim=${csim} synth=${synth} cosim=${cosim} validation=${validation} vsynth=${vsynth} export=${export} fifo_opt=${fifo_opt}" cd ${dir} - cmd="\"${hlscommand}\" -f build_prj.tcl \"${opt}\" &> build_prj.log" + cat > build_opt.tcl << EOF +array set opt { + reset ${reset} + csim ${csim} + synth ${synth} + cosim ${cosim} + validation ${validation} + export ${export} + vsynth ${vsynth} + fifo_opt ${fifo_opt} +} +EOF + cmd="\"${hlscommand}\" -f build_prj.tcl &> build_prj.log" eval ${cmd} if [ $? -eq 1 ]; then touch BUILD_FAILED @@ -69,7 +89,7 @@ function check_status { cd ${dir} if [ -f BUILD_FAILED ]; then echo "" - echo "Building project ${dir} (${opt}) failed. Log:" + echo "Building project ${dir} failed. Log:" cat build_prj.log echo "" failed=1 @@ -87,19 +107,19 @@ while getopts ":d:i:v:p:csrtlenah" opt; do ;; p) parallel=$OPTARG ;; - c) csim="csim=1" + c) csim=1 ;; - s) synth="synth=1" + s) synth=1 ;; - r) cosim="cosim=1" + r) cosim=1 ;; - t) validation="validation=1" + t) validation=1 ;; - l) vsynth="vsynth=1" + l) vsynth=1 ;; - e) export="export=1" + e) export=1 ;; - n) reset="reset=1" + n) reset=1 ;; a) hlstool='Vitis' hlscommand='vitis_hls' @@ -135,27 +155,25 @@ done source ${hlsdir}/${hlstool}/${hlsver}/settings64.sh -opt="${reset} ${csim} ${synth} ${cosim} ${validation} ${vsynth} ${export}" - if [ "${parallel}" -gt 1 ]; then # Run in parallel ( for dir in *-${hlstool}-${hlsver}/ ; do ((n=n%parallel)); ((n++==0)) && wait - run_hls "${hlscommand}" "${dir}" "${opt}" & + run_hls "${hlscommand}" "${dir}" "${reset}" "${csim}" "${synth}" "${cosim}" "${validation}" "${vsynth}" "${export}" "${fifo_opt}" & done wait ) else # Run sequentially for dir in *-${hlstool}-${hlsver}/ ; do - run_hls "${hlscommand}" "${dir}" "${opt}" + run_hls "${hlscommand}" "${dir}" "${reset}" "${csim}" "${synth}" "${cosim}" "${validation}" "${vsynth}" "${export}" "${fifo_opt}" done fi # Check for build errors for dir in *-${hlstool}-${hlsver}/ ; do - check_status "${dir}" "${opt}" + check_status "${dir}" done #cd "${rundir}" diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py index 7841a1a989..0d38155f35 100644 --- a/test/pytest/conftest.py +++ b/test/pytest/conftest.py @@ -6,6 +6,64 @@ _PROBLEMATIC_CHARS = ':;=%\'"<>|?*\\' +def pytest_addoption(parser): + parser.addoption( + '--backend-filter', + action='append', + default=[], + help='Run only parametrized test cases whose backend parameter matches one of these values.', + ) + parser.addoption( + '--backend-exclude', + action='append', + default=[], + help='Skip parametrized test cases whose backend parameter matches one of these values.', + ) + parser.addoption( + '--ci-exclude-nodeid', + action='append', + default=[], + help='Skip collected tests whose non-parametrized nodeid matches this value.', + ) + + +def _base_nodeid(nodeid): + return nodeid.split('[', 1)[0] + + +def pytest_collection_modifyitems(config, items): + backend_filter = set(config.getoption('--backend-filter')) + backend_exclude = set(config.getoption('--backend-exclude')) + excluded_nodeids = set(config.getoption('--ci-exclude-nodeid')) + + if not backend_filter and not backend_exclude and not excluded_nodeids: + return + + kept = [] + deselected = [] + for item in items: + base_nodeid = _base_nodeid(item.nodeid) + backend = getattr(item, 'callspec', None) + backend = backend.params.get('backend') if backend is not None else None + + selected = True + if base_nodeid in excluded_nodeids: + selected = False + if backend_filter and backend not in backend_filter: + selected = False + if backend_exclude and backend in backend_exclude: + selected = False + + if selected: + kept.append(item) + else: + deselected.append(item) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = kept + + def _sanitize_test_id(s: str) -> str: """ Sanitize a test identifier for use in paths. diff --git a/test/pytest/test_activations.py b/test/pytest/test_activations.py index 19f2ed9d01..7123b074d1 100644 --- a/test/pytest/test_activations.py +++ b/test/pytest/test_activations.py @@ -13,7 +13,7 @@ # Variable 'name' is simply used as an identifier for the activation -@pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Catapult', 'Quartus', 'oneAPI']) +@pytest.mark.parametrize('backend', ['Bambu', 'Vivado', 'Vitis', 'Catapult', 'Quartus', 'oneAPI']) @pytest.mark.parametrize('shape, io_type', [((8,), 'io_parallel'), ((8,), 'io_stream'), ((8, 8, 3), 'io_stream')]) @pytest.mark.parametrize( 'activation, name', diff --git a/test/pytest/test_auto_precision.py b/test/pytest/test_auto_precision.py index d3738c8461..1a0c19cf89 100644 --- a/test/pytest/test_auto_precision.py +++ b/test/pytest/test_auto_precision.py @@ -119,7 +119,7 @@ def keras_model_sepconv2d(): @pytest.mark.parametrize('io_type', ['io_stream', 'io_parallel']) -@pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Quartus']) +@pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Quartus', 'Bambu']) @pytest.mark.parametrize('model_type', ['conv1d', 'conv2d']) def test_auto_precision_conv( test_case_id, keras_model_conv1d, keras_model_conv2d, data_2d, data_3d, model_type, io_type, backend diff --git a/test/pytest/test_build_bambu.py b/test/pytest/test_build_bambu.py new file mode 100644 index 0000000000..057350e161 --- /dev/null +++ b/test/pytest/test_build_bambu.py @@ -0,0 +1,235 @@ +import os +from pathlib import Path + +import numpy as np +import pytest +from tensorflow.keras.layers import Dense +from tensorflow.keras.models import Sequential + +import hls4ml + +test_root_path = Path(__file__).parent + +# ----------------------------------------------------------------------------- +# fixtures / helpers +# ----------------------------------------------------------------------------- + + +@pytest.fixture(scope='module') +def simple_model(): + """Simple Keras model for build testing""" + model = Sequential() + model.add(Dense(8, input_shape=(2,))) + return model + + +def count_files_with_extension(directory, extension): + """Counts how many files in the directory and all its + subdirectories have files that end with extension""" + return sum(1 for _ in Path(directory).rglob(f'*{extension}')) + + +# ----------------------------------------------------------------------------- +# tests +# ----------------------------------------------------------------------------- + + +@pytest.mark.parametrize('io_type', ['io_parallel']) +@pytest.mark.parametrize('strategy', ['latency']) +@pytest.mark.parametrize('granularity', ['name']) +@pytest.mark.parametrize('batch_size', [10]) +@pytest.mark.parametrize('backend', ['Vitis', 'Bambu']) +def test_csimulation(test_case_id, simple_model, tmp_path, io_type, strategy, granularity, batch_size, backend): + output_dir = str(test_root_path / test_case_id) + + model = simple_model + X_input = np.random.rand(batch_size, 2).astype(np.float32) + + config = hls4ml.utils.config_from_keras_model(model, granularity=granularity) + config['Model']['Strategy'] = strategy + + hls_model = hls4ml.converters.convert_from_keras_model( + model, hls_config=config, output_dir=output_dir, io_type=io_type, backend=backend + ) + hls_model.compile() + y_pred = hls_model.predict(X_input) + + input_data_tb = str(tmp_path / 'input.npy') + output_data_tb = str(tmp_path / 'output.npy') + np.save(input_data_tb, X_input) + np.save(output_data_tb, y_pred) + + hls_model_csim = hls4ml.converters.convert_from_keras_model( + model, + hls_config=config, + output_dir=output_dir, + io_type=io_type, + backend=backend, + input_data_tb=input_data_tb, + output_data_tb=output_data_tb, + ) + hls_model_csim.compile() + hls_model_csim.build(synth=True, csim=True, log_to_stdout=True) + + bridge_result = np.loadtxt(os.path.join(output_dir, 'tb_data', 'tb_output_predictions.dat')) + csim_result = np.loadtxt(os.path.join(output_dir, 'tb_data', 'csim_results.log')) + assert np.allclose(bridge_result, csim_result, rtol=0.0, atol=1e-4) + + +@pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) +@pytest.mark.parametrize('strategy', ['latency']) +@pytest.mark.parametrize('granularity', ['name']) +@pytest.mark.parametrize('batch_size', [10]) +@pytest.mark.parametrize('backend', ['Vitis', 'Bambu']) +@pytest.mark.parametrize('part', ['xc7a100tcsg324-1']) +def test_cosimulation(test_case_id, simple_model, tmp_path, io_type, strategy, granularity, batch_size, backend, part): + output_dir = str(test_root_path / test_case_id) + + model = simple_model + X_input = np.random.rand(batch_size, 2).astype(np.float32) + + config = hls4ml.utils.config_from_keras_model(model, granularity=granularity) + config['Model']['Strategy'] = strategy + + hls_model = hls4ml.converters.convert_from_keras_model( + model, + hls_config=config, + output_dir=output_dir, + io_type=io_type, + backend=backend, + part=part, + clock_period=20, + ) + hls_model.compile() + y_pred = hls_model.predict(X_input) + + input_data_tb = str(os.path.join(output_dir, 'input.npy')) + output_data_tb = str(os.path.join(output_dir, 'output.npy')) + np.save(input_data_tb, X_input) + np.save(output_data_tb, y_pred) + + hls_model_cosim = hls4ml.converters.convert_from_keras_model( + model, + hls_config=config, + output_dir=output_dir, + io_type=io_type, + backend=backend, + input_data_tb=input_data_tb, + output_data_tb=output_data_tb, + part=part, + clock_period=20, + ) + hls_model_cosim.compile() + hls_model_cosim.build(csim=False, synth=True, cosim=True, log_to_stdout=True) + + bridge_result = np.loadtxt(os.path.join(output_dir, 'tb_data', 'tb_output_predictions.dat')) + cosim_result = np.loadtxt(os.path.join(output_dir, 'tb_data', 'rtl_cosim_results.log')) + assert np.allclose(bridge_result, cosim_result, rtol=0.0, atol=1e-4) + + +@pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) +@pytest.mark.parametrize('strategy', ['latency']) +@pytest.mark.parametrize('granularity', ['name']) +@pytest.mark.parametrize('backend', ['Vitis', 'Bambu']) +def test_synth(test_case_id, simple_model, io_type, strategy, granularity, backend): + """Test that a successful synth run produces the desired artifacts (.v file)""" + synth_proj_dir = test_root_path / test_case_id + + model = simple_model + + config = hls4ml.utils.config_from_keras_model(model, granularity=granularity) + config['Model']['Strategy'] = strategy + + hls_model = hls4ml.converters.convert_from_keras_model( + model, + hls_config=config, + output_dir=str(synth_proj_dir), + io_type=io_type, + backend=backend, + ) + hls_model.build(csim=False, synth=True) + + # Bambu-specific artifact checks + if backend == 'Bambu': + proj_name = hls_model.config.get_project_name() + assert Path(synth_proj_dir, f'{proj_name}.v').exists() + + # TODO: Vitis-specific artifact checks + + +@pytest.mark.parametrize('io_type', ['io_parallel']) +@pytest.mark.parametrize('strategy', ['latency']) +@pytest.mark.parametrize('granularity', ['name']) +@pytest.mark.parametrize('backend', ['Vitis', 'Bambu']) +@pytest.mark.parametrize('part', ['xc7a100tcsg324-1']) +def test_vsynth(test_case_id, simple_model, io_type, strategy, granularity, backend, part): + """Test that a successful vsynth run produces the desired reports. + + `part` stays parametrized so a target can be added, but the sweep is the + backend's own default 7-Series Artix. NanoXplore targets route into + family-specific branches and belong with the accelerator-layer tests. + """ + vsynth_proj_dir = test_root_path / test_case_id + + model = simple_model + + config = hls4ml.utils.config_from_keras_model(model, granularity=granularity) + config['Model']['Strategy'] = strategy + + hls_model = hls4ml.converters.convert_from_keras_model( + model, + hls_config=config, + output_dir=str(vsynth_proj_dir), + io_type=io_type, + backend=backend, + part=part, + clock_period=25, + ) + hls_model.build(csim=False, synth=True, cosim=True, vsynth=True) + + # Bambu-specific artifact checks + if backend == 'Bambu': + # Ensure we get bambu results file. Older Bambu versions produced + # `bambu_results_.xml`; current versions produce + # `bambu_results.xml` — match both. + assert sum(1 for _ in vsynth_proj_dir.rglob('bambu_results*.xml')) >= 1 + + # Ensure we get expected reports. The .rpt files come from the Vivado + # synthesis Bambu drives; without Vivado on PATH (e.g. the Bambu-only CI + # image) none are produced, so the check is only meaningful there. + if hls_model.config.get_config_value('FPGAFamily') == 'Xilinx': + num_reports = count_files_with_extension(vsynth_proj_dir / 'HLS_output', '.rpt') + if num_reports == 0: + pytest.skip('No Vivado reports produced (Vivado not available in this environment)') + assert num_reports >= 15 + + # TODO: Vitis-specific artifact checks + + +@pytest.mark.parametrize('backend', ['Bambu']) +def test_build_failure_is_reported(test_case_id, simple_model, monkeypatch, backend): + """A non-zero Bambu exit must raise instead of falling through to report parsing. + + Without the check, a failed run returns an empty result (or, with ``reset=False``, + a report left by a previous run) and ``build()`` looks like it succeeded. + """ + output_dir = str(test_root_path / test_case_id) + + config = hls4ml.utils.config_from_keras_model(simple_model, granularity='name') + hls_model = hls4ml.converters.convert_from_keras_model( + simple_model, hls_config=config, output_dir=output_dir, io_type='io_parallel', backend=backend + ) + + class FailingProcess: + returncode = 3 + + def communicate(self): + return ('', '') + + # Stand in for a Bambu installation that is present but exits non-zero. + backend_module = hls4ml.backends.bambu.bambu_backend + monkeypatch.setattr(backend_module.shutil, 'which', lambda name: f'/usr/bin/{name}') + monkeypatch.setattr(backend_module.subprocess, 'Popen', lambda *args, **kwargs: FailingProcess()) + + with pytest.raises(RuntimeError, match='exit code 3'): + hls_model.build(csim=False, synth=True) diff --git a/test/pytest/test_build_bambu_accelerator.py b/test/pytest/test_build_bambu_accelerator.py new file mode 100644 index 0000000000..14e84795eb --- /dev/null +++ b/test/pytest/test_build_bambu_accelerator.py @@ -0,0 +1,149 @@ +"""End-to-end tests for the Bambu accelerator layer. + +`test_build_bambu.py` covers the Bambu backend itself (HLS C++ -> Verilog). +This file covers what the accelerator layer adds on top: the float I/O +wrapper, the AXI slave RTL, the PLL block, and the `manifest.json` contract +that a place-and-route flow consumes. + +The tests stop at `bitstream=False`, so they need Bambu but no vendor P&R +tool -- everything asserted here is produced by hls4ml itself. That is the +whole point of the manifest seam: the artefact is complete and checkable +before any vendor tool sees it. +""" + +import json +from pathlib import Path + +import pytest +from tensorflow.keras.layers import Dense +from tensorflow.keras.models import Sequential + +import hls4ml + +test_root_path = Path(__file__).parent + +# Deliberately NOT powers of two. Bambu rounds each BRAM address port up to +# the next power of two, so 3 -> depth 4 and 5 -> depth 8: the element count +# and the BRAM depth DIFFER. That gap is the entire point of these shapes. +# With, say, 2 -> 4 the two conventions coincide and the assertions below pass +# under either one -- which is how the geometry bug reached hardware in the +# first place. `test_shapes_discriminate_the_conventions` locks this in. +N_IN = 3 +N_OUT = 5 + + +@pytest.fixture(scope='module') +def simple_model(): + model = Sequential() + model.add(Dense(N_OUT, input_shape=(N_IN,))) + return model + + +@pytest.fixture(scope='module') +def built_projects(simple_model): + """Build once per flow; the tests below read the artefacts. + + Module-scoped because a Bambu synth run is minutes, not seconds, and + every assertion here is a read-only look at the result. + """ + projects = {} + for io_type in ('io_parallel', 'io_stream'): + output_dir = test_root_path / f'hls4mlprj_bambu_accel_{io_type}' + config = hls4ml.utils.config_from_keras_model(simple_model, granularity='name') + hls_model = hls4ml.converters.convert_from_keras_model( + simple_model, + hls_config=config, + output_dir=str(output_dir), + io_type=io_type, + backend='NanoXploreAccelerator', + ) + hls_model.build(csim=False, synth=True, bitstream=False) + projects[io_type] = (hls_model, output_dir) + return projects + + +def _manifest(output_dir): + return json.loads((Path(output_dir) / 'manifest.json').read_text()) + + +def test_shapes_discriminate_the_conventions(built_projects): + """Guard the guard: if someone rounds these shapes to powers of two, the + BRAM depth equals the element count and every N_WORDS assertion in this + file silently stops testing anything.""" + _, output_dir = built_projects['io_parallel'] + m = _manifest(output_dir) + assert m['bram_slots'] != m['n_words'], ( + 'model shapes no longer distinguish BRAM depth from element count -- pick non-power-of-two layer sizes' + ) + + +@pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) +def test_manifest_contract(built_projects, io_type): + """The manifest is the whole public/private interface -- check its shape.""" + _, output_dir = built_projects[io_type] + m = _manifest(output_dir) + + assert m['manifest_version'] == 1 + assert m['top_module'] == 'myproject' + assert m['flow'] == ('stream' if io_type == 'io_stream' else 'parallel') + assert m['n_words'] == {'in': N_IN, 'out': N_OUT} + assert m['clock_mhz'] == pytest.approx(1000.0 / m['clock_period_ns']) + + # bram_slots is the parallel-only BRAM depth and must be a power of two + # (it is 2 ** address_port_width); stream has no depth at all. + if m['flow'] == 'parallel': + for key in ('in', 'out'): + depth = m['bram_slots'][key] + assert depth >= m['n_words'][key] + assert depth & (depth - 1) == 0, f'{key} depth {depth} is not a power of two' + else: + assert m['bram_slots'] is None + + # Container widths are >= the ap_fixed value widths they carry. + for key in ('in', 'out'): + assert m['data_widths'][key] >= m['fixed_point'][key]['total'] + + +@pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) +def test_rtl_file_list_is_complete(built_projects, io_type): + """`rtl_files` is what the P&R side blindly adds. A missing entry there + surfaces as a vendor elaboration error with no obvious cause, so assert + every listed file actually exists next to the manifest.""" + _, output_dir = built_projects[io_type] + m = _manifest(output_dir) + + assert m['rtl_files'], 'manifest lists no RTL files' + missing = [f for f in m['rtl_files'] if not (Path(output_dir) / f).exists()] + assert not missing, f'manifest lists files that were not written: {missing}' + + # The wrapper module the manifest names as top must be in the HLS output. + hls_top_v = Path(output_dir) / f'{m["hls_top"]}.v' + assert hls_top_v.exists() + assert 'module myproject' in hls_top_v.read_text() + + +@pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) +def test_top_localparams_match_the_manifest(built_projects, io_type): + """Regression test for the bsp_rc=4 DMA hang. + + The top file used to ship the reference project's hardcoded geometry, so + the AXI slave advertised fewer read beats than the firmware requested and + the burst hung on hardware. The generated localparams must agree with the + manifest -- and note HLS_*_N_WORDS means different things per flow: the + BRAM depth on parallel, the element count on stream. + """ + _, output_dir = built_projects[io_type] + m = _manifest(output_dir) + top = (Path(output_dir) / f'top_{m["flow"]}.v').read_text() + + expected = m['n_words'] if m['flow'] == 'stream' else m['bram_slots'] + assert f'localparam HLS_IN_N_WORDS = {expected["in"]};' in top + assert f'localparam HLS_OUT_N_WORDS = {expected["out"]};' in top + assert f'localparam HLS_IN_DATA_W = {m["data_widths"]["in"]};' in top + assert f'localparam HLS_OUT_DATA_W = {m["data_widths"]["out"]};' in top + + # The marker block must survive patching, or the next build cannot patch. + assert top.count('// HLS4ML PARAMS BEGIN') == 1 + assert top.count('// HLS4ML PARAMS END') == 1 + assert top.count('// HLS4ML PLL BEGIN') == 1 + assert top.count('// HLS4ML PLL END') == 1 diff --git a/test/pytest/test_dense_unrolled.py b/test/pytest/test_dense_unrolled.py index b1d2415931..45aefbe425 100644 --- a/test/pytest/test_dense_unrolled.py +++ b/test/pytest/test_dense_unrolled.py @@ -32,7 +32,7 @@ def test_resource_unrolled_parsing(test_case_id, strategy): # Tests a wide range of RF to ensure the unrolled resource kernel is correct @pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) @pytest.mark.parametrize('reuse_factor', [1, 2, 4, 8, 16, 32, 48, 64, 96, 192]) -@pytest.mark.parametrize('backend', ['Vitis', 'Vivado']) +@pytest.mark.parametrize('backend', ['Vitis', 'Vivado', 'Bambu']) def test_resource_unrolled_dense(test_case_id, io_type, reuse_factor, backend): input_shape = (16,) X = np.random.rand(100, *input_shape) diff --git a/test/pytest/test_keras_api.py b/test/pytest/test_keras_api.py index 606f2bc51d..d6bc4aba32 100644 --- a/test/pytest/test_keras_api.py +++ b/test/pytest/test_keras_api.py @@ -26,7 +26,7 @@ test_root_path = Path(__file__).parent -@pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Quartus', 'oneAPI']) +@pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Quartus', 'oneAPI', 'Bambu']) @pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) def test_dense(test_case_id, backend, io_type, synthesis_config): model = tf.keras.models.Sequential() @@ -137,6 +137,8 @@ def test_activations(test_case_id, activation_function, backend, io_type, synthe ('Vitis', 'Latency'), ('Quartus', 'Resource'), ('oneAPI', 'Resource'), + ('Bambu', 'Resource'), + ('Bambu', 'Latency'), ], ) @pytest.mark.parametrize('io_type', ['io_parallel', 'io_stream']) @@ -175,7 +177,7 @@ def test_conv1d(test_case_id, padds, backend, strategy, io_type, synthesis_confi # 5e-2 might be too high np.testing.assert_allclose(hls_prediction, keras_prediction, rtol=0, atol=5e-2) - if not (backend in ['Vivado', 'Vitis'] and io_type == 'io_stream' and padds == 'same'): + if not (backend in ['Vivado', 'Vitis', 'Bambu'] and io_type == 'io_stream' and padds == 'same'): # Vivado/Vitis inserts and additional layer for 'same' padding in io_stream assert len(model.layers) + 2 == len(hls_model.get_layers()) assert list(hls_model.get_layers())[1].attributes['name'] == model.layers[0]._name diff --git a/test/pytest/test_multi_dense.py b/test/pytest/test_multi_dense.py index 04c21f8923..ec52c10e98 100644 --- a/test/pytest/test_multi_dense.py +++ b/test/pytest/test_multi_dense.py @@ -19,6 +19,8 @@ ('Vitis', 'Resource'), ('Quartus', 'Resource'), ('oneAPI', 'Resource'), + ('Bambu', 'Resource'), + ('Bambu', 'Latency'), ('Catapult', 'Latency'), ('Catapult', 'Resource'), ], diff --git a/test/pytest/test_pooling.py b/test/pytest/test_pooling.py index 07e40e340e..fe2d96bda5 100644 --- a/test/pytest/test_pooling.py +++ b/test/pytest/test_pooling.py @@ -33,7 +33,7 @@ def keras_model_1d(request): return model, model_type, pads, strides -@pytest.mark.parametrize('backend', ['Quartus', 'Vitis', 'Vivado', 'Catapult', 'oneAPI']) +@pytest.mark.parametrize('backend', ['Quartus', 'Vitis', 'Vivado', 'Catapult', 'oneAPI', 'Bambu']) @pytest.mark.parametrize( 'keras_model_1d', [ @@ -128,7 +128,7 @@ def keras_model_2d(request): return model, model_type, pads, strides -@pytest.mark.parametrize('backend', ['Quartus', 'Vitis', 'Vivado', 'Catapult', 'oneAPI']) +@pytest.mark.parametrize('backend', ['Quartus', 'Vitis', 'Vivado', 'Catapult', 'oneAPI', 'Bambu']) @pytest.mark.parametrize( 'keras_model_2d', [ diff --git a/test/pytest/test_report.py b/test/pytest/test_report.py index 5424102235..f4c651614a 100644 --- a/test/pytest/test_report.py +++ b/test/pytest/test_report.py @@ -148,3 +148,38 @@ def test_report(hls_model_setup, capsys): captured = capsys.readouterr() # capture again to test assert captured.out == backend_config['expected_outcome'] + + +def test_bambu_report(tmp_path): + """Tests parsing of Bambu XML reports (reference schema: PandA 2026.06). + + The fixture uses the real root with attributes, + a top-level block, and cycle counts. + parse_bambu_report is called with part_family='Xilinx'; without Vivado + report files present only BambuMetrics is populated. + """ + output_dir = tmp_path / 'bambu' + output_dir.mkdir() + sample_report = test_root_path / 'test_report/Bambu/bambu_results_0.xml' + shutil.copy(sample_report, output_dir / 'bambu_results_0.xml') + + report = hls4ml.report.parse_bambu_report(str(output_dir), 'Xilinx') + + assert report is not None + m = report['BambuMetrics'] + + # Resource metrics from attributes + assert m['LUTS'] == 798 + assert m['REGISTERS'] == 808 + assert m['DSPS'] == 27 + assert m['BRAMS'] == 0 + assert m['SLICES'] == 292 + + # Cycle count from + assert m['Total cycles'] == 35 + assert m['Number of executions'] == 1 + assert m['Average execution'] == pytest.approx(35.0) + + # CYCLES from top-level (setdefault, so does not override resources) + assert m['CYCLES'] == 35 + assert m['AREA'] == 10248 diff --git a/test/pytest/test_report/Bambu/bambu_results_0.xml b/test/pytest/test_report/Bambu/bambu_results_0.xml new file mode 100644 index 0000000000..2175355f50 --- /dev/null +++ b/test/pytest/test_report/Bambu/bambu_results_0.xml @@ -0,0 +1,42 @@ + + + + + + + + 35 + + + + + diff --git a/test/pytest/test_softmax.py b/test/pytest/test_softmax.py index 418f64b558..3741796190 100644 --- a/test/pytest/test_softmax.py +++ b/test/pytest/test_softmax.py @@ -19,7 +19,7 @@ def generate_data(input_shape): return np.clip(d, -32, 31) -@pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Quartus', 'Catapult']) +@pytest.mark.parametrize('backend', ['Bambu', 'Vivado', 'Vitis', 'Quartus', 'Catapult']) @pytest.mark.parametrize('strategy', ['stable', 'latency', 'argmax']) @pytest.mark.parametrize( 'input_bits,input_shape,table_bits,io_type,custom_accum', @@ -43,6 +43,18 @@ def test_softmax(test_case_id, backend, strategy, generate_data, input_bits, inp table_type = f'fixed<{table_bits}, RND, SAT>' + if backend == 'Bambu': + # Bambu emits the softmax inverse LUT as a constexpr std::array. When + # fix_softmax_table_size shrinks the table (2**min(input_bw, table_bw) < + # table_size, default 1024), the constexpr initializer divides by zero + # while clang evaluates it at compile time and the build is rejected. + # Vivado/Vitis fill the LUT at runtime and are unaffected. See + # docs/backend/bambu.rst. + input_bw = int(input_bits.split(',')[0]) + table_bw = int(table_bits.split(',')[0]) + if 2 ** min(input_bw, table_bw) < 1024: + pytest.skip('Bambu cannot compile a resized softmax constexpr LUT (see docs/backend/bambu.rst)') + cfg = hls4ml.utils.config_from_keras_model(model, granularity='name', backend=backend) cfg['LayerName']['softmax']['Strategy'] = strategy cfg['LayerName']['softmax']['inv_table_t'] = table_type