diff --git a/src/frontends/common_translators/include/unconverted_ops_report.hpp b/src/frontends/common_translators/include/unconverted_ops_report.hpp new file mode 100644 index 00000000000000..c239fe80856078 --- /dev/null +++ b/src/frontends/common_translators/include/unconverted_ops_report.hpp @@ -0,0 +1,76 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "openvino/core/model.hpp" +#include "openvino/frontend/extension/telemetry.hpp" +#include "openvino/op/util/framework_node.hpp" +#include "openvino/op/util/multi_subgraph_base.hpp" + +namespace ov { +namespace frontend { + +/// \brief Structure containing information about unconverted operations +/// Map of operation types with no conversion rule (op_type -> empty string) +/// or operations that failed during conversion (op_type -> error message) +struct UnconvertedOpsReport { + std::map unconverted_ops; + + bool has_issues() const; + + /// \brief Add an unconverted operation if not already present + /// \param op_type Operation type name + /// \param error_message Error message (empty if no converter found, non-empty if conversion failed) + void add(const std::string& op_type, const std::string& error_message = {}); + + /// \brief Merge another report into this one + void merge(const UnconvertedOpsReport& other); +}; + +/// \brief Callback type for extracting unconverted operation info from framework-specific nodes +/// \param node The node to check +/// \return Optional pair of (op_type, error_message) if this is an unconverted framework node +using FrameworkNodeExtractor = + std::function>(const std::shared_ptr&)>; + +/// \brief Collect unconverted operations from a model +/// \param model The model to scan +/// \param extractor Callback to extract info from framework-specific nodes +/// \return Report containing all unconverted operations found +UnconvertedOpsReport collect_unconverted_ops(const std::shared_ptr& model, + const FrameworkNodeExtractor& extractor); + +/// \brief Callback type for adding additional error information +/// \param unsupported_ops Set of unsupported operation types (those without error messages) +/// \return Additional message to append to the error report +using AdditionalErrorCallback = std::function&)>; + +/// \brief Check conversion result, send telemetry, and optionally throw if there are unconverted operations +/// \param report The unconverted operations report +/// \param telemetry Telemetry extension (can be nullptr) +/// \param telemetry_prefix Frontend name prefix for telemetry events (e.g., "pytorch", "tf", "onnx", "jax") +/// \param error_message_prefix Prefix for filtering error messages in telemetry (e.g., "[PyTorch Frontend] ") +/// If non-empty, error_info telemetry events will be sent +/// \param additional_error Additional error message (e.g., from normalize step) +/// \param additional_callback Optional callback for frontend-specific additional error messages +/// \param throw_on_issues If true (default), throws when there are unconverted ops or additional_error is non-empty +/// \throws ov::frontend::OpConversionFailure if throw_on_issues is true and there are issues +void check_unconverted_ops(const UnconvertedOpsReport& report, + const std::shared_ptr& telemetry, + const std::string& telemetry_prefix, + const std::string& error_message_prefix = {}, + const std::string& additional_error = {}, + const AdditionalErrorCallback& additional_callback = nullptr, + bool throw_on_issues = true); + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/common_translators/src/unconverted_ops_report.cpp b/src/frontends/common_translators/src/unconverted_ops_report.cpp new file mode 100644 index 00000000000000..821dcb95c83c8b --- /dev/null +++ b/src/frontends/common_translators/src/unconverted_ops_report.cpp @@ -0,0 +1,147 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "unconverted_ops_report.hpp" + +#include + +#include "openvino/frontend/exception.hpp" +#include "openvino/util/common_util.hpp" + +namespace ov { +namespace frontend { + +bool UnconvertedOpsReport::has_issues() const { + return !unconverted_ops.empty(); +} + +void UnconvertedOpsReport::add(const std::string& op_type, const std::string& error_message) { + if (unconverted_ops.find(op_type) == unconverted_ops.end()) { + unconverted_ops[op_type] = error_message; + } +} + +void UnconvertedOpsReport::merge(const UnconvertedOpsReport& other) { + for (const auto& [op_type, msg] : other.unconverted_ops) { + add(op_type, msg); + } +} + +UnconvertedOpsReport collect_unconverted_ops(const std::shared_ptr& model, + const FrameworkNodeExtractor& extractor) { + UnconvertedOpsReport report; + if (!model) { + return report; + } + + for (const auto& node : model->get_ordered_ops()) { + // Try framework-specific extractor first + if (auto result = extractor(node)) { + report.add(result->first, result->second); + } + + // Handle MultiSubGraphOp (parent of Loop, If, etc.) - common for all frontends + if (const auto& subgraph_op = ov::as_type_ptr(node)) { + for (size_t i = 0; i < subgraph_op->get_internal_subgraphs_size(); ++i) { + report.merge(collect_unconverted_ops(subgraph_op->get_function(i), extractor)); + } + } + } + return report; +} + +namespace { + +std::string format_unconverted_ops_report(const UnconvertedOpsReport& report, + const std::string& additional_error, + const AdditionalErrorCallback& additional_callback) { + std::stringstream error_msg; + std::stringstream unconverted_ops_msg; + std::stringstream failed_ops_msg; + std::stringstream failed_ops_short; + + error_msg << "Model wasn't fully converted."; + unconverted_ops_msg << "-- No conversion rule found for operations: "; + failed_ops_msg << " Failed operations detailed log:"; + failed_ops_short << "-- Conversion is failed for: "; + + bool has_unsupported = false; + bool has_failed = false; + std::set unsupported_ops_set; + + for (const auto& [op_type, error] : report.unconverted_ops) { + if (error.empty()) { + // No conversion rule found + if (has_unsupported) { + unconverted_ops_msg << ", "; + } + unconverted_ops_msg << op_type; + unsupported_ops_set.insert(op_type); + has_unsupported = true; + } else { + // Conversion failed with error + if (has_failed) { + failed_ops_short << ", "; + } + failed_ops_short << op_type; + failed_ops_msg << "\n-- " << op_type << " with a message:\n" << error; + has_failed = true; + } + } + + if (has_failed) { + error_msg << failed_ops_msg.str(); + } + + error_msg << "\nSummary:" << additional_error; + + if (has_unsupported) { + error_msg << '\n' << unconverted_ops_msg.str(); + } + + if (has_failed) { + error_msg << '\n' << failed_ops_short.str(); + } + + // Add additional callback-provided information + if (additional_callback && has_unsupported) { + if (auto additional_info = additional_callback(unsupported_ops_set); !additional_info.empty()) { + error_msg << '\n' << additional_info; + } + } + + return error_msg.str(); +} + +} // namespace + +void check_unconverted_ops(const UnconvertedOpsReport& report, + const std::shared_ptr& telemetry, + const std::string& telemetry_prefix, + const std::string& error_message_prefix, + const std::string& additional_error, + const AdditionalErrorCallback& additional_callback, + bool throw_on_issues) { + // Send telemetry for all unconverted operations + if (telemetry) { + const bool send_error_info = !error_message_prefix.empty(); + for (const auto& [op_type, error] : report.unconverted_ops) { + telemetry->send_event("error_cause", telemetry_prefix + "_" + op_type); + if (send_error_info && !error.empty()) { + auto cropped_message = ov::util::filter_lines_by_prefix(error, error_message_prefix); + if (!cropped_message.empty()) { + telemetry->send_event("error_info", cropped_message); + } + } + } + } + + if (throw_on_issues && (report.has_issues() || !additional_error.empty())) { + FRONT_END_OP_CONVERSION_CHECK(false, + format_unconverted_ops_report(report, additional_error, additional_callback)); + } +} + +} // namespace frontend +} // namespace ov diff --git a/src/frontends/jax/src/frontend.cpp b/src/frontends/jax/src/frontend.cpp index 4b2f02988887ef..03a4077faae365 100644 --- a/src/frontends/jax/src/frontend.cpp +++ b/src/frontends/jax/src/frontend.cpp @@ -9,18 +9,18 @@ #include "op_table.hpp" #include "openvino/core/so_extension.hpp" #include "openvino/frontend/jax/extension/conversion.hpp" -#include "openvino/op/util/multi_subgraph_base.hpp" #include "openvino/util/log.hpp" #include "translate_session.hpp" +#include "unconverted_ops_report.hpp" namespace ov { namespace frontend { namespace jax { namespace { -std::map get_unconverted_types_from_model(const std::shared_ptr& model) { - std::map unconverted_ops_types; - for (const auto& node : model->get_ordered_ops()) { + +ov::frontend::FrameworkNodeExtractor make_jax_extractor() { + return [](const std::shared_ptr& node) -> std::optional> { if (const auto& fw_node = ov::as_type_ptr(node)) { const auto& attrs = fw_node->get_attrs(); FRONT_END_GENERAL_CHECK(attrs.find(JaxFrameworkNode::op_type_key) != attrs.end(), @@ -29,55 +29,12 @@ std::map get_unconverted_types_from_model(const std::s if (attrs.find(JaxFrameworkNode::failed_conversion_key) != attrs.end()) { exception_msg = attrs.at(JaxFrameworkNode::failed_conversion_key); } - if (!unconverted_ops_types.count(attrs.at(JaxFrameworkNode::op_type_key))) { - unconverted_ops_types[attrs.at(JaxFrameworkNode::op_type_key)] = exception_msg; - } + return std::make_pair(attrs.at(JaxFrameworkNode::op_type_key), std::move(exception_msg)); } - if (const auto& fw_node = ov::as_type_ptr(node)) { - for (size_t i = 0; i < fw_node->get_internal_subgraphs_size(); i++) { - const auto& internal_types = get_unconverted_types_from_model(fw_node->get_function(i)); - unconverted_ops_types.insert(internal_types.begin(), internal_types.end()); - } - } - } - return unconverted_ops_types; + return std::nullopt; + }; } -std::string pack_detailed_failure_report(const std::map& unconverted_ops, - const std::string& additional_error = "") { - std::stringstream error_msg; - std::stringstream unconverted_ops_msg; - std::stringstream failed_ops_msg; - std::stringstream failed_ops_short; - error_msg << "Model wasn't fully converted."; - unconverted_ops_msg << "-- No conversion rule found for operations: "; - failed_ops_msg << " Failed operations detailed log:"; - failed_ops_short << "-- Conversion is failed for: "; - bool at_least_one = false; - bool at_least_one_except = false; - for (auto&& op : unconverted_ops) { - if (op.second.empty()) { - if (at_least_one) - unconverted_ops_msg << ", "; - unconverted_ops_msg << op.first; - at_least_one = true; - } else { - if (at_least_one_except) - failed_ops_short << ", "; - failed_ops_short << op.first; - failed_ops_msg << "\n-- " << op.first << " with a message:\n" << op.second; - at_least_one_except = true; - } - } - if (at_least_one_except) - error_msg << failed_ops_msg.str(); - error_msg << "\nSummary:" << additional_error; - if (at_least_one) - error_msg << '\n' << unconverted_ops_msg.str(); - if (at_least_one_except) - error_msg << '\n' << failed_ops_short.str(); - return error_msg.str(); -} } // namespace FrontEnd::FrontEnd() {} @@ -91,14 +48,9 @@ std::shared_ptr FrontEnd::convert(const ov::frontend::InputModel::Ptr& mo converted_model = translate_session.get_converted_model(); } - const auto& unconverted_ops = get_unconverted_types_from_model(converted_model); - for (auto&& op : unconverted_ops) { - if (m_telemetry) { - m_telemetry->send_event("error_cause", "jax_" + op.first); - } - } - bool is_conversion_successful = unconverted_ops.size() == 0; - FRONT_END_OP_CONVERSION_CHECK(is_conversion_successful, pack_detailed_failure_report(unconverted_ops)); + const auto report = ov::frontend::collect_unconverted_ops(converted_model, make_jax_extractor()); + // JAX frontend does not send error_info telemetry - pass empty error_message_prefix + ov::frontend::check_unconverted_ops(report, m_telemetry, "jax", ""); return converted_model; } diff --git a/src/frontends/onnx/frontend/src/core/graph.cpp b/src/frontends/onnx/frontend/src/core/graph.cpp index f2f42091b7046f..ef62c58a066ec7 100644 --- a/src/frontends/onnx/frontend/src/core/graph.cpp +++ b/src/frontends/onnx/frontend/src/core/graph.cpp @@ -346,9 +346,9 @@ ov::OutputVector Graph::make_ov_nodes(const Node& onnx_node) { } } if (ov_subgraph_outputs.empty()) { // translation not possible (not supported op or exception during processing) + std::string onnx_domain = onnx_node.domain(); + int64_t opset_version = m_model->get_opset_version(onnx_domain); if (m_extensions.telemetry && !error_message.empty()) { - std::string onnx_domain = onnx_node.domain(); - int64_t opset_version = m_model->get_opset_version(onnx_domain); error_message = onnx_prefix + "Conversion failed for " + (onnx_domain != "" ? "***." + onnx_node.op_type() + "-X" : onnx_node.op_type() + "-" + std::to_string(opset_version)) + @@ -357,8 +357,9 @@ ov::OutputVector Graph::make_ov_nodes(const Node& onnx_node) { const auto not_supported_node = std::make_shared(onnx_node.get_ov_inputs(), onnx_node.get_outputs_size(), - onnx_node.domain(), + onnx_domain, onnx_node.op_type(), + opset_version, error_message); ov_subgraph_outputs = not_supported_node->outputs(); } diff --git a/src/frontends/onnx/frontend/src/core/graph.hpp b/src/frontends/onnx/frontend/src/core/graph.hpp index 3a62410f93439c..a564d439f7198f 100644 --- a/src/frontends/onnx/frontend/src/core/graph.hpp +++ b/src/frontends/onnx/frontend/src/core/graph.hpp @@ -55,6 +55,11 @@ class Graph : public std::enable_shared_from_this { ov::OutputVector make_ov_nodes(const ov::frontend::onnx::Node& onnx_node); const OpsetImports& get_opset_imports() const; + + int64_t get_opset_version(const std::string& domain) const { + return m_model->get_opset_version(domain); + } + virtual ~Graph() = default; const ov::frontend::ExtensionHolder& get_extensions() const { diff --git a/src/frontends/onnx/frontend/src/core/node.cpp b/src/frontends/onnx/frontend/src/core/node.cpp index 8c3a459a8435fd..d1e39f2c228135 100644 --- a/src/frontends/onnx/frontend/src/core/node.cpp +++ b/src/frontends/onnx/frontend/src/core/node.cpp @@ -58,6 +58,7 @@ class Node::Impl { const std::string& domain() const; const std::string& op_type() const; const std::string& name() const; + int64_t opset_version() const; const std::string& description() const; const std::vector>& get_output_names() const; @@ -128,6 +129,9 @@ const std::string& Node::Impl::op_type() const { const std::string& Node::Impl::name() const { return m_name; } +int64_t Node::Impl::opset_version() const { + return m_graph->get_opset_version(m_domain); +} const std::vector>& Node::Impl::get_output_names() const { return m_output_names; } @@ -436,6 +440,15 @@ const std::string& Node::get_name() const { FRONT_END_NOT_IMPLEMENTED(get_name); } +int64_t Node::opset_version() const { + if (m_pimpl != nullptr) { + return m_pimpl->opset_version(); + } else if (m_decoder != nullptr) { + return static_cast(m_decoder->get_op_set()); + } + FRONT_END_NOT_IMPLEMENTED(opset_version); +} + const std::vector> Node::get_output_names() const { if (m_pimpl != nullptr) { return m_pimpl->get_output_names(); diff --git a/src/frontends/onnx/frontend/src/core/node.hpp b/src/frontends/onnx/frontend/src/core/node.hpp index 90e0f6d11f5ca2..b2267dca4aa03a 100644 --- a/src/frontends/onnx/frontend/src/core/node.hpp +++ b/src/frontends/onnx/frontend/src/core/node.hpp @@ -60,6 +60,7 @@ class Node { const std::string& domain() const; const std::string& op_type() const; const std::string& get_name() const; + int64_t opset_version() const; std::vector get_attribute_names() const; const Attribute& get_attribute(const std::string& name) const; ov::Any get_attribute_any(const std::string& name) const; diff --git a/src/frontends/onnx/frontend/src/frontend.cpp b/src/frontends/onnx/frontend/src/frontend.cpp index 4bbd6e2275812d..e6e5338fc1d316 100644 --- a/src/frontends/onnx/frontend/src/frontend.cpp +++ b/src/frontends/onnx/frontend/src/frontend.cpp @@ -43,6 +43,7 @@ #include "ops_bridge.hpp" #include "transformations/resolve_names_collisions.hpp" #include "translate_session.hpp" +#include "unconverted_ops_report.hpp" #include "utils/common.hpp" #include "utils/onnx_internal.hpp" @@ -110,6 +111,37 @@ void enumerate_constants(const std::shared_ptr& model) { } } // !!! End of Experimental feature + +ov::frontend::FrameworkNodeExtractor make_onnx_extractor() { + return [](const std::shared_ptr& node) -> std::optional> { + if (const auto& fw_node = ov::as_type_ptr(node)) { + const auto& attrs = fw_node->get_attrs(); + const auto& domain = attrs.get_opset_name(); + const auto opset = fw_node->opset_version(); + // Format as "domain.OpType-version" or "OpType-version" for default ONNX domain + std::string node_name; + if (domain.empty()) { + node_name = attrs.get_type_name() + "-" + std::to_string(opset); + } else { + node_name = domain + "." + attrs.get_type_name(); // Don't show version for custom domains + } + return std::make_pair(node_name, std::string{}); + } else if (const auto& fw_node = ov::as_type_ptr(node)) { + const auto& attrs = fw_node->get_attrs(); + const auto& domain = attrs.get_opset_name(); + const auto opset = fw_node->opset_version(); + std::string node_name; + if (domain.empty()) { + node_name = attrs.get_type_name() + "-" + std::to_string(opset); + } else { + node_name = domain + "." + attrs.get_type_name(); // Don't show version for custom domains + } + return std::make_pair(node_name, fw_node->additional_error_message()); + } + return std::nullopt; + }; +} + } // namespace ONNX_FRONTEND_C_API ov::frontend::FrontEndVersion get_api_version() { @@ -221,7 +253,9 @@ std::shared_ptr FrontEnd::convert_partially(const ov::frontend::Input const auto& converted_model = model_onnx->convert(); - ov::frontend::onnx::common::collect_translation_exceptions(converted_model, m_extensions.telemetry); + // For convert_partially, we don't throw on unconverted ops but still send telemetry + const auto report = ov::frontend::collect_unconverted_ops(converted_model, make_onnx_extractor()); + ov::frontend::check_unconverted_ops(report, m_extensions.telemetry, "onnx", "[ONNX Frontend] ", "", nullptr, false); normalize(converted_model); return converted_model; @@ -262,13 +296,8 @@ std::shared_ptr FrontEnd::convert(const InputModel::Ptr& input_model) const auto& converted_model = model_onnx->convert(); - std::stringstream error_messages; - - if (ov::frontend::onnx::common::collect_translation_exceptions(converted_model, - m_extensions.telemetry, - &error_messages)) { - FRONT_END_THROW(error_messages.str()); - } + const auto report = ov::frontend::collect_unconverted_ops(converted_model, make_onnx_extractor()); + ov::frontend::check_unconverted_ops(report, m_extensions.telemetry, "onnx", "[ONNX Frontend] "); normalize(converted_model); return converted_model; @@ -382,10 +411,8 @@ std::shared_ptr FrontEnd::convert_unify(const InputModel::Ptr& input_ translate_graph(input_model, false, false, ov_model); - std::stringstream error_messages; - if (ov::frontend::onnx::common::collect_translation_exceptions(ov_model, m_extensions.telemetry, &error_messages)) { - FRONT_END_THROW(error_messages.str()); - } + const auto report = ov::frontend::collect_unconverted_ops(ov_model, make_onnx_extractor()); + ov::frontend::check_unconverted_ops(report, m_extensions.telemetry, "onnx", "[ONNX Frontend] "); normalize(ov_model); return ov_model; @@ -406,7 +433,9 @@ std::shared_ptr FrontEnd::convert_partially_unify(const InputModel::P std::shared_ptr ov_model; translate_graph(input_model, false, false, ov_model); - ov::frontend::onnx::common::collect_translation_exceptions(ov_model, m_extensions.telemetry); + // For convert_partially, we don't throw on unconverted ops but still send telemetry + const auto report = ov::frontend::collect_unconverted_ops(ov_model, make_onnx_extractor()); + ov::frontend::check_unconverted_ops(report, m_extensions.telemetry, "onnx", "[ONNX Frontend] ", "", nullptr, false); normalize(ov_model); return ov_model; diff --git a/src/frontends/onnx/frontend/src/onnx_framework_node.cpp b/src/frontends/onnx/frontend/src/onnx_framework_node.cpp index 0a3f148983cac1..afb189e84c4b62 100644 --- a/src/frontends/onnx/frontend/src/onnx_framework_node.cpp +++ b/src/frontends/onnx/frontend/src/onnx_framework_node.cpp @@ -29,12 +29,12 @@ std::shared_ptr ONNXSubgraphFrameworkNode::clone_with_new_inputs(const std::shared_ptr NotSupportedONNXNode::clone_with_new_inputs(const ov::OutputVector& inputs) const { const auto& attrs = get_attrs(); - std::string error_message = attrs.at(failed_conversion_key); return std::make_shared(inputs, get_output_size(), attrs.get_opset_name(), attrs.get_type_name(), - error_message); + opset_version(), + additional_error_message()); } bool NotSupportedONNXNode::visit_attributes(ov::AttributeVisitor& visitor) { diff --git a/src/frontends/onnx/frontend/src/onnx_framework_node.hpp b/src/frontends/onnx/frontend/src/onnx_framework_node.hpp index f1807abc4e9543..24781b19f871cc 100644 --- a/src/frontends/onnx/frontend/src/onnx_framework_node.hpp +++ b/src/frontends/onnx/frontend/src/onnx_framework_node.hpp @@ -54,6 +54,10 @@ class ONNXFrameworkNode : public ov::op::util::FrameworkNode { return ov_nodes; } + int64_t opset_version() const { + return m_node.opset_version(); + } + virtual std::shared_ptr clone_with_new_inputs(const ov::OutputVector& inputs) const override; virtual bool visit_attributes(ov::AttributeVisitor& visitor) override { @@ -98,6 +102,7 @@ class ONNXSubgraphFrameworkNode : public ONNXFrameworkNode { // which are inserted into ov::Model due to different lifetime and problematic sharing between dynamic libs. class NotSupportedONNXNode : public ov::op::util::FrameworkNode { static constexpr const char* failed_conversion_key = "onnx::NotSupportedONNXNode::failed_conversion_key"; + static constexpr const char* opset_version_key = "onnx::NotSupportedONNXNode::opset_version_key"; public: OPENVINO_OP("NotSupportedONNXNode", "util", ov::op::util::FrameworkNode); @@ -106,18 +111,26 @@ class NotSupportedONNXNode : public ov::op::util::FrameworkNode { const size_t output_size, const std::string& domain, const std::string& op_type, + int64_t opset_version, const std::string& additional_error_message) : ov::op::util::FrameworkNode(inputs, output_size) { ov::op::util::FrameworkNodeAttrs attrs; attrs.set_opset_name(domain); attrs.set_type_name(op_type); attrs[failed_conversion_key] = additional_error_message; + attrs[opset_version_key] = std::to_string(opset_version); set_attrs(attrs); } std::string additional_error_message() const { - auto attrs = get_attrs(); - return attrs[failed_conversion_key]; + const auto& attrs = get_attrs(); + return attrs.at(failed_conversion_key); + } + + int64_t opset_version() const { + const auto& attrs = get_attrs(); + auto it = attrs.find(opset_version_key); + return (it != attrs.end()) ? std::stoll(it->second) : -1; } virtual std::shared_ptr clone_with_new_inputs(const ov::OutputVector& inputs) const override; diff --git a/src/frontends/onnx/frontend/src/translate_session.cpp b/src/frontends/onnx/frontend/src/translate_session.cpp index b393e27654ba1b..99584f7191e1c4 100644 --- a/src/frontends/onnx/frontend/src/translate_session.cpp +++ b/src/frontends/onnx/frontend/src/translate_session.cpp @@ -157,6 +157,8 @@ void TranslateSession::translate_graph(const ov::frontend::InputModel::Ptr& inpu } if (!error_message.empty()) { auto telemetry = model_onnx->get_telemetry_extension(); + std::string onnx_domain = decoder->get_domain(); + uint64_t opset_version = decoder->get_op_set(); if (m_fail_fast) { if (telemetry && translator == nullptr) { telemetry->send_event("error_cause", "onnx_" + decoder->get_op_type()); @@ -164,8 +166,6 @@ void TranslateSession::translate_graph(const ov::frontend::InputModel::Ptr& inpu FRONT_END_THROW(error_message); } else { if (telemetry && !error_message.empty()) { - std::string onnx_domain = decoder->get_domain(); - uint64_t opset_version = decoder->get_op_set(); error_message = "[ONNX Frontend] Conversion failed for " + (onnx_domain != "" ? "***." + decoder->get_op_type() + "-X" : decoder->get_op_type() + "-" + std::to_string(opset_version)) + @@ -174,8 +174,9 @@ void TranslateSession::translate_graph(const ov::frontend::InputModel::Ptr& inpu auto operation = std::make_shared(node_context.get_ov_inputs(), decoder->get_output_size(), - decoder->get_domain(), + onnx_domain, decoder->get_op_type(), + static_cast(opset_version), error_message); operation->set_friendly_name(decoder->get_op_name()); ov_outputs = operation->outputs(); diff --git a/src/frontends/onnx/frontend/src/utils/common.cpp b/src/frontends/onnx/frontend/src/utils/common.cpp index cb7fbe1096ae4f..6fb1649aa45aa8 100644 --- a/src/frontends/onnx/frontend/src/utils/common.cpp +++ b/src/frontends/onnx/frontend/src/utils/common.cpp @@ -210,115 +210,6 @@ bool is_optimized_out(const ov::Output& node_output) { return rt_info.find(OPTIMIZED_OUT_NODE) != rt_info.end(); } -bool collect_translation_exceptions(const std::shared_ptr& partially_converted, - const std::shared_ptr& telemetry, - std::ostream* output_stream, - std::shared_ptr> unsupported_operations, - std::shared_ptr> failures) { - if (partially_converted == nullptr) { - return false; - } - - const std::string sep = ", "; - bool first_call = !unsupported_operations || !failures; - if (!unsupported_operations) { - unsupported_operations = std::make_shared>(); - } - if (!failures) { - failures = std::make_shared>(); - } - - auto print_unsupported = [&](const std::shared_ptr fw_node) { - if (output_stream) { - if (unsupported_operations->size() == 0) { - *output_stream << "OpenVINO does not support the following ONNX operations: "; - } else { - *output_stream << sep; - } - *output_stream << (fw_node->get_attrs().get_opset_name().empty() - ? "" - : fw_node->get_attrs().get_opset_name() + ".") - << fw_node->get_attrs().get_type_name(); - } - }; - - for (const auto& node : partially_converted->get_ordered_ops()) { - if (const auto& fw_node = ov::as_type_ptr(node)) { - const auto& attrs = fw_node->get_attrs(); - auto node_name = attrs.get_opset_name() + "." + attrs.get_type_name(); - if (unsupported_operations->count(node_name) > 0) { - continue; - } - - print_unsupported(fw_node); - unsupported_operations->insert(node_name); - } else if (const auto& fw_node = ov::as_type_ptr(node)) { - const auto& attrs = fw_node->get_attrs(); - - if (fw_node->additional_error_message().empty()) { - auto node_name = attrs.get_opset_name() + "." + attrs.get_type_name(); - if (unsupported_operations->count(node_name) > 0) { - continue; - } - print_unsupported(fw_node); - unsupported_operations->insert(node_name); - } else { - const auto node_fail = fw_node->additional_error_message(); - if (failures->count(node_fail) > 0) { - continue; - } - failures->insert(node_fail); - } - - } else if (const auto& if_node = ov::as_type_ptr(node)) { - collect_translation_exceptions(if_node->get_then_body(), - telemetry, - output_stream, - unsupported_operations, - failures); - collect_translation_exceptions(if_node->get_else_body(), - telemetry, - output_stream, - unsupported_operations, - failures); - } else if (const auto& loop_node = ov::as_type_ptr(node)) { - collect_translation_exceptions(loop_node->get_function(), - telemetry, - output_stream, - unsupported_operations, - failures); - } - } - - if (!first_call) { - return unsupported_operations->size() != 0 || failures->size() != 0; - } - - if (telemetry) { - for (const auto& op : *unsupported_operations) { - telemetry->send_event("error_cause", "onnx_" + op); - } - for (const auto& str : *failures) { - telemetry->send_event("error_info", ov::util::filter_lines_by_prefix(str, "[ONNX Frontend] ")); - } - } - - if (output_stream && failures->size() > 0) { - if (unsupported_operations->size() > 0) { - *output_stream << std::endl; - } - *output_stream << "Errors during ONNX translation:"; - for (const auto& failure_message : *failures) { - auto pos = failure_message.find_last_not_of('\n'); - *output_stream << std::endl - << (pos > 0 && pos < failure_message.length() ? failure_message.substr(0, pos + 1) - : failure_message); - } - } - - return unsupported_operations->size() != 0 || failures->size() != 0; -} - int64_t normalize_axis(const std::string& description, const int64_t axis, const Rank& rank) { const auto r = rank.get_length(); FRONT_END_GENERAL_CHECK(ov::util::is_axis_valid(axis, r), diff --git a/src/frontends/onnx/frontend/src/utils/common.hpp b/src/frontends/onnx/frontend/src/utils/common.hpp index 391621cf568ea3..913213597781a4 100644 --- a/src/frontends/onnx/frontend/src/utils/common.hpp +++ b/src/frontends/onnx/frontend/src/utils/common.hpp @@ -170,21 +170,6 @@ void mark_as_optimized_out(ov::Output& node_output); /// \brief Checks if a given output was marked as optimized out byt the function above. bool is_optimized_out(const ov::Output& node_output); -/// \brief Collect unsupported operators after convert_partially and all exceptions from translation process. -/// \param partially_converted ov::Model which has been partially converted -/// \param telemetry Pointer on a TelemetryExtension if telemetry is enabled -/// \param output_stream Pointer on a stream for printint error messages -/// \param unsupported_operations Set for collecting list of unsupported operations, should be nullptr for -/// first call (will be created internally) -/// \param failures Set for collecting list of failed conversions, should be nullptr for -/// first call (will be created internally) -/// \return Returns true in case any issues has been found -bool collect_translation_exceptions(const std::shared_ptr& partially_converted, - const std::shared_ptr& telemetry = nullptr, - std::ostream* output_stream = nullptr, - std::shared_ptr> unsupported_operations = nullptr, - std::shared_ptr> failures = nullptr); - // \brief OpenVINO supports only uint64 seeds with a meaningful 0 value (seed will be auto-generated). // Because we use a seed as a just meaningful identifier we may // just interpret its value as a 32-bit value (float zero value is same with diff --git a/src/frontends/onnx/tests/convert_tests.cpp b/src/frontends/onnx/tests/convert_tests.cpp index 729f1a37218a97..9cd59012ada314 100644 --- a/src/frontends/onnx/tests/convert_tests.cpp +++ b/src/frontends/onnx/tests/convert_tests.cpp @@ -14,51 +14,49 @@ using namespace ov::frontend::onnx::tests; TEST(ONNXFeConvertException, exception_if_node_unsupported) { - OV_EXPECT_THROW( - convert_model("unsupported_ops/add_unsupported.onnx"), - ov::AssertFailure, - testing::EndsWith("OpenVINO does not support the following ONNX operations: test_domain.UnsupportedAdd\n")); + OV_EXPECT_THROW(convert_model("unsupported_ops/add_unsupported.onnx"), + ov::AssertFailure, + testing::HasSubstr("No conversion rule found for operations: test_domain.UnsupportedAdd")); } TEST(ONNXFeConvertException, exception_if_more_nodes_unsupported) { - OV_EXPECT_THROW( - convert_model("unsupported_ops/two_unsupported_nodes.onnx"), - ov::AssertFailure, - testing::EndsWith("OpenVINO does not support the following ONNX operations: UnsupportedAdd, UnsupportedAbs\n")); + OV_EXPECT_THROW(convert_model("unsupported_ops/two_unsupported_nodes.onnx"), + ov::AssertFailure, + testing::AllOf(testing::HasSubstr("No conversion rule found for operations:"), + testing::HasSubstr("UnsupportedAdd"), + testing::HasSubstr("UnsupportedAbs"))); } TEST(ONNXFeConvertException, exception_if_onnx_validation_exception) { OV_EXPECT_THROW(convert_model("instance_norm_bad_scale_type.onnx"), ov::AssertFailure, - testing::EndsWith("Element types for data and scale input do not match (data element type: f32, " - "scale element type: u16).\n")); + testing::HasSubstr("Element types for data and scale input do not match (data element type: f32, " + "scale element type: u16).")); } TEST(ONNXFeConvertException, exception_if_other_translation_exception) { OV_EXPECT_THROW(convert_model("depth_to_space_bad_mode.onnx"), ov::AssertFailure, - testing::EndsWith("only 'DCR' and 'CRD' modes are supported\n")); + testing::HasSubstr("only 'DCR' and 'CRD' modes are supported")); } TEST(ONNXFeConvertException, exception_if_both_unsupported_and_other_translation_exception) { - OV_EXPECT_THROW( - convert_model("unsupported_ops/unsupported_add_and_incorrect_dts.onnx"), - ov::AssertFailure, - testing::HasSubstr("OpenVINO does not support the following ONNX operations: test_domain.UnsupportedAdd")); OV_EXPECT_THROW(convert_model("unsupported_ops/unsupported_add_and_incorrect_dts.onnx"), ov::AssertFailure, - testing::EndsWith("only 'DCR' and 'CRD' modes are supported\n")); + testing::HasSubstr("No conversion rule found for operations: test_domain.UnsupportedAdd")); + OV_EXPECT_THROW(convert_model("unsupported_ops/unsupported_add_and_incorrect_dts.onnx"), + ov::AssertFailure, + testing::HasSubstr("only 'DCR' and 'CRD' modes are supported")); } TEST(ONNXFeConvertException, exception_if_both_unsupported_onnx_validation_exception_and_other_exception) { - OV_EXPECT_THROW( - convert_model("unsupported_ops/unsupported_add_incorrect_dts_and_inst_norm_bad_scale.onnx"), - ov::AssertFailure, - testing::HasSubstr("OpenVINO does not support the following ONNX operations: test_domain.UnsupportedAdd")); + OV_EXPECT_THROW(convert_model("unsupported_ops/unsupported_add_incorrect_dts_and_inst_norm_bad_scale.onnx"), + ov::AssertFailure, + testing::HasSubstr("No conversion rule found for operations: test_domain.UnsupportedAdd")); OV_EXPECT_THROW(convert_model("unsupported_ops/unsupported_add_incorrect_dts_and_inst_norm_bad_scale.onnx"), ov::AssertFailure, testing::HasSubstr("'stop' input is not a scalar")); OV_EXPECT_THROW(convert_model("unsupported_ops/unsupported_add_incorrect_dts_and_inst_norm_bad_scale.onnx"), ov::AssertFailure, - testing::HasSubstr("only 'DCR' and 'CRD' modes are supported\n")); + testing::HasSubstr("only 'DCR' and 'CRD' modes are supported")); } diff --git a/src/frontends/onnx/tests/onnx_import.in.cpp b/src/frontends/onnx/tests/onnx_import.in.cpp index 8ed158236e77eb..be89b28dae2968 100644 --- a/src/frontends/onnx/tests/onnx_import.in.cpp +++ b/src/frontends/onnx/tests/onnx_import.in.cpp @@ -220,9 +220,9 @@ OPENVINO_TEST(${BACKEND_NAME}, onnx_model_unsupported_op) { FAIL() << "Expected ov::Exception"; } catch (const ov::Exception& err) { std::string what{err.what()}; - EXPECT_NE(what.find("OpenVINO does not support"), std::string::npos); - EXPECT_NE(what.find("FakeOpName"), std::string::npos); - EXPECT_NE(what.find("AnotherFakeOpName"), std::string::npos); + EXPECT_NE(what.find("No conversion rule found for operations"), std::string::npos); + EXPECT_NE(what.find("FakeOpName-8"), std::string::npos); + EXPECT_NE(what.find("AnotherFakeOpName-8"), std::string::npos); } catch (...) { FAIL() << "Expected ov::Exception"; } diff --git a/src/frontends/onnx/tests/onnx_importer_test.cpp b/src/frontends/onnx/tests/onnx_importer_test.cpp index fdf22dc035d101..1e1bb53e973810 100644 --- a/src/frontends/onnx/tests/onnx_importer_test.cpp +++ b/src/frontends/onnx/tests/onnx_importer_test.cpp @@ -94,7 +94,7 @@ TEST(ONNX_Importer_Tests, ImportModelWithNotSupportedOp) { FAIL() << "No exception was thrown despite the ONNX model not being supported"; } catch (const Exception& error) { EXPECT_PRED_FORMAT2(testing::IsSubstring, - std::string("OpenVINO does not support the following ONNX operations: NotSupported"), + std::string("No conversion rule found for operations: NotSupported-7"), error.what()); } catch (...) { FAIL() << "Expected 'Exception' exception was not thrown despite the ONNX model is not supported"; diff --git a/src/frontends/pytorch/src/frontend.cpp b/src/frontends/pytorch/src/frontend.cpp index 49d669966e0310..8b0d2007a9b3e6 100644 --- a/src/frontends/pytorch/src/frontend.cpp +++ b/src/frontends/pytorch/src/frontend.cpp @@ -6,10 +6,8 @@ #include "input_model.hpp" #include "op_table.hpp" -#include "openvino/core/graph_util.hpp" #include "openvino/core/so_extension.hpp" #include "openvino/frontend/pytorch/extension/conversion.hpp" -#include "openvino/op/util/multi_subgraph_base.hpp" #include "openvino/util/common_util.hpp" #include "openvino/util/log.hpp" #include "place.hpp" @@ -44,15 +42,16 @@ #include "transforms/tuple_unpack_replacer.hpp" #include "transforms/u4_block_repack.hpp" #include "translate_session.hpp" +#include "unconverted_ops_report.hpp" namespace ov { namespace frontend { namespace pytorch { namespace { -std::map get_unconverted_types_from_model(const std::shared_ptr& model) { - std::map unconverted_ops_types; - ov::traverse_nodes(model, [&](const std::shared_ptr& node) { + +ov::frontend::FrameworkNodeExtractor make_pytorch_extractor() { + return [](const std::shared_ptr& node) -> std::optional> { if (const auto& fw_node = ov::as_type_ptr(node)) { const auto& attrs = fw_node->get_attrs(); auto op_type_it = attrs.find(PtFrameworkNode::op_type_key); @@ -63,66 +62,20 @@ std::map get_unconverted_types_from_model(const std::s if (exception_it != attrs.end()) { exception_msg = exception_it->second; } - if (!unconverted_ops_types.count(op_type_it->second)) { - unconverted_ops_types.emplace(op_type_it->second, std::move(exception_msg)); - } + return std::make_pair(op_type_it->second, std::move(exception_msg)); } else if (const auto& fw_node = ov::as_type_ptr(node)) { auto op_type = std::string(fw_node->get_type_name()); - if (!unconverted_ops_types.count(op_type)) { - std::stringstream consumer; - if (fw_node->get_output_size() > 0) { - auto inputs = fw_node->output(0).get_target_inputs(); - if (inputs.size() > 0) { - consumer << " Consumer: " << *(inputs.begin()->get_node()); - } + std::stringstream consumer; + if (fw_node->get_output_size() > 0) { + auto inputs = fw_node->output(0).get_target_inputs(); + if (inputs.size() > 0) { + consumer << " Consumer: " << *(inputs.begin()->get_node()); } - unconverted_ops_types.emplace(op_type, "This is OpenVINO internal type." + consumer.str()); - } - } - if (const auto& fw_node = ov::as_type_ptr(node)) { - for (size_t i = 0; i < fw_node->get_internal_subgraphs_size(); ++i) { - const auto& internal_types = get_unconverted_types_from_model(fw_node->get_function(i)); - unconverted_ops_types.insert(internal_types.begin(), internal_types.end()); } + return std::make_pair(op_type, "This is OpenVINO internal type." + consumer.str()); } - }); - return unconverted_ops_types; -} - -std::string pack_detailed_failure_report(const std::map& unconverted_ops, - const std::string& additional_error = "") { - std::stringstream error_msg; - std::stringstream unconverted_ops_msg; - std::stringstream failed_ops_msg; - std::stringstream failed_ops_short; - error_msg << "Model wasn't fully converted."; - unconverted_ops_msg << "-- No conversion rule found for operations: "; - failed_ops_msg << " Failed operations detailed log:"; - failed_ops_short << "-- Conversion is failed for: "; - bool at_least_one = false; - bool at_least_one_except = false; - for (auto&& op : unconverted_ops) { - if (op.second.empty()) { - if (at_least_one) - unconverted_ops_msg << ", "; - unconverted_ops_msg << op.first; - at_least_one = true; - } else { - if (at_least_one_except) - failed_ops_short << ", "; - failed_ops_short << op.first; - failed_ops_msg << "\n-- " << op.first << " with a message:\n" << op.second; - at_least_one_except = true; - } - } - if (at_least_one_except) - error_msg << failed_ops_msg.str(); - error_msg << "\nSummary:" << additional_error; - if (at_least_one) - error_msg << '\n' << unconverted_ops_msg.str(); - if (at_least_one_except) - error_msg << '\n' << failed_ops_short.str(); - return error_msg.str(); + return std::nullopt; + }; } void update_parameter_info(std::shared_ptr& param, @@ -166,18 +119,8 @@ std::shared_ptr FrontEnd::convert(const ov::frontend::InputModel::Ptr& mo norm_err = "\n-- normalize step failed with: " + std::string(e.what()); } - const auto& unconverted_ops = get_unconverted_types_from_model(converted_model); - for (auto&& op : unconverted_ops) { - if (m_telemetry) { - m_telemetry->send_event("error_cause", "pytorch_" + op.first); - auto cropped_message = ov::util::filter_lines_by_prefix(op.second, get_pytorch_prefix()); - if (cropped_message.size()) { - m_telemetry->send_event("error_info", cropped_message); - } - } - } - bool is_conversion_successful = unconverted_ops.size() == 0 && norm_err.empty(); - FRONT_END_OP_CONVERSION_CHECK(is_conversion_successful, pack_detailed_failure_report(unconverted_ops, norm_err)); + const auto report = ov::frontend::collect_unconverted_ops(converted_model, make_pytorch_extractor()); + ov::frontend::check_unconverted_ops(report, m_telemetry, "pytorch", get_pytorch_prefix(), norm_err); if (pt_model->m_requested_places.size() != 0) { // Fake tensors mean that types were set to non-existent before conversion inputs. diff --git a/src/frontends/tensorflow/src/frontend.cpp b/src/frontends/tensorflow/src/frontend.cpp index ddc72b9bbb719a..527d0fe0aedc10 100644 --- a/src/frontends/tensorflow/src/frontend.cpp +++ b/src/frontends/tensorflow/src/frontend.cpp @@ -37,65 +37,75 @@ #include "transformations/transpose_sinking/ts_general.hpp" #include "transformations/uninitialized_variable_resolve.hpp" #include "translate_session.hpp" +#include "unconverted_ops_report.hpp" #include "utils.hpp" using namespace ov; using namespace ov::frontend::tensorflow; namespace { -void update_failures_unsupported_ops(const std::string& op_type, - const ov::op::util::FrameworkNodeAttrs& fw_node_attrs, - std::set& unsupported_operations, - std::unordered_map& failures) { - // if this operation is encountered among unsupported operations - // or conversion failures, skip it - if (failures.count(op_type) > 0 || unsupported_operations.count(op_type) > 0) { - return; - } - if (fw_node_attrs.find(FrameworkNode::failed_conversion_key) != fw_node_attrs.end()) { - // save only the first encountered failure that is more important for developer - // that means the translator is found but the conversion is failed - failures[op_type] = fw_node_attrs.at(FrameworkNode::failed_conversion_key); - } else { - // found new unsupported operation - unsupported_operations.insert(op_type); - } -} -void get_unsupported_operations_and_failures(const std::shared_ptr& model, - std::set& unsupported_operations, - std::unordered_map& failures) { - for (const auto& node : model->get_ordered_ops()) { +ov::frontend::FrameworkNodeExtractor make_tensorflow_extractor() { + return [](const std::shared_ptr& node) -> std::optional> { if (const auto& internal_op = ov::as_type_ptr(node)) { // handle internal operations separately // which can have elaborated reason of unconverted operation // like Const of string type auto op_type = internal_op->get_no_conversion_reason(); - if (unsupported_operations.count(op_type) > 0) { - continue; - } - unsupported_operations.insert(op_type); + return std::make_pair(op_type, std::string{}); } else if (const auto& variable = ov::as_type_ptr(node)) { auto op_type = variable->get_decoder()->get_op_type(); auto op_name = variable->get_name(); - failures[op_type] = "Variable or resource `" + op_name + "` is not initialized, model is inconsistent"; + return std::make_pair(op_type, + "Variable or resource `" + op_name + "` is not initialized, model is inconsistent"); } else if (const auto& fw_node = ov::as_type_ptr(node)) { auto op_type = fw_node->get_decoder()->get_op_type(); - auto fw_node_attrs = fw_node->get_attrs(); - update_failures_unsupported_ops(op_type, fw_node_attrs, unsupported_operations, failures); + const auto& fw_node_attrs = fw_node->get_attrs(); + if (fw_node_attrs.find(FrameworkNode::failed_conversion_key) != fw_node_attrs.end()) { + return std::make_pair(op_type, fw_node_attrs.at(FrameworkNode::failed_conversion_key)); + } + return std::make_pair(op_type, std::string{}); } else if (const auto& fw_node = ov::as_type_ptr(node)) { // handle auxiliary operations from common frontend like ComplexTypeMark + // Note: base FrameworkNode class doesn't have failed_conversion_key auto op_type = std::string(fw_node->get_type_name()); - auto fw_node_attrs = fw_node->get_attrs(); - update_failures_unsupported_ops(op_type, fw_node_attrs, unsupported_operations, failures); + return std::make_pair(op_type, std::string{}); } - if (const auto& fw_node = ov::as_type_ptr(node)) { - int subgraphs_size = static_cast(fw_node->get_internal_subgraphs_size()); - for (int i = 0; i < subgraphs_size; ++i) { - get_unsupported_operations_and_failures(fw_node->get_function(i), unsupported_operations, failures); + return std::nullopt; + }; +} + +ov::frontend::AdditionalErrorCallback make_tokenizer_callback() { + return [](const std::set& unsupported_ops) -> std::string { + std::stringstream additional_info; + additional_info << "To facilitate the conversion of unsupported operations, refer to Frontend Extension " + "documentation: " + "https://docs.openvino.ai/latest/openvino_docs_Extensibility_UG_Frontend_Extensions.html \n"; + + // Check for tokenizer operations + const auto& all_tokenizer_ops = ov::frontend::tensorflow::op::get_supported_ops_via_tokenizers(); + std::string unsupported_ops_from_tokenizers; + size_t tokenizer_counter = 0; + for (const auto& unsupported_operation : unsupported_ops) { + if (std::find(all_tokenizer_ops.begin(), all_tokenizer_ops.end(), unsupported_operation) != + all_tokenizer_ops.end()) { + if (tokenizer_counter > 0) { + unsupported_ops_from_tokenizers += ", "; + } + unsupported_ops_from_tokenizers += unsupported_operation; + ++tokenizer_counter; } } - } + + if (!unsupported_ops_from_tokenizers.empty()) { + additional_info << "\nEncountered unconverted operation(s) for which openvino-tokenizers package " + "provides conversion extension(s): " + << unsupported_ops_from_tokenizers + << ". Install OpenVINO Tokenizers, refer to the documentation: " + "https://docs.openvino.ai/2025/openvino-workflow-generative/ov-tokenizers.html \n"; + } + return additional_info.str(); + }; } void translate_framework_node(const std::shared_ptr& node, @@ -416,66 +426,13 @@ ov::frontend::InputModel::Ptr FrontEnd::load_impl(const std::vector& va std::shared_ptr FrontEnd::convert(const ov::frontend::InputModel::Ptr& model) const { auto f = convert_partially(model); - std::unordered_map failures; - std::set unsupported_operations; - get_unsupported_operations_and_failures(f, unsupported_operations, failures); - - std::stringstream exception_message; - for (const auto& failure : failures) { - auto exception_str = "[TensorFlow Frontend] Internal error, conversion is failed for " + failure.first + - " operation with a message:\n" + failure.second + "\n"; - exception_message << exception_str; - if (m_telemetry) { - m_telemetry->send_event("error_info", - ov::util::filter_lines_by_prefix(exception_str, "[TensorFlow Frontend] ")); - } - } - - if (m_telemetry) { - for (const auto& unsupported_operation : unsupported_operations) { - m_telemetry->send_event("error_cause", "tf_" + unsupported_operation); - } - } - if (unsupported_operations.size() > 0) { - exception_message << "[TensorFlow Frontend] Internal error, no translator found for operation(s): "; - size_t counter = 0; - size_t tokenizer_counter = 0; - std::string unsupported_ops_from_tokenizers; - const auto& all_tokenizer_ops = ov::frontend::tensorflow::op::get_supported_ops_via_tokenizers(); - for (const auto& unsupported_operation : unsupported_operations) { - if (counter > 0) { - exception_message << ", "; - } - exception_message << unsupported_operation; - ++counter; - - // collect a list of unconverted operations for which openvino-tokenizers provides conversion extensions - if (std::find(all_tokenizer_ops.begin(), all_tokenizer_ops.end(), unsupported_operation) != - all_tokenizer_ops.end()) { - if (tokenizer_counter > 0) { - unsupported_ops_from_tokenizers += ", "; - } - unsupported_ops_from_tokenizers += unsupported_operation; - ++tokenizer_counter; - } - } - exception_message - << "\nTo facilitate the conversion of unsupported operations, refer to Frontend Extension " - "documentation: " - "https://docs.openvino.ai/latest/openvino_docs_Extensibility_UG_Frontend_Extensions.html \n"; - - // recommend to use openvino-tokenizers if some unconverted operations from tokenizers are met - if (unsupported_ops_from_tokenizers.size() > 0) { - exception_message << "\nEncountered unconverted operation(s) for which openvino-tokenizers package " - "provides conversion extension(s): " - << unsupported_ops_from_tokenizers - << ". Install OpenVINO Tokenizers, refer to the documentation: " - "https://docs.openvino.ai/2025/openvino-workflow-generative/ov-tokenizers.html \n"; - } - } - - bool is_conversion_successful = ((unsupported_operations.size() == 0) && (failures.size() == 0)); - FRONT_END_OP_CONVERSION_CHECK(is_conversion_successful, exception_message.str()); + const auto report = ov::frontend::collect_unconverted_ops(f, make_tensorflow_extractor()); + ov::frontend::check_unconverted_ops(report, + m_telemetry, + "tf", + "[TensorFlow Frontend] ", + "", + make_tokenizer_callback()); return f; } diff --git a/src/frontends/tensorflow/tests/telemetry.cpp b/src/frontends/tensorflow/tests/telemetry.cpp index e39e5e4d8c5ff8..548a79588735da 100644 --- a/src/frontends/tensorflow/tests/telemetry.cpp +++ b/src/frontends/tensorflow/tests/telemetry.cpp @@ -86,7 +86,7 @@ TEST(TFTelemetryTest, test_nonexistent_add) { FAIL() << "Non-existent operation Adddd must not be supported by TF FE."; } catch (const OpConversionFailure& error) { std::string error_message = error.what(); - std::string ref_message = "Internal error, no translator found for operation(s): Adddd"; + std::string ref_message = "No conversion rule found for operations: Adddd"; ASSERT_TRUE(error_message.find(ref_message) != std::string::npos); ASSERT_EQ(model, nullptr); diff --git a/tests/layer_tests/ovc_python_api_tests/test_tf_unsupported_ops.py b/tests/layer_tests/ovc_python_api_tests/test_tf_unsupported_ops.py index 0a3b59e7ed645c..bd1792f8b4cb5b 100644 --- a/tests/layer_tests/ovc_python_api_tests/test_tf_unsupported_ops.py +++ b/tests/layer_tests/ovc_python_api_tests/test_tf_unsupported_ops.py @@ -83,7 +83,7 @@ def test_write_file_in_if(self): model = create_if_net(self.tmp_dir) with self.assertRaisesRegex(OpConversionFailure, - ".*Internal error, no translator found for operation\(s\): WriteFile.*"): + ".*No conversion rule found for operations: WriteFile.*"): convert_model(model) @pytest.mark.nightly @@ -94,5 +94,5 @@ def test_write_file_in_part_call(self): model = create_part_call_net(self.tmp_dir) with self.assertRaisesRegex(OpConversionFailure, - '.*Internal error, no translator found for operation\(s\): WriteFile.*'): + '.*No conversion rule found for operations: WriteFile.*'): convert_model(model)