diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..9087804 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python setup.py sdist + twine upload dist/* diff --git a/AUTHORS.rst b/AUTHORS.rst index 4832a4c..14346eb 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,10 +2,12 @@ Contributors ============ -* Yaman Umuroglu (maintainer) -* Sambhav Jain (maintainer) +* Yaman Umuroglu (@maltanar) (maintainer) +* Sambhav Jain (@sjain-stanford) * Jakoba Petri-Koenig (@auphelia) * Lucian Petrica (@quetric) * Tobias Alonso (@Tobi-Alonso) * Hendrik Borras (@HenniOVP) * Mirza Mrahorovic (@mmrahorovic) +* Felix Paul Jentzsch (@felixpj) +* Jon Ander Lezeta (@jalezeta) diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index 6ebe66a..7f02f60 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -30,11 +30,12 @@ from finn.custom_op.registry import getCustomOp from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy -from finn.util.fpgadataflow import ( +from finn.util.pyverilator import ( pyverilate_get_liveness_threshold_cycles, pyverilate_stitched_ip, + reset_rtlsim, + toggle_clk, ) -from finn.util.pyverilator import reset_rtlsim, toggle_clk try: from pyverilator import PyVerilator diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 3bb8bef..0af907e 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,6 +33,8 @@ from finn.custom_op.general.multithreshold import MultiThreshold from finn.custom_op.general.quantavgpool2d import QuantAvgPool2d from finn.custom_op.general.xnorpopcount import XnorPopcountMatMul +from finn.custom_op.logicnets.binary_truthtable import BinaryTruthTable +from finn.custom_op.logicnets.truthtable import TruthTable custom_op = dict() @@ -43,3 +45,5 @@ custom_op["MultiThreshold"] = MultiThreshold custom_op["XnorPopcountMatMul"] = XnorPopcountMatMul custom_op["Im2Col"] = Im2Col +custom_op["BinaryTruthTable"] = BinaryTruthTable +custom_op["TruthTable"] = TruthTable diff --git a/src/finn/custom_op/general/im2col.py b/src/finn/custom_op/general/im2col.py index 421a1e4..e76c613 100644 --- a/src/finn/custom_op/general/im2col.py +++ b/src/finn/custom_op/general/im2col.py @@ -19,6 +19,11 @@ def compute_conv_output_dim(ifm_dim, k, stride, total_pad=0, dilation=1): """ if ifm_dim == 1: # indicates dummy dimension, keep as-is + # Also ensure that every call to this function respects the expected + # kernel shape and padding + assert ( + k == 1 and total_pad == 0 + ), "Unexpected kernel shape and padding for 1D input image" out_dim = 1 else: out_dim = int(((ifm_dim + total_pad - dilation * (k - 1) - 1) / stride) + 1) @@ -26,21 +31,28 @@ def compute_conv_output_dim(ifm_dim, k, stride, total_pad=0, dilation=1): def get_im2col_indices_nchw( - x_shape, field_height, field_width, padding=0, stride_y=1, stride_x=1, dilation=1 + x_shape, + field_height, + field_width, + padding=0, + stride_h=1, + stride_w=1, + dilation_h=1, + dilation_w=1, ): """Returns im2col indices.""" # First figure out what the size of the output should be n, c, h, w = x_shape pad_h = padding[0] + padding[2] pad_w = padding[1] + padding[3] - out_height = compute_conv_output_dim(h, field_height, stride_y, pad_h, dilation) - out_width = compute_conv_output_dim(w, field_width, stride_x, pad_w, dilation) + out_height = compute_conv_output_dim(h, field_height, stride_h, pad_h, dilation_h) + out_width = compute_conv_output_dim(w, field_width, stride_w, pad_w, dilation_w) - i0 = dilation * np.repeat(np.arange(field_height), field_width) + i0 = dilation_h * np.repeat(np.arange(field_height), field_width) i0 = np.tile(i0, c) - i1 = stride_y * np.repeat(np.arange(out_height), out_width) - j0 = dilation * np.tile(np.arange(field_width), field_height * c) - j1 = stride_x * np.tile(np.arange(out_width), out_height) + i1 = stride_h * np.repeat(np.arange(out_height), out_width) + j0 = dilation_w * np.tile(np.arange(field_width), field_height * c) + j1 = stride_w * np.tile(np.arange(out_width), out_height) i = i0.reshape(-1, 1) + i1.reshape(1, -1) j = j0.reshape(-1, 1) + j1.reshape(1, -1) @@ -56,10 +68,11 @@ def im2col_indices_nchw( field_height, field_width, padding=[0, 0, 0, 0], - stride_y=1, - stride_x=1, + stride_h=1, + stride_w=1, pad_val=0, - dilation=1, + dilation_h=1, + dilation_w=1, ): """Performs im2col on image (2D tensor, possibly with 1-length dummy dimensions) x with given field height and width, as well as values for padding and stride size. @@ -67,32 +80,22 @@ def im2col_indices_nchw( # Zero-pad the input p = padding - if ifm_h == 1: # Shape of input image is: (1, C, 1, W) - x_padded = np.pad( - x, - ((0, 0), (0, 0), (0, 0), (p[1], p[3])), - mode="constant", - constant_values=pad_val, - ) - elif ifm_w == 1: # Shape of input image is: (1, C, H, 1) - x_padded = np.pad( - x, - ((0, 0), (0, 0), (p[0], p[2]), (0, 0)), - mode="constant", - constant_values=pad_val, - ) - elif ifm_h > 1 and ifm_w > 1: # Shape of input image is: (1, C, H, W) - x_padded = np.pad( - x, - ((0, 0), (0, 0), (p[0], p[2]), (p[1], p[3])), - mode="constant", - constant_values=pad_val, - ) - else: - raise Exception("Unknown combination of spatial sizes in im2col_indices_nchw") + x_padded = np.pad( + x, + ((0, 0), (0, 0), (p[0], p[2]), (p[1], p[3])), + mode="constant", + constant_values=pad_val, + ) k, i, j = get_im2col_indices_nchw( - x.shape, field_height, field_width, padding, stride_y, stride_x, dilation + x.shape, + field_height, + field_width, + padding, + stride_h, + stride_w, + dilation_h, + dilation_w, ) cols = x_padded[:, k, i, j] @@ -122,7 +125,7 @@ class Im2Col(CustomOp): def get_nodeattr_types(self): return { # stride and shape of convolution kernel - "stride": ("i", True, 1), + "stride": ("ints", True, []), "kernel_size": ("ints", True, []), # input tensor shape "input_shape": ("s", True, ""), @@ -134,16 +137,14 @@ def get_nodeattr_types(self): # depthwise: if 1, infer ConvolutionInputGenerator with depthwise == 1 "depthwise": ("i", False, 0, {0, 1}), # dilation factor applied to the conv kernel - "dilations": ("i", False, 1), + "dilations": ("ints", False, [1, 1]), } def make_shape_compatible_op(self, model): - k = self.get_nodeattr("kernel_size") # Assumption: Height x Width - k_h = k[0] - k_w = k[1] - stride = self.get_nodeattr("stride") + k_h, k_w = self.get_nodeattr("kernel_size") # Assumption: Height x Width + stride_h, stride_w = self.get_nodeattr("stride") ishape = self.get_nodeattr("input_shape") - dilation = self.get_nodeattr("dilations") + dilation_h, dilation_w = self.get_nodeattr("dilations") pad = self.get_nodeattr( "pad_amount" ) # padding: [H_begin, W_begin, H_end, W_end] @@ -166,16 +167,22 @@ def make_shape_compatible_op(self, model): # check that kernel tensor also respects any existing dummy dimensions if ifm_dim_h == 1: + kernel_1d = k_h == 1 + pad_1d = pad_h == 0 assert ( - k_h == 1 - ), "Unexpected kernel shape for input image of dimensions (N, 1, W, C)" + kernel_1d and pad_1d + ), "Unexpected kernel shape and padding for input image\ + of dimensions (N, 1, W, C)" if ifm_dim_w == 1: + kernel_1d = k_w == 1 + pad_1d = pad_w == 0 assert ( - k_w == 1 - ), "Unexpected kernel shape for input image of dimensions (N, H, 1, C)" + kernel_1d and pad_1d + ), "Unexpected kernel shape padding for input image\ + of dimensions (N, H, 1, C)" - ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride, pad_h, dilation) - ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride, pad_w, dilation) + ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride_h, pad_h, dilation_h) + ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride_w, pad_w, dilation_w) # implement tensor with correct shape values = np.random.randn(1, ofm_dim_h, ofm_dim_w, k_h * k_w * ifm_ch).astype( @@ -201,16 +208,13 @@ def infer_node_datatype(self, model): def execute_node(self, context, graph): node = self.onnx_node - k = self.get_nodeattr("kernel_size") # Assumption: Height x Width - k_h = k[0] - k_w = k[1] - - stride = self.get_nodeattr("stride") + k_h, k_w = self.get_nodeattr("kernel_size") # Assumption: Height x Width + stride_h, stride_w = self.get_nodeattr("stride") pad = self.get_nodeattr("pad_amount") pad_h = pad[0] + pad[2] pad_w = pad[1] + pad[3] pad_val = self.get_nodeattr("pad_value") - dilation = self.get_nodeattr("dilations") + dilation_h, dilation_w = self.get_nodeattr("dilations") iname = node.input[0] x = context[iname] @@ -218,29 +222,45 @@ def execute_node(self, context, graph): ret = util.get_by_name(qnt_annotations, iname, "tensor_name") ret = util.get_by_name(ret.quant_parameter_tensor_names, "finn_datatype", "key") idt = DataType[ret.value] - for val in pad: - if val != 0: - assert idt.allowed(val), "Im2Col dtype must allow pad_val" + if pad != [0, 0, 0, 0]: + assert idt.allowed(pad_val), "Im2Col dtype must allow pad_val" # check that input is NHWC assert x.ndim == 4, "Unexpected number of input dims for Im2Col" n, h, w, c = x.shape + # check that kernel tensor also respects any existing dummy dimensions if h == 1: + kernel_1d = k_h == 1 + pad_1d = pad_h == 0 assert ( - k_h == 1 - ), "Unexpected kernel shape for input image of dimensions (N, 1, W, C)" + kernel_1d and pad_1d + ), "Unexpected kernel shape and padding for input image\ + of dimensions (N, 1, W, C)" if w == 1: + kernel_1d = k_w == 1 + pad_1d = pad_w == 0 assert ( - k_w == 1 - ), "Unexpected kernel shape for input image of dimensions (N, H, 1, C)" + kernel_1d and pad_1d + ), "Unexpected kernel shape and padding for input image\ + of dimensions (N, H, 1, C)" - out_dim_h = compute_conv_output_dim(h, k_h, stride, pad_h, dilation) - out_dim_w = compute_conv_output_dim(w, k_w, stride, pad_w, dilation) + out_dim_h = compute_conv_output_dim(h, k_h, stride_h, pad_h, dilation_h) + out_dim_w = compute_conv_output_dim(w, k_w, stride_w, pad_w, dilation_w) # internally convert input to NCHW x = x.transpose(0, 3, 1, 2) # call NCHW im2col implementation ret = im2col_indices_nchw( - x, h, w, k_h, k_w, pad, stride, stride, pad_val=pad_val, dilation=dilation + x, + h, + w, + k_h, + k_w, + pad, + stride_h, + stride_w, + pad_val=pad_val, + dilation_h=dilation_h, + dilation_w=dilation_w, ) # result shape is (k_H*k_W*N, out_dim_H*out_dim_W), convert to NCHW ret = ret.reshape(n, c, k_h, k_w, out_dim_h, out_dim_w) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py new file mode 100644 index 0000000..6654339 --- /dev/null +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -0,0 +1,264 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 onnx +import os +from onnx import helper +from pyverilator import PyVerilator + +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.custom_op.base import CustomOp +from finn.util.basic import make_build_dir +from finn.util.data_packing import npy_to_rtlsim_input + + +def binary_truthtable(input, care_set, bits): + """Returns the output to a combination of x-bit input value. The care_set array + reflects the true values in the truth table results. The rest of the entries are + just zero or dont-cares. If 2 is provided in the care-set, the result to fifth + combination of inputs 010 is 1. The input is a vector size x, representing + x-bits binary input. + + The MSB in the input numpy array represents the LSB in the LUT. + + + An example is presented: + + inputs[0:2] = [1, 0, 1] + care_set = [1, 2] + + Possible combinations[2:0]: A B C | Results + 0 0 0 | 0 + 0 0 1 | 1 + 0 1 0 | 1 + 0 1 1 | 0 + 1 0 0 | 0 + 1 0 1 | 0 + 1 1 0 | 0 + 1 1 1 | 0 + + """ + + # calculate integer value of binary input + in_int = npy_to_rtlsim_input(input, DataType.BINARY, bits, False)[0] + # return 1 if the input is in result_one + output = 1 if in_int in care_set else 0 + + return output + + +class BinaryTruthTable(CustomOp): + """The class corresponing to the Binary TruthTable function.""" + + def get_nodeattr_types(self): + return { + # number of intput bits, 2 by default + "in_bits": ("i", True, 2), + # code generation mode + "code_mode": ("s", False, "Verilog"), + # output code directory + "code_dir": ("s", False, ""), + # execution mode, "pyhton" by default + "exec_mode": ("s", True, "python"), + } + + def make_shape_compatible_op(self, model): + node = self.onnx_node + val = np.random.randn(1).astype(np.bool) + tensor_name = ModelWrapper.make_new_valueinfo_name(model) + node = helper.make_node( + "Constant", + inputs=[], + outputs=["val"], + value=helper.make_tensor( + name=tensor_name, + data_type=onnx.TensorProto.FLOAT, + dims=val.shape, + vals=val.flatten().astype(float), + ), + ) + return node + + def infer_node_datatype(self, model): + node = self.onnx_node + # check that the input[0] is binary + assert ( + model.get_tensor_datatype(node.input[0]) == DataType["BINARY"] + ), """ The input vector DataType is not BINARY.""" + # check that the input[1] is UINT32 + assert ( + model.get_tensor_datatype(node.input[1]) == DataType["UINT32"] + ), """ The input vector DataType is not UINT32.""" + # set the output[0] tensor datatype to BINARY + model.set_tensor_datatype(node.output[0], DataType["BINARY"]) + + def execute_node(self, context, graph): + node = self.onnx_node + # load inputs + input_entry = context[node.input[0]] + care_set = context[node.input[1]] + # check input_entry size + in_size = input_entry.size + expected_in_size = self.get_nodeattr("in_bits") + assert ( + in_size == expected_in_size + ), """The input bit array vector is %i and should be %i""" % ( + in_size, + expected_in_size, + ) + # check input_entry shape + assert ( + len(input_entry.shape) == 1 + ), """The input vector has more than one dimension.""" + # load execution mode + mode = self.get_nodeattr("exec_mode") + if mode == "python": + # calculate output in Python mode + output = binary_truthtable( + input_entry, care_set, self.get_nodeattr("in_bits") + ) + elif mode == "rtlsim": + # check the code directory is not empty + nodeName = self.onnx_node.name + verilog_dir = self.get_nodeattr("code_dir") + "/" + nodeName + ".v" + if not os.path.exists(verilog_dir): + raise Exception("Non valid path for the Verilog file: %s" % verilog_dir) + sim = PyVerilator.build(verilog_dir) + bits = self.get_nodeattr("in_bits") + # convert input binary float array into an integer + value = npy_to_rtlsim_input(input_entry, DataType.BINARY, bits, False)[0] + # set value into the Verilog module + sim.io["in"] = value + # read result value + output = sim.io["result"] + else: + raise Exception( + """Invalid value for attribute exec_mode! Is currently set to: {} + has to be set to one of the following value ("python", "rtlsim")""".format( + mode + ) + ) + # return output + context[node.output[0]] = np.array([output], dtype=np.float32) + + def verify_node(self): + info_messages = [] + + # verify number of attributes + num_of_attr = 4 + if len(self.onnx_node.attribute) == num_of_attr: + info_messages.append("The number of attributes is correct") + else: + info_messages.append( + """The number of attributes is incorrect, + {} should have {} attributes""".format( + self.onnx_node.op_type, num_of_attr + ) + ) + + # verify that all necessary attributes exist + try: + self.get_nodeattr("in_bits") + self.get_nodeattr("exec_mode") + except Exception: + info_messages.append( + """The required attributes are not + set. BinaryTruthTable operation reqires in_bits and + exec_mode attributes.""" + ) + + # verify the number of inputs + if len(self.onnx_node.input) == 2: + info_messages.append("The number of inputs is correct") + else: + info_messages.append("BinaryTruthTable needs 2 data inputs") + + return info_messages + + def generate_verilog(self, care_set): + + input_bits = self.get_nodeattr("in_bits") + nodeName = self.onnx_node.name + # the module name is kept constant to "incomplete_table" + # the input name is kept constant to "in" + # the output is kept constant to "result" + verilog_string = "module %s (\n" % (nodeName) + verilog_string += "\tinput [%d:0] in,\n" % (input_bits - 1) + verilog_string += "\t output reg result\n" + verilog_string += ");\n\n" + verilog_string += "\talways @(in) begin\n" + verilog_string += "\t\tcase(in)\n" + + # fill the one entries + for val in care_set: + val = int(val) + verilog_string += "\t\t\t%d'b" % (input_bits) + verilog_string += bin(val)[2:].zfill(input_bits) + verilog_string += " : result = 1'b1;\n" + + # fill the rest of the combinations with 0 + verilog_string += "\t\t\tdefault: result = 1'b0;\n" + # close the module + verilog_string += "\t\tendcase\n\tend\nendmodule\n" + # create temporary folder and save attribute value + self.set_nodeattr("code_dir", make_build_dir("BinaryTruthTable_files_")) + # create and write verilog file + verilog_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".v", "w") + verilog_file.write(verilog_string) + verilog_file.close() + + def generate_pla(self, care_set): + + input_bits = self.get_nodeattr("in_bits") + nodeName = self.onnx_node.name + + pla_string = ".i %d\n" % (input_bits) + pla_string += ".o 1\n" + + pla_string += ".ilb" + for i in range(input_bits): + pla_string += " in_%d" % (i) + + pla_string += "\n" + + pla_string += ".ob out" + + pla_string += "\n" + pla_string += ".type fd\n" + + for index, val in enumerate(care_set): + pla_string += bin(val)[2:].zfill(input_bits) + pla_string += " 1" + pla_string += "\n" + + pla_string += "\n.e" + pla_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".pla", "w") + pla_file.write(pla_string) + pla_file.close() diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py new file mode 100644 index 0000000..9d71d09 --- /dev/null +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -0,0 +1,362 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 onnx +import os +from onnx import helper +from pyverilator import PyVerilator + +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.custom_op.base import CustomOp +from finn.util.basic import make_build_dir +from finn.util.data_packing import ( + npy_to_rtlsim_input, + unpack_innermost_dim_from_hex_string, +) + + +def _truthtable(input, care_set, results, in_bits): + """Returns the output to a combination of x-bit input value. The care_set array + reflects the specific input combinations considered. The result vector represents + every output to every combination in the care_set. Thus, the length of care_set + and results must be the same. All the arrays are numpy arrays + + + The MSB in the input numpy array represents the LSB in the LUT. + + + An example is presented: + in_bits = 3 + out_bits = 3 + input[0:2] = [1, 0, 1] + care_set = [1, 5] + results = [3, 1] + + The function checks if the decimal representation of the binary input is in + the care_set. If it is in the care_set, we check the result of the binary + input combination in the result. If the input is not part of the care_set, + the output will be zero. + + The input in this example is [1,0,1], which is '5' in decimal representation. + The input is part of the care_set. Then, we check what is the position of 5 + in the care_set, and we extract the value to input combination '5' from the + results vector, which in this case is '1' + + Possible combinations[2:0]: input[0:2] | results[0:2] + 0 0 0 | 0 0 0 + 0 0 1 | 0 1 1 + 0 1 0 | 0 0 0 + 0 1 1 | 0 0 0 + > 1 0 0 | 0 0 1 + 1 0 1 | 0 0 0 + 1 1 0 | 0 0 0 + 1 1 1 | 0 0 0 + + """ + + # calculate integer value of binary input + input_int = npy_to_rtlsim_input(input, DataType.BINARY, in_bits, False)[0] + if input_int in care_set: + index = np.where(care_set == input_int)[0][0] + output = results[index] + else: + output = 0 + + return output + + +class TruthTable(CustomOp): + """The class corresponing to the TruthTable function.""" + + def get_nodeattr_types(self): + return { + # number of intput bits, 4 by default + "in_bits": ("i", True, 4), + # number of output bits, 4 by default + "out_bits": ("i", True, 4), + # code generation mode + "code_mode": ("s", False, "Verilog"), + # output code directory + "code_dir": ("s", False, ""), + # execution mode, "python" by default + "exec_mode": ("s", True, "python"), + } + + def make_shape_compatible_op(self, model): + node = self.onnx_node + out_bits = self.get_nodeattr("out_bits") + + val = np.random.randint(2, size=out_bits) + + tensor_name = ModelWrapper.make_new_valueinfo_name(model) + node = helper.make_node( + "Constant", + inputs=[], + outputs=["val"], + value=helper.make_tensor( + name=tensor_name, + data_type=onnx.TensorProto.FLOAT, + dims=val.shape, + vals=val.flatten().astype(float), + ), + ) + return node + + def infer_node_datatype(self, model): + node = self.onnx_node + # check that the input[0] is binary + assert ( + model.get_tensor_datatype(node.input[0]) == DataType["BINARY"] + ), """ The input vector DataType is not BINARY.""" + # check that the input[1] is UINT32 + assert ( + model.get_tensor_datatype(node.input[1]) == DataType["UINT32"] + ), """ The input vector DataType is not UINT32.""" + # check that the input[2] is UINT32 + assert ( + model.get_tensor_datatype(node.input[2]) == DataType["UINT32"] + ), """ The input vector DataType is not UINT32.""" + # set the output[0] tensor datatype to BINARY + model.set_tensor_datatype(node.output[0], DataType["BINARY"]) + + def execute_node(self, context, graph): + node = self.onnx_node + # Load inputs + # We assume input[0] is the input_vector, input[1] the care_set + # and the input[2] the results + input_entry = context[node.input[0]] + care_set = context[node.input[1]] + results = context[node.input[2]] + # VERIFICATION: care_set and results tensor shape and sizes + # + # check input_entry size + in_size = input_entry.size + in_bits = self.get_nodeattr("in_bits") + out_bits = self.get_nodeattr("out_bits") + assert ( + in_size == in_bits + ), """The input bit array vector is %i and should be %i""" % ( + in_size, + in_bits, + ) + # check the maximum value of care_set values is smaller than 2^in_bits + max_care_set = np.amax(care_set) + max_in = 1 << in_bits + assert max_care_set < max_in + # check the maximum value of the results is smaller than 2^out_bits + max_results = np.amax(results) + max_out = 1 << out_bits + assert max_results < max_out + # check input_entry shape + assert ( + len(input_entry.shape) == 1 + ), """The input vector has more than one dimension.""" + # check care_set shape + assert ( + len(care_set.shape) == 1 + ), """The care_set vector has more than one dimension.""" + # check results shape + assert ( + len(results.shape) == 1 + ), """The results vector has more than one dimension.""" + # check the care_set and results sizes are the same + assert ( + care_set.size == results.size + ), """The care_set size is %s and results size is %s. Must + be the same """ % ( + care_set.size, + results.size, + ) + # load execution mode + mode = self.get_nodeattr("exec_mode") + if mode == "python": + # calculate output in Python mode + output = _truthtable(input_entry, care_set, results, in_bits) + elif mode == "rtlsim": + # check the code directory is not empty + nodeName = self.onnx_node.name + code_dir = self.get_nodeattr("code_dir") + verilog_dir = code_dir + "/" + nodeName + ".v" + if not os.path.exists(verilog_dir): + raise Exception("Non valid path for the Verilog file: %s" % verilog_dir) + # Create PyVerilator object + sim = PyVerilator.build(verilog_dir) + # Convert input binary array into an integer representation + value = npy_to_rtlsim_input(input_entry, DataType.BINARY, in_bits, False)[0] + # Set input value into the Verilog module + sim.io["in"] = value + # read result value + output = sim.io["result"] + else: + raise Exception( + """Invalid value for attribute exec_mode! Is currently set to: {} + has to be set to one of the following value ("python", "rtlsim")""".format( + mode + ) + ) + # return output and convert it back into a binary array + output_hex = np.array([hex(int(output))]) + out_array = unpack_innermost_dim_from_hex_string( + output_hex, DataType.BINARY, (out_bits,), out_bits + ) + context[node.output[0]] = out_array + + def verify_node(self): + info_messages = [] + + # verify number of attributes + num_of_attr = 5 + if len(self.onnx_node.attribute) == num_of_attr: + info_messages.append("The number of attributes is correct") + else: + info_messages.append( + """The number of attributes is incorrect, + {} should have {} attributes""".format( + self.onnx_node.op_type, num_of_attr + ) + ) + + # verify that all necessary attributes exist + try: + self.get_nodeattr("in_bits") + self.get_nodeattr("out_bits") + self.get_nodeattr("exec_mode") + except Exception: + info_messages.append( + """The required attributes are not + set. BinaryTruthTable operation reqires in_bits, + out_bits andb exec_mode attributes.""" + ) + + # verify the number of inputs + if len(self.onnx_node.input) == 3: + info_messages.append("The number of inputs is correct") + else: + info_messages.append("BinaryTruthTable needs 3 data inputs") + + return info_messages + + def generate_verilog(self, care_set, results): + + input_bits = self.get_nodeattr("in_bits") + output_bits = self.get_nodeattr("out_bits") + nodeName = self.onnx_node.name + # VERIFICATION: care_set and results tensor shape and sizes + # + # check the maximum value of care_set values is smaller than 2^in_bits + max_care_set = np.amax(care_set) + max_in = 1 << input_bits + assert max_care_set < max_in + # check the maximum value of the results is smaller than 2^out_bits + max_results = np.amax(results) + max_out = 1 << output_bits + assert max_results < max_out + # check care_set shape + assert ( + len(care_set.shape) == 1 + ), """The care_set vector has more than one dimension.""" + # check results shape + assert ( + len(results.shape) == 1 + ), """The results vector has more than one dimension.""" + # check the care_set and results sizes are the same + care_set_size = care_set.size + results_size = results.size + assert ( + care_set_size == results_size + ), """The care_set size is %s and results size is %s. Must + be the same """ % ( + care_set_size, + results_size, + ) + # the module name is kept constant to "incomplete_table" + # the input name is kept constant to "in" + # the output is kept constant to "result" + verilog_string = "module %s (\n" % (nodeName) + verilog_string += "\tinput [%d:0] in,\n" % (input_bits - 1) + verilog_string += "\t output reg [%d:0] result\n" % (output_bits - 1) + verilog_string += ");\n\n" + verilog_string += "\talways @(in) begin\n" + verilog_string += "\t\tcase(in)\n" + + # fill the one entries + for index, val in enumerate(care_set): + val = int(val) + verilog_string += "\t\t\t%d'b" % (input_bits) + verilog_string += bin(val)[2:].zfill(input_bits) + verilog_string += " : result = %d'b" % (output_bits) + verilog_string += bin(results[index])[2:].zfill(output_bits) + verilog_string += ";\n" + + # fill the rest of the combinations with 0 + verilog_string += "\t\t\tdefault: result = %d'b" % (output_bits) + verilog_string += bin(0)[2:].zfill(output_bits) + verilog_string += ";\n" + # close the module + verilog_string += "\t\tendcase\n\tend\nendmodule\n" + # create temporary folder and save attribute value + self.set_nodeattr("code_dir", make_build_dir("TruthTable_files_")) + # create and write verilog file + verilog_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".v", "w") + verilog_file.write(verilog_string) + verilog_file.close() + + def generate_pla(self, care_set, results): + + input_bits = self.get_nodeattr("in_bits") + output_bits = self.get_nodeattr("out_bits") + nodeName = self.onnx_node.name + + pla_string = ".i %d\n" % (input_bits) + pla_string += ".o %d\n" % (output_bits) + + pla_string += ".ilb" + for i in range(input_bits): + pla_string += " in_%d" % (i) + + pla_string += "\n" + + pla_string += ".ob" + for i in range(output_bits): + pla_string += " out_%d" % (i) + + pla_string += "\n" + pla_string += ".type fd\n" + + for index, val in enumerate(care_set): + pla_string += bin(val)[2:].zfill(input_bits) + pla_string += " " + pla_string += bin(results[index])[2:].zfill(output_bits) + pla_string += "\n" + + pla_string += "\n.e" + pla_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".pla", "w") + pla_file.write(pla_string) + pla_file.close() diff --git a/src/finn/transformation/change_3d_tensors_to_4d.py b/src/finn/transformation/change_3d_tensors_to_4d.py index 76d3c80..251f609 100644 --- a/src/finn/transformation/change_3d_tensors_to_4d.py +++ b/src/finn/transformation/change_3d_tensors_to_4d.py @@ -169,7 +169,7 @@ def apply(self, model): strides = get_by_name(n.attribute, "strides", "name").ints if len(dilations) == 1: # we must add another dimension to it dilations.append( - dilations[0] + 1 ) # only equal dilation value along each spatial axis is supported if len(kernel_shape) == 1: # we must add another dimension to it kernel_shape.append(1) diff --git a/src/finn/transformation/create_generic_partitions.py b/src/finn/transformation/create_generic_partitions.py index 5e4bb80..67da854 100755 --- a/src/finn/transformation/create_generic_partitions.py +++ b/src/finn/transformation/create_generic_partitions.py @@ -124,7 +124,7 @@ def apply(self, model): assert ( self.partitioning(node) != partition_id ), """cycle-free graph violated: partition depends on itself""" - print(node) + # print(node) predecessors = model.find_direct_predecessors(node) if predecessors is not None: next_to_check.extend(predecessors) @@ -141,11 +141,25 @@ def apply(self, model): for o in p_out_vi: p_model.graph.output.append(o) - # remove redundant input value_info entries + # remove redundant input and output value_info entries for i in p_in_vi: - if i in p_model.graph.value_info: + # the tensor can be both an input and value_info, so we also have to + # ensure that the tensor is not a relevant value_info before removing + if ( + i in p_model.graph.value_info + and p_model.find_producer(i.name) is None + ): p_model.graph.value_info.remove(i) + for o in p_out_vi: + # the tensor can both an output and value_info, so we also have to + # ensure that the tensor is not a relevant value_info before removing + if ( + o in p_model.graph.value_info + and p_model.find_consumers(o.name) is None + ): + p_model.graph.value_info.remove(o) + # save partition model p_model_filename = ( self.partition_dir + "/partition_" + str(partition_id) + ".onnx" diff --git a/src/finn/transformation/extend_partition.py b/src/finn/transformation/extend_partition.py new file mode 100644 index 0000000..738c36c --- /dev/null +++ b/src/finn/transformation/extend_partition.py @@ -0,0 +1,99 @@ +# 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. + +from finn.core.modelwrapper import ModelWrapper +from finn.transformation.base import Transformation +from finn.transformation.general import SortGraph +from finn.util.basic import get_by_name + + +class ExtendPartition(Transformation): + """Extends GenericPartition type nodes by inserting the graph pointed to by + the model attribute. + Argument 0: extend_index + * List that contains the node indices of the GenericPartition nodes + """ + + def __init__(self, extend_index): + super().__init__() + self.extend_index = extend_index + + def apply(self, model): + graph = model.graph + graph_modified = False + + partition_nodes_dict = { + ind: n + for ind, n in enumerate(graph.node) + if n.op_type == "GenericPartition" + } + + for k, v in partition_nodes_dict.items(): + if k in self.extend_index: + path_to_model = get_by_name(v.attribute, "model", "name").s.decode( + "utf-8" + ) + model_partition = ModelWrapper(path_to_model) + + # Append nodes + for partition_node in model_partition.graph.node: + graph.node.append(partition_node) + + # Append value infos + partition_valueinfos = [ + x.name for x in model_partition.graph.value_info + ] + for vi_name in partition_valueinfos: + vi = model_partition.get_tensor_valueinfo(vi_name) + graph.value_info.append(vi) + + # Append initializers + partition_initializers = [x for x in model_partition.graph.initializer] + for i in partition_initializers: + graph.initializer.append(i) + + # Append tensor annotations, except for the input/output tensors + # of the partitioned graph, as these will be present in the + # 'upper' model. + in_out_names = [x.name for x in model_partition.graph.input] + in_out_names += [x.name for x in model_partition.graph.output] + partition_annotations = [ + x + for x in model_partition.graph.quantization_annotation + if x.tensor_name not in in_out_names + ] + for a in partition_annotations: + graph.quantization_annotation.append(a) + + graph.node.remove(v) + graph_modified = True + + if graph_modified: + model = model.transform(SortGraph()) + + return (model, graph_modified) diff --git a/src/finn/transformation/infer_datatypes.py b/src/finn/transformation/infer_datatypes.py index 2b2e2a9..66d91ca 100644 --- a/src/finn/transformation/infer_datatypes.py +++ b/src/finn/transformation/infer_datatypes.py @@ -35,7 +35,14 @@ def _infer_node_datatype(model, node): """Infer output datatype(s) for a particular node. Returns True if any changes were made.""" - dt_identity_optypes = ["Reshape", "Transpose"] + dt_identity_optypes = [ + "Reshape", + "Transpose", + "Flatten", + "Slice", + "Gather", + "Identity", + ] idtypes = list(map(lambda x: model.get_tensor_datatype(x), node.input)) odtypes = list(map(lambda x: model.get_tensor_datatype(x), node.output)) op_type = node.op_type diff --git a/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py new file mode 100644 index 0000000..b4bae83 --- /dev/null +++ b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py @@ -0,0 +1,59 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 finn.custom_op.registry as registry +from finn.transformation.base import NodeLocalTransformation + + +def _genbintruthtable_verilog(node, care_set): + """Calls Verilog generation helper function inside the customOp class""" + op_type = node.op_type + try: + myOp = registry.getCustomOp(node) + myOp.generate_verilog(care_set) + + except KeyError: + # exception if op_type is not supported + raise Exception("Custom op_type %s is currently not supported." % op_type) + + +class GenBinaryTruthTableVerilog(NodeLocalTransformation): + """Generate a Verilog file for every node in the Graph using the + TruthTable custom operation""" + + def __init__(self, num_workers, care_set): + super().__init__(num_workers=num_workers) + self.care_set = care_set + + def applyNodeLocal(self, node): + op_type = node.op_type + if op_type == "BinaryTruthTable": + specific_care_set = self.care_set[node.input[1]] + _genbintruthtable_verilog(node, specific_care_set) + + return (node, False) diff --git a/src/finn/transformation/logicnets/gen_logicnets_verilog.py b/src/finn/transformation/logicnets/gen_logicnets_verilog.py new file mode 100644 index 0000000..66d102b --- /dev/null +++ b/src/finn/transformation/logicnets/gen_logicnets_verilog.py @@ -0,0 +1,283 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 os +import shutil + +import finn.custom_op.registry as registry +from finn.transformation.base import Transformation +from finn.transformation.logicnets.gen_bintruthtable_verilog import ( + GenBinaryTruthTableVerilog, +) +from finn.util.basic import make_build_dir + + +def _check_node_verilog(model): + graph = model.graph + # Check every BinaryTruthTable operation within the ONNX model + for node in graph.node: + if node.op_type == "BinaryTruthTable": + customOp = registry.getCustomOp(node) + # Check the code_dir attribute in the operation is not empty + nodeName = node.name + verilog_dir = customOp.get_nodeattr("code_dir") + "/" + nodeName + ".v" + # Return "False" if any of the BinaryTruthTable operations contain and + # empty code_dir + if not os.path.exists(verilog_dir): + return False + return True + + +def _create_logicnets_folder(model): + + code_dir = make_build_dir("logicnets_model_") + model.set_metadata_prop("code_dir", code_dir) + graph = model.graph + # Check every BinaryTruthTable operation within the ONNX model and copy into + # LogicNets folder + for node in graph.node: + if node.op_type == "BinaryTruthTable": + customOp = registry.getCustomOp(node) + nodeName = node.name + node_dir = customOp.get_nodeattr("code_dir") + node_file = node_dir + "/" + nodeName + ".v" + shutil.copy2(node_file, code_dir) + return model + + +def _generate_verilog(model, indices): + + # Generate verilog file + code_dir = model.get_metadata_prop("code_dir") + verilog_file = open(code_dir + "/" + "LogicNetsModule.v", "w") + + graph = model.graph + + # Check if every node in the graph is "BinaryTruthTable", "Gather" or "Concat" + # IMPORTANT: No other node can be present in the graph. + for node in model.graph.node: + if ( + (node.op_type != "BinaryTruthTable") + and (node.op_type != "Concat") + and (node.op_type != "Gather") + ): + raise Exception( + """NodeType %s detected. Every node must be either + BinaryTruthTable or Concat or Gather""" + % (node.op_type) + ) + + # Find the general input tensor name representing the input array. + # IMPORTANT: The input tensor is assumed to contain "input" in the TensorName + # This is the only tensor that contains "input" in the entire model. + input_tensor_name = None + for tensor in graph.input: + if "input" in tensor.name: + input_tensor_name = tensor.name + # Raise exception if input tensor is not found + if input_tensor_name is None: + raise Exception( + "General Input tensorName not found. The input tensor has to contain " + "the keyword input on the TensorName, and has to be the only one " + "following that rule in the entire ONNX model.\n" + ) + + # Find the general output tensor name representing the output array. + # IMPORTANT: The output tensor is assumed to contain "output" in the TensorName + # This is the only tensor that contains "output" in the entire model. + output_tensor_name = None + for tensor in graph.output: + if "output" in tensor.name: + output_tensor_name = tensor.name + # Raise exception if output tensor is not found + if output_tensor_name is None: + print("Output tensor not found in the graph.") + + # Get the input and output tensor shapes. Only supports batch 1. Extract 1, as the + # bits start from 0 + input_shape = model.get_tensor_shape(input_tensor_name)[0] - 1 + output_shape = model.get_tensor_shape(output_tensor_name)[0] - 1 + + # Write verilog + verilog_string = "module LogicNetsModule( input[%s:0] %s, output[%s:0] %s);\n\n" % ( + input_shape, + input_tensor_name, + output_shape, + output_tensor_name, + ) + + # IMPORTANT: One important assumption made here is that the nodes within te ONNX + # graph are ordered from input to output and based on the graph dependencies. + # This is done automatically when creating the ONNX model. It is worth mentioning + # that a random order is used, the verilog generation below will not work. + + # Algorithm: + # 1. Start checking from the first node in the graph, and look for + # BinaryTruthTable type nodes. + # + # 2. Get the node specific data: + # - Input tensorName (incoming data). + # - Output tensorName (LUT entry for given incoming data). + # - Get the "BinaryTruthTable" type unique nodeName. + # + # 3. Check for previous "Gather" type nodes. Every "BinaryTruthTable" has to be + # preceeded by "Gather" nodes. The correct "Gather" node is identified by + # checking the "BinaryTruthTable" Input tensorName to see if it matches + # the output tensorName of the "Gather" node. + # + # 4. Get the specific "Gather" node tensorNames: + # - RAW "input" array, input number 0 + # - Sparsity "index", input number 1 + # + # 5. Create a wire that connects the "BinaryTruthTable" verilog module input + # to specific bits of the "Gather" RAW "input" array. The connection is + # based on the sparsity "index" values. + # + # 6. Check the "Concat" nodes following the selected "BinaryTruthTable" + # operation. Another assumption is made, where every "BinaryTruthTable" + # operation is followed by "Concat"operation, where multiple + # "BinaryTruthTable" operation outputs have to be concatenated into a + # sigle tensor. + # + # 7. Check the index of the single "BinaryTruthTable" output within the + # entire concantenated array. + # + # 8. Check the output tensorName for the selected "Gather" node. + # + # 9. Create a wire to connect the output of the "BinaryTruthTable" verilog + # module into the following "BinaryTruthTable" module. Only a single wire + # is created per "Concat" node.The wire width is based on the width of + # the "Concat" node. + # + # Initialize variable to keep track which "Concat" nodes have been used. + concat_wires = [] + + for index, node in enumerate(graph.node): + # Step 1 + if node.op_type == "BinaryTruthTable": + # Step 2 + op_input_name = node.input[0] + op_output_name = node.output[0] + node_name = node.name + customOp = registry.getCustomOp(node) + in_bits = customOp.get_nodeattr("in_bits") - 1 + + # Step 3 + preceding_node = model.find_producer(op_input_name) + if preceding_node.op_type != "Gather": + raise Exception( + "The node_type preceding node %s is %s and must be Gather" + % (node.name, preceding_node.op_type) + ) + + # Step 4 + gather_index_name = preceding_node.input[1] + gather_input_name = preceding_node.input[0] + + # Step 5 + verilog_string += "wire [%s:0] %s = {" % (in_bits, op_input_name) + for index in indices[gather_index_name]: + verilog_string += "%s[%s]," % (gather_input_name, index) + verilog_string = verilog_string[:-1] + verilog_string += "};\n" + + # Step 6 + succesor_nodes = model.find_consumers(op_output_name) + for succesor_node in succesor_nodes: + # Step 7 + concat_out_position = list(succesor_node.input).index(op_output_name) + concat_out_name = succesor_node.output[0] + if concat_out_name != output_tensor_name and ( + concat_out_name not in concat_wires + ): + concat_size = model.get_tensor_shape(concat_out_name)[0] + verilog_string += "wire [%s:0] %s;\n" % ( + concat_size - 1, + concat_out_name, + ) + concat_wires.append(concat_out_name) + verilog_string += "%s %s_inst(.in(%s), .result(%s[%s]));\n\n" % ( + node_name, + node_name, + op_input_name, + concat_out_name, + concat_out_position, + ) + verilog_string += "endmodule" + + # Write verilog_string into final verilog_file + verilog_file.write(verilog_string) + verilog_file.close() + + +class GenLogicNetsVerilog(Transformation): + """Generate the Verilog file for a LogicNets based network + The transformation function takes three parameters, the care_set + , the sparsity indexes and the code_dir. Both parameters are dictionaries that + can be accessed using the tensor name that is considered in the ONNX + model. + + -The care_set represents which input combinations are + considered by the LUT. The care_set is a dictionary that maps the + actual care_set data into the tensorName used in the ONNX mode for every + BinaryTruthTable operation. + + -The sparsity indexes represent which signals incoming into the + BinaryTruthTable operation are relevant. The sparsity indexes are formed + through a dictionary that maps the actual index values into tensorName + used in every Gather node inside the ONNX model. + + -The code_dir is used to return the path of the generated verilog source + code.""" + + def __init__(self, care_set, indices): + super().__init__() + self.care_set = care_set + self.indices = indices + + def apply(self, model): + + # Check if the Verilog Files for every BinaryTruthTable operation has been + # generated. + # We call the parallel NodeLevel verilog generation transformation if + # it has not been generated. + # + if not _check_node_verilog(model): + model = model.transform( + GenBinaryTruthTableVerilog(num_workers=None, care_set=self.care_set) + ) + + # Create LogicNets folder and copy all individual BinaryTruthTable code into + # the folder. The code_dir is included as a metadata attribute + model = _create_logicnets_folder(model) + print(model.graph) + # Generate the Verilog wrapper that connects every individual BinaryTruthTable + # verilog module. + _generate_verilog(model, indices=self.indices) + + return (model, False) diff --git a/src/finn/transformation/logicnets/gen_truthtable_pla.py b/src/finn/transformation/logicnets/gen_truthtable_pla.py new file mode 100644 index 0000000..65fad07 --- /dev/null +++ b/src/finn/transformation/logicnets/gen_truthtable_pla.py @@ -0,0 +1,75 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 finn.custom_op.registry as registry +from finn.transformation.base import NodeLocalTransformation + + +def _gentruthtable_pla(node, care_set, results): + """Calls PLA generation helper function inside the TruthTable customOp class""" + op_type = node.op_type + try: + myOp = registry.getCustomOp(node) + myOp.generate_pla(care_set, results) + + except KeyError: + # exception if op_type is not supported + raise Exception("Custom op_type %s is currently not supported." % op_type) + + +def _genbinarytruthtable_pla(node, care_set): + """Calls PLA generation helper function inside the BinaryTruthTable customOp""" + op_type = node.op_type + try: + myOp = registry.getCustomOp(node) + myOp.generate_pla(care_set) + + except KeyError: + # exception if op_type is not supported + raise Exception("Custom op_type %s is currently not supported." % op_type) + + +class GenTruthTablePLA(NodeLocalTransformation): + """Generate a PLA file for every node in the Graph using the + TruthTable custom operation""" + + def __init__(self, num_workers, care_set): + super().__init__(num_workers=num_workers) + self.care_set = care_set + + def applyNodeLocal(self, node): + op_type = node.op_type + if op_type == "TruthTable": + specific_care_set = self.care_set[node.input[1]] + specific_results = self.care_set[node.input[2]] + _gentruthtable_pla(node, specific_care_set, specific_results) + elif op_type == "BinaryTruthTable": + specific_care_set = self.care_set[node.input[1]] + _genbinarytruthtable_pla(node, specific_care_set) + + return (node, False) diff --git a/src/finn/transformation/logicnets/gen_truthtable_verilog.py b/src/finn/transformation/logicnets/gen_truthtable_verilog.py new file mode 100644 index 0000000..0d03c13 --- /dev/null +++ b/src/finn/transformation/logicnets/gen_truthtable_verilog.py @@ -0,0 +1,60 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 finn.custom_op.registry as registry +from finn.transformation.base import NodeLocalTransformation + + +def _gentruthtable_verilog(node, care_set, results): + """Calls Verilog generation helper function inside the customOp class""" + op_type = node.op_type + try: + myOp = registry.getCustomOp(node) + myOp.generate_verilog(care_set, results) + + except KeyError: + # exception if op_type is not supported + raise Exception("Custom op_type %s is currently not supported." % op_type) + + +class GenTruthTableVerilog(NodeLocalTransformation): + """Generate a Verilog file for every node in the Graph using the + TruthTable custom operation""" + + def __init__(self, num_workers, care_set): + super().__init__(num_workers=num_workers) + self.care_set = care_set + + def applyNodeLocal(self, node): + op_type = node.op_type + if op_type == "TruthTable": + specific_care_set = self.care_set[node.input[1]] + specific_results = self.care_set[node.input[2]] + _gentruthtable_verilog(node, specific_care_set, specific_results) + + return (node, False) diff --git a/src/finn/transformation/lower_convs_to_matmul.py b/src/finn/transformation/lower_convs_to_matmul.py index 2cddea9..111c3ea 100644 --- a/src/finn/transformation/lower_convs_to_matmul.py +++ b/src/finn/transformation/lower_convs_to_matmul.py @@ -30,15 +30,14 @@ from onnx import TensorProto, helper from finn.transformation.base import Transformation -from finn.transformation.infer_shapes import InferShapes from finn.util.basic import get_by_name def _auto_pad_to_explicit_padding( - autopad_str, idim_h, idim_w, k_h, k_w, stride, n_dims + autopad_str, idim_h, idim_w, k_h, k_w, stride_h, stride_w, n_dims ): - pad_total_h = (stride - 1) * idim_h - stride + k_h - pad_total_w = (stride - 1) * idim_w - stride + k_w + pad_total_h = (stride_h - 1) * idim_h - stride_h + k_h + pad_total_w = (stride_w - 1) * idim_w - stride_w + k_w pad_half_small_h = int((pad_total_h / 2)) pad_half_small_w = int((pad_total_w / 2)) pad_half_large_h = pad_total_h - pad_half_small_h @@ -64,7 +63,6 @@ def apply(self, model): for n in graph.node: node_ind += 1 if n.op_type == "Conv": - graph_modified = True cnv_input = n.input[0] cnv_output = n.output[0] idt = model.get_tensor_datatype(cnv_input) @@ -73,7 +71,8 @@ def apply(self, model): k = get_by_name(n.attribute, "kernel_shape").ints k_h = k[0] k_w = k[1] - stride = get_by_name(n.attribute, "strides").ints[-1] + stride_h = get_by_name(n.attribute, "strides").ints[0] + stride_w = get_by_name(n.attribute, "strides").ints[1] group = get_by_name(n.attribute, "group").i weight_name = n.input[1] W_conv = model.get_initializer(weight_name) @@ -86,12 +85,8 @@ def apply(self, model): dilation_attr = get_by_name(n.attribute, "dilations") if dilation_attr is not None: dilation = dilation_attr.ints - assert ( - len(set(dilation)) <= 1 - ), "Only equal dilation value along each spatial axis is supported" - dilation = dilation[0] else: - dilation = 1 # default value + dilation = [1, 1] # default value # handle both auto_pad and explicit padding auto_pad = get_by_name(n.attribute, "auto_pad") if auto_pad is not None: @@ -107,7 +102,8 @@ def apply(self, model): ifm_dim_w, k_h, k_w, - stride, + stride_h, + stride_w, len(model.get_tensor_shape(n.input[0])) - 2, ) else: @@ -119,15 +115,6 @@ def apply(self, model): assert ( ifm_dim_h == 1 or ifm_dim_w == 1 ), "Padding is assumed to be 1D, image is 2D" - if ifm_dim_h == 1: # Assumption: dim H is not padded - pad_2D = [0, 0, 0, 0] - pad_2D[1] = pad[0] - pad_2D[3] = pad[1] - elif ifm_dim_w == 1: # Assumption: dim W is not padded - pad_2D = [0, 0, 0, 0] - pad_2D[0] = pad[0] - pad_2D[2] = pad[1] - pad = pad_2D # if depthwise conv create sparse matrix and variable "dw" # to store as attribute in Im2Col that indicates that the created @@ -146,7 +133,7 @@ def apply(self, model): # sparsity of the weight matrix. For this # we use the sparsity annotation of the # weight tensor - sparsity = {"dw": {"kernel_shape": k_h}} + sparsity = {"dw": {"kernel_shape": [k_h, k_w]}} model.set_tensor_sparsity(weight_name, sparsity) # additionally create variable "dw" to store # as attribute in Im2Col that indicates that the created @@ -179,7 +166,13 @@ def apply(self, model): padding = 0 # k_h=k_w==1: pointwise convolution, thus no im2col needed - if k_h == 1 and k_w == 1 and padding == 0 and stride == 1: + if ( + k_h == 1 + and k_w == 1 + and padding == 0 + and stride_h == 1 + and stride_w == 1 + ): need_im2col = False if need_im2col: @@ -215,7 +208,7 @@ def apply(self, model): [inp_trans_out], [im2col_out], domain="finn.custom_op.general", - stride=stride, + stride=[stride_h, stride_w], kernel_size=[k_h, k_w], pad_amount=pad, input_shape="(1,{},{},{})".format(ifm_dim_h, ifm_dim_w, ifm_ch), @@ -243,5 +236,4 @@ def apply(self, model): # remove old nodes graph.node.remove(n) - model = model.transform(InferShapes()) return (model, graph_modified) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 3781470..cacb5d4 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -44,6 +44,7 @@ pynq_part_map["Pynq-Z2"] = "xc7z020clg400-1" pynq_part_map["ZCU102"] = "xczu9eg-ffvb1156-2-e" pynq_part_map["ZCU104"] = "xczu7ev-ffvc1156-2-e" +pynq_part_map["ZCU111"] = "xczu28dr-ffvg1517-2-e" # native AXI HP port width (in bits) for PYNQ boards pynq_native_port_width = dict() @@ -52,6 +53,7 @@ pynq_native_port_width["Ultra96"] = 128 pynq_native_port_width["ZCU102"] = 128 pynq_native_port_width["ZCU104"] = 128 +pynq_native_port_width["ZCU111"] = 128 # Alveo device and platform mappings alveo_part_map = dict() diff --git a/src/finn/util/fpgadataflow.py b/src/finn/util/fpgadataflow.py index d3f741f..9f521f4 100644 --- a/src/finn/util/fpgadataflow.py +++ b/src/finn/util/fpgadataflow.py @@ -26,127 +26,7 @@ # 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 os -import subprocess - -from finn.util.basic import ( - get_by_name, - get_rtlsim_trace_depth, - is_finn_op, - make_build_dir, - which, -) - -try: - from pyverilator import PyVerilator -except ModuleNotFoundError: - PyVerilator = None - - -class IPGenBuilder: - """Builds the bash script to generate IP blocks using Vivado HLS.""" - - def __init__(self): - self.tcl_script = "" - self.ipgen_path = "" - self.code_gen_dir = "" - self.ipgen_script = "" - - def append_tcl(self, tcl_script): - """Sets member variable "tcl_script" to given tcl script.""" - self.tcl_script = tcl_script - - def set_ipgen_path(self, path): - """Sets member variable ipgen_path to given path.""" - self.ipgen_path = path - - def build(self, code_gen_dir): - """Builds the bash script with given parameters and saves it in given folder. - To guarantee the generation in the correct folder the bash script contains a - cd command.""" - assert which("vivado_hls") is not None, "vivado_hls not found in PATH" - self.code_gen_dir = code_gen_dir - self.ipgen_script = str(self.code_gen_dir) + "/ipgen.sh" - working_dir = os.environ["PWD"] - f = open(self.ipgen_script, "w") - f.write("#!/bin/bash \n") - f.write("cd {}\n".format(code_gen_dir)) - f.write("vivado_hls {}\n".format(self.tcl_script)) - f.write("cd {}\n".format(working_dir)) - f.close() - bash_command = ["bash", self.ipgen_script] - process_compile = subprocess.Popen(bash_command, stdout=subprocess.PIPE) - process_compile.communicate() - - -def pyverilate_stitched_ip(model, read_internal_signals=True): - """Given a model with stitched IP, return a PyVerilator sim object. - If read_internal_signals is True, it will be possible to examine the - internal (not only port) signals of the Verilog module, but this may - slow down compilation and emulation. - Trace depth is also controllable, see get_rtlsim_trace_depth() - """ - if PyVerilator is None: - raise ImportError("Installation of PyVerilator is required.") - - vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") - with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f: - all_verilog_srcs = f.read().split() - - def file_to_dir(x): - return os.path.dirname(os.path.realpath(x)) - - def file_to_basename(x): - return os.path.basename(os.path.realpath(x)) - - top_module_file_name = file_to_basename(model.get_metadata_prop("wrapper_filename")) - top_module_name = top_module_file_name.strip(".v") - build_dir = make_build_dir("pyverilator_ipstitched_") - - # dump all Verilog code to a single file - # this is because large models with many files require - # a verilator command line too long for bash on most systems - # NOTE: there are duplicates in this list, and some files - # are identical but in multiple directories (regslice_core.v) - - # remove duplicates from list by doing list -> set -> list - all_verilog_files = list(set(filter(lambda x: x.endswith(".v"), all_verilog_srcs))) - - # remove all but one instances of regslice_core.v - filtered_verilog_files = [] - remove_entry = False - for vfile in all_verilog_files: - if "regslice_core" in vfile: - if not remove_entry: - filtered_verilog_files.append(vfile) - remove_entry = True - else: - filtered_verilog_files.append(vfile) - - # concatenate all verilog code into a single file - with open(vivado_stitch_proj_dir + "/" + top_module_file_name, "w") as wf: - for vfile in filtered_verilog_files: - with open(vfile) as rf: - wf.write("//Added from " + vfile + "\n\n") - wf.write(rf.read()) - - sim = PyVerilator.build( - top_module_file_name, - verilog_path=[vivado_stitch_proj_dir], - build_dir=build_dir, - trace_depth=get_rtlsim_trace_depth(), - top_module_name=top_module_name, - auto_eval=False, - read_internal_signals=read_internal_signals, - ) - return sim - - -def pyverilate_get_liveness_threshold_cycles(): - """Return the number of no-output cycles rtlsim will wait before assuming - the simulation is not finishing and throwing an exception.""" - - return int(os.getenv("LIVENESS_THRESHOLD", 10000)) +from finn.util.basic import get_by_name, is_finn_op def is_fpgadataflow_node(node): @@ -161,93 +41,3 @@ def is_fpgadataflow_node(node): is_node = True return is_node - - -def rtlsim_multi_io(sim, io_dict, num_out_values, trace_file=""): - """Runs the pyverilator simulation by passing the input values to the simulation, - toggle the clock and observing the execution time. Function contains also an - observation loop that can abort the simulation if no output value is produced - after a set number of cycles. Can handle multiple i/o streams. See function - implementation for details on how the top-level signals should be named. - - Arguments: - - * sim: the PyVerilator object for simulation - * io_dict: a dict of dicts in the following format: - {"inputs" : {"in0" : , "in1" : }, - "outputs" : {"out0" : [], "out1" : []} } - is a list of Python arbitrary-precision ints indicating - what data to push into the simulation, and the output lists are - similarly filled when the simulation is complete - * num_out_values: number of total values to be read from the simulation to - finish the simulation and return. - - Returns: number of clock cycles elapsed for completion - - """ - - if trace_file != "": - sim.start_vcd_trace(trace_file) - - for outp in io_dict["outputs"]: - sim.io[outp + "_V_V_TREADY"] = 1 - - # observe if output is completely calculated - # total_cycle_count will contain the number of cycles the calculation ran - output_done = False - total_cycle_count = 0 - output_count = 0 - old_output_count = 0 - - # avoid infinite looping of simulation by aborting when there is no change in - # output values after 100 cycles - no_change_count = 0 - liveness_threshold = pyverilate_get_liveness_threshold_cycles() - - while not (output_done): - for inp in io_dict["inputs"]: - inputs = io_dict["inputs"][inp] - sim.io[inp + "_V_V_TVALID"] = 1 if len(inputs) > 0 else 0 - sim.io[inp + "_V_V_TDATA"] = inputs[0] if len(inputs) > 0 else 0 - if sim.io[inp + "_V_V_TREADY"] == 1 and sim.io[inp + "_V_V_TVALID"] == 1: - inputs = inputs[1:] - io_dict["inputs"][inp] = inputs - - for outp in io_dict["outputs"]: - outputs = io_dict["outputs"][outp] - if sim.io[outp + "_V_V_TVALID"] == 1 and sim.io[outp + "_V_V_TREADY"] == 1: - outputs = outputs + [sim.io[outp + "_V_V_TDATA"]] - output_count += 1 - io_dict["outputs"][outp] = outputs - - sim.io.ap_clk = 1 - sim.io.ap_clk = 0 - - total_cycle_count = total_cycle_count + 1 - - if output_count == old_output_count: - no_change_count = no_change_count + 1 - else: - no_change_count = 0 - old_output_count = output_count - - # check if all expected output words received - if output_count == num_out_values: - output_done = True - - # end sim on timeout - if no_change_count == liveness_threshold: - if trace_file != "": - sim.flush_vcd_trace() - sim.stop_vcd_trace() - raise Exception( - "Error in simulation! Takes too long to produce output. " - "Consider setting the LIVENESS_THRESHOLD env.var. to a " - "larger value." - ) - - if trace_file != "": - sim.flush_vcd_trace() - sim.stop_vcd_trace() - - return total_cycle_count diff --git a/src/finn/util/hls.py b/src/finn/util/hls.py new file mode 100644 index 0000000..fb23af0 --- /dev/null +++ b/src/finn/util/hls.py @@ -0,0 +1,74 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 os +import subprocess + +from finn.util.basic import which + + +class CallHLS: + """Call either vivado_hls or vitis_hls to run HLS build tcl scripts.""" + + def __init__(self, backend="vivado_hls"): + self.tcl_script = "" + self.ipgen_path = "" + self.code_gen_dir = "" + self.ipgen_script = "" + assert backend in [ + "vivado_hls", + "vitis_hls", + ], "Unrecognized backend for CallHLS" + self.backend = backend + + def append_tcl(self, tcl_script): + """Sets the tcl script to be executed.""" + self.tcl_script = tcl_script + + def set_ipgen_path(self, path): + """Sets member variable ipgen_path to given path.""" + self.ipgen_path = path + + def build(self, code_gen_dir): + """Builds the bash script with given parameters and saves it in given folder. + To guarantee the generation in the correct folder the bash script contains a + cd command.""" + assert which(self.backend) is not None, "%s not found in PATH" % self.backend + self.code_gen_dir = code_gen_dir + self.ipgen_script = str(self.code_gen_dir) + "/ipgen.sh" + working_dir = os.environ["PWD"] + f = open(self.ipgen_script, "w") + f.write("#!/bin/bash \n") + f.write("cd {}\n".format(code_gen_dir)) + f.write("%s %s\n" % (self.backend, self.tcl_script)) + f.write("cd {}\n".format(working_dir)) + f.close() + bash_command = ["bash", self.ipgen_script] + process_compile = subprocess.Popen(bash_command, stdout=subprocess.PIPE) + process_compile.communicate() diff --git a/src/finn/util/logicnets/logicnets.py b/src/finn/util/logicnets/logicnets.py new file mode 100644 index 0000000..b5a0726 --- /dev/null +++ b/src/finn/util/logicnets/logicnets.py @@ -0,0 +1,51 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 +from random import randint + + +def random_care_set(n_bits, n_entries): + """Generates a random care_set based on the binary n_bit size and number or desired + entries. The function checks if the n_entries is less than the possible + 2^(n_bits) - 1. Duplicated entries are also checked, and if exist, the duplicated + entry is taken out. Then, more random values are generated until m_entries different + random values are generated.""" + max_entries = 2 ** n_bits - 1 + + assert ( + n_entries <= max_entries + ), """Number of entries must be smaller than '2^(n_bits) - 1'""" + + care_set = np.array([]) + while care_set.size != n_entries: + for _ in range((n_entries - care_set.size)): + value = randint(0, max_entries) + care_set = np.append(care_set, value) + care_set = np.unique(care_set) + return care_set diff --git a/src/finn/util/pyverilator.py b/src/finn/util/pyverilator.py index fb022c1..b598a4a 100644 --- a/src/finn/util/pyverilator.py +++ b/src/finn/util/pyverilator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020, Xilinx +# Copyright (c) 2021, Xilinx # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -26,6 +26,183 @@ # 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 os + +from finn.util.basic import get_rtlsim_trace_depth, make_build_dir + +try: + from pyverilator import PyVerilator +except ModuleNotFoundError: + PyVerilator = None + + +def pyverilate_get_liveness_threshold_cycles(): + """Return the number of no-output cycles rtlsim will wait before assuming + the simulation is not finishing and throwing an exception.""" + + return int(os.getenv("LIVENESS_THRESHOLD", 10000)) + + +def rtlsim_multi_io(sim, io_dict, num_out_values, trace_file="", sname="_V_V_"): + """Runs the pyverilator simulation by passing the input values to the simulation, + toggle the clock and observing the execution time. Function contains also an + observation loop that can abort the simulation if no output value is produced + after a set number of cycles. Can handle multiple i/o streams. See function + implementation for details on how the top-level signals should be named. + + Arguments: + + * sim: the PyVerilator object for simulation + * io_dict: a dict of dicts in the following format: + {"inputs" : {"in0" : , "in1" : }, + "outputs" : {"out0" : [], "out1" : []} } + is a list of Python arbitrary-precision ints indicating + what data to push into the simulation, and the output lists are + similarly filled when the simulation is complete + * num_out_values: number of total values to be read from the simulation to + finish the simulation and return. + * trace_file: vcd dump filename, empty string (no vcd dump) by default + * sname: signal naming for streams, "_V_V_" by default, vitis_hls uses "_V_" + + Returns: number of clock cycles elapsed for completion + + """ + + if trace_file != "": + sim.start_vcd_trace(trace_file) + + for outp in io_dict["outputs"]: + sim.io[outp + sname + "TREADY"] = 1 + + # observe if output is completely calculated + # total_cycle_count will contain the number of cycles the calculation ran + output_done = False + total_cycle_count = 0 + output_count = 0 + old_output_count = 0 + + # avoid infinite looping of simulation by aborting when there is no change in + # output values after 100 cycles + no_change_count = 0 + liveness_threshold = pyverilate_get_liveness_threshold_cycles() + + while not (output_done): + for inp in io_dict["inputs"]: + inputs = io_dict["inputs"][inp] + sim.io[inp + sname + "TVALID"] = 1 if len(inputs) > 0 else 0 + sim.io[inp + sname + "TDATA"] = inputs[0] if len(inputs) > 0 else 0 + if ( + sim.io[inp + sname + "TREADY"] == 1 + and sim.io[inp + sname + "TVALID"] == 1 + ): + inputs = inputs[1:] + io_dict["inputs"][inp] = inputs + + for outp in io_dict["outputs"]: + outputs = io_dict["outputs"][outp] + if ( + sim.io[outp + sname + "TVALID"] == 1 + and sim.io[outp + sname + "TREADY"] == 1 + ): + outputs = outputs + [sim.io[outp + sname + "TDATA"]] + output_count += 1 + io_dict["outputs"][outp] = outputs + + sim.io.ap_clk = 1 + sim.io.ap_clk = 0 + + total_cycle_count = total_cycle_count + 1 + + if output_count == old_output_count: + no_change_count = no_change_count + 1 + else: + no_change_count = 0 + old_output_count = output_count + + # check if all expected output words received + if output_count == num_out_values: + output_done = True + + # end sim on timeout + if no_change_count == liveness_threshold: + if trace_file != "": + sim.flush_vcd_trace() + sim.stop_vcd_trace() + raise Exception( + "Error in simulation! Takes too long to produce output. " + "Consider setting the LIVENESS_THRESHOLD env.var. to a " + "larger value." + ) + + if trace_file != "": + sim.flush_vcd_trace() + sim.stop_vcd_trace() + + return total_cycle_count + + +def pyverilate_stitched_ip(model, read_internal_signals=True): + """Given a model with stitched IP, return a PyVerilator sim object. + If read_internal_signals is True, it will be possible to examine the + internal (not only port) signals of the Verilog module, but this may + slow down compilation and emulation. + Trace depth is also controllable, see get_rtlsim_trace_depth() + """ + if PyVerilator is None: + raise ImportError("Installation of PyVerilator is required.") + + vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") + with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f: + all_verilog_srcs = f.read().split() + + def file_to_dir(x): + return os.path.dirname(os.path.realpath(x)) + + def file_to_basename(x): + return os.path.basename(os.path.realpath(x)) + + top_module_file_name = file_to_basename(model.get_metadata_prop("wrapper_filename")) + top_module_name = top_module_file_name.strip(".v") + build_dir = make_build_dir("pyverilator_ipstitched_") + + # dump all Verilog code to a single file + # this is because large models with many files require + # a verilator command line too long for bash on most systems + # NOTE: there are duplicates in this list, and some files + # are identical but in multiple directories (regslice_core.v) + + # remove duplicates from list by doing list -> set -> list + all_verilog_files = list(set(filter(lambda x: x.endswith(".v"), all_verilog_srcs))) + + # remove all but one instances of regslice_core.v + filtered_verilog_files = [] + remove_entry = False + for vfile in all_verilog_files: + if "regslice_core" in vfile: + if not remove_entry: + filtered_verilog_files.append(vfile) + remove_entry = True + else: + filtered_verilog_files.append(vfile) + + # concatenate all verilog code into a single file + with open(vivado_stitch_proj_dir + "/" + top_module_file_name, "w") as wf: + for vfile in filtered_verilog_files: + with open(vfile) as rf: + wf.write("//Added from " + vfile + "\n\n") + wf.write(rf.read()) + + sim = PyVerilator.build( + top_module_file_name, + verilog_path=[vivado_stitch_proj_dir], + build_dir=build_dir, + trace_depth=get_rtlsim_trace_depth(), + top_module_name=top_module_name, + auto_eval=False, + read_internal_signals=read_internal_signals, + ) + return sim + def _find_signal(sim, signal_name): # handle both mixed caps and lowercase signal names diff --git a/tests/core/test_modelwrapper.py b/tests/core/test_modelwrapper.py index dd7891a..0cd51c6 100644 --- a/tests/core/test_modelwrapper.py +++ b/tests/core/test_modelwrapper.py @@ -68,7 +68,7 @@ def test_modelwrapper(): assert model.get_tensor_layout(first_conv_iname) == inp_layout inp_sparsity = model.get_tensor_sparsity(first_conv_iname) assert inp_sparsity is None - inp_sparsity = {"dw": {"kernel_shape": 3}} + inp_sparsity = {"dw": {"kernel_shape": [3, 3]}} model.set_tensor_sparsity(first_conv_iname, inp_sparsity) assert model.get_tensor_sparsity(first_conv_iname) == inp_sparsity diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py new file mode 100644 index 0000000..ec3779d --- /dev/null +++ b/tests/custom_op/test_binarytruthtable.py @@ -0,0 +1,136 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 onnx.helper as helper +from onnx import TensorProto + +import finn.core.onnx_exec as oxe +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.custom_op.registry import getCustomOp +from finn.transformation.general import GiveUniqueNodeNames +from finn.transformation.infer_datatypes import InferDataTypes +from finn.transformation.infer_shapes import InferShapes +from finn.transformation.logicnets.gen_bintruthtable_verilog import ( + GenBinaryTruthTableVerilog, +) +from finn.transformation.logicnets.gen_truthtable_pla import GenTruthTablePLA +from finn.util.data_packing import npy_to_rtlsim_input + +# export_onnx_path = "test_truthtable.onnx" + + +def test_binarytruthtable(): + + # Tensor with different input combinations + input_data_vector = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + ], + dtype=np.float32, + ) + # Set the care set + care_set_data = np.asarray([1, 58, 15, 89, 695, 6485]) + in_bits = 16 + + # Set input and output tensor information + inputs = helper.make_tensor_value_info("inputs", TensorProto.FLOAT, [in_bits]) + care_set = helper.make_tensor_value_info( + "care_set", TensorProto.FLOAT, [care_set_data.size] + ) + output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) + # Define the custom node with "python" mode + node_def = helper.make_node( + "BinaryTruthTable", + ["inputs", "care_set"], + ["output"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", + ) + # Create the graph and the model + modelproto = helper.make_model( + helper.make_graph([node_def], "test_model", [inputs, care_set], [output]) + ) + # Wrap the model for finn and set the input tensor datatypes as desired + model = ModelWrapper(modelproto) + model.set_tensor_datatype("inputs", DataType.BINARY) + model.set_tensor_datatype("care_set", DataType.UINT32) + + # test output shape + model = model.transform(InferShapes()) + assert model.get_tensor_shape("output") == [1] + # test output type + assert model.get_tensor_datatype("output") is DataType.FLOAT32 + model = model.transform(InferDataTypes()) + assert model.get_tensor_datatype("output") is DataType.BINARY + # Give unique names to each node + model = model.transform(GiveUniqueNodeNames()) + + # care_set dictionary + care_set_dict = { + "care_set": care_set_data, + } + + # Generate verilog + model = model.transform( + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_dict) + ) + + # Generate PLA files + model = model.transform(GenTruthTablePLA(num_workers=None, care_set=care_set_dict)) + + # Loop over "python" and "rtlsim" execution modes + for _ in range(2): + # Loop over different input combinations + for input_data in input_data_vector: + # Create input dictionary + in_dict = { + "inputs": input_data, + "care_set": care_set_data, + } + # Perform execution + out_dict = oxe.execute_onnx(model, in_dict) + + out_idx = npy_to_rtlsim_input(input_data, DataType.BINARY, in_bits, False)[ + 0 + ] + + entry = 1 if out_idx in care_set_data else 0 + expected = np.array([entry]) + # compare outputs + assert expected == out_dict["output"] + # Change execution mode into "rtlsim" for simulation with PyVerilator + myOp = getCustomOp(model.graph.node[0]) + myOp.set_nodeattr("exec_mode", "rtlsim") diff --git a/tests/custom_op/test_im2col.py b/tests/custom_op/test_im2col.py index 26e7eed..3a26639 100644 --- a/tests/custom_op/test_im2col.py +++ b/tests/custom_op/test_im2col.py @@ -9,36 +9,25 @@ from finn.transformation.infer_shapes import InferShapes -def check_two_dict_for_equality(dict1, dict2): - for key in dict1: - assert key in dict2, "Key: {} is not in both dictionaries".format(key) - assert ( - dict1[key] == dict2[key] - ), """Values for key {} are not the same - in both dictionaries""".format( - key - ) - - return True - - def execution_im2col( x, idt, k_h, k_w, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val=0, - dilation=1, + dilation_h=1, + dilation_w=1, ): pad_amt_h = pad_amt[0] + pad_amt[2] pad_amt_w = pad_amt[1] + pad_amt[3] - ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride, pad_amt_h, dilation) - ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride, pad_amt_w, dilation) + ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride_h, pad_amt_h, dilation_h) + ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride_w, pad_amt_w, dilation_w) # set up onnx model inp = helper.make_tensor_value_info( @@ -53,12 +42,12 @@ def execution_im2col( ["inp"], ["outp"], domain="finn.custom_op.general", - stride=stride, + stride=[stride_h, stride_w], kernel_size=[k_h, k_w], pad_amount=pad_amt, pad_value=pad_val, input_shape="(1,{},{},{})".format(ifm_dim_h, ifm_dim_w, ifm_ch), - dilations=dilation, + dilations=[dilation_h, dilation_w], ) graph = helper.make_graph( @@ -103,20 +92,38 @@ def execution_im2col( # pad_val | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | # k_H | 2 | 2 | 2 | 2 | 3 | 2 | 2 | 2 | 2 | 3 | # k_W | 2 | 2 | 2 | 2 | 3 | 1 | 1 | 1 | 1 | 1 | -# stride | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 1 | 2 | 2 | -# dilations | 1 | 2 | 2 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | +# stride_h | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 1 | 2 | 2 | +# stride_w | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 1 | 2 | 2 | +# dilation_h | 1 | 2 | 2 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | +# dilation_w | 1 | 2 | 2 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | +# ------------------------------------------------------------------------------ +# case id | 10 | 11 | 12 | 13 +# idt | INT8 | INT8 | INT8 | INT8 +# ifm_dim_H | 5 | 5 | 5 | 4 +# ifm_dim_W | 5 | 1 | 5 | 5 +# ifm_ch | 2 | 2 | 2 | 2 +# pad_amt | 1 | 1 | 1 | 1 +# pad_val | 0 | 0 | 0 | 0 +# k_H | 2 | 2 | 2 | 1 +# k_W | 2 | 1 | 2 | 2 +# stride_h | 1 | 2 | 1 | 1 +# stride_w | 2 | 1 | 2 | 2 +# dilation_h | 2 | 2 | 1 | 1 +# dilation_w | 2 | 2 | 2 | 2 def test_im2col_dilations(): case_id = 0 idt = DataType.INT8 k_H = 2 k_W = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 5 pad_amt = [0, 0, 0, 0] pad_val = 0 - dilation = 1 + dilation_h = 1 + dilation_w = 1 x = np.asarray( [ @@ -168,13 +175,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -185,13 +194,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 2 k_W = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 5 pad_amt = [0, 0, 0, 0] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -234,13 +245,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -251,13 +264,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 2 k_W = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 5 pad_amt = [1, 1, 1, 1] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -320,13 +335,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -337,13 +354,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 2 k_W = 2 - stride = 2 + stride_h = 2 + stride_w = 2 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 5 pad_amt = [1, 1, 1, 1] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -386,13 +405,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -403,13 +424,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 3 k_W = 3 - stride = 2 + stride_h = 2 + stride_w = 2 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 5 pad_amt = [1, 1, 1, 1] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -445,13 +468,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -462,13 +487,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 2 k_W = 1 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 1 pad_amt = [0, 0, 0, 0] pad_val = 0 - dilation = 1 + dilation_h = 1 + dilation_w = 1 x = np.asarray( [[[[1, -1]], [[2, -2]], [[3, -3]], [[4, -4]], [[5, -5]]]], @@ -485,13 +512,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -502,13 +531,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 2 k_W = 1 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 1 pad_amt = [0, 0, 0, 0] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [[[[1, -1]], [[2, -2]], [[3, -3]], [[4, -4]], [[5, -5]]]], @@ -525,13 +556,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -542,13 +575,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 2 k_W = 1 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 1 pad_amt = [1, 0, 1, 0] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [[[[1, -1]], [[2, -2]], [[3, -3]], [[4, -4]], [[5, -5]]]], @@ -573,13 +608,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -590,13 +627,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 2 k_W = 1 - stride = 2 + stride_h = 2 + stride_w = 2 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 1 pad_amt = [1, 0, 1, 0] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [[[[1, -1]], [[2, -2]], [[3, -3]], [[4, -4]], [[5, -5]]]], @@ -613,13 +652,15 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -630,13 +671,15 @@ def test_im2col_dilations(): idt = DataType.INT8 k_H = 3 k_W = 1 - stride = 2 + stride_h = 2 + stride_w = 2 ifm_ch = 2 ifm_dim_H = 5 ifm_dim_W = 1 pad_amt = [1, 0, 1, 0] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [[[[1, -1]], [[2, -2]], [[3, -3]], [[4, -4]], [[5, -5]]]], @@ -653,13 +696,298 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, + ifm_ch, + ifm_dim_H, + ifm_dim_W, + pad_amt, + pad_val, + dilation_h, + dilation_w, + ) + + assert (produced == expected).all(), "Test failed for case number {}".format( + case_id + ) + + case_id = 10 + idt = DataType.INT8 + k_H = 2 + k_W = 2 + stride_h = 1 + stride_w = 2 + ifm_ch = 2 + ifm_dim_H = 5 + ifm_dim_W = 5 + pad_amt = [1, 1, 1, 1] + pad_val = 0 + dilation_h = 2 + dilation_w = 2 + + x = np.asarray( + [ + [ + [[1, -1], [2, -2], [3, -3], [4, -4], [5, -5]], + [[6, -6], [7, -7], [8, -8], [9, -9], [10, -10]], + [[11, -11], [12, -12], [13, -13], [14, -14], [15, -15]], + [[16, -16], [17, -17], [18, -18], [19, -19], [20, -20]], + [[21, -21], [22, -22], [23, -23], [24, -24], [25, -25]], + ] + ], + dtype=np.float32, + ) + + expected = np.asarray( + [ + [ + [ + [0, 0, 0, 0, 0, 0, 7, -7], + [0, 0, 0, 0, 7, -7, 9, -9], + [0, 0, 0, 0, 9, -9, 0, 0], + ], + [ + [0, 0, 2, -2, 0, 0, 12, -12], + [2, -2, 4, -4, 12, -12, 14, -14], + [4, -4, 0, 0, 14, -14, 0, 0], + ], + [ + [0, 0, 7, -7, 0, 0, 17, -17], + [7, -7, 9, -9, 17, -17, 19, -19], + [9, -9, 0, 0, 19, -19, 0, 0], + ], + [ + [0, 0, 12, -12, 0, 0, 22, -22], + [12, -12, 14, -14, 22, -22, 24, -24], + [14, -14, 0, 0, 24, -24, 0, 0], + ], + [ + [0, 0, 17, -17, 0, 0, 0, 0], + [17, -17, 19, -19, 0, 0, 0, 0], + [19, -19, 0, 0, 0, 0, 0, 0], + ], + ] + ], + dtype=np.float32, + ) + + produced = execution_im2col( + x, + idt, + k_H, + k_W, + stride_h, + stride_w, + ifm_ch, + ifm_dim_H, + ifm_dim_W, + pad_amt, + pad_val, + dilation_h, + dilation_w, + ) + + assert (produced == expected).all(), "Test failed for case number {}".format( + case_id + ) + + case_id = 11 + idt = DataType.INT8 + k_H = 2 + k_W = 1 + stride_h = 2 + stride_w = 1 + ifm_ch = 2 + ifm_dim_H = 5 + ifm_dim_W = 1 + pad_amt = [1, 0, 1, 0] + pad_val = 0 + dilation_h = 2 + dilation_w = 2 + + x = np.asarray( + [[[[1, -1]], [[2, -2]], [[3, -3]], [[4, -4]], [[5, -5]]]], + dtype=np.float32, + ) + + expected = np.asarray( + [[[[0, 0, 2, -2]], [[2, -2, 4, -4]], [[4, -4, 0, 0]]]], + dtype=np.float32, + ) + + produced = execution_im2col( + x, + idt, + k_H, + k_W, + stride_h, + stride_w, + ifm_ch, + ifm_dim_H, + ifm_dim_W, + pad_amt, + pad_val, + dilation_h, + dilation_w, + ) + + assert (produced == expected).all(), "Test failed for case number {}".format( + case_id + ) + + case_id = 12 + idt = DataType.INT8 + k_H = 2 + k_W = 2 + stride_h = 1 + stride_w = 2 + ifm_ch = 2 + ifm_dim_H = 5 + ifm_dim_W = 5 + pad_amt = [1, 1, 1, 1] + pad_val = 0 + dilation_h = 1 + dilation_w = 2 + + x = np.asarray( + [ + [ + [[1, -1], [2, -2], [3, -3], [4, -4], [5, -5]], + [[6, -6], [7, -7], [8, -8], [9, -9], [10, -10]], + [[11, -11], [12, -12], [13, -13], [14, -14], [15, -15]], + [[16, -16], [17, -17], [18, -18], [19, -19], [20, -20]], + [[21, -21], [22, -22], [23, -23], [24, -24], [25, -25]], + ] + ], + dtype=np.float32, + ) + + expected = np.asarray( + [ + [ + [ + [0, 0, 0, 0, 0, 0, 2, -2], + [0, 0, 0, 0, 2, -2, 4, -4], + [0, 0, 0, 0, 4, -4, 0, 0], + ], + [ + [0, 0, 2, -2, 0, 0, 7, -7], + [2, -2, 4, -4, 7, -7, 9, -9], + [4, -4, 0, 0, 9, -9, 0, 0], + ], + [ + [0, 0, 7, -7, 0, 0, 12, -12], + [7, -7, 9, -9, 12, -12, 14, -14], + [9, -9, 0, 0, 14, -14, 0, 0], + ], + [ + [0, 0, 12, -12, 0, 0, 17, -17], + [12, -12, 14, -14, 17, -17, 19, -19], + [14, -14, 0, 0, 19, -19, 0, 0], + ], + [ + [0, 0, 17, -17, 0, 0, 22, -22], + [17, -17, 19, -19, 22, -22, 24, -24], + [19, -19, 0, 0, 24, -24, 0, 0], + ], + [ + [0, 0, 22, -22, 0, 0, 0, 0], + [22, -22, 24, -24, 0, 0, 0, 0], + [24, -24, 0, 0, 0, 0, 0, 0], + ], + ] + ], + dtype=np.float32, + ) + + produced = execution_im2col( + x, + idt, + k_H, + k_W, + stride_h, + stride_w, + ifm_ch, + ifm_dim_H, + ifm_dim_W, + pad_amt, + pad_val, + dilation_h, + dilation_w, + ) + + assert (produced == expected).all(), "Test failed for case number {}".format( + case_id + ) + + case_id = 13 + idt = DataType.INT8 + k_H = 2 + k_W = 3 + stride_h = 1 + stride_w = 2 + ifm_ch = 2 + ifm_dim_H = 4 + ifm_dim_W = 5 + pad_amt = [1, 1, 1, 1] + pad_val = 0 + dilation_h = 1 + dilation_w = 2 + + x = np.asarray( + [ + [ + [[1, -1], [2, -2], [3, -3], [4, -4], [5, -5]], + [[6, -6], [7, -7], [8, -8], [9, -9], [10, -10]], + [[11, -11], [12, -12], [13, -13], [14, -14], [15, -15]], + [[16, -16], [17, -17], [18, -18], [19, -19], [20, -20]], + ] + ], + dtype=np.float32, + ) + + expected = np.asarray( + [ + [ + [ + [0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 4, -4], + [0, 0, 0, 0, 0, 0, 2, -2, 4, -4, 0, 0], + ], + [ + [0, 0, 2, -2, 4, -4, 0, 0, 7, -7, 9, -9], + [2, -2, 4, -4, 0, 0, 7, -7, 9, -9, 0, 0], + ], + [ + [0, 0, 7, -7, 9, -9, 0, 0, 12, -12, 14, -14], + [7, -7, 9, -9, 0, 0, 12, -12, 14, -14, 0, 0], + ], + [ + [0, 0, 12, -12, 14, -14, 0, 0, 17, -17, 19, -19], + [12, -12, 14, -14, 0, 0, 17, -17, 19, -19, 0, 0], + ], + [ + [0, 0, 17, -17, 19, -19, 0, 0, 0, 0, 0, 0], + [17, -17, 19, -19, 0, 0, 0, 0, 0, 0, 0, 0], + ], + ] + ], + dtype=np.float32, + ) + + produced = execution_im2col( + x, + idt, + k_H, + k_W, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -677,15 +1005,18 @@ def test_im2col_dilations(): # pad_val | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | # k_H | 2 | 2 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | # k_W | 2 | 2 | 2 | 2 | 2 | 2 | 1 | 1 | 1 | -# stride | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | -# dilations | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +# stride_h | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | +# stride_w | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | +# dilation_h | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +# dilation_w | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | def test_im2col(): case_id = 0 # bipolar inputs with following im2col parameters idt = DataType.BIPOLAR k_h = 2 k_w = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 1 ifm_dim_h = 4 ifm_dim_w = 4 @@ -694,8 +1025,8 @@ def test_im2col(): pad_amt_w = pad_amt[1] + pad_amt[3] pad_val = 0 - ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride, pad_amt_h) - ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride, pad_amt_w) + ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride_h, pad_amt_h) + ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride_w, pad_amt_w) x = np.asarray( [ @@ -762,7 +1093,17 @@ def test_im2col(): ).reshape(1, ofm_dim_h, ofm_dim_w, k_h * k_w * ifm_ch) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -772,7 +1113,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 2 k_w = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_h = 4 ifm_dim_w = 4 @@ -815,7 +1157,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -825,7 +1177,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 2 k_w = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_h = 4 ifm_dim_w = 4 @@ -888,7 +1241,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -898,7 +1261,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 2 k_w = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_h = 4 ifm_dim_w = 5 @@ -944,7 +1308,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -954,7 +1328,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 3 k_w = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_h = 4 ifm_dim_w = 5 @@ -994,7 +1369,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -1004,7 +1389,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 3 k_w = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_h = 4 ifm_dim_w = 5 @@ -1064,7 +1450,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -1074,7 +1470,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 3 k_w = 1 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_h = 5 ifm_dim_w = 1 @@ -1092,7 +1489,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -1102,7 +1509,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 3 k_w = 1 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 2 ifm_dim_h = 5 ifm_dim_w = 1 @@ -1128,7 +1536,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -1138,7 +1556,8 @@ def test_im2col(): idt = DataType.INT8 k_h = 3 k_w = 1 - stride = 2 + stride_h = 2 + stride_w = 2 ifm_ch = 2 ifm_dim_h = 5 ifm_dim_w = 1 @@ -1156,7 +1575,17 @@ def test_im2col(): ) produced = execution_im2col( - x, idt, k_h, k_w, stride, ifm_ch, ifm_dim_h, ifm_dim_w, pad_amt, pad_val + x, + idt, + k_h, + k_w, + stride_h, + stride_w, + ifm_ch, + ifm_dim_h, + ifm_dim_w, + pad_amt, + pad_val, ) assert (produced == expected).all(), "Test failed for case number {}".format( case_id @@ -1167,7 +1596,8 @@ def test_im2col_infer_shapes(): idt = DataType.BIPOLAR k_h = 2 k_w = 2 - stride = 1 + stride_h = 1 + stride_w = 1 ifm_ch = 1 ifm_dim_h = 4 ifm_dim_w = 4 @@ -1176,8 +1606,8 @@ def test_im2col_infer_shapes(): pad_amt_w = pad_amt[1] + pad_amt[3] dilation = 1 - ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride, pad_amt_h, dilation) - ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride, pad_amt_w, dilation) + ofm_dim_h = compute_conv_output_dim(ifm_dim_h, k_h, stride_h, pad_amt_h, dilation) + ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride_w, pad_amt_w, dilation) # set up onnx model inp = helper.make_tensor_value_info( @@ -1194,10 +1624,10 @@ def test_im2col_infer_shapes(): ["abs"], ["im2col"], domain="finn.custom_op.general", - stride=stride, + stride=[stride_w, stride_w], kernel_size=[k_h, k_w], input_shape="(1,{},{},{})".format(ifm_dim_h, ifm_dim_w, ifm_ch), - dilations=dilation, + dilations=[dilation, dilation], ) abs1_node = helper.make_node("Abs", inputs=["im2col"], outputs=["outp"]) diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py new file mode 100644 index 0000000..92d652a --- /dev/null +++ b/tests/custom_op/test_truthtable.py @@ -0,0 +1,156 @@ +# Copyright (c) 2021 Xilinx, 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 Xilinx 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 onnx.helper as helper +from onnx import TensorProto + +import finn.core.onnx_exec as oxe +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.custom_op.registry import getCustomOp +from finn.transformation.general import GiveUniqueNodeNames +from finn.transformation.infer_datatypes import InferDataTypes +from finn.transformation.infer_shapes import InferShapes +from finn.transformation.logicnets.gen_truthtable_pla import GenTruthTablePLA +from finn.transformation.logicnets.gen_truthtable_verilog import GenTruthTableVerilog +from finn.util.data_packing import npy_to_rtlsim_input + + +def test_truthtable(): + + # Tensor with different input combinations + input_data_vector = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + ] + ) + + # Set the care set + care_set_data = np.asarray([1, 58, 15, 89, 695, 6485]) + in_bits = 16 + out_bits = 14 + results_data = np.asarray([5, 8473, 382, 8774, 9494, 9322]) + # Set input and output tensor information + in_val = helper.make_tensor_value_info("in_val", TensorProto.FLOAT, [in_bits]) + care_set = helper.make_tensor_value_info( + "care_set", TensorProto.FLOAT, [care_set_data.size] + ) + results = helper.make_tensor_value_info( + "results", TensorProto.FLOAT, [results_data.size] + ) + output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [out_bits]) + + # Define the custom node with "python" mode + custom_node = helper.make_node( + "TruthTable", + ["in_val", "care_set", "results"], + ["output"], + domain="finn.custom_op.general", + in_bits=in_bits, + out_bits=out_bits, + exec_mode="python", + ) + + # Create the graph and the model + custom_model = helper.make_model( + helper.make_graph( + [custom_node], "test_model", [in_val, care_set, results], [output] + ) + ) + # Wrap the model for finn and set the input tensor datatypes as desired + finn_model = ModelWrapper(custom_model) + finn_model.set_tensor_datatype("in_val", DataType.BINARY) + finn_model.set_tensor_datatype("care_set", DataType.UINT32) + finn_model.set_tensor_datatype("results", DataType.UINT32) + + # test output shape + finn_model = finn_model.transform(InferShapes()) + assert finn_model.get_tensor_shape("output") == [out_bits] + + # test output type + assert finn_model.get_tensor_datatype("output") is DataType.FLOAT32 + finn_model = finn_model.transform(InferDataTypes()) + assert finn_model.get_tensor_datatype("output") is DataType.BINARY + + # Give unique names to each node + finn_model = finn_model.transform(GiveUniqueNodeNames()) + + # care_set dictionary + care_set_dict = { + "care_set": care_set_data, + "results": results_data, + } + + # Generate Verilog + finn_model = finn_model.transform( + GenTruthTableVerilog(num_workers=None, care_set=care_set_dict) + ) + + # Generate PLA files + finn_model = finn_model.transform( + GenTruthTablePLA(num_workers=None, care_set=care_set_dict) + ) + # Loop over "python" and "rtlsim" execution modes + for _ in range(2): + # Loop over different input combinations + for input_data in input_data_vector: + # Create input dictionary + in_dict = { + "in_val": input_data, + "care_set": care_set_data, + "results": results_data, + } + + # Perform execution + out_dict = oxe.execute_onnx(finn_model, in_dict) + output = out_dict["output"] + + input_int = npy_to_rtlsim_input( + input_data, DataType.BINARY, in_bits, False + )[0] + + if input_int in care_set_data: + index = np.where(care_set_data == input_int)[0][0] + pred_output = results_data[index] + else: + pred_output = 0 + + output_dec = npy_to_rtlsim_input(output, DataType.BINARY, out_bits, False)[ + 0 + ] + assert output_dec == pred_output + # Change execution mode into "rtlsim" for simulation with PyVerilator + myOp = getCustomOp(finn_model.graph.node[0]) + myOp.set_nodeattr("exec_mode", "rtlsim") diff --git a/tests/logicnets-initial-model/concat_test.py b/tests/logicnets-initial-model/concat_test.py new file mode 100644 index 0000000..c5451ff --- /dev/null +++ b/tests/logicnets-initial-model/concat_test.py @@ -0,0 +1,55 @@ +import numpy as np +import onnx.helper as helper +from onnx import TensorProto + +import finn.core.onnx_exec as oxe +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.transformation.infer_datatypes import InferDataTypes +from finn.transformation.infer_shapes import InferShapes + +concat0_data = np.array([0, 0, 1], dtype=np.float32) +concat1_data = np.array([0, 1, 1], dtype=np.float32) +concat2_data = np.array([1, 1, 1], dtype=np.float32) + +concat0 = helper.make_tensor_value_info( + "concat0", TensorProto.FLOAT, [concat0_data.size] +) +concat1 = helper.make_tensor_value_info( + "concat1", TensorProto.FLOAT, [concat1_data.size] +) +concat2 = helper.make_tensor_value_info( + "concat2", TensorProto.FLOAT, [concat2_data.size] +) +output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [9]) + +concat = helper.make_node( + "Concat", + ["concat0", "concat1", "concat2"], + ["output"], + axis=0, +) + +modelproto = helper.make_model( + helper.make_graph([concat], "test_model", [concat0, concat1, concat2], [output]) +) + +model = ModelWrapper(modelproto) + +model.set_tensor_datatype("concat0", DataType.BINARY) +model.set_tensor_datatype("concat1", DataType.BINARY) +model.set_tensor_datatype("concat2", DataType.BINARY) +# model.set_tensor_datatype("output", DataType.BINARY) + +model = model.transform(InferShapes()) +model = model.transform(InferDataTypes()) + +in_dict = { + "concat0": concat0_data, + "concat1": concat1_data, + "concat2": concat2_data, +} + +out_dict = oxe.execute_onnx(model, in_dict) + +print(out_dict["output"].shape) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py new file mode 100644 index 0000000..2ef25e1 --- /dev/null +++ b/tests/logicnets-initial-model/custom_model.py @@ -0,0 +1,295 @@ +import numpy as np +import onnx.helper as helper +from onnx import TensorProto + +import finn.core.onnx_exec as oxe +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.transformation.general import GiveUniqueNodeNames +from finn.transformation.infer_datatypes import InferDataTypes +from finn.transformation.infer_shapes import InferShapes +from finn.transformation.logicnets.gen_logicnets_verilog import GenLogicNetsVerilog +from finn.util.data_packing import npy_to_rtlsim_input + +in_bits = 2 +care_set0_data = np.array([0, 1, 3], dtype=np.float32) +care_set1_data = np.array([1, 2, 3], dtype=np.float32) +indices0_data = np.array([1, 2]) +indices1_data = np.array([0, 1]) +indices_in0_data = np.array([0, 1]) +indices_in1_data = np.array([2, 3]) +indices_in2_data = np.array([4, 5]) + +general_input_data = np.array([0, 0, 0, 0, 0, 0], dtype=np.float32) + +general_input = helper.make_tensor_value_info( + "general_input", TensorProto.FLOAT, general_input_data.shape +) + +care_set0 = helper.make_tensor_value_info( + "care_set0", TensorProto.FLOAT, care_set0_data.shape +) +care_set1 = helper.make_tensor_value_info( + "care_set1", TensorProto.FLOAT, care_set1_data.shape +) +general_output = helper.make_tensor_value_info("general_output", TensorProto.FLOAT, [2]) + +indices0 = helper.make_tensor_value_info( + "indices0", TensorProto.INT64, indices0_data.shape +) +indices1 = helper.make_tensor_value_info( + "indices1", TensorProto.INT64, indices1_data.shape +) +indices_in0 = helper.make_tensor_value_info( + "indices_in0", TensorProto.INT64, indices_in0_data.shape +) +indices_in1 = helper.make_tensor_value_info( + "indices_in1", TensorProto.INT64, indices_in1_data.shape +) +indices_in2 = helper.make_tensor_value_info( + "indices_in2", TensorProto.INT64, indices_in2_data.shape +) + + +gather_in0 = helper.make_node( + "Gather", + ["general_input", "indices_in0"], + ["LUTin0"], +) + +gather_in1 = helper.make_node( + "Gather", + ["general_input", "indices_in1"], + ["LUTin1"], +) + +gather_in2 = helper.make_node( + "Gather", + ["general_input", "indices_in2"], + ["LUTin2"], +) + +LUT0 = helper.make_node( + "BinaryTruthTable", + ["LUTin0", "care_set0"], + ["concat_in0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT1 = helper.make_node( + "BinaryTruthTable", + ["LUTin1", "care_set0"], + ["concat_in1"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT2 = helper.make_node( + "BinaryTruthTable", + ["LUTin2", "care_set0"], + ["concat_in2"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT3 = helper.make_node( + "BinaryTruthTable", + ["sparse_out0", "care_set1"], + ["out0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT4 = helper.make_node( + "BinaryTruthTable", + ["sparse_out1", "care_set1"], + ["out1"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +concat0 = helper.make_node( + "Concat", + ["concat_in0", "concat_in1", "concat_in2"], + ["concat_out"], + axis=0, +) + +concat_out = helper.make_node( + "Concat", + ["out0", "out1"], + ["general_output"], + axis=0, +) + +gather0 = helper.make_node( + "Gather", + ["concat_out", "indices0"], + ["sparse_out0"], +) + +gather1 = helper.make_node( + "Gather", + ["concat_out", "indices1"], + ["sparse_out1"], +) + +graph = helper.make_graph( + nodes=[ + gather_in0, + concat0, + gather0, + LUT2, + LUT3, + LUT4, + gather1, + concat_out, + LUT0, + LUT1, + gather_in1, + gather_in2, + ], + name="my_LogicNets model", + inputs=[ + care_set0, + care_set1, + indices0, + indices1, + indices_in0, + indices_in1, + indices_in2, + general_input, + ], + outputs=[general_output], + value_info=[ + helper.make_tensor_value_info("LUTin0", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("LUTin1", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("LUTin2", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("concat_in0", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("concat_in1", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("concat_in2", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("concat_out", TensorProto.FLOAT, [3]), + helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info( + "sparse_out0", TensorProto.FLOAT, indices0_data.shape + ), + helper.make_tensor_value_info( + "sparse_out1", TensorProto.FLOAT, indices1_data.shape + ), + ], +) + +modelproto = helper.make_model(graph, producer_name="simple-model") + + +def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bits): + + in0 = input_data[0:1] + in1 = input_data[2:3] + in2 = input_data[4:5] + + in0_int = npy_to_rtlsim_input(in0, DataType.BINARY, in_bits, False)[0] + in1_int = npy_to_rtlsim_input(in1, DataType.BINARY, in_bits, False)[0] + in2_int = npy_to_rtlsim_input(in2, DataType.BINARY, in_bits, False)[0] + + concat_in0 = 1 if in0_int in care_set0 else 0 + concat_in1 = 1 if in1_int in care_set0 else 0 + concat_in2 = 1 if in2_int in care_set0 else 0 + + concat_out = [int(concat_in0), int(concat_in1), int(concat_in2)] + + sparse_out0 = np.array([concat_out[int(indices0[0])], concat_out[int(indices0[1])]]) + sparse_out1 = np.array([concat_out[indices1[0]], concat_out[indices1[1]]]) + + sparse_out0_int = npy_to_rtlsim_input(sparse_out0, DataType.BINARY, in_bits, False)[ + 0 + ] + sparse_out1_int = npy_to_rtlsim_input(sparse_out1, DataType.BINARY, in_bits, False)[ + 0 + ] + + out0 = 1 if sparse_out0_int in care_set1 else 0 + out1 = 1 if sparse_out1_int in care_set1 else 0 + + return np.array([out0, out1]) + + +input_dict = { + "care_set0": care_set0_data, + "general_input": general_input_data, + "care_set1": care_set1_data, + "indices0": indices0_data, + "indices1": indices1_data, + "indices_in0": indices_in0_data, + "indices_in1": indices_in1_data, + "indices_in2": indices_in2_data, +} + +model = ModelWrapper(modelproto) + +model.set_tensor_datatype("general_input", DataType.BINARY) +model.set_tensor_datatype("LUTin0", DataType.BINARY) +model.set_tensor_datatype("LUTin1", DataType.BINARY) +model.set_tensor_datatype("LUTin2", DataType.BINARY) +model.set_tensor_datatype("concat_in0", DataType.BINARY) +model.set_tensor_datatype("concat_in1", DataType.BINARY) +model.set_tensor_datatype("concat_in2", DataType.BINARY) +model.set_tensor_datatype("sparse_out0", DataType.BINARY) +model.set_tensor_datatype("sparse_out1", DataType.BINARY) +model.set_tensor_datatype("care_set0", DataType.UINT32) +model.set_tensor_datatype("care_set1", DataType.UINT32) +model.set_tensor_datatype("concatenated_input", DataType.UINT32) +model.set_tensor_datatype("indices_in0", DataType.UINT32) +model.set_tensor_datatype("indices_in1", DataType.UINT32) +model.set_tensor_datatype("indices_in2", DataType.UINT32) +model.set_tensor_datatype("indices0", DataType.UINT32) +model.set_tensor_datatype("indices1", DataType.UINT32) +model.set_tensor_datatype("general_output", DataType.BINARY) + +model = model.transform(InferShapes()) +model.save("after-shape.onnx") + +model = model.transform(InferDataTypes()) +model.save("after-datatypes.onnx") + +model = model.transform(GiveUniqueNodeNames()) +model.save("after-uniquenames.onnx") + +care_set_dict = { + "care_set0": care_set0_data, + "care_set1": care_set1_data, +} + +indices_dict = { + "indices_in0": indices_in0_data, + "indices_in1": indices_in1_data, + "indices_in2": indices_in2_data, + "indices0": indices0_data, + "indices1": indices1_data, +} + +model = model.transform( + GenLogicNetsVerilog(care_set=care_set_dict, indices=indices_dict) +) + +out = oxe.execute_onnx(model, input_dict) + +output = out["general_output"] + +expected = expected_output( + general_input_data, + indices0_data, + indices1_data, + care_set0_data, + care_set1_data, + in_bits, +) + +assert (output == expected).all diff --git a/tests/transformation/test_4d_conversion.py b/tests/transformation/test_4d_conversion.py index 9850834..18fe9cc 100644 --- a/tests/transformation/test_4d_conversion.py +++ b/tests/transformation/test_4d_conversion.py @@ -24,7 +24,7 @@ def generate_random_input(model): def set_all_initializers(model): - """ Sets all initializers of the graph to a random value. """ + """Sets all initializers of the graph to a random value.""" for n in model.graph.node: if len(n.input) > 1: init_name = n.input[1] diff --git a/tests/transformation/test_batchnorm_to_affine.py b/tests/transformation/test_batchnorm_to_affine.py index 09e4fc8..4adc874 100644 --- a/tests/transformation/test_batchnorm_to_affine.py +++ b/tests/transformation/test_batchnorm_to_affine.py @@ -70,7 +70,7 @@ def test_batchnorm_to_affine_shufflenet(): @pytest.mark.parametrize("epsilon", [0.0, 0.00001, 0.001]) def test_batchnorm_to_affine_epsilon(epsilon): - """ Dummy batchnorm node to test out the epsilon attribute. """ + """Dummy batchnorm node to test out the epsilon attribute.""" batchnorm_node = onnx.helper.make_node( "BatchNormalization", diff --git a/tests/transformation/test_conv_lowering.py b/tests/transformation/test_conv_lowering.py index ef5e133..604acf7 100644 --- a/tests/transformation/test_conv_lowering.py +++ b/tests/transformation/test_conv_lowering.py @@ -80,7 +80,7 @@ def test_conv_lowering_convmnist(): # input channels @pytest.mark.parametrize("ifm_ch", [2, 3]) # stride -@pytest.mark.parametrize("stride", [1, 2]) +@pytest.mark.parametrize("stride", [[1, 1], [1, 2], [2, 1], [2, 2]]) # padding @pytest.mark.parametrize("padding", [[0, 0, 0, 0], [1, 1, 1, 1]]) # dilations @@ -95,10 +95,8 @@ def test_dws_reg_conv_lowering( if k_w > ifm_dim_w: pytest.skip("Kernel width must be smaller than image height") # Ensure the right padding parameters are set - if ifm_dim_h == 1: - padding[0] = 0 - padding[2] = 0 if ifm_dim_w == 1: + dilations[1] = 1 padding[1] = 0 padding[3] = 0 @@ -107,20 +105,22 @@ def test_dws_reg_conv_lowering( ofm_ch = ifm_ch pad_h = padding[0] + padding[2] pad_w = padding[1] + padding[3] + stride_h = stride[0] + stride_w = stride[1] ofm_dim_h = compute_conv_output_dim( ifm_dim_h, k_h, - stride, + stride_h, pad_h, dilations[0], ) ofm_dim_w = compute_conv_output_dim( ifm_dim_w, k_w, - stride, + stride_w, pad_w, - dilations[0], + dilations[1], ) # set up onnx model @@ -146,7 +146,7 @@ def test_dws_reg_conv_lowering( outputs=["outp"], kernel_shape=[k_h, k_w], pads=padding, - strides=[stride, stride], + strides=[stride_h, stride_w], group=group, dilations=dilations, ) diff --git a/tests/transformation/test_extend_partition.py b/tests/transformation/test_extend_partition.py new file mode 100644 index 0000000..fa7f288 --- /dev/null +++ b/tests/transformation/test_extend_partition.py @@ -0,0 +1,322 @@ +# Copyright (c) 2020 Xilinx, 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 Xilinx 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 pytest + +import numpy as np +from onnx import TensorProto +from onnx import helper as oh + +import finn.core.onnx_exec as oxe +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.transformation.create_generic_partitions import PartitionFromDict +from finn.transformation.extend_partition import ExtendPartition +from finn.util.basic import gen_finn_dt_tensor + + +def create_model(): + MultiThreshold0_node = oh.make_node( + "MultiThreshold", + inputs=["in1_multithreshold0", "in2_multithreshold0"], + outputs=["out_multithreshold0"], + name="MultiThreshold0", + domain="finn.custom_op.general", + out_dtype="UINT4", + ) + + Conv0_node = oh.make_node( + "Conv", + inputs=["out_multithreshold0", "in2_conv0"], + outputs=["out_conv0"], + name="Conv0", + dilations=[1, 1], + group=1, + kernel_shape=[1, 1], + pads=[0, 0, 0, 0], + strides=[1, 1], + ) + + Conv1_node = oh.make_node( + "Conv", + inputs=["out_multithreshold0", "in2_conv1"], + outputs=["out_conv1"], + name="Conv1", + dilations=[1, 1], + group=1, + kernel_shape=[1, 1], + pads=[0, 0, 0, 0], + strides=[1, 1], + ) + + MultiThreshold1_node = oh.make_node( + "MultiThreshold", + inputs=["out_conv0", "in2_multithreshold1"], + outputs=["out_multithreshold1"], + name="MultiThreshold1", + domain="finn.custom_op.general", + out_dtype="UINT4", + ) + + MultiThreshold2_node = oh.make_node( + "MultiThreshold", + inputs=["out_conv1", "in2_multithreshold2"], + outputs=["out_multithreshold2"], + name="MultiThreshold2", + domain="finn.custom_op.general", + out_dtype="UINT4", + ) + + Add0_node = oh.make_node( + "Add", + inputs=["out_multithreshold1", "out_multithreshold2"], + outputs=["out_add0"], + name="Add0", + ) + + MultiThreshold3_node = oh.make_node( + "MultiThreshold", + inputs=["out_add0", "in2_multithreshold3"], + outputs=["out_multithreshold3"], + name="MultiThreshold3", + domain="finn.custom_op.general", + out_dtype="UINT4", + ) + + Conv2_node = oh.make_node( + "Conv", + inputs=["out_multithreshold3", "in2_conv2"], + outputs=["out_conv2"], + name="Conv2", + dilations=[1, 1], + group=1, + kernel_shape=[1, 1], + pads=[0, 0, 0, 0], + strides=[1, 1], + ) + + Conv3_node = oh.make_node( + "Conv", + inputs=["out_multithreshold3", "in2_conv3"], + outputs=["out_conv3"], + name="Conv3", + dilations=[1, 1], + group=1, + kernel_shape=[1, 1], + pads=[0, 0, 0, 0], + strides=[1, 1], + ) + + MultiThreshold4_node = oh.make_node( + "MultiThreshold", + inputs=["out_conv2", "in2_multithreshold4"], + outputs=["out_multithreshold4"], + name="MultiThreshold4", + domain="finn.custom_op.general", + out_dtype="UINT4", + ) + + MultiThreshold5_node = oh.make_node( + "MultiThreshold", + inputs=["out_conv3", "in2_multithreshold5"], + outputs=["out_multithreshold5"], + name="MultiThreshold5", + domain="finn.custom_op.general", + out_dtype="UINT4", + ) + + Add1_node = oh.make_node( + "Add", + inputs=["out_multithreshold4", "out_multithreshold5"], + outputs=["out_add1"], + name="Add1", + ) + + # Inputs/outputs (global) + t_type = TensorProto.FLOAT + t_shape = [1, 256, 128, 1] + in1_multithreshold0 = oh.make_tensor_value_info( + "in1_multithreshold0", t_type, t_shape + ) + out_add1 = oh.make_tensor_value_info("out_add1", t_type, t_shape) + + # Initializers + in2_multithreshold0 = oh.make_tensor_value_info( + "in2_multithreshold0", t_type, [256, 15] + ) + in2_conv0 = oh.make_tensor_value_info("in2_conv0", t_type, [256, 256, 1, 1]) + in2_conv1 = oh.make_tensor_value_info("in2_conv1", t_type, [256, 256, 1, 1]) + in2_multithreshold1 = oh.make_tensor_value_info( + "in2_multithreshold1", t_type, [256, 15] + ) + in2_multithreshold2 = oh.make_tensor_value_info( + "in2_multithreshold2", t_type, [256, 15] + ) + in2_multithreshold3 = oh.make_tensor_value_info( + "in2_multithreshold3", t_type, [256, 15] + ) + in2_conv2 = oh.make_tensor_value_info("in2_conv2", t_type, [256, 256, 1, 1]) + in2_conv3 = oh.make_tensor_value_info("in2_conv3", t_type, [256, 256, 1, 1]) + in2_multithreshold4 = oh.make_tensor_value_info( + "in2_multithreshold4", t_type, [256, 15] + ) + in2_multithreshold5 = oh.make_tensor_value_info( + "in2_multithreshold5", t_type, [256, 15] + ) + + # Value_infos + out_multithreshold0 = oh.make_tensor_value_info( + "out_multithreshold0", t_type, t_shape + ) + out_conv0 = oh.make_tensor_value_info("out_conv0", t_type, t_shape) + out_conv1 = oh.make_tensor_value_info("out_conv1", t_type, t_shape) + out_multithreshold1 = oh.make_tensor_value_info( + "out_multithreshold1", t_type, t_shape + ) + out_multithreshold2 = oh.make_tensor_value_info( + "out_multithreshold2", t_type, t_shape + ) + out_add0 = oh.make_tensor_value_info("out_add0", t_type, t_shape) + out_multithreshold3 = oh.make_tensor_value_info( + "out_multithreshold3", t_type, t_shape + ) + out_conv2 = oh.make_tensor_value_info("out_conv2", t_type, t_shape) + out_conv3 = oh.make_tensor_value_info("out_conv3", t_type, t_shape) + out_multithreshold4 = oh.make_tensor_value_info( + "out_multithreshold4", t_type, t_shape + ) + out_multithreshold5 = oh.make_tensor_value_info( + "out_multithreshold5", t_type, t_shape + ) + + graph = oh.make_graph( + nodes=[ + MultiThreshold0_node, + Conv0_node, + Conv1_node, + MultiThreshold1_node, + MultiThreshold2_node, + Add0_node, + MultiThreshold3_node, + Conv2_node, + Conv3_node, + MultiThreshold4_node, + MultiThreshold5_node, + Add1_node, + ], + name="test_graph", + inputs=[in1_multithreshold0], + outputs=[out_add1], + value_info=[ + in2_multithreshold0, + in2_conv0, + in2_conv1, + in2_multithreshold1, + in2_multithreshold2, + in2_multithreshold3, + in2_conv2, + in2_conv3, + in2_multithreshold4, + in2_multithreshold5, + out_multithreshold0, + out_conv0, + out_conv1, + out_multithreshold1, + out_multithreshold2, + out_add0, + out_multithreshold3, + out_conv2, + out_conv3, + out_multithreshold4, + out_multithreshold5, + ], + ) + + onnx_model = oh.make_model(graph, producer_name="test_model") + model = ModelWrapper(onnx_model) + + mt_weights = np.random.randint(low=-1000, high=1000, size=[6, 256, 15]) + mt_weights = np.sort(mt_weights, 2) + for i in range(0, 6): + model.set_initializer("in2_multithreshold" + str(i), mt_weights[i]) + + conv_weights = np.random.randint(low=-8, high=7, size=[4, 256, 256, 1, 1]).astype( + np.float32 + ) + for i in range(0, 4): + model.set_initializer("in2_conv" + str(i), conv_weights[i]) + model.set_tensor_datatype("in2_conv" + str(i), DataType.INT4) + + return model + + +# Partitioning +@pytest.mark.parametrize("p", [0, 1, 2]) +# Extending +@pytest.mark.parametrize("extend_id", [[0], [1], [0, 1]]) +def test_extend_partition(p, extend_id): + if p == 0: + if extend_id != [0]: + pytest.skip("Only the first partition node can be extended") + if p == 1: + if extend_id != [1]: + pytest.skip("Only the second partition node can be extended") + else: + extend_id = [6] # The 6th node is the index of the GenericPartition + # node, so we set the index to the right value + + model = create_model() + + # Partition the model first + partitionings = [ + {0: range(0, 6)}, + {0: range(6, 12)}, + {0: range(0, 6), 1: range(6, 12)}, + ] + partitioning = partitionings[p] + + model = model.transform(PartitionFromDict(partitioning)) + + # Create input data + input0_tensor_name = model.graph.input[0].name + + input_shape = model.get_tensor_shape(input0_tensor_name) + input_dtype = model.get_tensor_datatype(input0_tensor_name) + input_val = gen_finn_dt_tensor(input_dtype, input_shape) + input_dict = {} + input_dict[input0_tensor_name] = input_val + + # Extend the model + model_extended = model.transform(ExtendPartition(extend_id)) + + assert oxe.compare_execution(model, model_extended, input_dict) + + # Check if FINN data_types are retained + for n in model_extended.graph.node: + if n.op_type == "Conv": + assert model_extended.get_tensor_datatype(n.input[1]) == DataType.INT4 diff --git a/tests/transformation/test_general_transformation.py b/tests/transformation/test_general_transformation.py index ca93354..97536bd 100644 --- a/tests/transformation/test_general_transformation.py +++ b/tests/transformation/test_general_transformation.py @@ -138,12 +138,12 @@ def test_apply_config(): model = model.transform(GiveUniqueNodeNames()) # set up a config in a dict, then dump it to JSON config = {} - config["Defaults"] = {"kernel_size": [[3], ["Im2Col"]]} - config["Im2Col_0"] = {"kernel_size": [7]} + config["Defaults"] = {"kernel_size": [[3, 3], ["Im2Col"]]} + config["Im2Col_0"] = {"kernel_size": [7, 7]} with open("config.json", "w") as f: json.dump(config, f, indent=4) model = model.transform(ApplyConfig("config.json")) # check model - assert getCustomOp(model.graph.node[2]).get_nodeattr("kernel_size") == [7] - assert getCustomOp(model.graph.node[9]).get_nodeattr("kernel_size") == [3] + assert getCustomOp(model.graph.node[2]).get_nodeattr("kernel_size") == [7, 7] + assert getCustomOp(model.graph.node[9]).get_nodeattr("kernel_size") == [3, 3] os.remove("config.json") diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py new file mode 100644 index 0000000..cc3d248 --- /dev/null +++ b/tests/transformation/test_logicnets_verilog.py @@ -0,0 +1,308 @@ +import numpy as np +import onnx.helper as helper +from onnx import TensorProto +from pyverilator import PyVerilator + +import finn.core.onnx_exec as oxe +from finn.core.datatype import DataType +from finn.core.modelwrapper import ModelWrapper +from finn.transformation.general import GiveUniqueNodeNames +from finn.transformation.infer_datatypes import InferDataTypes +from finn.transformation.infer_shapes import InferShapes +from finn.transformation.logicnets.gen_logicnets_verilog import GenLogicNetsVerilog +from finn.util.data_packing import npy_to_rtlsim_input + + +def test_logicnets_verilog(): + in_bits = 2 + care_set0_data = np.array([0, 1, 3], dtype=np.float32) + care_set1_data = np.array([1, 2, 3], dtype=np.float32) + indices0_data = np.array([1, 2]) + indices1_data = np.array([0, 1]) + indices_in0_data = np.array([0, 1]) + indices_in1_data = np.array([2, 3]) + indices_in2_data = np.array([4, 5]) + + general_input_data = np.array([0, 0, 0, 0, 0, 0], dtype=np.float32) + + general_input = helper.make_tensor_value_info( + "general_input", TensorProto.FLOAT, general_input_data.shape + ) + + care_set0 = helper.make_tensor_value_info( + "care_set0", TensorProto.FLOAT, care_set0_data.shape + ) + care_set1 = helper.make_tensor_value_info( + "care_set1", TensorProto.FLOAT, care_set1_data.shape + ) + general_output = helper.make_tensor_value_info( + "general_output", TensorProto.FLOAT, [2] + ) + + indices0 = helper.make_tensor_value_info( + "indices0", TensorProto.INT64, indices0_data.shape + ) + indices1 = helper.make_tensor_value_info( + "indices1", TensorProto.INT64, indices1_data.shape + ) + indices_in0 = helper.make_tensor_value_info( + "indices_in0", TensorProto.INT64, indices_in0_data.shape + ) + indices_in1 = helper.make_tensor_value_info( + "indices_in1", TensorProto.INT64, indices_in1_data.shape + ) + indices_in2 = helper.make_tensor_value_info( + "indices_in2", TensorProto.INT64, indices_in2_data.shape + ) + + gather_in0 = helper.make_node( + "Gather", + ["general_input", "indices_in0"], + ["LUTin0"], + ) + + gather_in1 = helper.make_node( + "Gather", + ["general_input", "indices_in1"], + ["LUTin1"], + ) + + gather_in2 = helper.make_node( + "Gather", + ["general_input", "indices_in2"], + ["LUTin2"], + ) + + LUT0 = helper.make_node( + "BinaryTruthTable", + ["LUTin0", "care_set0"], + ["concat_in0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", + ) + + LUT1 = helper.make_node( + "BinaryTruthTable", + ["LUTin1", "care_set0"], + ["concat_in1"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", + ) + + LUT2 = helper.make_node( + "BinaryTruthTable", + ["LUTin2", "care_set0"], + ["concat_in2"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", + ) + + LUT3 = helper.make_node( + "BinaryTruthTable", + ["sparse_out0", "care_set1"], + ["out0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", + ) + + LUT4 = helper.make_node( + "BinaryTruthTable", + ["sparse_out1", "care_set1"], + ["out1"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", + ) + + concat0 = helper.make_node( + "Concat", + ["concat_in0", "concat_in1", "concat_in2"], + ["concat_out"], + axis=0, + ) + + concat_out = helper.make_node( + "Concat", + ["out0", "out1"], + ["general_output"], + axis=0, + ) + + gather0 = helper.make_node( + "Gather", + ["concat_out", "indices0"], + ["sparse_out0"], + ) + + gather1 = helper.make_node( + "Gather", + ["concat_out", "indices1"], + ["sparse_out1"], + ) + + graph = helper.make_graph( + nodes=[ + gather_in0, + concat0, + gather0, + LUT2, + LUT3, + LUT4, + gather1, + concat_out, + LUT0, + LUT1, + gather_in1, + gather_in2, + ], + name="my_LogicNets model", + inputs=[ + care_set0, + care_set1, + indices0, + indices1, + indices_in0, + indices_in1, + indices_in2, + general_input, + ], + outputs=[general_output], + value_info=[ + helper.make_tensor_value_info("LUTin0", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("LUTin1", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("LUTin2", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("concat_in0", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("concat_in1", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("concat_in2", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("concat_out", TensorProto.FLOAT, [3]), + helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info( + "sparse_out0", TensorProto.FLOAT, indices0_data.shape + ), + helper.make_tensor_value_info( + "sparse_out1", TensorProto.FLOAT, indices1_data.shape + ), + ], + ) + + modelproto = helper.make_model(graph, producer_name="simple-model") + + def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bits): + + in0 = input_data[0:1] + in1 = input_data[2:3] + in2 = input_data[4:5] + + in0_int = npy_to_rtlsim_input(in0, DataType.BINARY, in_bits, False)[0] + in1_int = npy_to_rtlsim_input(in1, DataType.BINARY, in_bits, False)[0] + in2_int = npy_to_rtlsim_input(in2, DataType.BINARY, in_bits, False)[0] + + concat_in0 = 1 if in0_int in care_set0 else 0 + concat_in1 = 1 if in1_int in care_set0 else 0 + concat_in2 = 1 if in2_int in care_set0 else 0 + + concat_out = [int(concat_in0), int(concat_in1), int(concat_in2)] + + sparse_out0 = np.array( + [concat_out[int(indices0[0])], concat_out[int(indices0[1])]] + ) + sparse_out1 = np.array([concat_out[indices1[0]], concat_out[indices1[1]]]) + + sparse_out0_int = npy_to_rtlsim_input( + sparse_out0, DataType.BINARY, in_bits, False + )[0] + sparse_out1_int = npy_to_rtlsim_input( + sparse_out1, DataType.BINARY, in_bits, False + )[0] + + out0 = 1 if sparse_out0_int in care_set1 else 0 + out1 = 1 if sparse_out1_int in care_set1 else 0 + + return np.array([out0, out1]) + + input_dict = { + "care_set0": care_set0_data, + "general_input": general_input_data, + "care_set1": care_set1_data, + "indices0": indices0_data, + "indices1": indices1_data, + "indices_in0": indices_in0_data, + "indices_in1": indices_in1_data, + "indices_in2": indices_in2_data, + } + + model = ModelWrapper(modelproto) + + model.set_tensor_datatype("general_input", DataType.BINARY) + model.set_tensor_datatype("LUTin0", DataType.BINARY) + model.set_tensor_datatype("LUTin1", DataType.BINARY) + model.set_tensor_datatype("LUTin2", DataType.BINARY) + model.set_tensor_datatype("concat_in0", DataType.BINARY) + model.set_tensor_datatype("concat_in1", DataType.BINARY) + model.set_tensor_datatype("concat_in2", DataType.BINARY) + model.set_tensor_datatype("concat_out", DataType.BINARY) + model.set_tensor_datatype("sparse_out0", DataType.BINARY) + model.set_tensor_datatype("sparse_out1", DataType.BINARY) + model.set_tensor_datatype("care_set0", DataType.UINT32) + model.set_tensor_datatype("care_set1", DataType.UINT32) + model.set_tensor_datatype("indices_in0", DataType.UINT32) + model.set_tensor_datatype("indices_in1", DataType.UINT32) + model.set_tensor_datatype("indices_in2", DataType.UINT32) + model.set_tensor_datatype("indices0", DataType.UINT32) + model.set_tensor_datatype("indices1", DataType.UINT32) + model.set_tensor_datatype("general_output", DataType.BINARY) + + model = model.transform(InferShapes()) + + model = model.transform(InferDataTypes()) + + model = model.transform(GiveUniqueNodeNames()) + + care_set_dict = { + "care_set0": care_set0_data, + "care_set1": care_set1_data, + } + + indices_dict = { + "indices_in0": indices_in0_data, + "indices_in1": indices_in1_data, + "indices_in2": indices_in2_data, + "indices0": indices0_data, + "indices1": indices1_data, + } + + model = model.transform( + GenLogicNetsVerilog(care_set=care_set_dict, indices=indices_dict) + ) + + out = oxe.execute_onnx(model, input_dict) + + output = out["general_output"] + + expected = expected_output( + general_input_data, + indices0_data, + indices1_data, + care_set0_data, + care_set1_data, + in_bits, + ) + + assert np.array_equal(output, expected) + + # PyVerilator simulation of generated Verilog code + code_dir = model.get_metadata_prop("code_dir") + print(code_dir) + verilog_dir = code_dir + "/" + "LogicNetsModule.v" + sim = PyVerilator.build(verilog_dir) + in_value = npy_to_rtlsim_input(general_input_data, DataType.BINARY, 6, False)[0] + sim.io["general_input"] = in_value + output_value_int = sim.io["general_output"] + output_value_bin = np.array(list(np.binary_repr(output_value_int))).astype(np.int8) + + assert np.array_equal(output_value_bin, expected) diff --git a/tests/transformation/test_merge_onnx_models.py b/tests/transformation/test_merge_onnx_models.py index 46dd0ef..aab0802 100644 --- a/tests/transformation/test_merge_onnx_models.py +++ b/tests/transformation/test_merge_onnx_models.py @@ -81,7 +81,7 @@ def test_merge_onnx_models(): model2.set_initializer("a1", a1_value) # set a dummy sparsity annotation to check if it gets correctly transferred # to the merged model - sparsity = {"dw": {"kernel_shape": 0}} + sparsity = {"dw": {"kernel_shape": [0, 0]}} model2.set_tensor_sparsity("a1", sparsity) model2 = model2.transform(InferShapes()) model2 = model2.transform(InferDataTypes())