変換 API の概要

OpenVINO 変換メカニズムを使用すると、ov::Model を変更する変換パスを開発できます。このメカニズムを使用すると、元のモデルに最適化を追加したり、サポートされていないサブグラフや操作をプラグインでサポートする新しい操作に変換したりできます。このガイドには、OpenVINO™ 変換の実装を始めるのに必要なすべての情報が含まれます。

モデルの操作

変換に移行する前に、ov::Model を変更できる関数について説明する必要があります。この節では、モデル表現ガイドを拡張し、ov::Model で操作を可能にする API を示します。

ノードの入力ポートと出力ポートの操作

まず最初に、ov::Node の入出力ポートについて説明します。各 OpenVINO™ 操作には、操作がパラメーターまたは定数タイプを持つ場合を除き、入力ポートと出力ポートがあります。

すべてのポートはノードに属しているため、ポートを使用して親ノードにアクセスし、特定の入力/出力の形状とタイプを取得し、出力ポートはすべてのコンシューマーを取得し、入力ポートはプロデューサー・ノードを取得できます。出力ポートを使用すると、新しく作成された操作の入力を設定できます。

コードの例をご覧ください。

// 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™ ヘルパー機能を使用する方法と、直接ポートメソッドを使用する 2 つのノード交換方法が提供されます。両方を確認していきます。

OpenVINO™ ヘルパー関数から始めます。最もポピュラーな関数は ov::replace_node(old_node, new_node) です。

負の演算を乗算に置き換えるケースをレビューします。

../../_images/ov_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));

もう 1 つの変換方法は挿入です。

../../_images/ov_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);
}

ノードの削除

置換のもう 1 つのタイプはノードの削除です。

動作を排除するため、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™ ランタイムには 3 つの主な変換タイプがあります。

../../_images/transformations_structure.png

変換の条件付きコンパイル

変換ライブラリーには、条件付きコンパイル機能をサポートする 2 つの内部マクロがあります。

  • 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());
ov::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> を他のノードの入力として渡さないでください。明示的な出力ポートを使用します。

  • ノードを、異なる形状を生成する別のノードに置き換える場合は、ov::Model に対する最初の validate_nodes_and_infer_types 呼び出しが行われるまで、新しい形状は伝播されないことに注意してください。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>();
    using namespace ov::pass;
    ADD_MATCHER(anchor, DecomposeDivideMatcher)
    ADD_MATCHER(anchor, ReluReluFusionMatcher)
    manager.run_passes(f);

変換のデバッグ方法

ov::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 がインストールされていることを確認してください。そうでない場合は、dot のみがサイレントに保存され、svg ファイルは保存されません。