From 080477aa9c5c83fa17ced32ef3593a7d1e91b99b Mon Sep 17 00:00:00 2001 From: Mateusz Bencer Date: Thu, 13 Jan 2022 09:39:14 +0100 Subject: [PATCH] MO fallback to old path if some features are not supported yet (#9034) --- .../prepare_model/Model_Optimizer_FAQ.md | 10 +- tools/mo/openvino/tools/mo/main.py | 82 +++++-- .../mo/unit_tests/mo/frontend_ngraph_test.py | 9 + .../mo/utils/mo_fallback_test_actual.py | 229 ++++++++++++++++++ 4 files changed, 307 insertions(+), 23 deletions(-) create mode 100644 tools/mo/unit_tests/mo/utils/mo_fallback_test_actual.py diff --git a/docs/MO_DG/prepare_model/Model_Optimizer_FAQ.md b/docs/MO_DG/prepare_model/Model_Optimizer_FAQ.md index 4f577817cdb..ef966213a91 100644 --- a/docs/MO_DG/prepare_model/Model_Optimizer_FAQ.md +++ b/docs/MO_DG/prepare_model/Model_Optimizer_FAQ.md @@ -626,4 +626,12 @@ The issue "SyntaxError: 'yield' inside list comprehension" might occur during co The following workarounds are suggested to resolve this issue: 1. Use Python 3.6/3.7 to convert MXNet\* models on Windows 2. Update MXNet: pip install mxnet=1.7.0.post2 -Note that you might have conflicts between previously installed PyPI dependencies. \ No newline at end of file +Note that you might have conflicts between previously installed PyPI dependencies.m + +#### 105. What does the message "The IR preparation was executed by the legacy MO path. ..." mean? + +For the models in ONNX* format, there are two available paths of IR conversion. +The old one is handled by the old Python* implementation, while the new one uses new C++ frontends. +Starting from the 2022.1 version, the default IR conversion path for ONNX models is processed using the new ONNX frontend. +Certain features, such as `--extensions` and `--transformations_config`, are not yet supported on the new frontends. +The IR conversion falls back to the old path if a user does not select any expected path of conversion explicitly (by `--use_new_frontend` or `--use_legacy_frontend` MO arguments) and unsupported pre-defined scenario is detected on the new frontend path. \ No newline at end of file diff --git a/tools/mo/openvino/tools/mo/main.py b/tools/mo/openvino/tools/mo/main.py index 2866564171e..dbce327df87 100644 --- a/tools/mo/openvino/tools/mo/main.py +++ b/tools/mo/openvino/tools/mo/main.py @@ -95,6 +95,15 @@ def print_argv(argv: argparse.Namespace, is_caffe: bool, is_tf: bool, is_mxnet: print('\n'.join(lines), flush=True) +def get_default_frontends(): + # Set which frontend to use by default, values should be 'new' or 'legacy' + default_frontends = { + 'onnx': 'legacy', + 'tf': 'legacy' + } + return default_frontends + + def get_moc_frontends(argv: argparse.Namespace): fem = argv.feManager @@ -117,13 +126,9 @@ def get_moc_frontends(argv: argparse.Namespace): else: return None, [] - # Set which frontend to use by default, values should be 'new' or 'legacy' - frontend_defaults = { - 'onnx': 'legacy', - 'tf': 'legacy' - } + default_frontends = get_default_frontends() # Disable MOC frontend if default is set to legacy and no user override - if frontend_defaults.get(moc_front_end.get_name()) == 'legacy' and not use_new_frontend: + if default_frontends.get(moc_front_end.get_name()) == 'legacy' and not use_new_frontend: moc_front_end = None return moc_front_end, available_moc_front_ends @@ -281,13 +286,17 @@ def arguments_post_parsing(argv: argparse.Namespace): log.debug("Placeholder shapes : {}".format(argv.placeholder_shapes)) - if hasattr(argv, 'extensions') and argv.extensions and argv.extensions != '': - extensions = argv.extensions.split(',') - else: - extensions = None - argv.freeze_placeholder_with_value, argv.input = get_freeze_placeholder_values(argv.input, argv.freeze_placeholder_with_value) + + load_extensions(argv, is_tf, is_caffe, is_mxnet, is_kaldi, is_onnx) + + return argv + +def load_extensions(argv: argparse.Namespace, is_tf: bool, is_caffe: bool, is_mxnet: bool, is_kaldi: bool, is_onnx:bool): + extensions = None + if hasattr(argv, 'extensions') and argv.extensions and argv.extensions != '': + extensions = argv.extensions.split(',') if is_tf: from openvino.tools.mo.front.tf.register_custom_ops import get_front_classes import_extensions.load_dirs(argv.framework, extensions, get_front_classes) @@ -308,25 +317,54 @@ def arguments_post_parsing(argv: argparse.Namespace): from openvino.tools.mo.front.onnx.register_custom_ops import get_front_classes import_extensions.load_dirs(argv.framework, extensions, get_front_classes) - return argv + +def check_fallback(argv : argparse.Namespace): + fallback_reasons = {} + + # Some frontend such as PDPD does not have legacy path so it has no reasons to fallback + if not any(deduce_framework_by_namespace(argv)): + return fallback_reasons + + # There is no possibility for fallback if a user strictly wants to use new frontend + if argv.use_new_frontend: + return fallback_reasons + + fallback_reasons['extensions'] = \ + lambda argv : hasattr(argv, 'extensions') and argv.extensions is not None and len(argv.extensions) > 0 \ + and argv.extensions != import_extensions.default_path() # extensions arg has default value + fallback_reasons['transformations_config'] = \ + lambda argv: hasattr(argv, 'transformations_config') and argv.transformations_config is not None and len(argv.transformations_config) > 0 + + reasons = [reason for reason, is_applicable in fallback_reasons.items() if is_applicable(argv)] + return reasons -def prepare_ir(argv): +def prepare_ir(argv : argparse.Namespace): argv = arguments_post_parsing(argv) - t = tm.Telemetry() graph = None ngraph_function = None moc_front_end, available_moc_front_ends = get_moc_frontends(argv) - if moc_front_end: - t.send_event("mo", "conversion_method", moc_front_end.get_name() + "_frontend") - moc_front_end.add_extension(TelemetryExtension("mo", t.send_event, t.send_error, t.send_stack_trace)) - moc_front_end.add_extension(ProgressReporterExtension(progress_printer(argv))) - ngraph_function = moc_pipeline(argv, moc_front_end) - else: - t.send_event("mo", "conversion_method", "mo_legacy") - graph = unified_pipeline(argv) + fallback_reasons = check_fallback(argv) + if len(fallback_reasons) == 0: + t.send_event("mo", "conversion_method", moc_front_end.get_name() + "_frontend") + moc_front_end.add_extension(TelemetryExtension("mo", t.send_event, t.send_error, t.send_stack_trace)) + moc_front_end.add_extension(ProgressReporterExtension(progress_printer(argv))) + ngraph_function = moc_pipeline(argv, moc_front_end) + return graph, ngraph_function + else: # apply fallback + reasons_message = ", ".join(fallback_reasons) + load_extensions(argv, *list(deduce_framework_by_namespace(argv))) + t.send_event("mo", "fallback_reason", reasons_message) + log.warning("The IR preparation was executed by the legacy MO path. " + "This is a fallback scenario applicable only for some specific cases. " + f"The detailed reason why fallback was executed: not supported {reasons_message} were used. " + "You can specify --use_new_frontend flag to force using the Frontend MO path to avoid additional checks. " + + refer_to_faq_msg(105)) + + t.send_event("mo", "conversion_method", "mo_legacy") + graph = unified_pipeline(argv) return graph, ngraph_function diff --git a/tools/mo/unit_tests/mo/frontend_ngraph_test.py b/tools/mo/unit_tests/mo/frontend_ngraph_test.py index e93b1e779cc..536f96b26de 100644 --- a/tools/mo/unit_tests/mo/frontend_ngraph_test.py +++ b/tools/mo/unit_tests/mo/frontend_ngraph_test.py @@ -59,3 +59,12 @@ def test_main_test(): status = subprocess.run(args, env=os.environ) assert not status.returncode + + +def test_mo_fallback_test(): + setup_env() + args = [sys.executable, '-m', 'pytest', + os.path.join(os.path.dirname(__file__), 'utils/mo_fallback_test_actual.py'), '-s'] + + status = subprocess.run(args, env=os.environ) + assert not status.returncode diff --git a/tools/mo/unit_tests/mo/utils/mo_fallback_test_actual.py b/tools/mo/unit_tests/mo/utils/mo_fallback_test_actual.py new file mode 100644 index 00000000000..065c73374d7 --- /dev/null +++ b/tools/mo/unit_tests/mo/utils/mo_fallback_test_actual.py @@ -0,0 +1,229 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from unittest.mock import Mock +from unittest.mock import patch + +import openvino +from openvino.tools.mo.main import prepare_ir +from openvino.frontend import FrontEndManager # pylint: disable=no-name-in-module,import-error +from onnx.helper import make_graph, make_model, make_tensor_value_info +import argparse +import os +import onnx +import paddle +import numpy as np +import shutil +import pytest +from generator import generator, generate + +try: + import openvino_telemetry as tm +except ImportError: + import openvino.tools.mo.utils.telemetry_stub as tm + +def base_args_config(use_legacy_fe:bool=None, use_new_fe:bool=None): + args = argparse.Namespace() + args.feManager = FrontEndManager() + args.extensions = None + args.use_legacy_frontend = use_legacy_fe + args.use_new_frontend = use_new_fe + args.framework = 'onnx' + args.model_name = None + args.input_model = None + args.silent = True + args.transform=[] + args.legacy_ir_generation = False + args.scale = None + args.output=None + args.input=None + args.input_shape=None + args.batch=None + args.mean_values=None + args.scale_values=None + args.output_dir=os.getcwd() + args.freeze_placeholder_with_value = None + args.transformations_config = None + args.disable_fusing = None + args.finegrain_fusing = None + args.disable_gfusing = None + args.disable_resnet_optimization = None + args.enable_concat_optimization = None + args.static_shape = None + args.disable_weights_compression = None + args.reverse_input_channels = None + args.data_type = None + args.layout = None + args.source_layout = None + args.target_layout = None + return args + + +def get_test_default_frontends(): + return { + 'onnx': 'new', + 'tf': 'legacy' + } + + +def save_paddle_model(name, exe, feedkeys:list, fetchlist:list, target_dir:str): + model_dir = os.path.join(target_dir, name) + if not os.path.exists(model_dir): + os.makedirs(model_dir) + + paddle.fluid.io.save_inference_model(model_dir, feedkeys, fetchlist, exe) + paddle.fluid.io.save_inference_model(model_dir, feedkeys, fetchlist, exe, model_filename=name+".pdmodel", params_filename=name+".pdiparams") + + +@generator +class TestMoFallback(unittest.TestCase): + def setUp(self): + tm.Telemetry.__init__ = Mock(return_value=None) + tm.Telemetry.send_event = Mock() + + self.models = {} + add = onnx.helper.make_node("Add", inputs=["in1", "in2"], outputs=["add_out"]) + input_tensors = [ + make_tensor_value_info("in1", onnx.TensorProto.FLOAT, (2, 2)), + make_tensor_value_info("in2", onnx.TensorProto.FLOAT, (2, 2)), + ] + output_tensors = [ + make_tensor_value_info("add_out", onnx.TensorProto.FLOAT, (1, 2)), + ] + graph = make_graph([add], "test_graph", input_tensors, output_tensors) + model = make_model(graph, producer_name="MO tests", + opset_imports=[onnx.helper.make_opsetid("", 13)]) + self.models["test_model.onnx"] = model + + for name, model in self.models.items(): + onnx.save(model, name) + + trans_config = 'config.json' + with open(trans_config, 'w') as f: + f.write("[]") # json format + self.trans_config_file = os.path.abspath(trans_config) + + self.paddle_dir = "paddle_dir" + paddle.enable_static() + if not os.path.exists(self.paddle_dir): + os.mkdir(self.paddle_dir) + x = np.array([-2, 0, 1]).astype('float32') + node_x = paddle.static.data(name='x', shape=x.shape, dtype='float32') + out = paddle.nn.functional.relu(node_x) + + cpu = paddle.static.cpu_places(1) + exe = paddle.static.Executor(cpu[0]) + exe.run(paddle.static.default_startup_program()) + + save_paddle_model("relu", exe, feedkeys=['x'], fetchlist=[out], target_dir=self.paddle_dir) + + + def tearDown(self): + for name in self.models.keys(): + os.remove(name) + os.remove(self.trans_config_file) + shutil.rmtree(self.paddle_dir) + + + @generate(*[('dir_to_extension', None, None, 'mo_legacy', 'extensions'), # fallback + ('dir_to_extension', None, True, 'onnx_frontend', None), + ('dir_to_extension', True, None, 'mo_legacy', None), + ('', True, None, 'mo_legacy', None), + ('', None, True, 'onnx_frontend', None), + (None, None, None, 'onnx_frontend', None), + ]) + def test_fallback_if_extension_specified(self, extension, use_legacy, use_new_fe, conversion_method, fallback_reason): + with patch('openvino.tools.mo.main.get_default_frontends') as default_fe: + default_fe.return_value = get_test_default_frontends() + args = base_args_config(use_legacy, use_new_fe) + args.extensions = extension + args.input_model = "test_model.onnx" + prepare_ir(args) + + tm.Telemetry.send_event.assert_any_call('mo', 'conversion_method', conversion_method) + if fallback_reason: + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason', fallback_reason) + else: + with pytest.raises(AssertionError): # not called + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason', fallback_reason) + + + @generate(*[(True, None, None, 'mo_legacy', 'transformations_config'), # fallback + (True, True, None, 'mo_legacy', None), + (False, None, True, 'onnx_frontend', None), + (False, None, None, 'onnx_frontend', None), + ]) + def test_fallback_if_tranformations_config_specified(self, trans_config_used, use_legacy, use_new_fe, expected_path, fallback_reason): + with patch('openvino.tools.mo.main.get_default_frontends') as default_fe: + default_fe.return_value = get_test_default_frontends() + args = base_args_config(use_legacy, use_new_fe) + args.input_model = "test_model.onnx" + args.transformations_config = self.trans_config_file if trans_config_used else None + + prepare_ir(args) + + tm.Telemetry.send_event.assert_any_call('mo', 'conversion_method', expected_path) + if fallback_reason: + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason', fallback_reason) + else: + with pytest.raises(AssertionError): # not called + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason', fallback_reason) + + + @generate(*[('dir_to_extension', True, None, 'mo_legacy', 'extensions, transformations_config'), # fallback + (None, True, None, 'mo_legacy', 'transformations_config'), # fallback + ('dir_to_extension', False, None, 'mo_legacy', 'extensions'), # fallback + (None, False, True, 'onnx_frontend', None), + ]) + def test_fallback_if_both_extension_and_trans_config_specified(self, extension, trans_config_used, use_new_fe, expected_path, fallback_reason): + with patch('openvino.tools.mo.main.get_default_frontends') as default_fe: + default_fe.return_value = get_test_default_frontends() + args = base_args_config(use_new_fe=use_new_fe) + args.extensions = extension + args.input_model = "test_model.onnx" + args.transformations_config = self.trans_config_file if trans_config_used else None + + prepare_ir(args) + + tm.Telemetry.send_event.assert_any_call('mo', 'conversion_method', expected_path) + if fallback_reason: + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason', fallback_reason) + else: + with pytest.raises(AssertionError): # not called + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason', fallback_reason) + + + @generate(*[(True, None, None, 'mo_legacy'), + (True, True, None, 'mo_legacy'), + (False, None, True, 'onnx_frontend'), + ]) + def test_fallback_if_legacy_set_as_default(self, trans_config_used, use_legacy, use_new_fe, expected_path): + with patch('openvino.tools.mo.main.get_default_frontends') as default_fe: + default_fe.return_value = {'onnx': 'legacy', 'tf': 'legacy'} + args = base_args_config(use_legacy, use_new_fe) + args.input_model = "test_model.onnx" + args.transformations_config = self.trans_config_file if trans_config_used else None + + prepare_ir(args) + + tm.Telemetry.send_event.assert_any_call('mo', 'conversion_method', expected_path) + with pytest.raises(AssertionError): # not called + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason') + + + @generate(*[(None, None, 'dir_to_extension', 'paddle_frontend'), + (True, None, None, 'paddle_frontend'), + (None, None, None, 'paddle_frontend'), + ]) + def test_no_fallback_if_pdpd(self, use_new_fe, use_legacy, extension, expected_path): + args = base_args_config(use_legacy, use_new_fe) + args.framework = 'paddle' + args.extensions = extension + args.input_model = 'paddle_dir/relu/relu.pdmodel' + + prepare_ir(args) + + tm.Telemetry.send_event.assert_any_call('mo', 'conversion_method', expected_path) + with pytest.raises(AssertionError): # not called + tm.Telemetry.send_event.assert_any_call('mo', 'fallback_reason')