diff --git a/src/finn/analysis/verify_custom_nodes.py b/src/finn/analysis/verify_custom_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..73d7ae590ac69226516ee1bf308ca6c2fbb41ce1 --- /dev/null +++ b/src/finn/analysis/verify_custom_nodes.py @@ -0,0 +1,16 @@ +import finn.custom_op.registry as registry + + +def verify_nodes(model): + """Checks if custom ops in graph are correctly built, with all attributes + and inputs. Returns {node op_type : info_messages} + *info_messages is list of strings about the result of the verification""" + + verification_dict = {} + for node in model.graph.node: + if node.domain == "finn": + op_type = node.op_type + inst = registry.custom_op[op_type](node) + verification_dict[op_type] = inst.verify_node() + + return verification_dict diff --git a/src/finn/custom_op/__init__.py b/src/finn/custom_op/__init__.py index 519d48e97075c6e6be57d187720269ac08312028..797916cf20ce9c61cc349df41383b687ed0c101b 100644 --- a/src/finn/custom_op/__init__.py +++ b/src/finn/custom_op/__init__.py @@ -78,3 +78,10 @@ class CustomOp(ABC): """Execute this CustomOp instance, given the execution context and ONNX graph.""" pass + + @abstractmethod + def verify_node(self): + """Verifies that all attributes the node needs are there and + that particular attributes are set correctly. Also checks if + the number of inputs is equal to the expected number""" + pass diff --git a/src/finn/custom_op/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py index 0160c5f02381b1af55182f168a0df7b202635266..6a37519d372687fbd21fea2c326f55531b6f1eea 100644 --- a/src/finn/custom_op/fpgadataflow/__init__.py +++ b/src/finn/custom_op/fpgadataflow/__init__.py @@ -40,7 +40,11 @@ class HLSCustomOp(CustomOp): self.code_gen_dict = {} def get_nodeattr_types(self): - return {"code_gen_dir": ("s", False, ""), "executable_path": ("s", False, "")} + return { + "backend": ("s", True, "fpgadataflow"), + "code_gen_dir": ("s", False, ""), + "executable_path": ("s", False, ""), + } def code_generation(self, model): node = self.onnx_node diff --git a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py index ce5a0fb3a18eb16952f98362ffffa0e0adf7e3e4..975da666f0ba728f542b06865aaa2c66c5f07c07 100644 --- a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py +++ b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py @@ -14,6 +14,9 @@ class StreamingFCLayer_Batch(HLSCustomOp): def get_nodeattr_types(self): my_attrs = { + # "backend": ("s", True, "fpgadataflow"), + # "code_gen_dir": ("s", True, ""), + # "executable_path": ("s", True, ""), "PE": ("i", True, 0), "SIMD": ("i", True, 0), "MW": ("i", True, 0), @@ -57,6 +60,90 @@ class StreamingFCLayer_Batch(HLSCustomOp): def infer_node_datatype(self, model): pass + def verify_node(self): + info_messages = [] + + # verify number of attributes + num_of_attr = 14 + 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 "domain" is set to "finn" + domain_value = self.onnx_node.domain + if domain_value == "finn": + info_messages.append("Attribute domain is set correctly") + else: + info_messages.append('Attribute domain should be set to "finn"') + + # verify that "backend" is set to "fpgadataflow" + backend_value = self.get_nodeattr("backend") + if backend_value == "fpgadataflow": + info_messages.append("Attribute backend is set correctly") + else: + info_messages.append('Attribute backend should be set to "fpgadataflow"') + + # verify that all necessary attributes exist + try: + self.get_nodeattr("code_gen_dir") + self.get_nodeattr("executable_path") + self.get_nodeattr("resType") + self.get_nodeattr("MW") + self.get_nodeattr("MH") + self.get_nodeattr("SIMD") + self.get_nodeattr("PE") + self.get_nodeattr("inputDataType") + self.get_nodeattr("weightDataType") + self.get_nodeattr("outputDataType") + self.get_nodeattr("ActVal") + self.get_nodeattr("binaryXnorMode") + self.get_nodeattr("noActivation") + info_messages.append("All necessary attributes exist") + except Exception: + info_messages.append( + """The necessary attributes do not exist. + StreamingFCLayer_Batch needs the following attributes: + code_gen_dir, executable_path, resType, MW, MH, SIMD, PE, + inputDataType, weightDataType, outputDataType, ActVal, + binaryXnorMode, noActivation""" + ) + + # verify the number of inputs depending on noActivation value + # check noActivation value to determine the number of inputs + no_act = self.get_nodeattr("noActivation") + + if no_act == 1: + if len(self.onnx_node.input) == 2: + info_messages.append("The number of inputs is correct") + else: + info_messages.append( + """StreamingFCLayer_Batch needs in no + activation mode 2 inputs (data input and weights)""" + ) + elif no_act == 0: + if len(self.onnx_node.input) == 3: + info_messages.append("The number of inputs is correct") + else: + info_messages.append( + """StreamingFCLayer_Batch needs 3 inputs + (data input and weights and threshold values)""" + ) + else: + info_messages.append( + """noActivation attribute contains {} should + be 0 or 1""".format( + no_act + ) + ) + + return info_messages + def get_input_datatype(self): return DataType[self.get_nodeattr("inputDataType")] diff --git a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py index 7be3b6cbc74713662713bd422ce79610c804a0e1..92f499b6771efe4455e7259a8eb62ab9c636cb1f 100644 --- a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py +++ b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py @@ -4,6 +4,9 @@ from finn.custom_op.fpgadataflow import HLSCustomOp class StreamingMaxPool_Batch(HLSCustomOp): def get_nodeattr_types(self): my_attrs = { + # "backend": ("s", True, "fpgadataflow"), + # "code_gen_dir": ("s", True, ""), + # "executable_path": ("s", True, ""), "ImgDim": ("i", True, 0), "PoolDim": ("i", True, 0), "NumChannels": ("i", True, 0), @@ -17,6 +20,58 @@ class StreamingMaxPool_Batch(HLSCustomOp): def infer_node_datatype(self, model): pass + def verify_node(self): + info_messages = [] + + # verify number of attributes + num_of_attr = 6 + 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 "domain" is set to "finn" + domain_value = self.onnx_node.domain + if domain_value == "finn": + info_messages.append("Attribute domain is set correctly") + else: + info_messages.append('Attribute domain should be set to "finn"') + + # verify that "backend" is set to "fpgadataflow" + backend_value = self.get_nodeattr("backend") + if backend_value == "fpgadataflow": + info_messages.append("Attribute backend is set correctly") + else: + info_messages.append('Attribute backend should be set to "fpgadataflow"') + + # verify that all necessary attributes exist + try: + self.get_nodeattr("code_gen_dir") + self.get_nodeattr("executable_path") + self.get_nodeattr("ImgDim") + self.get_nodeattr("PoolDim") + self.get_nodeattr("NumChannels") + info_messages.append("All necessary attributes exist") + except Exception: + info_messages.append( + """The necessary attributes do not exist. + StreamingMaxPool_Batch needs the following attributes: + code_gen_dir, executable_path, ImgDim, PoolDim, NumChannels""" + ) + + # verify the number of inputs + if len(self.onnx_node.input) == 1: + info_messages.append("The number of inputs is correct") + else: + info_messages.append("""StreamingMaxPool_Batch needs 1 data input""") + + return info_messages + def global_includes(self): self.code_gen_dict["$GLOBALS$"] = ['#include "maxpool.h"'] diff --git a/src/finn/custom_op/multithreshold.py b/src/finn/custom_op/multithreshold.py index 3cff87b3fa850a7bb35c39d028d359d3513cc80e..52cf2504b174b06df1ba0aa0bdac112fee872b91 100644 --- a/src/finn/custom_op/multithreshold.py +++ b/src/finn/custom_op/multithreshold.py @@ -85,3 +85,49 @@ class MultiThreshold(CustomOp): output = multithreshold(v, thresholds, out_scale, out_bias) # setting context according to output context[node.output[0]] = output + + def verify_node(self): + info_messages = [] + + # verify number of attributes + num_of_attr = 3 + 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 "domain" is set to "finn" + domain_value = self.onnx_node.domain + if domain_value == "finn": + info_messages.append("Attribute domain is set correctly") + else: + info_messages.append('Attribute domain should be set to "finn"') + + # verify that all necessary attributes exist + try: + self.get_nodeattr("out_scale") + self.get_nodeattr("out_bias") + self.get_nodeattr("out_dtype") + info_messages.append("All necessary attributes exist") + except Exception: + info_messages.append( + """The necessary attributes do not exist. + MultiThreshold needs the following attributes: + out_scale, out_bias, out_dtype""" + ) + + # 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( + """MultiThreshold needs 2 inputs + (data input and threshold values)""" + ) + + return info_messages diff --git a/src/finn/custom_op/xnorpopcount.py b/src/finn/custom_op/xnorpopcount.py index 8af7429a5d54d675e62b527cae6e2fdf55b09536..15ec57a002148a4e9213d28fe4f68c5a78837ab6 100644 --- a/src/finn/custom_op/xnorpopcount.py +++ b/src/finn/custom_op/xnorpopcount.py @@ -57,3 +57,36 @@ class XnorPopcountMatMul(CustomOp): output = xnorpopcountmatmul(inp0, inp1) # set context according to output name context[node.output[0]] = output + + def verify_node(self): + info_messages = [] + + # verify number of attributes + num_of_attr = 0 + 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 "domain" is set to "finn" + domain_value = self.onnx_node.domain + if domain_value == "finn": + info_messages.append("Attribute domain is set correctly") + else: + info_messages.append('Attribute domain should be set to "finn"') + + # verify that all necessary attributes exist + info_messages.append("XnorPopcountMatMul should not have any 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("XnorPopcountMatMul needs 2 data inputs") + + return info_messages diff --git a/tests/test_verify_custom_nodes.py b/tests/test_verify_custom_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..9146f9d30853e6c7a47436c100166a4fa9127e87 --- /dev/null +++ b/tests/test_verify_custom_nodes.py @@ -0,0 +1,156 @@ +from onnx import TensorProto, helper + +from finn.analysis.verify_custom_nodes import verify_nodes +from finn.core.modelwrapper import ModelWrapper + + +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 test_verify_custom_nodes(): + inp = helper.make_tensor_value_info("inp", TensorProto.FLOAT, [1, 13, 64]) + outp = helper.make_tensor_value_info("outp", TensorProto.FLOAT, [1, 1, 64]) + + # MultiThreshold + m_node = helper.make_node( + "MultiThreshold", + ["xnor_out", "threshs"], + ["outp"], + domain="finn", + out_scale=2.0, + out_bias=-1.0, + out_dtype="", + ) + + # XnorPopcountMatMul + xnor_node = helper.make_node( + "XnorPopcountMatMul", + ["fclayer_out0", "fclayer_out1"], + ["xnor_out"], + domain="finn", + ) + + # StreamingMaxPool_Batch + MaxPool_batch_node = helper.make_node( + "StreamingMaxPool_Batch", + ["inp"], + ["max_out"], + domain="finn", + backend="fpgadataflow", + code_gen_dir="", + executable_path="", + ImgDim=4, + PoolDim=2, + NumChannels=2, + ) + + # StreamingFCLayer_Batch - no activation + FCLayer0_node = helper.make_node( + "StreamingFCLayer_Batch", + ["max_out", "weights"], + ["fclayer_out0"], + domain="finn", + backend="fpgadataflow", + code_gen_dir="", + executable_path="", + resType="ap_resource_lut()", + MW=8, + MH=8, + SIMD=4, + PE=4, + inputDataType="<FINN DataType>", + weightDataType="<FINN DataType>", + outputDataType="<FINN DataType>", + ActVal=0, + binaryXnorMode=1, + noActivation=1, + ) + + # StreamingFCLayer_Batch - with activation + FCLayer1_node = helper.make_node( + "StreamingFCLayer_Batch", + ["fclayer_out0", "weights", "threshs"], + ["fclayer_out1"], + domain="finn", + backend="fpgadataflow", + code_gen_dir="", + executable_path="", + resType="ap_resource_lut()", + MW=8, + MH=8, + SIMD=4, + PE=4, + inputDataType="<FINN DataType>", + weightDataType="<FINN DataType>", + outputDataType="<FINN DataType>", + ActVal=0, + binaryXnorMode=1, + noActivation=0, + ) + + graph = helper.make_graph( + nodes=[MaxPool_batch_node, FCLayer0_node, FCLayer1_node, xnor_node, m_node], + name="custom_op_graph", + inputs=[inp], + outputs=[outp], + value_info=[ + helper.make_tensor_value_info("max_out", TensorProto.FLOAT, [1, 13, 64]), + helper.make_tensor_value_info("weights", TensorProto.FLOAT, [64, 32, 416]), + helper.make_tensor_value_info("threshs", TensorProto.FLOAT, [32, 32, 16]), + helper.make_tensor_value_info("xnor_out", TensorProto.FLOAT, [1, 32, 32]), + helper.make_tensor_value_info( + "fclayer_out0", TensorProto.FLOAT, [1, 32, 32] + ), + helper.make_tensor_value_info( + "fclayer_out1", TensorProto.FLOAT, [32, 64, 512] + ), + ], + ) + model = helper.make_model(graph, producer_name="custom-op-model") + model = ModelWrapper(model) + + produced = model.analysis(verify_nodes) + + expected = { + "StreamingMaxPool_Batch": [ + "The number of attributes is correct", + "Attribute domain is set correctly", + "Attribute backend is set correctly", + "All necessary attributes exist", + "The number of inputs is correct", + ], + "StreamingFCLayer_Batch": [ + "The number of attributes is correct", + "Attribute domain is set correctly", + "Attribute backend is set correctly", + "All necessary attributes exist", + "The number of inputs is correct", + ], + "XnorPopcountMatMul": [ + "The number of attributes is correct", + "Attribute domain is set correctly", + "XnorPopcountMatMul should not have any attributes", + "The number of inputs is correct", + ], + "MultiThreshold": [ + "The number of attributes is correct", + "Attribute domain is set correctly", + "All necessary attributes exist", + "The number of inputs is correct", + ], + } + + assert check_two_dict_for_equality( + produced, expected + ), """The produced output of + the verification analysis pass is not equal to the expected one"""