diff --git a/notebooks/internals/0_custom_analysis_pass.ipynb b/notebooks/internals/0_custom_analysis_pass.ipynb index 3db1d1c47acef301f7b89a05980aa68477ba567f..ce95f1297ff92cf8c19287ef4d564ba40e0ba6b1 100644 --- a/notebooks/internals/0_custom_analysis_pass.ipynb +++ b/notebooks/internals/0_custom_analysis_pass.ipynb @@ -51,43 +51,53 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Serving '../LFCW1A1.onnx' at http://0.0.0.0:8081\n" - ] - } - ], + "outputs": [], "source": [ "import netron\n", - "netron.start('../LFCW1A1.onnx', port=8081, host=\"0.0.0.0\")" + "from IPython.display import IFrame\n", + "def showInNetron(model_filename):\n", + " netron.start(model_filename, port=8081, host=\"0.0.0.0\")\n", + " return IFrame(src=\"http://0.0.0.0:8081/\", width=\"100%\", height=400)" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Serving '../LFCW1A1.onnx' at http://0.0.0.0:8081\n" + ] + }, { "data": { "text/html": [ - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>\n" + "\n", + " <iframe\n", + " width=\"100%\"\n", + " height=\"400\"\n", + " src=\"http://0.0.0.0:8081/\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " ></iframe>\n", + " " ], "text/plain": [ - "<IPython.core.display.HTML object>" + "<IPython.lib.display.IFrame at 0x7f34317f1278>" ] }, + "execution_count": 3, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" } ], "source": [ - "%%html\n", - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>" + "showInNetron(\"../LFCW1A1.onnx\")" ] }, { @@ -99,7 +109,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -116,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -146,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -173,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -187,13 +197,6 @@ "source": [ "print(model.analysis(count_equal_nodes))" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/notebooks/internals/1_custom_transformation_pass.ipynb b/notebooks/internals/1_custom_transformation_pass.ipynb index f0405c0db3b02cf19476ed6cc8d293a93df00b30..8e19bc7a090815a028bdd4ac1b971d0cc62f085c 100644 --- a/notebooks/internals/1_custom_transformation_pass.ipynb +++ b/notebooks/internals/1_custom_transformation_pass.ipynb @@ -55,7 +55,7 @@ " def transform(self, transformation, make_deepcopy=True):\n", " \"\"\"Applies given Transformation repeatedly until no more changes can be made\n", " and returns a transformed ModelWrapper instance.\n", - " \n", + "\n", " If make_deepcopy is specified, operates on a new (deep)copy of model.\n", " \"\"\"\n", " transformed_model = self\n", @@ -110,8 +110,9 @@ "output_type": "stream", "text": [ "class Transformation(ABC):\n", - " \"\"\"Transformation class all transformations are based on. Contains only \n", + " \"\"\"Transformation class all transformations are based on. Contains only\n", " abstract method apply() every transformation has to fill.\"\"\"\n", + "\n", " def __init__(self):\n", " super().__init__()\n", "\n", @@ -162,18 +163,13 @@ "cell_type": "code", "execution_count": 5, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Serving '../LFCW1A1.onnx' at http://0.0.0.0:8081\n" - ] - } - ], + "outputs": [], "source": [ "import netron\n", - "netron.start('../LFCW1A1.onnx', port=8081, host=\"0.0.0.0\")" + "from IPython.display import IFrame\n", + "def showInNetron(model_filename):\n", + " netron.start(model_filename, port=8081, host=\"0.0.0.0\")\n", + " return IFrame(src=\"http://0.0.0.0:8081/\", width=\"100%\", height=400)" ] }, { @@ -181,22 +177,37 @@ "execution_count": 6, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Serving '../LFCW1A1.onnx' at http://0.0.0.0:8081\n" + ] + }, { "data": { "text/html": [ - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>\n" + "\n", + " <iframe\n", + " width=\"100%\"\n", + " height=\"400\"\n", + " src=\"http://0.0.0.0:8081/\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " ></iframe>\n", + " " ], "text/plain": [ - "<IPython.core.display.HTML object>" + "<IPython.lib.display.IFrame at 0x7f57cafc5c88>" ] }, + "execution_count": 6, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" } ], "source": [ - "%%html\n", - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>" + "showInNetron('../LFCW1A1.onnx')" ] }, { @@ -248,7 +259,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -259,41 +270,32 @@ "Stopping http://0.0.0.0:8081\n", "Serving 'LFCW1A1_changed.onnx' at http://0.0.0.0:8081\n" ] - } - ], - "source": [ - "netron.start('LFCW1A1_changed.onnx', port=8081, host=\"0.0.0.0\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ + }, { "data": { "text/html": [ - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>\n" + "\n", + " <iframe\n", + " width=\"100%\"\n", + " height=\"400\"\n", + " src=\"http://0.0.0.0:8081/\"\n", + " frameborder=\"0\"\n", + " allowfullscreen\n", + " ></iframe>\n", + " " ], "text/plain": [ - "<IPython.core.display.HTML object>" + "<IPython.lib.display.IFrame at 0x7f57e15781d0>" ] }, + "execution_count": 10, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" } ], "source": [ - "%%html\n", - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>" + "showInNetron('LFCW1A1_changed.onnx')" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/notebooks/internals/2_custom_op.ipynb b/notebooks/internals/2_custom_op.ipynb deleted file mode 100644 index 9aaef9d42ccde42a8f3a0213f1c287a8d72c164a..0000000000000000000000000000000000000000 --- a/notebooks/internals/2_custom_op.ipynb +++ /dev/null @@ -1,982 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# FINN - CustomOps\n", - "-----------------------------------------------------------------\n", - "<font size=\"3\">This notebook should give a more detailed insight into FINN custom operation nodes. </font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">Following showSrc function is used to print the source code of function calls in the Jupyter notebook: </font>" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import inspect\n", - "\n", - "def showSrc(what):\n", - " print(\"\".join(inspect.getsourcelines(what)[0]))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">FINN uses many custom operations (`op_type` in ONNX NodeProto) that are not defined in the ONNX operator schema. These custom nodes are marked with `domain=\"finn\"` in the protobuf to identify them as such. These nodes can represent specific operations that we need for low-bit networks, or operations that are specific to a particular hardware backend.\n", - "\n", - "A very abstract version of a custom op node representing a streaming fc layer is shown below. </font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Outline\n", - "---------------------------\n", - "* <font size=\"3\">Basic FINN-ONNX node</font>\n", - "* <font size=\"3\">CustomOp class</font>\n", - "* <font size=\"3\">HLS FINN-ONNX node</font>\n", - "* <font size=\"3\">HLSCustomOp class</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Basic FINN-ONNX node\n", - "\n", - "<font size=\"3\">To create a FINN-ONNX node you can use the helper function of ONNX. Because it is an ONNX NodeProtobuf, but with several additional attributes. The procedure is shown with an example for a multithreshold node. </font>\n", - "\n", - "`multithreshold_node = helper.make_node(\n", - " \"MultiThreshold\",\n", - " [\"v\", \"thresholds\"],\n", - " [\"out\"],\n", - " domain=\"finn\",\n", - " out_scale=2.0,\n", - " out_bias=-1.0,\n", - " out_dtype=\"\",\n", - ")`\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">The `helper.make_node` function gets the op_type as first argument. In this case it is *MultiThreshold*. Then the inputs and outputs are passed. Beside the data input the multithreshold node has an additional input to pass the threshold values. \n", - "\n", - "The next attribute (`domain`) is to specify that it is a FINN-ONNX node. It must be set to `\"finn\"`, so that the functions that work with FINN-ONNX nodes can directly recognize that it is a CustomOp. The attributes `out_scale` and `out_bias` are special multithreshold attributes to manipulate the output value. `out_dtype` contains the output data type.\n", - " \n", - "**Note**: each FINN-ONNX node has its own special attributes, which must be set correctly to ensure proper processing.</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## CustomOp class\n", - "\n", - "<font size=\"3\">Custom Ops are represented in FINN as ONNX nodes on the one hand and by a CustomOp class on the other hand. This allows easier access to different attributes and introduces special custom op functions. See below for the standard CustomOp class.</font>" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "class CustomOp(ABC):\n", - " \"\"\"CustomOp class all custom op nodes are based on. Contains different functions \n", - " every custom node should have. Some as abstract methods, these have to be filled when\n", - " writing a new custom op node.\"\"\"\n", - " def __init__(self, onnx_node):\n", - " super().__init__()\n", - " self.onnx_node = onnx_node\n", - "\n", - " def get_nodeattr(self, name):\n", - " \"\"\"Get a node attribute by name. Data is stored inside the ONNX node's\n", - " AttributeProto container. Attribute must be part of get_nodeattr_types.\n", - " Default value is returned if attribute is not set.\"\"\"\n", - " try:\n", - " (dtype, req, def_val) = self.get_nodeattr_types()[name]\n", - " attr = get_by_name(self.onnx_node.attribute, name)\n", - " if attr is not None:\n", - " # dtype indicates which ONNX Attribute member to use\n", - " # (such as i, f, s...)\n", - " ret = attr.__getattribute__(dtype)\n", - " if dtype == \"s\":\n", - " # decode string attributes\n", - " ret = ret.decode(\"utf-8\")\n", - " return ret\n", - " else:\n", - " # not set, return default value\n", - " return def_val\n", - " except KeyError:\n", - " raise AttributeError(\"Op has no such attribute: \" + name)\n", - "\n", - " def set_nodeattr(self, name, value):\n", - " \"\"\"Set a node attribute by name. Data is stored inside the ONNX node's\n", - " AttributeProto container. Attribute must be part of get_nodeattr_types.\"\"\"\n", - " try:\n", - " (dtype, req, def_val) = self.get_nodeattr_types()[name]\n", - " attr = get_by_name(self.onnx_node.attribute, name)\n", - " if attr is not None:\n", - " # dtype indicates which ONNX Attribute member to use\n", - " # (such as i, f, s...)\n", - " if dtype == \"s\":\n", - " # encode string attributes\n", - " value = value.encode(\"utf-8\")\n", - " attr.__setattr__(dtype, value)\n", - " else:\n", - " # not set, create and insert AttributeProto\n", - " attr_proto = helper.make_attribute(name, value)\n", - " self.onnx_node.attribute.append(attr_proto)\n", - " except KeyError:\n", - " raise AttributeError(\"Op has no such attribute: \" + name)\n", - "\n", - " @abstractmethod\n", - " def get_nodeattr_types(self):\n", - " \"\"\"Returns a dict of permitted attributes for node, where:\n", - " returned_dict[attribute_name] = (dtype, require, default_value)\n", - " - dtype indicates which member of the ONNX AttributeProto\n", - " will be utilized\n", - " - require indicates whether this attribute is required\n", - " - default_val indicates the default value that will be used if the\n", - " attribute is not set\n", - " \"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def make_shape_compatible_op(self):\n", - " \"\"\"Returns a standard ONNX op which is compatible with this CustomOp\n", - " for performing shape inference.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def infer_node_datatype(self, model):\n", - " \"\"\"Set the DataType annotations corresponding to the outputs of this\n", - " node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def execute_node(self, context, graph):\n", - " \"\"\"Execute this CustomOp instance, given the execution context and\n", - " ONNX graph.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def verify_node(self):\n", - " \"\"\"Verifies that all attributes the node needs are there and\n", - " that particular attributes are set correctly. Also checks if\n", - " the number of inputs is equal to the expected number.\"\"\"\n", - " pass\n", - "\n" - ] - } - ], - "source": [ - "from finn.custom_op import CustomOp\n", - "showSrc(CustomOp)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">When instantiating the class, the ONNX node is passed to access all attributes of the node within the class. This is accompanied by the functions `get_nodeattr()`and `set_nodeattr()`, which each instance of this class has. Furthermore 4 abstract methods are implemented, which are described in more detail in the commands of the code and will be exemplarily explained for the multithreshold node in the following. </font>" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "class MultiThreshold(CustomOp):\n", - " \"\"\"Class that corresponds to a multithresholding node.\"\"\"\n", - " def get_nodeattr_types(self):\n", - " return {\n", - " \"out_dtype\": (\"s\", True, \"\"),\n", - " \"out_scale\": (\"f\", False, 1.0),\n", - " \"out_bias\": (\"f\", False, 0.0),\n", - " }\n", - "\n", - " def make_shape_compatible_op(self):\n", - " node = self.onnx_node\n", - " return helper.make_node(\"Relu\", [node.input[0]], [node.output[0]])\n", - "\n", - " def infer_node_datatype(self, model):\n", - " node = self.onnx_node\n", - " odt = self.get_nodeattr(\"out_dtype\")\n", - " model.set_tensor_datatype(node.output[0], DataType[odt])\n", - "\n", - " def execute_node(self, context, graph):\n", - " node = self.onnx_node\n", - " # save inputs\n", - " v = context[node.input[0]]\n", - " thresholds = context[node.input[1]]\n", - " # retrieve attributes if output scaling is used\n", - " out_scale = self.get_nodeattr(\"out_scale\")\n", - " out_bias = self.get_nodeattr(\"out_bias\")\n", - " # calculate output\n", - " output = multithreshold(v, thresholds, out_scale, out_bias)\n", - " # setting context according to output\n", - " context[node.output[0]] = output\n", - "\n", - " def verify_node(self):\n", - " info_messages = []\n", - "\n", - " # verify number of attributes\n", - " num_of_attr = 3\n", - " if len(self.onnx_node.attribute) == num_of_attr:\n", - " info_messages.append(\"The number of attributes is correct\")\n", - " else:\n", - " info_messages.append(\n", - " \"\"\"The number of attributes is incorrect,\n", - " {} should have {} attributes\"\"\".format(\n", - " self.onnx_node.op_type, num_of_attr\n", - " )\n", - " )\n", - "\n", - " # verify that \"domain\" is set to \"finn\"\n", - " domain_value = self.onnx_node.domain\n", - " if domain_value == \"finn\":\n", - " info_messages.append(\"Attribute domain is set correctly\")\n", - " else:\n", - " info_messages.append('Attribute domain should be set to \"finn\"')\n", - "\n", - " # verify that all necessary attributes exist\n", - " try:\n", - " self.get_nodeattr(\"out_scale\")\n", - " self.get_nodeattr(\"out_bias\")\n", - " self.get_nodeattr(\"out_dtype\")\n", - " info_messages.append(\"All necessary attributes exist\")\n", - " except Exception:\n", - " info_messages.append(\n", - " \"\"\"The necessary attributes do not exist.\n", - " MultiThreshold needs the following attributes:\n", - " out_scale, out_bias, out_dtype\"\"\"\n", - " )\n", - "\n", - " # verify the number of inputs\n", - " if len(self.onnx_node.input) == 2:\n", - " info_messages.append(\"The number of inputs is correct\")\n", - " else:\n", - " info_messages.append(\n", - " \"\"\"MultiThreshold needs 2 inputs\n", - " (data input and threshold values)\"\"\"\n", - " )\n", - "\n", - " return info_messages\n", - "\n" - ] - } - ], - "source": [ - "from finn.custom_op.multithreshold import MultiThreshold\n", - "showSrc(MultiThreshold)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\"> `get_nodeattr_types`: returns a dict for the permitted attributes for node. It returns a triple with following values for each of the special multithreshold attributes. </font>\n", - "* <font size=\"3\">`dtype`: indicates which member of the ONNX AttributeProto will be utilized </font>\n", - "* <font size=\"3\">`require`: indicates whether this attribute is required </font>\n", - "* <font size=\"3\">`default_value`: indicates the default value that will be used if the attribute is not set </font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">`make_shape_compatible_op`: To use the flow of FINN, the transformation pass [infer_shapes](https://github.com/Xilinx/finn/blob/master/src/finn/transformation/infer_shapes.py) is applied to the graphs in various places. In order for this transformation to be applied to CustomOps, they must first be converted to standard ONNX nodes with the same shape behavior. This means, nodes where the relationship between input and output shape is the same. \n", - "\n", - "This is done at this point. Since the output shape of a multithreshold node is the same as the input shape, it can be replaced by a `\"Relu\"` node from the standard node library of onnx.</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">`infer_node_datatype`: sets the output tensor data type accordingly to the attribute `out_dtype` </font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">`execute_node`: This function allows the execution of the node, depending on the CustomOp a different functionality has to be implemented. In the case of the multithreshold node the input values and the thresholds are first extracted and after the attributes for the output scaling have been retrieved, the output is calculated with the help of a separate function. For more details regarding this function please take a look in the code [here](https://github.com/Xilinx/finn/blob/master/src/finn/custom_op/multithreshold.py). </font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">FINN has a subset of CustomOps that correspond to the [finn-hls](https://finn-hlslib.readthedocs.io/en/latest/) library. In the next part of the Jupyter notebook these are described in more detail. </font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## HLS FINN-ONNX node\n", - "\n", - "<font size=\"3\">The creation of an HLS FINN-ONNX node looks very similar to the creation of a basic FINN-ONNX node. But three new attributes are introduced that are necessary to enable the processing of HLS FINN-ONNX nodes in FINN.</font>\n", - "\n", - "`FCLayer_node = helper.make_node(\n", - " \"StreamingFCLayer_Batch\",\n", - " node_inp_list,\n", - " node_outp_list,\n", - " domain=\"finn\",\n", - " backend=\"fpgadataflow\",\n", - " code_gen_dir=\"\",\n", - " executable_path=\"\",\n", - " resType=\"ap_resource_lut()\",\n", - " MW=mw,\n", - " MH=mh,\n", - " SIMD=simd,\n", - " PE=pe,\n", - " inputDataType=<FINN DataType>,\n", - " weightDataType=<FINN DataType>,\n", - " outputDataType=<FINN DataType>,\n", - " ActVal=actval,\n", - " binaryXnorMode=<0/1>,\n", - " noActivation=<0/1>\n", - ")`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">`\"StreamingFCLayer_Batch\"` describes the op_type, then the inputs and outputs are declared. This is still like building a default onnx node without additional attributes. But since this is a custom op node of FINN, the attribute `domain=\"finn\"` must be set. The streaming fc layer is a custom op from the [finn-hls](https://finn-hlslib.readthedocs.io/en/latest/) library, this information is set in the node using the `backend` attribute. To execute a custom op from the [finn-hls](https://finn-hlslib.readthedocs.io/en/latest/) library, the corresponding c++ code must be created and an executable must be produced. Where the generated code is stored is specified in the `code_gen_dir` attribute and `executable_path` specifies the path to the produced executable. In addition to the data types of the input and output tensors, the node also contains various other attributes resulting from the parameters of the corresponding [finn-hls](https://finn-hlslib.readthedocs.io/en/latest/) library function. More detailed information can be found in the documentation of [finn-hlslib](https://finn-hlslib.readthedocs.io/en/latest/).</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## HLSCustomOp class\n", - "\n", - "<font size=\"3\">If it is a node from the [finn-hls](https://finn-hlslib.readthedocs.io/en/latest/) library another class is used which is derived from the CustomOp class:</font>" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "class HLSCustomOp(CustomOp):\n", - " \"\"\"HLSCustomOp class all custom ops that correspond to a finn-hlslib \n", - " function are based on. Contains different functions every fpgadataflow \n", - " custom node should have. Some as abstract methods, these have to be filled\n", - " when writing a new fpgadataflow custom op node.\"\"\"\n", - " def __init__(self, onnx_node):\n", - " super().__init__(onnx_node)\n", - "\n", - " self.code_gen_dict = {}\n", - "\n", - " # getting templates from templates.py\n", - "\n", - " # template for single node execution\n", - " self.docompute_template = templates.docompute_template\n", - "\n", - " # templates for single node ip generation\n", - " # cpp file\n", - " self.ipgen_template = templates.ipgen_template\n", - " # tcl script\n", - " self.ipgentcl_template = templates.ipgentcl_template\n", - "\n", - " def get_nodeattr_types(self):\n", - " return {\n", - " \"backend\": (\"s\", True, \"fpgadataflow\"),\n", - " \"code_gen_dir_npysim\": (\"s\", False, \"\"),\n", - " \"code_gen_dir_ipgen\": (\"s\", False, \"\"),\n", - " \"executable_path\": (\"s\", False, \"\"),\n", - " \"ipgen_path\": (\"s\", False, \"\"),\n", - " \"exec_mode\": (\"s\", False, \"\"),\n", - " \"sim_cycles\": (\"i\", False, 0),\n", - " \"rtlsim_trace\": (\"s\", False, \"\"),\n", - " }\n", - "\n", - " def node_res_estimation(self):\n", - " \"\"\"Returns summarized resource estimation of BRAMs and LUTs \n", - " of the node.\"\"\"\n", - " resources = []\n", - " resources.append(\"BRAMs: \" + str(self.bram_estimation()))\n", - " resources.append(\"LUTs: \" + str(self.lut_estimation()))\n", - " return resources\n", - "\n", - " def bram_estimation(self):\n", - " \"\"\"Function for BRAM resource estimation, is member function of \n", - " HLSCustomOp class but has to be filled by every node\"\"\"\n", - " return 0\n", - "\n", - " def lut_estimation(self):\n", - " \"\"\"Function for LUT resource estimation, is member function of\n", - " HLSCustomOp class but has to be filled by every node\"\"\"\n", - " return 0\n", - "\n", - " def code_generation_ipgen(self, model, fpgapart, clk):\n", - " \"\"\"Generates c++ code and tcl script for ip generation.\"\"\"\n", - " node = self.onnx_node\n", - "\n", - " # generate top cpp file for ip generation\n", - " path = self.get_nodeattr(\"code_gen_dir_ipgen\")\n", - " self.generate_params(model, path)\n", - " self.global_includes()\n", - " self.defines(\"ipgen\")\n", - " self.blackboxfunction()\n", - " self.pragmas()\n", - " self.docompute()\n", - "\n", - " template = self.ipgen_template\n", - "\n", - " for key in self.code_gen_dict:\n", - " # transform list into long string separated by '\\n'\n", - " code_gen_line = \"\\n\".join(self.code_gen_dict[key])\n", - " template = template.replace(key, code_gen_line)\n", - " code_gen_dir = self.get_nodeattr(\"code_gen_dir_ipgen\")\n", - " f = open(os.path.join(code_gen_dir, \"top_{}.cpp\".format(node.name)), \"w\")\n", - " f.write(template)\n", - " f.close()\n", - " self.code_gen_dict.clear()\n", - "\n", - " # generate tcl script for ip generation\n", - " self.code_gen_dict[\"$PROJECTNAME$\"] = [\"project_{}\".format(node.name)]\n", - " self.code_gen_dict[\"$HWSRCDIR$\"] = [code_gen_dir]\n", - " self.code_gen_dict[\"$FPGAPART$\"] = [fpgapart]\n", - " self.code_gen_dict[\"$FINNHLSLIBDIR$\"] = [\"/workspace/finn-hlslib\"]\n", - " self.code_gen_dict[\"$TOPFXN$\"] = [node.name]\n", - " self.code_gen_dict[\"$CLKPERIOD$\"] = [str(clk)]\n", - "\n", - " template = self.ipgentcl_template\n", - "\n", - " for key in self.code_gen_dict:\n", - " # transform list into long string separated by '\\n'\n", - " code_gen_line = \"\\n\".join(self.code_gen_dict[key])\n", - " template = template.replace(key, code_gen_line)\n", - " code_gen_dir = self.get_nodeattr(\"code_gen_dir_ipgen\")\n", - " f = open(os.path.join(code_gen_dir, \"hls_syn_{}.tcl\".format(node.name)), \"w\")\n", - " f.write(template)\n", - " f.close()\n", - " self.code_gen_dict.clear()\n", - "\n", - " def ipgen_singlenode_code(self):\n", - " \"\"\"Builds the bash script for ip generation using the IPGenBuilder from \n", - " finn.util.fpgadataflow.\"\"\"\n", - " node = self.onnx_node\n", - " code_gen_dir = self.get_nodeattr(\"code_gen_dir_ipgen\")\n", - " builder = IPGenBuilder()\n", - " builder.append_tcl(code_gen_dir + \"/hls_syn_{}.tcl\".format(node.name))\n", - " builder.set_ipgen_path(code_gen_dir + \"/project_{}\".format(node.name))\n", - " builder.build(code_gen_dir)\n", - " self.set_nodeattr(\"ipgen_path\", builder.ipgen_path)\n", - "\n", - " def code_generation_npysim(self, model):\n", - " \"\"\"Generates c++ code for simulation (npysim).\"\"\"\n", - " node = self.onnx_node\n", - " path = self.get_nodeattr(\"code_gen_dir_npysim\")\n", - " self.generate_params(model, path)\n", - " self.global_includes()\n", - " self.defines(\"npysim\")\n", - " self.read_npy_data()\n", - " self.strm_decl()\n", - " self.docompute()\n", - " self.dataoutstrm()\n", - " self.save_as_npy()\n", - "\n", - " template = self.docompute_template\n", - "\n", - " for key in self.code_gen_dict:\n", - " # transform list into long string separated by '\\n'\n", - " code_gen_line = \"\\n\".join(self.code_gen_dict[key])\n", - " template = template.replace(key, code_gen_line)\n", - " code_gen_dir = self.get_nodeattr(\"code_gen_dir_npysim\")\n", - " f = open(os.path.join(code_gen_dir, \"execute_{}.cpp\".format(node.op_type)), \"w\")\n", - " f.write(template)\n", - " f.close()\n", - " self.code_gen_dict.clear()\n", - "\n", - " def compile_singlenode_code(self):\n", - " \"\"\"Builds the bash script for compilation using the CppBuilder from\n", - " finn.util.basic and executes the script to produce the executable.\"\"\"\n", - " code_gen_dir = self.get_nodeattr(\"code_gen_dir_npysim\")\n", - " builder = CppBuilder()\n", - " # to enable additional debug features please uncommand the next line\n", - " # builder.append_includes(\"-DDEBUG\")\n", - " builder.append_includes(\"-I/workspace/finn/src/finn/data/cpp\")\n", - " builder.append_includes(\"-I/workspace/cnpy/\")\n", - " builder.append_includes(\"-I/workspace/finn-hlslib\")\n", - " builder.append_includes(\"-I{}/include\".format(os.environ[\"VIVADO_PATH\"]))\n", - " builder.append_includes(\"--std=c++11\")\n", - " builder.append_sources(code_gen_dir + \"/*.cpp\")\n", - " builder.append_sources(\"/workspace/cnpy/cnpy.cpp\")\n", - " builder.append_includes(\"-lz\")\n", - " builder.set_executable_path(code_gen_dir + \"/node_model\")\n", - " builder.build(code_gen_dir)\n", - " self.set_nodeattr(\"executable_path\", builder.executable_path)\n", - "\n", - " def dynamic_input_to_npy(self, context, count):\n", - " \"\"\"Saves input (given context) into .npy files. \n", - " \n", - " Count indicates the number of inputs that have to be saved.\"\"\"\n", - " node = self.onnx_node\n", - " code_gen_dir = self.get_nodeattr(\"code_gen_dir_npysim\")\n", - " if code_gen_dir == \"\":\n", - " raise Exception(\n", - " \"\"\"\n", - "Found no codegen dir for this node, did you run the codegen_npysim transformation?\n", - " \"\"\"\n", - " )\n", - " # create a npy file for each input of the node (in_ind is input index)\n", - " # assuming dynamic inputs start from 0\n", - " for in_ind in range(count):\n", - " current_input_name = node.input[in_ind]\n", - " np.save(\n", - " os.path.join(code_gen_dir, \"input_{}.npy\".format(in_ind)),\n", - " context[current_input_name],\n", - " )\n", - "\n", - " def npy_to_dynamic_output(self, context):\n", - " \"\"\"Reads the output from a .npy file and saves it at the right place in \n", - " the context dictionary.\"\"\"\n", - " # TODO support multi-output nodes as needed\n", - " node = self.onnx_node\n", - " code_gen_dir = self.get_nodeattr(\"code_gen_dir_npysim\")\n", - " output = np.load(\"{}/output.npy\".format(code_gen_dir))\n", - " context[node.output[0]] = output\n", - "\n", - " def exec_precompiled_singlenode_model(self):\n", - " \"\"\"Executes precompiled executable.\"\"\"\n", - " executable_path = self.get_nodeattr(\"executable_path\")\n", - " if executable_path == \"\":\n", - " raise Exception(\n", - " \"\"\"\n", - "Found no executable for this node, did you run the codegen and\n", - "compilation transformations?\n", - " \"\"\"\n", - " )\n", - " process_execute = subprocess.Popen(executable_path, stdout=subprocess.PIPE)\n", - " process_execute.communicate()\n", - "\n", - " def reset_rtlsim(self, sim):\n", - " \"\"\"Sets reset input in pyverilator to zero, toggles the clock and set it\n", - " back to one\"\"\"\n", - " sim.io.ap_rst_n = 0\n", - " sim.io.ap_clk = 1\n", - " sim.io.ap_clk = 0\n", - " sim.io.ap_rst_n = 1\n", - "\n", - " def toggle_clk(self, sim):\n", - " \"\"\"Toggles the clock input in pyverilator once.\"\"\"\n", - " sim.io.ap_clk = 1\n", - " sim.io.ap_clk = 0\n", - "\n", - " def rtlsim(self, sim, inp):\n", - " \"\"\"Runs the pyverilator simulation by passing the input values to the simulation,\n", - " toggle the clock and observing the execution time. Function contains also an \n", - " observation loop that can abort the simulation if no output value is produced \n", - " after 100 cycles.\"\"\"\n", - " \n", - " trace_file = self.get_nodeattr(\"rtlsim_trace\")\n", - " if trace_file != \"\":\n", - " if trace_file == \"default\":\n", - " trace_file = self.onnx_node.name + \".vcd\"\n", - " sim.start_vcd_trace(trace_file)\n", - " inputs = inp\n", - " outputs = []\n", - " sim.io.out_V_V_TREADY = 1\n", - "\n", - " # observe if output is completely calculated\n", - " # observation_count will contain the number of cycles the calculation ran\n", - " num_out_values = self.get_number_output_values()\n", - " output_observed = False\n", - " observation_count = 0\n", - "\n", - " # avoid infinite looping of simulation by aborting when there is no change in\n", - " # output values after 100 cycles\n", - " no_change_count = 0\n", - " old_outputs = outputs\n", - " liveness_threshold = pyverilate_get_liveness_threshold_cycles()\n", - "\n", - " while not (output_observed):\n", - " sim.io.in0_V_V_TVALID = 1 if len(inputs) > 0 else 0\n", - " sim.io.in0_V_V_TDATA = inputs[0] if len(inputs) > 0 else 0\n", - " if sim.io.in0_V_V_TREADY == 1 and sim.io.in0_V_V_TVALID == 1:\n", - " inputs = inputs[1:]\n", - " if sim.io.out_V_V_TVALID == 1 and sim.io.out_V_V_TREADY == 1:\n", - " outputs = outputs + [sim.io.out_V_V_TDATA]\n", - " sim.io.ap_clk = 1\n", - " sim.io.ap_clk = 0\n", - "\n", - " observation_count = observation_count + 1\n", - " no_change_count = no_change_count + 1\n", - "\n", - " if len(outputs) == num_out_values:\n", - " self.set_nodeattr(\"sim_cycles\", observation_count)\n", - " output_observed = True\n", - "\n", - " if no_change_count == liveness_threshold:\n", - " if old_outputs == outputs:\n", - " if trace_file != \"\":\n", - " sim.flush_vcd_trace()\n", - " sim.stop_vcd_trace()\n", - " raise Exception(\n", - " \"Error in simulation! Takes too long to produce output. \"\n", - " \"Consider setting the LIVENESS_THRESHOLD env.var. to a \"\n", - " \"larger value.\"\n", - " )\n", - " else:\n", - " no_change_count = 0\n", - " old_outputs = outputs\n", - " if trace_file != \"\":\n", - " sim.flush_vcd_trace()\n", - " sim.stop_vcd_trace()\n", - " return outputs\n", - "\n", - " def execute_node(self, context, graph):\n", - " \"\"\"Executes single node using npysim or rtlsim.\"\"\"\n", - " mode = self.get_nodeattr(\"exec_mode\")\n", - " if mode == \"npysim\":\n", - " # save input(s)\n", - " self.dynamic_input_to_npy(context, 1)\n", - " # execute the precompiled model\n", - " self.exec_precompiled_singlenode_model()\n", - " # load output npy file\n", - " self.npy_to_dynamic_output(context)\n", - " elif mode == \"rtlsim\":\n", - " pass\n", - "\n", - " else:\n", - " raise Exception(\n", - " \"\"\"Invalid value for attribute exec_mode! Is currently set to: {}\n", - " has to be set to one of the following value (\"npysim\", \"rtlsim\")\"\"\".format(\n", - " mode\n", - " )\n", - " )\n", - "\n", - " def generate_params(self, model, path):\n", - " \"\"\"Function to generate parameters (i.e. weights and thresholds), \n", - " is member function of HLSCustomOp class but has to be filled \n", - " by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def get_number_output_values(self):\n", - " \"\"\"Function to get the number of expected output values, \n", - " is member function of HLSCustomOp class but has to be filled \n", - " by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def global_includes(self):\n", - " \"\"\"Function to set the global includes for c++ code that has to be generated\n", - " for npysim or rtlsim, is member function of HLSCustomOp class but has to \n", - " be filled by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def defines(self, var):\n", - " \"\"\"Function to set the define commands for c++ code that has to be generated\n", - " for npysim or rtlsim, is member function of HLSCustomOp class but has to \n", - " be filled by every node.\n", - " \n", - " var: makes it possible to reuse the function for different c++ code generation.\n", - " I.e. if set to \"ipgen\" in StreamingFCLayer_Batch additional PRAGMA defines are\n", - " added.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def read_npy_data(self):\n", - " \"\"\"Function to generate the commands for reading data from .npy file in c++, \n", - " is member function of HLSCustomOp class but has to be filled by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def strm_decl(self):\n", - " \"\"\"Function to generate the commands for the stream declaration in c++,\n", - " is member function of HLSCustomOp class but has to be filled\n", - " by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def docompute(self):\n", - " \"\"\"Function to generate the commands for the computational part of the \n", - " c++ code, is member function of HLSCustomOp class but has to be filled\n", - " by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def dataoutstrm(self):\n", - " \"\"\"Function to generate the commands for reading out data from c++ and convert \n", - " into npy format, is member function of HLSCustomOp class but has to be filled \n", - " by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def save_as_npy(self):\n", - " \"\"\"Function to generate the commands for saving data in .npy file in c++,\n", - " is member function of HLSCustomOp class but has to be filled by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def blackboxfunction(self):\n", - " \"\"\"Function to generate a blackbock function in c++ from which an IP block \n", - " will be generated, is member function of HLSCustomOp class but has to be filled \n", - " by every node.\"\"\"\n", - " pass\n", - "\n", - " @abstractmethod\n", - " def pragmas(self):\n", - " \"\"\"Function to generate the pragma commands in c++, is member function of \n", - " HLSCustomOp class but has to be filled by every node.\"\"\"\n", - " pass\n", - "\n", - " def get_folded_input_shape(self):\n", - " \"\"\"Returns folded input shape (according to synapse folding), if implemented.\"\"\"\n", - " raise Exception(\"get_folded_input_shape not implemented for this op\")\n", - "\n", - " def get_folded_output_shape(self):\n", - " \"\"\"Returns folded output shape (according to neuron folding), if implemented.\"\"\"\n", - " raise Exception(\"get_folded_output_shape not implemented for this op\")\n", - "\n", - " def get_instream_width(self):\n", - " \"\"\"Returns input stream width, if implemented.\"\"\"\n", - " raise Exception(\"get_instream_width not implemented for this op\")\n", - "\n", - " def get_outstream_width(self):\n", - " \"\"\"Returns output stream width, if implemented.\"\"\"\n", - " raise Exception(\"get_outstream_width not implemented for this op\")\n", - "\n" - ] - } - ], - "source": [ - "from finn.custom_op.fpgadataflow import HLSCustomOp\n", - "showSrc(HLSCustomOp)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">When creating an instance of this class, a template is introduced, which forms the layout for the c++ code to execute the node. It has some general constructs, like the inclusion of bnn-library.h, which contains the references to the finn-hls library, and of cnpy.h and npy2apintstream.hpp, which support the transfer of python numpy arrays in c++. The idea of this template is to replace the variables marked with `$ $` with c++ calls during code generation. Then the template can be written into a .cpp file and be compiled.\n", - "\n", - "**`get_nodeattr_types()`**: each instance of the HLSCustomOp class must have the attributes `code_gen_dir` and `executable_path`, since to execute these nodes c++ code must be generated and correspondingly the executables.\n", - "\n", - "</font>\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">**`code_generation(model)`**: all functions required for code generation are called and the `$ $` variables in the template are replaced accordingly and written into a .cpp file. Almost all of these subfunctions are implemented as abstract methods in the class, so they are completely customized for each custom op node. A special function is `generate_params()`. This is not implemented as an abstract method, but as a normal function, but contains by default only `pass`. This is because some custom op nodes do not have parameters that need to be generated and in this way the function is skipped. For example for a streaming fc layer node a parameter generation is necessary. How such a parameter generation can look like is described in more detail in the course of this notebook.\n", - "</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">**`compile_singlenode_code()`**: To compile the generated code, the compile command must be built. This is done in this function. It creates an instance of the `CppBuilder()` class and assembles the various components for the function. The `.build` function creates the executable and then sets the corresponding attribute. The class `CppBuilder` is a transformation and a more detailed description can be found in Jupyter notebook [FINN-CodeGenerationAndCompilation](FINN-CodeGenerationAndCompilation.ipynb).\n", - "</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">**`dynamic_input_to_npy(context, count)`**: creates a .npy file for all inputs of the node. These files will be stored in the directory specified by code_gen_dir. The argument `count` must be used to specify the number of inputs. `context` contains the values for the inputs.</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">**`npy_to_dynamic_output(context)`**: reads the output values and sets `context` dictionary accordingly. When executing the c++ executable of the node, the output values are written to a .npy file. </font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">**`exec_precompiled_singlenode_model()`**: executes precompiled executable which is specified in `executable_path`</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">**`execute_node(context,graph)`**: calls first `dynamic_input_to_npy()`, then executes the executable using `exec_precompiled_singlenode_model()` and at the end reads the output .npy file with `npy_to_dynamic_output`</font>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Generate Parameter\n", - "<font size=\"3\">Parameters have to be generated for specific types of HLSCustomOps. For example if the node is a streaming fc layer, there are weights and activation values, which are written to separate .h files and added to the template using `#include`. For streaming fc layer the parameter generation looks like this:\n", - "</font>" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " def generate_params(self, model, path):\n", - " \"\"\"Saves weights into params.h and if existing thresholds into thresh.h.\"\"\"\n", - " code_gen_dir = path\n", - " # weights\n", - " weights = model.get_initializer(self.onnx_node.input[1])\n", - " # convert weights into hlslib-compatible format\n", - " weight_tensor = self.get_hls_compatible_weight_tensor(weights)\n", - " export_wdt = self.get_weight_datatype()\n", - " # we have converted bipolar weights to binary for export,\n", - " # so use it as such for weight generation\n", - " if self.get_weight_datatype() == DataType.BIPOLAR:\n", - " export_wdt = DataType.BINARY\n", - " weight_hls_code = numpy_to_hls_code(\n", - " weight_tensor, export_wdt, \"weights\", True, True\n", - " )\n", - " # write weights into params.h\n", - " # code_gen_dir = self.get_nodeattr(\"code_gen_dir_npysim\")\n", - " f_weights = open(\"{}/params.h\".format(code_gen_dir), \"w\")\n", - "\n", - " if export_wdt.bitwidth() != 1:\n", - " f_weights.write(\n", - " \"static FixedPointWeights<{},{},{},{}> weights = \".format(\n", - " self.get_nodeattr(\"SIMD\"),\n", - " export_wdt.get_hls_datatype_str(),\n", - " self.get_nodeattr(\"PE\"),\n", - " self.calc_wmem(),\n", - " )\n", - " )\n", - " else:\n", - " f_weights.write(\n", - " \"static BinaryWeights<{},{},{}> weights = \".format(\n", - " self.get_nodeattr(\"SIMD\"), self.get_nodeattr(\"PE\"), self.calc_wmem()\n", - " )\n", - " )\n", - " f_weights.write(weight_hls_code)\n", - " f_weights.close()\n", - "\n", - " # thresholds\n", - " if len(self.onnx_node.input) > 2:\n", - " thresholds = model.get_initializer(self.onnx_node.input[2])\n", - " if thresholds is not None:\n", - " threshold_tensor = self.get_hls_compatible_threshold_tensor(thresholds)\n", - " tdt = DataType.INT32\n", - " # use UINT32 threshold export for bipolar times bipolar\n", - " inp_is_bipolar = self.get_input_datatype() == DataType.BIPOLAR\n", - " wt_is_bipolar = self.get_weight_datatype() == DataType.BIPOLAR\n", - " # reinterpret inp/wt as bipolar if bin_xnor_mode is iset\n", - " inp_is_binary = self.get_input_datatype() == DataType.BINARY\n", - " wt_is_binary = self.get_weight_datatype() == DataType.BINARY\n", - " bin_xnor_mode = self.get_nodeattr(\"binaryXnorMode\") == 1\n", - " inp_is_bipolar = inp_is_bipolar or (inp_is_binary and bin_xnor_mode)\n", - " wt_is_bipolar = wt_is_bipolar or (wt_is_binary and bin_xnor_mode)\n", - " if inp_is_bipolar and wt_is_bipolar:\n", - " tdt = DataType.UINT32\n", - " thresholds_hls_code = numpy_to_hls_code(\n", - " threshold_tensor, tdt, \"thresholds\", False, True\n", - " )\n", - " # write thresholds into thresh.h\n", - " # code_gen_dir = self.get_nodeattr(\"code_gen_dir_npysim\")\n", - " f_thresh = open(\"{}/thresh.h\".format(code_gen_dir), \"w\")\n", - " tdt_hls = tdt.get_hls_datatype_str()\n", - " # use binary to export bipolar activations\n", - " export_odt = self.get_output_datatype()\n", - " if self.get_output_datatype() == DataType.BIPOLAR:\n", - " export_odt = DataType.BINARY\n", - " odt_hls = export_odt.get_hls_datatype_str()\n", - " f_thresh.write(\n", - " \"static ThresholdsActivation<{},{},{},{},{},{},{}> threshs \\\n", - " = \".format(\n", - " self.calc_tmem(),\n", - " self.get_nodeattr(\"PE\"),\n", - " threshold_tensor.shape[-1],\n", - " tdt_hls,\n", - " odt_hls,\n", - " self.get_nodeattr(\"ActVal\"),\n", - " \"std::less_equal<%s>\" % tdt_hls,\n", - " )\n", - " )\n", - " f_thresh.write(thresholds_hls_code)\n", - " f_thresh.close()\n", - "\n" - ] - } - ], - "source": [ - "from finn.custom_op.fpgadataflow.streamingfclayer_batch import StreamingFCLayer_Batch\n", - "showSrc(StreamingFCLayer_Batch.generate_params)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "<font size=\"3\">First, the values for the weights are extracted with `get_initializer()` using the ModelWrapper. At this point it is assumed that the second input of the streamingfclayer specifies the weights. After a few manipulations the weights are written in `params.h`. If there are threshold values, they will be prepared and written to `thresh.h`. </font>" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/internals/3_verify_hls_custom_op.ipynb b/notebooks/internals/3_verify_hls_custom_op.ipynb deleted file mode 100644 index 5c3b43cd03d45be03a6c853a19169fbcc5c5acbf..0000000000000000000000000000000000000000 --- a/notebooks/internals/3_verify_hls_custom_op.ipynb +++ /dev/null @@ -1,569 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# FINN - Verification of an HLSCustomOp node\n", - "-----------------------------------------------------------------\n", - "This notebook is about the verification flow and options for FINN custom operation nodes. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Outline\n", - "-------------\n", - "* Example model (sliding window function)\n", - "* c++ high level simulation\n", - "* Vivado IP synthesis and pyverilator execution flow" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Example model\n", - "To show the possibilities of how to verify a FINN HLSCustomOp node, an example model with the [sliding window function](https://finn-hlslib.readthedocs.io/en/latest/library/swg.html) of the finn-hlslib is used. For that a corresponding ONNX node is created. The ONNX node contains all the template parameters of the corresponding finn-hlslib function as attributes. The function is shown below." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the next step the individual parameters are defined. At first the class 'DataType' is imported from FINN to be able to use data types like bipolar. With the member function `bitwidth()` the parameter `Input_precision` can be derived directly from this data type. The other parameters are set to reasonable values. The output dimension can be calculated using the input dimension, the kernel size and the value for stride." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from finn.core.datatype import DataType\n", - "idt = DataType.BIPOLAR # input data type\n", - "ip = idt.bitwidth() # input precision\n", - "k = 2 # kernel size\n", - "ifm_dim = 4 # input dimension\n", - "ifm_ch = 1 # input channels\n", - "stride = 2 # stride\n", - "simd = ifm_ch # simd\n", - "\n", - "# output dimension\n", - "ofm_dim = int(((ifm_dim - k) / stride) + 1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An additional variable is defined to be able to infer the shape of the output tensor. Furthermore the output data type is set to the same value as the input data type." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "out_pix = ofm_dim * ofm_dim\n", - "odt = idt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To create an ONNX node, first TensorProto and helper are imported from ONNX. These can be used to create tensors, nodes, graphs and models in ONNX. After importing, the input and output tensors can be created." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from onnx import TensorProto, helper\n", - "\n", - "inp = helper.make_tensor_value_info(\n", - " \"inp\", TensorProto.FLOAT, [1, ifm_ch, ifm_dim, ifm_dim]\n", - ")\n", - "outp = helper.make_tensor_value_info(\n", - " \"outp\", TensorProto.FLOAT, [1, out_pix, k * k * ifm_ch]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the node can be built. This node is directly integrated into a graph environment and from this the ONNX model is created. For more information about the creation and manipulation of an ONNX model, please refer to jupyter notebook [FINN-HowToWorkWithONNX](FINN-HowToWorkWithONNX.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "SlidingWindow_node = helper.make_node(\n", - " \"ConvolutionInputGenerator\",\n", - " [\"inp\"],\n", - " [\"outp\"],\n", - " domain=\"finn\",\n", - " backend=\"fpgadataflow\",\n", - " ConvKernelDim=k,\n", - " IFMChannels=ifm_ch,\n", - " Input_precision=ip,\n", - " IFMDim=ifm_dim,\n", - " OFMDim=ofm_dim,\n", - " SIMD=simd,\n", - " Stride=stride,\n", - " inputDataType=idt.name,\n", - " outputDataType=odt.name,\n", - " )\n", - "graph = helper.make_graph(\n", - " nodes=[SlidingWindow_node],\n", - " name=\"slidingwindow_graph\",\n", - " inputs=[inp],\n", - " outputs=[outp],\n", - " )\n", - "\n", - "model = helper.make_model(graph, producer_name=\"slidingwindow-model\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "FINN provides a thin wrapper around the ONNX model with a lot of helper functions that can be used by importing the class `ModelWrapper`. More information about `ModelWrapper` can be found in Jupyter notebook [FINN-ModelWrapper](FINN-ModelWrapper.ipynb). Here it is used to assign FINN data types to the tensors." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "from finn.core.modelwrapper import ModelWrapper\n", - "\n", - "model = ModelWrapper(model)\n", - "\n", - "model.set_tensor_datatype(\"inp\", idt)\n", - "model.set_tensor_datatype(\"outp\", odt)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "What the model looks like can be visualized with netron. Netron is a visualizer for neural network, deep learning and machine learning models. For this the model is first saved." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "model.save(\"original_model.onnx\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Serving 'original_model.onnx' at http://0.0.0.0:8081\n" - ] - } - ], - "source": [ - "import netron\n", - "netron.start('original_model.onnx', port=8081, host=\"0.0.0.0\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>\n" - ], - "text/plain": [ - "<IPython.core.display.HTML object>" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%%html\n", - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that we have the model, we can use various features of FINN to manipulate it. The basic principle of FINN is that there are transformation and analysis passes that can be applied to a model. A transformation pass changes a given model and returns the changed model. An analysis pass traverses the graph structure and produces information about certain properties. It returns a dictionary of named properties.\n", - "\n", - "The following section describes the transformation passes that can be used to verify an HLSCustomOp node. Firstly the verification with a c++ high level simulation is shown and afterwards with a Vivado IP synthesis and pyverilator execution flow." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### c++ high level simulation\n", - "\n", - "First, an additional attribute must be set to specify which of the two verification types should be used when executing the node. This is done with the transformation pass `SetExecMode`, to which the desired mode is passed. After that the transformation pass `CodeGen_npysim` can be applied. With this transformation c++ code is generated and stored in a temporary directory. In addition, a further attribute is set, which contains the path to this directory." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode\n", - "from finn.transformation.fpgadataflow.codegen_npysim import CodeGen_npysim\n", - "\n", - "model = model.transform(SetExecMode(\"npysim\"))\n", - "model = model.transform(CodeGen_npysim())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you now save the model again and display it, these changes can be seen." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Stopping http://0.0.0.0:8081\n", - "Serving 'modified_model.onnx' at http://0.0.0.0:8081\n" - ] - } - ], - "source": [ - "model.save(\"modified_model.onnx\")\n", - "netron.start('modified_model.onnx', port=8081, host=\"0.0.0.0\")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>\n" - ], - "text/plain": [ - "<IPython.core.display.HTML object>" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%%html\n", - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next step is to create the executable from the .cpp file using the `Compile` transformation. The path to the executable is also stored in a new attribute." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "from finn.transformation.fpgadataflow.compile import Compile\n", - "model = model.transform(Compile())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "All required files are now available and we can execute the node. This is done with the `execute_onnx` function, which gets the model and an input dictionary. That means we have to create an input tensor first. For this we use a numpy array." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[[[-1. -1. 1. 1.]\n", - " [-1. -1. -1. -1.]\n", - " [ 1. -1. 1. -1.]\n", - " [ 1. 1. 1. -1.]]]]\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "x = np.asarray([-1, -1, 1, 1, -1, -1, -1, -1, 1, -1, 1, -1, 1, 1, 1, -1], dtype=np.float32).reshape(1, ifm_ch, ifm_dim, ifm_dim)\n", - "print(x)\n", - "input_dict = {\"inp\": (x + 1) /2}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To be able to use `execute_onnx()` `onnx_exec` must be imported. Inside `execute_onnx()` the attribute `exec_mode` is read and if \"npysim\" is selected, the input array is saved in a .npy file and the previously created executable is executed. The output is saved in another .npy file and is read by `execute_onnx()` and saved as output." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[[-1. -1. -1. -1.]\n", - " [ 1. 1. -1. -1.]\n", - " [ 1. -1. 1. 1.]\n", - " [ 1. -1. 1. -1.]]]\n" - ] - } - ], - "source": [ - "import finn.core.onnx_exec as oxe\n", - "y_npysim = oxe.execute_onnx(model, input_dict)[\"outp\"]\n", - "print(y_npysim)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A different transformation flow can be used for verification. This will be discussed in the next section." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Vivado IP synthesis and pyverilator execution flow\n", - "\n", - "In this verification a .cpp code is generated from the node, which is synthesized to an IP block using Vivado. Afterwards the functionality can be simulated with [pyverilator](https://github.com/maltanar/pyverilator). Pyverilator is a tool which makes it possible to simulate verilog files using verilator via a python interface." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the first step `exec_mode` must be set to \"rtlsim\" in order to select the corresponding functionality when executing the node. In addition, the nodes in the model are assigned unique names using the `GiveUniqueNodeNames()` transformation. Then the transformation `CodeGen_ipgen()` can be executed. Two arguments are passed to this transformation, one is an fpga part and the other is a value for the clock." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "from finn.transformation.general import GiveUniqueNodeNames\n", - "from finn.transformation.fpgadataflow.codegen_ipgen import CodeGen_ipgen\n", - "model = model.transform(SetExecMode(\"rtlsim\"))\n", - "model = model.transform(GiveUniqueNodeNames())\n", - "model = model.transform(CodeGen_ipgen(\"xc7z020clg400-1\", 5))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "During the transformation a new attribute with the temporary directory is set, in which the .cpp and a .tcl script are stored, with which the synthesis can be started. This can be seen in the following using netron." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Stopping http://0.0.0.0:8081\n", - "Serving 'modified_model.onnx' at http://0.0.0.0:8081\n" - ] - } - ], - "source": [ - "model.save(\"modified_model.onnx\")\n", - "netron.start('modified_model.onnx', port=8081, host=\"0.0.0.0\")" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>\n" - ], - "text/plain": [ - "<IPython.core.display.HTML object>" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%%html\n", - "<iframe src=\"http://0.0.0.0:8081/\" style=\"position: relative; width: 100%;\" height=\"400\"></iframe>" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next step is to perform the synthesis using the `HLSSynth_IPGen()` transformation and set another attribute with the project directory, which contains the IP block. \n", - "\n", - "So that the execution can run without errors, two env variables must be set inside the jupyter notebook." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "env: PWD=/workspace/finn/notebooks\n" - ] - } - ], - "source": [ - "# env variable has to be set because it is used inside the trafo\n", - "%env PWD=/workspace/finn/notebooks\n", - "\n", - "from finn.transformation.fpgadataflow.hlssynth_ipgen import HLSSynth_IPGen\n", - "model = model.transform(HLSSynth_IPGen())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the execution can run again and pyverilator is used in the background to simulate the generated verilog files." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[[-1., -1., -1., -1.],\n", - " [ 1., 1., -1., -1.],\n", - " [ 1., -1., 1., 1.],\n", - " [ 1., -1., 1., -1.]]], dtype=float32)" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "y_rtlsim = oxe.execute_onnx(model, input_dict)[\"outp\"]\n", - "y_rtlsim" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the last step it can be checked whether the two results from the simulations match." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "assert (y_npysim == y_rtlsim).all()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -}