转换 API 概述

OpenVINO 转换机制允许开发转换传递来修改 ov::Model。您可以使用此机制对原始模型应用额外的优化,或将不支持的子图和操作转换为插件支持的新操作。本指南包含您开始实施 OpenVINO™ 转换所需的所有必要信息。

使用模型

在进入转换部分之前,需要先了解一下可以修改 ov::Model 的功能。本章节扩展了模型表示指南,并展示了一个允许我们使用 ov::Model 进行操作的 API。

使用节点输入和输出端口

首先,我们谈谈 ov::Node 输入/输出端口。每个 OpenVINO™ 操作都有输入和输出端口,但操作有 ParameterConstant 类型的情况除外。

每个端口都属于其节点,因此我们可以使用端口访问父节点,获取特定输入/输出的形状和类型,在输出端口的情况下获取所有消费者,在输入端口的情况下获取生产者节点。通过输出端口,我们可以为新创建的操作设置输入。

我们看一下代码示例。

// Let's suppose that node is opset8::Convolution operation
// as we know opset8::Convolution has two input ports (data, weights) and one output port
ov::Input<ov::Node> data = node->input(0);
ov::Input<ov::Node> weights = node->input(1);
ov::Output<ov::Node> output = node->output(0);

// Getting shape and type
auto pshape = data.get_partial_shape();
auto el_type = data.get_element_type();

// Getting parent for input port
ov::Output<ov::Node> parent_output;
parent_output = data.get_source_output();

// Another short way to get partent for output port
parent_output = node->input_value(0);

// Getting all consumers for output port
auto consumers = output.get_target_inputs();

节点替换

OpenVINO™ 提供了两种节点替换方法:通过 OpenVINO™ 辅助函数和直接通过端口。下面我们再来看看这两种方法。

我们先说说 OpenVINO™ 辅助函数。最受欢迎的函数是 ov::replace_node(old_node, new_node)

我们来看一看真实的替换用例,其中负运算被乘法替换。

_images/ngraph_replace_node.png
bool ov_replace_node(std::shared_ptr<ov::Node> node) {
    // Step 1. Verify that node has opset8::Negative type
    auto neg = std::dynamic_pointer_cast<ov::opset8::Negative>(node);
    if (!neg) {
        return false;
    }

    // Step 2. Create opset8::Multiply operation where the first input is negative operation input and second as Constant with -1 value
    auto mul = std::make_shared<ov::opset8::Multiply>(neg->input_value(0),
                                                      ov::opset8::Constant::create(neg->get_element_type(), ov::Shape{1}, {-1}));

    mul->set_friendly_name(neg->get_friendly_name());
    ov::copy_runtime_info(neg, mul);

    // Step 3. Replace Negative operation with Multiply operation
    ov::replace_node(neg, mul);
    return true;

    // Step 4. Negative operation will be removed automatically because all consumers was moved to Multiply operation
}

ov::replace_node 有一个约束条件,即两个操作的输出端口数必须相同;否则,它会引发异常。

执行相同替换的替代方法如下:

// All neg->output(0) consumers will be moved to mul->output(0) port
neg->output(0).replace(mul->output(0));

另一个转换示例是插入。

_images/ngraph_insert_node.png
// Step 1. Lets suppose that we have a node with single output port and we want to insert additional operation new_node after it
void insert_example(std::shared_ptr<ov::Node> node) {
    // Get all consumers for node
    auto consumers = node->output(0).get_target_inputs();

    // Step 2. Create new node. Let it be opset8::Relu.
    auto new_node = std::make_shared<ov::opset8::Relu>(node);

    // Step 3. Reconnect all consumers to new_node
    for (auto input : consumers) {
        input.replace_source_output(new_node);
    }
}

插入操作的替代方法是复制节点和使用 ov::replace_node()

void insert_example_with_copy(std::shared_ptr<ov::Node> node) {
    // Make a node copy
    auto node_copy = node->clone_with_new_inputs(node->input_values());
    // Create new node
    auto new_node = std::make_shared<ov::opset8::Relu>(node_copy);
    ov::replace_node(node, new_node);
}

节点消除

另一种节点替换是消除。

为了取消操作,OpenVINO™ 有一种特殊方法,该方法考虑了与 OpenVINO™ 运行时相关的所有限制。

// Suppose we have a node that we want to remove
bool success = ov::replace_output_update_name(node->output(0), node->input_value(0));

如果 ov::replace_output_update_name() 成功替换,它会自动保存友好名称和运行时信息。

转换类型

OpenVINO™ 运行时有三种主要转换类型:

_images/transformations_structure.png

转换条件编译

转换库拥有两个内部宏来支持条件编译功能。

  • MATCHER_SCOPE(region)- 如果不使用匹配器,允许禁用匹配器传递。该区域名称应具有唯一性。此宏创建一个局部变量 matcher_name,您应该将其用作匹配器名称。

  • RUN_ON_MODEL_SCOPE(region)- 如果未使用,允许禁用 run_on_model 传递。该区域名称应具有唯一性。

转换编写基本知识

在开发转换时,您需要遵循这些转换规则:

1.友好名称

每个 ov::Node 都拥有唯一名称和友好名称。在转换中,我们仅关注友好名称,因为它代表模型的名称。为了避免在用其他节点或子图替换节点时丢失友好名称,请将原始友好名称设置为替换子图中的最新节点。参见下面的示例。

// Replace Div operation with Power and Multiply sub-graph and set original friendly name to Multiply operation
auto pow = std::make_shared<ov::opset8::Power>(div->input(1).get_source_output(),
                                               ov::op::v0::Constant::create(div->get_input_element_type(1), ov::Shape{1}, {-1}));
auto mul = std::make_shared<ov::opset8::Multiply>(div->input(0).get_source_output(), pow);
mul->set_friendly_name(div->get_friendly_name());
ngraph::replace_node(div, mul);

在更高级的情况下,当被替换的操作有多个输出并且我们在它的输出中添加额外消费者时,我们决定如何通过排列来设置友好名称。

2.运行时信息

运行时信息是一张位于 ov::Node 类中的图表 std::map<std::string, ov::Any>。它代表 ov::Node 中的其他属性。这些属性可以由用户或插件设置,并且在执行更改 ov::Model 的转换时,我们需要保留这些属性,因为它们不会自动传播。在大多数情况下,转换具有以下类型:1:1(将节点替换为另一个节点)、1:N(将节点替换为子图)、N:1(将子图融合为单个节点)、N:M(任何其他转换)。目前,没有自动检测转换类型的机制,因此我们需要手动传播此运行时信息。参见以下示例。

// Replace Transpose with Reshape operation (1:1)
ov::copy_runtime_info(transpose, reshape);

// Replace Div operation with Power and Multiply sub-graph (1:N)
ov::copy_runtime_info(div, {pow, mul});

// Fuse Convolution with Add operation (N:1)
ov::copy_runtime_info({conv, bias}, {conv_fused});

// Any other transformation that replaces one sub-graph with another sub-graph (N:M)
ov::copy_runtime_info({a, b, c}, {e, f});

当转换有多个融合或分解时,必须为每种情况多次调用 ov::copy_runtime_info

注意:copy_runtime_info 从目标节点中删除 rt_info。如果您想要保留它,则需要在如下所示的源节点中指定它们:copy_runtime_info({a, b, c}, {a, b})

3.常量折叠

如果转换插入了需要折叠的常量子图,不要忘记在转换后使用 ov::pass::ConstantFolding() 或者直接调用常量折叠进行操作。以下示例显示了如何构建常量子图。

// After ConstantFolding pass Power will be replaced with Constant
auto input = std::make_shared<ov::opset8::Parameter>(ov::element::f32, ov::Shape{1});
auto pow = std::make_shared<ov::opset8::Power>(ov::opset8::Constant::create(ov::element::f32, ov::Shape{1}, {2}),
                                               ov::opset8::Constant::create(ov::element::f32, ov::Shape{1}, {3}));
auto mul = std::make_shared<ov::opset8::Multiply>(input /\* not constant input \*/, pow);

手动常量折叠比 ov::pass::ConstantFolding() 更可取,因为它更快。

您可以在下面找到手动常量折叠的示例:

template <class T>
ov::Output<ov::Node> eltwise_fold(const ov::Output<ov::Node>& input0, const ov::Output<ov::Node>& input1) {
    auto eltwise = std::make_shared<T>(input0, input1);
    ov::OutputVector output(eltwise->get_output_size());
    // If constant folding wasn't successful return eltwise output
    if (!eltwise->constant_fold(output, {input0, input1})) {
        return eltwise->output(0);
    }
    return output[0];
}

转换中的常见错误

在转换开发流程中:

  • 请勿使用弃用的 OpenVINO™ API。弃用的方法在其定义中有 OPENVINO_DEPRECATED 宏。

  • 如果节点类型未知或有多个输出,请勿作为其他节点的输入传递 shared_ptr<Node>。使用明确的输出端口。

  • 如果您用另一个产生不同形状的节点替换某一节点,请记住新形状不会传播,直到第一个 validate_nodes_and_infer_types 调用 ov::Model。如果您正在使用 ov::pass::Manager,它将在每次转换执行后自动调用此方法。

  • 如果转换创建了常量子图,请不要忘记调用 ov::pass::ConstantFolding 传递。

  • 如果您不开发降级转换传递,请使用最新的 OpSet。

  • ov::pass::MatcherPass 开发回调时,不要更改拓扑顺序中根节点之后的节点。

使用传递管理器

ov::pass::Manager 是一个容器类,可以存储转换列表并执行转换。此类的主要目的是为分组的转换列表提供高级别表示。它可以在模型上注册和应用任何转换传递。此外,ov::pass::Manager 具有扩展的调试功能(在如何调试转换部分中查找更多信息)。

以下示例显示了 ov::pass::Manager 的基本使用情况

ov::pass::Manager manager;
manager.register_pass<ov::pass::MyModelTransformation>();
// Two matchers will run independently (two independent graph traversals)
// pass::Manager automatically creates GraphRewrite container for each MatcherPass
manager.register_pass<ov::pass::DecomposeDivideMatcher>();
manager.register_pass<ov::pass::ReluReluFusionMatcher>();
manager.run_passes(f);

另一个示例显示了如何将多个匹配器传递融合为单个 GraphRewrite。

// Register anchor GraphRewrite pass inside manager that will execute two matchers simultaneously
ov::pass::Manager manager;
auto anchor = manager.register_pass<ov::pass::GraphRewrite>();
anchor->add_matcher<ov::pass::DecomposeDivideMatcher>();
anchor->add_matcher<ov::pass::ReluReluFusionMatcher>();
manager.run_passes(f);

如何调试转换

如果您使用 ngraph::pass::Manager 运行转换序列,则可以通过使用以下环境变量获得额外的调试功能:

OV_PROFILE_PASS_ENABLE=1 - enables performance measurement for each transformation and prints execution status
OV_ENABLE_VISUALIZE_TRACING=1 -  enables visualization after each transformation. By default, it saves dot and svg files.

注意:确保您的机器上安装了 dot;否则,它只会默认保存没有 svg 文件的 dot 文件。