diff --git a/docs/finn/command_line.rst b/docs/finn/command_line.rst index 07fb3b094c..9dced6255d 100644 --- a/docs/finn/command_line.rst +++ b/docs/finn/command_line.rst @@ -81,7 +81,7 @@ as it goes through numerous steps: Running step: step_measure_rtlsim_performance [15/19] Running step: step_out_of_context_synthesis [16/19] Running step: step_synthesize_bitfile [17/19] - Running step: step_make_pynq_driver [18/19] + Running step: step_make_driver [18/19] Running step: step_deployment_package [19/19] diff --git a/docs/finn/hw_build.rst b/docs/finn/hw_build.rst index 39c39eb7df..5e1f67e9f2 100644 --- a/docs/finn/hw_build.rst +++ b/docs/finn/hw_build.rst @@ -35,7 +35,7 @@ To rapidly test the generated design on PYNQ platforms, FINN is capable of generating a Python driver for the given design. This driver packs/unpacks the input/output tensors in the expected format, then uses PYNQ APIs to initiate data movement and transfer back the results to the host CPU. The generation of -the driver is done by transformation pass :py:mod:`finn.transformation.fpgadataflow.make_pynq_driver.MakePYNQDriver`. +the driver is done by transformation pass :py:mod:`finn.transformation.fpgadataflow.make_driver.MakePYNQDriver`. DMA and DWC Node Insertion --------------------------- diff --git a/docs/finn/source_code/finn.transformation.fpgadataflow.rst b/docs/finn/source_code/finn.transformation.fpgadataflow.rst index f56b5fcf01..d72dd467a2 100644 --- a/docs/finn/source_code/finn.transformation.fpgadataflow.rst +++ b/docs/finn/source_code/finn.transformation.fpgadataflow.rst @@ -149,7 +149,7 @@ finn.transformation.fpgadataflow.insert\_tlastmarker finn.transformation.fpgadataflow.make\_pynq\_driver ---------------------------------------------------------- -.. automodule:: finn.transformation.fpgadataflow.make_pynq_driver +.. automodule:: finn.transformation.fpgadataflow.make_driver :members: :undoc-members: :show-inheritance: diff --git a/notebooks/advanced/4_advanced_builder_settings.ipynb b/notebooks/advanced/4_advanced_builder_settings.ipynb index e9e37459c8..db85d59552 100644 --- a/notebooks/advanced/4_advanced_builder_settings.ipynb +++ b/notebooks/advanced/4_advanced_builder_settings.ipynb @@ -1565,7 +1565,7 @@ "metadata": {}, "source": [ "You can see that after the generation of the estimate reports, the code generation and the ip generation is invoked (`step_hw_codegen` and `step_hw_ipgen`). The FIFO depths are determined and the FIFOs are inserted in the network (`step_set_fifo_depths`), we can then create an IP design of our whole network by stitching the IPs from each layer together (`step_create_stitched_ip`). At this point we have an implementation of the neural network that we can integrate within a bigger FPGA design, we can run performance measurements using simulation (`step_measure_rtlsim_performance`) and out-of-context synthesis (`step_out_of_context_synthesis`) for it.\n", - "The FINN builder also provides automatic system integration for Zynq and Alveo devices, this can be invoked by running `step_synthesize_bitfile`, `step_make_pynq_driver` and `step_deployment_package`." + "The FINN builder also provides automatic system integration for Zynq and Alveo devices, this can be invoked by running `step_synthesize_bitfile`, `step_make_driver` and `step_deployment_package`." ] }, { @@ -1782,7 +1782,7 @@ " \"step_measure_rtlsim_performance\",\n", " \"step_out_of_context_synthesis\",\n", " \"step_synthesize_bitfile\",\n", - " \"step_make_pynq_driver\",\n", + " \"step_make_driver\",\n", " \"step_deployment_package\",\n", "]\n", "\n", diff --git a/notebooks/end2end_example/bnn-pynq/cnv_end2end_example.ipynb b/notebooks/end2end_example/bnn-pynq/cnv_end2end_example.ipynb index 55e6f9b7aa..2b01f24557 100644 --- a/notebooks/end2end_example/bnn-pynq/cnv_end2end_example.ipynb +++ b/notebooks/end2end_example/bnn-pynq/cnv_end2end_example.ipynb @@ -456,7 +456,7 @@ "metadata": {}, "outputs": [], "source": [ - "from finn.transformation.fpgadataflow.make_pynq_driver import MakePYNQDriver\n", + "from finn.transformation.fpgadataflow.make_driver import MakePYNQDriver\n", "model = model.transform(MakePYNQDriver(\"zynq-iodma\"))" ] }, diff --git a/notebooks/end2end_example/bnn-pynq/tfc_end2end_example.ipynb b/notebooks/end2end_example/bnn-pynq/tfc_end2end_example.ipynb index 974e513f58..b0510b0fdb 100644 --- a/notebooks/end2end_example/bnn-pynq/tfc_end2end_example.ipynb +++ b/notebooks/end2end_example/bnn-pynq/tfc_end2end_example.ipynb @@ -751,7 +751,7 @@ "metadata": {}, "outputs": [], "source": [ - "from finn.transformation.fpgadataflow.make_pynq_driver import MakePYNQDriver\n", + "from finn.transformation.fpgadataflow.make_driver import MakePYNQDriver\n", "model = model.transform(MakePYNQDriver(\"zynq-iodma\"))" ] }, diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 64c9adc16c..3bc2c46794 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -62,6 +62,7 @@ class DataflowOutputType(str, Enum): RTLSIM_PERFORMANCE = "rtlsim_performance" BITFILE = "bitfile" PYNQ_DRIVER = "pynq_driver" + CPP_DRIVER = "cpp_driver" DEPLOYMENT_PACKAGE = "deployment_package" @@ -123,7 +124,7 @@ class VerificationStepType(str, Enum): "step_measure_rtlsim_performance", "step_out_of_context_synthesis", "step_synthesize_bitfile", - "step_make_pynq_driver", + "step_make_driver", "step_deployment_package", ] @@ -356,6 +357,11 @@ class DataflowBuildConfig: #: rtlsim, otherwise they will be replaced by RTL implementations. rtlsim_use_vivado_comps: Optional[bool] = True + #: Determine if the C++ driver should be generated instead of the PYNQ driver + #: If set to latest newest version will be used + #: If set to commit hash specified version will be used + cpp_driver_version: Optional[str] = "latest" + def _resolve_hls_clk_period(self): if self.hls_clk_period_ns is None: # use same clk for synth and hls if not explicitly specified diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 88cc04438c..d0fd9636a5 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -31,6 +31,7 @@ import numpy as np import os import shutil +import warnings from copy import deepcopy from functools import partial from qonnx.core.modelwrapper import ModelWrapper @@ -87,7 +88,7 @@ from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.insert_dwc import InsertDWC from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO -from finn.transformation.fpgadataflow.make_pynq_driver import MakePYNQDriver +from finn.transformation.fpgadataflow.make_driver import MakeCPPDriver, MakePYNQDriver from finn.transformation.fpgadataflow.make_zynq_proj import ZynqBuild from finn.transformation.fpgadataflow.minimize_accumulator_width import ( MinimizeAccumulatorWidth, @@ -732,15 +733,31 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi return model -def step_make_pynq_driver(model: ModelWrapper, cfg: DataflowBuildConfig): - """Create a PYNQ Python driver that can be used to interface the generated - accelerator.""" +def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): + """Create a driver that can be used to interface the generated accelerator. + Use DataflowBuildConfig to select PYNQ Python or C++ driver.""" + driver_dir = os.path.join(cfg.output_dir, "driver") if DataflowOutputType.PYNQ_DRIVER in cfg.generate_outputs: - driver_dir = cfg.output_dir + "/driver" + # generate PYNQ driver model = model.transform(MakePYNQDriver(cfg._resolve_driver_platform())) shutil.copytree(model.get_metadata_prop("pynq_driver_dir"), driver_dir, dirs_exist_ok=True) print("PYNQ Python driver written into " + driver_dir) + elif DataflowOutputType.CPP_DRIVER in cfg.generate_outputs: + # generate C++ Driver + model = model.transform( + MakeCPPDriver( + cfg._resolve_driver_platform(), + version=cfg.cpp_driver_version, + ) + ) + shutil.copytree(model.get_metadata_prop("cpp_driver_dir"), driver_dir, dirs_exist_ok=True) + print("C++ driver written into " + driver_dir) + else: + warnings.warn( + "The step step_make_driver is in the build list but will not be executed" + + " since no driver is selected in generate_outputs in your build.py file!" + ) return model @@ -862,7 +879,7 @@ def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig): "step_set_fifo_depths": step_set_fifo_depths, "step_create_stitched_ip": step_create_stitched_ip, "step_measure_rtlsim_performance": step_measure_rtlsim_performance, - "step_make_pynq_driver": step_make_pynq_driver, + "step_make_driver": step_make_driver, "step_out_of_context_synthesis": step_out_of_context_synthesis, "step_synthesize_bitfile": step_synthesize_bitfile, "step_deployment_package": step_deployment_package, diff --git a/src/finn/transformation/fpgadataflow/make_driver.py b/src/finn/transformation/fpgadataflow/make_driver.py new file mode 100644 index 0000000000..dc0cd2a518 --- /dev/null +++ b/src/finn/transformation/fpgadataflow/make_driver.py @@ -0,0 +1,391 @@ +# Copyright (C) 2020, Xilinx, Inc. +# Copyright (C) 2025, Advanced Micro Devices, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of FINN nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import json +import numpy as np +import os +import qonnx +import shlex +import shutil +import subprocess +import warnings +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.custom_op.registry import getCustomOp +from qonnx.transformation.base import Transformation +from string import Template +from typing import Dict, Tuple + +import finn.util +from finn.util.basic import make_build_dir +from finn.util.data_packing import get_driver_shapes, to_external_tensor + +from . import template_driver + + +class MakeCPPDriver(Transformation): + """Create CPP code to correctly interface the generated + accelerator, including data packing/unpacking. Should be called + after conversion to HLS layers, folding and the creation of + dataflow partitions for correct operation. + platform: has to be "alveo", otherwise an error is thrown + Outcome if successful: sets the cpp_driver_dir attribute in the ONNX + ModelProto's metadata_props field, with the created driver dir as the + value. + runtime writeable weights not yet supported. + """ + + # TODO: Enable multiple input types! Now only assumes the first one + def resolve_dt_name(s: str) -> str: + s = s.replace("DataType[", "").replace("]", "") + if s in ["BINARY", "TERNARY", "BIPOLAR"]: + return "Datatype" + s[0] + s[1:].lower() + elif s.startswith("U"): + return "DatatypeUint<" + s.replace("UINT", "") + ">" + elif s.startswith("I"): + return "DatatypeInt<" + s.replace("INT", "") + ">" + elif "FLOAT" in s: + return "DatatypeFloat<" + s.replace("FLOAT", "") + ">" + elif "FIXED" in s: + return "DatatypeFixed" + s.replace("FIXED", "") + else: + raise RuntimeError(f"Unknown datatype for C++ Driver:{s}") + + def __init__( + self, + platform: str, + version: str, + ): + super().__init__() + self.platform: str = platform + assert ( + platform == "alveo" + ), "CPP driver only supported for Alveo devices, please use PYNQ driver instead." + self.version = version + + # Define variables for the repository URL and commit hash + self.repository_url = "https://github.com/eki-project/finn-cpp-driver.git" + if version == "latest" or version is None: + self.commit_hash = "HEAD" + else: + self.commit_hash = version + + def apply(self, model: ModelWrapper) -> Tuple[ModelWrapper, bool]: + driver_shapes: Dict = get_driver_shapes(model) + ext_weight_dma_cnt: int # noqa + weights_dir: str # noqa + # ext_weight_dma_cnt, weights_dir = write_weights(model, cpp_driver_dir) + + # * Creating the driver dir if it doesnt exist yet + # create a temporary folder for the generated driver + cpp_driver_dir = make_build_dir(prefix="cpp_driver_") + model.set_metadata_prop("cpp_driver_dir", cpp_driver_dir) + xclbin_path = model.get_metadata_prop("bitfile") + json_path = os.path.join(cpp_driver_dir, "acceleratorconfig.json") + header_path = os.path.join(cpp_driver_dir, "AcceleratorDatatypes.h") + + # Get the base C++ driver repo + def run_command(command, cwd=None, debug=False): + try: + result = subprocess.run( + shlex.split(command), cwd=cwd, check=True, text=True, capture_output=True + ) + if debug: + print(result.stdout) # Print the output for debugging purposes + except subprocess.CalledProcessError as e: + print(f"Error running command: {command}") + print(f"Output:{e.stdout}; Error:{e.stderr}") + raise e + + # Step-by-step equivalent of the provided bash script + run_command("git init", cwd=cpp_driver_dir) + run_command(f"git remote add origin {self.repository_url}", cwd=cpp_driver_dir) + run_command(f"git fetch origin {self.commit_hash} --depth=1", cwd=cpp_driver_dir) + run_command("git checkout FETCH_HEAD", cwd=cpp_driver_dir) + run_command("git submodule update --init --recursive", cwd=cpp_driver_dir) + run_command("./buildDependencies.sh", cwd=cpp_driver_dir) + + # * Writing the header file + inputDatatype: str = MakeCPPDriver.resolve_dt_name( + driver_shapes["idt"][0].replace("'", "") + ) # .get_canonical_name()) + outputDatatype: str = MakeCPPDriver.resolve_dt_name( + driver_shapes["odt"][0].replace("'", "") + ) # .get_canonical_name()) + with open( + os.path.join( + cpp_driver_dir, "src", "FINNCppDriver", "config", "FinnDriverUsedDatatypes.h.in" + ), + "r", + ) as f_in: + header = f_in.read() + template_handler = Template(header) + templated_str = template_handler.substitute( + inputDatatype=inputDatatype, outputDatatype=outputDatatype + ) + with open(header_path, "w+") as f: + f.write(templated_str) + + # * Writing the json file + # TODO: Update this for multi-fpga usage (more than one device!) + # Path of the xclbin in the finn compiler project + # Get kernel names using xclbinutil + + if shutil.which("xclbinutil") is None: + raise RuntimeError( + "xclbinutil not in PATH or not installed.\ + Required to read kernel names for driver config!" + ) + run_command( + f"xclbinutil -i {xclbin_path} --dump-section IP_LAYOUT:JSON:ip_layout.json --force", + cwd=os.path.dirname(xclbin_path), + ) + ips = None + with open(os.path.join(os.path.dirname(xclbin_path), "ip_layout.json")) as f: + ips = json.loads(f.read())["ip_layout"]["m_ip_data"] + + # Get only ips that are kernels + isIO = ( + lambda x: x["m_type"] == "IP_KERNEL" + and x["m_base_address"] != "not_used" + and ("idma" in x["m_name"] or "odma" in x["m_name"]) + ) + idmas = [x["m_name"] for x in ips if isIO(x) and "idma" in x["m_name"]] + odmas = [x["m_name"] for x in ips if isIO(x) and "odma" in x["m_name"]] + + def formatKernelName(kname: str): + kparts = kname.split(":") + return kparts[0] + ":{" + kparts[1] + "}" + + # Create idma and odma entries + jsonIdmas = [] + jsonOdmas = [] + for i in range(len(driver_shapes["idma_names"])): + jsonIdmas.append( + { + "kernelName": [ + formatKernelName(name) + for name in idmas + if driver_shapes["idma_names"][i] in name + ][0], + "normalShape": driver_shapes["ishape_normal"][i], + "foldedShape": driver_shapes["ishape_folded"][i], + "packedShape": driver_shapes["ishape_packed"][i], + } + ) + for i in range(len(driver_shapes["odma_names"])): + jsonOdmas.append( + { + "kernelName": [ + formatKernelName(name) + for name in odmas + if driver_shapes["odma_names"][i] in name + ][0], + "normalShape": driver_shapes["oshape_normal"][i], + "foldedShape": driver_shapes["oshape_folded"][i], + "packedShape": driver_shapes["oshape_packed"][i], + } + ) + + data = [] + data.append( + { + "xrtDeviceIndex": 0, + "xclbinPath": os.path.abspath(xclbin_path), + "name": "MainDevice", + "idmas": jsonIdmas, + "odmas": jsonOdmas, + } + ) + with open(json_path, "w+") as f: + f.write(json.dumps(data, indent=4)) + + return (model, False) + + +class MakePYNQDriver(Transformation): + """Create PYNQ Python code to correctly interface the generated + accelerator, including data packing/unpacking. Should be called + after conversion to HLS layers, folding and the creation of + dataflow partitions for correct operation. + + platform: one of ["zynq-iodma", "alveo"] + + Outcome if successful: sets the pynq_driver_dir attribute in the ONNX + ModelProto's metadata_props field, with the created driver dir as the + value. If any layers use runtime-writable parameters, those will be gathered + under the runtime_weights/ subfolder of the pynq_driver_dir. + """ + + def __init__(self, platform): + super().__init__() + self.platform = platform + + def apply(self, model): + # create a temporary folder for the generated driver + pynq_driver_dir = make_build_dir(prefix="pynq_driver_") + model.set_metadata_prop("pynq_driver_dir", pynq_driver_dir) + + # create the base FINN driver -- same for all accels + driver_base_template = ( + os.environ["FINN_ROOT"] + "/src/finn/qnn-data/templates/driver/driver_base.py" + ) + driver_base_py = pynq_driver_dir + "/driver_base.py" + shutil.copy(driver_base_template, driver_base_py) + # driver depends on qonnx and finn packages + # extract individual source files and copy to driver folder + qonnx_target_path = pynq_driver_dir + "/qonnx" + finn_target_path = pynq_driver_dir + "/finn" + os.makedirs(qonnx_target_path + "/core", exist_ok=True) + os.makedirs(qonnx_target_path + "/util", exist_ok=True) + os.makedirs(finn_target_path + "/util", exist_ok=True) + qonnx_path = qonnx.__path__[0] + finn_util_path = finn.util.__path__[0] + files_to_copy = [] + files_to_copy.append( + (qonnx_path + "/core/datatype.py", qonnx_target_path + "/core/datatype.py") + ) + files_to_copy.append( + (qonnx_path + "/core/__init__.py", qonnx_target_path + "/core/__init__.py") + ) + files_to_copy.append((qonnx_path + "/util/basic.py", qonnx_target_path + "/util/basic.py")) + files_to_copy.append( + (qonnx_path + "/util/__init__.py", qonnx_target_path + "/util/__init__.py") + ) + files_to_copy.append( + ( + finn_util_path + "/data_packing.py", + finn_target_path + "/util/data_packing.py", + ) + ) + files_to_copy.append( + ( + finn_util_path + "/__init__.py", + finn_target_path + "/util/__init__.py", + ) + ) + for src_file, target_file in files_to_copy: + shutil.copy(src_file, target_file) + + driver_shapes: Dict = get_driver_shapes(model) + + # generate external weights npy files + weights_dir = pynq_driver_dir + "/runtime_weights" + + os.makedirs(weights_dir) + idma_idx = 0 + ext_weight_dma_cnt = 0 + + for node in model.graph.node: + assert ( + node.op_type == "StreamingDataflowPartition" + ), "CreateDataflowPartition needs to be applied before driver generation" + + if len(node.input) > 0: + producer = model.find_producer(node.input[0]) + init_tensor = model.get_initializer(node.input[0]) + else: + producer = None + init_tensor = None + + if producer is None: # input dma? + sdp_inst = getCustomOp(node) + idma_name = sdp_inst.get_nodeattr("instance_name") + df_model = ModelWrapper(sdp_inst.get_nodeattr("model")) + assert df_model.graph.node[0].op_type == "IODMA_hls" + iodma_node = getCustomOp(df_model.graph.node[0]) + if iodma_node.get_nodeattr("burstMode") == "wrap": # input weights dma? + init_tensor = df_model.get_initializer(iodma_node.onnx_node.input[0]) + ext_weight_dma_cnt += 1 + w_dtype = df_model.get_tensor_datatype(iodma_node.onnx_node.input[0]) + init_external_tensor = to_external_tensor(init_tensor, w_dtype) + np.save(weights_dir + "/" + idma_name + ".npy", init_external_tensor) + idma_idx += 1 + + # fill in the driver template + driver_py = pynq_driver_dir + "/driver.py" + driver = template_driver.pynq_driver_template + + driver = driver.replace("$PLATFORM$", self.platform) + driver = driver.replace("$INPUT_FINN_DATATYPE$", str(driver_shapes["idt"]).replace('"', "")) + driver = driver.replace("$INPUT_SHAPE_NORMAL$", str(driver_shapes["ishape_normal"])) + driver = driver.replace("$INPUT_SHAPE_FOLDED$", str(driver_shapes["ishape_folded"])) + driver = driver.replace("$INPUT_SHAPE_PACKED$", str(driver_shapes["ishape_packed"])) + driver = driver.replace( + "$OUTPUT_FINN_DATATYPE$", str(driver_shapes["odt"]).replace('"', "") + ) + driver = driver.replace("$OUTPUT_SHAPE_NORMAL$", str(driver_shapes["oshape_normal"])) + driver = driver.replace("$OUTPUT_SHAPE_FOLDED$", str(driver_shapes["oshape_folded"])) + driver = driver.replace("$OUTPUT_SHAPE_PACKED$", str(driver_shapes["oshape_packed"])) + driver = driver.replace("$INPUT_DMA_NAME$", "%s" % str(driver_shapes["idma_names"])) + driver = driver.replace("$OUTPUT_DMA_NAME$", "%s" % str(driver_shapes["odma_names"])) + driver = driver.replace("$NUM_INPUTS$", str(len(driver_shapes["idma_names"]))) + driver = driver.replace("$NUM_OUTPUTS$", str(len(driver_shapes["odma_names"]))) + driver = driver.replace("$EXT_WEIGHT_NUM$", str(ext_weight_dma_cnt)) + + with open(driver_py, "w") as f: + f.write(driver) + + # add validate.py to run full top-1 test (only for suitable networks) + validate_py = pynq_driver_dir + "/validate.py" + validate_template = ( + os.environ["FINN_ROOT"] + "/src/finn/qnn-data/templates/driver/validate.py" + ) + shutil.copy(validate_template, validate_py) + + # generate weight files for runtime-writable layers + + for sdp_ind, sdp_node in enumerate(model.graph.node): + assert sdp_node.op_type == "StreamingDataflowPartition" + # get dataflow model + sdp_node = getCustomOp(sdp_node) + dataflow_model_filename = sdp_node.get_nodeattr("model") + dataflow_model = ModelWrapper(dataflow_model_filename) + rt_layer_ind = 0 + for node in dataflow_model.graph.node: + if node.op_type.startswith("MVAU") or node.op_type.startswith("Thresholding"): + node_inst = getCustomOp(node) + is_rt_weights = node_inst.get_nodeattr("runtime_writeable_weights") + if is_rt_weights == 1: + fcl_w = dataflow_model.get_initializer(node.input[1]) + w_filename = weights_dir + "/%d_%d_%s.dat" % ( + sdp_ind, + rt_layer_ind, + node.name, + ) + node_inst.make_weight_file(fcl_w, "decoupled_runtime", w_filename) + rt_layer_ind += 1 + elif node.op_type == "StreamingDataflowPartition": + warnings.warn( + """Nested StreamingDataflowPartition are not supported + """ + ) + else: + continue + + return (model, False) diff --git a/src/finn/transformation/fpgadataflow/make_pynq_driver.py b/src/finn/transformation/fpgadataflow/make_pynq_driver.py deleted file mode 100644 index ea9bd2aa26..0000000000 --- a/src/finn/transformation/fpgadataflow/make_pynq_driver.py +++ /dev/null @@ -1,305 +0,0 @@ -# Copyright (c) 2020, Xilinx -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of FINN nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import numpy as np -import os -import qonnx -import shutil -import warnings -from qonnx.core.modelwrapper import ModelWrapper -from qonnx.custom_op.registry import getCustomOp -from qonnx.transformation.base import Transformation -from qonnx.util.basic import gen_finn_dt_tensor, roundup_to_integer_multiple - -import finn.util -import finn.util.data_packing as dpk -from finn.util.basic import make_build_dir -from finn.util.data_packing import ( - hexstring2npbytearray, - pack_innermost_dim_as_hex_string, -) - -from . import template_driver - - -def to_external_tensor(init, w_dtype): - """Return an appropriately formatted and packed numpy byte array for given - external parameter tensor.""" - - weight_width = init.shape[1] * w_dtype.bitwidth() - weight_width_padded = roundup_to_integer_multiple(weight_width, 4) - hex_init = pack_innermost_dim_as_hex_string(init, w_dtype, weight_width_padded, prefix="0x") - ext_weight = np.array([], dtype=np.uint8) - for line in hex_init: - array_line = [x for x in reversed(hexstring2npbytearray(line, remove_prefix="0x"))] - ext_weight = np.append(ext_weight, array_line) - - return ext_weight - - -class MakePYNQDriver(Transformation): - """Create PYNQ Python code to correctly interface the generated - accelerator, including data packing/unpacking. Should be called - after conversion to HLS layers, folding and the creation of - dataflow partitions for correct operation. - - platform: one of ["zynq-iodma", "alveo"] - - Outcome if successful: sets the pynq_driver_dir attribute in the ONNX - ModelProto's metadata_props field, with the created driver dir as the - value. If any layers use runtime-writable parameters, those will be gathered - under the runtime_weights/ subfolder of the pynq_driver_dir. - """ - - def __init__(self, platform): - super().__init__() - self.platform = platform - - def apply(self, model): - # create a temporary folder for the generated driver - pynq_driver_dir = make_build_dir(prefix="pynq_driver_") - model.set_metadata_prop("pynq_driver_dir", pynq_driver_dir) - - # create the base FINN driver -- same for all accels - driver_base_template = ( - os.environ["FINN_ROOT"] + "/src/finn/qnn-data/templates/driver/driver_base.py" - ) - driver_base_py = pynq_driver_dir + "/driver_base.py" - shutil.copy(driver_base_template, driver_base_py) - # driver depends on qonnx and finn packages - # extract individual source files and copy to driver folder - qonnx_target_path = pynq_driver_dir + "/qonnx" - finn_target_path = pynq_driver_dir + "/finn" - os.makedirs(qonnx_target_path + "/core", exist_ok=True) - os.makedirs(qonnx_target_path + "/util", exist_ok=True) - os.makedirs(finn_target_path + "/util", exist_ok=True) - qonnx_path = qonnx.__path__[0] - finn_util_path = finn.util.__path__[0] - files_to_copy = [] - files_to_copy.append( - (qonnx_path + "/core/datatype.py", qonnx_target_path + "/core/datatype.py") - ) - files_to_copy.append( - (qonnx_path + "/core/__init__.py", qonnx_target_path + "/core/__init__.py") - ) - files_to_copy.append((qonnx_path + "/util/basic.py", qonnx_target_path + "/util/basic.py")) - files_to_copy.append( - (qonnx_path + "/util/__init__.py", qonnx_target_path + "/util/__init__.py") - ) - files_to_copy.append( - ( - finn_util_path + "/data_packing.py", - finn_target_path + "/util/data_packing.py", - ) - ) - files_to_copy.append( - ( - finn_util_path + "/__init__.py", - finn_target_path + "/util/__init__.py", - ) - ) - for src_file, target_file in files_to_copy: - shutil.copy(src_file, target_file) - # extract input-output shapes from the graph - # TODO convert this to an analysis pass? - idt = [] - idma_names = [] - ishape_normal = [] - ishape_folded = [] - ishape_packed = [] - for idma_ind, graph_in in enumerate(model.graph.input): - i_tensor_name = graph_in.name - # get inp tensor properties - i_tensor_dt = model.get_tensor_datatype(i_tensor_name) - i_tensor_shape_normal = tuple(model.get_tensor_shape(i_tensor_name)) - # go down into dataflow partition to get folded shape info etc - # TODO consider setting these as attributes during dataflow partitioning - i_consumer = model.find_consumer(i_tensor_name) - assert ( - i_consumer.op_type == "StreamingDataflowPartition" - ), """ - Ensure CreateDataflowPartition called before driver creation.""" - first_df_model = ModelWrapper(getCustomOp(i_consumer).get_nodeattr("model")) - assert ( - first_df_model.graph.node[0].op_type == "IODMA_hls" - ), "First partition must hold input IODMA" - successors = model.find_direct_successors(i_consumer) - successor_input_num = list(successors[0].input).index(i_consumer.output[0]) - successor_sdp = getCustomOp(successors[0]) - successor_df_model = ModelWrapper(successor_sdp.get_nodeattr("model")) - first_node = successor_df_model.find_consumer( - successor_df_model.graph.input[successor_input_num].name - ) - i_tensor_shape_folded = tuple(getCustomOp(first_node).get_folded_input_shape()) - # generate dummy folded i/o tensors and their packed versions - i_tensor_dummy_folded = gen_finn_dt_tensor(i_tensor_dt, i_tensor_shape_folded) - i_tensor_dummy_packed = dpk.finnpy_to_packed_bytearray( - i_tensor_dummy_folded, i_tensor_dt - ) - i_tensor_shape_packed = i_tensor_dummy_packed.shape - # append all input tensor info to relevant lists - idt.append("DataType['%s']" % i_tensor_dt.name) - ishape_normal.append(i_tensor_shape_normal) - ishape_folded.append(i_tensor_shape_folded) - ishape_packed.append(i_tensor_shape_packed) - idma_names.append(getCustomOp(i_consumer).get_nodeattr("instance_name")) - - odt = [] - odma_names = [] - oshape_normal = [] - oshape_folded = [] - oshape_packed = [] - for odma_ind, graph_out in enumerate(model.graph.output): - o_tensor_name = graph_out.name - # get inp tensor properties - o_tensor_dt = model.get_tensor_datatype(o_tensor_name) - o_tensor_shape_normal = tuple(model.get_tensor_shape(o_tensor_name)) - # go down into IODMA partition to get folded shape info etc - # TODO consider setting these as attributes during dataflow partitioning - o_producer = model.find_producer(o_tensor_name) - assert ( - o_producer.op_type == "StreamingDataflowPartition" - ), """ - Ensure CreateDataflowPartition called before driver creation.""" - df_model = ModelWrapper(getCustomOp(o_producer).get_nodeattr("model")) - assert ( - df_model.graph.node[-1].op_type == "IODMA_hls" - ), "Partition must hold output IODMA" - predecessors = model.find_direct_predecessors(o_producer) - predecessor_output_num = list(predecessors[0].output).index(o_producer.input[0]) - predecessor_sdp = getCustomOp(predecessors[0]) - predecessor_df_model = ModelWrapper(predecessor_sdp.get_nodeattr("model")) - last_node = predecessor_df_model.find_producer( - predecessor_df_model.graph.output[predecessor_output_num].name - ) - o_tensor_shape_folded = tuple(getCustomOp(last_node).get_folded_output_shape()) - o_tensor_dummy_folded = gen_finn_dt_tensor(o_tensor_dt, o_tensor_shape_folded) - o_tensor_dummy_packed = dpk.finnpy_to_packed_bytearray( - o_tensor_dummy_folded, o_tensor_dt - ) - o_tensor_shape_packed = o_tensor_dummy_packed.shape - # append all output tensor info to relevant lists - odt.append("DataType['%s']" % o_tensor_dt.name) - oshape_normal.append(o_tensor_shape_normal) - oshape_folded.append(o_tensor_shape_folded) - oshape_packed.append(o_tensor_shape_packed) - odma_names.append(getCustomOp(o_producer).get_nodeattr("instance_name")) - - # generate external weights npy files - weights_dir = pynq_driver_dir + "/runtime_weights" - - os.makedirs(weights_dir) - idma_idx = 0 - ext_weight_dma_cnt = 0 - - for node in model.graph.node: - assert ( - node.op_type == "StreamingDataflowPartition" - ), "CreateDataflowPartition needs to be applied before driver generation" - - if len(node.input) > 0: - producer = model.find_producer(node.input[0]) - init_tensor = model.get_initializer(node.input[0]) - else: - producer = None - init_tensor = None - - if producer is None: # input dma? - sdp_inst = getCustomOp(node) - idma_name = sdp_inst.get_nodeattr("instance_name") - df_model = ModelWrapper(sdp_inst.get_nodeattr("model")) - assert df_model.graph.node[0].op_type == "IODMA_hls" - iodma_node = getCustomOp(df_model.graph.node[0]) - if iodma_node.get_nodeattr("burstMode") == "wrap": # input weights dma? - init_tensor = df_model.get_initializer(iodma_node.onnx_node.input[0]) - ext_weight_dma_cnt += 1 - w_dtype = df_model.get_tensor_datatype(iodma_node.onnx_node.input[0]) - init_external_tensor = to_external_tensor(init_tensor, w_dtype) - np.save(weights_dir + "/" + idma_name + ".npy", init_external_tensor) - idma_idx += 1 - - # fill in the driver template - driver_py = pynq_driver_dir + "/driver.py" - driver = template_driver.pynq_driver_template - - driver = driver.replace("$PLATFORM$", self.platform) - driver = driver.replace("$INPUT_FINN_DATATYPE$", str(idt).replace('"', "")) - driver = driver.replace("$INPUT_SHAPE_NORMAL$", str(ishape_normal)) - driver = driver.replace("$INPUT_SHAPE_FOLDED$", str(ishape_folded)) - driver = driver.replace("$INPUT_SHAPE_PACKED$", str(ishape_packed)) - driver = driver.replace("$OUTPUT_FINN_DATATYPE$", str(odt).replace('"', "")) - driver = driver.replace("$OUTPUT_SHAPE_NORMAL$", str(oshape_normal)) - driver = driver.replace("$OUTPUT_SHAPE_FOLDED$", str(oshape_folded)) - driver = driver.replace("$OUTPUT_SHAPE_PACKED$", str(oshape_packed)) - driver = driver.replace("$INPUT_DMA_NAME$", "%s" % str(idma_names)) - driver = driver.replace("$OUTPUT_DMA_NAME$", "%s" % str(odma_names)) - driver = driver.replace("$NUM_INPUTS$", str(len(idma_names))) - driver = driver.replace("$NUM_OUTPUTS$", str(len(odma_names))) - driver = driver.replace("$EXT_WEIGHT_NUM$", str(ext_weight_dma_cnt)) - - with open(driver_py, "w") as f: - f.write(driver) - - # add validate.py to run full top-1 test (only for suitable networks) - validate_py = pynq_driver_dir + "/validate.py" - validate_template = ( - os.environ["FINN_ROOT"] + "/src/finn/qnn-data/templates/driver/validate.py" - ) - shutil.copy(validate_template, validate_py) - - # generate weight files for runtime-writable layers - - for sdp_ind, sdp_node in enumerate(model.graph.node): - assert sdp_node.op_type == "StreamingDataflowPartition" - # get dataflow model - sdp_node = getCustomOp(sdp_node) - dataflow_model_filename = sdp_node.get_nodeattr("model") - dataflow_model = ModelWrapper(dataflow_model_filename) - rt_layer_ind = 0 - for node in dataflow_model.graph.node: - if node.op_type.startswith("MVAU") or node.op_type.startswith("Thresholding"): - node_inst = getCustomOp(node) - is_rt_weights = node_inst.get_nodeattr("runtime_writeable_weights") - if is_rt_weights == 1: - fcl_w = dataflow_model.get_initializer(node.input[1]) - w_filename = weights_dir + "/%d_%d_%s.dat" % ( - sdp_ind, - rt_layer_ind, - node.name, - ) - node_inst.make_weight_file(fcl_w, "decoupled_runtime", w_filename) - rt_layer_ind += 1 - elif node.op_type == "StreamingDataflowPartition": - warnings.warn( - """Nested StreamingDataflowPartition are not supported - """ - ) - else: - continue - - return (model, False) diff --git a/src/finn/transformation/fpgadataflow/vitis_build.py b/src/finn/transformation/fpgadataflow/vitis_build.py index 157d81cf35..fe63a97617 100644 --- a/src/finn/transformation/fpgadataflow/vitis_build.py +++ b/src/finn/transformation/fpgadataflow/vitis_build.py @@ -189,6 +189,7 @@ def apply(self, model): object_files = [] idma_idx = 0 odma_idx = 0 + mem_idx = 0 instance_names = {} for node in model.graph.node: assert node.op_type == "StreamingDataflowPartition", "Invalid link graph" @@ -229,18 +230,20 @@ def apply(self, model): if node_slr != -1: config.append("slr=%s:SLR%d" % (instance_names[node.name], node_slr)) # assign memory banks - if producer is None or consumer is None: + if producer is None or consumer is None or consumer == []: node_mem_port = sdp_node.get_nodeattr("mem_port") if node_mem_port == "": # configure good defaults based on board if "u50" in self.platform or "u280" in self.platform or "u55c" in self.platform: # Use HBM where available (also U50 does not have DDR) mem_type = "HBM" - mem_idx = 0 + node_mem_port = "%s[%d]" % (mem_type, mem_idx) + # mem_idx += 1 elif "u200" in self.platform: # Use DDR controller in static region of U200 mem_type = "DDR" mem_idx = 1 + node_mem_port = "%s[%d]" % (mem_type, mem_idx) elif "u250" in self.platform: # Use DDR controller on the node's SLR if set, otherwise 0 mem_type = "DDR" @@ -248,10 +251,11 @@ def apply(self, model): mem_idx = 0 else: mem_idx = node_slr + node_mem_port = "%s[%d]" % (mem_type, mem_idx) else: mem_type = "DDR" mem_idx = 1 - node_mem_port = "%s[%d]" % (mem_type, mem_idx) + node_mem_port = "%s[%d]" % (mem_type, mem_idx) config.append("sp=%s.m_axi_gmem0:%s" % (instance_names[node.name], node_mem_port)) # connect streams if producer is not None: diff --git a/src/finn/util/data_packing.py b/src/finn/util/data_packing.py index 6a72d38058..61773d29b4 100644 --- a/src/finn/util/data_packing.py +++ b/src/finn/util/data_packing.py @@ -1,4 +1,5 @@ -# Copyright (c) 2020 Xilinx, Inc. +# Copyright (C) 2020 Xilinx, Inc. +# Copyright (C) 2025, Advanced Micro Devices, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -32,7 +33,10 @@ import sys from bitstring import BitArray from qonnx.core.datatype import DataType -from qonnx.util.basic import roundup_to_integer_multiple +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.custom_op.registry import getCustomOp +from qonnx.util.basic import gen_finn_dt_tensor, roundup_to_integer_multiple +from typing import Dict def array2hexstring(array, dtype, pad_to_nbits, prefix="0x", reverse=False): @@ -449,3 +453,110 @@ def packed_bytearray_to_finnpy( ) return ret + + +def to_external_tensor(init, w_dtype): + """Return an appropriately formatted and packed numpy byte array for given + external parameter tensor.""" + + weight_width = init.shape[1] * w_dtype.bitwidth() + weight_width_padded = roundup_to_integer_multiple(weight_width, 4) + hex_init = pack_innermost_dim_as_hex_string(init, w_dtype, weight_width_padded, prefix="0x") + ext_weight = np.array([], dtype=np.uint8) + for line in hex_init: + array_line = [x for x in reversed(hexstring2npbytearray(line, remove_prefix="0x"))] + ext_weight = np.append(ext_weight, array_line) + + return ext_weight + + +def get_driver_shapes(model: ModelWrapper) -> Dict: + idt = [] + idma_names = [] + ishape_normal = [] + ishape_folded = [] + ishape_packed = [] + for idma_ind, graph_in in enumerate(model.graph.input): + i_tensor_name = graph_in.name + # get inp tensor properties + i_tensor_dt = model.get_tensor_datatype(i_tensor_name) + i_tensor_shape_normal = tuple(model.get_tensor_shape(i_tensor_name)) + # go down into dataflow partition to get folded shape info etc + # TODO consider setting these as attributes during dataflow partitioning + i_consumer = model.find_consumer(i_tensor_name) + assert ( + i_consumer.op_type == "StreamingDataflowPartition" + ), """ + Ensure CreateDataflowPartition called before driver creation.""" + first_df_model = ModelWrapper(getCustomOp(i_consumer).get_nodeattr("model")) + assert ( + first_df_model.graph.node[0].op_type == "IODMA_hls" + ), "First partition must hold input IODMA" + successors = model.find_direct_successors(i_consumer) + successor_input_num = list(successors[0].input).index(i_consumer.output[0]) + successor_sdp = getCustomOp(successors[0]) + successor_df_model = ModelWrapper(successor_sdp.get_nodeattr("model")) + first_node = successor_df_model.find_consumer( + successor_df_model.graph.input[successor_input_num].name + ) + i_tensor_shape_folded = tuple(getCustomOp(first_node).get_folded_input_shape()) + # generate dummy folded i/o tensors and their packed versions + i_tensor_dummy_folded = gen_finn_dt_tensor(i_tensor_dt, i_tensor_shape_folded) + i_tensor_dummy_packed = finnpy_to_packed_bytearray(i_tensor_dummy_folded, i_tensor_dt) + i_tensor_shape_packed = i_tensor_dummy_packed.shape + # append all input tensor info to relevant lists + idt.append("DataType['%s']" % i_tensor_dt.name) + ishape_normal.append(i_tensor_shape_normal) + ishape_folded.append(i_tensor_shape_folded) + ishape_packed.append(i_tensor_shape_packed) + idma_names.append(getCustomOp(i_consumer).get_nodeattr("instance_name")) + + odt = [] + odma_names = [] + oshape_normal = [] + oshape_folded = [] + oshape_packed = [] + for odma_ind, graph_out in enumerate(model.graph.output): + o_tensor_name = graph_out.name + # get inp tensor properties + o_tensor_dt = model.get_tensor_datatype(o_tensor_name) + o_tensor_shape_normal = tuple(model.get_tensor_shape(o_tensor_name)) + # go down into IODMA partition to get folded shape info etc + # TODO consider setting these as attributes during dataflow partitioning + o_producer = model.find_producer(o_tensor_name) + assert ( + o_producer.op_type == "StreamingDataflowPartition" + ), """ + Ensure CreateDataflowPartition called before driver creation.""" + df_model = ModelWrapper(getCustomOp(o_producer).get_nodeattr("model")) + assert df_model.graph.node[-1].op_type == "IODMA_hls", "Partition must hold output IODMA" + predecessors = model.find_direct_predecessors(o_producer) + predecessor_output_num = list(predecessors[0].output).index(o_producer.input[0]) + predecessor_sdp = getCustomOp(predecessors[0]) + predecessor_df_model = ModelWrapper(predecessor_sdp.get_nodeattr("model")) + last_node = predecessor_df_model.find_producer( + predecessor_df_model.graph.output[predecessor_output_num].name + ) + o_tensor_shape_folded = tuple(getCustomOp(last_node).get_folded_output_shape()) + o_tensor_dummy_folded = gen_finn_dt_tensor(o_tensor_dt, o_tensor_shape_folded) + o_tensor_dummy_packed = finnpy_to_packed_bytearray(o_tensor_dummy_folded, o_tensor_dt) + o_tensor_shape_packed = o_tensor_dummy_packed.shape + # append all output tensor info to relevant lists + odt.append("DataType['%s']" % o_tensor_dt.name) + oshape_normal.append(o_tensor_shape_normal) + oshape_folded.append(o_tensor_shape_folded) + oshape_packed.append(o_tensor_shape_packed) + odma_names.append(getCustomOp(o_producer).get_nodeattr("instance_name")) + + return { + "idt": idt, + "idma_names": idma_names, + "ishape_normal": ishape_normal, + "ishape_folded": ishape_folded, + "ishape_packed": ishape_packed, + "odt": odt, + "odma_names": odma_names, + "oshape_normal": oshape_normal, + "oshape_folded": oshape_folded, + "oshape_packed": oshape_packed, + } diff --git a/tests/end2end/test_end2end_bnn_pynq.py b/tests/end2end/test_end2end_bnn_pynq.py index 03303e2647..0b28e2d20b 100644 --- a/tests/end2end/test_end2end_bnn_pynq.py +++ b/tests/end2end/test_end2end_bnn_pynq.py @@ -74,7 +74,7 @@ from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.insert_dwc import InsertDWC -from finn.transformation.fpgadataflow.make_pynq_driver import MakePYNQDriver +from finn.transformation.fpgadataflow.make_driver import MakeCPPDriver, MakePYNQDriver from finn.transformation.fpgadataflow.minimize_accumulator_width import ( MinimizeAccumulatorWidth, ) @@ -356,8 +356,13 @@ def deploy_based_on_board(model, model_title, topology, wbits, abits, board): # driver.py and python libraries pynq_driver_dir = model.get_metadata_prop("pynq_driver_dir") - copytree(pynq_driver_dir, deployment_dir, dirs_exist_ok=True) - model.set_metadata_prop("pynq_deploy_dir", deployment_dir) + if not None: + copytree(pynq_driver_dir, deployment_dir, dirs_exist_ok=True) + model.set_metadata_prop("pynq_deploy_dir", deployment_dir) + else: + cpp_driver_dir = model.get_metadata_prop("cpp_driver_dir") + copytree(cpp_driver_dir, deployment_dir, dirs_exist_ok=True) + model.set_metadata_prop("cpp_deploy_dir", deployment_dir) # parameters that make up inputs to test case(s) @@ -811,14 +816,17 @@ def test_build(self, topology, wbits, abits, board): @pytest.mark.slow @pytest.mark.vivado @pytest.mark.vitis - def test_make_pynq_driver(self, topology, wbits, abits, board): + def test_make_driver(self, topology, wbits, abits, board): build_data = get_build_env(board, target_clk_ns) if build_data["kind"] == "alveo" and ("VITIS_PATH" not in os.environ): pytest.skip("VITIS_PATH not set") prev_chkpt_name = get_checkpoint_name(board, topology, wbits, abits, "build") model = load_test_checkpoint_or_skip(prev_chkpt_name) board_to_driver_platform = "alveo" if build_data["kind"] == "alveo" else "zynq-iodma" - model = model.transform(MakePYNQDriver(board_to_driver_platform)) + if build_data["kind"] == "alveo" and topology == "tfc": + model = model.transform(MakeCPPDriver(board_to_driver_platform, version="latest")) + else: + model = model.transform(MakePYNQDriver(board_to_driver_platform)) model.save(get_checkpoint_name(board, topology, wbits, abits, "driver")) def test_deploy(self, topology, wbits, abits, board): diff --git a/tutorials/fpga_flow/README.md b/tutorials/fpga_flow/README.md index 71f2a2a625..1fcda7f265 100644 --- a/tutorials/fpga_flow/README.md +++ b/tutorials/fpga_flow/README.md @@ -57,7 +57,7 @@ The build should finish in about 10 minutes, and the FINN docker will close on s Running step: step_measure_rtlsim_performance [13/18] Running step: step_out_of_context_synthesis [14/18] Running step: step_synthesize_bitfile [15/18] - Running step: step_make_pynq_driver [16/18] + Running step: step_make_driver [16/18] Running step: step_deployment_package [17/18] Running step: custom_step_gen_tb_and_io [18/18] Completed successfully