diff --git a/src/bindings/python/src/openvino/frontend/tensorflow/__init__.py b/src/bindings/python/src/openvino/frontend/tensorflow/__init__.py index 271bce0d94c..0f2ee54aff8 100644 --- a/src/bindings/python/src/openvino/frontend/tensorflow/__init__.py +++ b/src/bindings/python/src/openvino/frontend/tensorflow/__init__.py @@ -13,6 +13,7 @@ from openvino.utils import _add_openvino_libs_to_search_path _add_openvino_libs_to_search_path() try: + from openvino.frontend.tensorflow.py_tensorflow_frontend import _FrontEndPyGraphIterator as GraphIterator from openvino.frontend.tensorflow.py_tensorflow_frontend import ConversionExtensionTensorflow as ConversionExtension from openvino.frontend.tensorflow.py_tensorflow_frontend import OpExtensionTensorflow as OpExtension except ImportError as err: diff --git a/src/bindings/python/src/openvino/frontend/tensorflow/graph_iterator.py b/src/bindings/python/src/openvino/frontend/tensorflow/graph_iterator.py new file mode 100644 index 00000000000..e44b40f42df --- /dev/null +++ b/src/bindings/python/src/openvino/frontend/tensorflow/graph_iterator.py @@ -0,0 +1,89 @@ +# Copyright (C) 2018-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# flake8: noqa +# mypy: ignore-errors + +import tensorflow as tf +from openvino.frontend.tensorflow.node_decoder import TFGraphNodeDecoder +from openvino.frontend.tensorflow.py_tensorflow_frontend import _FrontEndPyGraphIterator as GraphIterator + + +class GraphIteratorTFGraph(GraphIterator): + def __init__(self, tf_graph: tf.Graph, inner_graph: bool = False): + GraphIterator.__init__(self) + self.m_graph = tf_graph + self.m_node_index = 0 + self.m_decoders = [] + self.m_inner_graph = inner_graph + + self.m_vars = None + if hasattr(tf_graph, "variables"): + # This field is needed to keep the link to graph variables, + # otherwise Python releases memory kept by variables when it is accessed from c++ bindings + self.m_vars = tf_graph.variables + + for op in tf_graph.get_operations(): + self.m_decoders.append(TFGraphNodeDecoder(op, inner_graph)) + + self.m_iterators = {} + for func_name, _ in self.m_graph._functions.items(): + self.m_iterators[func_name] = None + + def get_input_names(self) -> list: + inp_ops = filter(lambda op: op.type == "Placeholder", self.m_graph.get_operations()) + inp_names = [] + for inp in inp_ops: + assert isinstance(inp, tf.Operation), "Unknown node type. Expected tf.Operation, got {}".format(type(inp)) + assert hasattr(inp, "node_def") and isinstance(inp.node_def, tf.compat.v1.NodeDef), \ + "Could not find node_def in node {}".format(inp.name) + type_attr = inp.node_def.attr["dtype"].type + + # Placeholders with type "resource" have exact values in "variables" field, + # so they are passed to TF FE as constants. + # For this reason they are not listed as model inputs. + if tf.dtypes.DType(type_attr).name != "resource" or self.m_inner_graph: + inp_names.append(inp.name) + return inp_names + + def get_output_names(self) -> list: + # tf.Graph has ordered outputs which are stored in 'outputs' field, + # but using this field results in mismatch of outputs in inner graph and outputs in outer graph + # during the injection of subgraph. + # For this reason only nodes without outputs are considered graph outputs here + # as this approach does not lead to conflicts. + # The order of outputs is important and wrong order may lead to conversion error. + non_outputs = set() + for op in self.m_graph.get_operations(): + assert isinstance(op, tf.Operation), "Unknown node type. Expected tf.Operation, got {}".format(type(op)) + for inp in op.inputs: + non_outputs.add(inp.op.name) + + outputs = [] + for op in self.m_graph.get_operations(): + if op.name not in non_outputs: + for output in op.outputs: + outputs = [output.name] + outputs + return outputs + + def is_end(self) -> bool: + return self.m_node_index >= len(self.m_decoders) + + def reset(self): + self.m_node_index = 0 + + def size(self) -> int: + return len(self.m_decoders) + + def next_impl(self): + self.m_node_index += 1 + + def get_decoder(self): + return self.m_decoders[self.m_node_index] + + def get_body_graph_iterator(self, func_name): + if func_name not in self.m_iterators: + return None + if self.m_iterators[func_name] is None: + self.m_iterators[func_name] = GraphIteratorTFGraph(self.m_graph._functions[func_name].graph, True) + return self.m_iterators[func_name] diff --git a/src/bindings/python/src/openvino/frontend/tensorflow/node_decoder.py b/src/bindings/python/src/openvino/frontend/tensorflow/node_decoder.py new file mode 100644 index 00000000000..9a7429b46c7 --- /dev/null +++ b/src/bindings/python/src/openvino/frontend/tensorflow/node_decoder.py @@ -0,0 +1,157 @@ +# Copyright (C) 2018-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# flake8: noqa +# mypy: ignore-errors + +import numpy as np +import tensorflow as tf +from openvino.frontend.tensorflow.py_tensorflow_frontend import _FrontEndDecoderBase as DecoderBase +from openvino.runtime import PartialShape, Type, OVAny, Tensor + + +def tf_type_to_ov_type(tf_type_int): + tf_type = tf.dtypes.as_dtype(tf_type_int) + if tf_type.name == "variant": + return Type.dynamic + numpy_type = tf_type.as_numpy_dtype + try: + ret_type = Type(numpy_type) + except: + ret_type = Type.undefined + return ret_type + + +def tf_attr_to_numpy(attr): + attr_type = attr.WhichOneof("value") + if attr_type == "func": + return attr.func.name + if attr_type == "s": + return attr.s.decode("utf-8") + if attr_type == "f": + return np.float32(attr.f) + if attr_type == "type": + return tf_type_to_ov_type(attr.type) + if attr_type == "list": + list_value = attr.list + return list(list_value.ListFields()[0][1]) + if attr_type is None: + return None + return getattr(attr, attr.WhichOneof("value")) + + +def tf_attr_to_ov(attr): + return OVAny(tf_attr_to_numpy(attr)) + + +class TFGraphNodeDecoder(DecoderBase): + def __init__(self, operation: tf.Operation, inner_graph: bool): + DecoderBase.__init__(self) + assert isinstance(operation, tf.Operation), "Unknown operation type. " \ + "Expected tf.Operation, got {}".format(type(operation)) + self.m_operation = operation + self.m_inner_graph = inner_graph + if self.m_operation.type == "Const": + value = self.m_operation.node_def.attr["value"].tensor + # copies tensor value from node_def + self.m_parsed_content = tf.make_ndarray(value) + + if self.m_operation.type == "Placeholder": + data_type = self.m_operation.node_def.attr["dtype"].type + if tf.dtypes.DType(data_type).name == "resource" and not self.m_inner_graph: + variable_value = TFGraphNodeDecoder.get_variable(self.m_operation) + if variable_value is not None: + # does not copy data + self.m_parsed_content = variable_value.value().__array__() + + def get_op_name(self) -> str: + return self.m_operation.name + + def get_op_type(self) -> str: + if self.m_operation.type == "Placeholder": + type_attr = tf.dtypes.DType(self.m_operation.node_def.attr["dtype"].type) + if type_attr.name == "resource" and not self.m_inner_graph: + if TFGraphNodeDecoder.get_variable(self.m_operation) is not None: + return "Const" + raise Exception("Could not get variable for resource Placeholder {0}".format(self.m_operation.name)) + return self.m_operation.type + + @staticmethod + def get_variable(operation): + tf_graph = operation.graph + if not hasattr(tf_graph, "captures"): + return None + for var_tensor, op_tensor in tf_graph.captures: + if operation.outputs[0].name == op_tensor.name: + resource_name = var_tensor._name + for variable_value in operation.graph.variables: + if variable_value.name == resource_name: + return variable_value + return None + return None + + def get_attribute(self, name): + if name == "shape" or name == "_output_shapes": + if self.m_operation.node_def.attr["shape"].shape.unknown_rank: + return OVAny(PartialShape.dynamic()) + shape_dims = self.m_operation.node_def.attr["shape"].shape.dim + shape = [dim.size for dim in shape_dims] + type_num = self.m_operation.node_def.attr["dtype"].type + if type_num is not None and tf.dtypes.DType(type_num).name == "resource": + if self.m_inner_graph: + return OVAny(PartialShape.dynamic()) + variable_value = TFGraphNodeDecoder.get_variable(self.m_operation) + return OVAny(PartialShape(list(variable_value.shape))) + return OVAny(PartialShape(shape)) + if name == "dtype": + type_num = self.m_operation.node_def.attr["dtype"].type + if tf.dtypes.DType(type_num).name == "resource": + if not self.m_inner_graph: + variable_value = TFGraphNodeDecoder.get_variable(self.m_operation) + return OVAny(tf_type_to_ov_type(variable_value.dtype)) + else: + return OVAny(Type.undefined) + return OVAny(tf_type_to_ov_type(type_num)) + + if name == "value": + if self.m_parsed_content.size == 1: + if isinstance(self.m_parsed_content, np.ndarray): + return OVAny(Tensor(self.m_parsed_content)) + return OVAny(Tensor(np.array([self.m_parsed_content]), shape=[1])) + ov_tensor = Tensor(self.m_parsed_content, shared_memory=True) + ov_tensor = OVAny(ov_tensor) + return ov_tensor + attr_value = self.m_operation.node_def.attr[name] + + return tf_attr_to_ov(attr_value) + + def get_input_size(self) -> int: + return len(self.m_operation.inputs) + + def get_input_node_name(self, input_port_idx): + assert input_port_idx >= 0, "Got negative input node index." + assert input_port_idx < len(self.m_operation.inputs), "Input node index is out of range. Got {}, " \ + "when number of input nodes {}.".format(input_port_idx, + len(self.m_operation.inputs)) + return self.m_operation.inputs[input_port_idx].op.name + + def get_input_node_name_output_port_index(self, input_port_idx): + tensor_name = self.m_operation.inputs[input_port_idx].name + if ":" in tensor_name: + port_idx_str = tensor_name[tensor_name.rfind(":") + 1:len(tensor_name)] + if port_idx_str.isdigit(): + return int(port_idx_str) + else: + return 0 + return 0 + + def get_input_node_name_output_port_name(self, input_port_idx): + tensor_name = self.m_operation.inputs[input_port_idx].name + if ":" not in tensor_name: + return "" + first_col_idx = tensor_name.find(":") + last_col_idx = tensor_name.rfind(":") + if first_col_idx == last_col_idx: + return "" + + return tensor_name[first_col_idx + 1: last_col_idx] diff --git a/src/bindings/python/src/openvino/frontend/tensorflow/utils.py b/src/bindings/python/src/openvino/frontend/tensorflow/utils.py new file mode 100644 index 00000000000..d7df293fdcc --- /dev/null +++ b/src/bindings/python/src/openvino/frontend/tensorflow/utils.py @@ -0,0 +1,221 @@ +# Copyright (C) 2018-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# flake8: noqa +# mypy: ignore-errors + + +from openvino.tools.mo.moc_frontend.shape_utils import get_static_shape +from openvino.tools.mo.utils.versions_checker import get_environment_setup # pylint: disable=no-name-in-module +from openvino.tools.mo.utils.error import Error +from distutils.version import LooseVersion +import logging as log + + +def trace_tf_model_if_needed(input_model, placeholder_shapes, placeholder_data_types, example_input): + import tensorflow as tf + if not isinstance(input_model, (tf.keras.layers.Layer, tf.Module, tf.keras.Model, tf.types.experimental.GenericFunction)): + return input_model + return trace_tf_model(input_model, placeholder_shapes, placeholder_data_types, example_input) + + +def get_input_spec_from_model(model): + import tensorflow as tf + if hasattr(model, "_build_input_shape") and model._build_input_shape is not None: + if isinstance(model._build_input_shape, list): + input_spec = [[tf.TensorSpec(shape) for shape in model._build_input_shape]] + else: + input_spec = [tf.TensorSpec(model._build_input_shape)] + else: + input_spec = [tf.TensorSpec(None)] + return input_spec + + +def create_example_input_by_user_shapes(input_shapes, input_types): + import tensorflow as tf + if input_shapes is None: + return None + if isinstance(input_shapes, dict): + res = {} + for name, shape in input_shapes.items(): + shape = get_static_shape(shape, 1) + args = {} + if name in input_types: + args["dtype"] = input_types[name] + tensor = tf.zeros(shape=shape, **args) + res[name] = tensor + return res + elif isinstance(input_shapes, list): + res = [] + for idx, shape in enumerate(input_shapes): + shape = get_static_shape(shape, 1) + args = {} + if idx < len(input_types): + args["dtype"] = input_types[idx] + tensor = tf.zeros(shape=shape, **args) + res.append(tensor) + return res + raise Error("Could not create example input by provided shape {}".format(input_shapes)) + + +def get_concrete_func(tf_function, example_input, input_needs_packing, error_message, use_example_input=True): + """ + Runs tracing of TF function and returns a concrete function. + + :param tf_function: TF function that needs to be traced. + :param example_input: Example of function input. + :param input_needs_packing: determines if input needs to be packed in a list before passing to TF function. + It is used when original function was wrapped in outer TF function, which changes function signature. + In this case wrapper TF function always expects list of inputs which are unpacked inside subfunction. + So list/tuple are treated as multiple inputs of original model. + Non list/tuple are treated as single input, and it needs packing to a list, + as wrapper function always expect list of inputs. + :param error_message: Error message which should be shown in case of tracing error. + :param use_example_input: Determines if example_input should be used. + + :returns: Object of type tf.types.experimental.ConcreteFunction. + """ + if input_needs_packing and not isinstance(example_input, (list, tuple)): + example_input = [example_input] + try: + if use_example_input: + if not input_needs_packing and isinstance(example_input, (list, tuple)): + concrete_func = tf_function.get_concrete_function(*example_input) + else: + concrete_func = tf_function.get_concrete_function(example_input) + + else: + concrete_func = tf_function.get_concrete_function() + except Exception as e: + raise Exception(error_message.format(e)) + return concrete_func + + +def trace_tf_model(model, input_shapes, input_types, example_input): + import tensorflow as tf + if isinstance(model.__call__, tf.types.experimental.GenericFunction): + tf_function = model.__call__ + input_needs_packing = False + elif isinstance(model, tf.types.experimental.GenericFunction): + tf_function = model + input_needs_packing = False + else: + # Wrap model to tf.Function. + # In this case we loose input/output tensor names. + @tf.function + def tf_function(args): + return model(*args) + input_needs_packing = True + + if example_input is not None: + concrete_func = get_concrete_func(tf_function, example_input, input_needs_packing, + "Could not trace the TF model with the following error: {}") + elif input_shapes is not None: + inp = create_example_input_by_user_shapes(input_shapes, input_types) + concrete_func = get_concrete_func(tf_function, inp, input_needs_packing, + "Could not trace the TF model with the following error: {}") + else: + if isinstance(tf_function, tf.types.experimental.GenericFunction) and \ + tf_function.input_signature is not None: + concrete_func = get_concrete_func(tf_function, None, input_needs_packing, + "Could not trace the TF model with the following error: {}", + use_example_input=False) + else: + input_spec = get_input_spec_from_model(model) + concrete_func = get_concrete_func(tf_function, input_spec, input_needs_packing, + "Could not trace the TF model with the following error: {}.\n" + "Please provide 'example_input'.") + + return concrete_func + + +def type_supported_by_tf_fe(input_model): + import tensorflow as tf + # Types that require tracing + if isinstance(input_model, (tf.keras.layers.Layer, tf.Module, tf.keras.Model, tf.types.experimental.GenericFunction)): + return True + # Types that do not require tracing + if isinstance(input_model, (tf.Graph, tf.types.experimental.ConcreteFunction)): + return True + # GraphIterator + elif model_is_graph_iterator(input_model): + return True + return False + + +def create_tf_graph_iterator(input_model, placeholder_shapes, placeholder_data_types, example_input): + input_model = trace_tf_model_if_needed(input_model, placeholder_shapes, placeholder_data_types, example_input) + + import tensorflow as tf + from openvino.frontend.tensorflow.graph_iterator import GraphIteratorTFGraph + if model_is_graph_iterator(input_model): + return input_model + if isinstance(input_model, tf.Graph): + return GraphIteratorTFGraph(input_model) + elif isinstance(input_model, tf.types.experimental.ConcreteFunction): + return GraphIteratorTFGraph(input_model.graph) + raise Exception("Could not wrap model of type {} to GraphIteratorTFGraph.".format(type(input_model))) + + +def extract_model_graph(argv): + model = argv["input_model"] + import tensorflow as tf + trackable_is_imported = False + try: + from tensorflow.python.training.tracking.base import Trackable # pylint: disable=no-name-in-module,import-error + trackable_is_imported = True + except: + log.warning("Could not import tensorflow.python.training.tracking.base.Trackable type.") + env_setup = get_environment_setup("tf") + if isinstance(model, tf.Graph): + return True + if isinstance(model, tf.compat.v1.GraphDef): + graph = tf.Graph() + with graph.as_default(): + tf.graph_util.import_graph_def(model) + argv["input_model"] = graph + return True + if isinstance(model, tf.compat.v1.Session): + argv["input_model"] = model.graph + return True + if env_setup["tensorflow"] >= LooseVersion("2.6.0") and isinstance(model, (tf.types.experimental.GenericFunction, + tf.types.experimental.ConcreteFunction)): + return True + if isinstance(model, tf.train.Checkpoint): + if isinstance(model.root, tf.keras.Model): + argv["input_model"] = model.root + return True + else: + raise Error("Unknown checkpoint format.") + + if isinstance(model, (tf.keras.layers.Layer, tf.Module, tf.keras.Model)): + return True + if trackable_is_imported and isinstance(model, Trackable): + if hasattr(model, "signatures") and len(model.signatures.items()): + if "serving_default" in model.signatures: + argv["input_model"] = model.signatures["serving_default"] + elif "default" in model.signatures: + argv["input_model"] = model.signatures["default"] + else: + for signature_name, signature in model.signatures.items(): + argv["input_model"] = model.signatures[signature_name] + log.warning("Could not find the default signature. " + "The following signature was used for conversion: {}".format(signature_name)) + break + + elif hasattr(model, "graph"): + argv["input_model"] = model.graph + else: + raise Error("Could not find signature of graph in a Trackable object.") + return True + if model_is_graph_iterator(model): + return True + return False + + +def model_is_graph_iterator(model): + try: + from openvino.frontend.tensorflow.graph_iterator import GraphIteratorTFGraph + except: + return False + return isinstance(model, GraphIteratorTFGraph) diff --git a/src/bindings/python/src/pyopenvino/frontend/frontend.cpp b/src/bindings/python/src/pyopenvino/frontend/frontend.cpp index 0e79a203f4a..c908928409e 100644 --- a/src/bindings/python/src/pyopenvino/frontend/frontend.cpp +++ b/src/bindings/python/src/pyopenvino/frontend/frontend.cpp @@ -59,6 +59,21 @@ void regclass_frontend_FrontEnd(py::module m) { :rtype: openvino.frontend.InputModel )"); + fem.def( + "supported", + [](FrontEnd& self, const py::object& model) { + return self.supported({Common::utils::py_object_to_any(model)}); + }, + py::arg("model"), + R"( + Checks if model type is supported. + + :param model: Object describing the model. It can be path to model file. + :type model: Any + :return: True if model type is supported, otherwise False. + :rtype: bool + )"); + fem.def("convert", static_cast (FrontEnd::*)(const InputModel::Ptr&) const>(&FrontEnd::convert), py::arg("model"), diff --git a/src/bindings/python/src/pyopenvino/frontend/manager.cpp b/src/bindings/python/src/pyopenvino/frontend/manager.cpp index 388ecd40801..924c4889872 100644 --- a/src/bindings/python/src/pyopenvino/frontend/manager.cpp +++ b/src/bindings/python/src/pyopenvino/frontend/manager.cpp @@ -10,6 +10,7 @@ #include "openvino/frontend/exception.hpp" #include "pyopenvino/frontend/manager.hpp" +#include "pyopenvino/utils/utils.hpp" namespace py = pybind11; @@ -76,15 +77,19 @@ void regclass_frontend_FrontEndManager(py::module m) { fem.def( "load_by_model", - [](const std::shared_ptr& fem, const std::string& model_path) { - return fem->load_by_model(model_path); + [](const std::shared_ptr& fem, const py::object& model) { + if (py::isinstance(model, py::module_::import("pathlib").attr("Path"))) { + std::string model_path = Common::utils::convert_path_to_string(model); + return fem->load_by_model(model_path); + } + return fem->load_by_model({Common::utils::py_object_to_any(model)}); }, - py::arg("model_path"), + py::arg("model"), R"( - Selects and loads appropriate frontend depending on model file extension and other file info (header). + Selects and loads appropriate frontend depending on model type or model file extension and other file info (header). - :param model_path: A path to a model file/directory. - :type model_path: str + :param model_path: A model object or path to a model file/directory. + :type model_path: Any :return: Frontend interface for further loading of models. 'None' if no suitable frontend is found. :rtype: openvino.frontend.FrontEnd )"); diff --git a/src/bindings/python/src/pyopenvino/frontend/tensorflow/decoder_base.cpp b/src/bindings/python/src/pyopenvino/frontend/tensorflow/decoder_base.cpp new file mode 100644 index 00000000000..23ee8405574 --- /dev/null +++ b/src/bindings/python/src/pyopenvino/frontend/tensorflow/decoder_base.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2018-2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include +#include + +#include "decoder_base.hpp" + + +namespace py = pybind11; + +using namespace ov::frontend; +using ov::Any; + +void regclass_frontend_tensorflow_decoder_base(py::module m) { + py::class_> cls(m, "_FrontEndDecoderBase"); + cls.def(py::init<>()); + +} \ No newline at end of file diff --git a/src/bindings/python/src/pyopenvino/frontend/tensorflow/decoder_base.hpp b/src/bindings/python/src/pyopenvino/frontend/tensorflow/decoder_base.hpp new file mode 100644 index 00000000000..e2fc698d1ab --- /dev/null +++ b/src/bindings/python/src/pyopenvino/frontend/tensorflow/decoder_base.hpp @@ -0,0 +1,54 @@ +// Copyright (C) 2018-2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include "openvino/frontend/tensorflow/decoder.hpp" + +namespace py = pybind11; + +/// Trampoline class to support inheritance from GraphIterator in Python +class PyDecoderBase : public ov::frontend::tensorflow::DecoderBase { + + ov::Any get_attribute(const std::string &name) const override{ + PYBIND11_OVERRIDE_PURE(ov::Any, DecoderBase, get_attribute, name); + } + + size_t get_input_size() const override{ + PYBIND11_OVERRIDE_PURE(size_t, DecoderBase, get_input_size); + } + + + std::string get_input_node_name(size_t input_port_idx) const { + PYBIND11_OVERRIDE_PURE(std::string, DecoderBase, get_input_node_name, input_port_idx); + } + + size_t get_input_node_name_output_port_index(size_t input_port_idx) const { + PYBIND11_OVERRIDE_PURE(size_t, DecoderBase, get_input_node_name_output_port_index, input_port_idx); + } + + std::string get_input_node_name_output_port_name(size_t input_port_idx) const { + PYBIND11_OVERRIDE_PURE(std::string, DecoderBase, get_input_node_name_output_port_name, input_port_idx); + } + + void get_input_node(size_t input_port_idx, + std::string &producer_name, + std::string &producer_output_port_name, + size_t &producer_output_port_index) const override{ + producer_name = get_input_node_name(input_port_idx); + producer_output_port_index = get_input_node_name_output_port_index(input_port_idx); + producer_output_port_name = get_input_node_name_output_port_name(input_port_idx); + } + + const std::string &get_op_type() const override{ + PYBIND11_OVERRIDE_PURE(std::string&, DecoderBase, get_op_type); + } + + const std::string &get_op_name() const override{ + PYBIND11_OVERRIDE_PURE(std::string&, DecoderBase, get_op_name); + } +}; + +void regclass_frontend_tensorflow_decoder_base(py::module m); \ No newline at end of file diff --git a/src/bindings/python/src/pyopenvino/frontend/tensorflow/graph_iterator.cpp b/src/bindings/python/src/pyopenvino/frontend/tensorflow/graph_iterator.cpp new file mode 100644 index 00000000000..2476f54c146 --- /dev/null +++ b/src/bindings/python/src/pyopenvino/frontend/tensorflow/graph_iterator.cpp @@ -0,0 +1,22 @@ +// Copyright (C) 2018-2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include +#include + +#include "graph_iterator.hpp" + +#include "openvino/frontend/graph_iterator.hpp" + +namespace py = pybind11; + +using namespace ov::frontend; +using ov::Any; + +void regclass_frontend_tensorflow_graph_iterator(py::module m) { + py::class_>(m, "_FrontEndPyGraphIterator") + .def(py::init<>()); + +} \ No newline at end of file diff --git a/src/bindings/python/src/pyopenvino/frontend/tensorflow/graph_iterator.hpp b/src/bindings/python/src/pyopenvino/frontend/tensorflow/graph_iterator.hpp new file mode 100644 index 00000000000..e417f83c528 --- /dev/null +++ b/src/bindings/python/src/pyopenvino/frontend/tensorflow/graph_iterator.hpp @@ -0,0 +1,66 @@ +// Copyright (C) 2018-2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include "openvino/frontend/graph_iterator.hpp" +#include "openvino/frontend/decoder.hpp" + +namespace py = pybind11; + +/// Trampoline class to support inheritance from GraphIterator in Python +class PyGraphIterator : public ov::frontend::tensorflow::GraphIterator { + /* Inherit the constructors */ + using ov::frontend::tensorflow::GraphIterator::GraphIterator; + + /// \brief Get a number of operation nodes in the graph + size_t size() const override{ + PYBIND11_OVERRIDE_PURE(size_t, GraphIterator, size); + } + + /// \brief Set iterator to the start position + void reset() override { + PYBIND11_OVERRIDE_PURE(void, GraphIterator, reset); + } + + /// \brief Move to the next node in the graph + void next() override { + next_impl(); + } + + /// Implementation of next method, it is needed to be in separate method to avoid shadowing of Python "next" operator. + void next_impl() { + PYBIND11_OVERRIDE_PURE(void, GraphIterator, next_impl); + } + + /// \brief Returns true if iterator goes out of the range of available nodes + bool is_end() const override { + PYBIND11_OVERRIDE_PURE(bool, GraphIterator, is_end); + } + + /// \brief Return a pointer to a decoder of the current node + std::shared_ptr get_decoder() const override{ + PYBIND11_OVERRIDE_PURE(std::shared_ptr, GraphIterator, get_decoder); + } + + /// \brief Checks if the main model graph contains a function of the requested name in the library + /// Returns GraphIterator to this function and nullptr, if it does not exist + std::shared_ptr get_body_graph_iterator(const std::string& func_name) const override{ + PYBIND11_OVERRIDE_PURE(std::shared_ptr, GraphIterator, get_body_graph_iterator, func_name); + } + + /// \brief Returns a vector of input names in the original order + std::vector get_input_names() const override{ + PYBIND11_OVERRIDE_PURE(std::vector, GraphIterator, get_input_names); + + } + + /// \brief Returns a vector of output names in the original order + std::vector get_output_names() const override{ + PYBIND11_OVERRIDE_PURE(std::vector, GraphIterator, get_output_names); + } +}; + +void regclass_frontend_tensorflow_graph_iterator(py::module m); \ No newline at end of file diff --git a/src/bindings/python/src/pyopenvino/frontend/tensorflow/py_module.cpp b/src/bindings/python/src/pyopenvino/frontend/tensorflow/py_module.cpp index ab81b9a39d5..66f03d7421f 100644 --- a/src/bindings/python/src/pyopenvino/frontend/tensorflow/py_module.cpp +++ b/src/bindings/python/src/pyopenvino/frontend/tensorflow/py_module.cpp @@ -7,10 +7,14 @@ #include #include "extension.hpp" +#include "graph_iterator.hpp" +#include "decoder_base.hpp" namespace py = pybind11; PYBIND11_MODULE(py_tensorflow_frontend, m) { regclass_frontend_tensorflow_ConversionExtension(m); regclass_frontend_tensorflow_OpExtension(m); + regclass_frontend_tensorflow_graph_iterator(m); + regclass_frontend_tensorflow_decoder_base(m); } diff --git a/src/bindings/python/src/pyopenvino/utils/utils.cpp b/src/bindings/python/src/pyopenvino/utils/utils.cpp index 53d93396c5a..1b3764eac85 100644 --- a/src/bindings/python/src/pyopenvino/utils/utils.cpp +++ b/src/bindings/python/src/pyopenvino/utils/utils.cpp @@ -15,6 +15,7 @@ #include "meta_data.hpp" #include "openvino/core/except.hpp" #include "openvino/frontend/decoder.hpp" +#include "openvino/frontend/graph_iterator.hpp" using Version = ov::pass::Serialize::Version; @@ -284,17 +285,22 @@ ov::AnyMap py_object_to_any_map(const py::object& py_obj) { ov::Any py_object_to_any(const py::object& py_obj) { // Python types + py::object float_32_type = py::module_::import("numpy").attr("float32"); if (py::isinstance(py_obj)) { return py_obj.cast(); } else if (py::isinstance(py_obj)) { return py_obj.cast(); } else if (py::isinstance(py_obj)) { return py_obj.cast(); + } else if (py::isinstance(py_obj, float_32_type)) { + return py_obj.cast(); } else if (py::isinstance(py_obj)) { return py_obj.cast(); + } else if (py::isinstance(py_obj)) { + return {}; } else if (py::isinstance(py_obj)) { auto _list = py_obj.cast(); - enum class PY_TYPE : int { UNKNOWN = 0, STR, INT, FLOAT, BOOL }; + enum class PY_TYPE : int { UNKNOWN = 0, STR, INT, FLOAT, BOOL, PARTIAL_SHAPE }; PY_TYPE detected_type = PY_TYPE::UNKNOWN; for (const auto& it : _list) { auto check_type = [&](PY_TYPE type) { @@ -312,6 +318,8 @@ ov::Any py_object_to_any(const py::object& py_obj) { check_type(PY_TYPE::FLOAT); } else if (py::isinstance(it)) { check_type(PY_TYPE::BOOL); + } else if (py::isinstance(it)) { + check_type(PY_TYPE::PARTIAL_SHAPE); } } @@ -327,6 +335,8 @@ ov::Any py_object_to_any(const py::object& py_obj) { return _list.cast>(); case PY_TYPE::BOOL: return _list.cast>(); + case PY_TYPE::PARTIAL_SHAPE: + return _list.cast>(); default: OPENVINO_ASSERT(false, "Unsupported attribute type."); } @@ -337,6 +347,8 @@ ov::Any py_object_to_any(const py::object& py_obj) { return py::cast(py_obj); } else if (py::isinstance(py_obj)) { return py::cast(py_obj); + } else if (py::isinstance(py_obj)) { + return py::cast(py_obj); } else if (py::isinstance(py_obj)) { return py::cast(py_obj); } else if (py::isinstance(py_obj)) { @@ -351,9 +363,14 @@ ov::Any py_object_to_any(const py::object& py_obj) { return py::cast(py_obj); } else if (py::isinstance(py_obj)) { return py::cast(py_obj); + } else if (py::isinstance(py_obj)) { + return py::cast(py_obj); // FrontEnd Decoder } else if (py::isinstance(py_obj)) { return py::cast>(py_obj); + // TF FrontEnd GraphIterator + } else if (py::isinstance(py_obj)) { + return py::cast>(py_obj); // Custom FrontEnd Types } else if (py::isinstance(py_obj)) { return py::cast(py_obj); diff --git a/src/bindings/python/tests/test_frontend/test_frontendmanager.py b/src/bindings/python/tests/test_frontend/test_frontendmanager.py index 48fd5ed0c4d..e1ed922bf0b 100644 --- a/src/bindings/python/tests/test_frontend/test_frontendmanager.py +++ b/src/bindings/python/tests/test_frontend/test_frontendmanager.py @@ -123,6 +123,18 @@ def test_load_by_model(): assert stat.supported == 1 +@mock_needed +def test_load_by_model_path(): + clear_all_stat() + import pathlib + fe = fem.load_by_model(pathlib.Path("abc.test_mock_py_mdl")) + assert fe is not None + assert fe.get_name() == MOCK_PY_FRONTEND_NAME + stat = get_fe_stat() + assert stat.get_name == 1 + assert stat.supported == 1 + + @mock_needed def test_convert_model(): clear_all_stat() diff --git a/src/frontends/common/include/openvino/frontend/decoder.hpp b/src/frontends/common/include/openvino/frontend/decoder.hpp index d51f41844f6..ecf06f0dbf6 100644 --- a/src/frontends/common/include/openvino/frontend/decoder.hpp +++ b/src/frontends/common/include/openvino/frontend/decoder.hpp @@ -48,7 +48,7 @@ public: virtual ~IDecoder() = default; }; -class FRONTEND_API DecoderBase { +class FRONTEND_API DecoderBase : public IDecoder { public: using OpTypeByName = std::unordered_map; /// \brief Get attribute value by name diff --git a/src/frontends/common/include/openvino/frontend/frontend.hpp b/src/frontends/common/include/openvino/frontend/frontend.hpp index 9cc9ef6160f..30717a0ecdd 100644 --- a/src/frontends/common/include/openvino/frontend/frontend.hpp +++ b/src/frontends/common/include/openvino/frontend/frontend.hpp @@ -52,6 +52,9 @@ public: inline bool supported(const Types&... vars) const { return supported_impl({ov::Any(vars)...}); } + inline bool supported(const ov::AnyVector& vars) const { + return supported_impl(vars); + } /// \brief Loads an input model by any specified arguments. Each FrontEnd separately /// defines what arguments it can accept. diff --git a/src/frontends/tensorflow/include/openvino/frontend/tensorflow/graph_iterator.hpp b/src/frontends/common/include/openvino/frontend/graph_iterator.hpp similarity index 90% rename from src/frontends/tensorflow/include/openvino/frontend/tensorflow/graph_iterator.hpp rename to src/frontends/common/include/openvino/frontend/graph_iterator.hpp index dd7f7f3697f..9e3b5a6372b 100644 --- a/src/frontends/tensorflow/include/openvino/frontend/tensorflow/graph_iterator.hpp +++ b/src/frontends/common/include/openvino/frontend/graph_iterator.hpp @@ -5,15 +5,15 @@ #pragma once #include "openvino/core/any.hpp" -#include "openvino/frontend/tensorflow/decoder.hpp" -#include "openvino/frontend/tensorflow/visibility.hpp" +#include "openvino/frontend/decoder.hpp" +#include "openvino/frontend/visibility.hpp" namespace ov { namespace frontend { namespace tensorflow { /// Abstract representation for an input model graph that gives nodes in topologically sorted order -class TENSORFLOW_API GraphIterator : ov::RuntimeAttribute { +class FRONTEND_API GraphIterator : ::ov::RuntimeAttribute { public: OPENVINO_RTTI("Variant::GraphIterator"); diff --git a/src/frontends/common/include/openvino/frontend/input_model.hpp b/src/frontends/common/include/openvino/frontend/input_model.hpp index b0df0cbe1ee..58529e4fa2b 100644 --- a/src/frontends/common/include/openvino/frontend/input_model.hpp +++ b/src/frontends/common/include/openvino/frontend/input_model.hpp @@ -10,6 +10,7 @@ #include "openvino/core/partial_shape.hpp" #include "openvino/core/type/element_type.hpp" +#include "openvino/frontend/graph_iterator.hpp" #include "openvino/frontend/place.hpp" #include "openvino/frontend/visibility.hpp" diff --git a/src/frontends/common/include/openvino/frontend/manager.hpp b/src/frontends/common/include/openvino/frontend/manager.hpp index 837e5eb8d6d..36dc807726b 100644 --- a/src/frontends/common/include/openvino/frontend/manager.hpp +++ b/src/frontends/common/include/openvino/frontend/manager.hpp @@ -59,6 +59,9 @@ public: FrontEnd::Ptr load_by_model(const Types&... vars) { return load_by_model_impl({ov::Any{vars}...}); } + FrontEnd::Ptr load_by_model(const std::vector& variants) { + return load_by_model_impl(variants); + } /// \brief Gets list of registered frontends. Any not loaded frontends will be loaded by this call std::vector get_available_front_ends(); diff --git a/src/frontends/tensorflow/src/frontend.cpp b/src/frontends/tensorflow/src/frontend.cpp index c280ad9543c..b47aef03c91 100644 --- a/src/frontends/tensorflow/src/frontend.cpp +++ b/src/frontends/tensorflow/src/frontend.cpp @@ -16,8 +16,8 @@ #include "helper_transforms/saved_model_unused_remover.hpp" #include "input_model.hpp" #include "op_table.hpp" +#include "openvino/frontend/graph_iterator.hpp" #include "openvino/frontend/tensorflow/extension/conversion.hpp" -#include "openvino/frontend/tensorflow/graph_iterator.hpp" #include "openvino/op/util/multi_subgraph_base.hpp" #include "openvino/pass/manager.hpp" #include "openvino/util/common_util.hpp" diff --git a/src/frontends/tensorflow/src/graph_iterator_proto.hpp b/src/frontends/tensorflow/src/graph_iterator_proto.hpp index a5da5277269..43ddb485e03 100644 --- a/src/frontends/tensorflow/src/graph_iterator_proto.hpp +++ b/src/frontends/tensorflow/src/graph_iterator_proto.hpp @@ -11,8 +11,8 @@ #include "decoder_proto.hpp" #include "graph.pb.h" #include "openvino/frontend/exception.hpp" +#include "openvino/frontend/graph_iterator.hpp" #include "openvino/frontend/tensorflow/decoder.hpp" -#include "openvino/frontend/tensorflow/graph_iterator.hpp" namespace ov { namespace frontend { diff --git a/src/frontends/tensorflow/src/input_model.cpp b/src/frontends/tensorflow/src/input_model.cpp index 766ec29fb3f..27a184368bb 100644 --- a/src/frontends/tensorflow/src/input_model.cpp +++ b/src/frontends/tensorflow/src/input_model.cpp @@ -9,7 +9,7 @@ #include #include "openvino/frontend/exception.hpp" -#include "openvino/frontend/tensorflow/graph_iterator.hpp" +#include "openvino/frontend/graph_iterator.hpp" #include "openvino/frontend/tensorflow/node_context.hpp" #include "openvino/opsets/opset7.hpp" #include "openvino/util/log.hpp" diff --git a/src/frontends/tensorflow/src/input_model.hpp b/src/frontends/tensorflow/src/input_model.hpp index 7acd8f3c148..179f244b8bf 100644 --- a/src/frontends/tensorflow/src/input_model.hpp +++ b/src/frontends/tensorflow/src/input_model.hpp @@ -5,8 +5,8 @@ #pragma once #include "openvino/frontend/extension/telemetry.hpp" +#include "openvino/frontend/graph_iterator.hpp" #include "openvino/frontend/input_model.hpp" -#include "openvino/frontend/tensorflow/graph_iterator.hpp" #include "place.hpp" #include "translate_session.hpp" diff --git a/tests/layer_tests/mo_python_api_tests/mo_convert_legacy_extensions_test_actual.py b/tests/layer_tests/mo_python_api_tests/mo_convert_legacy_extensions_test_actual.py index 5b26a73cc4c..dce52a204da 100644 --- a/tests/layer_tests/mo_python_api_tests/mo_convert_legacy_extensions_test_actual.py +++ b/tests/layer_tests/mo_python_api_tests/mo_convert_legacy_extensions_test_actual.py @@ -66,8 +66,9 @@ class LegacyExtTest(unittest.TestCase): with tempfile.TemporaryDirectory(dir=self.test_directory) as tmpdir: ext_path1 = os.path.join(os.path.dirname(__file__), "test_legacy_exts/test_exts_dir1") ext_path2 = os.path.join(os.path.dirname(__file__), "test_legacy_exts/test_exts_dir2") - model = create_tf_model() - out_xml = os.path.join(tmpdir, "model.xml") + tf_model = create_tf_model() + tf.io.write_graph(tf_model, tmpdir, 'model.pb', False) + model = os.path.join(tmpdir, 'model.pb') # tests for convert_model() ov_model = convert_model(model, extensions=ext_path1) @@ -82,13 +83,11 @@ class LegacyExtTest(unittest.TestCase): flag, msg = compare_functions(ov_model, create_ref_model_2(), False) assert flag, msg - tf.io.write_graph(model, tmpdir, 'model.pb', False) - inp_model = os.path.join(tmpdir, 'model.pb') from openvino.runtime import Core core = Core() # tests for MO cli tool - exit_code, stderr = generate_ir(coverage=False, **{"input_model": inp_model, + exit_code, stderr = generate_ir(coverage=False, **{"input_model": model, "extensions": ext_path1, "output_dir": tmpdir}) assert not exit_code @@ -97,7 +96,7 @@ class LegacyExtTest(unittest.TestCase): flag, msg = compare_functions(ov_model, create_ref_model_1(), False) assert flag, msg - exit_code, stderr = generate_ir(coverage=False, **{"input_model": inp_model, + exit_code, stderr = generate_ir(coverage=False, **{"input_model": model, "extensions": ','.join([ext_path1, ext_path2]), "output_dir": tmpdir}) assert not exit_code diff --git a/tests/layer_tests/mo_python_api_tests/test_mo_convert_tf.py b/tests/layer_tests/mo_python_api_tests/test_mo_convert_tf.py index 986758a22c3..849eb94d191 100644 --- a/tests/layer_tests/mo_python_api_tests/test_mo_convert_tf.py +++ b/tests/layer_tests/mo_python_api_tests/test_mo_convert_tf.py @@ -178,9 +178,11 @@ def create_tf_module_dynamic(tmp_dir): def __call__(self, x, y): return tf.nn.sigmoid(tf.nn.relu(x + y)) - shape = PartialShape([-1, 3, 4]) - param1 = ov.opset8.parameter(shape, dtype=np.float32) - param2 = ov.opset8.parameter(shape, dtype=np.float32) + input_shapes = [PartialShape([-1, Dimension(3, -1), Dimension(4)]), + PartialShape([-1, Dimension(3), Dimension(4, -1)])] + + param1 = ov.opset8.parameter(input_shapes[0], dtype=np.float32) + param2 = ov.opset8.parameter(input_shapes[1], dtype=np.float32) add = ov.opset8.add(param1, param2) relu = ov.opset8.relu(add) sigm = ov.opset8.sigmoid(relu) @@ -189,8 +191,7 @@ def create_tf_module_dynamic(tmp_dir): model_ref = Model([sigm], parameter_list, "test") net = Net() - return net, model_ref, {'input_shape': [PartialShape([-1, Dimension(3, -1), Dimension(4)]), - PartialShape([-1, Dimension(3), Dimension(4, -1)])]} + return net, model_ref, {'input_shape': input_shapes} def create_keras_layer(tmp_dir): @@ -227,9 +228,11 @@ def create_keras_layer_dynamic(tmp_dir): def call(self, x, y): return tf.sigmoid(tf.nn.relu(x + y)) - shape = PartialShape([-1, 3, 4]) - param1 = ov.opset8.parameter(shape, dtype=np.float32) - param2 = ov.opset8.parameter(shape, dtype=np.float32) + input_shapes = [PartialShape([-1, Dimension(3, -1), Dimension(4)]), + PartialShape([-1, Dimension(3), Dimension(4, -1)])] + + param1 = ov.opset8.parameter(input_shapes[0], dtype=np.float32) + param2 = ov.opset8.parameter(input_shapes[1], dtype=np.float32) add = ov.opset8.add(param1, param2) relu = ov.opset8.relu(add) sigm = ov.opset8.sigmoid(relu) @@ -238,8 +241,7 @@ def create_keras_layer_dynamic(tmp_dir): model_ref = Model([sigm], parameter_list, "test") net = LayerModel() - return net, model_ref, {'input_shape': [PartialShape([-1, Dimension(3, -1), Dimension(4)]), - PartialShape([-1, Dimension(3), Dimension(4, -1)])]} + return net, model_ref, {'input_shape': input_shapes} def create_tf_checkpoint(tmp_dir): @@ -345,6 +347,246 @@ def create_tf_saved_model_dir(temp_dir): return temp_dir + "/model", model_ref +def create_tf_stateful_partioned_call_net(temp_dir): + import tensorflow as tf + tf.compat.v1.reset_default_graph() + + data_shape = [1, 1, 10, 10] + filters_shape = [3, 3, 1, 1] + + strides = [1, 1] + pads_begin = [0, 0] + pads_end = [0, 0] + dilations = [1, 1] + + @tf.function + def second_func(input, filter): + conv = tf.raw_ops.Conv2D(input=input, filter=filter, strides=[1, 1, 1, 1], padding='SAME', data_format='NCHW') + return conv + + @tf.function( + input_signature=[tf.TensorSpec(shape=data_shape, dtype=tf.float32), + tf.TensorSpec(shape=filters_shape, dtype=tf.float32)]) + def first_func(input, filter): + conv = second_func(input, filter) + return conv + + tf_model = first_func + + param1 = ov.opset8.parameter(data_shape, dtype=np.float32) + param2 = ov.opset8.parameter(filters_shape, dtype=np.float32) + transpose2 = ov.opset8.transpose(param2, np.array([3, 2, 0, 1])) + conv = ov.opset11.convolution(param1, transpose2, strides, pads_begin, pads_end, dilations, auto_pad="same_upper") + + parameter_list = [param1, param2] + model_ref = Model([conv], parameter_list, "test") + + return tf_model, model_ref, {} + + +def create_keras_layer_input_list(): + import tensorflow as tf + class LayerModel(tf.keras.layers.Layer): + + def __init__(self): + super(LayerModel, self).__init__() + + def call(self, x, y): + res_list = [tf.sigmoid(tf.nn.relu(x + y)), tf.nn.relu(x), tf.sigmoid(y)] + return res_list + + input_shapes = [PartialShape([1, 2, 3]), + PartialShape([1, 2, 3])] + + param1 = ov.opset8.parameter(input_shapes[0], dtype=np.float32) + param2 = ov.opset8.parameter(input_shapes[1], dtype=np.float32) + add = ov.opset8.add(param1, param2) + relu1 = ov.opset8.relu(add) + sigm1 = ov.opset8.sigmoid(relu1) + relu2 = ov.opset8.relu(param1) + sigm2 = ov.opset8.sigmoid(param2) + + parameter_list = [param1, param2] + model_ref = Model([sigm1, relu2, sigm2], parameter_list, "test") + return LayerModel(), model_ref + + +def create_keras_layer_input_list_one_inp(): + import tensorflow as tf + class LayerModel(tf.keras.layers.Layer): + + def __init__(self): + super(LayerModel, self).__init__() + + def call(self, x): + res_list = [tf.sigmoid(tf.nn.relu(x)), tf.nn.relu(x)] + return res_list + + input_shapes = [PartialShape([1,2,3])] + + param1 = ov.opset8.parameter(input_shapes[0], dtype=np.float32) + relu1 = ov.opset8.relu(param1) + sigm1 = ov.opset8.sigmoid(relu1) + parameter_list = [param1] + model_ref = Model([sigm1, relu1], parameter_list, "test") + + return LayerModel(), model_ref + + +def create_keras_layer_input_dict(): + import tensorflow as tf + class LayerModel(tf.keras.layers.Layer): + + def __init__(self): + super(LayerModel, self).__init__() + + def call(self, args): + res = {} + res['result'] = tf.sigmoid(tf.nn.relu(args['a'] + args['b'])) + return res + + input_shapes = [PartialShape([1, 2, 3]), + PartialShape([1, 2, 3])] + + param1 = ov.opset8.parameter(input_shapes[0], dtype=np.float32) + param2 = ov.opset8.parameter(input_shapes[1], dtype=np.float32) + add = ov.opset8.add(param1, param2) + relu1 = ov.opset8.relu(add) + sigm1 = ov.opset8.sigmoid(relu1) + + parameter_list = [param1, param2] + model_ref = Model([sigm1], parameter_list, "test") + return LayerModel(), model_ref + + +def create_keras_layer_input_dict_one_inp(): + import tensorflow as tf + class LayerModel(tf.keras.layers.Layer): + + def __init__(self): + super(LayerModel, self).__init__() + + def call(self, args): + res = {} + res['result'] = tf.sigmoid(tf.nn.relu(args['args'])) + return res + + input_shapes = [PartialShape([1, 2, 3]), + PartialShape([1, 2, 3])] + + param1 = ov.opset8.parameter(input_shapes[0], dtype=np.float32) + param2 = ov.opset8.parameter(input_shapes[1], dtype=np.float32) + relu1 = ov.opset8.relu(param1) + sigm1 = ov.opset8.sigmoid(relu1) + parameter_list = [param1, param2] + model_ref = Model([sigm1], parameter_list, "test") + return LayerModel(), model_ref + + +def single_param_function_reference(shape, const_value): + param1 = ov.opset8.parameter(shape, dtype=np.float32) + const = ov.opset8.constant(const_value, dtype=np.float32) + sigm = ov.opset8.sigmoid(param1) + mul = ov.opset8.multiply(sigm, const) + parameter_list = [param1] + return Model([mul], parameter_list, "test") + + +def two_params_function_reference(shapes, const_value): + param1 = ov.opset8.parameter(shapes[0], dtype=np.float32) + param2 = ov.opset8.parameter(shapes[1], dtype=np.float32) + const = ov.opset8.constant(const_value, dtype=np.float32) + sigm = ov.opset8.sigmoid(param1) + add = ov.opset8.add(sigm, param2) + mul = ov.opset8.multiply(add, const) + parameter_list = [param1, param2] + return Model([mul], parameter_list, "test") + + +def create_keras_layer_with_example_input_1(tmp_dir): + model, model_ref = create_keras_layer_input_list() + example_input = (np.random.rand(1,2,3).astype(np.float32), np.random.rand(1,2,3).astype(np.float32)) + return model, model_ref, {'example_input': example_input} + + +def create_keras_layer_with_example_input_2(tmp_dir): + model, model_ref = create_keras_layer_input_dict() + example_input = {'a': np.random.rand(1,2,3).astype(np.float32), 'b': np.random.rand(1,2,3).astype(np.float32)} + return model, model_ref, {'example_input': example_input} + + +def create_keras_layer_with_input_shapes_case1(tmp_dir): + model, model_ref = create_keras_layer_input_list() + return model, model_ref, {'input_shape': [[1, 2, 3], [1, 2, 3]]} + + +def create_keras_layer_with_input_shapes_case2(tmp_dir): + model, model_ref = create_keras_layer_input_list() + return model, model_ref, {'input': [([1, 2, 3], np.float32), ([1, 2, 3], np.float32)]} + + +def create_keras_layer_with_input_shapes_case3(tmp_dir): + model, model_ref = create_keras_layer_input_dict_one_inp() + return model, model_ref, {'input': ['args'], 'input_shape': [1, 2, 3]} + + +def create_keras_layer_with_input_shapes_case4(tmp_dir): + model, model_ref = create_keras_layer_input_list_one_inp() + return model, model_ref, {'input': [1, 2, 3]} + + +def create_keras_layer_with_tf_function_call(tmp_dir): + import tensorflow as tf + class LayerModel(tf.Module): + def __init__(self): + super(LayerModel, self).__init__() + self.var1 = tf.Variable(5.0) + + @tf.function(input_signature=[tf.TensorSpec([1, 2], tf.float32), tf.TensorSpec([1, 2], tf.float32)]) + def __call__(self, input1, input2): + sigm = tf.nn.sigmoid(input1) + input2 + return sigm * self.var1 + model = LayerModel() + model_ref = two_params_function_reference([[1, 2], [1, 2]], [[5.0]]) + return model, model_ref, {} + + +def create_keras_layer_with_tf_function_call_no_signature(tmp_dir): + import tensorflow as tf + class LayerModel(tf.Module): + def __init__(self): + super(LayerModel, self).__init__() + self.var1 = tf.Variable(5.0) + + @tf.function() + def __call__(self, input1, input2): + sigm = tf.nn.sigmoid(input1) + input2 + return sigm * self.var1 + model = LayerModel() + example_input = [np.random.rand(2, 3).astype(np.float32), np.random.rand(2, 3).astype(np.float32)] + + model_ref = two_params_function_reference([[2, 3], [2, 3]], [[5.0]]) + return model, model_ref, {'example_input': example_input} + + +def create_keras_layer_with_tf_function_call_no_signature_single_input(tmp_dir): + import tensorflow as tf + class LayerModel(tf.Module): + def __init__(self): + super(LayerModel, self).__init__() + self.var1 = tf.Variable(5.0) + + @tf.function() + def __call__(self, input1): + sigm = tf.nn.sigmoid(input1) + return sigm * self.var1 + model = LayerModel() + example_input = np.random.rand(2, 3).astype(np.float32) + + model_ref = single_param_function_reference([2, 3], [[5.0]]) + return model, model_ref, {'example_input': example_input} + + class TestMoConvertTF(CommonMOConvertTest): test_data = [ # TF2 @@ -356,6 +598,16 @@ class TestMoConvertTF(CommonMOConvertTest): create_keras_layer_dynamic, create_tf_module_dynamic, create_tf_module_layout_list, + create_tf_stateful_partioned_call_net, + create_keras_layer_with_example_input_1, + create_keras_layer_with_example_input_2, + create_keras_layer_with_input_shapes_case1, + create_keras_layer_with_input_shapes_case2, + create_keras_layer_with_input_shapes_case3, + create_keras_layer_with_input_shapes_case4, + create_keras_layer_with_tf_function_call, + create_keras_layer_with_tf_function_call_no_signature, + create_keras_layer_with_tf_function_call_no_signature_single_input, # TF1 create_tf_graph, @@ -364,7 +616,14 @@ class TestMoConvertTF(CommonMOConvertTest): create_tf_session, ] - @pytest.mark.parametrize("create_model", test_data) + test_data_legacy = [ + # TF2 + create_keras_model, + create_tf_function, + create_tf_checkpoint, + ] + + @pytest.mark.parametrize("create_model", test_data_legacy) @pytest.mark.nightly @pytest.mark.precommit_tf_fe @pytest.mark.precommit @@ -414,6 +673,28 @@ class TFConvertTest(unittest.TestCase): y = tf.nn.sigmoid(tf.nn.relu(x1 + x2)) return y - with self.assertRaisesRegex(AssertionError, - ".*'input_signature' needs to be set for model conversion.*"): + with self.assertRaisesRegex(Exception, ".*Please provide 'example_input'.*"): convert_model(function) + + +class TestTFLoadByModel(unittest.TestCase): + def test_load_by_model_tf_graph_iterator(self): + def simple_tf_model(): + import tensorflow as tf + + tf.compat.v1.reset_default_graph() + + with tf.compat.v1.Session() as sess: + inp = tf.compat.v1.placeholder(tf.float32, [1, 2, 3], "Input") + _ = tf.nn.sigmoid(inp, name="Sigmoid") + + tf.compat.v1.global_variables_initializer() + tf_net = sess.graph + return tf_net + from openvino.frontend.tensorflow.graph_iterator import GraphIteratorTFGraph + from openvino.frontend import FrontEndManager + model = GraphIteratorTFGraph(simple_tf_model()) + fem = FrontEndManager() + fe = fem.load_by_model(model) + assert fe is not None + assert fe.get_name() == "tf" diff --git a/tools/mo/openvino/tools/mo/convert.py b/tools/mo/openvino/tools/mo/convert.py index da65e8c5152..bc8e76fdd19 100644 --- a/tools/mo/openvino/tools/mo/convert.py +++ b/tools/mo/openvino/tools/mo/convert.py @@ -25,6 +25,7 @@ def convert_model( input: [str, list, tuple, InputCutInfo] = None, output: [str, list] = None, input_shape: [str, PartialShape, Shape, list] = None, + example_input: Any = None, batch: int = None, mean_values: [str, dict, list] = (), scale_values: [str, dict, list] = (), @@ -44,12 +45,8 @@ def convert_model( stream_output: bool = False, # PaddlePaddle-specific parameters: - # example_input: Any = None, which can be shared with PyTorch-specific parameters example_output: Any = None, - # PyTorch-specific parameters: - example_input: Any = None, - # TensorFlow*-specific parameters input_model_is_text: bool = None, input_checkpoint: [str, pathlib.Path] = None, @@ -166,6 +163,11 @@ def convert_model( for each input separated by a comma, for example: [1,3,227,227],[2,4] for a model with two inputs with 4D and 2D shapes. Alternatively, specify shapes with the --input option. + :param example_input: + Sample of model input in original framework. + For PyTorch it can be torch.Tensor. + For Tensorflow it can be tf.Tensor or numpy.ndarray. + For PaddlePaddle it can be Paddle Variable. :param batch: Set batch size. It applies to 1D or higher dimension inputs. The default dimension index for the batch is zero. @@ -271,15 +273,9 @@ def convert_model( Switch model conversion progress display to a multiline mode. PaddlePaddle-specific parameters: - :param example_input: - Sample of model input in original framework. For PaddlePaddle it can be Paddle Variable. :param example_output: Sample of model output in original framework. For PaddlePaddle it can be Paddle Variable. - PyTorch-specific parameters: - :param example_input: - Sample of model input in original framework. For PyTorch it can be torch.Tensor. - TensorFlow*-specific parameters: :param input_model_is_text: TensorFlow*: treat the input model file as a text protobuf format. If diff --git a/tools/mo/openvino/tools/mo/convert_impl.py b/tools/mo/openvino/tools/mo/convert_impl.py index 1372931fd87..dc42100144d 100644 --- a/tools/mo/openvino/tools/mo/convert_impl.py +++ b/tools/mo/openvino/tools/mo/convert_impl.py @@ -44,20 +44,25 @@ from openvino.tools.mo.utils.model_analysis import AnalysisResults from openvino.tools.mo.utils.version import VersionChecker from openvino.tools.mo.utils.guess_framework import deduce_legacy_frontend_by_namespace from openvino.tools.mo.utils.logger import init_logger, progress_printer -from openvino.tools.mo.utils.utils import refer_to_faq_msg +from openvino.tools.mo.utils.utils import refer_to_faq_msg, check_values_equal from openvino.tools.mo.utils.telemetry_utils import send_params_info, send_framework_info, send_conversion_result, \ get_tid -from openvino.tools.mo.utils.versions_checker import get_environment_setup # pylint: disable=no-name-in-module from openvino.tools.mo.moc_frontend.check_config import legacy_extensions_used from openvino.tools.mo.moc_frontend.pytorch_frontend_utils import get_pytorch_decoder from openvino.tools.mo.moc_frontend.paddle_frontend_utils import paddle_frontend_converter -from openvino.tools.mo.moc_frontend.shape_utils import parse_input_shapes, get_static_shape +from openvino.tools.mo.moc_frontend.shape_utils import parse_input_shapes # pylint: disable=no-name-in-module,import-error from openvino.frontend import FrontEndManager, OpConversionFailure, ProgressReporterExtension, TelemetryExtension from openvino.runtime import get_version as get_rt_version from openvino.runtime import Type, PartialShape +try: + from openvino.frontend.tensorflow.utils import type_supported_by_tf_fe, create_tf_graph_iterator, extract_model_graph # pylint: disable=no-name-in-module,import-error + tf_frontend_with_python_bindings_installed = True +except (ModuleNotFoundError, ImportError): + tf_frontend_with_python_bindings_installed = False + def load_extensions(argv: argparse.Namespace, is_tf: bool, is_caffe: bool, is_mxnet: bool, is_kaldi: bool, is_onnx: bool): @@ -377,6 +382,7 @@ def prepare_ir(argv: argparse.Namespace): is_tf, _, _, _, _ = deduce_legacy_frontend_by_namespace(argv) argv = arguments_post_parsing(argv) t = tm.Telemetry() + graph = None ngraph_function = None fallback_reasons = [] @@ -387,8 +393,15 @@ def prepare_ir(argv: argparse.Namespace): path_to_aux_pb = None orig_argv_values = {"input_model": argv.input_model, "model_name": argv.model_name} if not argv.use_legacy_frontend and is_tf: - from openvino.tools.mo.front.tf.loader import convert_to_pb - path_to_aux_pb = convert_to_pb(argv) + if tf_frontend_with_python_bindings_installed and 'tf' in available_moc_front_ends and \ + type_supported_by_tf_fe(argv.input_model): + argv.input_model = create_tf_graph_iterator(argv.input_model, + argv.placeholder_shapes, + argv.placeholder_data_types, + getattr(argv, "example_input", None)) + else: + from openvino.tools.mo.front.tf.loader import convert_to_pb + path_to_aux_pb = convert_to_pb(argv) try: 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)) @@ -417,7 +430,7 @@ def prepare_ir(argv: argparse.Namespace): if is_tf and path_to_aux_pb is not None: argv.input_model = orig_argv_values["input_model"] argv.model_name = orig_argv_values["model_name"] - if os.path.exists(path_to_aux_pb): + if path_to_aux_pb is not None and os.path.exists(path_to_aux_pb): os.remove(path_to_aux_pb) if len(fallback_reasons) > 0: @@ -520,47 +533,7 @@ def emit_ir(graph: Graph, argv: argparse.Namespace, non_default_params: dict): def check_model_object(argv): model = argv['input_model'] if 'tensorflow' in sys.modules: - import tensorflow as tf - env_setup = get_environment_setup("tf") - - if isinstance(model, tf.compat.v1.GraphDef): - return "tf" - if isinstance(model, tf.compat.v1.Graph): - argv['input_model'] = model.as_graph_def() - return "tf" - if isinstance(model, tf.compat.v1.Session): - argv['input_model'] = model.graph_def - return "tf" - if env_setup["tensorflow"] >= LooseVersion("2.6.0") and isinstance(model, tf.types.experimental.ConcreteFunction): - argv['input_model'] = model.graph.as_graph_def() - return "tf" - if env_setup["tensorflow"] >= LooseVersion("2.6.0") and isinstance(model, tf.types.experimental.GenericFunction): - argv['input_model'] = model - return "tf" - if isinstance(model, tf.keras.Model): - return "tf" - if isinstance(model, tf.train.Checkpoint): - if isinstance(model.root, tf.keras.Model): - argv['input_model'] = model.root - return "tf" - else: - raise Error("Unknown checkpoint format.") - - if isinstance(model, tf.keras.layers.Layer) or isinstance(model, tf.Module): - assert 'input_shape' in argv and argv['input_shape'] is not None, \ - "Converting of {} requires providing of input_shape.".format(type(model)) - assert len(argv['input_shape']) > 0, "Please provide non-empty input shape." - inputs = [] - for shape_idx, shape in enumerate(parse_input_shapes(argv)): - inp_shape = get_static_shape(shape) - batch_size = None - if len(inp_shape) > 1: - batch_size = inp_shape[0] - inp_shape = inp_shape[1:] - inputs.append(tf.keras.Input(shape=inp_shape, batch_size=batch_size)) - outputs = model(*inputs) - argv['input_model'] = tf.keras.Model(inputs, outputs) - argv['input_shape'] = None + if tf_frontend_with_python_bindings_installed and extract_model_graph(argv): return "tf" if 'torch' in sys.modules: import torch @@ -630,9 +603,9 @@ def args_dict_to_list(cli_parser, **kwargs): for key, value in kwargs.items(): if value is None: continue - if key in signature.parameters and signature.parameters[key].default == value: + if key in signature.parameters and check_values_equal(signature.parameters[key].default, value): continue - if cli_parser.get_default(key) == value: + if check_values_equal(cli_parser.get_default(key), value): continue # skip parser checking for non str objects if not isinstance(value, (str, bool)): @@ -653,9 +626,9 @@ def get_non_default_params(argv, cli_parser): # make dictionary with parameters which have non-default values to be serialized in IR in rt_info non_default_params = {} for arg, arg_value in vars(argv).items(): - if arg in signature.parameters and arg_value == signature.parameters[arg].default: + if arg in signature.parameters and check_values_equal(arg_value, signature.parameters[arg].default): continue - if arg_value == cli_parser.get_default(arg): + if check_values_equal(arg_value, cli_parser.get_default(arg)): continue value = depersonalize(arg_value, arg) # Skip complex classes in params to prevent @@ -819,7 +792,7 @@ def pack_params_to_args_namespace(args: dict, cli_parser: argparse.ArgumentParse # Non string params like input_model or extensions are ignored by parse_args() # so we need to set them in argv separately - if value is not None and getattr(argv, key, None) != value: + if value is not None and not check_values_equal(getattr(argv, key, None), value): setattr(argv, key, value) else: argv = cli_parser.parse_args() diff --git a/tools/mo/openvino/tools/mo/front/tf/loader.py b/tools/mo/openvino/tools/mo/front/tf/loader.py index c1374903d84..e1a60f4bd81 100644 --- a/tools/mo/openvino/tools/mo/front/tf/loader.py +++ b/tools/mo/openvino/tools/mo/front/tf/loader.py @@ -6,6 +6,7 @@ import logging as log import os import re from distutils.version import LooseVersion +from pathlib import Path from openvino.tools.mo.graph.graph import Node from openvino.tools.mo.utils.error import Error, FrameworkError @@ -327,6 +328,8 @@ def load_tf_graph_def(graph_file_name: str = "", is_binary: bool = True, checkpo def convert_to_pb(argv: argparse.Namespace): from openvino.tools.mo.utils.cli_parser import get_model_name + if argv.input_model is not None and not isinstance(argv.input_model, (str, Path)): + return None env_setup = get_environment_setup("tf") if "tensorflow" in env_setup and env_setup["tensorflow"] >= LooseVersion("2.0.0"): tf.keras.backend.clear_session() diff --git a/tools/mo/openvino/tools/mo/utils/telemetry_utils.py b/tools/mo/openvino/tools/mo/utils/telemetry_utils.py index 62215923507..d53b70956cb 100644 --- a/tools/mo/openvino/tools/mo/utils/telemetry_utils.py +++ b/tools/mo/openvino/tools/mo/utils/telemetry_utils.py @@ -4,6 +4,7 @@ import argparse from collections import Counter import numpy as np +import numbers from openvino.tools.mo.front.common.partial_infer.utils import is_fully_defined, unmask_shape, int64_array from openvino.tools.mo.graph.graph import Graph @@ -11,6 +12,7 @@ from openvino.tools.mo.middle.pattern_match import for_graph_and_each_sub_graph_ from openvino.tools.mo.utils.cli_parser import get_params_with_paths_list from openvino.tools.mo.utils.telemetry_params import telemetry_params from openvino.tools.mo.utils.version import VersionChecker +from openvino.tools.mo.utils.utils import check_values_equal try: import openvino_telemetry as tm @@ -68,6 +70,16 @@ def send_shapes_info(framework: str, graph: Graph): "{partially_defined_shape:" + is_partially_defined + ",fw:" + framework + "}") +def arg_to_str(arg): + # This method converts to string only known types, otherwise returns string with name of the type + from openvino.runtime import PartialShape, Shape, Type, Layout + if isinstance(arg, (PartialShape, Shape, Type, Layout)): + return str(arg) + if isinstance(arg, (str, numbers.Number, bool)): + return str(arg) + return str(type(arg)) + + def send_params_info(argv: argparse.Namespace, cli_parser: argparse.ArgumentParser): """ This function sends information about used command line parameters. @@ -78,13 +90,13 @@ def send_params_info(argv: argparse.Namespace, cli_parser: argparse.ArgumentPars params_with_paths = get_params_with_paths_list() for arg in vars(argv): arg_value = getattr(argv, arg) - if arg_value != cli_parser.get_default(arg): + if not check_values_equal(arg_value, cli_parser.get_default(arg)): if arg in params_with_paths: # If command line argument value is a directory or a path to file it is not sent # as it may contain confidential information. "1" value is used instead. param_str = arg + ":" + str(1) else: - param_str = arg + ":" + str(arg_value) + param_str = arg + ":" + arg_to_str(arg_value) t.send_event('mo', 'cli_parameters', param_str) diff --git a/tools/mo/openvino/tools/mo/utils/utils.py b/tools/mo/openvino/tools/mo/utils/utils.py index cdb83ae2e84..17d3f47a413 100644 --- a/tools/mo/openvino/tools/mo/utils/utils.py +++ b/tools/mo/openvino/tools/mo/utils/utils.py @@ -145,3 +145,14 @@ def unique_by(xs: list, predicate: Callable) -> list: """ groups = group_by_with_binary_predicate(xs, predicate) return [group[0] for group in groups] + + +def check_values_equal(val1, val2): + # This method is needed to check equality of values where some values can be None + if val1 is None and val2 is None: + return True + if val1 is None: + return False + if val2 is None: + return False + return val1 == val2 diff --git a/tools/mo/unit_tests/mo/convert/logger_test_actual.py b/tools/mo/unit_tests/mo/convert/logger_test_actual.py index 0f00cc19f37..c8bc9f56b0f 100644 --- a/tools/mo/unit_tests/mo/convert/logger_test_actual.py +++ b/tools/mo/unit_tests/mo/convert/logger_test_actual.py @@ -2,9 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 import logging as log +import os import sys +import tempfile -def create_tf_model(): + +def create_tf_model(out_dir): import tensorflow as tf tf.compat.v1.reset_default_graph() @@ -18,42 +21,45 @@ def create_tf_model(): tf.compat.v1.global_variables_initializer() tf_net = sess.graph_def - return tf_net + tf.io.write_graph(tf_net, out_dir + os.sep, 'model_bool.pb', as_text=False) + return out_dir + os.sep + 'model_bool.pb' + def run_main(): from openvino.tools.mo import convert_model log.basicConfig(format="[ %(levelname)s ] %(message)s", level=log.INFO, stream=sys.stdout) + test_directory = os.path.dirname(os.path.realpath(__file__)) - tf_model = create_tf_model() - _ = convert_model(tf_model) + with tempfile.TemporaryDirectory(dir=test_directory) as tmpdir: + tf_model = create_tf_model(test_directory) + _ = convert_model(tf_model) - log.info("test message 1") + log.info("test message 1") - logger = log.getLogger() - assert logger.level == 20 - assert len(logger.handlers) == 1 - assert len(logger.filters) == 0 + logger = log.getLogger() + assert logger.level == 20 + assert len(logger.handlers) == 1 + assert len(logger.filters) == 0 - _ = convert_model(tf_model, log_level="DEBUG", silent=False) + _ = convert_model(tf_model, log_level="DEBUG", silent=False) - log.info("test message 2") + log.info("test message 2") - logger = log.getLogger() - assert logger.level == 20 - assert len(logger.handlers) == 1 - assert len(logger.filters) == 0 + logger = log.getLogger() + assert logger.level == 20 + assert len(logger.handlers) == 1 + assert len(logger.filters) == 0 + _ = convert_model(tf_model, log_level="CRITICAL", silent=False) - _ = convert_model(tf_model, log_level="CRITICAL", silent=False) + log.info("test message 3") + logger = log.getLogger() + assert logger.level == 20 + assert len(logger.handlers) == 1 + assert len(logger.filters) == 0 - log.info("test message 3") - - logger = log.getLogger() - assert logger.level == 20 - assert len(logger.handlers) == 1 - assert len(logger.filters) == 0 if __name__ == "__main__": run_main() diff --git a/tools/mo/unit_tests/mo/convert/meta_data_test_actual.py b/tools/mo/unit_tests/mo/convert/meta_data_test_actual.py index 643510a5a63..40acd6d5da4 100644 --- a/tools/mo/unit_tests/mo/convert/meta_data_test_actual.py +++ b/tools/mo/unit_tests/mo/convert/meta_data_test_actual.py @@ -39,7 +39,7 @@ class MetaDataTestTF(unittest.TestCase): assert key in ref_meta, "Unexpected runtime info attribute: {}".format(key) def test_meta_data_tf(self): - def create_tf_model(): + def create_tf_model(out_dir): import tensorflow as tf tf.compat.v1.reset_default_graph() @@ -53,13 +53,14 @@ class MetaDataTestTF(unittest.TestCase): tf.compat.v1.global_variables_initializer() tf_net = sess.graph_def - return tf_net + tf.io.write_graph(tf_net, out_dir + os.sep, 'model_bool.pb', as_text=False) + return out_dir + os.sep + 'model_bool.pb' def ref_meta_data(): return { 'MO_version': get_version(), 'Runtime_version': get_rt_version(), - 'legacy_frontend': "True", + 'legacy_frontend': "False", 'conversion_parameters': { 'scale': "1.5", 'batch': "1" @@ -67,11 +68,11 @@ class MetaDataTestTF(unittest.TestCase): } with tempfile.TemporaryDirectory(dir=self.test_directory) as tmpdir: - model = create_tf_model() + model = create_tf_model(tmpdir) out_xml = os.path.join(tmpdir, "model.xml") ref_meta = ref_meta_data() - ov_model = convert_model(model, scale=1.5, batch=1, use_legacy_frontend=True) + ov_model = convert_model(model, scale=1.5, batch=1) self.check_meta_data(ov_model, ref_meta) serialize(ov_model, out_xml.encode('utf-8'), out_xml.replace('.xml', '.bin').encode('utf-8')) diff --git a/tools/mo/unit_tests/mo/utils/cli_parser_test.py b/tools/mo/unit_tests/mo/utils/cli_parser_test.py index 809f4183ff6..7f44d2066bf 100644 --- a/tools/mo/unit_tests/mo/utils/cli_parser_test.py +++ b/tools/mo/unit_tests/mo/utils/cli_parser_test.py @@ -2041,7 +2041,7 @@ class TestConvertModelParamsParsing(unittest.TestCase): 'log_level', 'input', 'output', 'mean_values', 'scale_values', 'source_layout', 'target_layout', 'layout', 'compress_to_fp16', 'transform', 'extensions', 'batch', 'silent', 'version', 'progress', 'stream_output', - 'transformations_config'}, + 'transformations_config', 'example_input'}, 'Caffe*-specific parameters:': {'input_proto', 'caffe_parser_path', 'k', 'disable_omitting_optional', 'enable_flattening_nested_params'}, 'TensorFlow*-specific parameters:': {'input_model_is_text', 'input_checkpoint', 'input_meta_graph', @@ -2052,8 +2052,7 @@ class TestConvertModelParamsParsing(unittest.TestCase): 'MXNet-specific parameters:': {'input_symbol', 'nd_prefix_name', 'pretrained_model_name', 'save_params_from_nd', 'legacy_mxnet_model', 'enable_ssd_gluoncv'}, 'Kaldi-specific parameters:': {'counts', 'remove_output_softmax', 'remove_memory'}, - 'PaddlePaddle-specific parameters:': {'example_input', 'example_output'}, - 'PyTorch-specific parameters:': {'example_input'} + 'PaddlePaddle-specific parameters:': {'example_output'}, } params = get_mo_convert_params() @@ -2065,7 +2064,7 @@ class TestConvertModelParamsParsing(unittest.TestCase): for group_name, params in ref_params.items(): for param_name in params: param_name = '--' + param_name - if group_name == 'PyTorch-specific parameters:' or group_name == 'PaddlePaddle-specific parameters:': + if group_name == 'PaddlePaddle-specific parameters:': assert param_name not in cli_parser._option_string_actions else: assert param_name in cli_parser._option_string_actions