diff --git a/docker/quicktest.sh b/docker/quicktest.sh index 02e014cd3cc7bb88eebd02f03ff599913079152b..b06feccdc578a59c8ef00531871e1211c2a407e5 100755 --- a/docker/quicktest.sh +++ b/docker/quicktest.sh @@ -16,6 +16,11 @@ elif [ $1 = "rtlsim" ]; then elif [ $1 = "end2end" ]; then echo "Running end2end test suite with no parallelism" python setup.py test --addopts "-k end2end" +elif [ $1 = "full" ]; then + echo "Running full test suite, each step with appropriate parallelism" + $0 main; + $0 rtlsim; + $0 end2end; else echo "Unrecognized argument to quicktest.sh" fi diff --git a/src/finn/core/modelwrapper.py b/src/finn/core/modelwrapper.py index 646add188c5d475cf37ccd33cf24d29d61754ae1..98b234592ebe0c704fafd1eed980325d8566e7e2 100644 --- a/src/finn/core/modelwrapper.py +++ b/src/finn/core/modelwrapper.py @@ -36,6 +36,11 @@ from onnx import TensorProto import finn.util.basic as util import finn.util.onnx as onnxutil from finn.core.datatype import DataType +from finn.transformation.general import ( + RemoveUnusedTensors, + RemoveStaticGraphInputs, + SortGraph, +) class ModelWrapper: @@ -87,7 +92,7 @@ class ModelWrapper: """Runs given anaylsis_fxn on this model and return resulting dict.""" return analysis_fxn(self) - def transform(self, transformation, make_deepcopy=True): + def transform(self, transformation, make_deepcopy=True, cleanup=True): """Applies given Transformation repeatedly until no more changes can be made and returns a transformed ModelWrapper instance. @@ -101,6 +106,22 @@ class ModelWrapper: (transformed_model, model_was_changed) = transformation.apply( transformed_model ) + if cleanup: + transformed_model.cleanup() + return transformed_model + + def cleanup(self): + "Run cleanup transformations on the model." + transformed_model = self + cleanup_transforms = [ + RemoveUnusedTensors(), + RemoveStaticGraphInputs(), + SortGraph(), + ] + for trn in cleanup_transforms: + transformed_model = transformed_model.transform( + trn, cleanup=False, make_deepcopy=False + ) return transformed_model def check_compatibility(self): diff --git a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py index bc266e4934c41d6f5f1261e0a30e90cb72ba83a8..181e04f7142053708cc5b2338a8078f6c9fc8303 100644 --- a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py +++ b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py @@ -77,7 +77,7 @@ class StreamingFCLayer_Batch(HLSCustomOp): "weightDataType": ("s", True, ""), "outputDataType": ("s", True, ""), # FINN DataType for accumulator -- auto-computed and updated - "accDataType": ("s", False, "DataType.INT32"), + "accDataType": ("s", False, "INT32"), # use xnor-popcount for binary weights/inputs, thus treating them # as bipolar "binaryXnorMode": ("i", False, 0), @@ -483,6 +483,10 @@ class StreamingFCLayer_Batch(HLSCustomOp): adt = DataType.get_smallest_possible(0 - acc_max) else: adt = DataType.get_smallest_possible(acc_max) + # ensure a datatype divisible by 8-bits in case this is the last node + bw = roundup_to_integer_multiple(adt.bitwidth(), 8) + new_adt_name = adt.name.replace(str(adt.bitwidth()), str(bw)) + adt = DataType[new_adt_name] self.set_nodeattr("accDataType", adt.name) # for no-activation nodes, output dt = acc dt self.set_nodeattr("outputDataType", adt.name) diff --git a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py index 5ec4ab14d65d63523856a6bb107bf75c1ca5a261..fb8b4358abd772d13c355f797649dc3b51975b4d 100644 --- a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py +++ b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py @@ -112,6 +112,7 @@ class CreateDataflowPartition(Transformation): "dataflow_partition" + str(target_partition_id) + "_" ) df_model_filename = df_model_dir + "/df_model.onnx" + df_model.cleanup() df_model.save(df_model_filename) # remove all dataflow nodes from the non-dataflow model # keep track of where the dataflow part starts diff --git a/src/finn/transformation/general.py b/src/finn/transformation/general.py index 4303eb17f39a9949f5729e895e449bbb6a633033..690364a7d827086217ffdf9d246c2f4c5f395772 100644 --- a/src/finn/transformation/general.py +++ b/src/finn/transformation/general.py @@ -189,6 +189,9 @@ class SortGraph(Transformation): # Probably this is faster than copying initializers and more robust in general def apply(self, model): + if len(model.graph.node) == 1: + # single-node graph, nothing to sort + return (model, False) # Gather graph structure graph_dependencies = {} node_list = [ @@ -214,7 +217,7 @@ class SortGraph(Transformation): for new_idx, sorted_idx in enumerate(sorted_node_indexes): model.graph.node.insert(new_idx, node_list[sorted_idx]) - return model, False + return (model, False) class ConvertSubToAdd(Transformation): diff --git a/src/finn/transformation/merge_onnx_models.py b/src/finn/transformation/merge_onnx_models.py index 5dc6127ed189311c72a119932394aca4745e3608..ceacab197150fe6d32e3a9eda268aed186b1a8bc 100644 --- a/src/finn/transformation/merge_onnx_models.py +++ b/src/finn/transformation/merge_onnx_models.py @@ -31,12 +31,12 @@ from onnx import helper from finn.transformation import Transformation from finn.core.modelwrapper import ModelWrapper -import finn.util.basic as util from finn.transformation.infer_shapes import InferShapes from finn.transformation.infer_datatypes import InferDataTypes from finn.transformation.infer_data_layouts import InferDataLayouts from finn.transformation.general import ( GiveReadableTensorNames, + GiveRandomTensorNames, GiveUniqueNodeNames, GiveUniqueParameterTensors, ) @@ -59,6 +59,9 @@ class MergeONNXModels(Transformation): graph_modified = False pre_model = self.pre_model post_model = copy.deepcopy(model) + # to avoid mix-ups, start by giving all tensors random names + pre_model = pre_model.transform(GiveRandomTensorNames()) + post_model = post_model.transform(GiveRandomTensorNames()) # check for dynamic outputs of pre model dyn_outp = [] @@ -94,27 +97,6 @@ class MergeONNXModels(Transformation): for n in post_model.graph.node: n.name = "" - # randomize all tensor names - names1 = pre_model.get_all_tensor_names() - names2 = post_model.get_all_tensor_names() - used_names = names1 + names2 - - # pre_model - for tensor_name in names1: - new_name = util.random_string() - while new_name in used_names: - new_name = util.random_string() - pre_model.rename_tensor(tensor_name, new_name) - used_names.append(new_name) - - # post_model - for tensor in names2: - new_name = util.random_string() - while new_name in used_names: - new_name = util.random_string() - post_model.rename_tensor(tensor_name, new_name) - used_names.append(new_name) - # check if models can be merged output_model_a = dyn_outp[0].name input_model_b = dyn_inp[0].name @@ -124,6 +106,9 @@ class MergeONNXModels(Transformation): output_a_shape == input_b_shape ), "Models can't be merged! Shapes don't match." + pre_model.save("pre.onnx") + post_model.save("post.onnx") + # connect output of one model to input of the other for n in pre_model.graph.node: if output_model_a == n.output[0]: @@ -132,83 +117,43 @@ class MergeONNXModels(Transformation): # extract information for new model # nodes - node_list_a = pre_model.graph.node - node_list_b = post_model.graph.node - - node_list = node_list_a - for node in node_list_b: - node_list.append(node) + node_pre = [node for node in pre_model.graph.node] + node_post = [node for node in post_model.graph.node] + node_new = node_pre + node_post # in and output inp = pre_model.graph.input[0] outp = post_model.graph.output[0] + vi_pre = [x for x in pre_model.graph.value_info] + out_pre = [x for x in pre_model.graph.output] + qa_pre = [x for x in pre_model.graph.quantization_annotation] + init_pre = [x for x in pre_model.graph.initializer] + + vi_post = [x for x in post_model.graph.value_info] + qa_post = [x for x in post_model.graph.quantization_annotation] + init_post = [x for x in post_model.graph.initializer] + + vi_new = vi_pre + vi_post + out_pre + qa_new = qa_pre + qa_post + init_new = init_pre + init_post + # create new graph and model new_graph = helper.make_graph( - nodes=node_list, + nodes=node_new, name="fuse-graph", inputs=[inp], outputs=[outp], - value_info=[], + value_info=vi_new, ) new_model = helper.make_model(new_graph, producer_name="fuse_model") new_model = ModelWrapper(new_model) - # add value info from both models to new model - # pre model - vi_pre = [x for x in pre_model.graph.input] - vi_pre += [x for x in pre_model.graph.output] - vi_pre += [x for x in pre_model.graph.value_info] - for vi in vi_pre: - # preserve intializers, quantization/sparsity annotation, etc. - # initializer - init_val = pre_model.get_initializer(vi.name) - if init_val is not None: - new_model.set_initializer(vi.name, init_val) - # FINN datatype - dtype = pre_model.get_tensor_datatype(vi.name) - new_model.set_tensor_datatype(vi.name, dtype) - # data layout - data_layout = pre_model.get_tensor_layout(vi.name) - if data_layout is not None: - new_model.set_tensor_layout(vi.name, data_layout) - # sparsity - sparsity = pre_model.get_tensor_sparsity(vi.name) - if sparsity is not None: - new_model.set_tensor_sparsity(vi.name, sparsity) - # graph input should not be part of graph.value_info, so don't insert - # if current vi == inp, but the quantization annotation is preserved - if vi == inp: - continue - new_model.graph.value_info.append(vi) - - # post model - vi_model = [x for x in post_model.graph.input] - vi_model += [x for x in post_model.graph.output] - vi_model += [x for x in post_model.graph.value_info] - for vi in vi_model: - # preserve intializers, quantization/sparsity annotation, etc. - # initializer - init_val = post_model.get_initializer(vi.name) - if init_val is not None: - new_model.set_initializer(vi.name, init_val) - # FINN datatype - dtype = post_model.get_tensor_datatype(vi.name) - new_model.set_tensor_datatype(vi.name, dtype) - # data layout - data_layout = post_model.get_tensor_layout(vi.name) - if data_layout is not None: - new_model.set_tensor_layout(vi.name, data_layout) - # sparsity - sparsity = post_model.get_tensor_sparsity(vi.name) - if sparsity is not None: - new_model.set_tensor_sparsity(vi.name, sparsity) - # graph output should not be part of graph.value_info, so don't insert - # if current vi == outp, but the quantization annotation is preserved - if vi == outp: - continue - new_model.graph.value_info.append(vi) + for i in init_new: + new_model.graph.initializer.append(i) + for qa in qa_new: + new_model.graph.quantization_annotation.append(qa) # tidy-up new model model = new_model diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 62d5947e3b7e06375cc9d48a2cf32b4f685e7861..cc759bebb1b856a84e25978d442e460332092d23 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -156,13 +156,19 @@ def make_build_dir(prefix=""): def get_by_name(container, name, name_field="name"): - """Return item from container by .name field if it exists, None otherwise""" + """Return item from container by .name field if it exists, None otherwise. + Will throw an Exception if multiple items are found, since this violates the + ONNX standard.""" names = [getattr(x, name_field) for x in container] - try: - ind = names.index(name) - return container[ind] - except ValueError: + + inds = [i for i, e in enumerate(names) if e == name] + if len(inds) > 1: + raise Exception("Found multiple get_by_name matches, undefined behavior") + elif len(inds) == 0: return None + else: + ind = inds[0] + return container[ind] def remove_by_name(container, name, name_field="name"): diff --git a/tests/fpgadataflow/test_convert_to_hls_layers_fc.py b/tests/fpgadataflow/test_convert_to_hls_layers_fc.py index 30d5ae64cfec84e089426d389a8cb607cd71c12f..bd600c6c57d00d5fc03152f75b9f2f8c6beeeb2c 100644 --- a/tests/fpgadataflow/test_convert_to_hls_layers_fc.py +++ b/tests/fpgadataflow/test_convert_to_hls_layers_fc.py @@ -89,7 +89,6 @@ def test_convert_to_hls_layers_tfc_w1a1(): assert fc3.op_type == "StreamingFCLayer_Batch" assert model.get_tensor_shape(fc3.input[0]) == [1, 64] assert model.get_tensor_shape(fc3.input[1]) == [64, 10] - os.remove(export_onnx_path) fc0w = getCustomOp(fc0) fc0w.set_nodeattr("SIMD", 784)