diff --git a/src/finn/core/modelwrapper.py b/src/finn/core/modelwrapper.py index 1d5d3a978948998dce95d74ee780dfb998376465..473a33c47885a9e21e0c331233b85fb265933456 100644 --- a/src/finn/core/modelwrapper.py +++ b/src/finn/core/modelwrapper.py @@ -115,6 +115,19 @@ class ModelWrapper: qa.quant_parameter_tensor_names.append(dt) qnt_annotations.append(qa) + def get_tensor_valueinfo(self, tensor_name): + """Returns ValueInfoProto of tensor with given name, if it has one.""" + graph = self._model_proto.graph + vi_names = [(x.name, x) for x in graph.input] + vi_names += [(x.name, x) for x in graph.output] + vi_names += [(x.name, x) for x in graph.value_info] + try: + vi_ind = [x[0] for x in vi_names].index(tensor_name) + vi = vi_names[vi_ind][1] + return vi + except ValueError: + return None + def get_tensor_shape(self, tensor_name): """Returns the shape of tensor with given name, if it has ValueInfoProto.""" graph = self._model_proto.graph diff --git a/src/finn/custom_op/registry.py b/src/finn/custom_op/registry.py index bd1605c96d8bb7a0687686b48912bd3b61a8f6cc..c38a28dae46f6e39ed45e58647385b79fe271bcc 100644 --- a/src/finn/custom_op/registry.py +++ b/src/finn/custom_op/registry.py @@ -7,6 +7,7 @@ from finn.custom_op.fpgadataflow.streamingfclayer_batch import StreamingFCLayer_ from finn.custom_op.fpgadataflow.streamingmaxpool_batch import StreamingMaxPool_Batch from finn.custom_op.fpgadataflow.tlastmarker import TLastMarker from finn.custom_op.multithreshold import MultiThreshold +from finn.custom_op.streamingdataflowpartition import StreamingDataflowPartition from finn.custom_op.xnorpopcount import XnorPopcountMatMul # create a mapping of all known CustomOp names and classes @@ -18,6 +19,7 @@ custom_op["StreamingMaxPool_Batch"] = StreamingMaxPool_Batch custom_op["StreamingFCLayer_Batch"] = StreamingFCLayer_Batch custom_op["ConvolutionInputGenerator"] = ConvolutionInputGenerator custom_op["TLastMarker"] = TLastMarker +custom_op["StreamingDataflowPartition"] = StreamingDataflowPartition def getCustomOp(node): diff --git a/src/finn/custom_op/streamingdataflowpartition.py b/src/finn/custom_op/streamingdataflowpartition.py new file mode 100644 index 0000000000000000000000000000000000000000..856ba6d5604c37440985648bb91b18fffeaf79fd --- /dev/null +++ b/src/finn/custom_op/streamingdataflowpartition.py @@ -0,0 +1,65 @@ +from finn.custom_op import CustomOp + +# note that the StreamingDataflowPartition node is only a meta/container node, +# it does not produce any HLS or bitfile by itself. it's a placeholder for +# a group of fpgadataflow nodes that have been separated out into a FINN-ONNX +# model of its own. + + +class StreamingDataflowPartition(CustomOp): + def get_nodeattr_types(self): + return { + "model": ("s", True, ""), + } + + def make_shape_compatible_op(self): + pass + + def infer_node_datatype(self, model): + pass + + def execute_node(self, context, graph): + # TODO add RPC execution with synthesized bitfile? + # whole-design rtlsim with PyVerilator may also be an alternative + pass + + def verify_node(self): + info_messages = [] + + # verify number of attributes + num_of_attr = 1 + 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("model") + info_messages.append("All necessary attributes exist") + except Exception: + info_messages.append( + """The necessary attributes do not exist. + StreamingDataflowPartition needs the following attribute(s): + model""" + ) + + # 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("StreamingDataflowPartition needs 1 data input") + + return info_messages diff --git a/src/finn/data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx b/src/finn/data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx new file mode 100644 index 0000000000000000000000000000000000000000..aada6f07e9d3910122d2eb357d8a8c1224e9fbab Binary files /dev/null and b/src/finn/data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx differ diff --git a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py new file mode 100644 index 0000000000000000000000000000000000000000..af43b7ea55c30155ff0ff326d2a26942ab69bad2 --- /dev/null +++ b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py @@ -0,0 +1,74 @@ +import copy + +from onnx import helper + +from finn.transformation import Transformation +from finn.util.basic import get_by_name, make_build_dir + + +class CreateDataflowPartition(Transformation): + """Split a graph into two graphs; one which contains non-FINN-dataflow nodes + and a StreamingDataflowPartition node, and another which only contains + FINN dataflow nodes. The StreamingDataflowPartition has a model attribute + that indicates the filename for the second graph that only contains + dataflow nodes. No action is taken if there are no dataflow nodes.""" + + def __init__(self): + super().__init__() + + def apply(self, model): + # TODO we currently assume that all dataflow nodes are connected to + # each other, forming a single partition. check the assumption and/or + # improve this. + all_nodes = list(model.graph.node) + df_nodes = filter( + lambda x: get_by_name(x.attribute, "backend") is not None, all_nodes + ) + df_nodes = filter( + lambda x: get_by_name(x.attribute, "backend").s.decode("UTF-8") + == "fpgadataflow", + df_nodes, + ) + df_nodes = list(df_nodes) + non_df_nodes = filter(lambda x: x not in df_nodes, all_nodes) + non_df_nodes = list(non_df_nodes) + + if len(df_nodes) == 0: + # no changes if no dataflow nodes are present + return (model, False) + else: + # partition the model into two models + df_model = copy.deepcopy(model) + non_df_model = model + # remove all non-dataflow nodes from the dataflow model + for node_to_remove in non_df_nodes: + df_model.graph.node.remove(node_to_remove) + # identify the entry and exit points for the dataflow part + df_in = df_model.graph.node[0].input[0] + df_out = df_model.graph.node[-1].output[0] + df_in_vi = df_model.get_tensor_valueinfo(df_in) + df_out_vi = df_model.get_tensor_valueinfo(df_out) + # set df graph in/out to be df_in/df_out + df_model.graph.input.remove(df_model.graph.input[0]) + df_model.graph.input.insert(0, df_in_vi) + df_model.graph.output.remove(df_model.graph.output[0]) + df_model.graph.output.insert(0, df_out_vi) + df_model_dir = make_build_dir("dataflow_partition_") + df_model_filename = df_model_dir + "/df_model.onnx" + df_model.save(df_model_filename) + # remove all dataflow nodes from the non-dataflow model + # keep track of where the dataflow part starts + df_start_ind = all_nodes.index(df_nodes[0]) + for node_to_remove in df_nodes: + non_df_model.graph.node.remove(node_to_remove) + # create StreamingDataflow node with df_in/df_out io + df_node = helper.make_node( + "StreamingDataflowPartition", + [df_in], + [df_out], + # use the model attribute to mark the df model + model=df_model_filename, + ) + non_df_model.graph.node.insert(df_start_ind, df_node) + + return (non_df_model, False) diff --git a/tests/fpgadataflow/test_create_dataflow_partition.py b/tests/fpgadataflow/test_create_dataflow_partition.py new file mode 100644 index 0000000000000000000000000000000000000000..e0b8b491b0ed926a56331a8e125f3de4ecd91615 --- /dev/null +++ b/tests/fpgadataflow/test_create_dataflow_partition.py @@ -0,0 +1,21 @@ +import os.path +from pkgutil import get_data + +from finn.core.modelwrapper import ModelWrapper +from finn.custom_op.registry import getCustomOp +from finn.transformation.fpgadataflow.create_dataflow_partition import ( + CreateDataflowPartition, +) + + +def test_create_dataflow_partition(): + # load the onnx model + raw_m = get_data( + "finn", "data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx" + ) + model = ModelWrapper(raw_m) + model = model.transform(CreateDataflowPartition()) + assert model.graph.node[2].op_type == "StreamingDataflowPartition" + sdp_node = getCustomOp(model.graph.node[2]) + assert sdp_node.__class__.__name__ == "StreamingDataflowPartition" + assert os.path.isfile(sdp_node.get_nodeattr("model"))