From 1e5017a30a977b6be576eaefe9ec39e958716b2f Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:12:36 +0200 Subject: [PATCH 01/15] Vivado/Vitis build flow and report improvements (from PR #1448) Vendored here so this branch builds and tests on its own. This code is NOT part of this PR's contribution -- it is the content of open PR #1448 by the same author, on which the Bambu backend depends: * parse_vivado_report() split into per-report helpers; bambu_report.py reuses _parse_csim_results, _parse_rtl_cosim_results, _parse_implementation_report, _parse_timing_report, _parse_power_report * PATHS table and _path(), including the vivado_reports/post_route_*.rpt locations Bambu writes its own results into * statistics.tcl and the vivado_synth.tcl rewrite that produce them Review it in #1448, not here. When #1448 merges, rebasing drops this commit. --- .gitignore | 2 +- hls4ml/backends/symbolic/symbolic_backend.py | 45 +- hls4ml/backends/vitis/vitis_backend.py | 2 + hls4ml/backends/vivado/vivado_backend.py | 40 +- .../vivado_accelerator_backend.py | 95 +-- hls4ml/cli/__init__.py | 40 +- hls4ml/report/vivado_report.py | 589 +++++++++++------- hls4ml/templates/vitis/build_opt.tcl | 1 + hls4ml/templates/vitis/build_prj.tcl | 91 ++- hls4ml/templates/vivado/build_opt.tcl | 11 + hls4ml/templates/vivado/build_prj.tcl | 114 +++- hls4ml/templates/vivado/statistics.tcl | 72 +++ hls4ml/templates/vivado/vivado_synth.tcl | 61 +- .../vivado_accelerator/build_opt.tcl | 12 + .../pynq-z2/tcl_scripts/axi_lite_design.tcl | 22 +- .../pynq-z2/tcl_scripts/axi_stream_design.tcl | 22 +- .../zcu102/tcl_scripts/axi_stream_design.tcl | 23 +- hls4ml/utils/symbolic_utils.py | 29 +- hls4ml/writer/vivado_accelerator_writer.py | 50 +- hls4ml/writer/vivado_writer.py | 10 + test/build-prj.sh | 64 +- 21 files changed, 1007 insertions(+), 388 deletions(-) create mode 100644 hls4ml/templates/vivado/build_opt.tcl create mode 100644 hls4ml/templates/vivado/statistics.tcl create mode 100644 hls4ml/templates/vivado_accelerator/build_opt.tcl 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/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/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/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/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/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}" From 0babe2253973739733f3b376b1e8ecec2446e478 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:03:53 +0200 Subject: [PATCH 02/15] feat(bambu): add HLS C++ templates and nnet_utils for the Bambu backend Firmware templates, build scripts and the nnet_utils header library used by the Bambu backend. Two submodules come with them: templates/bambu/ac_types ferrandi/ac_types, Bambu's fork templates/bambu/nnet_utils/gcem kthohr/gcem, constexpr math for tables ac_types diverges from the hlslibs/ac_types already vendored for Catapult, so it cannot be shared with that backend. --- .gitmodules | 6 + hls4ml/templates/bambu/ac_types | 1 + hls4ml/templates/bambu/build_bambu.sh | 7 + hls4ml/templates/bambu/build_lib.sh | 97 ++ hls4ml/templates/bambu/build_lib_float.sh | 98 ++ .../templates/bambu/build_lib_multigraph.sh | 50 + hls4ml/templates/bambu/build_tb_exe.sh | 97 ++ hls4ml/templates/bambu/build_tb_float_exe.sh | 98 ++ hls4ml/templates/bambu/firmware/defines.h | 23 + hls4ml/templates/bambu/firmware/myproject.cpp | 23 + hls4ml/templates/bambu/firmware/myproject.h | 21 + .../bambu/firmware/myproject_float.h | 11 + hls4ml/templates/bambu/firmware/parameters.h | 19 + hls4ml/templates/bambu/ip_stitcher.tcl | 658 ++++++++++ hls4ml/templates/bambu/myproject_bridge.cpp | 71 + .../templates/bambu/myproject_float_test.cpp | 82 ++ hls4ml/templates/bambu/myproject_test.cpp | 101 ++ hls4ml/templates/bambu/nnet_utils/gcem | 1 + .../bambu/nnet_utils/nnet_activation.h | 1158 +++++++++++++++++ .../bambu/nnet_utils/nnet_activation_stream.h | 905 +++++++++++++ .../bambu/nnet_utils/nnet_batchnorm.h | 124 ++ .../bambu/nnet_utils/nnet_batchnorm_stream.h | 126 ++ .../bambu/nnet_utils/nnet_code_gen.h | 28 + .../templates/bambu/nnet_utils/nnet_common.h | 76 ++ .../templates/bambu/nnet_utils/nnet_conv1d.h | 118 ++ .../bambu/nnet_utils/nnet_conv1d_latency.h | 171 +++ .../bambu/nnet_utils/nnet_conv1d_resource.h | 123 ++ .../bambu/nnet_utils/nnet_conv1d_stream.h | 97 ++ .../templates/bambu/nnet_utils/nnet_conv2d.h | 80 ++ .../bambu/nnet_utils/nnet_conv2d_latency.h | 96 ++ .../bambu/nnet_utils/nnet_conv2d_resource.h | 112 ++ .../bambu/nnet_utils/nnet_conv2d_stream.h | 119 ++ .../bambu/nnet_utils/nnet_conv_stream.h | 392 ++++++ .../bambu/nnet_utils/nnet_cropping.h | 92 ++ .../bambu/nnet_utils/nnet_cropping_stream.h | 75 ++ .../bambu/nnet_utils/nnet_da_wrappers.h | 127 ++ .../templates/bambu/nnet_utils/nnet_dense.h | 92 ++ .../bambu/nnet_utils/nnet_dense_compressed.h | 95 ++ .../bambu/nnet_utils/nnet_dense_latency.h | 78 ++ .../bambu/nnet_utils/nnet_dense_resource.h | 284 ++++ .../bambu/nnet_utils/nnet_dense_stream.h | 73 ++ .../bambu/nnet_utils/nnet_depthwise_product.h | 312 +++++ .../templates/bambu/nnet_utils/nnet_einsum.h | 86 ++ .../bambu/nnet_utils/nnet_einsum_dense.h | 114 ++ .../templates/bambu/nnet_utils/nnet_embed.h | 47 + .../bambu/nnet_utils/nnet_embed_stream.h | 34 + .../bambu/nnet_utils/nnet_function_stubs.h | 89 ++ .../templates/bambu/nnet_utils/nnet_garnet.h | 826 ++++++++++++ .../templates/bambu/nnet_utils/nnet_helpers.h | 384 ++++++ .../templates/bambu/nnet_utils/nnet_image.h | 41 + .../bambu/nnet_utils/nnet_image_stream.h | 72 + .../bambu/nnet_utils/nnet_layernorm.h | 142 ++ hls4ml/templates/bambu/nnet_utils/nnet_math.h | 178 +++ .../templates/bambu/nnet_utils/nnet_merge.h | 283 ++++ .../bambu/nnet_utils/nnet_merge_stream.h | 390 ++++++ hls4ml/templates/bambu/nnet_utils/nnet_mult.h | 116 ++ .../templates/bambu/nnet_utils/nnet_padding.h | 176 +++ .../bambu/nnet_utils/nnet_padding_stream.h | 87 ++ .../templates/bambu/nnet_utils/nnet_pooling.h | 316 +++++ .../bambu/nnet_utils/nnet_pooling_stream.h | 618 +++++++++ .../bambu/nnet_utils/nnet_recr_activations.h | 56 + .../bambu/nnet_utils/nnet_recurrent.h | 881 +++++++++++++ .../bambu/nnet_utils/nnet_sepconv1d.h | 46 + .../bambu/nnet_utils/nnet_sepconv1d_latency.h | 94 ++ .../bambu/nnet_utils/nnet_sepconv1d_stream.h | 124 ++ .../bambu/nnet_utils/nnet_sepconv2d.h | 51 + .../bambu/nnet_utils/nnet_sepconv2d_latency.h | 95 ++ .../bambu/nnet_utils/nnet_sepconv2d_stream.h | 148 +++ .../bambu/nnet_utils/nnet_sepconv_stream.h | 248 ++++ .../templates/bambu/nnet_utils/nnet_stream.h | 349 +++++ .../bambu/nnet_utils/nnet_time_distributed.h | 121 ++ .../bambu/nnet_utils/nnet_transpose.h | 40 + .../bambu/nnet_utils/nnet_transpose_stream.h | 71 + .../templates/bambu/nnet_utils/nnet_types.h | 83 ++ 74 files changed, 12823 insertions(+) create mode 160000 hls4ml/templates/bambu/ac_types create mode 100644 hls4ml/templates/bambu/build_bambu.sh create mode 100755 hls4ml/templates/bambu/build_lib.sh create mode 100644 hls4ml/templates/bambu/build_lib_float.sh create mode 100644 hls4ml/templates/bambu/build_lib_multigraph.sh create mode 100644 hls4ml/templates/bambu/build_tb_exe.sh create mode 100644 hls4ml/templates/bambu/build_tb_float_exe.sh create mode 100644 hls4ml/templates/bambu/firmware/defines.h create mode 100644 hls4ml/templates/bambu/firmware/myproject.cpp create mode 100644 hls4ml/templates/bambu/firmware/myproject.h create mode 100644 hls4ml/templates/bambu/firmware/myproject_float.h create mode 100644 hls4ml/templates/bambu/firmware/parameters.h create mode 100644 hls4ml/templates/bambu/ip_stitcher.tcl create mode 100644 hls4ml/templates/bambu/myproject_bridge.cpp create mode 100644 hls4ml/templates/bambu/myproject_float_test.cpp create mode 100644 hls4ml/templates/bambu/myproject_test.cpp create mode 160000 hls4ml/templates/bambu/nnet_utils/gcem create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_activation.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_activation_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_batchnorm.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_batchnorm_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_code_gen.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_common.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv1d.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv1d_latency.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv1d_resource.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv1d_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv2d.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv2d_latency.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv2d_resource.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv2d_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_conv_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_cropping.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_cropping_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_da_wrappers.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_dense.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_dense_compressed.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_dense_latency.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_dense_resource.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_dense_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_depthwise_product.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_einsum.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_einsum_dense.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_embed.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_embed_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_function_stubs.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_garnet.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_helpers.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_image.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_image_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_layernorm.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_math.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_merge.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_merge_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_mult.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_padding.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_padding_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_pooling.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_pooling_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_recr_activations.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_recurrent.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_latency.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_sepconv1d_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_latency.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_sepconv2d_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_sepconv_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_time_distributed.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_transpose.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_transpose_stream.h create mode 100644 hls4ml/templates/bambu/nnet_utils/nnet_types.h 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/hls4ml/templates/bambu/ac_types b/hls4ml/templates/bambu/ac_types new file mode 160000 index 0000000000..35e101ad01 --- /dev/null +++ b/hls4ml/templates/bambu/ac_types @@ -0,0 +1 @@ +Subproject commit 35e101ad016cc8c5caec0737ee43f83ed2c67c1b 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 From 9069fc81c3254320ab4da09a4a94dacc7b35176a Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:04:12 +0200 Subject: [PATCH 03/15] feat(bambu): add the Bambu backend and its layer optimizer passes BambuBackend converts hls4ml models to HLS C++ and drives Bambu/PandA to synthesizable Verilog, for both io_parallel and io_stream. partname_to_bambu maps hls4ml part names onto Bambu device names. Not registered yet -- registration lands with the writer and report modules it depends on. --- hls4ml/backends/bambu/__init__.py | 0 hls4ml/backends/bambu/bambu_backend.py | 1290 +++++++++++++++++ hls4ml/backends/bambu/bambu_types.py | 140 ++ hls4ml/backends/bambu/passes/__init__.py | 0 hls4ml/backends/bambu/passes/bn_quant.py | 166 +++ .../backends/bambu/passes/broadcast_stream.py | 116 ++ hls4ml/backends/bambu/passes/conv_same_pad.py | 105 ++ hls4ml/backends/bambu/passes/conv_stream.py | 47 + .../bambu/passes/convolution_templates.py | 688 +++++++++ .../backends/bambu/passes/core_templates.py | 398 +++++ .../bambu/passes/distributed_arithmetic.py | 410 ++++++ hls4ml/backends/bambu/passes/einsum.py | 109 ++ hls4ml/backends/bambu/passes/einsum_dense.py | 148 ++ .../bambu/passes/fifo_depth_optimization.py | 104 ++ .../backends/bambu/passes/garnet_templates.py | 249 ++++ .../backends/bambu/passes/im2col_codegen.py | 116 ++ .../backends/bambu/passes/merge_templates.py | 107 ++ .../backends/bambu/passes/pipeline_style.py | 131 ++ hls4ml/backends/bambu/passes/pointwise.py | 87 ++ .../bambu/passes/pooling_templates.py | 109 ++ .../bambu/passes/quantization_templates.py | 36 + .../bambu/passes/recurrent_templates.py | 518 +++++++ .../bambu/passes/reshaping_templates.py | 205 +++ .../bambu/passes/resource_strategy.py | 60 + .../backends/bambu/passes/transform_types.py | 52 + .../backends/bambu/passes/unrolled_codegen.py | 237 +++ 26 files changed, 5628 insertions(+) create mode 100644 hls4ml/backends/bambu/__init__.py create mode 100644 hls4ml/backends/bambu/bambu_backend.py create mode 100644 hls4ml/backends/bambu/bambu_types.py create mode 100644 hls4ml/backends/bambu/passes/__init__.py create mode 100644 hls4ml/backends/bambu/passes/bn_quant.py create mode 100644 hls4ml/backends/bambu/passes/broadcast_stream.py create mode 100644 hls4ml/backends/bambu/passes/conv_same_pad.py create mode 100644 hls4ml/backends/bambu/passes/conv_stream.py create mode 100644 hls4ml/backends/bambu/passes/convolution_templates.py create mode 100644 hls4ml/backends/bambu/passes/core_templates.py create mode 100644 hls4ml/backends/bambu/passes/distributed_arithmetic.py create mode 100644 hls4ml/backends/bambu/passes/einsum.py create mode 100644 hls4ml/backends/bambu/passes/einsum_dense.py create mode 100644 hls4ml/backends/bambu/passes/fifo_depth_optimization.py create mode 100644 hls4ml/backends/bambu/passes/garnet_templates.py create mode 100644 hls4ml/backends/bambu/passes/im2col_codegen.py create mode 100644 hls4ml/backends/bambu/passes/merge_templates.py create mode 100644 hls4ml/backends/bambu/passes/pipeline_style.py create mode 100644 hls4ml/backends/bambu/passes/pointwise.py create mode 100644 hls4ml/backends/bambu/passes/pooling_templates.py create mode 100644 hls4ml/backends/bambu/passes/quantization_templates.py create mode 100644 hls4ml/backends/bambu/passes/recurrent_templates.py create mode 100644 hls4ml/backends/bambu/passes/reshaping_templates.py create mode 100644 hls4ml/backends/bambu/passes/resource_strategy.py create mode 100644 hls4ml/backends/bambu/passes/transform_types.py create mode 100644 hls4ml/backends/bambu/passes/unrolled_codegen.py 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..d2ecf3c2f9 --- /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', + '--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 From 7d3b3229d3726d9108d0a1b559e16d1374581613 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:04:58 +0200 Subject: [PATCH 04/15] feat(bambu): add the Bambu writer and report parsing bambu_writer.py emits the HLS C++ project; bambu_report.py parses Bambu's XML results and reuses the Vivado report helpers for the post-route utilization, timing and power numbers. --- hls4ml/report/bambu_report.py | 148 ++++ hls4ml/writer/bambu_writer.py | 1295 +++++++++++++++++++++++++++++++++ 2 files changed, 1443 insertions(+) create mode 100644 hls4ml/report/bambu_report.py create mode 100644 hls4ml/writer/bambu_writer.py 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/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) From 855618743d6a8752788523328f5e8490b99b693b Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:06:33 +0200 Subject: [PATCH 05/15] feat(bambu): register the Bambu backend, writer and report parser Only the Bambu lines: the accelerator layer registers itself separately. --- hls4ml/backends/__init__.py | 3 +++ hls4ml/report/__init__.py | 1 + hls4ml/writer/__init__.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/hls4ml/backends/__init__.py b/hls4ml/backends/__init__.py index 07a089cdf8..20ae7dec45 100644 --- a/hls4ml/backends/__init__.py +++ b/hls4ml/backends/__init__.py @@ -13,6 +13,8 @@ from hls4ml.backends.vitis.vitis_backend import VitisBackend # isort: skip +from hls4ml.backends.bambu.bambu_backend import BambuBackend # isort: skip + def _register_builtin_backends(): register_backend('Vivado', VivadoBackend) @@ -23,6 +25,7 @@ def _register_builtin_backends(): register_backend('SymbolicExpression', SymbolicExpressionBackend) register_backend('oneAPI', OneAPIBackend) register_backend('Libero', LiberoBackend) + register_backend('Bambu', BambuBackend) _register_builtin_backends() 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/writer/__init__.py b/hls4ml/writer/__init__.py index 8c48f79d2d..c7118f6019 100644 --- a/hls4ml/writer/__init__.py +++ b/hls4ml/writer/__init__.py @@ -1,3 +1,4 @@ +from hls4ml.writer.bambu_writer import BambuWriter from hls4ml.writer.catapult_writer import CatapultWriter from hls4ml.writer.libero_writer import LiberoWriter from hls4ml.writer.oneapi_writer import OneAPIWriter @@ -16,3 +17,4 @@ register_writer('Catapult', CatapultWriter) register_writer('Libero', LiberoWriter) register_writer('SymbolicExpression', SymbolicExpressionWriter) +register_writer('Bambu', BambuWriter) From a70c0c031798a54f50ed4c7dcf512b6c35d77ad2 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:14:25 +0200 Subject: [PATCH 06/15] test(bambu): end-to-end build test and Bambu test parametrization test_build_bambu.py drives csim, cosim, synth and vsynth on the default Xilinx part. The existing keras/activation/softmax/pooling suites gain 'Bambu' alongside the other backends. conftest.py grows --backend-filter / --backend-exclude / --ci-exclude-nodeid so a pipeline without the Bambu toolchain can deselect these cases; the CI wiring that uses them is a separate PR. --- test/pytest/conftest.py | 58 +++++ test/pytest/test_activations.py | 2 +- test/pytest/test_auto_precision.py | 2 +- test/pytest/test_build_bambu.py | 235 ++++++++++++++++++ test/pytest/test_dense_unrolled.py | 2 +- test/pytest/test_keras_api.py | 6 +- test/pytest/test_multi_dense.py | 2 + test/pytest/test_pooling.py | 4 +- test/pytest/test_report.py | 35 +++ .../test_report/Bambu/bambu_results_0.xml | 42 ++++ test/pytest/test_softmax.py | 14 +- 11 files changed, 394 insertions(+), 8 deletions(-) create mode 100644 test/pytest/test_build_bambu.py create mode 100644 test/pytest/test_report/Bambu/bambu_results_0.xml 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_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 From 336ec2ddf24e41f7c7055dc27441fbe0319b07c9 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:15:05 +0200 Subject: [PATCH 07/15] docs(bambu): add backend page and list it in the toctree --- docs/backend/bambu.rst | 64 ++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 65 insertions(+) create mode 100644 docs/backend/bambu.rst 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/index.rst b/docs/index.rst index 35e79f75bb..1fe2fb48f5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ backend/oneapi backend/catapult backend/quartus + backend/bambu backend/sr .. toctree:: From 832d1878f445373fe91afd2bc9425fee758ecdbc Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Tue, 28 Jul 2026 10:22:21 +0200 Subject: [PATCH 08/15] chore(bambu): bump ac_types submodule to abb773b Picks up six upstream commits on ferrandi/ac_types dev/panda-hls since 35e101a, mostly hls::stream/ac_channel work: DEPTH template parameter on the stream copy constructor and assignment operator (#3), peek support (#4), a gcc 15 / system C++ library fix (#5), improved hls::stream channel data type support (#6), a cosim regression fix (#7), and better set_slc/set_slc2 (#8). --- hls4ml/templates/bambu/ac_types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hls4ml/templates/bambu/ac_types b/hls4ml/templates/bambu/ac_types index 35e101ad01..abb773b4d5 160000 --- a/hls4ml/templates/bambu/ac_types +++ b/hls4ml/templates/bambu/ac_types @@ -1 +1 @@ -Subproject commit 35e101ad016cc8c5caec0737ee43f83ed2c67c1b +Subproject commit abb773b4d5152dd96ed8a3da1efde82d66991cdc From ee08425de11e9c6bd54d3f6db796dee4d5af5113 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Tue, 28 Jul 2026 10:22:26 +0200 Subject: [PATCH 09/15] fix(bambu): point the legacy ac_types include at the submodule layout Since ac_types became a git submodule the headers live under ac_types/include, not at the top level, so the USE_HLS4ML_AC_TYPES escape hatch was passing an include path with no headers in it. --- hls4ml/backends/bambu/bambu_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hls4ml/backends/bambu/bambu_backend.py b/hls4ml/backends/bambu/bambu_backend.py index d2ecf3c2f9..55433a20f9 100644 --- a/hls4ml/backends/bambu/bambu_backend.py +++ b/hls4ml/backends/bambu/bambu_backend.py @@ -447,7 +447,7 @@ def build( # bug on io_stream's ac_channel FIFOs, so it is filtered below. REQ_ARGS = [ '-lm', - '-Ifirmware/ac_types', + '-Ifirmware/ac_types/include', '--compiler=I386_CLANG16', CC_TEMPLATE_DEPTH, '--generate-interface=INFER', From 9f3993f01d0e213afe9af5d2b6adebee2fe21d4f Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:21:07 +0200 Subject: [PATCH 10/15] feat(bambu_accelerator): add the AXI4 slave and top-level RTL templates AXI4-Lite slave for both IO flows, the top-level wrappers that instantiate the Bambu-generated core alongside it, and the supporting skid buffer, FIFO and address-increment modules. --- .../bambu_accelerator/rtl/AXISlaveParallel.v | 1212 +++++++++++++++ .../bambu_accelerator/rtl/AXISlaveStream.v | 1298 +++++++++++++++++ .../bambu_accelerator/rtl/axi_addr.v | 235 +++ .../templates/bambu_accelerator/rtl/sfifo.v | 241 +++ .../bambu_accelerator/rtl/skidbuffer.v | 502 +++++++ .../bambu_accelerator/rtl/top_parallel.v | 698 +++++++++ .../bambu_accelerator/rtl/top_stream.v | 684 +++++++++ 7 files changed, 4870 insertions(+) create mode 100644 hls4ml/templates/bambu_accelerator/rtl/AXISlaveParallel.v create mode 100644 hls4ml/templates/bambu_accelerator/rtl/AXISlaveStream.v create mode 100644 hls4ml/templates/bambu_accelerator/rtl/axi_addr.v create mode 100644 hls4ml/templates/bambu_accelerator/rtl/sfifo.v create mode 100644 hls4ml/templates/bambu_accelerator/rtl/skidbuffer.v create mode 100644 hls4ml/templates/bambu_accelerator/rtl/top_parallel.v create mode 100644 hls4ml/templates/bambu_accelerator/rtl/top_stream.v 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 From 7a1b25768cce0e41513b960e169c374162720aea Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:21:07 +0200 Subject: [PATCH 11/15] feat(bambu_accelerator): add the abstract accelerator layer BambuAcceleratorBackend turns a Bambu-generated HLS core into an integrable accelerator: float I/O wrapper, AXI slave instantiation, PLL configuration and a versioned manifest.json describing the artefact. The backend never imports a vendor tool. Everything a place-and-route flow needs -- RTL file list, clock, port map, data widths -- goes into manifest.json, and the concrete backend shells out to a single CLI. The one abstract method is _generate_bitstream, so this layer is fully testable without a vendor licence. pll_solver.py solves an NX_PLL_U configuration for a requested clock period so the hardware clock and the timing constraint agree by construction. It needs OR-Tools, an optional dependency. Abstract and unregistered; the concrete backend follows. --- hls4ml/backends/bambu_accelerator/__init__.py | 0 .../bambu_accelerator_backend.py | 510 +++++++++++++++++ .../backends/bambu_accelerator/pll_solver.py | 536 ++++++++++++++++++ hls4ml/backends/bambu_accelerator/wrapper.py | 339 +++++++++++ hls4ml/writer/bambu_accelerator_writer.py | 438 ++++++++++++++ 5 files changed, 1823 insertions(+) create mode 100644 hls4ml/backends/bambu_accelerator/__init__.py create mode 100644 hls4ml/backends/bambu_accelerator/bambu_accelerator_backend.py create mode 100644 hls4ml/backends/bambu_accelerator/pll_solver.py create mode 100644 hls4ml/backends/bambu_accelerator/wrapper.py create mode 100644 hls4ml/writer/bambu_accelerator_writer.py 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/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) From bae3d386ed3f6fd21cddc6e992874d8811f087d3 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:21:16 +0200 Subject: [PATCH 12/15] feat(nanoxplore): add the NanoXplore NG-ULTRA accelerator backend Concrete BambuAccelerator subclass: NG-ULTRA defaults (nx2h540tsc, 20 ns to match the DevKit's 50 MHz oscillator) and the place-and-route call, made through the CLI seam (hls4ml-nanoxplore-bitstream , overridable via BitStreamCommand) with report.json read back. --- .../nanoxplore_accelerator/__init__.py | 0 .../nanoxplore_accelerator_backend.py | 95 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 hls4ml/backends/nanoxplore_accelerator/__init__.py create mode 100644 hls4ml/backends/nanoxplore_accelerator/nanoxplore_accelerator_backend.py 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.' + ) From 8669296e663316e7446313952af72a787dce5f58 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:22:02 +0200 Subject: [PATCH 13/15] feat(nanoxplore): register the accelerator backend and writer Adds the accelerator half of the registration files, on top of the Bambu half from the previous PR. BambuAcceleratorBackend stays unregistered -- it is abstract. The pll extra ships as optional-dependencies.nanoxplore (OR-Tools). --- hls4ml/backends/__init__.py | 5 +++++ hls4ml/writer/__init__.py | 3 +++ pyproject.toml | 1 + 3 files changed, 9 insertions(+) diff --git a/hls4ml/backends/__init__.py b/hls4ml/backends/__init__.py index 20ae7dec45..499f0c46d4 100644 --- a/hls4ml/backends/__init__.py +++ b/hls4ml/backends/__init__.py @@ -15,6 +15,10 @@ 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) @@ -26,6 +30,7 @@ def _register_builtin_backends(): register_backend('oneAPI', OneAPIBackend) register_backend('Libero', LiberoBackend) register_backend('Bambu', BambuBackend) + register_backend('NanoXploreAccelerator', NanoXploreAcceleratorBackend) _register_builtin_backends() diff --git a/hls4ml/writer/__init__.py b/hls4ml/writer/__init__.py index c7118f6019..650365e4ec 100644 --- a/hls4ml/writer/__init__.py +++ b/hls4ml/writer/__init__.py @@ -1,4 +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 @@ -18,3 +20,4 @@ register_writer('Libero', LiberoWriter) register_writer('SymbolicExpression', SymbolicExpressionWriter) register_writer('Bambu', BambuWriter) +register_writer('BambuAccelerator', BambuAcceleratorWriter) 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", From 2dad18ff79157a2c821df2617624b5368d6f9255 Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:22:12 +0200 Subject: [PATCH 14/15] test(bambu_accelerator): end-to-end coverage for the accelerator layer Builds both IO flows with bitstream=False, so the tests need Bambu but no vendor place-and-route tool, and assert on the manifest contract, the RTL file list, and the generated top-level localparams. The model shapes are deliberately not powers of two. Bambu rounds each BRAM address port up to the next power of two, so the element count and the BRAM depth differ -- which matters because HLS_*_N_WORDS means the depth on io_parallel and the element count on io_stream. With power-of-two shapes the two coincide and the assertions pass under either convention, so a dedicated test guards the shapes themselves. --- test/pytest/test_build_bambu_accelerator.py | 149 ++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 test/pytest/test_build_bambu_accelerator.py 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 From f2fe150bb09db6947678955b374e34601155e73e Mon Sep 17 00:00:00 2001 From: nicologhielmetti Date: Mon, 20 Jul 2026 15:22:46 +0200 Subject: [PATCH 15/15] docs(nanoxplore): add the accelerator backend page --- docs/backend/nanoxplore.rst | 64 +++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 65 insertions(+) create mode 100644 docs/backend/nanoxplore.rst 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 1fe2fb48f5..3ce9b32761 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,6 +39,7 @@ backend/catapult backend/quartus backend/bambu + backend/nanoxplore backend/sr .. toctree::