From 992ed0243cdea133edd0708298d690d999e2aeb7 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 17 Feb 2021 15:57:17 +0000 Subject: [PATCH 01/85] Add TruthTable custom operation --- src/finn/custom_op/general/__init__.py | 2 + src/finn/custom_op/general/truthtable.py | 98 ++++++++++++++++++++++++ tests/custom_op/test_truthtable.py | 85 ++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 src/finn/custom_op/general/truthtable.py create mode 100644 tests/custom_op/test_truthtable.py diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 1d1770f..7792370 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,6 +33,7 @@ from finn.custom_op.general.quantavgpool2d import QuantAvgPool2d from finn.custom_op.general.streamingdataflowpartition import StreamingDataflowPartition from finn.custom_op.general.xnorpopcount import XnorPopcountMatMul +from finn.custom_op.general.truthtable import TruthTable custom_op = dict() @@ -43,3 +44,4 @@ custom_op["MultiThreshold"] = MultiThreshold custom_op["XnorPopcountMatMul"] = XnorPopcountMatMul custom_op["Im2Col"] = Im2Col +custom_op["TruthTable"] = TruthTable diff --git a/src/finn/custom_op/general/truthtable.py b/src/finn/custom_op/general/truthtable.py new file mode 100644 index 0000000..7b9ac33 --- /dev/null +++ b/src/finn/custom_op/general/truthtable.py @@ -0,0 +1,98 @@ +import numpy as np +import onnx.helper as helper + +from finn.core.datatype import DataType +from finn.custom_op.base import CustomOp + + +def truthtable(inputs, results): + """Returns the output to a combination of x-bit input value. The results array + reflect the 1 values in the truth table result. If 5 is provided in the result vector, + the result to fifth combination of inputs 101 is 1. The input is a vector size x, representing + x-bits binary input. An example is presented: + + inputs = [1, 0, 1] + results = [1, 2] + + Possible combinations: 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 + + """ + inputs = inputs[::-1] #reverse input array for C style indexing + + in_int = 0 #integer representation of the binary input + + for idx,in_val in enumerate(inputs): + in_int += ((1< Date: Mon, 22 Feb 2021 15:17:29 +0000 Subject: [PATCH 02/85] Move truthtables CustomOp into logicnets folder --- src/finn/custom_op/general/__init__.py | 2 +- .../{general => logicnets}/truthtable.py | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) rename src/finn/custom_op/{general => logicnets}/truthtable.py (73%) diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 7792370..d650eac 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,7 +33,7 @@ from finn.custom_op.general.quantavgpool2d import QuantAvgPool2d from finn.custom_op.general.streamingdataflowpartition import StreamingDataflowPartition from finn.custom_op.general.xnorpopcount import XnorPopcountMatMul -from finn.custom_op.general.truthtable import TruthTable +from finn.custom_op.logicnets.truthtable import TruthTable custom_op = dict() diff --git a/src/finn/custom_op/general/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py similarity index 73% rename from src/finn/custom_op/general/truthtable.py rename to src/finn/custom_op/logicnets/truthtable.py index 7b9ac33..9ec78f7 100644 --- a/src/finn/custom_op/general/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,4 +1,3 @@ -import numpy as np import onnx.helper as helper from finn.core.datatype import DataType @@ -7,9 +6,9 @@ def truthtable(inputs, results): """Returns the output to a combination of x-bit input value. The results array - reflect the 1 values in the truth table result. If 5 is provided in the result vector, - the result to fifth combination of inputs 101 is 1. The input is a vector size x, representing - x-bits binary input. An example is presented: + reflect the 1 values in the truth table result. If 5 is provided in the result + vector, the result to fifth combination of inputs 101 is 1. The input is a + vector size x, representing x-bits binary input. An example is presented: inputs = [1, 0, 1] results = [1, 2] @@ -24,54 +23,57 @@ def truthtable(inputs, results): 1 0 1 | 0 1 1 0 | 0 1 1 1 | 0 - + """ - inputs = inputs[::-1] #reverse input array for C style indexing - - in_int = 0 #integer representation of the binary input + inputs = inputs[::-1] # reverse input array for C style indexing + + in_int = 0 # integer representation of the binary input - for idx,in_val in enumerate(inputs): - in_int += ((1< Date: Mon, 22 Feb 2021 15:27:15 +0000 Subject: [PATCH 03/85] Reformat truthtable testing function --- tests/custom_op/test_truthtable.py | 32 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index 7b25fa6..6945e09 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -41,12 +41,16 @@ def test_truthtable(): - inputs = helper.make_tensor_value_info("inputs", TensorProto.FLOAT, [10]) #Input bitwidth 10 - results = helper.make_tensor_value_info("results", TensorProto.FLOAT, [5]) #5 results are 1 among all possible combinations + inputs = helper.make_tensor_value_info( + "inputs", TensorProto.FLOAT, [10] + ) # Input bitwidth 10 + results = helper.make_tensor_value_info( + "results", TensorProto.FLOAT, [5] + ) # 5 results are 1 among all possible combinations output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) node_def = helper.make_node( - "TruthTable", ["inputs", "results"], ["output"], domain = "finn.custom_op.general" + "TruthTable", ["inputs", "results"], ["output"], domain="finn.custom_op.general" ) modelproto = helper.make_model( helper.make_graph([node_def], "test_model", [inputs, results], [output]) @@ -55,31 +59,25 @@ def test_truthtable(): model = ModelWrapper(modelproto) model.set_tensor_datatype("inputs", DataType.BINARY) model.set_tensor_datatype("results", DataType.UINT32) - #test output shape + # test output shape model = model.transform(InferShapes()) assert model.get_tensor_shape("output") == [1] - #test output type + # 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 - #perform execution - input_data = np.asarray([1,0,0,1,1,0,0,0,1,1], dtype=np.float32) - results_data = np.asarray([5,8,14,198,611], dtype=np.float32) + # perform execution + input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) + results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) in_dict = {"inputs": input_data, "results": results_data} out_dict = oxe.execute_onnx(model, in_dict) - #calculate result here for comparison with the custom op + # calculate result here for comparison with the custom op input_data = input_data[::-1] out_idx = 0 for idx, val in enumerate(input_data): - out_idx += ((1< Date: Tue, 23 Feb 2021 16:32:39 +0000 Subject: [PATCH 04/85] Add attributes --- src/finn/custom_op/logicnets/truthtable.py | 26 +++++++++++++++++++--- tests/custom_op/test_truthtable.py | 11 ++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 9ec78f7..5820e0f 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,4 +1,5 @@ -import onnx.helper as helper +import numpy as np +from onnx import TensorProto, helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp @@ -43,12 +44,31 @@ class TruthTable(CustomOp): """The class corresponing to the TruthTable function. """ def get_nodeattr_types(self): - return {} + return { + # The number used for the Don't care entries + "dont_care": ("i", False, 0), + # Number of intput bits, 2 by default + "in_bits": ("i", True, 2), + # Code generation mode + "code_mode": ("s", False, "Verilog"), + } def make_shape_compatible_op(self, model): node = self.onnx_node + iname = node.input[0] + ishape = model.get_tensor_shape(iname) + input_bits = self.get_nodeattr("in_bits") + assert input_bits == ishape[0] return helper.make_node( - "TruthTable", [node.input[0], node.input[1]], [node.output[0]] + "Constant", + inputs=[], + outputs=[self.onnx_node.output[0]], + value=helper.make_tensor( + name="const_tensor", + data_type=TensorProto.BINARY, + dims=1, + vals=np.random.randint(2), + ), ) def infer_node_datatype(self, model): diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index 6945e09..c3f9c66 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -41,6 +41,9 @@ def test_truthtable(): + input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) + results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) + inputs = helper.make_tensor_value_info( "inputs", TensorProto.FLOAT, [10] ) # Input bitwidth 10 @@ -50,7 +53,11 @@ def test_truthtable(): output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) node_def = helper.make_node( - "TruthTable", ["inputs", "results"], ["output"], domain="finn.custom_op.general" + "TruthTable", + ["inputs", "results"], + ["output"], + domain="finn.custom_op.general", + in_bits=input_data.size, ) modelproto = helper.make_model( helper.make_graph([node_def], "test_model", [inputs, results], [output]) @@ -67,8 +74,6 @@ def test_truthtable(): model = model.transform(InferDataTypes()) assert model.get_tensor_datatype("output") is DataType.BINARY # perform execution - input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) - results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) in_dict = {"inputs": input_data, "results": results_data} out_dict = oxe.execute_onnx(model, in_dict) From 95ecfee51f45cc16bb215b39f58793021f5ef563 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 23 Feb 2021 17:25:46 +0000 Subject: [PATCH 05/85] Fix shape_compatibility error --- src/finn/custom_op/logicnets/truthtable.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 5820e0f..bd67c0f 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,5 +1,4 @@ -import numpy as np -from onnx import TensorProto, helper +from onnx import helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp @@ -60,15 +59,7 @@ def make_shape_compatible_op(self, model): input_bits = self.get_nodeattr("in_bits") assert input_bits == ishape[0] return helper.make_node( - "Constant", - inputs=[], - outputs=[self.onnx_node.output[0]], - value=helper.make_tensor( - name="const_tensor", - data_type=TensorProto.BINARY, - dims=1, - vals=np.random.randint(2), - ), + "TruthTable", [node.input[0], node.input[1]], [node.output[0]] ) def infer_node_datatype(self, model): From f2521adc3fd0127fcf8efad34ba8cb03c26670f7 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 26 Feb 2021 20:08:44 +0000 Subject: [PATCH 06/85] Adding zero entries in the custom operations, attributes, and a transformation to generate a Verilog truth table based on the table entries. --- src/finn/custom_op/logicnets/truthtable.py | 57 ++++++++----- src/finn/transformation/gen_verilog_truth.py | 85 ++++++++++++++++++++ tests/custom_op/test_truthtable.py | 52 +++++++++--- 3 files changed, 162 insertions(+), 32 deletions(-) create mode 100644 src/finn/transformation/gen_verilog_truth.py diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index bd67c0f..44ed2f7 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,17 +1,21 @@ +import numpy as np from onnx import helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp -def truthtable(inputs, results): - """Returns the output to a combination of x-bit input value. The results array - reflect the 1 values in the truth table result. If 5 is provided in the result - vector, the result to fifth combination of inputs 101 is 1. The input is a - vector size x, representing x-bits binary input. An example is presented: +def truthtable(inputs, result_one, result_zero, node): + """Returns the output to a combination of x-bit input value. The result_one array + reflect the 1 values in the truth table results. The result_zero vector represents + the 0 values in the truth table results. The rest of the results are imcomplete + table entries. If 5 is provided in the result vector, the result to fifth + combination of inputs 101 is 1. The input is a vector size x, representing + x-bits binary input. An example is presented: inputs = [1, 0, 1] - results = [1, 2] + result_one = [1, 2] + result_zero = [0, 3, 5] Possible combinations: A B C | Results ------------------- @@ -19,22 +23,29 @@ def truthtable(inputs, results): 0 0 1 | 1 0 1 0 | 1 0 1 1 | 0 - 1 0 0 | 0 + 1 0 0 | X 1 0 1 | 0 - 1 1 0 | 0 - 1 1 1 | 0 + 1 1 0 | X + 1 1 1 | X """ + # check if any of the values overlaps + assert np.any(np.in1d(result_one, result_zero)) == 0 + inputs = inputs[::-1] # reverse input array for C style indexing in_int = 0 # integer representation of the binary input + dont_care = node.get_nodeattr("dont_care") # get the dont care value + for idx, in_val in enumerate(inputs): in_int += (1 << idx) * in_val # calculate integer value of binary input output = ( - 1 if in_int in results else 0 - ) # return 1 if the result entry for that value is 1 + 1 if in_int in result_one else (0 if in_int in result_zero else dont_care) + ) # return 1 if the input is in result_one + # return 0 if the input is in result_zero + # return dont_care if the input is incomplete return output @@ -50,14 +61,16 @@ def get_nodeattr_types(self): "in_bits": ("i", True, 2), # Code generation mode "code_mode": ("s", False, "Verilog"), + # Output code directory + "code_dir": ("s", False, ""), } def make_shape_compatible_op(self, model): node = self.onnx_node - iname = node.input[0] - ishape = model.get_tensor_shape(iname) - input_bits = self.get_nodeattr("in_bits") - assert input_bits == ishape[0] + # iname = node.input[0] + # ishape = model.get_tensor_shape(iname) + # input_bits = self.get_nodeattr("in_bits") + # assert input_bits == ishape[0] return helper.make_node( "TruthTable", [node.input[0], node.input[1]], [node.output[0]] ) @@ -68,19 +81,25 @@ def infer_node_datatype(self, model): assert ( model.get_tensor_datatype(node.input[0]) == DataType["BINARY"] ), """ The input vector DataType is not BINARY.""" - # check that the input[0] is UINT32 + # check that the input[1] is UINT32 assert ( model.get_tensor_datatype(node.input[1]) == DataType["UINT32"] ), """ The input vector DataType is not UINT32.""" - model.set_tensor_datatype(node.output[0], DataType["BINARY"]) + # 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 output to UINT32 + model.set_tensor_datatype(node.output[0], DataType["UINT32"]) def execute_node(self, context, graph): node = self.onnx_node # load inputs input_entry = context[node.input[0]] - results = context[node.input[1]] + result_one = context[node.input[1]] + result_zero = context[node.input[2]] # calculate output - output = truthtable(input_entry, results) + output = truthtable(input_entry, result_one, result_zero, self) # store output context[node.output[0]] = output diff --git a/src/finn/transformation/gen_verilog_truth.py b/src/finn/transformation/gen_verilog_truth.py new file mode 100644 index 0000000..fb83ae6 --- /dev/null +++ b/src/finn/transformation/gen_verilog_truth.py @@ -0,0 +1,85 @@ +# 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 _generate_verilog(myOp, result_one, result_zero): + input_bits = myOp.get_nodeattr("in_bits") + dont_care_entry = myOp.get_nodeattr("dont_care") + # the module name is kept constant to "inconsistent_table" + # the input name is kept constant to "in" + # the output is kept constant to "result" + verilog_string = "module inconsistent_table (\n" + 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 result_one: + val = int(val) + verilog_string += "\t\t\t%d'b" % (input_bits) + verilog_string += bin(val)[2:].zfill(input_bits) + print(val) + verilog_string += " : result = 1'b1;\n" + # fill the zero entries + for val in result_zero: + val = int(val) + verilog_string += "\t\t\t%d'b" % (input_bits) + verilog_string += bin(val)[2:].zfill(input_bits) + verilog_string += " : result = 1'b0;\n" + # fill the default case for dont_care or inconsistent entries + verilog_string += "\t\t\tdefault: result = 1'b%d;\n" % (dont_care_entry) + # close the module + verilog_string += "\t\tendcase\n\tend\nendmodule\n" + # open file, write string and close file + verilog_file = open("my_truthtable.v", "w") + verilog_file.write(verilog_string) + verilog_file.close() + + +class GenVerilogTruthTable(NodeLocalTransformation): + """Generate a Verilog file for every node in the Graph using the + TruthTable custom operation""" + + def __init__(self, num_workers, result_one, result_zero): + super().__init__(num_workers=num_workers) + self.result_one = result_one + self.result_zero = result_zero + + def applyNodeLocal(self, node): + op_type = node.op_type + if op_type == "TruthTable": + myOp = registry.getCustomOp(node) + print(self.result_one) + _generate_verilog(myOp, self.result_one, self.result_zero) + + return (node, False) diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index c3f9c66..41fac44 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -33,6 +33,7 @@ import finn.core.onnx_exec as oxe from finn.core.datatype import DataType from finn.core.modelwrapper import ModelWrapper +from finn.transformation.gen_verilog_truth import GenVerilogTruthTable from finn.transformation.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes @@ -42,39 +43,54 @@ def test_truthtable(): input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) - results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) + result_one_data = np.asarray([58, 15, 89, 695, 6485], dtype=np.float32) + result_zero_data = np.asarray([52, 65, 1908, 6101], dtype=np.float32) + dont_care = 0 + in_bits = 16 inputs = helper.make_tensor_value_info( - "inputs", TensorProto.FLOAT, [10] - ) # Input bitwidth 10 - results = helper.make_tensor_value_info( - "results", TensorProto.FLOAT, [5] - ) # 5 results are 1 among all possible combinations + "inputs", TensorProto.FLOAT, [input_data.size] + ) + result_one = helper.make_tensor_value_info( + "result_one", TensorProto.FLOAT, [result_one_data.size] + ) + result_zero = helper.make_tensor_value_info( + "result_zero", TensorProto.FLOAT, [result_zero_data.size] + ) output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) node_def = helper.make_node( "TruthTable", - ["inputs", "results"], + ["inputs", "result_one", "result_zero"], ["output"], domain="finn.custom_op.general", - in_bits=input_data.size, + in_bits=in_bits, + dont_care=dont_care, ) modelproto = helper.make_model( - helper.make_graph([node_def], "test_model", [inputs, results], [output]) + helper.make_graph( + [node_def], "test_model", [inputs, result_one, result_zero], [output] + ) ) model = ModelWrapper(modelproto) model.set_tensor_datatype("inputs", DataType.BINARY) - model.set_tensor_datatype("results", DataType.UINT32) + model.set_tensor_datatype("result_one", DataType.UINT32) + model.set_tensor_datatype("result_zero", 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 + assert model.get_tensor_datatype("output") is DataType.UINT32 # perform execution - in_dict = {"inputs": input_data, "results": results_data} + in_dict = { + "inputs": input_data, + "result_one": result_one_data, + "result_zero": result_zero_data, + } out_dict = oxe.execute_onnx(model, in_dict) # calculate result here for comparison with the custom op @@ -82,7 +98,17 @@ def test_truthtable(): out_idx = 0 for idx, val in enumerate(input_data): out_idx += (1 << idx) * val - entry = 1 if out_idx in results_data else 0 + entry = ( + 1 + if out_idx in result_one_data + else (0 if out_idx in result_zero_data else dont_care) + ) # compare outputs assert entry == out_dict["output"] + # test transformation to generate verilog + model = model.transform( + GenVerilogTruthTable( + num_workers=None, result_one=result_one_data, result_zero=result_zero_data + ) + ) From a655834c9fffcdefbdf8beaca7d8db5a3548edc7 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 2 Mar 2021 17:21:51 +0000 Subject: [PATCH 07/85] There are a bunch of changes in this commit: - Remove 0 values in table entries - All the specified entries are 1 and named "care_set" - Remove attribute for dont_care symbol - Everything is named to represent the custom binary truth table operation - Change the way the transformation is performed. - The transformation itself checks if the node is "BinaryTruthTable" type customOp - The transformation calls a helper function inside the customOp class that generates the Verilog - The care_set tensor is given as an attribute to the transformation --- my_truthtable.v | 17 ++++ src/finn/custom_op/general/__init__.py | 4 +- src/finn/custom_op/logicnets/truthtable.py | 93 +++++++++++-------- .../gen_bintruthtable_verilog.py} | 55 +++-------- tests/custom_op/test_truthtable.py | 42 +++------ 5 files changed, 103 insertions(+), 108 deletions(-) create mode 100644 my_truthtable.v rename src/finn/transformation/{gen_verilog_truth.py => logicnets/gen_bintruthtable_verilog.py} (50%) diff --git a/my_truthtable.v b/my_truthtable.v new file mode 100644 index 0000000..be57489 --- /dev/null +++ b/my_truthtable.v @@ -0,0 +1,17 @@ +module incomplete_table ( + input [15:0] in, + output reg result +); + + always @(in) begin + case(in) + 16'b0000000000000001 : result = 1'b1; + 16'b0000000000111010 : result = 1'b1; + 16'b0000000000001111 : result = 1'b1; + 16'b0000000001011001 : result = 1'b1; + 16'b0000001010110111 : result = 1'b1; + 16'b0001100101010101 : result = 1'b1; + default: result = 1'b0; + endcase + end +endmodule diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index d650eac..79cc5bf 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,7 +33,7 @@ from finn.custom_op.general.quantavgpool2d import QuantAvgPool2d from finn.custom_op.general.streamingdataflowpartition import StreamingDataflowPartition from finn.custom_op.general.xnorpopcount import XnorPopcountMatMul -from finn.custom_op.logicnets.truthtable import TruthTable +from finn.custom_op.logicnets.truthtable import BinaryTruthTable custom_op = dict() @@ -44,4 +44,4 @@ custom_op["MultiThreshold"] = MultiThreshold custom_op["XnorPopcountMatMul"] = XnorPopcountMatMul custom_op["Im2Col"] = Im2Col -custom_op["TruthTable"] = TruthTable +custom_op["BinaryTruthTable"] = BinaryTruthTable diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 44ed2f7..b75d756 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,21 +1,20 @@ import numpy as np +import onnx from onnx import helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp -def truthtable(inputs, result_one, result_zero, node): - """Returns the output to a combination of x-bit input value. The result_one array - reflect the 1 values in the truth table results. The result_zero vector represents - the 0 values in the truth table results. The rest of the results are imcomplete - table entries. If 5 is provided in the result vector, the result to fifth +def truthtable_binary(inputs, care_set, node): + """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 5 is provided in the care-set, the result to fifth combination of inputs 101 is 1. The input is a vector size x, representing x-bits binary input. An example is presented: inputs = [1, 0, 1] - result_one = [1, 2] - result_zero = [0, 3, 5] + care_set = [1, 2] Possible combinations: A B C | Results ------------------- @@ -23,40 +22,30 @@ def truthtable(inputs, result_one, result_zero, node): 0 0 1 | 1 0 1 0 | 1 0 1 1 | 0 - 1 0 0 | X + 1 0 0 | 0 1 0 1 | 0 - 1 1 0 | X - 1 1 1 | X + 1 1 0 | 0 + 1 1 1 | 0 """ - # check if any of the values overlaps - assert np.any(np.in1d(result_one, result_zero)) == 0 inputs = inputs[::-1] # reverse input array for C style indexing - in_int = 0 # integer representation of the binary input - - dont_care = node.get_nodeattr("dont_care") # get the dont care value + in_int = 0 # initialize integer representation of the binary input array for idx, in_val in enumerate(inputs): in_int += (1 << idx) * in_val # calculate integer value of binary input - output = ( - 1 if in_int in result_one else (0 if in_int in result_zero else dont_care) - ) # return 1 if the input is in result_one - # return 0 if the input is in result_zero - # return dont_care if the input is incomplete + output = 1 if in_int in care_set else 0 # return 1 if the input is in result_one return output -class TruthTable(CustomOp): +class BinaryTruthTable(CustomOp): """The class corresponing to the TruthTable function. """ def get_nodeattr_types(self): return { - # The number used for the Don't care entries - "dont_care": ("i", False, 0), # Number of intput bits, 2 by default "in_bits": ("i", True, 2), # Code generation mode @@ -67,13 +56,19 @@ def get_nodeattr_types(self): def make_shape_compatible_op(self, model): node = self.onnx_node - # iname = node.input[0] - # ishape = model.get_tensor_shape(iname) - # input_bits = self.get_nodeattr("in_bits") - # assert input_bits == ishape[0] - return helper.make_node( - "TruthTable", [node.input[0], node.input[1]], [node.output[0]] + val = np.random.randn(1).astype(np.bool) + node = helper.make_node( + "Constant", + inputs=[], + outputs=["val"], + value=helper.make_tensor( + name="const_tensor", + data_type=onnx.TensorProto.BOOL, + dims=val.shape, + vals=val.flatten().astype(bool), + ), ) + return node def infer_node_datatype(self, model): node = self.onnx_node @@ -86,20 +81,15 @@ def infer_node_datatype(self, model): 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 output to UINT32 - model.set_tensor_datatype(node.output[0], DataType["UINT32"]) + 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]] - result_one = context[node.input[1]] - result_zero = context[node.input[2]] + care_set = context[node.input[1]] # calculate output - output = truthtable(input_entry, result_one, result_zero, self) + output = truthtable_binary(input_entry, care_set, self) # store output context[node.output[0]] = output @@ -128,3 +118,32 @@ def verify_node(self): # taken from "xnorpopcount.py" info_messages.append("TruthTable needs 2 data inputs") return info_messages + + def generate_verilog(self, care_set): + + input_bits = self.get_nodeattr("in_bits") + # 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 incomplete_table (\n" + 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" + # open file, write string and close file + verilog_file = open("my_truthtable.v", "w") + verilog_file.write(verilog_string) + verilog_file.close() diff --git a/src/finn/transformation/gen_verilog_truth.py b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py similarity index 50% rename from src/finn/transformation/gen_verilog_truth.py rename to src/finn/transformation/logicnets/gen_bintruthtable_verilog.py index fb83ae6..d60bb6f 100644 --- a/src/finn/transformation/gen_verilog_truth.py +++ b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py @@ -30,56 +30,29 @@ from finn.transformation.base import NodeLocalTransformation -def _generate_verilog(myOp, result_one, result_zero): - input_bits = myOp.get_nodeattr("in_bits") - dont_care_entry = myOp.get_nodeattr("dont_care") - # the module name is kept constant to "inconsistent_table" - # the input name is kept constant to "in" - # the output is kept constant to "result" - verilog_string = "module inconsistent_table (\n" - 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" +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) - # fill the one entries - for val in result_one: - val = int(val) - verilog_string += "\t\t\t%d'b" % (input_bits) - verilog_string += bin(val)[2:].zfill(input_bits) - print(val) - verilog_string += " : result = 1'b1;\n" - # fill the zero entries - for val in result_zero: - val = int(val) - verilog_string += "\t\t\t%d'b" % (input_bits) - verilog_string += bin(val)[2:].zfill(input_bits) - verilog_string += " : result = 1'b0;\n" - # fill the default case for dont_care or inconsistent entries - verilog_string += "\t\t\tdefault: result = 1'b%d;\n" % (dont_care_entry) - # close the module - verilog_string += "\t\tendcase\n\tend\nendmodule\n" - # open file, write string and close file - verilog_file = open("my_truthtable.v", "w") - verilog_file.write(verilog_string) - verilog_file.close() + except KeyError: + # exception if op_type is not supported + raise Exception("Custom op_type %s is currently not supported." % op_type) -class GenVerilogTruthTable(NodeLocalTransformation): +class GenBinaryTruthTableVerilog(NodeLocalTransformation): """Generate a Verilog file for every node in the Graph using the TruthTable custom operation""" - def __init__(self, num_workers, result_one, result_zero): + def __init__(self, num_workers, care_set): super().__init__(num_workers=num_workers) - self.result_one = result_one - self.result_zero = result_zero + self.care_set = care_set def applyNodeLocal(self, node): op_type = node.op_type - if op_type == "TruthTable": - myOp = registry.getCustomOp(node) - print(self.result_one) - _generate_verilog(myOp, self.result_one, self.result_zero) + if op_type == "BinaryTruthTable": + _genbintruthtable_verilog(node, self.care_set) return (node, False) diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index 41fac44..04745e4 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -33,9 +33,11 @@ import finn.core.onnx_exec as oxe from finn.core.datatype import DataType from finn.core.modelwrapper import ModelWrapper -from finn.transformation.gen_verilog_truth import GenVerilogTruthTable from finn.transformation.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes +from finn.transformation.logicnets.gen_bintruthtable_verilog import ( + GenBinaryTruthTableVerilog, +) export_onnx_path = "test_truthtable.onnx" @@ -43,40 +45,31 @@ def test_truthtable(): input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) - result_one_data = np.asarray([58, 15, 89, 695, 6485], dtype=np.float32) - result_zero_data = np.asarray([52, 65, 1908, 6101], dtype=np.float32) - dont_care = 0 + care_set_data = np.asarray([1, 58, 15, 89, 695, 6485], dtype=np.float32) in_bits = 16 inputs = helper.make_tensor_value_info( "inputs", TensorProto.FLOAT, [input_data.size] ) - result_one = helper.make_tensor_value_info( - "result_one", TensorProto.FLOAT, [result_one_data.size] - ) - result_zero = helper.make_tensor_value_info( - "result_zero", TensorProto.FLOAT, [result_zero_data.size] + 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]) node_def = helper.make_node( - "TruthTable", - ["inputs", "result_one", "result_zero"], + "BinaryTruthTable", + ["inputs", "care_set"], ["output"], domain="finn.custom_op.general", in_bits=in_bits, - dont_care=dont_care, ) modelproto = helper.make_model( - helper.make_graph( - [node_def], "test_model", [inputs, result_one, result_zero], [output] - ) + helper.make_graph([node_def], "test_model", [inputs, care_set], [output]) ) model = ModelWrapper(modelproto) model.set_tensor_datatype("inputs", DataType.BINARY) - model.set_tensor_datatype("result_one", DataType.UINT32) - model.set_tensor_datatype("result_zero", DataType.UINT32) + model.set_tensor_datatype("care_set", DataType.UINT32) # test output shape model = model.transform(InferShapes()) @@ -84,12 +77,11 @@ def test_truthtable(): # test output type assert model.get_tensor_datatype("output") is DataType.FLOAT32 model = model.transform(InferDataTypes()) - assert model.get_tensor_datatype("output") is DataType.UINT32 + assert model.get_tensor_datatype("output") is DataType.BINARY # perform execution in_dict = { "inputs": input_data, - "result_one": result_one_data, - "result_zero": result_zero_data, + "care_set": care_set_data, } out_dict = oxe.execute_onnx(model, in_dict) @@ -98,17 +90,11 @@ def test_truthtable(): out_idx = 0 for idx, val in enumerate(input_data): out_idx += (1 << idx) * val - entry = ( - 1 - if out_idx in result_one_data - else (0 if out_idx in result_zero_data else dont_care) - ) + entry = 1 if out_idx in care_set_data else 0 # compare outputs assert entry == out_dict["output"] # test transformation to generate verilog model = model.transform( - GenVerilogTruthTable( - num_workers=None, result_one=result_one_data, result_zero=result_zero_data - ) + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) ) From f8eb7c14a98a4e5db96c1d9b075256318b99e3c7 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 3 Mar 2021 11:56:23 +0000 Subject: [PATCH 08/85] Modify function and file names to keed consistency --- src/finn/custom_op/general/__init__.py | 2 +- .../logicnets/{truthtable.py => binary_truthtable.py} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/finn/custom_op/logicnets/{truthtable.py => binary_truthtable.py} (97%) diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 79cc5bf..214af52 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,7 +33,7 @@ from finn.custom_op.general.quantavgpool2d import QuantAvgPool2d from finn.custom_op.general.streamingdataflowpartition import StreamingDataflowPartition from finn.custom_op.general.xnorpopcount import XnorPopcountMatMul -from finn.custom_op.logicnets.truthtable import BinaryTruthTable +from finn.custom_op.logicnets.binary_truthtable import BinaryTruthTable custom_op = dict() diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py similarity index 97% rename from src/finn/custom_op/logicnets/truthtable.py rename to src/finn/custom_op/logicnets/binary_truthtable.py index b75d756..e63c94d 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -6,7 +6,7 @@ from finn.custom_op.base import CustomOp -def truthtable_binary(inputs, care_set, node): +def binary_truthtable(inputs, care_set, node): """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 5 is provided in the care-set, the result to fifth @@ -89,7 +89,7 @@ def execute_node(self, context, graph): input_entry = context[node.input[0]] care_set = context[node.input[1]] # calculate output - output = truthtable_binary(input_entry, care_set, self) + output = binary_truthtable(input_entry, care_set, self) # store output context[node.output[0]] = output From 1154cebd393772392bae189b723ecece21ce4142 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 3 Mar 2021 15:11:45 +0000 Subject: [PATCH 09/85] Add Copyright (c) 2021 Xilinx, Inc. --- .../custom_op/logicnets/binary_truthtable.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index e63c94d..18e5789 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -1,3 +1,31 @@ +# 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 from onnx import helper From b18d59cfe4873a881e943ad9f88b874345d56da5 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 3 Mar 2021 16:12:40 +0000 Subject: [PATCH 10/85] Add custom directory entry for the verilog file --- my_truthtable.v | 17 ----------------- .../custom_op/logicnets/binary_truthtable.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 19 deletions(-) delete mode 100644 my_truthtable.v diff --git a/my_truthtable.v b/my_truthtable.v deleted file mode 100644 index be57489..0000000 --- a/my_truthtable.v +++ /dev/null @@ -1,17 +0,0 @@ -module incomplete_table ( - input [15:0] in, - output reg result -); - - always @(in) begin - case(in) - 16'b0000000000000001 : result = 1'b1; - 16'b0000000000111010 : result = 1'b1; - 16'b0000000000001111 : result = 1'b1; - 16'b0000000001011001 : result = 1'b1; - 16'b0000001010110111 : result = 1'b1; - 16'b0001100101010101 : result = 1'b1; - default: result = 1'b0; - endcase - end -endmodule diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 18e5789..f9c1c5b 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -28,6 +28,7 @@ import numpy as np import onnx +import os from onnx import helper from finn.core.datatype import DataType @@ -79,7 +80,7 @@ def get_nodeattr_types(self): # Code generation mode "code_mode": ("s", False, "Verilog"), # Output code directory - "code_dir": ("s", False, ""), + "code_dir": ("s", False, "src/finn/data/verilog/truthtable/"), } def make_shape_compatible_op(self, model): @@ -172,6 +173,12 @@ def generate_verilog(self, care_set): # close the module verilog_string += "\t\tendcase\n\tend\nendmodule\n" # open file, write string and close file - verilog_file = open("my_truthtable.v", "w") + + dir = self.get_nodeattr("code_dir") + + if not os.path.exists(dir): + os.makedirs(dir) + + verilog_file = open(dir + "binary_truthtable.v", "w") verilog_file.write(verilog_string) verilog_file.close() From a9626f112ed92285d5ab8774ffaa02daaf8f8b23 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Thu, 4 Mar 2021 14:58:56 +0000 Subject: [PATCH 11/85] Add PyVerilator test for the generated binary truth table verilog files --- .../custom_op/logicnets/binary_truthtable.py | 2 +- .../verilog/truthtable/incomplete_table.v | 17 +++++ .../verilog/truthtable/wrapper_truthtable.v | 20 ++++++ tests/util/test_pyverilog_binarytruthtable.py | 70 +++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/finn/data/verilog/truthtable/incomplete_table.v create mode 100644 src/finn/data/verilog/truthtable/wrapper_truthtable.v create mode 100644 tests/util/test_pyverilog_binarytruthtable.py diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index f9c1c5b..9fb288f 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -179,6 +179,6 @@ def generate_verilog(self, care_set): if not os.path.exists(dir): os.makedirs(dir) - verilog_file = open(dir + "binary_truthtable.v", "w") + verilog_file = open(dir + "incomplete_table.v", "w") verilog_file.write(verilog_string) verilog_file.close() diff --git a/src/finn/data/verilog/truthtable/incomplete_table.v b/src/finn/data/verilog/truthtable/incomplete_table.v new file mode 100644 index 0000000..be57489 --- /dev/null +++ b/src/finn/data/verilog/truthtable/incomplete_table.v @@ -0,0 +1,17 @@ +module incomplete_table ( + input [15:0] in, + output reg result +); + + always @(in) begin + case(in) + 16'b0000000000000001 : result = 1'b1; + 16'b0000000000111010 : result = 1'b1; + 16'b0000000000001111 : result = 1'b1; + 16'b0000000001011001 : result = 1'b1; + 16'b0000001010110111 : result = 1'b1; + 16'b0001100101010101 : result = 1'b1; + default: result = 1'b0; + endcase + end +endmodule diff --git a/src/finn/data/verilog/truthtable/wrapper_truthtable.v b/src/finn/data/verilog/truthtable/wrapper_truthtable.v new file mode 100644 index 0000000..bef7555 --- /dev/null +++ b/src/finn/data/verilog/truthtable/wrapper_truthtable.v @@ -0,0 +1,20 @@ + +`timescale 1 ns / 1 ps + +(* CORE_GENERATION_INFO="wrapper_truthtable,hls_ip_2019_1,{HLS_INPUT_TYPE=cxx,HLS_INPUT_FLOAT=0,HLS_INPUT_FIXED=1,HLS_INPUT_PART=xc7z020-clg400-1,HLS_INPUT_CLOCK=5.000000,HLS_INPUT_ARCH=others,HLS_SYN_CLOCK=3.552000,HLS_SYN_LAT=0,HLS_SYN_TPT=none,HLS_SYN_MEM=0,HLS_SYN_DSP=0,HLS_SYN_FF=144,HLS_SYN_LUT=271,HLS_VERSION=2019_1}" *) + + +module wrapper_truthtable ( + input_data, + result_data +); + +input [15:0]input_data; + output result_data; + +incomplete_table my_incomplete_table( + .in(input_data), + .result(result_data) +); + +endmodule //wrapper_truthtable diff --git a/tests/util/test_pyverilog_binarytruthtable.py b/tests/util/test_pyverilog_binarytruthtable.py new file mode 100644 index 0000000..e9505b6 --- /dev/null +++ b/tests/util/test_pyverilog_binarytruthtable.py @@ -0,0 +1,70 @@ +# Copyright (c) 2021, Xilinx +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of FINN nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import numpy as np +from pyverilator import PyVerilator + +from finn.core.datatype import DataType +from finn.util.data_packing import npy_to_rtlsim_input + + +def test_pyverilator_binarytruthtable(): + # file_root = pk.resource_filename("finn.data","verilog/myadd") + + sim = PyVerilator.build( + "/workspace/finn-base/src/finn/data/verilog/truthtable/wrapper_truthtable.v", + top_module_name="wrapper_truthtable", + ) + + expected_ports = [ + "input_data", + "result_data", + ] + + for port in expected_ports: + assert port in sim.io + + sim.io["input_data"] = 0 + + array = 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, 0], + [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], + ] + ) + + value = npy_to_rtlsim_input(array, DataType.BINARY, 16, False) + + for val in value: + sim.io["input_data"] = val + result = sim.io["result_data"] + assert result == 1 From d67885f76cb073c0a1b2520df43458b4cec977e4 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 5 Mar 2021 10:18:06 +0000 Subject: [PATCH 12/85] Add new execution mode: python or rtlsim --- .../custom_op/logicnets/binary_truthtable.py | 50 ++++++++++--- ...truthtable.py => test_binarytruthtable.py} | 65 +++++++++++------ tests/util/test_pyverilog_binarytruthtable.py | 70 ------------------- 3 files changed, 84 insertions(+), 101 deletions(-) rename tests/custom_op/{test_truthtable.py => test_binarytruthtable.py} (63%) delete mode 100644 tests/util/test_pyverilog_binarytruthtable.py diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 9fb288f..8e7bc39 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -30,9 +30,11 @@ import onnx import os from onnx import helper +from pyverilator import PyVerilator from finn.core.datatype import DataType from finn.custom_op.base import CustomOp +from finn.util.data_packing import npy_to_rtlsim_input def binary_truthtable(inputs, care_set, node): @@ -80,7 +82,13 @@ def get_nodeattr_types(self): # Code generation mode "code_mode": ("s", False, "Verilog"), # Output code directory - "code_dir": ("s", False, "src/finn/data/verilog/truthtable/"), + "code_dir": ( + "s", + False, + "/workspace/finn-base/src/finn/data/verilog/truthtable/", + ), + # Execution mode, "pyhton" by default + "exec_mode": ("s", True, "python"), } def make_shape_compatible_op(self, model): @@ -101,28 +109,52 @@ def make_shape_compatible_op(self, model): def infer_node_datatype(self, model): node = self.onnx_node - # check that the input[0] is binary + # 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 + # 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 + # Check that the input[2] is UINT32 model.set_tensor_datatype(node.output[0], DataType["BINARY"]) def execute_node(self, context, graph): node = self.onnx_node - # load inputs + # Load inputs input_entry = context[node.input[0]] care_set = context[node.input[1]] - # calculate output - output = binary_truthtable(input_entry, care_set, self) - # store output + # 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) + elif mode == "rtlsim": + # Generate PyVerilator object if Verilog file exits, + # otherwise generate Verilog and proceed + verilog_dir = self.get_nodeattr("code_dir") + "incomplete_table.v" + if not os.path.exists(verilog_dir): + self.generate_verilog(care_set) + 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]] = output - def verify_node(self): # taken from "xnorpopcount.py" + def verify_node(self): info_messages = [] # verify number of attributes diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_binarytruthtable.py similarity index 63% rename from tests/custom_op/test_truthtable.py rename to tests/custom_op/test_binarytruthtable.py index 04745e4..8f98174 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -33,6 +33,7 @@ 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.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes from finn.transformation.logicnets.gen_bintruthtable_verilog import ( @@ -42,31 +43,44 @@ export_onnx_path = "test_truthtable.onnx" -def test_truthtable(): +def test_binarytruthtable(): - input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) + # 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], dtype=np.float32) in_bits = 16 - inputs = helper.make_tensor_value_info( - "inputs", TensorProto.FLOAT, [input_data.size] - ) + # Set input and care_set 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) @@ -78,22 +92,29 @@ def test_truthtable(): assert model.get_tensor_datatype("output") is DataType.FLOAT32 model = model.transform(InferDataTypes()) assert model.get_tensor_datatype("output") is DataType.BINARY - # perform execution - in_dict = { - "inputs": input_data, - "care_set": care_set_data, - } - out_dict = oxe.execute_onnx(model, in_dict) - - # calculate result here for comparison with the custom op - input_data = input_data[::-1] - out_idx = 0 - for idx, val in enumerate(input_data): - out_idx += (1 << idx) * val - entry = 1 if out_idx in care_set_data else 0 + # Loop over "python" and "rtlsim" execution modes + for x 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) + # Calculate result here locally for comparison with the CustomOp result + input_data = input_data[::-1] + out_idx = 0 + for idx, val in enumerate(input_data): + out_idx += (1 << idx) * val + entry = 1 if out_idx in care_set_data else 0 + # compare outputs + assert entry == out_dict["output"] + # Change execution mode into "rtlsim" for simulation with PyVerilator + myOp = getCustomOp(model.graph.node[0]) + myOp.set_nodeattr("exec_mode", "rtlsim") - # compare outputs - assert entry == out_dict["output"] # test transformation to generate verilog model = model.transform( GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) diff --git a/tests/util/test_pyverilog_binarytruthtable.py b/tests/util/test_pyverilog_binarytruthtable.py deleted file mode 100644 index e9505b6..0000000 --- a/tests/util/test_pyverilog_binarytruthtable.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2021, Xilinx -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of FINN nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import numpy as np -from pyverilator import PyVerilator - -from finn.core.datatype import DataType -from finn.util.data_packing import npy_to_rtlsim_input - - -def test_pyverilator_binarytruthtable(): - # file_root = pk.resource_filename("finn.data","verilog/myadd") - - sim = PyVerilator.build( - "/workspace/finn-base/src/finn/data/verilog/truthtable/wrapper_truthtable.v", - top_module_name="wrapper_truthtable", - ) - - expected_ports = [ - "input_data", - "result_data", - ] - - for port in expected_ports: - assert port in sim.io - - sim.io["input_data"] = 0 - - array = 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, 0], - [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], - ] - ) - - value = npy_to_rtlsim_input(array, DataType.BINARY, 16, False) - - for val in value: - sim.io["input_data"] = val - result = sim.io["result_data"] - assert result == 1 From b37621dbc7a7b5d2e2bea76c300c2dd37bec9f02 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 5 Mar 2021 14:32:38 +0000 Subject: [PATCH 13/85] Add random LUT generator utility function. --- src/finn/util/logicnets/logicnets.py | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/finn/util/logicnets/logicnets.py diff --git a/src/finn/util/logicnets/logicnets.py b/src/finn/util/logicnets/logicnets.py new file mode 100644 index 0000000..637812b --- /dev/null +++ b/src/finn/util/logicnets/logicnets.py @@ -0,0 +1,95 @@ +# 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 os +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 + + +def gen_verilog(n_bits, care_set, dir): + """The function generated a verilog module based on the n_bit input values and the + care_set. The result for every value in the care_set is 1. The module creates a + LUT based on a n_bits:1 mapping""" + + # 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 incomplete_table (\n" + verilog_string += "\tinput [%d:0] in,\n" % (n_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" % (n_bits) + verilog_string += bin(val)[2:].zfill(n_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" + # open file, write string and close file + + if not os.path.exists(dir): + os.makedirs(dir) + + verilog_file = open(dir + "incomplete_table.v", "w") + verilog_file.write(verilog_string) + verilog_file.close() + + +def random_lut_verilog(n_bits, n_entries, dir): + """This function generates random care set and the verilog representation + of the LUT based on the care_set.""" + care_set = random_care_set(n_bits, n_entries) + gen_verilog(n_bits, care_set, dir) From 72b088cb2a96bd5f5661de7f883b20901ad0434a Mon Sep 17 00:00:00 2001 From: jalezeta Date: Mon, 8 Mar 2021 17:14:34 +0000 Subject: [PATCH 14/85] Merge branch 'dev' into feature/incomplete_tables --- .../verilog/truthtable/wrapper_truthtable.v | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/finn/data/verilog/truthtable/wrapper_truthtable.v diff --git a/src/finn/data/verilog/truthtable/wrapper_truthtable.v b/src/finn/data/verilog/truthtable/wrapper_truthtable.v deleted file mode 100644 index bef7555..0000000 --- a/src/finn/data/verilog/truthtable/wrapper_truthtable.v +++ /dev/null @@ -1,20 +0,0 @@ - -`timescale 1 ns / 1 ps - -(* CORE_GENERATION_INFO="wrapper_truthtable,hls_ip_2019_1,{HLS_INPUT_TYPE=cxx,HLS_INPUT_FLOAT=0,HLS_INPUT_FIXED=1,HLS_INPUT_PART=xc7z020-clg400-1,HLS_INPUT_CLOCK=5.000000,HLS_INPUT_ARCH=others,HLS_SYN_CLOCK=3.552000,HLS_SYN_LAT=0,HLS_SYN_TPT=none,HLS_SYN_MEM=0,HLS_SYN_DSP=0,HLS_SYN_FF=144,HLS_SYN_LUT=271,HLS_VERSION=2019_1}" *) - - -module wrapper_truthtable ( - input_data, - result_data -); - -input [15:0]input_data; - output result_data; - -incomplete_table my_incomplete_table( - .in(input_data), - .result(result_data) -); - -endmodule //wrapper_truthtable From a9e246b9f5dede8b67d3870b5cc2889c0a9169d5 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Mon, 8 Mar 2021 18:21:57 +0000 Subject: [PATCH 15/85] Change verilog file path to temporary folder --- .../custom_op/logicnets/binary_truthtable.py | 54 ++++++++----------- src/finn/util/logicnets/logicnets.py | 44 --------------- tests/custom_op/test_binarytruthtable.py | 13 ++--- 3 files changed, 30 insertions(+), 81 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 8e7bc39..110aeef 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -34,6 +34,7 @@ from finn.core.datatype import DataType 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 @@ -77,17 +78,13 @@ class BinaryTruthTable(CustomOp): def get_nodeattr_types(self): return { - # Number of intput bits, 2 by default + # number of intput bits, 2 by default "in_bits": ("i", True, 2), - # Code generation mode + # code generation mode "code_mode": ("s", False, "Verilog"), - # Output code directory - "code_dir": ( - "s", - False, - "/workspace/finn-base/src/finn/data/verilog/truthtable/", - ), - # Execution mode, "pyhton" by default + # output code directory + "code_dir": ("s", False, ""), + # execution mode, "pyhton" by default "exec_mode": ("s", True, "python"), } @@ -109,40 +106,39 @@ def make_shape_compatible_op(self, model): def infer_node_datatype(self, model): node = self.onnx_node - # Check that the input[0] is binary + # 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 + # 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 + # check that the input[2] is UINT32 model.set_tensor_datatype(node.output[0], DataType["BINARY"]) def execute_node(self, context, graph): node = self.onnx_node - # Load inputs + # load inputs input_entry = context[node.input[0]] care_set = context[node.input[1]] - # Load execution mode + # load execution mode mode = self.get_nodeattr("exec_mode") if mode == "python": - # Calculate output in Python mode + # calculate output in Python mode output = binary_truthtable(input_entry, care_set, self) elif mode == "rtlsim": - # Generate PyVerilator object if Verilog file exits, - # otherwise generate Verilog and proceed - verilog_dir = self.get_nodeattr("code_dir") + "incomplete_table.v" + # check the code directory is not empty + verilog_dir = self.get_nodeattr("code_dir") + "/incomplete_table.v" if not os.path.exists(verilog_dir): - self.generate_verilog(care_set) + 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 + # 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 + # set value into the Verilog module sim.io["in"] = value - # Read result value + # read result value output = sim.io["result"] else: raise Exception( @@ -151,7 +147,7 @@ def execute_node(self, context, graph): mode ) ) - # Return output + # return output context[node.output[0]] = output def verify_node(self): @@ -204,13 +200,9 @@ def generate_verilog(self, care_set): verilog_string += "\t\t\tdefault: result = 1'b0;\n" # close the module verilog_string += "\t\tendcase\n\tend\nendmodule\n" - # open file, write string and close file - - dir = self.get_nodeattr("code_dir") - - if not os.path.exists(dir): - os.makedirs(dir) - - verilog_file = open(dir + "incomplete_table.v", "w") + # create temporary folder and save attribute value + self.set_nodeattr("code_dir", make_build_dir("verilog_")) + # create and write verilog file + verilog_file = open(self.get_nodeattr("code_dir") + "/incomplete_table.v", "w") verilog_file.write(verilog_string) verilog_file.close() diff --git a/src/finn/util/logicnets/logicnets.py b/src/finn/util/logicnets/logicnets.py index 637812b..b5a0726 100644 --- a/src/finn/util/logicnets/logicnets.py +++ b/src/finn/util/logicnets/logicnets.py @@ -27,7 +27,6 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import numpy as np -import os from random import randint @@ -50,46 +49,3 @@ def random_care_set(n_bits, n_entries): care_set = np.append(care_set, value) care_set = np.unique(care_set) return care_set - - -def gen_verilog(n_bits, care_set, dir): - """The function generated a verilog module based on the n_bit input values and the - care_set. The result for every value in the care_set is 1. The module creates a - LUT based on a n_bits:1 mapping""" - - # 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 incomplete_table (\n" - verilog_string += "\tinput [%d:0] in,\n" % (n_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" % (n_bits) - verilog_string += bin(val)[2:].zfill(n_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" - # open file, write string and close file - - if not os.path.exists(dir): - os.makedirs(dir) - - verilog_file = open(dir + "incomplete_table.v", "w") - verilog_file.write(verilog_string) - verilog_file.close() - - -def random_lut_verilog(n_bits, n_entries, dir): - """This function generates random care set and the verilog representation - of the LUT based on the care_set.""" - care_set = random_care_set(n_bits, n_entries) - gen_verilog(n_bits, care_set, dir) diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 8f98174..1dee911 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -92,8 +92,14 @@ def test_binarytruthtable(): assert model.get_tensor_datatype("output") is DataType.FLOAT32 model = model.transform(InferDataTypes()) assert model.get_tensor_datatype("output") is DataType.BINARY + + # Generate verilog + model = model.transform( + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) + ) + # Loop over "python" and "rtlsim" execution modes - for x in range(2): + for _ in range(2): # Loop over different input combinations for input_data in input_data_vector: # Create input dictionary @@ -114,8 +120,3 @@ def test_binarytruthtable(): # Change execution mode into "rtlsim" for simulation with PyVerilator myOp = getCustomOp(model.graph.node[0]) myOp.set_nodeattr("exec_mode", "rtlsim") - - # test transformation to generate verilog - model = model.transform( - GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) - ) From 8c308e2eee9c275aefa3795d81ca8afa452d32b1 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Mon, 8 Mar 2021 19:46:03 +0000 Subject: [PATCH 16/85] Simplify the binary array to integer conversion --- .../custom_op/logicnets/binary_truthtable.py | 52 ++++++++++--------- .../verilog/truthtable/incomplete_table.v | 17 ------ tests/custom_op/test_binarytruthtable.py | 11 ++-- 3 files changed, 34 insertions(+), 46 deletions(-) delete mode 100644 src/finn/data/verilog/truthtable/incomplete_table.v diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 110aeef..519625a 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -38,37 +38,39 @@ from finn.util.data_packing import npy_to_rtlsim_input -def binary_truthtable(inputs, care_set, node): +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 5 is provided in the care-set, the result to fifth - combination of inputs 101 is 1. The input is a vector size x, representing - x-bits binary input. An example is presented: + 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. - inputs = [1, 0, 1] - care_set = [1, 2] - - Possible combinations: 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 + ************************************************************************** + The MSB in the input numpy array represents the LSB in the LUT. + ************************************************************************** - """ + An example is presented: - inputs = inputs[::-1] # reverse input array for C style indexing + inputs[0:2] = [1, 0, 1] + care_set = [1, 2] - in_int = 0 # initialize integer representation of the binary input array + 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 - for idx, in_val in enumerate(inputs): - in_int += (1 << idx) * in_val # calculate integer value of binary input + """ - output = 1 if in_int in care_set else 0 # return 1 if the input is in result_one + # 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 @@ -126,7 +128,9 @@ def execute_node(self, context, graph): mode = self.get_nodeattr("exec_mode") if mode == "python": # calculate output in Python mode - output = binary_truthtable(input_entry, care_set, self) + output = binary_truthtable( + input_entry, care_set, self.get_nodeattr("in_bits") + ) elif mode == "rtlsim": # check the code directory is not empty verilog_dir = self.get_nodeattr("code_dir") + "/incomplete_table.v" diff --git a/src/finn/data/verilog/truthtable/incomplete_table.v b/src/finn/data/verilog/truthtable/incomplete_table.v deleted file mode 100644 index be57489..0000000 --- a/src/finn/data/verilog/truthtable/incomplete_table.v +++ /dev/null @@ -1,17 +0,0 @@ -module incomplete_table ( - input [15:0] in, - output reg result -); - - always @(in) begin - case(in) - 16'b0000000000000001 : result = 1'b1; - 16'b0000000000111010 : result = 1'b1; - 16'b0000000000001111 : result = 1'b1; - 16'b0000000001011001 : result = 1'b1; - 16'b0000001010110111 : result = 1'b1; - 16'b0001100101010101 : result = 1'b1; - default: result = 1'b0; - endcase - end -endmodule diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 1dee911..cbc105a 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -39,6 +39,7 @@ from finn.transformation.logicnets.gen_bintruthtable_verilog import ( GenBinaryTruthTableVerilog, ) +from finn.util.data_packing import npy_to_rtlsim_input export_onnx_path = "test_truthtable.onnx" @@ -109,11 +110,11 @@ def test_binarytruthtable(): } # Perform execution out_dict = oxe.execute_onnx(model, in_dict) - # Calculate result here locally for comparison with the CustomOp result - input_data = input_data[::-1] - out_idx = 0 - for idx, val in enumerate(input_data): - out_idx += (1 << idx) * val + + 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 # compare outputs assert entry == out_dict["output"] From 11a6b32e33346b4f7fbea42f6f74d7f907cc0e17 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 9 Mar 2021 09:49:15 +0000 Subject: [PATCH 17/85] Check the shape and the size of the input vector --- src/finn/custom_op/logicnets/binary_truthtable.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 519625a..5948eb9 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -124,6 +124,19 @@ def execute_node(self, context, graph): # 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": From 29945d22df8e987caf8b334c8fa9b5c554602f69 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 9 Mar 2021 10:42:40 +0000 Subject: [PATCH 18/85] Add UniqueNodeNames to create different verilog module names and files --- src/finn/custom_op/logicnets/binary_truthtable.py | 8 +++++--- tests/custom_op/test_binarytruthtable.py | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 5948eb9..c389a78 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -146,7 +146,8 @@ def execute_node(self, context, graph): ) elif mode == "rtlsim": # check the code directory is not empty - verilog_dir = self.get_nodeattr("code_dir") + "/incomplete_table.v" + 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) @@ -196,10 +197,11 @@ def verify_node(self): 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 incomplete_table (\n" + 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" @@ -220,6 +222,6 @@ def generate_verilog(self, care_set): # create temporary folder and save attribute value self.set_nodeattr("code_dir", make_build_dir("verilog_")) # create and write verilog file - verilog_file = open(self.get_nodeattr("code_dir") + "/incomplete_table.v", "w") + verilog_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".v", "w") verilog_file.write(verilog_string) verilog_file.close() diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index cbc105a..8fb954b 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -34,6 +34,7 @@ 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 ( @@ -93,6 +94,8 @@ def test_binarytruthtable(): 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()) # Generate verilog model = model.transform( From 5917c1b5a727928af18f36fb569852f08f5f35e3 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 10 Mar 2021 18:42:57 +0000 Subject: [PATCH 19/85] Initial commit of simple LogicNets model (Non working code) --- tests/logicnets-initial-model/concat_test.py | 55 +++++ tests/logicnets-initial-model/custom_model.py | 198 ++++++++++++++++++ tests/logicnets-initial-model/gather_test.py | 56 +++++ 3 files changed, 309 insertions(+) create mode 100644 tests/logicnets-initial-model/concat_test.py create mode 100644 tests/logicnets-initial-model/custom_model.py create mode 100644 tests/logicnets-initial-model/gather_test.py 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..3873a2f --- /dev/null +++ b/tests/logicnets-initial-model/custom_model.py @@ -0,0 +1,198 @@ +import numpy as np +import onnx +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_bintruthtable_verilog import ( + GenBinaryTruthTableVerilog, +) +from finn.util.data_packing import npy_to_rtlsim_input + +in_bits = 2 +care_set_data = np.array([1, 2, 3], dtype=np.float32) +indices0_data = np.array([1, 2]) +indices1_data = np.array([0, 1]) +in0_data = np.array([0, 1], dtype=np.float32) +in1_data = np.array([0, 1], dtype=np.float32) +in2_data = np.array([0, 1], dtype=np.float32) + +in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, [in_bits]) +in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, [in_bits]) +in2 = helper.make_tensor_value_info("in2", TensorProto.FLOAT, [in_bits]) +care_set = helper.make_tensor_value_info( + "care_set", TensorProto.FLOAT, [care_set_data.size] +) +out0 = helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1]) +out1 = helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1]) +indices0 = helper.make_tensor_value_info( + "indices0", TensorProto.FLOAT, indices0_data.shape +) +indices1 = helper.make_tensor_value_info( + "indices1", TensorProto.FLOAT, indices1_data.shape +) + +LUT0 = helper.make_node( + "BinaryTruthTable", + ["in0", "care_set"], + ["concat_in0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT1 = helper.make_node( + "BinaryTruthTable", + ["in1", "care_set"], + ["concat_in1"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT2 = helper.make_node( + "BinaryTruthTable", + ["in2", "care_set"], + ["concat_in2"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT3 = helper.make_node( + "BinaryTruthTable", + ["sparse_out0", "care_set"], + ["out0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT4 = helper.make_node( + "BinaryTruthTable", + ["sparse_out1", "care_set"], + ["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, +) + +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=[ + LUT0, + LUT1, + LUT2, + LUT3, + LUT4, + concat0, + gather0, + gather1, + ], + name="my_LogicNets model", + inputs=[in0, in1, in2, care_set, indices0, indices1], + outputs=[out0, out1], + value_info=[ + 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("sparse_out0", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("sparse_out1", TensorProto.FLOAT, [2]), + ], +) + +modelproto = helper.make_model(graph, producer_name="simple-model") +onnx.save(modelproto, "simple-model.onnx") + + +def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): + + 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_set else 0 + concat_in1 = 1 if in1_int in care_set else 0 + concat_in2 = 1 if in2_int in care_set 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_set else 0 + out1 = 1 if sparse_out1_int in care_set else 0 + + return out0, out1 + + +input_dict = { + "in0": in0_data, + "in1": in1_data, + "in2": in2_data, + "care_set": care_set_data, + "indices0": indices0_data, + "indices1": indices1_data, +} + +model = ModelWrapper(modelproto) + +model.save("after_wrap.onnx") + +model.set_tensor_datatype("in0", DataType.BINARY) +model.set_tensor_datatype("in1", DataType.BINARY) +model.set_tensor_datatype("in2", 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_set", DataType.UINT32) +model.set_tensor_datatype("indices0", DataType.UINT32) +model.set_tensor_datatype("indices1", DataType.UINT32) +# model.set_tensor_datatype("out0",DataType.BINARY) +# model.set_tensor_datatype("out1",DataType.BINARY) + +model = model.transform(InferDataTypes()) +model = model.transform(InferShapes()) +model.save("after-datatypes.onnx") +model = model.transform(GiveUniqueNodeNames()) +model.save("after-uniquenames.onnx") + +model = model.transform( + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) +) + +out = oxe.execute_onnx(model, input_dict) +print(input_dict) diff --git a/tests/logicnets-initial-model/gather_test.py b/tests/logicnets-initial-model/gather_test.py new file mode 100644 index 0000000..e236699 --- /dev/null +++ b/tests/logicnets-initial-model/gather_test.py @@ -0,0 +1,56 @@ +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 + +array_data = np.array([[1, 0, 1, 1], [1, 0, 1, 1]]) + +indices_data = np.array([0]) + + +array = helper.make_tensor_value_info("array", TensorProto.FLOAT, array_data.shape) +indices = helper.make_tensor_value_info( + "indices", TensorProto.FLOAT, indices_data.shape +) +output = helper.make_tensor_value_info("output", TensorProto.FLOAT, indices_data.shape) + +gather0 = helper.make_node( + "Gather", + inputs=["array", "indices"], + outputs=["output"], +) + +modelproto = helper.make_model( + helper.make_graph([gather0], "test_model", [array, indices], [output]) +) + + +model = ModelWrapper(modelproto) + +model.save("initial.onnx") + +model.set_tensor_datatype("array", DataType.BINARY) +model.set_tensor_datatype("indices", DataType.UINT32) +model.set_tensor_datatype("output", DataType.BINARY) + +model = model.transform(InferShapes()) + +model.save("after-shapes.onnx") + +model = model.transform(InferDataTypes()) + +model.save("after-types.onnx") + +in_dict = { + "array": array_data, + "indices": indices_data, +} + +out_dict = oxe.execute_onnx(model, in_dict) + +print(out_dict) From 8dc3ccf80ea576dc683ca3db9c63de880cbefd69 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 10 Mar 2021 18:43:32 +0000 Subject: [PATCH 20/85] Initial commit of simple LogicNets model (Non working code) --- tests/logicnets-initial-model/gather_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/logicnets-initial-model/gather_test.py b/tests/logicnets-initial-model/gather_test.py index e236699..f8a1310 100644 --- a/tests/logicnets-initial-model/gather_test.py +++ b/tests/logicnets-initial-model/gather_test.py @@ -8,7 +8,7 @@ from finn.transformation.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes -array_data = np.array([[1, 0, 1, 1], [1, 0, 1, 1]]) +array_data = np.array([[1, 0, 1, 1]]) indices_data = np.array([0]) From fa9c6414b213781914d50cff054756c69a40f7c8 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 12 Mar 2021 13:28:52 +0000 Subject: [PATCH 21/85] BinaryTruthTable returns a vector instead of a scalar and make_Shape_Compatible returns a constant node with inputs of the original node --- src/finn/custom_op/logicnets/binary_truthtable.py | 8 ++++---- tests/custom_op/test_binarytruthtable.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index c389a78..86f5c63 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -95,13 +95,13 @@ def make_shape_compatible_op(self, model): val = np.random.randn(1).astype(np.bool) node = helper.make_node( "Constant", - inputs=[], + inputs=[node.input[0], node.input[1]], outputs=["val"], value=helper.make_tensor( name="const_tensor", - data_type=onnx.TensorProto.BOOL, + data_type=onnx.TensorProto.FLOAT, dims=val.shape, - vals=val.flatten().astype(bool), + vals=val.flatten().astype(float), ), ) return node @@ -166,7 +166,7 @@ def execute_node(self, context, graph): ) ) # return output - context[node.output[0]] = output + context[node.output[0]] = np.array([output]) def verify_node(self): info_messages = [] diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 8fb954b..1ff61a5 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -119,8 +119,9 @@ def test_binarytruthtable(): ] entry = 1 if out_idx in care_set_data else 0 + expected = np.array([entry]) # compare outputs - assert entry == out_dict["output"] + 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") From 4acb7768165ae80725834301955f4479a6e7a920 Mon Sep 17 00:00:00 2001 From: jalezeta <51440887+jalezeta@users.noreply.github.com> Date: Fri, 12 Mar 2021 16:41:21 +0000 Subject: [PATCH 22/85] Update binary_truthtable.py Specify output array type to np.float32 --- src/finn/custom_op/logicnets/binary_truthtable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 86f5c63..a488185 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -166,7 +166,7 @@ def execute_node(self, context, graph): ) ) # return output - context[node.output[0]] = np.array([output]) + context[node.output[0]] = np.array([output], dtype=np.float32) def verify_node(self): info_messages = [] From d082996a984c3b21cbcd06b3ef4f5ecd7f3dc372 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 12 Mar 2021 16:59:25 +0000 Subject: [PATCH 23/85] Fix simple LogicNets model creation --- .../after-datatypes.onnx | Bin 0 -> 1600 bytes .../logicnets-initial-model/after-shape.onnx | Bin 0 -> 1560 bytes .../after-uniquenames.onnx | Bin 0 -> 1735 bytes tests/logicnets-initial-model/after_wrap.onnx | Bin 0 -> 1090 bytes tests/logicnets-initial-model/custom_model.py | 42 +++++++++++------- .../logicnets-initial-model/simple-model.onnx | Bin 0 -> 1090 bytes 6 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 tests/logicnets-initial-model/after-datatypes.onnx create mode 100644 tests/logicnets-initial-model/after-shape.onnx create mode 100644 tests/logicnets-initial-model/after-uniquenames.onnx create mode 100644 tests/logicnets-initial-model/after_wrap.onnx create mode 100644 tests/logicnets-initial-model/simple-model.onnx diff --git a/tests/logicnets-initial-model/after-datatypes.onnx b/tests/logicnets-initial-model/after-datatypes.onnx new file mode 100644 index 0000000000000000000000000000000000000000..f743eb899e5e9c4b07f4e6a4ad186ed56ca6ffd2 GIT binary patch literal 1600 zcmcIjO;5r=6tsmvUC`(r5HD&ldca6%y>SDh#)M#E@SvAwDXXlZ+oroD@Nc;KFWr8m zz6vEC6|)tc)wBd7jok9rB-O}qcY?6h)Ge`mzv(Gy)({R&P+(pwLQP} zK{*Y`G`2I>+L_z3v)0;KTd=!BAU$w4!P!a^_GYqCx{cCqR#1`5nW*om@X%FCR}^G0 zK4uOTZCnvP2?Vtj+HRB{h@8+MhipQ!nUXyOH9#)KNb~>u&KSbpL&GgJjMR|e8HxIx+m)5~lWxvm-B2HSJ6GOJ=QkAvZj6(~}=KYt*t nvrCjtlg(OHNlJ+c>l6~gWtw`pCH3FsmhbydUG?Ukeo*}eV0Xn~ literal 0 HcmV?d00001 diff --git a/tests/logicnets-initial-model/after-shape.onnx b/tests/logicnets-initial-model/after-shape.onnx new file mode 100644 index 0000000000000000000000000000000000000000..4cb8bdcff4af2fc3cb4378cb5836a5a4f3e1bcfc GIT binary patch literal 1560 zcmcIjK~KUk7Jji7!qe2PmmbE18=s)47TDE0; z3ygS>Tl2ox_qzAK8D(4*%=PE=(e=i3-ubB_7Zn(9Bf}v9wFQ+JIUaY2v=!Po>oQJ) za1boybU;RP+N_~tx}=W1zTYVMp`3brr4^c|sLZ%MVp7!gm8N%U-;DE?vk=mAZO?B_ zD5n9L$9CpUJM&O>)=oR?0CukkqzBF>I9qAL-aNr z$IPLkjVr<@fuOcR+l|s2ky9GvkWEN7Q?iGk2FRrtY5srT8AI6n+VB94W9)|ZyEkD@ zpGt8Pt@-dg+Hs7KVQ9FBbVH9Ja~o3a09Fkow80BQg{P&g@p7WN!DHzJA7K>b3$(Qa zgHO5UmXYcS)*b50r3dE289p)X3{3m)t_7Mo5TA5j`}Q)nc|`BUoGBMGD6# zKax-ACw7f3XACy#A>2l9-n@C9nYB~LdrHj18UH>HCVY1FcON}ls3N3`9G^v;Dy}i| z1L-qO4cf*%Az8E>MGHM0vGI&kw{x(Lb(`oTf8sukMLV@{si%Q_wJP0Ds4gTO3$0rA zt7Ts`{|G71{DsoNoCe|91D8Bvv(y7A^gs$ckezw(3O#rQ9=siRxJ6+0ASNL8vi$@L z-FAK?yK+0VBIMvjmZ;dABUh6-7vI5nu-c5axS^2ltqmP93G z1eTGkjAUgm7J|ZHl+{(E&0{*>!4UH<;8Ddciq7fc{as)@U(!MFAp9ZM>f7qI-5;)A z8b-)*EL=slV#6D+o=B(o7^Br9fVP!l(1P{Y%Ff38 z10Xw>DX)RNuJ>C zd9OTwlKc8n8A#jG4brwWJ?gLfS1EO1%k*bI=GE$)~RVOBv$OIRR5<7~G1b@TcpR5yT`cPra zX5Tx%JDtw7#X3f&St4Fjy%Ndfn?ee{R3o4<&qZW}We{r>a~nA{=uV~L`C*>#>}JlF zi5T?YUhG9274L&)cCZ_*j-EewfRXI3*uM{!8!t+0qO@jhv+=6I^;|Q&9|~ogElny zUYh=$s|UPPbVmUy)k?;~j2MnDXufm15%~>rh{@qEnX&W`edx7}KZP~VWz83lWycu+ zr5?gLfS1EO1%k*bI=GE$)~RVOBv$OIRR5<7~G1b@TcpR5yT`cPra zX5Tx%JDtw7#X3f&St4Fjy%Ndfn?ee{R3o4<&qZW}We{r>a~nA{=uV~L`C*>#>}JlF zi5T?YUhG9274L&)cCZ_*j-EewfRXI3*uM{!8!t+0qO@jhv+=6I^;|Q&9|~ogElny zUYh=$s|UPPbVmUy)k?;~j2MnDXufm15%~>rh{@qEnX&W`edx7}KZP~VWz83lWycu+ zr5 Date: Tue, 16 Mar 2021 11:08:54 +0000 Subject: [PATCH 24/85] Remove inputs and add custom tensor name in make_shape compatible function --- src/finn/custom_op/logicnets/binary_truthtable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index a488185..f45fae6 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -33,6 +33,7 @@ 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 @@ -93,12 +94,13 @@ def get_nodeattr_types(self): 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=[node.input[0], node.input[1]], + inputs=[], outputs=["val"], value=helper.make_tensor( - name="const_tensor", + name=tensor_name, data_type=onnx.TensorProto.FLOAT, dims=val.shape, vals=val.flatten().astype(float), From 6a01bda839435e521085c21af042d1fca1371fbb Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 26 Mar 2021 16:13:08 +0000 Subject: [PATCH 25/85] Change operation pattern of LogicNets model to --- tests/logicnets-initial-model/custom_model.py | 61 ++++++++++++++++--- tests/logicnets-initial-model/gather_test.py | 56 ----------------- 2 files changed, 51 insertions(+), 66 deletions(-) delete mode 100644 tests/logicnets-initial-model/gather_test.py diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index df537df..46f705f 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -18,6 +18,7 @@ care_set_data = np.array([1, 2, 3], dtype=np.float32) indices0_data = np.array([1, 2]) indices1_data = np.array([0, 1]) +indices_in_data = np.array([0, 1]) in0_data = np.array([0, 1], dtype=np.float32) in1_data = np.array([0, 1], dtype=np.float32) in2_data = np.array([0, 1], dtype=np.float32) @@ -28,17 +29,39 @@ care_set = helper.make_tensor_value_info( "care_set", TensorProto.FLOAT, care_set_data.shape ) -out0 = helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1]) -out1 = helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1]) +final_output = helper.make_tensor_value_info("final_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_in = helper.make_tensor_value_info( + "indices_in", TensorProto.INT64, indices_in_data.shape +) + +gather_in0 = helper.make_node( + "Gather", + ["in0", "indices_in"], + ["LUTin0"], +) + +gather_in1 = helper.make_node( + "Gather", + ["in1", "indices_in"], + ["LUTin1"], +) + +gather_in2 = helper.make_node( + "Gather", + ["in2", "indices_in"], + ["LUTin2"], +) + LUT0 = helper.make_node( "BinaryTruthTable", - ["in0", "care_set"], + ["LUTin0", "care_set"], ["concat_in0"], domain="finn.custom_op.general", in_bits=in_bits, @@ -47,7 +70,7 @@ LUT1 = helper.make_node( "BinaryTruthTable", - ["in1", "care_set"], + ["LUTin1", "care_set"], ["concat_in1"], domain="finn.custom_op.general", in_bits=in_bits, @@ -56,7 +79,7 @@ LUT2 = helper.make_node( "BinaryTruthTable", - ["in2", "care_set"], + ["LUTin2", "care_set"], ["concat_in2"], domain="finn.custom_op.general", in_bits=in_bits, @@ -88,6 +111,13 @@ axis=0, ) +concat_out = helper.make_node( + "Concat", + ["out0", "out1"], + ["final_output"], + axis=0, +) + gather0 = helper.make_node( "Gather", ["concat_out", "indices0"], @@ -107,18 +137,27 @@ LUT2, LUT3, LUT4, + gather_in0, + gather_in1, + gather_in2, concat0, gather0, gather1, + concat_out, ], name="my_LogicNets model", - inputs=[in0, in1, in2, care_set, indices0, indices1], - outputs=[out0, out1], + inputs=[in0, in1, in2, care_set, indices0, indices1, indices_in], + outputs=[final_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 ), @@ -175,6 +214,9 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model.set_tensor_datatype("in0", DataType.BINARY) model.set_tensor_datatype("in1", DataType.BINARY) model.set_tensor_datatype("in2", 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) @@ -183,8 +225,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model.set_tensor_datatype("care_set", DataType.UINT32) model.set_tensor_datatype("indices0", DataType.UINT32) model.set_tensor_datatype("indices1", DataType.UINT32) -model.set_tensor_datatype("out0", DataType.BINARY) -model.set_tensor_datatype("out1", DataType.BINARY) +model.set_tensor_datatype("final_output", DataType.BINARY) model = model.transform(InferShapes()) model.save("after-shape.onnx") @@ -201,7 +242,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): out = oxe.execute_onnx(model, input_dict) -output = np.array([out["out0"], out["out1"]]) +output = out["final_output"] expected = expected_output( in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits diff --git a/tests/logicnets-initial-model/gather_test.py b/tests/logicnets-initial-model/gather_test.py deleted file mode 100644 index f8a1310..0000000 --- a/tests/logicnets-initial-model/gather_test.py +++ /dev/null @@ -1,56 +0,0 @@ -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 - -array_data = np.array([[1, 0, 1, 1]]) - -indices_data = np.array([0]) - - -array = helper.make_tensor_value_info("array", TensorProto.FLOAT, array_data.shape) -indices = helper.make_tensor_value_info( - "indices", TensorProto.FLOAT, indices_data.shape -) -output = helper.make_tensor_value_info("output", TensorProto.FLOAT, indices_data.shape) - -gather0 = helper.make_node( - "Gather", - inputs=["array", "indices"], - outputs=["output"], -) - -modelproto = helper.make_model( - helper.make_graph([gather0], "test_model", [array, indices], [output]) -) - - -model = ModelWrapper(modelproto) - -model.save("initial.onnx") - -model.set_tensor_datatype("array", DataType.BINARY) -model.set_tensor_datatype("indices", DataType.UINT32) -model.set_tensor_datatype("output", DataType.BINARY) - -model = model.transform(InferShapes()) - -model.save("after-shapes.onnx") - -model = model.transform(InferDataTypes()) - -model.save("after-types.onnx") - -in_dict = { - "array": array_data, - "indices": indices_data, -} - -out_dict = oxe.execute_onnx(model, in_dict) - -print(out_dict) From 908ffab63106587c74f2919de57797a69505a596 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 26 Mar 2021 17:05:11 +0000 Subject: [PATCH 26/85] Fix small problem, missing input index data in the input dictionary --- tests/logicnets-initial-model/custom_model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 46f705f..40152d8 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -206,6 +206,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): "care_set": care_set_data, "indices0": indices0_data, "indices1": indices1_data, + "indices_in": indices_in_data, } model = ModelWrapper(modelproto) From a0bfc0faa0a01d401721d870f7f1f5f12ce3a26f Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Mon, 29 Mar 2021 13:23:54 +0100 Subject: [PATCH 27/85] Add Concat operation at the beginning of the model --- tests/logicnets-initial-model/custom_model.py | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 40152d8..2178531 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -18,7 +18,10 @@ care_set_data = np.array([1, 2, 3], dtype=np.float32) indices0_data = np.array([1, 2]) indices1_data = np.array([0, 1]) -indices_in_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]) + in0_data = np.array([0, 1], dtype=np.float32) in1_data = np.array([0, 1], dtype=np.float32) in2_data = np.array([0, 1], dtype=np.float32) @@ -37,25 +40,38 @@ indices1 = helper.make_tensor_value_info( "indices1", TensorProto.INT64, indices1_data.shape ) -indices_in = helper.make_tensor_value_info( - "indices_in", TensorProto.INT64, indices_in_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 +) + +concat_in = helper.make_node( + "Concat", + ["in0", "in1", "in2"], + ["concatenated_input"], + axis=0, ) gather_in0 = helper.make_node( "Gather", - ["in0", "indices_in"], + ["concatenated_input", "indices_in0"], ["LUTin0"], ) gather_in1 = helper.make_node( "Gather", - ["in1", "indices_in"], + ["concatenated_input", "indices_in1"], ["LUTin1"], ) gather_in2 = helper.make_node( "Gather", - ["in2", "indices_in"], + ["concatenated_input", "indices_in2"], ["LUTin2"], ) @@ -132,11 +148,7 @@ graph = helper.make_graph( nodes=[ - LUT0, - LUT1, - LUT2, - LUT3, - LUT4, + concat_in, gather_in0, gather_in1, gather_in2, @@ -144,11 +156,27 @@ gather0, gather1, concat_out, + LUT0, + LUT1, + LUT2, + LUT3, + LUT4, ], name="my_LogicNets model", - inputs=[in0, in1, in2, care_set, indices0, indices1, indices_in], + inputs=[ + in0, + in1, + in2, + care_set, + indices0, + indices1, + indices_in0, + indices_in1, + indices_in2, + ], outputs=[final_output], value_info=[ + helper.make_tensor_value_info("concatenated_input", TensorProto.FLOAT, [6]), 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]), @@ -206,7 +234,9 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): "care_set": care_set_data, "indices0": indices0_data, "indices1": indices1_data, - "indices_in": indices_in_data, + "indices_in0": indices_in0_data, + "indices_in1": indices_in1_data, + "indices_in2": indices_in2_data, } model = ModelWrapper(modelproto) @@ -224,6 +254,10 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model.set_tensor_datatype("sparse_out0", DataType.BINARY) model.set_tensor_datatype("sparse_out1", DataType.BINARY) model.set_tensor_datatype("care_set", 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("final_output", DataType.BINARY) From 48ef9465fe1680b904e3bde531eb43b6d20ca5f3 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Mon, 29 Mar 2021 15:31:36 +0100 Subject: [PATCH 28/85] Support for dedicated care_set in the node level Verilog generation --- src/finn/transformation/logicnets/gen_bintruthtable_verilog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py index d60bb6f..b4bae83 100644 --- a/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py +++ b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py @@ -53,6 +53,7 @@ def __init__(self, num_workers, care_set): def applyNodeLocal(self, node): op_type = node.op_type if op_type == "BinaryTruthTable": - _genbintruthtable_verilog(node, self.care_set) + specific_care_set = self.care_set[node.input[1]] + _genbintruthtable_verilog(node, specific_care_set) return (node, False) From c208bbbaec0f3c67ead10807c2e55eda55d7629e Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Mon, 29 Mar 2021 15:54:24 +0100 Subject: [PATCH 29/85] Add support for separate care sets --- tests/logicnets-initial-model/custom_model.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 2178531..1a63439 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -15,7 +15,8 @@ from finn.util.data_packing import npy_to_rtlsim_input in_bits = 2 -care_set_data = np.array([1, 2, 3], dtype=np.float32) +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]) @@ -29,8 +30,11 @@ in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, in0_data.shape) in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, in1_data.shape) in2 = helper.make_tensor_value_info("in2", TensorProto.FLOAT, in2_data.shape) -care_set = helper.make_tensor_value_info( - "care_set", TensorProto.FLOAT, care_set_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 ) final_output = helper.make_tensor_value_info("final_output", TensorProto.FLOAT, [2]) @@ -77,7 +81,7 @@ LUT0 = helper.make_node( "BinaryTruthTable", - ["LUTin0", "care_set"], + ["LUTin0", "care_set0"], ["concat_in0"], domain="finn.custom_op.general", in_bits=in_bits, @@ -86,7 +90,7 @@ LUT1 = helper.make_node( "BinaryTruthTable", - ["LUTin1", "care_set"], + ["LUTin1", "care_set0"], ["concat_in1"], domain="finn.custom_op.general", in_bits=in_bits, @@ -95,7 +99,7 @@ LUT2 = helper.make_node( "BinaryTruthTable", - ["LUTin2", "care_set"], + ["LUTin2", "care_set0"], ["concat_in2"], domain="finn.custom_op.general", in_bits=in_bits, @@ -104,7 +108,7 @@ LUT3 = helper.make_node( "BinaryTruthTable", - ["sparse_out0", "care_set"], + ["sparse_out0", "care_set1"], ["out0"], domain="finn.custom_op.general", in_bits=in_bits, @@ -113,7 +117,7 @@ LUT4 = helper.make_node( "BinaryTruthTable", - ["sparse_out1", "care_set"], + ["sparse_out1", "care_set1"], ["out1"], domain="finn.custom_op.general", in_bits=in_bits, @@ -167,7 +171,8 @@ in0, in1, in2, - care_set, + care_set0, + care_set1, indices0, indices1, indices_in0, @@ -231,7 +236,8 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): "in0": in0_data, "in1": in1_data, "in2": in2_data, - "care_set": care_set_data, + "care_set0": care_set0_data, + "care_set1": care_set1_data, "indices0": indices0_data, "indices1": indices1_data, "indices_in0": indices_in0_data, @@ -253,7 +259,8 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): 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_set", DataType.UINT32) +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) @@ -271,16 +278,23 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model = model.transform(GiveUniqueNodeNames()) model.save("after-uniquenames.onnx") +care_set_dict = { + "care_set0": care_set0_data, + "care_set1": care_set1_data, +} + model = model.transform( - GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_dict) ) out = oxe.execute_onnx(model, input_dict) output = out["final_output"] -expected = expected_output( - in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits -) +# expected = expected_output( +# in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits +# ) -assert (output == expected).all +print(output) +# print(expected) +# assert (output == expected).all From 8b83e328137dd86b30174865e012883a8e1e9441 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Tue, 30 Mar 2021 08:52:25 +0100 Subject: [PATCH 30/85] Add python based prediction and result assertion --- tests/logicnets-initial-model/custom_model.py | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 1a63439..f067cde 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -23,9 +23,9 @@ indices_in1_data = np.array([2, 3]) indices_in2_data = np.array([4, 5]) -in0_data = np.array([0, 1], dtype=np.float32) -in1_data = np.array([0, 1], dtype=np.float32) -in2_data = np.array([0, 1], dtype=np.float32) +in0_data = np.array([0, 0], dtype=np.float32) +in1_data = np.array([0, 0], dtype=np.float32) +in2_data = np.array([0, 0], dtype=np.float32) in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, in0_data.shape) in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, in1_data.shape) @@ -154,17 +154,17 @@ nodes=[ concat_in, gather_in0, - gather_in1, - gather_in2, concat0, gather0, + LUT2, + LUT3, + LUT4, gather1, concat_out, LUT0, LUT1, - LUT2, - LUT3, - LUT4, + gather_in1, + gather_in2, ], name="my_LogicNets model", inputs=[ @@ -204,15 +204,14 @@ onnx.save(modelproto, "simple-model.onnx") -def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): - +def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_bits): 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_set else 0 - concat_in1 = 1 if in1_int in care_set else 0 - concat_in2 = 1 if in2_int in care_set else 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)] @@ -226,8 +225,8 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): 0 ] - out0 = 1 if sparse_out0_int in care_set else 0 - out1 = 1 if sparse_out1_int in care_set else 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]) @@ -291,10 +290,15 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): output = out["final_output"] -# expected = expected_output( -# in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits -# ) +expected = expected_output( + in0_data, + in1_data, + in0_data, + indices0_data, + indices1_data, + care_set0_data, + care_set1_data, + in_bits, +) -print(output) -# print(expected) -# assert (output == expected).all +assert (output == expected).all From 5a0a8d8d8866df6f17fc4a25a83f3a4d0b38db3f Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Tue, 30 Mar 2021 12:44:20 +0100 Subject: [PATCH 31/85] Remove input concat block. We assume the general input to the model is a single array with 6-bits. This array is later sliced based on the gather block. --- tests/logicnets-initial-model/custom_model.py | 58 ++++++++----------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index f067cde..2c015e5 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -23,20 +23,19 @@ indices_in1_data = np.array([2, 3]) indices_in2_data = np.array([4, 5]) -in0_data = np.array([0, 0], dtype=np.float32) -in1_data = np.array([0, 0], dtype=np.float32) -in2_data = np.array([0, 0], dtype=np.float32) +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 +) -in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, in0_data.shape) -in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, in1_data.shape) -in2 = helper.make_tensor_value_info("in2", TensorProto.FLOAT, in2_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 ) -final_output = helper.make_tensor_value_info("final_output", TensorProto.FLOAT, [2]) +general_output = helper.make_tensor_value_info("general_output", TensorProto.FLOAT, [2]) indices0 = helper.make_tensor_value_info( "indices0", TensorProto.INT64, indices0_data.shape @@ -54,28 +53,22 @@ "indices_in2", TensorProto.INT64, indices_in2_data.shape ) -concat_in = helper.make_node( - "Concat", - ["in0", "in1", "in2"], - ["concatenated_input"], - axis=0, -) gather_in0 = helper.make_node( "Gather", - ["concatenated_input", "indices_in0"], + ["general_input", "indices_in0"], ["LUTin0"], ) gather_in1 = helper.make_node( "Gather", - ["concatenated_input", "indices_in1"], + ["general_input", "indices_in1"], ["LUTin1"], ) gather_in2 = helper.make_node( "Gather", - ["concatenated_input", "indices_in2"], + ["general_input", "indices_in2"], ["LUTin2"], ) @@ -134,7 +127,7 @@ concat_out = helper.make_node( "Concat", ["out0", "out1"], - ["final_output"], + ["general_output"], axis=0, ) @@ -152,7 +145,6 @@ graph = helper.make_graph( nodes=[ - concat_in, gather_in0, concat0, gather0, @@ -168,9 +160,7 @@ ], name="my_LogicNets model", inputs=[ - in0, - in1, - in2, + general_input, care_set0, care_set1, indices0, @@ -179,9 +169,8 @@ indices_in1, indices_in2, ], - outputs=[final_output], + outputs=[general_output], value_info=[ - helper.make_tensor_value_info("concatenated_input", TensorProto.FLOAT, [6]), 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]), @@ -204,7 +193,12 @@ onnx.save(modelproto, "simple-model.onnx") -def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_bits): +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] @@ -232,9 +226,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ input_dict = { - "in0": in0_data, - "in1": in1_data, - "in2": in2_data, + "general_input": general_input_data, "care_set0": care_set0_data, "care_set1": care_set1_data, "indices0": indices0_data, @@ -247,9 +239,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ model = ModelWrapper(modelproto) model.save("after_wrap.onnx") -model.set_tensor_datatype("in0", DataType.BINARY) -model.set_tensor_datatype("in1", DataType.BINARY) -model.set_tensor_datatype("in2", DataType.BINARY) +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) @@ -266,7 +256,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ 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("final_output", DataType.BINARY) +model.set_tensor_datatype("general_output", DataType.BINARY) model = model.transform(InferShapes()) model.save("after-shape.onnx") @@ -288,12 +278,10 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ out = oxe.execute_onnx(model, input_dict) -output = out["final_output"] +output = out["general_output"] expected = expected_output( - in0_data, - in1_data, - in0_data, + general_input_data, indices0_data, indices1_data, care_set0_data, From 780205744b9ac487e5edec2b66943007b9ded97c Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Thu, 1 Apr 2021 15:13:32 +0100 Subject: [PATCH 32/85] 1- Add Verilog generation for Custom logicnets model.\n2- Add test for verilog generation code using PyVerilator simulation. --- .../logicnets/gen_logicnets_verilog.py | 280 ++++++++++++++++ tests/custom_op/test_binarytruthtable.py | 7 +- tests/logicnets-initial-model/custom_model.py | 18 +- .../transformation/test_logicnets_verilog.py | 315 ++++++++++++++++++ 4 files changed, 613 insertions(+), 7 deletions(-) create mode 100644 src/finn/transformation/logicnets/gen_logicnets_verilog.py create mode 100644 tests/transformation/test_logicnets_verilog.py 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..620cc96 --- /dev/null +++ b/src/finn/transformation/logicnets/gen_logicnets_verilog.py @@ -0,0 +1,280 @@ +# 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): + graph = model.graph + + # TO BE DISCUSSED + # try: + # code_dir + # except : + # code_dir = make_build_dir("logicnets_model_") + + # 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 code_dir + + +def _generate_verilog(model, indices, code_dir): + + # Generate verilog file + verilog_file = open(code_dir + "/" + "LogicNetsModule.v", "w") + + graph = model.graph + + # 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, + ) + # Get number of nodes in the ONNX graph + number_nodes = len(graph.node) + + # 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 + for j in range(index): + if (graph.node[j].op_type == "Gather") and ( + graph.node[j].output[0] == op_input_name + ): + # Step 4 + gather_index_name = graph.node[j].input[1] + gather_input_name = graph.node[j].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" + break + k = index + # Step 6 + while k < number_nodes: + if (graph.node[k].op_type == "Concat") and op_output_name in graph.node[ + k + ].input: + # Step 7 + concat_out_position = list(graph.node[k].input).index( + op_output_name + ) + # Step 8 + concat_out_name = graph.node[k].output[0] + # Step 9 + 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, + ) + break + k += 1 + 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, code_dir): + super().__init__() + self.care_set = care_set + self.indices = indices + self.code_dir = code_dir + + 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 + code_dir = _create_logicnets_folder(model, code_dir=self.code_dir) + + # Generate the Verilog wrapper that connects every individual BinaryTruthTable + # verilog module. + _generate_verilog(model, indices=self.indices, code_dir=code_dir) + + return (model, False) diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 1ff61a5..c13d0e6 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -97,9 +97,14 @@ def test_binarytruthtable(): # 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_data) + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_dict) ) # Loop over "python" and "rtlsim" execution modes diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 2c015e5..47cdf4f 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -9,9 +9,7 @@ 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_logicnets_verilog import GenLogicNetsVerilog from finn.util.data_packing import npy_to_rtlsim_input in_bits = 2 @@ -160,7 +158,6 @@ ], name="my_LogicNets model", inputs=[ - general_input, care_set0, care_set1, indices0, @@ -168,6 +165,7 @@ indices_in0, indices_in1, indices_in2, + general_input, ], outputs=[general_output], value_info=[ @@ -226,8 +224,8 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit input_dict = { - "general_input": general_input_data, "care_set0": care_set0_data, + "general_input": general_input_data, "care_set1": care_set1_data, "indices0": indices0_data, "indices1": indices1_data, @@ -272,8 +270,16 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit "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( - GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_dict) + GenLogicNetsVerilog(care_set=care_set_dict, indices=indices_dict) ) out = oxe.execute_onnx(model, input_dict) diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py new file mode 100644 index 0000000..20b901b --- /dev/null +++ b/tests/transformation/test_logicnets_verilog.py @@ -0,0 +1,315 @@ +import numpy as np +import onnx +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.basic import make_build_dir +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") + onnx.save(modelproto, "simple-model.onnx") + + 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.save("after_wrap.onnx") + + 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, + } + + # Generate directory for storing Verilog files + code_dir = make_build_dir("logicnets_model_") + model = model.transform( + GenLogicNetsVerilog( + care_set=care_set_dict, indices=indices_dict, code_dir=code_dir + ) + ) + + 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 + 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) From 741464fa8fc63223e05ba4d69bde636921eed075 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Thu, 1 Apr 2021 15:26:13 +0100 Subject: [PATCH 33/85] Reformat verilog generation file --- tests/transformation/test_logicnets_verilog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py index 20b901b..023d6a6 100644 --- a/tests/transformation/test_logicnets_verilog.py +++ b/tests/transformation/test_logicnets_verilog.py @@ -303,6 +303,7 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit care_set1_data, in_bits, ) + assert np.array_equal(output, expected) # PyVerilator simulation of generated Verilog code @@ -312,4 +313,5 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit 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) From 32fef3338e0cd82bb3c29ad9703db66b249bf98a Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 2 Apr 2021 15:22:34 +0100 Subject: [PATCH 34/85] 1- Add TruthTable customOp for X:Y LUT operations(python and rtlsim).\n2- Add node level Verilog generation transformation. \n3- Add tests to verify functionalities. --- src/finn/custom_op/general/__init__.py | 2 + .../custom_op/logicnets/binary_truthtable.py | 20 +- src/finn/custom_op/logicnets/truthtable.py | 330 ++++++++++++++++++ .../logicnets/gen_truthtable_verilog.py | 60 ++++ tests/custom_op/test_binarytruthtable.py | 4 +- tests/custom_op/test_truthtable.py | 151 ++++++++ 6 files changed, 559 insertions(+), 8 deletions(-) create mode 100644 src/finn/custom_op/logicnets/truthtable.py create mode 100644 src/finn/transformation/logicnets/gen_truthtable_verilog.py create mode 100644 tests/custom_op/test_truthtable.py diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 50f1121..0af907e 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -34,6 +34,7 @@ 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() @@ -45,3 +46,4 @@ custom_op["XnorPopcountMatMul"] = XnorPopcountMatMul custom_op["Im2Col"] = Im2Col custom_op["BinaryTruthTable"] = BinaryTruthTable +custom_op["TruthTable"] = TruthTable diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index f45fae6..1ad8121 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -77,7 +77,7 @@ def binary_truthtable(input, care_set, bits): class BinaryTruthTable(CustomOp): - """The class corresponing to the TruthTable function. """ + """The class corresponing to the Binary TruthTable function. """ def get_nodeattr_types(self): return { @@ -118,7 +118,7 @@ def infer_node_datatype(self, model): assert ( model.get_tensor_datatype(node.input[1]) == DataType["UINT32"] ), """ The input vector DataType is not UINT32.""" - # check that the input[2] is UINT32 + # set the output[0] tensor datatype to BINARY model.set_tensor_datatype(node.output[0], DataType["BINARY"]) def execute_node(self, context, graph): @@ -174,7 +174,7 @@ def verify_node(self): info_messages = [] # verify number of attributes - num_of_attr = 0 + num_of_attr = 4 if len(self.onnx_node.attribute) == num_of_attr: info_messages.append("The number of attributes is correct") else: @@ -186,13 +186,21 @@ def verify_node(self): ) # verify that all necessary attributes exist - info_messages.append("Truthtable should not have any attributes") + 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("TruthTable needs 2 data inputs") + info_messages.append("BinaryTruthTable needs 2 data inputs") return info_messages @@ -222,7 +230,7 @@ def generate_verilog(self, care_set): # 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("verilog_")) + self.set_nodeattr("code_dir", make_build_dir("BinaryTruthTable_verilog_")) # create and write verilog file verilog_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".v", "w") verilog_file.write(verilog_string) diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py new file mode 100644 index 0000000..e1245a4 --- /dev/null +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -0,0 +1,330 @@ +# 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 Binary 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_verilog_")) + # create and write verilog file + verilog_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".v", "w") + verilog_file.write(verilog_string) + verilog_file.close() 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/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index c13d0e6..5f1a60d 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -42,7 +42,7 @@ ) from finn.util.data_packing import npy_to_rtlsim_input -export_onnx_path = "test_truthtable.onnx" +# export_onnx_path = "test_truthtable.onnx" def test_binarytruthtable(): @@ -63,7 +63,7 @@ def test_binarytruthtable(): care_set_data = np.asarray([1, 58, 15, 89, 695, 6485], dtype=np.float32) in_bits = 16 - # Set input and care_set tensor information + # 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] diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py new file mode 100644 index 0000000..f623086 --- /dev/null +++ b/tests/custom_op/test_truthtable.py @@ -0,0 +1,151 @@ +# 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_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) + ) + + # 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") From 055a9692fd23a9779cad1899fc7d0b0242c75660 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 9 Apr 2021 02:07:00 +0100 Subject: [PATCH 35/85] Make the algorithm cleaner by using ModelWrapper functions to find preceding and succeding nodes. Add assertions. --- .../logicnets/gen_logicnets_verilog.py | 127 +++++++++--------- .../transformation/test_logicnets_verilog.py | 9 +- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/finn/transformation/logicnets/gen_logicnets_verilog.py b/src/finn/transformation/logicnets/gen_logicnets_verilog.py index 620cc96..64a6776 100644 --- a/src/finn/transformation/logicnets/gen_logicnets_verilog.py +++ b/src/finn/transformation/logicnets/gen_logicnets_verilog.py @@ -34,8 +34,7 @@ from finn.transformation.logicnets.gen_bintruthtable_verilog import ( GenBinaryTruthTableVerilog, ) - -# from finn.util.basic import make_build_dir +from finn.util.basic import make_build_dir def _check_node_verilog(model): @@ -54,15 +53,11 @@ def _check_node_verilog(model): return True -def _create_logicnets_folder(model, code_dir): - graph = model.graph - - # TO BE DISCUSSED - # try: - # code_dir - # except : - # code_dir = make_build_dir("logicnets_model_") +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: @@ -72,16 +67,31 @@ def _create_logicnets_folder(model, code_dir): node_dir = customOp.get_nodeattr("code_dir") node_file = node_dir + "/" + nodeName + ".v" shutil.copy2(node_file, code_dir) - return code_dir + return model -def _generate_verilog(model, indices, code_dir): +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. @@ -120,8 +130,6 @@ def _generate_verilog(model, indices, code_dir): output_shape, output_tensor_name, ) - # Get number of nodes in the ONNX graph - number_nodes = len(graph.node) # 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. @@ -178,52 +186,48 @@ def _generate_verilog(model, indices, code_dir): node_name = node.name customOp = registry.getCustomOp(node) in_bits = customOp.get_nodeattr("in_bits") - 1 + # Step 3 - for j in range(index): - if (graph.node[j].op_type == "Gather") and ( - graph.node[j].output[0] == op_input_name - ): - # Step 4 - gather_index_name = graph.node[j].input[1] - gather_input_name = graph.node[j].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" - break - k = index + 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 - while k < number_nodes: - if (graph.node[k].op_type == "Concat") and op_output_name in graph.node[ - k - ].input: - # Step 7 - concat_out_position = list(graph.node[k].input).index( - op_output_name - ) - # Step 8 - concat_out_name = graph.node[k].output[0] - # Step 9 - 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, + 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_out_position, ) - break - k += 1 + 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 @@ -251,11 +255,10 @@ class GenLogicNetsVerilog(Transformation): - The code_dir is used to return the path of the generated verilog source code.""" - def __init__(self, care_set, indices, code_dir): + def __init__(self, care_set, indices): super().__init__() self.care_set = care_set self.indices = indices - self.code_dir = code_dir def apply(self, model): @@ -270,11 +273,11 @@ def apply(self, model): ) # Create LogicNets folder and copy all individual BinaryTruthTable code into - # the folder - code_dir = _create_logicnets_folder(model, code_dir=self.code_dir) - + # 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, code_dir=code_dir) + _generate_verilog(model, indices=self.indices) return (model, False) diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py index 023d6a6..02e3297 100644 --- a/tests/transformation/test_logicnets_verilog.py +++ b/tests/transformation/test_logicnets_verilog.py @@ -11,7 +11,6 @@ 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.basic import make_build_dir from finn.util.data_packing import npy_to_rtlsim_input @@ -283,12 +282,8 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit "indices1": indices1_data, } - # Generate directory for storing Verilog files - code_dir = make_build_dir("logicnets_model_") model = model.transform( - GenLogicNetsVerilog( - care_set=care_set_dict, indices=indices_dict, code_dir=code_dir - ) + GenLogicNetsVerilog(care_set=care_set_dict, indices=indices_dict) ) out = oxe.execute_onnx(model, input_dict) @@ -307,6 +302,8 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit 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] From b99707da769827d5de5b14a8c42435fcbb010dc7 Mon Sep 17 00:00:00 2001 From: Mirza Mrahorovic <34712307+mmrahorovic@users.noreply.github.com> Date: Mon, 12 Apr 2021 22:15:22 +0200 Subject: [PATCH 36/85] [create_generic_partitions]: minor modification, removed redundant output value_info entries. (#26) --- .../create_generic_partitions.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) 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" From aea01d6c4f02e3c1d699af6b367faefccd6cf4f8 Mon Sep 17 00:00:00 2001 From: Mirza Mrahorovic <34712307+mmrahorovic@users.noreply.github.com> Date: Mon, 12 Apr 2021 22:25:31 +0200 Subject: [PATCH 37/85] [extend_partition]: added a new transformation ExtendPartition. (#27) [test_extend_partition]: added a test case for the new transformation. --- src/finn/transformation/extend_partition.py | 99 ++++++ tests/transformation/test_extend_partition.py | 322 ++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 src/finn/transformation/extend_partition.py create mode 100644 tests/transformation/test_extend_partition.py 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/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 From b94395bdef6c05c1b7957c11362891c8fe59ac36 Mon Sep 17 00:00:00 2001 From: Mirza Mrahorovic <34712307+mmrahorovic@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:08:20 +0200 Subject: [PATCH 38/85] Added support for non-equal strides along different axes (#25) * [im2col]: added support for non-equal strides along different axes and cleaned up the code. [lower_convs_to_matmul]: added support for non-equal strides along different axes and cleaned up the code. [test_conv_lowering]: added test case for non-equal strides along different axes. * [im2col]: minor fix. [test_im2col]: added test case for non-equal strides along different axes. --- src/finn/custom_op/general/im2col.py | 110 +++--- .../transformation/lower_convs_to_matmul.py | 34 +- tests/custom_op/test_im2col.py | 356 +++++++++++++++--- tests/transformation/test_conv_lowering.py | 10 +- 4 files changed, 388 insertions(+), 122 deletions(-) diff --git a/src/finn/custom_op/general/im2col.py b/src/finn/custom_op/general/im2col.py index 421a1e4..505f8ac 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,21 @@ 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=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) + out_width = compute_conv_output_dim(w, field_width, stride_w, pad_w, dilation) i0 = dilation * np.repeat(np.arange(field_height), field_width) i0 = np.tile(i0, c) - i1 = stride_y * np.repeat(np.arange(out_height), out_width) + i1 = stride_h * 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) + 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,8 +61,8 @@ 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, ): @@ -67,32 +72,15 @@ 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 ) cols = x_padded[:, k, i, j] @@ -122,7 +110,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, ""), @@ -142,6 +130,8 @@ def make_shape_compatible_op(self, model): k_h = k[0] k_w = k[1] stride = self.get_nodeattr("stride") + stride_h = stride[0] + stride_w = stride[1] ishape = self.get_nodeattr("input_shape") dilation = self.get_nodeattr("dilations") pad = self.get_nodeattr( @@ -166,16 +156,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) + ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride_w, pad_w, dilation) # implement tensor with correct shape values = np.random.randn(1, ofm_dim_h, ofm_dim_w, k_h * k_w * ifm_ch).astype( @@ -204,8 +200,9 @@ def execute_node(self, context, graph): k = self.get_nodeattr("kernel_size") # Assumption: Height x Width k_h = k[0] k_w = k[1] - stride = self.get_nodeattr("stride") + stride_h = stride[0] + stride_w = stride[1] pad = self.get_nodeattr("pad_amount") pad_h = pad[0] + pad[2] pad_w = pad[1] + pad[3] @@ -218,29 +215,44 @@ 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) + out_dim_w = compute_conv_output_dim(w, k_w, stride_w, pad_w, dilation) # 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=dilation, ) # 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/transformation/lower_convs_to_matmul.py b/src/finn/transformation/lower_convs_to_matmul.py index 2cddea9..f1da6e2 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) @@ -107,7 +106,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 +119,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 @@ -179,7 +170,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 +212,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 +240,4 @@ def apply(self, model): # remove old nodes graph.node.remove(n) - model = model.transform(InferShapes()) return (model, graph_modified) diff --git a/tests/custom_op/test_im2col.py b/tests/custom_op/test_im2col.py index 26e7eed..092a05c 100644 --- a/tests/custom_op/test_im2col.py +++ b/tests/custom_op/test_im2col.py @@ -27,7 +27,8 @@ def execution_im2col( idt, k_h, k_w, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_h, ifm_dim_w, @@ -37,8 +38,8 @@ def execution_im2col( ): 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) + 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( @@ -53,7 +54,7 @@ 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, @@ -103,14 +104,29 @@ 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 | +# stride_h | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 1 | 2 | 2 | +# stride_w | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 1 | 2 | 2 | # dilations | 1 | 2 | 2 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | +# ------------------------------------------------------------------------------ +# case id | 10 | 11 | +# idt | INT8 | INT8 | +# ifm_dim_H | 5 | 5 | +# ifm_dim_W | 5 | 1 | +# ifm_ch | 2 | 2 | +# pad_amt | 1 | 1 | +# pad_val | 0 | 0 | +# k_H | 2 | 2 | +# k_W | 2 | 1 | +# stride_h | 1 | 2 | +# stride_w | 2 | 1 | +# dilations | 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 @@ -168,7 +184,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -185,7 +202,8 @@ 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 @@ -234,7 +252,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -251,7 +270,8 @@ 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 @@ -320,7 +340,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -337,7 +358,8 @@ 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 @@ -386,7 +408,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -403,7 +426,8 @@ 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 @@ -445,7 +469,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -462,7 +487,8 @@ 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 @@ -485,7 +511,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -502,7 +529,8 @@ 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 @@ -525,7 +553,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -542,7 +571,8 @@ 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 @@ -573,7 +603,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -590,7 +621,8 @@ 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 @@ -613,7 +645,8 @@ def test_im2col_dilations(): idt, k_H, k_W, - stride, + stride_h, + stride_w, ifm_ch, ifm_dim_H, ifm_dim_W, @@ -630,7 +663,8 @@ 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 @@ -653,7 +687,128 @@ 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, + ) + + 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 = 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, + ) + + 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 = 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, @@ -677,7 +832,8 @@ 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 | +# stride_h | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | +# stride_w | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | # dilations | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | def test_im2col(): case_id = 0 @@ -685,7 +841,8 @@ def test_im2col(): 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 +851,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 +919,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 +939,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 +983,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 +1003,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 +1067,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 +1087,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 +1134,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 +1154,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 +1195,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 +1215,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 +1276,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 +1296,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 +1315,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 +1335,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 +1362,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 +1382,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 +1401,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 +1422,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 +1432,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,7 +1450,7 @@ 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, diff --git a/tests/transformation/test_conv_lowering.py b/tests/transformation/test_conv_lowering.py index ef5e133..e0a4443 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 @@ -107,18 +107,20 @@ 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], ) @@ -146,7 +148,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, ) From 94beb27de0decb58d31555823860a24da5f09c5a Mon Sep 17 00:00:00 2001 From: Yaman Umuroglu Date: Mon, 19 Apr 2021 20:28:59 +0100 Subject: [PATCH 39/85] Changes for supporting vitis_hls (#28) * [Refactor] split up RTL/HLS-related utils * [Util] rename to CallHLS and allow specifying vivado_hls/vitis_hls * [Util] more flexible stream naming in rtlsim_multi_io --- src/finn/core/rtlsim_exec.py | 5 +- src/finn/util/fpgadataflow.py | 212 +--------------------------------- src/finn/util/hls.py | 74 ++++++++++++ src/finn/util/pyverilator.py | 179 +++++++++++++++++++++++++++- 4 files changed, 256 insertions(+), 214 deletions(-) create mode 100644 src/finn/util/hls.py 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/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/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 From dd9755626419908a545ae923f4f267f1e88833f1 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 17 Feb 2021 15:57:17 +0000 Subject: [PATCH 40/85] Add TruthTable custom operation --- src/finn/custom_op/general/__init__.py | 2 + src/finn/custom_op/general/truthtable.py | 98 ++++++++++++++++++++++++ tests/custom_op/test_truthtable.py | 85 ++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 src/finn/custom_op/general/truthtable.py create mode 100644 tests/custom_op/test_truthtable.py diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 3bb8bef..32e63d2 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,6 +33,7 @@ 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.general.truthtable import TruthTable custom_op = dict() @@ -43,3 +44,4 @@ custom_op["MultiThreshold"] = MultiThreshold custom_op["XnorPopcountMatMul"] = XnorPopcountMatMul custom_op["Im2Col"] = Im2Col +custom_op["TruthTable"] = TruthTable diff --git a/src/finn/custom_op/general/truthtable.py b/src/finn/custom_op/general/truthtable.py new file mode 100644 index 0000000..7b9ac33 --- /dev/null +++ b/src/finn/custom_op/general/truthtable.py @@ -0,0 +1,98 @@ +import numpy as np +import onnx.helper as helper + +from finn.core.datatype import DataType +from finn.custom_op.base import CustomOp + + +def truthtable(inputs, results): + """Returns the output to a combination of x-bit input value. The results array + reflect the 1 values in the truth table result. If 5 is provided in the result vector, + the result to fifth combination of inputs 101 is 1. The input is a vector size x, representing + x-bits binary input. An example is presented: + + inputs = [1, 0, 1] + results = [1, 2] + + Possible combinations: 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 + + """ + inputs = inputs[::-1] #reverse input array for C style indexing + + in_int = 0 #integer representation of the binary input + + for idx,in_val in enumerate(inputs): + in_int += ((1< Date: Mon, 22 Feb 2021 15:17:29 +0000 Subject: [PATCH 41/85] Move truthtables CustomOp into logicnets folder --- src/finn/custom_op/general/__init__.py | 2 +- .../{general => logicnets}/truthtable.py | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) rename src/finn/custom_op/{general => logicnets}/truthtable.py (73%) diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 32e63d2..777e9c0 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,7 +33,7 @@ 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.general.truthtable import TruthTable +from finn.custom_op.logicnets.truthtable import TruthTable custom_op = dict() diff --git a/src/finn/custom_op/general/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py similarity index 73% rename from src/finn/custom_op/general/truthtable.py rename to src/finn/custom_op/logicnets/truthtable.py index 7b9ac33..9ec78f7 100644 --- a/src/finn/custom_op/general/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,4 +1,3 @@ -import numpy as np import onnx.helper as helper from finn.core.datatype import DataType @@ -7,9 +6,9 @@ def truthtable(inputs, results): """Returns the output to a combination of x-bit input value. The results array - reflect the 1 values in the truth table result. If 5 is provided in the result vector, - the result to fifth combination of inputs 101 is 1. The input is a vector size x, representing - x-bits binary input. An example is presented: + reflect the 1 values in the truth table result. If 5 is provided in the result + vector, the result to fifth combination of inputs 101 is 1. The input is a + vector size x, representing x-bits binary input. An example is presented: inputs = [1, 0, 1] results = [1, 2] @@ -24,54 +23,57 @@ def truthtable(inputs, results): 1 0 1 | 0 1 1 0 | 0 1 1 1 | 0 - + """ - inputs = inputs[::-1] #reverse input array for C style indexing - - in_int = 0 #integer representation of the binary input + inputs = inputs[::-1] # reverse input array for C style indexing + + in_int = 0 # integer representation of the binary input - for idx,in_val in enumerate(inputs): - in_int += ((1< Date: Mon, 22 Feb 2021 15:27:15 +0000 Subject: [PATCH 42/85] Reformat truthtable testing function --- tests/custom_op/test_truthtable.py | 32 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index 7b25fa6..6945e09 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -41,12 +41,16 @@ def test_truthtable(): - inputs = helper.make_tensor_value_info("inputs", TensorProto.FLOAT, [10]) #Input bitwidth 10 - results = helper.make_tensor_value_info("results", TensorProto.FLOAT, [5]) #5 results are 1 among all possible combinations + inputs = helper.make_tensor_value_info( + "inputs", TensorProto.FLOAT, [10] + ) # Input bitwidth 10 + results = helper.make_tensor_value_info( + "results", TensorProto.FLOAT, [5] + ) # 5 results are 1 among all possible combinations output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) node_def = helper.make_node( - "TruthTable", ["inputs", "results"], ["output"], domain = "finn.custom_op.general" + "TruthTable", ["inputs", "results"], ["output"], domain="finn.custom_op.general" ) modelproto = helper.make_model( helper.make_graph([node_def], "test_model", [inputs, results], [output]) @@ -55,31 +59,25 @@ def test_truthtable(): model = ModelWrapper(modelproto) model.set_tensor_datatype("inputs", DataType.BINARY) model.set_tensor_datatype("results", DataType.UINT32) - #test output shape + # test output shape model = model.transform(InferShapes()) assert model.get_tensor_shape("output") == [1] - #test output type + # 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 - #perform execution - input_data = np.asarray([1,0,0,1,1,0,0,0,1,1], dtype=np.float32) - results_data = np.asarray([5,8,14,198,611], dtype=np.float32) + # perform execution + input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) + results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) in_dict = {"inputs": input_data, "results": results_data} out_dict = oxe.execute_onnx(model, in_dict) - #calculate result here for comparison with the custom op + # calculate result here for comparison with the custom op input_data = input_data[::-1] out_idx = 0 for idx, val in enumerate(input_data): - out_idx += ((1< Date: Tue, 23 Feb 2021 16:32:39 +0000 Subject: [PATCH 43/85] Add attributes --- src/finn/custom_op/logicnets/truthtable.py | 26 +++++++++++++++++++--- tests/custom_op/test_truthtable.py | 11 ++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 9ec78f7..5820e0f 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,4 +1,5 @@ -import onnx.helper as helper +import numpy as np +from onnx import TensorProto, helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp @@ -43,12 +44,31 @@ class TruthTable(CustomOp): """The class corresponing to the TruthTable function. """ def get_nodeattr_types(self): - return {} + return { + # The number used for the Don't care entries + "dont_care": ("i", False, 0), + # Number of intput bits, 2 by default + "in_bits": ("i", True, 2), + # Code generation mode + "code_mode": ("s", False, "Verilog"), + } def make_shape_compatible_op(self, model): node = self.onnx_node + iname = node.input[0] + ishape = model.get_tensor_shape(iname) + input_bits = self.get_nodeattr("in_bits") + assert input_bits == ishape[0] return helper.make_node( - "TruthTable", [node.input[0], node.input[1]], [node.output[0]] + "Constant", + inputs=[], + outputs=[self.onnx_node.output[0]], + value=helper.make_tensor( + name="const_tensor", + data_type=TensorProto.BINARY, + dims=1, + vals=np.random.randint(2), + ), ) def infer_node_datatype(self, model): diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index 6945e09..c3f9c66 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -41,6 +41,9 @@ def test_truthtable(): + input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) + results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) + inputs = helper.make_tensor_value_info( "inputs", TensorProto.FLOAT, [10] ) # Input bitwidth 10 @@ -50,7 +53,11 @@ def test_truthtable(): output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) node_def = helper.make_node( - "TruthTable", ["inputs", "results"], ["output"], domain="finn.custom_op.general" + "TruthTable", + ["inputs", "results"], + ["output"], + domain="finn.custom_op.general", + in_bits=input_data.size, ) modelproto = helper.make_model( helper.make_graph([node_def], "test_model", [inputs, results], [output]) @@ -67,8 +74,6 @@ def test_truthtable(): model = model.transform(InferDataTypes()) assert model.get_tensor_datatype("output") is DataType.BINARY # perform execution - input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) - results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) in_dict = {"inputs": input_data, "results": results_data} out_dict = oxe.execute_onnx(model, in_dict) From 06b0364823f7d992956019c9c75c03c0bc6712d4 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 23 Feb 2021 17:25:46 +0000 Subject: [PATCH 44/85] Fix shape_compatibility error --- src/finn/custom_op/logicnets/truthtable.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 5820e0f..bd67c0f 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,5 +1,4 @@ -import numpy as np -from onnx import TensorProto, helper +from onnx import helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp @@ -60,15 +59,7 @@ def make_shape_compatible_op(self, model): input_bits = self.get_nodeattr("in_bits") assert input_bits == ishape[0] return helper.make_node( - "Constant", - inputs=[], - outputs=[self.onnx_node.output[0]], - value=helper.make_tensor( - name="const_tensor", - data_type=TensorProto.BINARY, - dims=1, - vals=np.random.randint(2), - ), + "TruthTable", [node.input[0], node.input[1]], [node.output[0]] ) def infer_node_datatype(self, model): From 5ec25823179e2f086bd6aed43cae16757ced50a2 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 26 Feb 2021 20:08:44 +0000 Subject: [PATCH 45/85] Adding zero entries in the custom operations, attributes, and a transformation to generate a Verilog truth table based on the table entries. --- src/finn/custom_op/logicnets/truthtable.py | 57 ++++++++----- src/finn/transformation/gen_verilog_truth.py | 85 ++++++++++++++++++++ tests/custom_op/test_truthtable.py | 52 +++++++++--- 3 files changed, 162 insertions(+), 32 deletions(-) create mode 100644 src/finn/transformation/gen_verilog_truth.py diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index bd67c0f..44ed2f7 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,17 +1,21 @@ +import numpy as np from onnx import helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp -def truthtable(inputs, results): - """Returns the output to a combination of x-bit input value. The results array - reflect the 1 values in the truth table result. If 5 is provided in the result - vector, the result to fifth combination of inputs 101 is 1. The input is a - vector size x, representing x-bits binary input. An example is presented: +def truthtable(inputs, result_one, result_zero, node): + """Returns the output to a combination of x-bit input value. The result_one array + reflect the 1 values in the truth table results. The result_zero vector represents + the 0 values in the truth table results. The rest of the results are imcomplete + table entries. If 5 is provided in the result vector, the result to fifth + combination of inputs 101 is 1. The input is a vector size x, representing + x-bits binary input. An example is presented: inputs = [1, 0, 1] - results = [1, 2] + result_one = [1, 2] + result_zero = [0, 3, 5] Possible combinations: A B C | Results ------------------- @@ -19,22 +23,29 @@ def truthtable(inputs, results): 0 0 1 | 1 0 1 0 | 1 0 1 1 | 0 - 1 0 0 | 0 + 1 0 0 | X 1 0 1 | 0 - 1 1 0 | 0 - 1 1 1 | 0 + 1 1 0 | X + 1 1 1 | X """ + # check if any of the values overlaps + assert np.any(np.in1d(result_one, result_zero)) == 0 + inputs = inputs[::-1] # reverse input array for C style indexing in_int = 0 # integer representation of the binary input + dont_care = node.get_nodeattr("dont_care") # get the dont care value + for idx, in_val in enumerate(inputs): in_int += (1 << idx) * in_val # calculate integer value of binary input output = ( - 1 if in_int in results else 0 - ) # return 1 if the result entry for that value is 1 + 1 if in_int in result_one else (0 if in_int in result_zero else dont_care) + ) # return 1 if the input is in result_one + # return 0 if the input is in result_zero + # return dont_care if the input is incomplete return output @@ -50,14 +61,16 @@ def get_nodeattr_types(self): "in_bits": ("i", True, 2), # Code generation mode "code_mode": ("s", False, "Verilog"), + # Output code directory + "code_dir": ("s", False, ""), } def make_shape_compatible_op(self, model): node = self.onnx_node - iname = node.input[0] - ishape = model.get_tensor_shape(iname) - input_bits = self.get_nodeattr("in_bits") - assert input_bits == ishape[0] + # iname = node.input[0] + # ishape = model.get_tensor_shape(iname) + # input_bits = self.get_nodeattr("in_bits") + # assert input_bits == ishape[0] return helper.make_node( "TruthTable", [node.input[0], node.input[1]], [node.output[0]] ) @@ -68,19 +81,25 @@ def infer_node_datatype(self, model): assert ( model.get_tensor_datatype(node.input[0]) == DataType["BINARY"] ), """ The input vector DataType is not BINARY.""" - # check that the input[0] is UINT32 + # check that the input[1] is UINT32 assert ( model.get_tensor_datatype(node.input[1]) == DataType["UINT32"] ), """ The input vector DataType is not UINT32.""" - model.set_tensor_datatype(node.output[0], DataType["BINARY"]) + # 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 output to UINT32 + model.set_tensor_datatype(node.output[0], DataType["UINT32"]) def execute_node(self, context, graph): node = self.onnx_node # load inputs input_entry = context[node.input[0]] - results = context[node.input[1]] + result_one = context[node.input[1]] + result_zero = context[node.input[2]] # calculate output - output = truthtable(input_entry, results) + output = truthtable(input_entry, result_one, result_zero, self) # store output context[node.output[0]] = output diff --git a/src/finn/transformation/gen_verilog_truth.py b/src/finn/transformation/gen_verilog_truth.py new file mode 100644 index 0000000..fb83ae6 --- /dev/null +++ b/src/finn/transformation/gen_verilog_truth.py @@ -0,0 +1,85 @@ +# 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 _generate_verilog(myOp, result_one, result_zero): + input_bits = myOp.get_nodeattr("in_bits") + dont_care_entry = myOp.get_nodeattr("dont_care") + # the module name is kept constant to "inconsistent_table" + # the input name is kept constant to "in" + # the output is kept constant to "result" + verilog_string = "module inconsistent_table (\n" + 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 result_one: + val = int(val) + verilog_string += "\t\t\t%d'b" % (input_bits) + verilog_string += bin(val)[2:].zfill(input_bits) + print(val) + verilog_string += " : result = 1'b1;\n" + # fill the zero entries + for val in result_zero: + val = int(val) + verilog_string += "\t\t\t%d'b" % (input_bits) + verilog_string += bin(val)[2:].zfill(input_bits) + verilog_string += " : result = 1'b0;\n" + # fill the default case for dont_care or inconsistent entries + verilog_string += "\t\t\tdefault: result = 1'b%d;\n" % (dont_care_entry) + # close the module + verilog_string += "\t\tendcase\n\tend\nendmodule\n" + # open file, write string and close file + verilog_file = open("my_truthtable.v", "w") + verilog_file.write(verilog_string) + verilog_file.close() + + +class GenVerilogTruthTable(NodeLocalTransformation): + """Generate a Verilog file for every node in the Graph using the + TruthTable custom operation""" + + def __init__(self, num_workers, result_one, result_zero): + super().__init__(num_workers=num_workers) + self.result_one = result_one + self.result_zero = result_zero + + def applyNodeLocal(self, node): + op_type = node.op_type + if op_type == "TruthTable": + myOp = registry.getCustomOp(node) + print(self.result_one) + _generate_verilog(myOp, self.result_one, self.result_zero) + + return (node, False) diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index c3f9c66..41fac44 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -33,6 +33,7 @@ import finn.core.onnx_exec as oxe from finn.core.datatype import DataType from finn.core.modelwrapper import ModelWrapper +from finn.transformation.gen_verilog_truth import GenVerilogTruthTable from finn.transformation.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes @@ -42,39 +43,54 @@ def test_truthtable(): input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) - results_data = np.asarray([5, 8, 14, 198, 611], dtype=np.float32) + result_one_data = np.asarray([58, 15, 89, 695, 6485], dtype=np.float32) + result_zero_data = np.asarray([52, 65, 1908, 6101], dtype=np.float32) + dont_care = 0 + in_bits = 16 inputs = helper.make_tensor_value_info( - "inputs", TensorProto.FLOAT, [10] - ) # Input bitwidth 10 - results = helper.make_tensor_value_info( - "results", TensorProto.FLOAT, [5] - ) # 5 results are 1 among all possible combinations + "inputs", TensorProto.FLOAT, [input_data.size] + ) + result_one = helper.make_tensor_value_info( + "result_one", TensorProto.FLOAT, [result_one_data.size] + ) + result_zero = helper.make_tensor_value_info( + "result_zero", TensorProto.FLOAT, [result_zero_data.size] + ) output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) node_def = helper.make_node( "TruthTable", - ["inputs", "results"], + ["inputs", "result_one", "result_zero"], ["output"], domain="finn.custom_op.general", - in_bits=input_data.size, + in_bits=in_bits, + dont_care=dont_care, ) modelproto = helper.make_model( - helper.make_graph([node_def], "test_model", [inputs, results], [output]) + helper.make_graph( + [node_def], "test_model", [inputs, result_one, result_zero], [output] + ) ) model = ModelWrapper(modelproto) model.set_tensor_datatype("inputs", DataType.BINARY) - model.set_tensor_datatype("results", DataType.UINT32) + model.set_tensor_datatype("result_one", DataType.UINT32) + model.set_tensor_datatype("result_zero", 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 + assert model.get_tensor_datatype("output") is DataType.UINT32 # perform execution - in_dict = {"inputs": input_data, "results": results_data} + in_dict = { + "inputs": input_data, + "result_one": result_one_data, + "result_zero": result_zero_data, + } out_dict = oxe.execute_onnx(model, in_dict) # calculate result here for comparison with the custom op @@ -82,7 +98,17 @@ def test_truthtable(): out_idx = 0 for idx, val in enumerate(input_data): out_idx += (1 << idx) * val - entry = 1 if out_idx in results_data else 0 + entry = ( + 1 + if out_idx in result_one_data + else (0 if out_idx in result_zero_data else dont_care) + ) # compare outputs assert entry == out_dict["output"] + # test transformation to generate verilog + model = model.transform( + GenVerilogTruthTable( + num_workers=None, result_one=result_one_data, result_zero=result_zero_data + ) + ) From 670b4609c594988a632f4f94a2fd6c6c22de509c Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 2 Mar 2021 17:21:51 +0000 Subject: [PATCH 46/85] There are a bunch of changes in this commit: - Remove 0 values in table entries - All the specified entries are 1 and named "care_set" - Remove attribute for dont_care symbol - Everything is named to represent the custom binary truth table operation - Change the way the transformation is performed. - The transformation itself checks if the node is "BinaryTruthTable" type customOp - The transformation calls a helper function inside the customOp class that generates the Verilog - The care_set tensor is given as an attribute to the transformation --- my_truthtable.v | 17 ++++ src/finn/custom_op/general/__init__.py | 4 +- src/finn/custom_op/logicnets/truthtable.py | 93 +++++++++++-------- .../gen_bintruthtable_verilog.py} | 55 +++-------- tests/custom_op/test_truthtable.py | 42 +++------ 5 files changed, 103 insertions(+), 108 deletions(-) create mode 100644 my_truthtable.v rename src/finn/transformation/{gen_verilog_truth.py => logicnets/gen_bintruthtable_verilog.py} (50%) diff --git a/my_truthtable.v b/my_truthtable.v new file mode 100644 index 0000000..be57489 --- /dev/null +++ b/my_truthtable.v @@ -0,0 +1,17 @@ +module incomplete_table ( + input [15:0] in, + output reg result +); + + always @(in) begin + case(in) + 16'b0000000000000001 : result = 1'b1; + 16'b0000000000111010 : result = 1'b1; + 16'b0000000000001111 : result = 1'b1; + 16'b0000000001011001 : result = 1'b1; + 16'b0000001010110111 : result = 1'b1; + 16'b0001100101010101 : result = 1'b1; + default: result = 1'b0; + endcase + end +endmodule diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index 777e9c0..cd1be34 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,7 +33,7 @@ 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.truthtable import TruthTable +from finn.custom_op.logicnets.truthtable import BinaryTruthTable custom_op = dict() @@ -44,4 +44,4 @@ custom_op["MultiThreshold"] = MultiThreshold custom_op["XnorPopcountMatMul"] = XnorPopcountMatMul custom_op["Im2Col"] = Im2Col -custom_op["TruthTable"] = TruthTable +custom_op["BinaryTruthTable"] = BinaryTruthTable diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 44ed2f7..b75d756 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -1,21 +1,20 @@ import numpy as np +import onnx from onnx import helper from finn.core.datatype import DataType from finn.custom_op.base import CustomOp -def truthtable(inputs, result_one, result_zero, node): - """Returns the output to a combination of x-bit input value. The result_one array - reflect the 1 values in the truth table results. The result_zero vector represents - the 0 values in the truth table results. The rest of the results are imcomplete - table entries. If 5 is provided in the result vector, the result to fifth +def truthtable_binary(inputs, care_set, node): + """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 5 is provided in the care-set, the result to fifth combination of inputs 101 is 1. The input is a vector size x, representing x-bits binary input. An example is presented: inputs = [1, 0, 1] - result_one = [1, 2] - result_zero = [0, 3, 5] + care_set = [1, 2] Possible combinations: A B C | Results ------------------- @@ -23,40 +22,30 @@ def truthtable(inputs, result_one, result_zero, node): 0 0 1 | 1 0 1 0 | 1 0 1 1 | 0 - 1 0 0 | X + 1 0 0 | 0 1 0 1 | 0 - 1 1 0 | X - 1 1 1 | X + 1 1 0 | 0 + 1 1 1 | 0 """ - # check if any of the values overlaps - assert np.any(np.in1d(result_one, result_zero)) == 0 inputs = inputs[::-1] # reverse input array for C style indexing - in_int = 0 # integer representation of the binary input - - dont_care = node.get_nodeattr("dont_care") # get the dont care value + in_int = 0 # initialize integer representation of the binary input array for idx, in_val in enumerate(inputs): in_int += (1 << idx) * in_val # calculate integer value of binary input - output = ( - 1 if in_int in result_one else (0 if in_int in result_zero else dont_care) - ) # return 1 if the input is in result_one - # return 0 if the input is in result_zero - # return dont_care if the input is incomplete + output = 1 if in_int in care_set else 0 # return 1 if the input is in result_one return output -class TruthTable(CustomOp): +class BinaryTruthTable(CustomOp): """The class corresponing to the TruthTable function. """ def get_nodeattr_types(self): return { - # The number used for the Don't care entries - "dont_care": ("i", False, 0), # Number of intput bits, 2 by default "in_bits": ("i", True, 2), # Code generation mode @@ -67,13 +56,19 @@ def get_nodeattr_types(self): def make_shape_compatible_op(self, model): node = self.onnx_node - # iname = node.input[0] - # ishape = model.get_tensor_shape(iname) - # input_bits = self.get_nodeattr("in_bits") - # assert input_bits == ishape[0] - return helper.make_node( - "TruthTable", [node.input[0], node.input[1]], [node.output[0]] + val = np.random.randn(1).astype(np.bool) + node = helper.make_node( + "Constant", + inputs=[], + outputs=["val"], + value=helper.make_tensor( + name="const_tensor", + data_type=onnx.TensorProto.BOOL, + dims=val.shape, + vals=val.flatten().astype(bool), + ), ) + return node def infer_node_datatype(self, model): node = self.onnx_node @@ -86,20 +81,15 @@ def infer_node_datatype(self, model): 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 output to UINT32 - model.set_tensor_datatype(node.output[0], DataType["UINT32"]) + 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]] - result_one = context[node.input[1]] - result_zero = context[node.input[2]] + care_set = context[node.input[1]] # calculate output - output = truthtable(input_entry, result_one, result_zero, self) + output = truthtable_binary(input_entry, care_set, self) # store output context[node.output[0]] = output @@ -128,3 +118,32 @@ def verify_node(self): # taken from "xnorpopcount.py" info_messages.append("TruthTable needs 2 data inputs") return info_messages + + def generate_verilog(self, care_set): + + input_bits = self.get_nodeattr("in_bits") + # 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 incomplete_table (\n" + 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" + # open file, write string and close file + verilog_file = open("my_truthtable.v", "w") + verilog_file.write(verilog_string) + verilog_file.close() diff --git a/src/finn/transformation/gen_verilog_truth.py b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py similarity index 50% rename from src/finn/transformation/gen_verilog_truth.py rename to src/finn/transformation/logicnets/gen_bintruthtable_verilog.py index fb83ae6..d60bb6f 100644 --- a/src/finn/transformation/gen_verilog_truth.py +++ b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py @@ -30,56 +30,29 @@ from finn.transformation.base import NodeLocalTransformation -def _generate_verilog(myOp, result_one, result_zero): - input_bits = myOp.get_nodeattr("in_bits") - dont_care_entry = myOp.get_nodeattr("dont_care") - # the module name is kept constant to "inconsistent_table" - # the input name is kept constant to "in" - # the output is kept constant to "result" - verilog_string = "module inconsistent_table (\n" - 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" +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) - # fill the one entries - for val in result_one: - val = int(val) - verilog_string += "\t\t\t%d'b" % (input_bits) - verilog_string += bin(val)[2:].zfill(input_bits) - print(val) - verilog_string += " : result = 1'b1;\n" - # fill the zero entries - for val in result_zero: - val = int(val) - verilog_string += "\t\t\t%d'b" % (input_bits) - verilog_string += bin(val)[2:].zfill(input_bits) - verilog_string += " : result = 1'b0;\n" - # fill the default case for dont_care or inconsistent entries - verilog_string += "\t\t\tdefault: result = 1'b%d;\n" % (dont_care_entry) - # close the module - verilog_string += "\t\tendcase\n\tend\nendmodule\n" - # open file, write string and close file - verilog_file = open("my_truthtable.v", "w") - verilog_file.write(verilog_string) - verilog_file.close() + except KeyError: + # exception if op_type is not supported + raise Exception("Custom op_type %s is currently not supported." % op_type) -class GenVerilogTruthTable(NodeLocalTransformation): +class GenBinaryTruthTableVerilog(NodeLocalTransformation): """Generate a Verilog file for every node in the Graph using the TruthTable custom operation""" - def __init__(self, num_workers, result_one, result_zero): + def __init__(self, num_workers, care_set): super().__init__(num_workers=num_workers) - self.result_one = result_one - self.result_zero = result_zero + self.care_set = care_set def applyNodeLocal(self, node): op_type = node.op_type - if op_type == "TruthTable": - myOp = registry.getCustomOp(node) - print(self.result_one) - _generate_verilog(myOp, self.result_one, self.result_zero) + if op_type == "BinaryTruthTable": + _genbintruthtable_verilog(node, self.care_set) return (node, False) diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index 41fac44..04745e4 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -33,9 +33,11 @@ import finn.core.onnx_exec as oxe from finn.core.datatype import DataType from finn.core.modelwrapper import ModelWrapper -from finn.transformation.gen_verilog_truth import GenVerilogTruthTable from finn.transformation.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes +from finn.transformation.logicnets.gen_bintruthtable_verilog import ( + GenBinaryTruthTableVerilog, +) export_onnx_path = "test_truthtable.onnx" @@ -43,40 +45,31 @@ def test_truthtable(): input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) - result_one_data = np.asarray([58, 15, 89, 695, 6485], dtype=np.float32) - result_zero_data = np.asarray([52, 65, 1908, 6101], dtype=np.float32) - dont_care = 0 + care_set_data = np.asarray([1, 58, 15, 89, 695, 6485], dtype=np.float32) in_bits = 16 inputs = helper.make_tensor_value_info( "inputs", TensorProto.FLOAT, [input_data.size] ) - result_one = helper.make_tensor_value_info( - "result_one", TensorProto.FLOAT, [result_one_data.size] - ) - result_zero = helper.make_tensor_value_info( - "result_zero", TensorProto.FLOAT, [result_zero_data.size] + 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]) node_def = helper.make_node( - "TruthTable", - ["inputs", "result_one", "result_zero"], + "BinaryTruthTable", + ["inputs", "care_set"], ["output"], domain="finn.custom_op.general", in_bits=in_bits, - dont_care=dont_care, ) modelproto = helper.make_model( - helper.make_graph( - [node_def], "test_model", [inputs, result_one, result_zero], [output] - ) + helper.make_graph([node_def], "test_model", [inputs, care_set], [output]) ) model = ModelWrapper(modelproto) model.set_tensor_datatype("inputs", DataType.BINARY) - model.set_tensor_datatype("result_one", DataType.UINT32) - model.set_tensor_datatype("result_zero", DataType.UINT32) + model.set_tensor_datatype("care_set", DataType.UINT32) # test output shape model = model.transform(InferShapes()) @@ -84,12 +77,11 @@ def test_truthtable(): # test output type assert model.get_tensor_datatype("output") is DataType.FLOAT32 model = model.transform(InferDataTypes()) - assert model.get_tensor_datatype("output") is DataType.UINT32 + assert model.get_tensor_datatype("output") is DataType.BINARY # perform execution in_dict = { "inputs": input_data, - "result_one": result_one_data, - "result_zero": result_zero_data, + "care_set": care_set_data, } out_dict = oxe.execute_onnx(model, in_dict) @@ -98,17 +90,11 @@ def test_truthtable(): out_idx = 0 for idx, val in enumerate(input_data): out_idx += (1 << idx) * val - entry = ( - 1 - if out_idx in result_one_data - else (0 if out_idx in result_zero_data else dont_care) - ) + entry = 1 if out_idx in care_set_data else 0 # compare outputs assert entry == out_dict["output"] # test transformation to generate verilog model = model.transform( - GenVerilogTruthTable( - num_workers=None, result_one=result_one_data, result_zero=result_zero_data - ) + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) ) From cc6843893c0c9d7f018c94fb1e488ad28d31d5f6 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 3 Mar 2021 11:56:23 +0000 Subject: [PATCH 47/85] Modify function and file names to keed consistency --- src/finn/custom_op/general/__init__.py | 2 +- .../logicnets/{truthtable.py => binary_truthtable.py} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/finn/custom_op/logicnets/{truthtable.py => binary_truthtable.py} (97%) diff --git a/src/finn/custom_op/general/__init__.py b/src/finn/custom_op/general/__init__.py index cd1be34..50f1121 100644 --- a/src/finn/custom_op/general/__init__.py +++ b/src/finn/custom_op/general/__init__.py @@ -33,7 +33,7 @@ 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.truthtable import BinaryTruthTable +from finn.custom_op.logicnets.binary_truthtable import BinaryTruthTable custom_op = dict() diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py similarity index 97% rename from src/finn/custom_op/logicnets/truthtable.py rename to src/finn/custom_op/logicnets/binary_truthtable.py index b75d756..e63c94d 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -6,7 +6,7 @@ from finn.custom_op.base import CustomOp -def truthtable_binary(inputs, care_set, node): +def binary_truthtable(inputs, care_set, node): """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 5 is provided in the care-set, the result to fifth @@ -89,7 +89,7 @@ def execute_node(self, context, graph): input_entry = context[node.input[0]] care_set = context[node.input[1]] # calculate output - output = truthtable_binary(input_entry, care_set, self) + output = binary_truthtable(input_entry, care_set, self) # store output context[node.output[0]] = output From 1ac978e11f0c4b4f776a15f201464425fb692e23 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 3 Mar 2021 15:11:45 +0000 Subject: [PATCH 48/85] Add Copyright (c) 2021 Xilinx, Inc. --- .../custom_op/logicnets/binary_truthtable.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index e63c94d..18e5789 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -1,3 +1,31 @@ +# 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 from onnx import helper From 5294bda6c312ea420023398f323a8cf03fdb762a Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 3 Mar 2021 16:12:40 +0000 Subject: [PATCH 49/85] Add custom directory entry for the verilog file --- my_truthtable.v | 17 ----------------- .../custom_op/logicnets/binary_truthtable.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 19 deletions(-) delete mode 100644 my_truthtable.v diff --git a/my_truthtable.v b/my_truthtable.v deleted file mode 100644 index be57489..0000000 --- a/my_truthtable.v +++ /dev/null @@ -1,17 +0,0 @@ -module incomplete_table ( - input [15:0] in, - output reg result -); - - always @(in) begin - case(in) - 16'b0000000000000001 : result = 1'b1; - 16'b0000000000111010 : result = 1'b1; - 16'b0000000000001111 : result = 1'b1; - 16'b0000000001011001 : result = 1'b1; - 16'b0000001010110111 : result = 1'b1; - 16'b0001100101010101 : result = 1'b1; - default: result = 1'b0; - endcase - end -endmodule diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 18e5789..f9c1c5b 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -28,6 +28,7 @@ import numpy as np import onnx +import os from onnx import helper from finn.core.datatype import DataType @@ -79,7 +80,7 @@ def get_nodeattr_types(self): # Code generation mode "code_mode": ("s", False, "Verilog"), # Output code directory - "code_dir": ("s", False, ""), + "code_dir": ("s", False, "src/finn/data/verilog/truthtable/"), } def make_shape_compatible_op(self, model): @@ -172,6 +173,12 @@ def generate_verilog(self, care_set): # close the module verilog_string += "\t\tendcase\n\tend\nendmodule\n" # open file, write string and close file - verilog_file = open("my_truthtable.v", "w") + + dir = self.get_nodeattr("code_dir") + + if not os.path.exists(dir): + os.makedirs(dir) + + verilog_file = open(dir + "binary_truthtable.v", "w") verilog_file.write(verilog_string) verilog_file.close() From 003e227c7527b87910413a07888b81c1c59b5c4d Mon Sep 17 00:00:00 2001 From: jalezeta Date: Thu, 4 Mar 2021 14:58:56 +0000 Subject: [PATCH 50/85] Add PyVerilator test for the generated binary truth table verilog files --- .../custom_op/logicnets/binary_truthtable.py | 2 +- .../verilog/truthtable/incomplete_table.v | 17 +++++ .../verilog/truthtable/wrapper_truthtable.v | 20 ++++++ tests/util/test_pyverilog_binarytruthtable.py | 70 +++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/finn/data/verilog/truthtable/incomplete_table.v create mode 100644 src/finn/data/verilog/truthtable/wrapper_truthtable.v create mode 100644 tests/util/test_pyverilog_binarytruthtable.py diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index f9c1c5b..9fb288f 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -179,6 +179,6 @@ def generate_verilog(self, care_set): if not os.path.exists(dir): os.makedirs(dir) - verilog_file = open(dir + "binary_truthtable.v", "w") + verilog_file = open(dir + "incomplete_table.v", "w") verilog_file.write(verilog_string) verilog_file.close() diff --git a/src/finn/data/verilog/truthtable/incomplete_table.v b/src/finn/data/verilog/truthtable/incomplete_table.v new file mode 100644 index 0000000..be57489 --- /dev/null +++ b/src/finn/data/verilog/truthtable/incomplete_table.v @@ -0,0 +1,17 @@ +module incomplete_table ( + input [15:0] in, + output reg result +); + + always @(in) begin + case(in) + 16'b0000000000000001 : result = 1'b1; + 16'b0000000000111010 : result = 1'b1; + 16'b0000000000001111 : result = 1'b1; + 16'b0000000001011001 : result = 1'b1; + 16'b0000001010110111 : result = 1'b1; + 16'b0001100101010101 : result = 1'b1; + default: result = 1'b0; + endcase + end +endmodule diff --git a/src/finn/data/verilog/truthtable/wrapper_truthtable.v b/src/finn/data/verilog/truthtable/wrapper_truthtable.v new file mode 100644 index 0000000..bef7555 --- /dev/null +++ b/src/finn/data/verilog/truthtable/wrapper_truthtable.v @@ -0,0 +1,20 @@ + +`timescale 1 ns / 1 ps + +(* CORE_GENERATION_INFO="wrapper_truthtable,hls_ip_2019_1,{HLS_INPUT_TYPE=cxx,HLS_INPUT_FLOAT=0,HLS_INPUT_FIXED=1,HLS_INPUT_PART=xc7z020-clg400-1,HLS_INPUT_CLOCK=5.000000,HLS_INPUT_ARCH=others,HLS_SYN_CLOCK=3.552000,HLS_SYN_LAT=0,HLS_SYN_TPT=none,HLS_SYN_MEM=0,HLS_SYN_DSP=0,HLS_SYN_FF=144,HLS_SYN_LUT=271,HLS_VERSION=2019_1}" *) + + +module wrapper_truthtable ( + input_data, + result_data +); + +input [15:0]input_data; + output result_data; + +incomplete_table my_incomplete_table( + .in(input_data), + .result(result_data) +); + +endmodule //wrapper_truthtable diff --git a/tests/util/test_pyverilog_binarytruthtable.py b/tests/util/test_pyverilog_binarytruthtable.py new file mode 100644 index 0000000..e9505b6 --- /dev/null +++ b/tests/util/test_pyverilog_binarytruthtable.py @@ -0,0 +1,70 @@ +# Copyright (c) 2021, Xilinx +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of FINN nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import numpy as np +from pyverilator import PyVerilator + +from finn.core.datatype import DataType +from finn.util.data_packing import npy_to_rtlsim_input + + +def test_pyverilator_binarytruthtable(): + # file_root = pk.resource_filename("finn.data","verilog/myadd") + + sim = PyVerilator.build( + "/workspace/finn-base/src/finn/data/verilog/truthtable/wrapper_truthtable.v", + top_module_name="wrapper_truthtable", + ) + + expected_ports = [ + "input_data", + "result_data", + ] + + for port in expected_ports: + assert port in sim.io + + sim.io["input_data"] = 0 + + array = 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, 0], + [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], + ] + ) + + value = npy_to_rtlsim_input(array, DataType.BINARY, 16, False) + + for val in value: + sim.io["input_data"] = val + result = sim.io["result_data"] + assert result == 1 From 3d1af47f1185d93cadc28f9367a12b008cedd8f4 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 5 Mar 2021 10:18:06 +0000 Subject: [PATCH 51/85] Add new execution mode: python or rtlsim --- .../custom_op/logicnets/binary_truthtable.py | 50 ++++++++++--- ...truthtable.py => test_binarytruthtable.py} | 65 +++++++++++------ tests/util/test_pyverilog_binarytruthtable.py | 70 ------------------- 3 files changed, 84 insertions(+), 101 deletions(-) rename tests/custom_op/{test_truthtable.py => test_binarytruthtable.py} (63%) delete mode 100644 tests/util/test_pyverilog_binarytruthtable.py diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 9fb288f..8e7bc39 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -30,9 +30,11 @@ import onnx import os from onnx import helper +from pyverilator import PyVerilator from finn.core.datatype import DataType from finn.custom_op.base import CustomOp +from finn.util.data_packing import npy_to_rtlsim_input def binary_truthtable(inputs, care_set, node): @@ -80,7 +82,13 @@ def get_nodeattr_types(self): # Code generation mode "code_mode": ("s", False, "Verilog"), # Output code directory - "code_dir": ("s", False, "src/finn/data/verilog/truthtable/"), + "code_dir": ( + "s", + False, + "/workspace/finn-base/src/finn/data/verilog/truthtable/", + ), + # Execution mode, "pyhton" by default + "exec_mode": ("s", True, "python"), } def make_shape_compatible_op(self, model): @@ -101,28 +109,52 @@ def make_shape_compatible_op(self, model): def infer_node_datatype(self, model): node = self.onnx_node - # check that the input[0] is binary + # 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 + # 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 + # Check that the input[2] is UINT32 model.set_tensor_datatype(node.output[0], DataType["BINARY"]) def execute_node(self, context, graph): node = self.onnx_node - # load inputs + # Load inputs input_entry = context[node.input[0]] care_set = context[node.input[1]] - # calculate output - output = binary_truthtable(input_entry, care_set, self) - # store output + # 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) + elif mode == "rtlsim": + # Generate PyVerilator object if Verilog file exits, + # otherwise generate Verilog and proceed + verilog_dir = self.get_nodeattr("code_dir") + "incomplete_table.v" + if not os.path.exists(verilog_dir): + self.generate_verilog(care_set) + 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]] = output - def verify_node(self): # taken from "xnorpopcount.py" + def verify_node(self): info_messages = [] # verify number of attributes diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_binarytruthtable.py similarity index 63% rename from tests/custom_op/test_truthtable.py rename to tests/custom_op/test_binarytruthtable.py index 04745e4..8f98174 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -33,6 +33,7 @@ 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.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes from finn.transformation.logicnets.gen_bintruthtable_verilog import ( @@ -42,31 +43,44 @@ export_onnx_path = "test_truthtable.onnx" -def test_truthtable(): +def test_binarytruthtable(): - input_data = np.asarray([1, 0, 0, 1, 1, 0, 0, 0, 1, 1], dtype=np.float32) + # 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], dtype=np.float32) in_bits = 16 - inputs = helper.make_tensor_value_info( - "inputs", TensorProto.FLOAT, [input_data.size] - ) + # Set input and care_set 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) @@ -78,22 +92,29 @@ def test_truthtable(): assert model.get_tensor_datatype("output") is DataType.FLOAT32 model = model.transform(InferDataTypes()) assert model.get_tensor_datatype("output") is DataType.BINARY - # perform execution - in_dict = { - "inputs": input_data, - "care_set": care_set_data, - } - out_dict = oxe.execute_onnx(model, in_dict) - - # calculate result here for comparison with the custom op - input_data = input_data[::-1] - out_idx = 0 - for idx, val in enumerate(input_data): - out_idx += (1 << idx) * val - entry = 1 if out_idx in care_set_data else 0 + # Loop over "python" and "rtlsim" execution modes + for x 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) + # Calculate result here locally for comparison with the CustomOp result + input_data = input_data[::-1] + out_idx = 0 + for idx, val in enumerate(input_data): + out_idx += (1 << idx) * val + entry = 1 if out_idx in care_set_data else 0 + # compare outputs + assert entry == out_dict["output"] + # Change execution mode into "rtlsim" for simulation with PyVerilator + myOp = getCustomOp(model.graph.node[0]) + myOp.set_nodeattr("exec_mode", "rtlsim") - # compare outputs - assert entry == out_dict["output"] # test transformation to generate verilog model = model.transform( GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) diff --git a/tests/util/test_pyverilog_binarytruthtable.py b/tests/util/test_pyverilog_binarytruthtable.py deleted file mode 100644 index e9505b6..0000000 --- a/tests/util/test_pyverilog_binarytruthtable.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2021, Xilinx -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of FINN nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import numpy as np -from pyverilator import PyVerilator - -from finn.core.datatype import DataType -from finn.util.data_packing import npy_to_rtlsim_input - - -def test_pyverilator_binarytruthtable(): - # file_root = pk.resource_filename("finn.data","verilog/myadd") - - sim = PyVerilator.build( - "/workspace/finn-base/src/finn/data/verilog/truthtable/wrapper_truthtable.v", - top_module_name="wrapper_truthtable", - ) - - expected_ports = [ - "input_data", - "result_data", - ] - - for port in expected_ports: - assert port in sim.io - - sim.io["input_data"] = 0 - - array = 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, 0], - [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], - ] - ) - - value = npy_to_rtlsim_input(array, DataType.BINARY, 16, False) - - for val in value: - sim.io["input_data"] = val - result = sim.io["result_data"] - assert result == 1 From 2dedb60aa753a24ddd5f24d189e7b832c7c322b1 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 5 Mar 2021 14:32:38 +0000 Subject: [PATCH 52/85] Add random LUT generator utility function. --- src/finn/util/logicnets/logicnets.py | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/finn/util/logicnets/logicnets.py diff --git a/src/finn/util/logicnets/logicnets.py b/src/finn/util/logicnets/logicnets.py new file mode 100644 index 0000000..637812b --- /dev/null +++ b/src/finn/util/logicnets/logicnets.py @@ -0,0 +1,95 @@ +# 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 os +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 + + +def gen_verilog(n_bits, care_set, dir): + """The function generated a verilog module based on the n_bit input values and the + care_set. The result for every value in the care_set is 1. The module creates a + LUT based on a n_bits:1 mapping""" + + # 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 incomplete_table (\n" + verilog_string += "\tinput [%d:0] in,\n" % (n_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" % (n_bits) + verilog_string += bin(val)[2:].zfill(n_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" + # open file, write string and close file + + if not os.path.exists(dir): + os.makedirs(dir) + + verilog_file = open(dir + "incomplete_table.v", "w") + verilog_file.write(verilog_string) + verilog_file.close() + + +def random_lut_verilog(n_bits, n_entries, dir): + """This function generates random care set and the verilog representation + of the LUT based on the care_set.""" + care_set = random_care_set(n_bits, n_entries) + gen_verilog(n_bits, care_set, dir) From af38ff9a9ac860907333f65825ebea3b2e36c2f5 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Mon, 8 Mar 2021 17:14:34 +0000 Subject: [PATCH 53/85] Merge branch 'dev' into feature/incomplete_tables --- .../verilog/truthtable/wrapper_truthtable.v | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/finn/data/verilog/truthtable/wrapper_truthtable.v diff --git a/src/finn/data/verilog/truthtable/wrapper_truthtable.v b/src/finn/data/verilog/truthtable/wrapper_truthtable.v deleted file mode 100644 index bef7555..0000000 --- a/src/finn/data/verilog/truthtable/wrapper_truthtable.v +++ /dev/null @@ -1,20 +0,0 @@ - -`timescale 1 ns / 1 ps - -(* CORE_GENERATION_INFO="wrapper_truthtable,hls_ip_2019_1,{HLS_INPUT_TYPE=cxx,HLS_INPUT_FLOAT=0,HLS_INPUT_FIXED=1,HLS_INPUT_PART=xc7z020-clg400-1,HLS_INPUT_CLOCK=5.000000,HLS_INPUT_ARCH=others,HLS_SYN_CLOCK=3.552000,HLS_SYN_LAT=0,HLS_SYN_TPT=none,HLS_SYN_MEM=0,HLS_SYN_DSP=0,HLS_SYN_FF=144,HLS_SYN_LUT=271,HLS_VERSION=2019_1}" *) - - -module wrapper_truthtable ( - input_data, - result_data -); - -input [15:0]input_data; - output result_data; - -incomplete_table my_incomplete_table( - .in(input_data), - .result(result_data) -); - -endmodule //wrapper_truthtable From ebd9c5d0457d6c1f390a0eb09758fed102ed7548 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Mon, 8 Mar 2021 18:21:57 +0000 Subject: [PATCH 54/85] Change verilog file path to temporary folder --- .../custom_op/logicnets/binary_truthtable.py | 54 ++++++++----------- src/finn/util/logicnets/logicnets.py | 44 --------------- tests/custom_op/test_binarytruthtable.py | 13 ++--- 3 files changed, 30 insertions(+), 81 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 8e7bc39..110aeef 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -34,6 +34,7 @@ from finn.core.datatype import DataType 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 @@ -77,17 +78,13 @@ class BinaryTruthTable(CustomOp): def get_nodeattr_types(self): return { - # Number of intput bits, 2 by default + # number of intput bits, 2 by default "in_bits": ("i", True, 2), - # Code generation mode + # code generation mode "code_mode": ("s", False, "Verilog"), - # Output code directory - "code_dir": ( - "s", - False, - "/workspace/finn-base/src/finn/data/verilog/truthtable/", - ), - # Execution mode, "pyhton" by default + # output code directory + "code_dir": ("s", False, ""), + # execution mode, "pyhton" by default "exec_mode": ("s", True, "python"), } @@ -109,40 +106,39 @@ def make_shape_compatible_op(self, model): def infer_node_datatype(self, model): node = self.onnx_node - # Check that the input[0] is binary + # 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 + # 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 + # check that the input[2] is UINT32 model.set_tensor_datatype(node.output[0], DataType["BINARY"]) def execute_node(self, context, graph): node = self.onnx_node - # Load inputs + # load inputs input_entry = context[node.input[0]] care_set = context[node.input[1]] - # Load execution mode + # load execution mode mode = self.get_nodeattr("exec_mode") if mode == "python": - # Calculate output in Python mode + # calculate output in Python mode output = binary_truthtable(input_entry, care_set, self) elif mode == "rtlsim": - # Generate PyVerilator object if Verilog file exits, - # otherwise generate Verilog and proceed - verilog_dir = self.get_nodeattr("code_dir") + "incomplete_table.v" + # check the code directory is not empty + verilog_dir = self.get_nodeattr("code_dir") + "/incomplete_table.v" if not os.path.exists(verilog_dir): - self.generate_verilog(care_set) + 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 + # 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 + # set value into the Verilog module sim.io["in"] = value - # Read result value + # read result value output = sim.io["result"] else: raise Exception( @@ -151,7 +147,7 @@ def execute_node(self, context, graph): mode ) ) - # Return output + # return output context[node.output[0]] = output def verify_node(self): @@ -204,13 +200,9 @@ def generate_verilog(self, care_set): verilog_string += "\t\t\tdefault: result = 1'b0;\n" # close the module verilog_string += "\t\tendcase\n\tend\nendmodule\n" - # open file, write string and close file - - dir = self.get_nodeattr("code_dir") - - if not os.path.exists(dir): - os.makedirs(dir) - - verilog_file = open(dir + "incomplete_table.v", "w") + # create temporary folder and save attribute value + self.set_nodeattr("code_dir", make_build_dir("verilog_")) + # create and write verilog file + verilog_file = open(self.get_nodeattr("code_dir") + "/incomplete_table.v", "w") verilog_file.write(verilog_string) verilog_file.close() diff --git a/src/finn/util/logicnets/logicnets.py b/src/finn/util/logicnets/logicnets.py index 637812b..b5a0726 100644 --- a/src/finn/util/logicnets/logicnets.py +++ b/src/finn/util/logicnets/logicnets.py @@ -27,7 +27,6 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import numpy as np -import os from random import randint @@ -50,46 +49,3 @@ def random_care_set(n_bits, n_entries): care_set = np.append(care_set, value) care_set = np.unique(care_set) return care_set - - -def gen_verilog(n_bits, care_set, dir): - """The function generated a verilog module based on the n_bit input values and the - care_set. The result for every value in the care_set is 1. The module creates a - LUT based on a n_bits:1 mapping""" - - # 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 incomplete_table (\n" - verilog_string += "\tinput [%d:0] in,\n" % (n_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" % (n_bits) - verilog_string += bin(val)[2:].zfill(n_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" - # open file, write string and close file - - if not os.path.exists(dir): - os.makedirs(dir) - - verilog_file = open(dir + "incomplete_table.v", "w") - verilog_file.write(verilog_string) - verilog_file.close() - - -def random_lut_verilog(n_bits, n_entries, dir): - """This function generates random care set and the verilog representation - of the LUT based on the care_set.""" - care_set = random_care_set(n_bits, n_entries) - gen_verilog(n_bits, care_set, dir) diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 8f98174..1dee911 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -92,8 +92,14 @@ def test_binarytruthtable(): assert model.get_tensor_datatype("output") is DataType.FLOAT32 model = model.transform(InferDataTypes()) assert model.get_tensor_datatype("output") is DataType.BINARY + + # Generate verilog + model = model.transform( + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) + ) + # Loop over "python" and "rtlsim" execution modes - for x in range(2): + for _ in range(2): # Loop over different input combinations for input_data in input_data_vector: # Create input dictionary @@ -114,8 +120,3 @@ def test_binarytruthtable(): # Change execution mode into "rtlsim" for simulation with PyVerilator myOp = getCustomOp(model.graph.node[0]) myOp.set_nodeattr("exec_mode", "rtlsim") - - # test transformation to generate verilog - model = model.transform( - GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) - ) From ac4b33d25d54f5d5debabd0ed2653f47842a9e0b Mon Sep 17 00:00:00 2001 From: jalezeta Date: Mon, 8 Mar 2021 19:46:03 +0000 Subject: [PATCH 55/85] Simplify the binary array to integer conversion --- .../custom_op/logicnets/binary_truthtable.py | 52 ++++++++++--------- .../verilog/truthtable/incomplete_table.v | 17 ------ tests/custom_op/test_binarytruthtable.py | 11 ++-- 3 files changed, 34 insertions(+), 46 deletions(-) delete mode 100644 src/finn/data/verilog/truthtable/incomplete_table.v diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 110aeef..519625a 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -38,37 +38,39 @@ from finn.util.data_packing import npy_to_rtlsim_input -def binary_truthtable(inputs, care_set, node): +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 5 is provided in the care-set, the result to fifth - combination of inputs 101 is 1. The input is a vector size x, representing - x-bits binary input. An example is presented: + 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. - inputs = [1, 0, 1] - care_set = [1, 2] - - Possible combinations: 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 + ************************************************************************** + The MSB in the input numpy array represents the LSB in the LUT. + ************************************************************************** - """ + An example is presented: - inputs = inputs[::-1] # reverse input array for C style indexing + inputs[0:2] = [1, 0, 1] + care_set = [1, 2] - in_int = 0 # initialize integer representation of the binary input array + 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 - for idx, in_val in enumerate(inputs): - in_int += (1 << idx) * in_val # calculate integer value of binary input + """ - output = 1 if in_int in care_set else 0 # return 1 if the input is in result_one + # 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 @@ -126,7 +128,9 @@ def execute_node(self, context, graph): mode = self.get_nodeattr("exec_mode") if mode == "python": # calculate output in Python mode - output = binary_truthtable(input_entry, care_set, self) + output = binary_truthtable( + input_entry, care_set, self.get_nodeattr("in_bits") + ) elif mode == "rtlsim": # check the code directory is not empty verilog_dir = self.get_nodeattr("code_dir") + "/incomplete_table.v" diff --git a/src/finn/data/verilog/truthtable/incomplete_table.v b/src/finn/data/verilog/truthtable/incomplete_table.v deleted file mode 100644 index be57489..0000000 --- a/src/finn/data/verilog/truthtable/incomplete_table.v +++ /dev/null @@ -1,17 +0,0 @@ -module incomplete_table ( - input [15:0] in, - output reg result -); - - always @(in) begin - case(in) - 16'b0000000000000001 : result = 1'b1; - 16'b0000000000111010 : result = 1'b1; - 16'b0000000000001111 : result = 1'b1; - 16'b0000000001011001 : result = 1'b1; - 16'b0000001010110111 : result = 1'b1; - 16'b0001100101010101 : result = 1'b1; - default: result = 1'b0; - endcase - end -endmodule diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 1dee911..cbc105a 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -39,6 +39,7 @@ from finn.transformation.logicnets.gen_bintruthtable_verilog import ( GenBinaryTruthTableVerilog, ) +from finn.util.data_packing import npy_to_rtlsim_input export_onnx_path = "test_truthtable.onnx" @@ -109,11 +110,11 @@ def test_binarytruthtable(): } # Perform execution out_dict = oxe.execute_onnx(model, in_dict) - # Calculate result here locally for comparison with the CustomOp result - input_data = input_data[::-1] - out_idx = 0 - for idx, val in enumerate(input_data): - out_idx += (1 << idx) * val + + 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 # compare outputs assert entry == out_dict["output"] From 0613341a973fa96f8d55f6e234755c371a9bddd3 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 9 Mar 2021 09:49:15 +0000 Subject: [PATCH 56/85] Check the shape and the size of the input vector --- src/finn/custom_op/logicnets/binary_truthtable.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 519625a..5948eb9 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -124,6 +124,19 @@ def execute_node(self, context, graph): # 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": From 79e4cfd843b749eb7ed8b0939537bb75823b9074 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Tue, 9 Mar 2021 10:42:40 +0000 Subject: [PATCH 57/85] Add UniqueNodeNames to create different verilog module names and files --- src/finn/custom_op/logicnets/binary_truthtable.py | 8 +++++--- tests/custom_op/test_binarytruthtable.py | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 5948eb9..c389a78 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -146,7 +146,8 @@ def execute_node(self, context, graph): ) elif mode == "rtlsim": # check the code directory is not empty - verilog_dir = self.get_nodeattr("code_dir") + "/incomplete_table.v" + 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) @@ -196,10 +197,11 @@ def verify_node(self): 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 incomplete_table (\n" + 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" @@ -220,6 +222,6 @@ def generate_verilog(self, care_set): # create temporary folder and save attribute value self.set_nodeattr("code_dir", make_build_dir("verilog_")) # create and write verilog file - verilog_file = open(self.get_nodeattr("code_dir") + "/incomplete_table.v", "w") + verilog_file = open(self.get_nodeattr("code_dir") + "/" + nodeName + ".v", "w") verilog_file.write(verilog_string) verilog_file.close() diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index cbc105a..8fb954b 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -34,6 +34,7 @@ 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 ( @@ -93,6 +94,8 @@ def test_binarytruthtable(): 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()) # Generate verilog model = model.transform( From c7d0033d49d61abec326da5551d1eb114ea980f6 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 10 Mar 2021 18:42:57 +0000 Subject: [PATCH 58/85] Initial commit of simple LogicNets model (Non working code) --- tests/logicnets-initial-model/concat_test.py | 55 +++++ tests/logicnets-initial-model/custom_model.py | 198 ++++++++++++++++++ tests/logicnets-initial-model/gather_test.py | 56 +++++ 3 files changed, 309 insertions(+) create mode 100644 tests/logicnets-initial-model/concat_test.py create mode 100644 tests/logicnets-initial-model/custom_model.py create mode 100644 tests/logicnets-initial-model/gather_test.py 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..3873a2f --- /dev/null +++ b/tests/logicnets-initial-model/custom_model.py @@ -0,0 +1,198 @@ +import numpy as np +import onnx +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_bintruthtable_verilog import ( + GenBinaryTruthTableVerilog, +) +from finn.util.data_packing import npy_to_rtlsim_input + +in_bits = 2 +care_set_data = np.array([1, 2, 3], dtype=np.float32) +indices0_data = np.array([1, 2]) +indices1_data = np.array([0, 1]) +in0_data = np.array([0, 1], dtype=np.float32) +in1_data = np.array([0, 1], dtype=np.float32) +in2_data = np.array([0, 1], dtype=np.float32) + +in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, [in_bits]) +in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, [in_bits]) +in2 = helper.make_tensor_value_info("in2", TensorProto.FLOAT, [in_bits]) +care_set = helper.make_tensor_value_info( + "care_set", TensorProto.FLOAT, [care_set_data.size] +) +out0 = helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1]) +out1 = helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1]) +indices0 = helper.make_tensor_value_info( + "indices0", TensorProto.FLOAT, indices0_data.shape +) +indices1 = helper.make_tensor_value_info( + "indices1", TensorProto.FLOAT, indices1_data.shape +) + +LUT0 = helper.make_node( + "BinaryTruthTable", + ["in0", "care_set"], + ["concat_in0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT1 = helper.make_node( + "BinaryTruthTable", + ["in1", "care_set"], + ["concat_in1"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT2 = helper.make_node( + "BinaryTruthTable", + ["in2", "care_set"], + ["concat_in2"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT3 = helper.make_node( + "BinaryTruthTable", + ["sparse_out0", "care_set"], + ["out0"], + domain="finn.custom_op.general", + in_bits=in_bits, + exec_mode="python", +) + +LUT4 = helper.make_node( + "BinaryTruthTable", + ["sparse_out1", "care_set"], + ["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, +) + +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=[ + LUT0, + LUT1, + LUT2, + LUT3, + LUT4, + concat0, + gather0, + gather1, + ], + name="my_LogicNets model", + inputs=[in0, in1, in2, care_set, indices0, indices1], + outputs=[out0, out1], + value_info=[ + 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("sparse_out0", TensorProto.FLOAT, [2]), + helper.make_tensor_value_info("sparse_out1", TensorProto.FLOAT, [2]), + ], +) + +modelproto = helper.make_model(graph, producer_name="simple-model") +onnx.save(modelproto, "simple-model.onnx") + + +def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): + + 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_set else 0 + concat_in1 = 1 if in1_int in care_set else 0 + concat_in2 = 1 if in2_int in care_set 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_set else 0 + out1 = 1 if sparse_out1_int in care_set else 0 + + return out0, out1 + + +input_dict = { + "in0": in0_data, + "in1": in1_data, + "in2": in2_data, + "care_set": care_set_data, + "indices0": indices0_data, + "indices1": indices1_data, +} + +model = ModelWrapper(modelproto) + +model.save("after_wrap.onnx") + +model.set_tensor_datatype("in0", DataType.BINARY) +model.set_tensor_datatype("in1", DataType.BINARY) +model.set_tensor_datatype("in2", 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_set", DataType.UINT32) +model.set_tensor_datatype("indices0", DataType.UINT32) +model.set_tensor_datatype("indices1", DataType.UINT32) +# model.set_tensor_datatype("out0",DataType.BINARY) +# model.set_tensor_datatype("out1",DataType.BINARY) + +model = model.transform(InferDataTypes()) +model = model.transform(InferShapes()) +model.save("after-datatypes.onnx") +model = model.transform(GiveUniqueNodeNames()) +model.save("after-uniquenames.onnx") + +model = model.transform( + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) +) + +out = oxe.execute_onnx(model, input_dict) +print(input_dict) diff --git a/tests/logicnets-initial-model/gather_test.py b/tests/logicnets-initial-model/gather_test.py new file mode 100644 index 0000000..e236699 --- /dev/null +++ b/tests/logicnets-initial-model/gather_test.py @@ -0,0 +1,56 @@ +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 + +array_data = np.array([[1, 0, 1, 1], [1, 0, 1, 1]]) + +indices_data = np.array([0]) + + +array = helper.make_tensor_value_info("array", TensorProto.FLOAT, array_data.shape) +indices = helper.make_tensor_value_info( + "indices", TensorProto.FLOAT, indices_data.shape +) +output = helper.make_tensor_value_info("output", TensorProto.FLOAT, indices_data.shape) + +gather0 = helper.make_node( + "Gather", + inputs=["array", "indices"], + outputs=["output"], +) + +modelproto = helper.make_model( + helper.make_graph([gather0], "test_model", [array, indices], [output]) +) + + +model = ModelWrapper(modelproto) + +model.save("initial.onnx") + +model.set_tensor_datatype("array", DataType.BINARY) +model.set_tensor_datatype("indices", DataType.UINT32) +model.set_tensor_datatype("output", DataType.BINARY) + +model = model.transform(InferShapes()) + +model.save("after-shapes.onnx") + +model = model.transform(InferDataTypes()) + +model.save("after-types.onnx") + +in_dict = { + "array": array_data, + "indices": indices_data, +} + +out_dict = oxe.execute_onnx(model, in_dict) + +print(out_dict) From 88dd4265066ba048ceddfbbada39a053c19f732d Mon Sep 17 00:00:00 2001 From: jalezeta Date: Wed, 10 Mar 2021 18:43:32 +0000 Subject: [PATCH 59/85] Initial commit of simple LogicNets model (Non working code) --- tests/logicnets-initial-model/gather_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/logicnets-initial-model/gather_test.py b/tests/logicnets-initial-model/gather_test.py index e236699..f8a1310 100644 --- a/tests/logicnets-initial-model/gather_test.py +++ b/tests/logicnets-initial-model/gather_test.py @@ -8,7 +8,7 @@ from finn.transformation.infer_datatypes import InferDataTypes from finn.transformation.infer_shapes import InferShapes -array_data = np.array([[1, 0, 1, 1], [1, 0, 1, 1]]) +array_data = np.array([[1, 0, 1, 1]]) indices_data = np.array([0]) From 5c41a7d16e7b26fb30cd07d79fcb432722acc969 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 12 Mar 2021 13:28:52 +0000 Subject: [PATCH 60/85] BinaryTruthTable returns a vector instead of a scalar and make_Shape_Compatible returns a constant node with inputs of the original node --- src/finn/custom_op/logicnets/binary_truthtable.py | 8 ++++---- tests/custom_op/test_binarytruthtable.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index c389a78..86f5c63 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -95,13 +95,13 @@ def make_shape_compatible_op(self, model): val = np.random.randn(1).astype(np.bool) node = helper.make_node( "Constant", - inputs=[], + inputs=[node.input[0], node.input[1]], outputs=["val"], value=helper.make_tensor( name="const_tensor", - data_type=onnx.TensorProto.BOOL, + data_type=onnx.TensorProto.FLOAT, dims=val.shape, - vals=val.flatten().astype(bool), + vals=val.flatten().astype(float), ), ) return node @@ -166,7 +166,7 @@ def execute_node(self, context, graph): ) ) # return output - context[node.output[0]] = output + context[node.output[0]] = np.array([output]) def verify_node(self): info_messages = [] diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 8fb954b..1ff61a5 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -119,8 +119,9 @@ def test_binarytruthtable(): ] entry = 1 if out_idx in care_set_data else 0 + expected = np.array([entry]) # compare outputs - assert entry == out_dict["output"] + 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") From 3179a867be2b7dc13aad9d43ba6e1702e6fa3cba Mon Sep 17 00:00:00 2001 From: jalezeta <51440887+jalezeta@users.noreply.github.com> Date: Fri, 12 Mar 2021 16:41:21 +0000 Subject: [PATCH 61/85] Update binary_truthtable.py Specify output array type to np.float32 --- src/finn/custom_op/logicnets/binary_truthtable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 86f5c63..a488185 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -166,7 +166,7 @@ def execute_node(self, context, graph): ) ) # return output - context[node.output[0]] = np.array([output]) + context[node.output[0]] = np.array([output], dtype=np.float32) def verify_node(self): info_messages = [] From 749ff9d2f37eb818f86c78cf2b5f2d861e4d9057 Mon Sep 17 00:00:00 2001 From: jalezeta Date: Fri, 12 Mar 2021 16:59:25 +0000 Subject: [PATCH 62/85] Fix simple LogicNets model creation --- .../after-datatypes.onnx | Bin 0 -> 1600 bytes .../logicnets-initial-model/after-shape.onnx | Bin 0 -> 1560 bytes .../after-uniquenames.onnx | Bin 0 -> 1735 bytes tests/logicnets-initial-model/after_wrap.onnx | Bin 0 -> 1090 bytes tests/logicnets-initial-model/custom_model.py | 42 +++++++++++------- .../logicnets-initial-model/simple-model.onnx | Bin 0 -> 1090 bytes 6 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 tests/logicnets-initial-model/after-datatypes.onnx create mode 100644 tests/logicnets-initial-model/after-shape.onnx create mode 100644 tests/logicnets-initial-model/after-uniquenames.onnx create mode 100644 tests/logicnets-initial-model/after_wrap.onnx create mode 100644 tests/logicnets-initial-model/simple-model.onnx diff --git a/tests/logicnets-initial-model/after-datatypes.onnx b/tests/logicnets-initial-model/after-datatypes.onnx new file mode 100644 index 0000000000000000000000000000000000000000..f743eb899e5e9c4b07f4e6a4ad186ed56ca6ffd2 GIT binary patch literal 1600 zcmcIjO;5r=6tsmvUC`(r5HD&ldca6%y>SDh#)M#E@SvAwDXXlZ+oroD@Nc;KFWr8m zz6vEC6|)tc)wBd7jok9rB-O}qcY?6h)Ge`mzv(Gy)({R&P+(pwLQP} zK{*Y`G`2I>+L_z3v)0;KTd=!BAU$w4!P!a^_GYqCx{cCqR#1`5nW*om@X%FCR}^G0 zK4uOTZCnvP2?Vtj+HRB{h@8+MhipQ!nUXyOH9#)KNb~>u&KSbpL&GgJjMR|e8HxIx+m)5~lWxvm-B2HSJ6GOJ=QkAvZj6(~}=KYt*t nvrCjtlg(OHNlJ+c>l6~gWtw`pCH3FsmhbydUG?Ukeo*}eV0Xn~ literal 0 HcmV?d00001 diff --git a/tests/logicnets-initial-model/after-shape.onnx b/tests/logicnets-initial-model/after-shape.onnx new file mode 100644 index 0000000000000000000000000000000000000000..4cb8bdcff4af2fc3cb4378cb5836a5a4f3e1bcfc GIT binary patch literal 1560 zcmcIjK~KUk7Jji7!qe2PmmbE18=s)47TDE0; z3ygS>Tl2ox_qzAK8D(4*%=PE=(e=i3-ubB_7Zn(9Bf}v9wFQ+JIUaY2v=!Po>oQJ) za1boybU;RP+N_~tx}=W1zTYVMp`3brr4^c|sLZ%MVp7!gm8N%U-;DE?vk=mAZO?B_ zD5n9L$9CpUJM&O>)=oR?0CukkqzBF>I9qAL-aNr z$IPLkjVr<@fuOcR+l|s2ky9GvkWEN7Q?iGk2FRrtY5srT8AI6n+VB94W9)|ZyEkD@ zpGt8Pt@-dg+Hs7KVQ9FBbVH9Ja~o3a09Fkow80BQg{P&g@p7WN!DHzJA7K>b3$(Qa zgHO5UmXYcS)*b50r3dE289p)X3{3m)t_7Mo5TA5j`}Q)nc|`BUoGBMGD6# zKax-ACw7f3XACy#A>2l9-n@C9nYB~LdrHj18UH>HCVY1FcON}ls3N3`9G^v;Dy}i| z1L-qO4cf*%Az8E>MGHM0vGI&kw{x(Lb(`oTf8sukMLV@{si%Q_wJP0Ds4gTO3$0rA zt7Ts`{|G71{DsoNoCe|91D8Bvv(y7A^gs$ckezw(3O#rQ9=siRxJ6+0ASNL8vi$@L z-FAK?yK+0VBIMvjmZ;dABUh6-7vI5nu-c5axS^2ltqmP93G z1eTGkjAUgm7J|ZHl+{(E&0{*>!4UH<;8Ddciq7fc{as)@U(!MFAp9ZM>f7qI-5;)A z8b-)*EL=slV#6D+o=B(o7^Br9fVP!l(1P{Y%Ff38 z10Xw>DX)RNuJ>C zd9OTwlKc8n8A#jG4brwWJ?gLfS1EO1%k*bI=GE$)~RVOBv$OIRR5<7~G1b@TcpR5yT`cPra zX5Tx%JDtw7#X3f&St4Fjy%Ndfn?ee{R3o4<&qZW}We{r>a~nA{=uV~L`C*>#>}JlF zi5T?YUhG9274L&)cCZ_*j-EewfRXI3*uM{!8!t+0qO@jhv+=6I^;|Q&9|~ogElny zUYh=$s|UPPbVmUy)k?;~j2MnDXufm15%~>rh{@qEnX&W`edx7}KZP~VWz83lWycu+ zr5?gLfS1EO1%k*bI=GE$)~RVOBv$OIRR5<7~G1b@TcpR5yT`cPra zX5Tx%JDtw7#X3f&St4Fjy%Ndfn?ee{R3o4<&qZW}We{r>a~nA{=uV~L`C*>#>}JlF zi5T?YUhG9274L&)cCZ_*j-EewfRXI3*uM{!8!t+0qO@jhv+=6I^;|Q&9|~ogElny zUYh=$s|UPPbVmUy)k?;~j2MnDXufm15%~>rh{@qEnX&W`edx7}KZP~VWz83lWycu+ zr5 Date: Tue, 16 Mar 2021 11:08:54 +0000 Subject: [PATCH 63/85] Remove inputs and add custom tensor name in make_shape compatible function --- src/finn/custom_op/logicnets/binary_truthtable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index a488185..f45fae6 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -33,6 +33,7 @@ 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 @@ -93,12 +94,13 @@ def get_nodeattr_types(self): 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=[node.input[0], node.input[1]], + inputs=[], outputs=["val"], value=helper.make_tensor( - name="const_tensor", + name=tensor_name, data_type=onnx.TensorProto.FLOAT, dims=val.shape, vals=val.flatten().astype(float), From 7fa95293af873a38b2bed38f35724b9bbec1ac7f Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 26 Mar 2021 16:13:08 +0000 Subject: [PATCH 64/85] Change operation pattern of LogicNets model to --- tests/logicnets-initial-model/custom_model.py | 61 ++++++++++++++++--- tests/logicnets-initial-model/gather_test.py | 56 ----------------- 2 files changed, 51 insertions(+), 66 deletions(-) delete mode 100644 tests/logicnets-initial-model/gather_test.py diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index df537df..46f705f 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -18,6 +18,7 @@ care_set_data = np.array([1, 2, 3], dtype=np.float32) indices0_data = np.array([1, 2]) indices1_data = np.array([0, 1]) +indices_in_data = np.array([0, 1]) in0_data = np.array([0, 1], dtype=np.float32) in1_data = np.array([0, 1], dtype=np.float32) in2_data = np.array([0, 1], dtype=np.float32) @@ -28,17 +29,39 @@ care_set = helper.make_tensor_value_info( "care_set", TensorProto.FLOAT, care_set_data.shape ) -out0 = helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1]) -out1 = helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1]) +final_output = helper.make_tensor_value_info("final_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_in = helper.make_tensor_value_info( + "indices_in", TensorProto.INT64, indices_in_data.shape +) + +gather_in0 = helper.make_node( + "Gather", + ["in0", "indices_in"], + ["LUTin0"], +) + +gather_in1 = helper.make_node( + "Gather", + ["in1", "indices_in"], + ["LUTin1"], +) + +gather_in2 = helper.make_node( + "Gather", + ["in2", "indices_in"], + ["LUTin2"], +) + LUT0 = helper.make_node( "BinaryTruthTable", - ["in0", "care_set"], + ["LUTin0", "care_set"], ["concat_in0"], domain="finn.custom_op.general", in_bits=in_bits, @@ -47,7 +70,7 @@ LUT1 = helper.make_node( "BinaryTruthTable", - ["in1", "care_set"], + ["LUTin1", "care_set"], ["concat_in1"], domain="finn.custom_op.general", in_bits=in_bits, @@ -56,7 +79,7 @@ LUT2 = helper.make_node( "BinaryTruthTable", - ["in2", "care_set"], + ["LUTin2", "care_set"], ["concat_in2"], domain="finn.custom_op.general", in_bits=in_bits, @@ -88,6 +111,13 @@ axis=0, ) +concat_out = helper.make_node( + "Concat", + ["out0", "out1"], + ["final_output"], + axis=0, +) + gather0 = helper.make_node( "Gather", ["concat_out", "indices0"], @@ -107,18 +137,27 @@ LUT2, LUT3, LUT4, + gather_in0, + gather_in1, + gather_in2, concat0, gather0, gather1, + concat_out, ], name="my_LogicNets model", - inputs=[in0, in1, in2, care_set, indices0, indices1], - outputs=[out0, out1], + inputs=[in0, in1, in2, care_set, indices0, indices1, indices_in], + outputs=[final_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 ), @@ -175,6 +214,9 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model.set_tensor_datatype("in0", DataType.BINARY) model.set_tensor_datatype("in1", DataType.BINARY) model.set_tensor_datatype("in2", 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) @@ -183,8 +225,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model.set_tensor_datatype("care_set", DataType.UINT32) model.set_tensor_datatype("indices0", DataType.UINT32) model.set_tensor_datatype("indices1", DataType.UINT32) -model.set_tensor_datatype("out0", DataType.BINARY) -model.set_tensor_datatype("out1", DataType.BINARY) +model.set_tensor_datatype("final_output", DataType.BINARY) model = model.transform(InferShapes()) model.save("after-shape.onnx") @@ -201,7 +242,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): out = oxe.execute_onnx(model, input_dict) -output = np.array([out["out0"], out["out1"]]) +output = out["final_output"] expected = expected_output( in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits diff --git a/tests/logicnets-initial-model/gather_test.py b/tests/logicnets-initial-model/gather_test.py deleted file mode 100644 index f8a1310..0000000 --- a/tests/logicnets-initial-model/gather_test.py +++ /dev/null @@ -1,56 +0,0 @@ -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 - -array_data = np.array([[1, 0, 1, 1]]) - -indices_data = np.array([0]) - - -array = helper.make_tensor_value_info("array", TensorProto.FLOAT, array_data.shape) -indices = helper.make_tensor_value_info( - "indices", TensorProto.FLOAT, indices_data.shape -) -output = helper.make_tensor_value_info("output", TensorProto.FLOAT, indices_data.shape) - -gather0 = helper.make_node( - "Gather", - inputs=["array", "indices"], - outputs=["output"], -) - -modelproto = helper.make_model( - helper.make_graph([gather0], "test_model", [array, indices], [output]) -) - - -model = ModelWrapper(modelproto) - -model.save("initial.onnx") - -model.set_tensor_datatype("array", DataType.BINARY) -model.set_tensor_datatype("indices", DataType.UINT32) -model.set_tensor_datatype("output", DataType.BINARY) - -model = model.transform(InferShapes()) - -model.save("after-shapes.onnx") - -model = model.transform(InferDataTypes()) - -model.save("after-types.onnx") - -in_dict = { - "array": array_data, - "indices": indices_data, -} - -out_dict = oxe.execute_onnx(model, in_dict) - -print(out_dict) From be403f90ec18f1d4bdec8243dbf3a20ce73c06db Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 26 Mar 2021 17:05:11 +0000 Subject: [PATCH 65/85] Fix small problem, missing input index data in the input dictionary --- tests/logicnets-initial-model/custom_model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 46f705f..40152d8 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -206,6 +206,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): "care_set": care_set_data, "indices0": indices0_data, "indices1": indices1_data, + "indices_in": indices_in_data, } model = ModelWrapper(modelproto) From 35fc6afcd1cb0ac17aea2ef0335725682b2b0246 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Mon, 29 Mar 2021 13:23:54 +0100 Subject: [PATCH 66/85] Add Concat operation at the beginning of the model --- tests/logicnets-initial-model/custom_model.py | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 40152d8..2178531 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -18,7 +18,10 @@ care_set_data = np.array([1, 2, 3], dtype=np.float32) indices0_data = np.array([1, 2]) indices1_data = np.array([0, 1]) -indices_in_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]) + in0_data = np.array([0, 1], dtype=np.float32) in1_data = np.array([0, 1], dtype=np.float32) in2_data = np.array([0, 1], dtype=np.float32) @@ -37,25 +40,38 @@ indices1 = helper.make_tensor_value_info( "indices1", TensorProto.INT64, indices1_data.shape ) -indices_in = helper.make_tensor_value_info( - "indices_in", TensorProto.INT64, indices_in_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 +) + +concat_in = helper.make_node( + "Concat", + ["in0", "in1", "in2"], + ["concatenated_input"], + axis=0, ) gather_in0 = helper.make_node( "Gather", - ["in0", "indices_in"], + ["concatenated_input", "indices_in0"], ["LUTin0"], ) gather_in1 = helper.make_node( "Gather", - ["in1", "indices_in"], + ["concatenated_input", "indices_in1"], ["LUTin1"], ) gather_in2 = helper.make_node( "Gather", - ["in2", "indices_in"], + ["concatenated_input", "indices_in2"], ["LUTin2"], ) @@ -132,11 +148,7 @@ graph = helper.make_graph( nodes=[ - LUT0, - LUT1, - LUT2, - LUT3, - LUT4, + concat_in, gather_in0, gather_in1, gather_in2, @@ -144,11 +156,27 @@ gather0, gather1, concat_out, + LUT0, + LUT1, + LUT2, + LUT3, + LUT4, ], name="my_LogicNets model", - inputs=[in0, in1, in2, care_set, indices0, indices1, indices_in], + inputs=[ + in0, + in1, + in2, + care_set, + indices0, + indices1, + indices_in0, + indices_in1, + indices_in2, + ], outputs=[final_output], value_info=[ + helper.make_tensor_value_info("concatenated_input", TensorProto.FLOAT, [6]), 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]), @@ -206,7 +234,9 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): "care_set": care_set_data, "indices0": indices0_data, "indices1": indices1_data, - "indices_in": indices_in_data, + "indices_in0": indices_in0_data, + "indices_in1": indices_in1_data, + "indices_in2": indices_in2_data, } model = ModelWrapper(modelproto) @@ -224,6 +254,10 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model.set_tensor_datatype("sparse_out0", DataType.BINARY) model.set_tensor_datatype("sparse_out1", DataType.BINARY) model.set_tensor_datatype("care_set", 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("final_output", DataType.BINARY) From 0282ef696d40bc092c1cdf34646e9e63f5456acf Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Mon, 29 Mar 2021 15:31:36 +0100 Subject: [PATCH 67/85] Support for dedicated care_set in the node level Verilog generation --- src/finn/transformation/logicnets/gen_bintruthtable_verilog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py index d60bb6f..b4bae83 100644 --- a/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py +++ b/src/finn/transformation/logicnets/gen_bintruthtable_verilog.py @@ -53,6 +53,7 @@ def __init__(self, num_workers, care_set): def applyNodeLocal(self, node): op_type = node.op_type if op_type == "BinaryTruthTable": - _genbintruthtable_verilog(node, self.care_set) + specific_care_set = self.care_set[node.input[1]] + _genbintruthtable_verilog(node, specific_care_set) return (node, False) From 0a892501e5ebb596c1bb5804d02732b7a35e3513 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Mon, 29 Mar 2021 15:54:24 +0100 Subject: [PATCH 68/85] Add support for separate care sets --- tests/logicnets-initial-model/custom_model.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 2178531..1a63439 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -15,7 +15,8 @@ from finn.util.data_packing import npy_to_rtlsim_input in_bits = 2 -care_set_data = np.array([1, 2, 3], dtype=np.float32) +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]) @@ -29,8 +30,11 @@ in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, in0_data.shape) in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, in1_data.shape) in2 = helper.make_tensor_value_info("in2", TensorProto.FLOAT, in2_data.shape) -care_set = helper.make_tensor_value_info( - "care_set", TensorProto.FLOAT, care_set_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 ) final_output = helper.make_tensor_value_info("final_output", TensorProto.FLOAT, [2]) @@ -77,7 +81,7 @@ LUT0 = helper.make_node( "BinaryTruthTable", - ["LUTin0", "care_set"], + ["LUTin0", "care_set0"], ["concat_in0"], domain="finn.custom_op.general", in_bits=in_bits, @@ -86,7 +90,7 @@ LUT1 = helper.make_node( "BinaryTruthTable", - ["LUTin1", "care_set"], + ["LUTin1", "care_set0"], ["concat_in1"], domain="finn.custom_op.general", in_bits=in_bits, @@ -95,7 +99,7 @@ LUT2 = helper.make_node( "BinaryTruthTable", - ["LUTin2", "care_set"], + ["LUTin2", "care_set0"], ["concat_in2"], domain="finn.custom_op.general", in_bits=in_bits, @@ -104,7 +108,7 @@ LUT3 = helper.make_node( "BinaryTruthTable", - ["sparse_out0", "care_set"], + ["sparse_out0", "care_set1"], ["out0"], domain="finn.custom_op.general", in_bits=in_bits, @@ -113,7 +117,7 @@ LUT4 = helper.make_node( "BinaryTruthTable", - ["sparse_out1", "care_set"], + ["sparse_out1", "care_set1"], ["out1"], domain="finn.custom_op.general", in_bits=in_bits, @@ -167,7 +171,8 @@ in0, in1, in2, - care_set, + care_set0, + care_set1, indices0, indices1, indices_in0, @@ -231,7 +236,8 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): "in0": in0_data, "in1": in1_data, "in2": in2_data, - "care_set": care_set_data, + "care_set0": care_set0_data, + "care_set1": care_set1_data, "indices0": indices0_data, "indices1": indices1_data, "indices_in0": indices_in0_data, @@ -253,7 +259,8 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): 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_set", DataType.UINT32) +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) @@ -271,16 +278,23 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): model = model.transform(GiveUniqueNodeNames()) model.save("after-uniquenames.onnx") +care_set_dict = { + "care_set0": care_set0_data, + "care_set1": care_set1_data, +} + model = model.transform( - GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_data) + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_dict) ) out = oxe.execute_onnx(model, input_dict) output = out["final_output"] -expected = expected_output( - in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits -) +# expected = expected_output( +# in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits +# ) -assert (output == expected).all +print(output) +# print(expected) +# assert (output == expected).all From ab3c976062d665a7e75985ef85748ff2605fc7be Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Tue, 30 Mar 2021 08:52:25 +0100 Subject: [PATCH 69/85] Add python based prediction and result assertion --- tests/logicnets-initial-model/custom_model.py | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 1a63439..f067cde 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -23,9 +23,9 @@ indices_in1_data = np.array([2, 3]) indices_in2_data = np.array([4, 5]) -in0_data = np.array([0, 1], dtype=np.float32) -in1_data = np.array([0, 1], dtype=np.float32) -in2_data = np.array([0, 1], dtype=np.float32) +in0_data = np.array([0, 0], dtype=np.float32) +in1_data = np.array([0, 0], dtype=np.float32) +in2_data = np.array([0, 0], dtype=np.float32) in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, in0_data.shape) in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, in1_data.shape) @@ -154,17 +154,17 @@ nodes=[ concat_in, gather_in0, - gather_in1, - gather_in2, concat0, gather0, + LUT2, + LUT3, + LUT4, gather1, concat_out, LUT0, LUT1, - LUT2, - LUT3, - LUT4, + gather_in1, + gather_in2, ], name="my_LogicNets model", inputs=[ @@ -204,15 +204,14 @@ onnx.save(modelproto, "simple-model.onnx") -def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): - +def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_bits): 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_set else 0 - concat_in1 = 1 if in1_int in care_set else 0 - concat_in2 = 1 if in2_int in care_set else 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)] @@ -226,8 +225,8 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): 0 ] - out0 = 1 if sparse_out0_int in care_set else 0 - out1 = 1 if sparse_out1_int in care_set else 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]) @@ -291,10 +290,15 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set, in_bits): output = out["final_output"] -# expected = expected_output( -# in0_data, in1_data, in0_data, indices0_data, indices1_data, care_set_data, in_bits -# ) +expected = expected_output( + in0_data, + in1_data, + in0_data, + indices0_data, + indices1_data, + care_set0_data, + care_set1_data, + in_bits, +) -print(output) -# print(expected) -# assert (output == expected).all +assert (output == expected).all From 14f2a1bf5c68ad42d7e1a89712743435acd52151 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Tue, 30 Mar 2021 12:44:20 +0100 Subject: [PATCH 70/85] Remove input concat block. We assume the general input to the model is a single array with 6-bits. This array is later sliced based on the gather block. --- tests/logicnets-initial-model/custom_model.py | 58 ++++++++----------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index f067cde..2c015e5 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -23,20 +23,19 @@ indices_in1_data = np.array([2, 3]) indices_in2_data = np.array([4, 5]) -in0_data = np.array([0, 0], dtype=np.float32) -in1_data = np.array([0, 0], dtype=np.float32) -in2_data = np.array([0, 0], dtype=np.float32) +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 +) -in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, in0_data.shape) -in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, in1_data.shape) -in2 = helper.make_tensor_value_info("in2", TensorProto.FLOAT, in2_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 ) -final_output = helper.make_tensor_value_info("final_output", TensorProto.FLOAT, [2]) +general_output = helper.make_tensor_value_info("general_output", TensorProto.FLOAT, [2]) indices0 = helper.make_tensor_value_info( "indices0", TensorProto.INT64, indices0_data.shape @@ -54,28 +53,22 @@ "indices_in2", TensorProto.INT64, indices_in2_data.shape ) -concat_in = helper.make_node( - "Concat", - ["in0", "in1", "in2"], - ["concatenated_input"], - axis=0, -) gather_in0 = helper.make_node( "Gather", - ["concatenated_input", "indices_in0"], + ["general_input", "indices_in0"], ["LUTin0"], ) gather_in1 = helper.make_node( "Gather", - ["concatenated_input", "indices_in1"], + ["general_input", "indices_in1"], ["LUTin1"], ) gather_in2 = helper.make_node( "Gather", - ["concatenated_input", "indices_in2"], + ["general_input", "indices_in2"], ["LUTin2"], ) @@ -134,7 +127,7 @@ concat_out = helper.make_node( "Concat", ["out0", "out1"], - ["final_output"], + ["general_output"], axis=0, ) @@ -152,7 +145,6 @@ graph = helper.make_graph( nodes=[ - concat_in, gather_in0, concat0, gather0, @@ -168,9 +160,7 @@ ], name="my_LogicNets model", inputs=[ - in0, - in1, - in2, + general_input, care_set0, care_set1, indices0, @@ -179,9 +169,8 @@ indices_in1, indices_in2, ], - outputs=[final_output], + outputs=[general_output], value_info=[ - helper.make_tensor_value_info("concatenated_input", TensorProto.FLOAT, [6]), 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]), @@ -204,7 +193,12 @@ onnx.save(modelproto, "simple-model.onnx") -def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_bits): +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] @@ -232,9 +226,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ input_dict = { - "in0": in0_data, - "in1": in1_data, - "in2": in2_data, + "general_input": general_input_data, "care_set0": care_set0_data, "care_set1": care_set1_data, "indices0": indices0_data, @@ -247,9 +239,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ model = ModelWrapper(modelproto) model.save("after_wrap.onnx") -model.set_tensor_datatype("in0", DataType.BINARY) -model.set_tensor_datatype("in1", DataType.BINARY) -model.set_tensor_datatype("in2", DataType.BINARY) +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) @@ -266,7 +256,7 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ 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("final_output", DataType.BINARY) +model.set_tensor_datatype("general_output", DataType.BINARY) model = model.transform(InferShapes()) model.save("after-shape.onnx") @@ -288,12 +278,10 @@ def expected_output(in0, in1, in2, indices0, indices1, care_set0, care_set1, in_ out = oxe.execute_onnx(model, input_dict) -output = out["final_output"] +output = out["general_output"] expected = expected_output( - in0_data, - in1_data, - in0_data, + general_input_data, indices0_data, indices1_data, care_set0_data, From f6a5bad6ac0ce6b0e6015f588921982542f6459d Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Thu, 1 Apr 2021 15:13:32 +0100 Subject: [PATCH 71/85] 1- Add Verilog generation for Custom logicnets model.\n2- Add test for verilog generation code using PyVerilator simulation. --- .../logicnets/gen_logicnets_verilog.py | 280 ++++++++++++++++ tests/custom_op/test_binarytruthtable.py | 7 +- tests/logicnets-initial-model/custom_model.py | 18 +- .../transformation/test_logicnets_verilog.py | 315 ++++++++++++++++++ 4 files changed, 613 insertions(+), 7 deletions(-) create mode 100644 src/finn/transformation/logicnets/gen_logicnets_verilog.py create mode 100644 tests/transformation/test_logicnets_verilog.py 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..620cc96 --- /dev/null +++ b/src/finn/transformation/logicnets/gen_logicnets_verilog.py @@ -0,0 +1,280 @@ +# 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): + graph = model.graph + + # TO BE DISCUSSED + # try: + # code_dir + # except : + # code_dir = make_build_dir("logicnets_model_") + + # 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 code_dir + + +def _generate_verilog(model, indices, code_dir): + + # Generate verilog file + verilog_file = open(code_dir + "/" + "LogicNetsModule.v", "w") + + graph = model.graph + + # 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, + ) + # Get number of nodes in the ONNX graph + number_nodes = len(graph.node) + + # 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 + for j in range(index): + if (graph.node[j].op_type == "Gather") and ( + graph.node[j].output[0] == op_input_name + ): + # Step 4 + gather_index_name = graph.node[j].input[1] + gather_input_name = graph.node[j].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" + break + k = index + # Step 6 + while k < number_nodes: + if (graph.node[k].op_type == "Concat") and op_output_name in graph.node[ + k + ].input: + # Step 7 + concat_out_position = list(graph.node[k].input).index( + op_output_name + ) + # Step 8 + concat_out_name = graph.node[k].output[0] + # Step 9 + 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, + ) + break + k += 1 + 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, code_dir): + super().__init__() + self.care_set = care_set + self.indices = indices + self.code_dir = code_dir + + 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 + code_dir = _create_logicnets_folder(model, code_dir=self.code_dir) + + # Generate the Verilog wrapper that connects every individual BinaryTruthTable + # verilog module. + _generate_verilog(model, indices=self.indices, code_dir=code_dir) + + return (model, False) diff --git a/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 1ff61a5..c13d0e6 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -97,9 +97,14 @@ def test_binarytruthtable(): # 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_data) + GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_dict) ) # Loop over "python" and "rtlsim" execution modes diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 2c015e5..47cdf4f 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -9,9 +9,7 @@ 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_logicnets_verilog import GenLogicNetsVerilog from finn.util.data_packing import npy_to_rtlsim_input in_bits = 2 @@ -160,7 +158,6 @@ ], name="my_LogicNets model", inputs=[ - general_input, care_set0, care_set1, indices0, @@ -168,6 +165,7 @@ indices_in0, indices_in1, indices_in2, + general_input, ], outputs=[general_output], value_info=[ @@ -226,8 +224,8 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit input_dict = { - "general_input": general_input_data, "care_set0": care_set0_data, + "general_input": general_input_data, "care_set1": care_set1_data, "indices0": indices0_data, "indices1": indices1_data, @@ -272,8 +270,16 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit "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( - GenBinaryTruthTableVerilog(num_workers=None, care_set=care_set_dict) + GenLogicNetsVerilog(care_set=care_set_dict, indices=indices_dict) ) out = oxe.execute_onnx(model, input_dict) diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py new file mode 100644 index 0000000..20b901b --- /dev/null +++ b/tests/transformation/test_logicnets_verilog.py @@ -0,0 +1,315 @@ +import numpy as np +import onnx +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.basic import make_build_dir +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") + onnx.save(modelproto, "simple-model.onnx") + + 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.save("after_wrap.onnx") + + 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, + } + + # Generate directory for storing Verilog files + code_dir = make_build_dir("logicnets_model_") + model = model.transform( + GenLogicNetsVerilog( + care_set=care_set_dict, indices=indices_dict, code_dir=code_dir + ) + ) + + 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 + 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) From 84f35a87d0c27f4b240efe9c4962da26e27b846d Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Thu, 1 Apr 2021 15:26:13 +0100 Subject: [PATCH 72/85] Reformat verilog generation file --- tests/transformation/test_logicnets_verilog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py index 20b901b..023d6a6 100644 --- a/tests/transformation/test_logicnets_verilog.py +++ b/tests/transformation/test_logicnets_verilog.py @@ -303,6 +303,7 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit care_set1_data, in_bits, ) + assert np.array_equal(output, expected) # PyVerilator simulation of generated Verilog code @@ -312,4 +313,5 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit 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) From 268014169df9c36fb3404c6819cdd74113f940da Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 9 Apr 2021 02:07:00 +0100 Subject: [PATCH 73/85] Make the algorithm cleaner by using ModelWrapper functions to find preceding and succeding nodes. Add assertions. --- .../logicnets/gen_logicnets_verilog.py | 127 +++++++++--------- .../transformation/test_logicnets_verilog.py | 9 +- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/finn/transformation/logicnets/gen_logicnets_verilog.py b/src/finn/transformation/logicnets/gen_logicnets_verilog.py index 620cc96..64a6776 100644 --- a/src/finn/transformation/logicnets/gen_logicnets_verilog.py +++ b/src/finn/transformation/logicnets/gen_logicnets_verilog.py @@ -34,8 +34,7 @@ from finn.transformation.logicnets.gen_bintruthtable_verilog import ( GenBinaryTruthTableVerilog, ) - -# from finn.util.basic import make_build_dir +from finn.util.basic import make_build_dir def _check_node_verilog(model): @@ -54,15 +53,11 @@ def _check_node_verilog(model): return True -def _create_logicnets_folder(model, code_dir): - graph = model.graph - - # TO BE DISCUSSED - # try: - # code_dir - # except : - # code_dir = make_build_dir("logicnets_model_") +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: @@ -72,16 +67,31 @@ def _create_logicnets_folder(model, code_dir): node_dir = customOp.get_nodeattr("code_dir") node_file = node_dir + "/" + nodeName + ".v" shutil.copy2(node_file, code_dir) - return code_dir + return model -def _generate_verilog(model, indices, code_dir): +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. @@ -120,8 +130,6 @@ def _generate_verilog(model, indices, code_dir): output_shape, output_tensor_name, ) - # Get number of nodes in the ONNX graph - number_nodes = len(graph.node) # 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. @@ -178,52 +186,48 @@ def _generate_verilog(model, indices, code_dir): node_name = node.name customOp = registry.getCustomOp(node) in_bits = customOp.get_nodeattr("in_bits") - 1 + # Step 3 - for j in range(index): - if (graph.node[j].op_type == "Gather") and ( - graph.node[j].output[0] == op_input_name - ): - # Step 4 - gather_index_name = graph.node[j].input[1] - gather_input_name = graph.node[j].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" - break - k = index + 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 - while k < number_nodes: - if (graph.node[k].op_type == "Concat") and op_output_name in graph.node[ - k - ].input: - # Step 7 - concat_out_position = list(graph.node[k].input).index( - op_output_name - ) - # Step 8 - concat_out_name = graph.node[k].output[0] - # Step 9 - 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, + 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_out_position, ) - break - k += 1 + 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 @@ -251,11 +255,10 @@ class GenLogicNetsVerilog(Transformation): - The code_dir is used to return the path of the generated verilog source code.""" - def __init__(self, care_set, indices, code_dir): + def __init__(self, care_set, indices): super().__init__() self.care_set = care_set self.indices = indices - self.code_dir = code_dir def apply(self, model): @@ -270,11 +273,11 @@ def apply(self, model): ) # Create LogicNets folder and copy all individual BinaryTruthTable code into - # the folder - code_dir = _create_logicnets_folder(model, code_dir=self.code_dir) - + # 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, code_dir=code_dir) + _generate_verilog(model, indices=self.indices) return (model, False) diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py index 023d6a6..02e3297 100644 --- a/tests/transformation/test_logicnets_verilog.py +++ b/tests/transformation/test_logicnets_verilog.py @@ -11,7 +11,6 @@ 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.basic import make_build_dir from finn.util.data_packing import npy_to_rtlsim_input @@ -283,12 +282,8 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit "indices1": indices1_data, } - # Generate directory for storing Verilog files - code_dir = make_build_dir("logicnets_model_") model = model.transform( - GenLogicNetsVerilog( - care_set=care_set_dict, indices=indices_dict, code_dir=code_dir - ) + GenLogicNetsVerilog(care_set=care_set_dict, indices=indices_dict) ) out = oxe.execute_onnx(model, input_dict) @@ -307,6 +302,8 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit 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] From 8d89a9db247a09045a55aa164124c73638459377 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Tue, 20 Apr 2021 01:30:42 +0100 Subject: [PATCH 74/85] Add PLA file backend support for BinaryTruthTable and TruthTable customOp-s --- .../custom_op/logicnets/binary_truthtable.py | 31 +++++++- src/finn/custom_op/logicnets/truthtable.py | 35 ++++++++- .../logicnets/gen_truthtable_pla.py | 75 +++++++++++++++++++ tests/custom_op/test_binarytruthtable.py | 6 +- tests/custom_op/test_truthtable.py | 7 +- 5 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 src/finn/transformation/logicnets/gen_truthtable_pla.py diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 1ad8121..cb11943 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -230,8 +230,37 @@ def generate_verilog(self, care_set): # 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_verilog_")) + 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 index e1245a4..7305081 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -323,8 +323,41 @@ def generate_verilog(self, care_set, results): # 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_verilog_")) + 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/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/tests/custom_op/test_binarytruthtable.py b/tests/custom_op/test_binarytruthtable.py index 5f1a60d..ec3779d 100644 --- a/tests/custom_op/test_binarytruthtable.py +++ b/tests/custom_op/test_binarytruthtable.py @@ -40,6 +40,7 @@ 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" @@ -60,7 +61,7 @@ def test_binarytruthtable(): dtype=np.float32, ) # Set the care set - care_set_data = np.asarray([1, 58, 15, 89, 695, 6485], dtype=np.float32) + care_set_data = np.asarray([1, 58, 15, 89, 695, 6485]) in_bits = 16 # Set input and output tensor information @@ -107,6 +108,9 @@ def test_binarytruthtable(): 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 diff --git a/tests/custom_op/test_truthtable.py b/tests/custom_op/test_truthtable.py index f623086..92d652a 100644 --- a/tests/custom_op/test_truthtable.py +++ b/tests/custom_op/test_truthtable.py @@ -38,6 +38,7 @@ 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 @@ -112,11 +113,15 @@ def test_truthtable(): "results": results_data, } - # Generate verilog + # 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 From 2c08044c5e9011c19911e731a18ac20d775bbf46 Mon Sep 17 00:00:00 2001 From: Mirza Mrahorovic <34712307+mmrahorovic@users.noreply.github.com> Date: Mon, 10 May 2021 00:08:26 +0200 Subject: [PATCH 75/85] Changes for supporting non-equal dilation (#29) * added support for non-equal dilation value along (H, W) dimension * added test cases for non-equal dilation configurations * appending dilation value along dummy dimension correctly (i.e. with a '1') * changed tensor sparsity annotation for consistency --- src/finn/custom_op/general/im2col.py | 62 ++-- .../transformation/change_3d_tensors_to_4d.py | 2 +- .../transformation/lower_convs_to_matmul.py | 8 +- tests/core/test_modelwrapper.py | 2 +- tests/custom_op/test_im2col.py | 286 ++++++++++++++---- tests/transformation/test_conv_lowering.py | 6 +- .../test_general_transformation.py | 8 +- .../transformation/test_merge_onnx_models.py | 2 +- 8 files changed, 276 insertions(+), 100 deletions(-) diff --git a/src/finn/custom_op/general/im2col.py b/src/finn/custom_op/general/im2col.py index 505f8ac..e76c613 100644 --- a/src/finn/custom_op/general/im2col.py +++ b/src/finn/custom_op/general/im2col.py @@ -31,20 +31,27 @@ 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_h=1, stride_w=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_h, pad_h, dilation) - out_width = compute_conv_output_dim(w, field_width, stride_w, 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_h * np.repeat(np.arange(out_height), out_width) - j0 = dilation * np.tile(np.arange(field_width), field_height * c) + 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) @@ -64,7 +71,8 @@ def im2col_indices_nchw( 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. @@ -80,7 +88,14 @@ def im2col_indices_nchw( ) k, i, j = get_im2col_indices_nchw( - x.shape, field_height, field_width, padding, stride_h, stride_w, dilation + x.shape, + field_height, + field_width, + padding, + stride_h, + stride_w, + dilation_h, + dilation_w, ) cols = x_padded[:, k, i, j] @@ -122,18 +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") - stride_h = stride[0] - stride_w = stride[1] + 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] @@ -170,8 +181,8 @@ def make_shape_compatible_op(self, model): ), "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_h, pad_h, dilation) - ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride_w, 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( @@ -197,17 +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") - stride_h = stride[0] - stride_w = stride[1] + 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] @@ -237,8 +244,8 @@ def execute_node(self, context, graph): ), "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_h, pad_h, dilation) - out_dim_w = compute_conv_output_dim(w, k_w, stride_w, 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 @@ -252,7 +259,8 @@ def execute_node(self, context, graph): stride_h, stride_w, pad_val=pad_val, - dilation=dilation, + 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/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/lower_convs_to_matmul.py b/src/finn/transformation/lower_convs_to_matmul.py index f1da6e2..111c3ea 100644 --- a/src/finn/transformation/lower_convs_to_matmul.py +++ b/src/finn/transformation/lower_convs_to_matmul.py @@ -85,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: @@ -137,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 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_im2col.py b/tests/custom_op/test_im2col.py index 092a05c..3a26639 100644 --- a/tests/custom_op/test_im2col.py +++ b/tests/custom_op/test_im2col.py @@ -9,19 +9,6 @@ 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, @@ -34,12 +21,13 @@ def execution_im2col( 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_h, pad_amt_h, dilation) - ofm_dim_w = compute_conv_output_dim(ifm_dim_w, k_w, stride_w, 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( @@ -59,7 +47,7 @@ def execution_im2col( 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( @@ -106,20 +94,22 @@ def execution_im2col( # k_W | 2 | 2 | 2 | 2 | 3 | 1 | 1 | 1 | 1 | 1 | # stride_h | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 1 | 2 | 2 | # stride_w | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 1 | 2 | 2 | -# dilations | 1 | 2 | 2 | 2 | 2 | 1 | 2 | 2 | 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 | -# idt | INT8 | INT8 | -# ifm_dim_H | 5 | 5 | -# ifm_dim_W | 5 | 1 | -# ifm_ch | 2 | 2 | -# pad_amt | 1 | 1 | -# pad_val | 0 | 0 | -# k_H | 2 | 2 | -# k_W | 2 | 1 | -# stride_h | 1 | 2 | -# stride_w | 2 | 1 | -# dilations | 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 @@ -132,7 +122,8 @@ def test_im2col_dilations(): ifm_dim_W = 5 pad_amt = [0, 0, 0, 0] pad_val = 0 - dilation = 1 + dilation_h = 1 + dilation_w = 1 x = np.asarray( [ @@ -191,7 +182,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -209,7 +201,8 @@ def test_im2col_dilations(): ifm_dim_W = 5 pad_amt = [0, 0, 0, 0] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -259,7 +252,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -277,7 +271,8 @@ def test_im2col_dilations(): ifm_dim_W = 5 pad_amt = [1, 1, 1, 1] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -347,7 +342,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -365,7 +361,8 @@ def test_im2col_dilations(): ifm_dim_W = 5 pad_amt = [1, 1, 1, 1] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -415,7 +412,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -433,7 +431,8 @@ def test_im2col_dilations(): ifm_dim_W = 5 pad_amt = [1, 1, 1, 1] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -476,7 +475,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -494,7 +494,8 @@ def test_im2col_dilations(): 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]]]], @@ -518,7 +519,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -536,7 +538,8 @@ def test_im2col_dilations(): 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]]]], @@ -560,7 +563,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -578,7 +582,8 @@ def test_im2col_dilations(): 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]]]], @@ -610,7 +615,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -628,7 +634,8 @@ def test_im2col_dilations(): 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]]]], @@ -652,7 +659,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -670,7 +678,8 @@ def test_im2col_dilations(): 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]]]], @@ -694,7 +703,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -712,7 +722,8 @@ def test_im2col_dilations(): ifm_dim_W = 5 pad_amt = [1, 1, 1, 1] pad_val = 0 - dilation = 2 + dilation_h = 2 + dilation_w = 2 x = np.asarray( [ @@ -772,7 +783,8 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + dilation_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -790,7 +802,8 @@ def test_im2col_dilations(): 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]]]], @@ -814,7 +827,167 @@ def test_im2col_dilations(): ifm_dim_W, pad_amt, pad_val, - dilation, + 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_h, + dilation_w, ) assert (produced == expected).all(), "Test failed for case number {}".format( @@ -834,7 +1007,8 @@ def test_im2col_dilations(): # k_W | 2 | 2 | 2 | 2 | 2 | 2 | 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 | -# dilations | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +# 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 @@ -1453,7 +1627,7 @@ def test_im2col_infer_shapes(): 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/transformation/test_conv_lowering.py b/tests/transformation/test_conv_lowering.py index e0a4443..604acf7 100644 --- a/tests/transformation/test_conv_lowering.py +++ b/tests/transformation/test_conv_lowering.py @@ -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 @@ -122,7 +120,7 @@ def test_dws_reg_conv_lowering( k_w, stride_w, pad_w, - dilations[0], + dilations[1], ) # set up onnx model 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_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()) From ac0b86a63eb937b869bfa453a996a8a8b8506546 Mon Sep 17 00:00:00 2001 From: Felix Jentzsch <45395194+fpjentzsch@users.noreply.github.com> Date: Mon, 17 May 2021 18:13:10 +0100 Subject: [PATCH 76/85] Support infer_datatype for flatten layer (#30) * Support infer_datatype for flatten layer * [InferDT] add more identity op types for datatype inference * [Lint] fix linting issues Co-authored-by: Yaman Umuroglu --- src/finn/transformation/infer_datatypes.py | 9 ++++++++- tests/transformation/test_4d_conversion.py | 2 +- tests/transformation/test_batchnorm_to_affine.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) 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/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", From fea6f14729062c1f2e4b7cdc14349f59f7b15ecb Mon Sep 17 00:00:00 2001 From: Yaman Umuroglu Date: Fri, 4 Jun 2021 14:54:49 +0100 Subject: [PATCH 77/85] Update AUTHORS.rst --- AUTHORS.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 4832a4c..1120664 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,10 +2,11 @@ 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) From 42fa89a839acd30f65bf57158336cbd291d4c020 Mon Sep 17 00:00:00 2001 From: Yaman Umuroglu Date: Fri, 4 Jun 2021 16:06:13 +0100 Subject: [PATCH 78/85] Create python-publish.yml --- .github/workflows/python-publish.yml | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..3bfabfc --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,36 @@ +# 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 + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +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 build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} From c928353369f9f4c76e6bbd5c2d0f68c20763c44b Mon Sep 17 00:00:00 2001 From: Felix Jentzsch <45395194+fpjentzsch@users.noreply.github.com> Date: Fri, 4 Jun 2021 17:08:33 +0200 Subject: [PATCH 79/85] Add ZCU111 board to part map (#32) --- src/finn/util/basic.py | 2 ++ 1 file changed, 2 insertions(+) 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() From 7cb1196c8cdb9c2eb6010e05037bb2561033b90a Mon Sep 17 00:00:00 2001 From: Yaman Umuroglu Date: Fri, 4 Jun 2021 16:14:22 +0100 Subject: [PATCH 80/85] Update AUTHORS.rst --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1120664..14346eb 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,3 +10,4 @@ Contributors * Hendrik Borras (@HenniOVP) * Mirza Mrahorovic (@mmrahorovic) * Felix Paul Jentzsch (@felixpj) +* Jon Ander Lezeta (@jalezeta) From d4b80dd17f79295581f99f37ab9a7f1bd2c01878 Mon Sep 17 00:00:00 2001 From: Yaman Umuroglu Date: Sat, 5 Jun 2021 00:00:27 +0100 Subject: [PATCH 81/85] GHA for PyPI, sdist only (#35) * finn-base v0.0.2 (#34) * Modified set_nodeattr to allow using it on repeated fields (#18) * [base]: changed how the floats, ints, strings, tensors, graphs and sparse_tensors field of AttributeProto is set. * [Core] restrict attributes to tested types Co-authored-by: Yaman Umuroglu * Support for non-square input images and kernels for im2col node (#20) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [Im2Col] style fixes and comments Co-authored-by: Yaman Umuroglu * Update AUTHORS.rst * Support for non-square input images and kernels in LowerConvsToMatMul transformation (#16) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col] function compute_conv_output_dim can now be called in case non-equal and equal padding is assumed. [test_im2col] changed function call to compute_conv_output_dim. [test_conv_lowering] changed function call to compute_conv_output_dim. [lower_convs_to_matmul] removed old assertion. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [test_conv_lowering]: minor fix for test case depthwise and regular convolutions * Support for non-square input images and kernels for im2col node (#20) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [Im2Col] style fixes and comments Co-authored-by: Yaman Umuroglu * Update AUTHORS.rst Co-authored-by: Yaman Umuroglu * Added support for dilation value = 2 for 1D and 2D images/kernels (#17) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col] added support for dilations = 2 for 2D and 1D images. Dilation value must be equal along each axis. [test_im2col] added several test cases in case dilations = 2 (a.o. cases where image and kernel are 2D and 1D, with and without padding, and with stride = 2). [lower_convs_to_matmul] added support for dilation value. Dilation value must be equal along each axis. [test_conv_lowering] added test case for dilations = 2 for 2D and 1D images and kernels, with and without padding, and with stride = 2. * [lower_convs_to_matmul] removed old assertion [test_conv_lowering] added more dilation values to test cases. Dilation values of {1, 2, 3, 4} are tested. * [im2col] function compute_conv_output_dim can now be called in case non-equal and equal padding is assumed. [test_im2col] changed function call to compute_conv_output_dim [test_conv_lowering] changed function call to compute_conv_output_dim * [im2col] function compute_conv_output_dim can now be called in case non-equal and equal padding is assumed. [test_im2col] changed function call to compute_conv_output_dim. [test_conv_lowering] changed function call to compute_conv_output_dim. [lower_convs_to_matmul] removed old assertion. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [test_conv_lowering]: minor fix for test case depthwise and regular convolutions * [im2col]: minor style adjustment. [test_conv_lowering]: merged test functions into one test function. * Support for non-square input images and kernels for im2col node (#20) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [Im2Col] style fixes and comments Co-authored-by: Yaman Umuroglu * Update AUTHORS.rst * Support for non-square input images and kernels in LowerConvsToMatMul transformation (#16) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col] function compute_conv_output_dim can now be called in case non-equal and equal padding is assumed. [test_im2col] changed function call to compute_conv_output_dim. [test_conv_lowering] changed function call to compute_conv_output_dim. [lower_convs_to_matmul] removed old assertion. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [test_conv_lowering]: minor fix for test case depthwise and regular convolutions * Support for non-square input images and kernels for im2col node (#20) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [Im2Col] style fixes and comments Co-authored-by: Yaman Umuroglu * Update AUTHORS.rst Co-authored-by: Yaman Umuroglu Co-authored-by: Yaman Umuroglu * Update quantavgpool2d.py (#22) Add "Copyright (c) 2021 Xilinx, Inc" heather * [batchnorm_to_affine]: epsilon value is now read out from the attributes. (#21) [test_batchnorm_to_affine]: added a test case for various epsilon values. * Added 3D to 4D (tensor) transformation (#19) * [im2col.py]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [im2col]: support for non-square input images and kernels, [test_im2col]: added/modified several test cases for (non-)square images and kernels * [test_general_transformation]: changed kernel_size attribute to list instead of integer as required by im2col node * [base]: changed how the "ints" field of AttributeProto set. [test_general_transformation]: changed the type of the kernel_size attribute to list of integers * removed unused import * [base]: added support for writing repeated fields in AttributeProto * minor style changes * [im2col, test_im2col]: added support for non-equal padding * [lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding. [test_conv_lowering]: added/modified test cases for non-equal padding, depthwise convolution and 'standard' convolution. * [test_conv_lowering]: included 1D depthwise and regular convolutions in tests * Revert "[test_conv_lowering]: included 1D depthwise and regular convolutions in tests" This reverts commit 3ff449c42d709e640ca904c41a241bb94fc9e335 * Revert "[lower_convs_to_matmul]: added support for non-square input images and kernels and non-equal padding." This reverts commit 15e34ed8d07d4a55996f162bd3bd1aa24b33c3ac. * Revert "[im2col, test_im2col]: added support for non-equal padding" This reverts commit c524020ee8a7b363eb0c30d70cf21332e9c73678. * [im2col] added support for dilations = 2 for 2D and 1D images. Dilation value must be equal along each axis. [test_im2col] added several test cases in case dilations = 2 (a.o. cases where image and kernel are 2D and 1D, with and without padding, and with stride = 2). [lower_convs_to_matmul] added support for dilation value. Dilation value must be equal along each axis. [test_conv_lowering] added test case for dilations = 2 for 2D and 1D images and kernels, with and without padding, and with stride = 2. * [lower_convs_to_matmul] removed old assertion [test_conv_lowering] added more dilation values to test cases. Dilation values of {1, 2, 3, 4} are tested. * [im2col] function compute_conv_output_dim can now be called in case non-equal and equal padding is assumed. [test_im2col] changed function call to compute_conv_output_dim [test_conv_lowering] changed function call to compute_conv_output_dim * [im2col] function compute_conv_output_dim can now be called in case non-equal and equal padding is assumed. [test_im2col] changed function call to compute_conv_output_dim. [test_conv_lowering] changed function call to compute_conv_output_dim. [lower_convs_to_matmul] removed old assertion. * [im2col]: minor fix with assumption on kernel dimension [lower_convs_to_matmul]: minor fix with assumption on kernel dimension * [change_3d_tensors_to_4d]: added new transformation that transforms 3D tensors to 4D and changes the nodes accordingly [test_4d_conversion]: test function for 3D to 4D tensor transformation * [change_3d_tensors_to_4d]: added new transformation that changes 3D tensors to 4D. [test_4d_conversion]: added a test case for the 3D to 4D transformation. * [change_3d_tensors_to_4d]: added 3D to 4D transformation (for QuartzNet). [test_4d_conversion]: added test case for 3D to 4D transform. * [change_3d_tensors_to_4d]: changed how an invalid graph is handled. [test_4d_conversion]: changed the test case for an invalid graph. * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: changed how a square kernel is instantiated. [lower_convs_to_matmul]: changed how the kernel size attribute is read (based on how a square kernel is instantiated). * [im2col]: minor change in style. * [im2col]: minor style change and changed the way how a square kernel is instantiated. * [test_conv_lowering]: merged tests for depthwise and standard convolutions. * [test_conv_lowering]: minor fix for test case depthwise and regular convolutions * [im2col]: minor style adjustment. [test_conv_lowering]: merged test functions into one test function. * [change_3d_tensors_to_4d]: style fixes and comments. [test_4d_converions]: rearranged code. * [Transform] check invalid node list length * [change_3d_tensors_to_4d]: rearranged the code to make it more readable. Co-authored-by: Yaman Umuroglu * [Docs] update tutorials * [Util] experimental: fast_mode data packing for binary (#24) * Generic partitioning feature (#23) * Add basic partitioning functionality * Mount build dir within docker container * Support for non-linear models and multi in/out partitions * Remove dataflowpartition custom op from finn-base * Fix temporary build dir for CI * Fix docstring * [create_generic_partitions]: minor modification, removed redundant output value_info entries. (#26) * [extend_partition]: added a new transformation ExtendPartition. (#27) [test_extend_partition]: added a test case for the new transformation. * Added support for non-equal strides along different axes (#25) * [im2col]: added support for non-equal strides along different axes and cleaned up the code. [lower_convs_to_matmul]: added support for non-equal strides along different axes and cleaned up the code. [test_conv_lowering]: added test case for non-equal strides along different axes. * [im2col]: minor fix. [test_im2col]: added test case for non-equal strides along different axes. * Changes for supporting vitis_hls (#28) * [Refactor] split up RTL/HLS-related utils * [Util] rename to CallHLS and allow specifying vivado_hls/vitis_hls * [Util] more flexible stream naming in rtlsim_multi_io * Changes for supporting non-equal dilation (#29) * added support for non-equal dilation value along (H, W) dimension * added test cases for non-equal dilation configurations * appending dilation value along dummy dimension correctly (i.e. with a '1') * changed tensor sparsity annotation for consistency * Support infer_datatype for flatten layer (#30) * Support infer_datatype for flatten layer * [InferDT] add more identity op types for datatype inference * [Lint] fix linting issues Co-authored-by: Yaman Umuroglu * Update AUTHORS.rst * Create python-publish.yml * Add ZCU111 board to part map (#32) * Update AUTHORS.rst Co-authored-by: Mirza Mrahorovic <34712307+mmrahorovic@users.noreply.github.com> Co-authored-by: jalezeta <51440887+jalezeta@users.noreply.github.com> Co-authored-by: Felix Jentzsch <45395194+fpjentzsch@users.noreply.github.com> * Update python-publish.yml * Update python-publish.yml * Lint Co-authored-by: Mirza Mrahorovic <34712307+mmrahorovic@users.noreply.github.com> Co-authored-by: jalezeta <51440887+jalezeta@users.noreply.github.com> Co-authored-by: Felix Jentzsch <45395194+fpjentzsch@users.noreply.github.com> --- .github/workflows/python-publish.yml | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 3bfabfc..9087804 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,16 +1,11 @@ # 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 -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Upload Python Package on: release: - types: [published] + types: [created] jobs: deploy: @@ -26,11 +21,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + 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/* From 8bcf791d2ecdbdd9ac9edd024a17ca588fe877d9 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 2 Jul 2021 08:24:27 +0100 Subject: [PATCH 82/85] Delete onnx file generation --- .../logicnets-initial-model/after-datatypes.onnx | Bin 1600 -> 0 bytes tests/logicnets-initial-model/after-shape.onnx | Bin 1560 -> 0 bytes .../after-uniquenames.onnx | Bin 1735 -> 0 bytes tests/logicnets-initial-model/after_wrap.onnx | Bin 1090 -> 0 bytes tests/logicnets-initial-model/custom_model.py | 2 -- tests/logicnets-initial-model/simple-model.onnx | Bin 1090 -> 0 bytes tests/transformation/test_logicnets_verilog.py | 5 ----- 7 files changed, 7 deletions(-) delete mode 100644 tests/logicnets-initial-model/after-datatypes.onnx delete mode 100644 tests/logicnets-initial-model/after-shape.onnx delete mode 100644 tests/logicnets-initial-model/after-uniquenames.onnx delete mode 100644 tests/logicnets-initial-model/after_wrap.onnx delete mode 100644 tests/logicnets-initial-model/simple-model.onnx diff --git a/tests/logicnets-initial-model/after-datatypes.onnx b/tests/logicnets-initial-model/after-datatypes.onnx deleted file mode 100644 index f743eb899e5e9c4b07f4e6a4ad186ed56ca6ffd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1600 zcmcIjO;5r=6tsmvUC`(r5HD&ldca6%y>SDh#)M#E@SvAwDXXlZ+oroD@Nc;KFWr8m zz6vEC6|)tc)wBd7jok9rB-O}qcY?6h)Ge`mzv(Gy)({R&P+(pwLQP} zK{*Y`G`2I>+L_z3v)0;KTd=!BAU$w4!P!a^_GYqCx{cCqR#1`5nW*om@X%FCR}^G0 zK4uOTZCnvP2?Vtj+HRB{h@8+MhipQ!nUXyOH9#)KNb~>u&KSbpL&GgJjMR|e8HxIx+m)5~lWxvm-B2HSJ6GOJ=QkAvZj6(~}=KYt*t nvrCjtlg(OHNlJ+c>l6~gWtw`pCH3FsmhbydUG?Ukeo*}eV0Xn~ diff --git a/tests/logicnets-initial-model/after-shape.onnx b/tests/logicnets-initial-model/after-shape.onnx deleted file mode 100644 index 4cb8bdcff4af2fc3cb4378cb5836a5a4f3e1bcfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1560 zcmcIjK~KUk7Jji7!qe2PmmbE18=s)47TDE0; z3ygS>Tl2ox_qzAK8D(4*%=PE=(e=i3-ubB_7Zn(9Bf}v9wFQ+JIUaY2v=!Po>oQJ) za1boybU;RP+N_~tx}=W1zTYVMp`3brr4^c|sLZ%MVp7!gm8N%U-;DE?vk=mAZO?B_ zD5n9L$9CpUJM&O>)=oR?0CukkqzBF>I9qAL-aNr z$IPLkjVr<@fuOcR+l|s2ky9GvkWEN7Q?iGk2FRrtY5srT8AI6n+VB94W9)|ZyEkD@ zpGt8Pt@-dg+Hs7KVQ9FBbVH9Ja~o3a09Fkow80BQg{P&g@p7WN!DHzJA7K>b3$(Qa zgHO5UmXYcS)*b50r3dE289p)X3{3m)t_7Mo5TA5j`}Q)nc|`BUoGBMGD6# zKax-ACw7f3XACy#A>2l9-n@C9nYB~LdrHj18UH>HCVY1FcON}ls3N3`9G^v;Dy}i| z1L-qO4cf*%Az8E>MGHM0vGI&kw{x(Lb(`oTf8sukMLV@{si%Q_wJP0Ds4gTO3$0rA zt7Ts`{|G71{DsoNoCe|91D8Bvv(y7A^gs$ckezw(3O#rQ9=siRxJ6+0ASNL8vi$@L z-FAK?yK+0VBIMvjmZ;dABUh6-7vI5nu-c5axS^2ltqmP93G z1eTGkjAUgm7J|ZHl+{(E&0{*>!4UH<;8Ddciq7fc{as)@U(!MFAp9ZM>f7qI-5;)A z8b-)*EL=slV#6D+o=B(o7^Br9fVP!l(1P{Y%Ff38 z10Xw>DX)RNuJ>C zd9OTwlKc8n8A#jG4brwWJ?gLfS1EO1%k*bI=GE$)~RVOBv$OIRR5<7~G1b@TcpR5yT`cPra zX5Tx%JDtw7#X3f&St4Fjy%Ndfn?ee{R3o4<&qZW}We{r>a~nA{=uV~L`C*>#>}JlF zi5T?YUhG9274L&)cCZ_*j-EewfRXI3*uM{!8!t+0qO@jhv+=6I^;|Q&9|~ogElny zUYh=$s|UPPbVmUy)k?;~j2MnDXufm15%~>rh{@qEnX&W`edx7}KZP~VWz83lWycu+ zr5?gLfS1EO1%k*bI=GE$)~RVOBv$OIRR5<7~G1b@TcpR5yT`cPra zX5Tx%JDtw7#X3f&St4Fjy%Ndfn?ee{R3o4<&qZW}We{r>a~nA{=uV~L`C*>#>}JlF zi5T?YUhG9274L&)cCZ_*j-EewfRXI3*uM{!8!t+0qO@jhv+=6I^;|Q&9|~ogElny zUYh=$s|UPPbVmUy)k?;~j2MnDXufm15%~>rh{@qEnX&W`edx7}KZP~VWz83lWycu+ zr5 Date: Fri, 2 Jul 2021 09:28:03 +0100 Subject: [PATCH 83/85] Remove non-used package --- tests/logicnets-initial-model/custom_model.py | 1 - tests/transformation/test_logicnets_verilog.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/logicnets-initial-model/custom_model.py b/tests/logicnets-initial-model/custom_model.py index 123ab30..2ef25e1 100644 --- a/tests/logicnets-initial-model/custom_model.py +++ b/tests/logicnets-initial-model/custom_model.py @@ -1,5 +1,4 @@ import numpy as np -import onnx import onnx.helper as helper from onnx import TensorProto diff --git a/tests/transformation/test_logicnets_verilog.py b/tests/transformation/test_logicnets_verilog.py index f968eda..cc3d248 100644 --- a/tests/transformation/test_logicnets_verilog.py +++ b/tests/transformation/test_logicnets_verilog.py @@ -1,5 +1,4 @@ import numpy as np -import onnx import onnx.helper as helper from onnx import TensorProto from pyverilator import PyVerilator @@ -246,11 +245,11 @@ def expected_output(input_data, indices0, indices1, care_set0, care_set1, in_bit 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("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) From b90a44dd3bfb79d4c61e283b1c7e4580d9e639a6 Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 2 Jul 2021 09:41:48 +0100 Subject: [PATCH 84/85] Solve linting problems --- src/finn/custom_op/logicnets/binary_truthtable.py | 2 +- src/finn/custom_op/logicnets/truthtable.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index cb11943..0fa4f5a 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -77,7 +77,7 @@ def binary_truthtable(input, care_set, bits): class BinaryTruthTable(CustomOp): - """The class corresponing to the Binary TruthTable function. """ + """The class corresponing to the Binary TruthTable function.""" def get_nodeattr_types(self): return { diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 7305081..4f83bae 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -94,7 +94,7 @@ def _truthtable(input, care_set, results, in_bits): class TruthTable(CustomOp): - """The class corresponing to the Binary TruthTable function. """ + """The class corresponing to the TruthTable function.""" def get_nodeattr_types(self): return { From 14051a23670834ce41fd19a7134968648d2d112b Mon Sep 17 00:00:00 2001 From: Jon Ander Lezeta Date: Fri, 2 Jul 2021 13:04:47 +0100 Subject: [PATCH 85/85] Fix docstring issues --- src/finn/custom_op/logicnets/binary_truthtable.py | 4 +--- src/finn/custom_op/logicnets/truthtable.py | 5 ++--- .../transformation/logicnets/gen_logicnets_verilog.py | 8 ++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/finn/custom_op/logicnets/binary_truthtable.py b/src/finn/custom_op/logicnets/binary_truthtable.py index 0fa4f5a..6654339 100644 --- a/src/finn/custom_op/logicnets/binary_truthtable.py +++ b/src/finn/custom_op/logicnets/binary_truthtable.py @@ -46,9 +46,8 @@ def binary_truthtable(input, care_set, bits): 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: @@ -56,7 +55,6 @@ def binary_truthtable(input, care_set, bits): care_set = [1, 2] Possible combinations[2:0]: A B C | Results - --------------------- 0 0 0 | 0 0 0 1 | 1 0 1 0 | 1 diff --git a/src/finn/custom_op/logicnets/truthtable.py b/src/finn/custom_op/logicnets/truthtable.py index 4f83bae..9d71d09 100644 --- a/src/finn/custom_op/logicnets/truthtable.py +++ b/src/finn/custom_op/logicnets/truthtable.py @@ -48,9 +48,9 @@ def _truthtable(input, care_set, results, in_bits): 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 @@ -70,7 +70,6 @@ def _truthtable(input, care_set, results, in_bits): 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 diff --git a/src/finn/transformation/logicnets/gen_logicnets_verilog.py b/src/finn/transformation/logicnets/gen_logicnets_verilog.py index 64a6776..66d102b 100644 --- a/src/finn/transformation/logicnets/gen_logicnets_verilog.py +++ b/src/finn/transformation/logicnets/gen_logicnets_verilog.py @@ -103,7 +103,7 @@ def _generate_verilog(model, indices): 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 " + "the keyword input on the TensorName, and has to be the only one " "following that rule in the entire ONNX model.\n" ) @@ -242,17 +242,17 @@ class GenLogicNetsVerilog(Transformation): can be accessed using the tensor name that is considered in the ONNX model. - - The care_set represents which input combinations are + -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 + -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 + -The code_dir is used to return the path of the generated verilog source code.""" def __init__(self, care_set, indices):