トランスフォーメーション API 概要#

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

モデルの動作#

トランスフォーメーションに進む前に、変更できる ov::Model 関数について少し説明します。この節では、モデル表現ガイドを拡張し、ov::Model で操作を可能にする API を示します。

ノードの入力と出力ポートの動作#

各 OpenVINO 操作には、パラメーター定数タイプを除いて、ov::Node 入力ポートと出力ポートがあります。OpenVINO では、ノード操作という用語は同じ意味で使用されますが、この記事ではそれらの使用に一貫性を保っています。

すべてのポートはノードに関連付けられており、その形状、タイプ、出力ポートのすべての消費者、入力ポートの生産者ノードなど、ポートが属するノードへのアクセスを許可します。

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

// ノードが ov::op::v0::Convolution タイプであると仮定します。// 既知のとおり、ov::op::v0::Convolution には 2 つの入力ポート (データ、重み) と 1 つの出力ポートがあります。 
ov::Input<ov::Node> data = node->input(0); 
ov::Input<ov::Node> weights = node->input(1); 
ov::Output<ov::Node> output = node->output(0); 

// 形状とタイプを取得 
auto pshape = data.get_partial_shape(); 
auto el_type = data.get_element_type(); 

// 入力ポートの親、つまり入力によってマップされた出力を取得 
ov::Output<ov::Node> parent_output; 
parent_output = data.get_source_output(); 

// 出力ポートの親を取得する別の簡単な方法 
parent_output = node->input_value(0); 

// 出力ポートのすべての消費を取得 
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) { 
    // ステップ 1. ノードが ov::op::v0::Negative タイプであることを確認 
    auto neg = std::dynamic_pointer_cast<ov::op::v0::Negative>(node); 
    if (!neg) { 
        return false; 
    } 

    // ステップ 2. 最初の入力を負の値の出力とし、2 番目を -1 の値を持つ定数として ov::op::v1::Multiply 演算を作成 
    auto mul = std::make_shared<ov::op::v1::Multiply>(neg->input_value(0), ov::op::v0::Constant::create(neg->get_element_type(), ov::Shape{1}, {-1})); 

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

    // ステップ 3. 負の演算を乗算演算に置き換える 
    ov::replace_node(neg, mul); 
    return true; 

    // ステップ 4. すべての消費者が乗算演算に移動されたため、負の演算は自動的に削除されます 
}

ov::replace_node には、両方の演算の出力ポートが同数でなければならないという制約があります。同数でないと例外が発生します。

同じ置換を行う代替方法は次のとおりです:

// すべての neg->output(0) 消費者は mul->output(0) ポートに移動されます 
neg->output(0).replace(mul->output(0));

もう 1 つの変換方法は挿入です。Relu ノードを挿入してみましょう。

../../_images/ov_insert_node.png
// ステップ 1. 出力ポートが 1 つあるノードがあり、その後に追加の操作である新しいノードを挿入 
void insert_example(std::shared_ptr<ov::Node> node) { 
    // ノードのすべての消費者を取得 
    auto consumers = node->output(0).get_target_inputs(); 

    // ステップ 2. 新しいノード ov::op::v0::Relu を作成 
    auto new_node = std::make_shared<ov::op::v0::Relu>(node); 

    // ステップ 3. すべての消費者を 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) { 
    // ノードをコピー 
    auto node_copy = node->clone_with_new_inputs(node->input_values()); 
    // 新しいノードを作成 
    auto new_node = std::make_shared<ov::op::v0::Relu>(node_copy); 
    ov::replace_node(node, new_node); 
}

ノードの削除#

置換のもう 1 つのタイプはノードを排除することです。

ノードを排除するため、OpenVINO™ には OpenVINO™ ランタイムに関連するすべての制限を考慮した特殊メソッドがあります。

// 削除したいノードがあるとします 
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 には一意の名前とフレンドリー名があります。トランスフォーメーションでは、モデルの観点から名前を表すため、フレンドリー名のみが重要になります。ノードを他のノードまたはサブグラフに置き換える際にフレンドリー名が失われないように、サブグラフを置き換える際に元のフレンドリー名を最新のノードに設定します。次の例を参照してください。

// Div 操作を Power および Multiply サブグラフに置き換え、元のフレンドリー名を Multiply 操作に設定します。
 
auto pow = std::make_shared<ov::op::v1::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::op::v1::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 (その他のトランスフォーメーション)。現在、トランスフォーメーション・タイプを自動的に検出するメカニズムがないため、ランタイムで情報を手動で伝達する必要があります。以下の例をご覧ください:

// 転置を変形操作に置き換える (1:1) 
ov::copy_runtime_info(transpose, reshape); 

// Div 操作を Power と Multiply サブグラフに置き換える (1:N) 
ov::copy_runtime_info(div, {pow, mul}); 

// 加算操作による畳み込みの融合 (N:1) 
ov::copy_runtime_info({conv, bias}, {conv_fused}); 

// 1 つのサブグラフを別のサブグラフに置き換えるその他の変換 (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() を使用することを忘れないでください。または、操作の定数折り込みを直接呼び出してください。以下の例は、定数サブグラフを構築する方法を示しています。

// ConstantFolding パスの後、Power は Constant に置き換えられます 
auto input = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::Shape{1}); 

auto pow = std::make_shared<ov::op::v1::Power>(ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {2}), ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {3})); 

auto mul = std::make_shared<ov::op::v1::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()); 
    // 定数畳み込みが成功しなかった場合は、eltwise 出力を返す 
    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>(); 
// 2 つのマッチャーが独立して実行されます (2 つの独立したグラフ・トラバーサル) 
// pass::Manager は MatcherPass ごとに GraphRewrite コンテナを自動的に作成します 
manager.register_pass<ov::pass::DecomposeDivideMatcher>(); 
manager.register_pass<ov::pass::ReluReluFusionMatcher>(); 
manager.run_passes(f);

別の例では、複数のマッチャーパスを単一の GraphRewrite に結合する方法を示します。

 // 2 つのマッチャーを同時に実行するマネージャー内にアンカー GraphRewrite パスを登録 
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 - 各変換のパフォーマンス測定を可能にし、実行ステータスを出力します。 

OV_ENABLE_VISUALIZE_TRACING=1 - 各変換後の視覚化を有効にします。デフォルトでは、dotファイルと svg ファイルを保存します。

システムに dot がインストールされていることを確認してください。そうでない場合は、dot のみがサイレントに保存され、svg ファイルは保存されません。

関連情報#