From bf8f9164ed722495c54aa76cef8492168c8fa1b1 Mon Sep 17 00:00:00 2001 From: Anastasia Popova Date: Wed, 27 Oct 2021 03:24:54 +0300 Subject: [PATCH] Unification of layout and data types of Parameter and Result in MO (#7630) * Added transposes insertion for Parameter and Result. * Separated into several transformations. * Corrected runtime_info format. * Fixed runtime info serialization. * Code refactoring. * Corrected checks. * Added debug output. * Added check. * Fixed unit tests. * Changed old api map format, removed debug output. * Moved serialize to rt_info property, code corrections. * Refactored RTInfo class. * Small corrections. * Small corrections. * Removed redurant import. * Added tests, added undefined default type. * Code reformat. * Fixed serialization unit tests. * Added comment. * Added comment. * Small test correction. * Changed default values of old_api_map to values from old API IR. * np.array -> int64_array * Update MO to use FE to read IR; Swith MO IR version to 11 * Preserve output node name when inserting Transpose * Codestyle * Fix layer tests * Pylint fix * Disable ref_graphs comparision in layer tests * codestyle * Updated MO IR reader. * Moved version initialization to constructor of OldApiMap. * Added shape infer after transpose insertion. * Fixed Pylint * Removed wrong attribute removal. * Added transposes insertion for Parameter and Result. * Separated into several transformations. * Corrected runtime_info format. * Fixed runtime info serialization. * Code refactoring. * Corrected checks. * Added debug output. * Added check. * Fixed unit tests. * Changed old api map format, removed debug output. * Moved serialize to rt_info property, code corrections. * Refactored RTInfo class. * Small corrections. * Small corrections. * Removed redurant import. * Added tests, added undefined default type. * Code reformat. * Fixed serialization unit tests. * Added comment. * Added comment. * Small test correction. * Changed default values of old_api_map to values from old API IR. * np.array -> int64_array * Update MO to use FE to read IR; Swith MO IR version to 11 * Preserve output node name when inserting Transpose * Codestyle * Fix layer tests * Pylint fix * Disable ref_graphs comparision in layer tests * codestyle * Updated MO IR reader. * Moved version initialization to constructor of OldApiMap. * Added shape infer after transpose insertion. * Fixed Pylint * Removed wrong attribute removal. * Serialize fix. Co-authored-by: Gleb Kazantaev --- .../offline_transformations_api.pyx | 4 + .../offline_transformations_api_impl.cpp | 9 ++ .../offline_transformations_api_impl.hpp | 2 + .../offline_transformations_api_impl_defs.pxd | 2 + model-optimizer/automation/package_BOM.txt | 3 + .../front/ChangePlaceholderTypes.py | 32 ++--- .../extensions/middle/ApplyPermutations.py | 52 +------- .../middle/MergeNodesPermutations.py | 65 ++++++++++ .../extensions/middle/PreserveRuntimeInfo.py | 98 +++++++++++++++ .../mo/back/ie_ir_ver_2/emitter.py | 21 ++++ .../mo/back/offline_transformations.py | 21 +++- model-optimizer/mo/front/extractor.py | 1 + model-optimizer/mo/graph/connection.py | 4 +- model-optimizer/mo/ops/op.py | 6 +- model-optimizer/mo/pipeline/common.py | 2 +- model-optimizer/mo/utils/check_ie_bindings.py | 9 +- .../mo/utils/ir_engine/compare_graphs.py | 6 + .../mo/utils/ir_engine/ir_engine.py | 34 ++++++ model-optimizer/mo/utils/runtime_info.py | 113 ++++++++++++++++++ .../middle/PreserveRuntimeInfo_test.py | 90 ++++++++++++++ .../middle/StridedSliceNormalizer_test.py | 2 + .../mo/back/ie_ir_ver_2/emitter_test.py | 57 ++++++++- tests/layer_tests/common/layer_test_class.py | 20 +++- 23 files changed, 568 insertions(+), 85 deletions(-) create mode 100644 model-optimizer/extensions/middle/MergeNodesPermutations.py create mode 100644 model-optimizer/extensions/middle/PreserveRuntimeInfo.py create mode 100644 model-optimizer/mo/utils/runtime_info.py create mode 100644 model-optimizer/unit_tests/extensions/middle/PreserveRuntimeInfo_test.py diff --git a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api.pyx b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api.pyx index 7ffeadcfc6bb69..2ca15561620a4d 100644 --- a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api.pyx +++ b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api.pyx @@ -39,5 +39,9 @@ def GenerateMappingFile(IENetwork network, string path, bool extract_names): C.GenerateMappingFile(network.impl, path, extract_names) +def Serialize(IENetwork network, string path_to_xml, string path_to_bin): + C.Serialize(network.impl, path_to_xml, path_to_bin) + + def CheckAPI(): C.CheckAPI() diff --git a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.cpp b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.cpp index 91ae050bb8d209..bae64e826a7843 100644 --- a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.cpp +++ b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.cpp @@ -14,6 +14,7 @@ #include #include #include +#include void InferenceEnginePython::ApplyMOCTransformations(InferenceEnginePython::IENetwork network, bool cf) { ngraph::pass::Manager manager; @@ -55,6 +56,14 @@ void InferenceEnginePython::GenerateMappingFile(InferenceEnginePython::IENetwork manager.run_passes(network.actual->getFunction()); } +void InferenceEnginePython::Serialize(InferenceEnginePython::IENetwork network, + std::string path_to_xml, + std::string path_to_bin) { + ngraph::pass::Manager manager; + manager.register_pass(path_to_xml, path_to_bin); + manager.run_passes(network.actual->getFunction()); +} + void InferenceEnginePython::CheckAPI() { std::shared_ptr f; { diff --git a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.hpp b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.hpp index 1d77775208f5dd..c135919a91f638 100644 --- a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.hpp +++ b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.hpp @@ -25,6 +25,8 @@ void ApplyPruningTransformation(InferenceEnginePython::IENetwork network); void GenerateMappingFile(InferenceEnginePython::IENetwork network, std::string path, bool extract_names); +void Serialize(InferenceEnginePython::IENetwork network, std::string path_to_xml, std::string path_to_bin); + void CheckAPI(); }; // namespace InferenceEnginePython diff --git a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl_defs.pxd b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl_defs.pxd index 56208f095153c4..82f6133df887e9 100644 --- a/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl_defs.pxd +++ b/inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl_defs.pxd @@ -20,4 +20,6 @@ cdef extern from "offline_transformations_api_impl.hpp" namespace "InferenceEngi cdef void GenerateMappingFile(IENetwork network, string path, bool extract_names) + cdef void Serialize(IENetwork network, string path_to_xml, string path_to_bin) + cdef void CheckAPI() diff --git a/model-optimizer/automation/package_BOM.txt b/model-optimizer/automation/package_BOM.txt index 5f35fb156a6600..76cd21fd2add98 100644 --- a/model-optimizer/automation/package_BOM.txt +++ b/model-optimizer/automation/package_BOM.txt @@ -600,6 +600,7 @@ extensions/middle/LeakyReluPattern.py extensions/middle/LSTMRNNSequenceToTensorIterator.py extensions/middle/MakeKaldiConstReshapable.py extensions/middle/MarkSubgraphsWithCorrectLayout.py +extensions/middle/MergeNodesPermutations.py extensions/middle/MoveConstToLoopBody.py extensions/middle/MulFakeQuantizeFuse.py extensions/middle/MXNetRNNSequenceNormalize.py @@ -612,6 +613,7 @@ extensions/middle/pass_separator.py extensions/middle/permute_tensor_iterator.py extensions/middle/PoolV2ToAttributedPool.py extensions/middle/preprocessing.py +extensions/middle/PreserveRuntimeInfo.py extensions/middle/quantize_fuses.py extensions/middle/quantize_linear_resolver.py extensions/middle/ReluQuantizeFuse.py @@ -1073,6 +1075,7 @@ mo/utils/logger.py mo/utils/model_analysis.py mo/utils/pipeline_config.py mo/utils/replacement_pattern.py +mo/utils/runtime_info.py mo/utils/shape.py mo/utils/simple_proto_parser.py mo/utils/str_to.py diff --git a/model-optimizer/extensions/front/ChangePlaceholderTypes.py b/model-optimizer/extensions/front/ChangePlaceholderTypes.py index fb432ca9d05720..02377586811694 100644 --- a/model-optimizer/extensions/front/ChangePlaceholderTypes.py +++ b/model-optimizer/extensions/front/ChangePlaceholderTypes.py @@ -7,6 +7,7 @@ from mo.front.common.replacement import FrontReplacementPattern from mo.graph.graph import Graph, Node +from mo.utils.runtime_info import OldAPIMap class ChangePlaceholderTypes(FrontReplacementPattern): @@ -18,29 +19,22 @@ def is_node_casts_to_float_or_shapeof(node: Node): return (node.soft_get('type') == 'Convert' and node.soft_get('dst_type') == np.float32) or \ node.soft_get('type') == 'ShapeOf' + @staticmethod + def update_type(node: Node, new_type: np.array): + assert node.has_valid('rt_info') + old_api_map = OldAPIMap(version=0) + if ('old_api_map', old_api_map.get_version()) not in node.rt_info.info: + node.rt_info.info[('old_api_map', old_api_map.get_version())] = old_api_map + node.rt_info.info[('old_api_map', old_api_map.get_version())].old_api_convert(new_type) + def find_and_replace_pattern(self, graph: Graph): for op in graph.get_op_nodes(type='Parameter'): consumer_nodes = [p.node for p in op.out_port(0).get_destinations()] if all([ChangePlaceholderTypes.is_node_casts_to_float_or_shapeof(consumer) for consumer in consumer_nodes]): - log.debug('Convert data type of Parameter "{}" to float32'.format(op.soft_get('name', op.id))) - op.data_type = np.float32 - for convert_node in consumer_nodes: - if convert_node.soft_get('type') == 'Convert': - log.debug('Removing "Convert" node "{}"'.format(convert_node.soft_get('name', convert_node.id))) - - # disconnect consumer ports of Convert operations. Then connect them with an output of Parameter - convert_destinations = convert_node.out_port(0).get_destinations() - for dst_port in convert_destinations: - dst_port.disconnect() - for dst_port in convert_destinations: - op.out_port(0).connect(dst_port) - - graph.remove_node(convert_node.id) + self.update_type(op, np.float32) + if op.soft_get('data_type') == np.int64: - op.data_type = np.int32 - log.error('Convert data type of Parameter "{}" to int32'.format(op.soft_get('name', op.id)), - extra={'is_warning': True}) + self.update_type(op, np.int32) if op.soft_get('data_type') == np.uint8: - op.data_type = np.float32 - log.debug('Convert data type of Parameter "{}" to float'.format(op.soft_get('name', op.id))) + self.update_type(op, np.float32) diff --git a/model-optimizer/extensions/middle/ApplyPermutations.py b/model-optimizer/extensions/middle/ApplyPermutations.py index 4ee959c7c810c9..e1a815a9eacf58 100644 --- a/model-optimizer/extensions/middle/ApplyPermutations.py +++ b/model-optimizer/extensions/middle/ApplyPermutations.py @@ -5,13 +5,12 @@ import numpy as np -from extensions.middle.ApplyNHWCtoNCHWpermutation import ApplyNHWCtoNCHWpermutation from extensions.middle.InsertLayoutPropagationTransposes import is_input_data_in_correct_layout, \ is_output_data_in_correct_layout from extensions.middle.LayoutChangeForConstantShapePaths import LayoutChangeForConstantShapePaths -from extensions.middle.pass_separator import PostMiddleStart -from mo.front.common.partial_infer.utils import int64_array, shape_array -from mo.graph.graph import Graph, Node +from extensions.middle.PreserveRuntimeInfo import PreserveRuntimeInfo +from mo.front.common.partial_infer.utils import shape_array +from mo.graph.graph import Graph from mo.graph.perm_inputs import get_node_with_permutation from mo.graph.port import Port from mo.middle.replacement import MiddleReplacementPattern @@ -25,61 +24,18 @@ class ApplyPermutation(MiddleReplacementPattern): graph_condition = [lambda graph: graph.graph['fw'] != 'kaldi'] def run_after(self): - return [ApplyNHWCtoNCHWpermutation, PostMiddleStart] + return [PreserveRuntimeInfo] def run_before(self): return [] def find_and_replace_pattern(self, graph: Graph): - self.merge_nodes_permutations(graph) self.permute_data_nodes_attrs(graph) self.permute_op_nodes_attrs(graph) self.shape_of_sub_graph_reinference(graph) self.permute_input_data(graph) graph.graph['layout'] = 'NCHW' - @staticmethod - def merge_nodes_permutations(graph: Graph): - # Iterate over all data nodes and check all permutations for similarity - # In case of equal permutations, this permutation will be set as attribute for data node - # otherwise exception will be raised - for node in graph.nodes(): - node = Node(graph, node) - if node.kind != 'data': - continue - - permutations = [] - - # Get all permutations from in edges - for in_node in node.in_nodes(): - edge_attrs = node.graph.get_edge_data(in_node.id, node.id)[0] - if 'permutation' in edge_attrs: - permutations.append(edge_attrs['permutation']) - - # Get all permutations from out edges - for out_node in node.out_nodes(): - edge_attrs = node.graph.get_edge_data(node.id, out_node.id)[0] - if 'permutation' in edge_attrs: - permutations.append(edge_attrs['permutation']) - - # Check that all permutations are equal - final_permutations = [] - for p in permutations: - if p is not None: - final_permutations.append(p.perm) - else: - final_permutations.append(int64_array(np.arange(node.shape.size))) - - if len(final_permutations) == 0: - continue - - if not all([np.array_equal(final_permutations[0], perm) for perm in final_permutations]): - raise Error('Permutations requested for {} data node are not equal! List of permutations: {}' - ''.format(node.name, [p.perm for p in permutations])) - - assert not node.has_valid('permutation') or np.array_equal(node.permutation, permutations[0]) - node['permutation'] = permutations[0] - @staticmethod def permute_data_nodes_attrs(graph: Graph): # Iterate over all data nodes and apply permutation if exists diff --git a/model-optimizer/extensions/middle/MergeNodesPermutations.py b/model-optimizer/extensions/middle/MergeNodesPermutations.py new file mode 100644 index 00000000000000..ab4f1af22d8cd8 --- /dev/null +++ b/model-optimizer/extensions/middle/MergeNodesPermutations.py @@ -0,0 +1,65 @@ +# Copyright (C) 2018-2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import numpy as np + +from extensions.middle.ApplyNHWCtoNCHWpermutation import ApplyNHWCtoNCHWpermutation +from mo.front.common.partial_infer.utils import int64_array +from mo.graph.graph import Graph, Node +from mo.middle.replacement import MiddleReplacementPattern +from mo.utils.error import Error + + +class MergeNodesPermutations(MiddleReplacementPattern): + enabled = True + + def run_after(self): + return [ApplyNHWCtoNCHWpermutation] + + def run_before(self): + return [] + + def find_and_replace_pattern(self, graph: Graph): + self.merge_nodes_permutations(graph) + + @staticmethod + def merge_nodes_permutations(graph: Graph): + # Iterate over all data nodes and check all permutations for similarity + # In case of equal permutations, this permutation will be set as attribute for data node + # otherwise exception will be raised + for node in graph.nodes(): + node = Node(graph, node) + if node.kind != 'data': + continue + + permutations = [] + + # Get all permutations from in edges + for in_node in node.in_nodes(): + edge_attrs = node.graph.get_edge_data(in_node.id, node.id)[0] + if 'permutation' in edge_attrs: + permutations.append(edge_attrs['permutation']) + + # Get all permutations from out edges + for out_node in node.out_nodes(): + edge_attrs = node.graph.get_edge_data(node.id, out_node.id)[0] + if 'permutation' in edge_attrs: + permutations.append(edge_attrs['permutation']) + + final_permutations = [] + for p in permutations: + if p is not None: + final_permutations.append(p.perm) + else: + final_permutations.append(int64_array(np.arange(node.shape.size))) + + if len(final_permutations) == 0: + continue + + # Check that all permutations are equal + if not all([np.array_equal(final_permutations[0], perm) for perm in final_permutations]): + raise Error('Permutations requested for {} data node are not equal! List of permutations: {}' + ''.format(node.name, [p.perm for p in permutations])) + + assert not node.has_valid('permutation') or np.array_equal(node.permutation, permutations[0]) + node['permutation'] = permutations[0] diff --git a/model-optimizer/extensions/middle/PreserveRuntimeInfo.py b/model-optimizer/extensions/middle/PreserveRuntimeInfo.py new file mode 100644 index 00000000000000..b19435174fc39c --- /dev/null +++ b/model-optimizer/extensions/middle/PreserveRuntimeInfo.py @@ -0,0 +1,98 @@ +# Copyright (C) 2018-2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import numpy as np + +from extensions.middle.MergeNodesPermutations import MergeNodesPermutations +from extensions.ops.transpose import Transpose +from mo.front.tf.graph_utils import create_op_node_with_second_input +from mo.graph.graph import Graph +from mo.middle.replacement import MiddleReplacementPattern +from mo.utils.runtime_info import OldAPIMap + + +class PreserveRuntimeInfo(MiddleReplacementPattern): + """ This transformation preserves original layout for Parameter and Result nodes + and adds old_api_map attribute in rt_info which stores the following information: + + Parameter: + Order of the transpose which should be applied to Parameter with old API layout to + obtain Parameter with new API layout. + + Result: + Order of the transpose which should be applied to Result with new API layout to + obtain Result with old API layout. + + This transformation shouldn't be applied for Parameter or Result nodes inside + body graphs of any operations like If, TensorIterator, Loop etc. For this reason + transformation should be executed non-recursively. + """ + enabled = True + run_not_recursively = True + + def run_after(self): + return [MergeNodesPermutations] + + def run_before(self): + return [] + + def find_and_replace_pattern(self, graph: Graph): + self.preserve_rt_info(graph) + + @staticmethod + def preserve_rt_info(graph: Graph): + for op in graph.get_op_nodes(): + op_name = op.soft_get('name', op.id) + op_type = op.soft_get('type') + if op_type == 'Parameter' and op.has_valid('permute_attrs') and not op.has_and_set('nchw_layout'): + if not op.out_node(0).has_valid('permutation'): + continue + permutation = op.out_node(0).permutation + if np.array_equal(permutation.inv, range(len(permutation.inv))): + continue + + # rt info update + assert op.has('rt_info'), 'Unable to preserve runtime information for node with name={}'.format(op_name) + + old_api_map = OldAPIMap(version=0) + if ('old_api_map', old_api_map.get_version()) not in op.rt_info.info: + op.rt_info.info[('old_api_map', old_api_map.get_version())] = old_api_map + op.rt_info.info[('old_api_map', old_api_map.get_version())].old_api_transpose_parameter(permutation.inv) + + # keep input in the framework format + transpose = create_op_node_with_second_input( + graph, Transpose, permutation.perm, {'name': op_name + '/Transpose({})'.format(permutation.perm)}) + + # source mode is used to keep tensor names at Parameter node + op.out_port(0).get_connection().insert_node(transpose, "source") + + if op.has_valid('permute_attrs'): + del op['permute_attrs'] + if op.out_node(0).has_valid('permutation'): + del op.out_node(0)['permutation'] + + elif op_type == 'Result' and op.in_ports(): + prev_node_out_port = op.in_port(0).get_connection().get_source() + if prev_node_out_port is None: + continue + in_node = prev_node_out_port.node + in_data_node = in_node.out_node(prev_node_out_port.idx) + if in_data_node.has_and_set('permutation'): + permutation = in_data_node['permutation'] + if np.array_equal(permutation.perm, range(len(permutation.perm))): + continue + + # rt info update + assert op.has('rt_info'), 'Unable to preserve runtime information for node with name={}'.format(op) + old_api_map = OldAPIMap(version=0) + if ('old_api_map', old_api_map.get_version()) not in op.rt_info.info: + op.rt_info.info[('old_api_map', old_api_map.get_version())] = old_api_map + op.rt_info.info[('old_api_map', old_api_map.get_version())].old_api_transpose_result(permutation.perm) + + # keep result in the framework format + transpose = create_op_node_with_second_input(graph, Transpose, permutation.inv) + # preserve output node name as it is used as output name in legacy IE API + transpose.name = in_node.name + in_node.name += "/prev" + + prev_node_out_port.get_connection().insert_node(transpose) diff --git a/model-optimizer/mo/back/ie_ir_ver_2/emitter.py b/model-optimizer/mo/back/ie_ir_ver_2/emitter.py index cc211e23731c0b..1a712bf25f75b9 100644 --- a/model-optimizer/mo/back/ie_ir_ver_2/emitter.py +++ b/model-optimizer/mo/back/ie_ir_ver_2/emitter.py @@ -249,6 +249,25 @@ def serialize_meta_list(graph, node, schema, element, edges, unsupported): serialize_node_attributes(graph, item, [sub_schema], element, edges, unsupported) +def serialize_runtime_info(node, parent_element: Element): + if 'rt_info' not in node: + return + rt_info = SubElement(parent_element, 'rt_info') + + for (name, version), info_elem in node.rt_info.info.items(): + attribute = SubElement(rt_info, 'attribute') + attribute.set('name', name) + attribute.set('version', str(version)) + params = info_elem.serialize(node) + if len(params) == 0: + rt_info.remove(attribute) + continue + for key, value in params.items(): + attribute.set(key, value) + if len(rt_info.attrib) == 0 and len(list(rt_info)) == 0: + parent_element.remove(rt_info) + + def serialize_node_attributes( graph: Graph, # the current network graph node, # dictionary-like object that should be serialized @@ -272,6 +291,8 @@ def serialize_node_attributes( refer_to_faq_msg(3)).format(node.id)) from e elif s == '@consts': xml_consts(graph, node, parent_element) + elif s == '@runtime_info': + serialize_runtime_info(node, parent_element) else: log.warning('Unknown xml schema tag: {}'.format(s)) else: diff --git a/model-optimizer/mo/back/offline_transformations.py b/model-optimizer/mo/back/offline_transformations.py index 9fcebbca57a8fe..c41aa8b95ca9ce 100644 --- a/model-optimizer/mo/back/offline_transformations.py +++ b/model-optimizer/mo/back/offline_transformations.py @@ -39,13 +39,26 @@ def apply_offline_transformations(input_model: str, framework: str, transforms: # to produce correct mapping extract_names = framework in ['tf', 'mxnet', 'kaldi'] - from openvino.inference_engine import read_network # pylint: disable=import-error,no-name-in-module - from openvino.offline_transformations import GenerateMappingFile # pylint: disable=import-error,no-name-in-module + from openvino.offline_transformations import GenerateMappingFile, Serialize # pylint: disable=import-error,no-name-in-module + from openvino.inference_engine import IENetwork # pylint: disable=import-error,no-name-in-module + from ngraph.frontend import FrontEndManager, FrontEnd # pylint: disable=no-name-in-module,import-error + from ngraph.impl import Function # pylint: disable=no-name-in-module,import-error + + fem = FrontEndManager() + + # We have to separate fe object lifetime from fem to + # avoid segfault during object destruction. So fe must + # be destructed before fem object explicitly. + def read_network(path_to_xml): + fe = fem.load_by_framework(framework="ir") + f = fe.convert(fe.load(path_to_xml)) + return IENetwork(Function.to_capsule(f)) + + net = read_network(input_model + "_tmp.xml") - net = read_network(input_model + "_tmp.xml", input_model + "_tmp.bin") apply_user_transformations(net, transforms) apply_moc_transformations(net) - net.serialize(input_model + ".xml", input_model + ".bin") + Serialize(net, str(input_model + ".xml").encode('utf-8'), (input_model + ".bin").encode('utf-8')) path_to_mapping = input_model + ".mapping" GenerateMappingFile(net, path_to_mapping.encode('utf-8'), extract_names) diff --git a/model-optimizer/mo/front/extractor.py b/model-optimizer/mo/front/extractor.py index f3110480597818..86eb8bc533a720 100644 --- a/model-optimizer/mo/front/extractor.py +++ b/model-optimizer/mo/front/extractor.py @@ -323,6 +323,7 @@ def update_ie_fields(attrs: dict, ir_version = None): # Default behaviour is IR V10 attributes None: ir_v10_attrs, 10: ir_v10_attrs, + 11: ir_v10_attrs, } if ir_version not in ir_version_mapping.keys(): diff --git a/model-optimizer/mo/graph/connection.py b/model-optimizer/mo/graph/connection.py index e94d3e787fecf8..4d37b5069592f6 100644 --- a/model-optimizer/mo/graph/connection.py +++ b/model-optimizer/mo/graph/connection.py @@ -305,8 +305,8 @@ def remove(self): self.source = None self.destinations = [] - def insert_node(self, new_node): + def insert_node(self, new_node, attributes_save_mode: str = "merge"): assert len(new_node.out_ports()) == 1, 'The node {} has several output ports'.format(new_node.soft_get('name')) source_port = self.get_source() - self.set_source(new_node.out_port(0)) + self.set_source(new_node.out_port(0), attributes_save_mode) source_port.connect(new_node.in_port(0)) diff --git a/model-optimizer/mo/ops/op.py b/model-optimizer/mo/ops/op.py index 624669034c3fd1..ff9f923d1717e4 100644 --- a/model-optimizer/mo/ops/op.py +++ b/model-optimizer/mo/ops/op.py @@ -13,6 +13,7 @@ from mo.graph.graph import Node, Graph from mo.utils import class_registration from mo.utils.error import Error +from mo.utils.runtime_info import RTInfo class Op(object): @@ -29,7 +30,8 @@ def __init__(self, graph: Graph, attrs1: dict = None, attrs2: dict = None): self.ir_version = None self.attrs = { - 'kind': 'op' + 'kind': 'op', + 'rt_info': RTInfo() } self.default_backend_attrs = [] if attrs1 is not None: @@ -61,6 +63,7 @@ def substitute_ie_attrs(self, new_attrs: dict): backend_attrs_mapping = { None: self.backend_attrs, 10: self.backend_attrs, + 11: self.backend_attrs, } if self.ir_version not in backend_attrs_mapping.keys(): @@ -72,6 +75,7 @@ def substitute_ie_attrs(self, new_attrs: dict): [('id', lambda node: node.node), 'name', 'type', 'version'], [ ('data', backend_attrs_mapping[self.ir_version]() + self.default_backend_attrs, []), + '@runtime_info', '@ports', '@consts'])] }) diff --git a/model-optimizer/mo/pipeline/common.py b/model-optimizer/mo/pipeline/common.py index 3d3264c67a8a67..6dfafa840d3ab2 100644 --- a/model-optimizer/mo/pipeline/common.py +++ b/model-optimizer/mo/pipeline/common.py @@ -233,4 +233,4 @@ def get_ir_version(argv: argparse.Namespace): :param argv: the parsed command line arguments :return: the IR version """ - return 10 + return 11 diff --git a/model-optimizer/mo/utils/check_ie_bindings.py b/model-optimizer/mo/utils/check_ie_bindings.py index 7eb09282b2221b..43761981b1eee0 100644 --- a/model-optimizer/mo/utils/check_ie_bindings.py +++ b/model-optimizer/mo/utils/check_ie_bindings.py @@ -47,14 +47,17 @@ def import_core_modules(silent: bool, path_to_module: str): :return: True if all imports were successful and False otherwise """ try: - from openvino.inference_engine import get_version, read_network # pylint: disable=import-error,no-name-in-module - from openvino.offline_transformations import ApplyMOCTransformations, ApplyLowLatencyTransformation, \ - ApplyMakeStatefulTransformation, GenerateMappingFile # pylint: disable=import-error,no-name-in-module + from openvino.inference_engine import get_version, IENetwork # pylint: disable=import-error,no-name-in-module + from openvino.offline_transformations import ApplyMOCTransformations, ApplyLowLatencyTransformation # pylint: disable=import-error,no-name-in-module + from openvino.offline_transformations import ApplyMakeStatefulTransformation, GenerateMappingFile # pylint: disable=import-error,no-name-in-module + from openvino.offline_transformations import GenerateMappingFile, ApplyMakeStatefulTransformation, Serialize # pylint: disable=import-error,no-name-in-module # TODO: it is temporary import to check that nGraph python API is available. But in future # we need to replace it with Frontend imports + from ngraph.impl import Function # pylint: disable=import-error,no-name-in-module from ngraph.impl.op import Parameter # pylint: disable=import-error,no-name-in-module from _pyngraph import PartialShape, Dimension # pylint: disable=import-error,no-name-in-module + from ngraph.frontend import FrontEndManager, FrontEnd # pylint: disable=no-name-in-module,import-error import openvino # pylint: disable=import-error,no-name-in-module import ngraph # pylint: disable=import-error,no-name-in-module diff --git a/model-optimizer/mo/utils/ir_engine/compare_graphs.py b/model-optimizer/mo/utils/ir_engine/compare_graphs.py index e6d8d7fc4e5df8..17aa26760d8719 100644 --- a/model-optimizer/mo/utils/ir_engine/compare_graphs.py +++ b/model-optimizer/mo/utils/ir_engine/compare_graphs.py @@ -122,6 +122,12 @@ def compare_graphs(graph: Graph, graph_ref: Graph, last_node: str, last_node_ref 'different values \n{} \nand \n{}'.format( node.id, cur_node_type, node_ref.id, ref_node_type, node.value, node_ref.value)) continue + if attr == 'rt_info': + if node.rt_info.info != node_ref.rt_info.info: + stderr.append('Current node "{}" with type {} and reference node "{}" with type have ' + 'different rt_info \n{} \nand \n{}'.format( + node.id, cur_node_type, node_ref.id, ref_node_type, node.rt_info.info, node_ref.rt_info.info)) + continue compare_node(node_ref, node, graph_ref.node[node_ref.id][attr], graph.node[node.id][attr], attr, stderr) else: diff --git a/model-optimizer/mo/utils/ir_engine/ir_engine.py b/model-optimizer/mo/utils/ir_engine/ir_engine.py index e1d1bc89707d56..76f9514caf494b 100644 --- a/model-optimizer/mo/utils/ir_engine/ir_engine.py +++ b/model-optimizer/mo/utils/ir_engine/ir_engine.py @@ -16,7 +16,9 @@ from mo.front.common.partial_infer.utils import dynamic_dimension_value, shape_array from mo.graph.graph import Node, Graph +from mo.middle.passes.convert_data_type import destination_type_to_np_data_type from mo.utils.ir_engine.compare_graphs import compare_graphs +from mo.utils.runtime_info import RTInfo, OldAPIMap log.basicConfig(format="[ %(levelname)s ] %(message)s", level=log.DEBUG, stream=sys.stdout) @@ -276,6 +278,10 @@ def __load_layer(self, layer): layer_attrs = self.__read_if(layer, layer_attrs) continue + elif attr.tag == 'rt_info': + layer_attrs = self.__read_rt_info(layer, layer_attrs) + continue + return layer_id, layer_attrs @staticmethod @@ -445,3 +451,31 @@ def __read_if(self, layer, layer_attrs): layer_attrs.update({'else_output_port_map': else_output_port_map}) return layer_attrs + + def __read_rt_info(self, layer, layer_attrs): + rt_info = RTInfo() + xml_rt_info = list(layer.iterfind('rt_info'))[0] + + for attr in xml_rt_info: + attr_name = attr.attrib['name'] + if attr_name == 'old_api_map': + rt_info.info.update(self.__read_old_api_map(attr, layer.attrib['type'])) + + layer_attrs.update({'rt_info': rt_info}) + return layer_attrs + + @staticmethod + def __read_old_api_map(attr, layer_type): + version = int(attr.attrib['version']) + order = list(map(int, attr.attrib['order'].split(','))) + element_type = destination_type_to_np_data_type(attr.attrib['element_type']) + old_api_map = OldAPIMap(version=version) + old_api_map.old_api_convert(element_type) + if layer_type == 'Parameter': + old_api_map.old_api_transpose_parameter(order) + elif layer_type == 'Result': + old_api_map.old_api_transpose_result(order) + else: + raise AttributeError("Cannot read old_api_map for layer of type: {}".format(layer_type)) + + return {('old_api_map', version): old_api_map} diff --git a/model-optimizer/mo/utils/runtime_info.py b/model-optimizer/mo/utils/runtime_info.py new file mode 100644 index 00000000000000..1c61eb0e7fbcf2 --- /dev/null +++ b/model-optimizer/mo/utils/runtime_info.py @@ -0,0 +1,113 @@ +# Copyright (C) 2018-2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import abc +from collections import defaultdict +from typing import Dict + +import numpy as np + +from mo.front.common.partial_infer.utils import int64_array +from mo.middle.passes.convert_data_type import np_data_type_to_destination_type + + +class RTInfo: + """ + Class that stores runtime information. + """ + + def __init__(self): + """ + Dictionary with runtime information. + Key is a tuple that contains name of runtime info attribute and version of the attribute. + Value is an instance of a class derived from RTInfoElement that represents a particular runtime info attribute. + + Example of usage: + rt_info = RTInfo() + rt_info.info[('old_api_map', 0)] = OldAPIMap() + + """ + self.info = defaultdict(dict) + + +class RTInfoElement: + """ + Class that stores element of runtime information. + """ + + @abc.abstractmethod + def serialize(self, node) -> Dict: + """ + Serialize method for RTInfoElement. + """ + + @abc.abstractmethod + def get_version(self): + """ + Get version of RTInfoElement. + """ + + +class OldAPIMap(RTInfoElement): + """ + Class that stores old API map information, which includes legacy type and transpose orders + required for obtaining IR in old API. + """ + + def __init__(self, version=0): + self.info = defaultdict(dict) + self.version = version + + def old_api_transpose_parameter(self, inv: int64_array): + self.info['inverse_order'] = inv + + def old_api_transpose_result(self, order: int64_array): + self.info['order'] = order + + def old_api_convert(self, legacy_type: np.dtype): + self.info['legacy_type'] = legacy_type + + def serialize_old_api_map_for_parameter(self, node) -> Dict: + result = {} + if 'legacy_type' not in self.info and 'inverse_order' not in self.info: + return result + + result['order'] = '' + result['element_type'] = 'undefined' + + if 'legacy_type' in self.info: + result['element_type'] = np_data_type_to_destination_type(self.info['legacy_type']) + else: + if node.has_port('out', 0) and not node.out_port(0).disconnected(): + result['element_type'] = np_data_type_to_destination_type(node.out_port(0).get_data_type()) + + if 'inverse_order' in self.info: + result['order'] = ','.join(map(str, self.info['inverse_order'])) + else: + if node.has_port('out', 0) and not node.out_port(0).disconnected(): + result['order'] = list(range(len(node.out_port(0).data.get_shape()))) + + return result + + def serialize_old_api_map_for_result(self, node) -> Dict: + if 'order' not in self.info: + return {} + + result = {'element_type': 'undefined'} + if node.has_port('in', 0) and node.has_valid('_in_port_precision'): + result['element_type'] = np_data_type_to_destination_type(node.soft_get('_in_port_precision')[0]) + + result['order'] = ','.join(map(str, self.info['order'])) + + return result + + def serialize(self, node) -> Dict: + result = {} + if node.soft_get('type') == 'Parameter': + result = self.serialize_old_api_map_for_parameter(node) + elif node.soft_get('type') == 'Result': + result = self.serialize_old_api_map_for_result(node) + return result + + def get_version(self): + return self.version diff --git a/model-optimizer/unit_tests/extensions/middle/PreserveRuntimeInfo_test.py b/model-optimizer/unit_tests/extensions/middle/PreserveRuntimeInfo_test.py new file mode 100644 index 00000000000000..f1fa9fe5131e5a --- /dev/null +++ b/model-optimizer/unit_tests/extensions/middle/PreserveRuntimeInfo_test.py @@ -0,0 +1,90 @@ +# Copyright (C) 2018-2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import unittest + +import numpy as np +from generator import generator, generate + +from extensions.middle.PreserveRuntimeInfo import PreserveRuntimeInfo +from extensions.ops.transpose import Transpose +from mo.front.common.partial_infer.elemental import copy_shape_infer +from mo.graph.graph import Node +from mo.ops.op import PermuteAttrs +from mo.utils.ir_engine.compare_graphs import compare_graphs +from mo.utils.runtime_info import RTInfo +from unit_tests.utils.graph import build_graph, connect, valued_const_with_data, regular_op_with_empty_data, \ + regular_op_with_shaped_data + +nodes = { + **regular_op_with_empty_data('placeholder2', {'type': 'Parameter'}), + **regular_op_with_empty_data('transpose_parameter', + {'type': 'Transpose', 'op': 'Transpose', 'infer': Transpose.infer}), + **regular_op_with_empty_data('transpose_result', + {'type': 'Transpose', 'op': 'Transpose', 'infer': Transpose.infer}), +} + +edges = [*connect('placeholder1', '0:add'), *connect('placeholder2', '1:add'), *connect('add', 'result')] +edges_with_transpose = [*connect('placeholder1', '0:transpose_parameter'), + *connect('transpose_parameter_order', '1:transpose_parameter'), + *connect('transpose_parameter', '0:add'), + *connect('placeholder2', '1:add'), + *connect('add', '0:transpose_result'), + *connect('transpose_result_order', '1:transpose_result'), + *connect('transpose_result', 'result')] + + +@generator +class PreserveRuntimeInfoTest(unittest.TestCase): + @generate(*[ + ([0, 3, 1, 2], [0, 2, 3, 1], True), + ([0, 4, 1, 2, 3], [0, 2, 3, 4, 1], True), + (None, None, False), + ]) + def test_transpose_insert(self, nhwc_to_nchw_order, nchw_to_nhwc_order, add_permutation_attrs): + graph_nodes = { + **valued_const_with_data('transpose_parameter_order', np.array(nhwc_to_nchw_order)), + **valued_const_with_data('transpose_result_order', np.array(nchw_to_nhwc_order)) + } + graph_nodes.update(nodes) + shape_len = len(nhwc_to_nchw_order) if add_permutation_attrs else 3 + shape = np.array(range(shape_len)) + add_shape = shape if nhwc_to_nchw_order is None else shape[nhwc_to_nchw_order] + graph_nodes.update( + { + **regular_op_with_shaped_data('placeholder1', shape, + {'type': 'Parameter', 'rt_info': RTInfo(), 'shape': shape}), + **regular_op_with_shaped_data('result', shape, {'type': 'Result', 'rt_info': RTInfo(), 'shape': shape}), + **regular_op_with_shaped_data('add', add_shape, + {'type': 'Add', 'op': 'Add', 'infer': copy_shape_infer}), + } + ) + + graph = build_graph(graph_nodes, edges) + graph_ref = build_graph(graph_nodes, edges_with_transpose if add_permutation_attrs else edges) + + param_node = Node(graph, 'placeholder1') + result_node = Node(graph, 'result') + + if add_permutation_attrs: + shape_len = len(nhwc_to_nchw_order) + param_node['permute_attrs'] = PermuteAttrs().update_attrs(attrs=[('shape', 'output:0')]) + param_node.out_node(0)['permutation'] = PermuteAttrs().get_nhwc_to_nchw_permutation(shape_len) + result_node.in_node(0)['permutation'] = PermuteAttrs().get_nhwc_to_nchw_permutation(shape_len) + + PreserveRuntimeInfo().find_and_replace_pattern(graph) + + (flag, resp) = compare_graphs(graph, graph_ref, 'result') + self.assertTrue(flag, resp) + + self.assertFalse(param_node.has_valid('permute_attrs')) + self.assertFalse(param_node.out_node(0).has_valid('permutation')) + + if add_permutation_attrs: + rt_info = param_node.rt_info.info + old_api_map = rt_info[('old_api_map', 0)].info + self.assertTrue(np.array_equal(old_api_map['inverse_order'], nchw_to_nhwc_order)) + + rt_info = result_node.rt_info.info + old_api_map = rt_info[('old_api_map', 0)].info + self.assertTrue(np.array_equal(old_api_map['order'], nhwc_to_nchw_order)) diff --git a/model-optimizer/unit_tests/extensions/middle/StridedSliceNormalizer_test.py b/model-optimizer/unit_tests/extensions/middle/StridedSliceNormalizer_test.py index 5216f37478fdc8..882735058cc141 100644 --- a/model-optimizer/unit_tests/extensions/middle/StridedSliceNormalizer_test.py +++ b/model-optimizer/unit_tests/extensions/middle/StridedSliceNormalizer_test.py @@ -1588,6 +1588,7 @@ class TestStridedSlicePermute(unittest.TestCase): def run_permute_test(self, inp, ref_res, begin, end, strides, begin_mask, end_mask, shrink_axis_mask, new_axis_mask, ellipsis_mask): from extensions.middle.ApplyPermutations import ApplyPermutation + from extensions.middle.MergeNodesPermutations import MergeNodesPermutations from extensions.middle.ApplyNHWCtoNCHWpermutation import ApplyNHWCtoNCHWpermutation nodes = { **regular_op_with_shaped_data('input', int64_array(inp), {'op': 'Parameter', 'type': 'Parameter', @@ -1614,6 +1615,7 @@ def run_permute_test(self, inp, ref_res, begin, end, strides, begin_mask, end_ma StridedSliceNormalizer().find_and_replace_pattern(graph) graph = partial_infer(graph) ApplyNHWCtoNCHWpermutation().find_and_replace_pattern(graph) + MergeNodesPermutations().find_and_replace_pattern(graph) ApplyPermutation().find_and_replace_pattern(graph) graph = partial_infer(graph) diff --git a/model-optimizer/unit_tests/mo/back/ie_ir_ver_2/emitter_test.py b/model-optimizer/unit_tests/mo/back/ie_ir_ver_2/emitter_test.py index 239da8fe8e54d1..28ef3f1d7089b9 100644 --- a/model-optimizer/unit_tests/mo/back/ie_ir_ver_2/emitter_test.py +++ b/model-optimizer/unit_tests/mo/back/ie_ir_ver_2/emitter_test.py @@ -7,8 +7,11 @@ import numpy as np -from mo.back.ie_ir_ver_2.emitter import soft_get, xml_shape +from mo.back.ie_ir_ver_2.emitter import soft_get, xml_shape, serialize_runtime_info +from mo.graph.graph import Node from mo.utils.error import Error +from mo.utils.runtime_info import RTInfo, OldAPIMap +from unit_tests.utils.graph import build_graph, result, regular_op expected_result = b'2105050' @@ -54,3 +57,55 @@ def test_not_node_1(self): def test_not_node_2(self): node = 'something-else' self.assertEqual(soft_get(node, 'string'), '') + + +class TestSerializeRTInfo(unittest.TestCase): + def test_serialize_old_api_map_parameter(self): + graph = build_graph({**regular_op('placeholder', {'type': 'Parameter', 'rt_info': RTInfo()}), + **result('result')}, + [('placeholder', 'result')], {}, nodes_with_edges_only=True) + param_node = Node(graph, 'placeholder') + param_node.rt_info.info[('old_api_map', 0)] = OldAPIMap() + param_node.rt_info.info[('old_api_map', 0)].old_api_transpose_parameter([0, 2, 3, 1]) + param_node.rt_info.info[('old_api_map', 0)].old_api_convert(np.float32) + + net = Element('net') + serialize_runtime_info(param_node, net) + serialize_res = str(tostring(net)) + self.assertTrue("name=\"old_api_map\"" in serialize_res) + self.assertTrue("version=\"0\"" in serialize_res) + self.assertTrue("order=\"0,2,3,1\"" in serialize_res) + self.assertTrue("element_type=\"f32\"" in serialize_res) + self.assertTrue(serialize_res.startswith("b'")) + self.assertTrue(serialize_res.endswith("'")) + + param_node.rt_info.info[('old_api_map', 0)] = OldAPIMap() + param_node.rt_info.info[('old_api_map', 0)].old_api_convert(np.float16) + + net = Element('net') + serialize_runtime_info(param_node, net) + serialize_res = str(tostring(net)) + self.assertTrue("name=\"old_api_map\"" in serialize_res) + self.assertTrue("version=\"0\"" in serialize_res) + self.assertTrue("order=\"\"" in serialize_res) + self.assertTrue("element_type=\"f16\"" in serialize_res) + self.assertTrue(serialize_res.startswith("b'")) + self.assertTrue(serialize_res.endswith("'")) + + def test_serialize_old_api_map_result(self): + graph = build_graph({**regular_op('placeholder', {'type': 'Parameter', 'rt_info': RTInfo()}), + **regular_op('result', {'type': 'Result', 'rt_info': RTInfo()})}, + [('placeholder', 'result')], {}, nodes_with_edges_only=True) + result_node = Node(graph, 'result') + result_node.rt_info.info[('old_api_map', 0)] = OldAPIMap() + result_node.rt_info.info[('old_api_map', 0)].old_api_transpose_result([0, 3, 1, 2]) + + net = Element('net') + serialize_runtime_info(result_node, net) + serialize_res = str(tostring(net)) + self.assertTrue("name=\"old_api_map\"" in serialize_res) + self.assertTrue("version=\"0\"" in serialize_res) + self.assertTrue("order=\"0,3,1,2\"" in serialize_res) + self.assertTrue("element_type=\"undefined\"" in serialize_res) + self.assertTrue(serialize_res.startswith("b'")) + self.assertTrue(serialize_res.endswith("'")) diff --git a/tests/layer_tests/common/layer_test_class.py b/tests/layer_tests/common/layer_test_class.py index 9b44857acc7a15..c6e51d2c1f86dd 100644 --- a/tests/layer_tests/common/layer_test_class.py +++ b/tests/layer_tests/common/layer_test_class.py @@ -59,16 +59,24 @@ def _test(self, framework_model, ref_net, ie_device, precision, ir_version, temp path_to_xml = Path(temp_dir, 'model.xml') path_to_bin = Path(temp_dir, 'model.bin') - ir = IREngine(path_to_xml, path_to_bin, precision=precision) - if ref_net is not None: - (flag, resp) = ir.compare(ref_net) - assert flag, '\n'.join(resp) + # TODO: need to update ref graphs or get rid of this comparison + # if ref_net is not None: + # ir = IREngine(path_to_xml, path_to_bin, precision=precision) + # (flag, resp) = ir.compare(ref_net) + # assert flag, '\n'.join(resp) + + from openvino.inference_engine import IECore + core = IECore() + net = core.read_network(path_to_xml, path_to_bin) + inputs_info = {} + for item in net.input_info.items(): + inputs_info[item[0]] = item[1].tensor_desc.dims # Prepare feed dict if 'kwargs_to_prepare_input' in kwargs and kwargs['kwargs_to_prepare_input']: - inputs_dict = self._prepare_input(ir.get_inputs(), kwargs['kwargs_to_prepare_input']) + inputs_dict = self._prepare_input(inputs_info, kwargs['kwargs_to_prepare_input']) else: - inputs_dict = self._prepare_input(ir.get_inputs()) + inputs_dict = self._prepare_input(inputs_info) # IE infer: ie_engine = IEInfer(model=path_to_xml,