Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#pragma once

#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>

#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<std::string, std::string> unconverted_ops;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::map<std::string, std::string> unconverted_ops;
std::unordered_map<std::string, std::string> 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<std::optional<std::pair<std::string, std::string>>(const std::shared_ptr<ov::Node>&)>;

/// \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<ov::Model>& 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<std::string(const std::set<std::string>&)>;

/// \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<TelemetryExtension>& 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
147 changes: 147 additions & 0 deletions src/frontends/common_translators/src/unconverted_ops_report.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#include "unconverted_ops_report.hpp"

#include <sstream>

#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<ov::Model>& 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)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (auto result = extractor(node)) {
if (const 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<ov::op::util::MultiSubGraphOp>(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: ";
Comment on lines +64 to +67
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we impact telemetry mechanism that collect fails and separate into FEs by [FW_NAME]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that message is not processed by telemetry. The telemetry logic is preserved


bool has_unsupported = false;
bool has_failed = false;
std::set<std::string> unsupported_ops_set;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::set<std::string> unsupported_ops_set;
std::unordered_set<std::string> 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<TelemetryExtension>& 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
68 changes: 10 additions & 58 deletions src/frontends/jax/src/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string, std::string> get_unconverted_types_from_model(const std::shared_ptr<Model>& model) {
std::map<std::string, std::string> unconverted_ops_types;
for (const auto& node : model->get_ordered_ops()) {

ov::frontend::FrameworkNodeExtractor make_jax_extractor() {
return [](const std::shared_ptr<ov::Node>& node) -> std::optional<std::pair<std::string, std::string>> {
if (const auto& fw_node = ov::as_type_ptr<JaxFrameworkNode>(node)) {
const auto& attrs = fw_node->get_attrs();
FRONT_END_GENERAL_CHECK(attrs.find(JaxFrameworkNode::op_type_key) != attrs.end(),
Expand All @@ -29,55 +29,12 @@ std::map<std::string, std::string> 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<ov::op::util::MultiSubGraphOp>(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<std::string, std::string>& 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() {}
Expand All @@ -91,14 +48,9 @@ std::shared_ptr<Model> 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;
}

Expand Down
7 changes: 4 additions & 3 deletions src/frontends/onnx/frontend/src/core/graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) +
Expand All @@ -357,8 +357,9 @@ ov::OutputVector Graph::make_ov_nodes(const Node& onnx_node) {
const auto not_supported_node =
std::make_shared<ov::frontend::onnx::NotSupportedONNXNode>(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();
}
Expand Down
5 changes: 5 additions & 0 deletions src/frontends/onnx/frontend/src/core/graph.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class Graph : public std::enable_shared_from_this<Graph> {
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 {
Expand Down
13 changes: 13 additions & 0 deletions src/frontends/onnx/frontend/src/core/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::reference_wrapper<const std::string>>& get_output_names() const;
Expand Down Expand Up @@ -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<std::reference_wrapper<const std::string>>& Node::Impl::get_output_names() const {
return m_output_names;
}
Expand Down Expand Up @@ -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<int64_t>(m_decoder->get_op_set());
}
FRONT_END_NOT_IMPLEMENTED(opset_version);
}

const std::vector<std::reference_wrapper<const std::string>> Node::get_output_names() const {
if (m_pimpl != nullptr) {
return m_pimpl->get_output_names();
Expand Down
Loading
Loading