フロントエンド拡張

ここでの目的は、フロントエンド拡張クラスを使用して、フレームワーク・モデル表現から OpenVINO 表現へのカスタム操作のマッピングを容易にする方法を説明することです。全体の流れを理解するには、OpenVINO 拡張性を参照してください。

この API は、ONNX、TensorFlow Lite、PaddlePaddle、および TensorFlow に存在する新しいフロントエンドにのみ適用されます。異なるモデル形式が使用されている場合は、従来のモデル・オプティマイザー拡張ガイドに従ってください。

このドキュメントはテンプレート拡張機能に基づいて書かれており、実際のカスタム操作のプレースホルダーである最小限の Identity 操作をベースとした拡張機能開発の詳細を示します。コンパイル可能な完全なコードをレビューして、それがどのように機能するかを確認できます。

拡張機能のその他の例は、openvino_contrib リポジトリーで見つかります。

OpExtension を使用した単一操作マッピング

このセクションでは、フレームワーク表現の単一操作が OpenVINO 表現の単一操作にマップされる場合について説明します。これは 1 対 1 マッピングと呼ばれます。次の条件をすべて満たす場合に正常に動作する OpExtension クラスがあります。

  1. フレームワーク表現での操作への入力数は、OpenVINO 表現の場合と同じです。

  2. 出力数も両方の表現は同じです。

  3. 入力にはインデックスを付けることができ、それに応じて順番にマッピングされます。フレームワーク表現のインデックス 0 を持つ入力は、OpenVINO 表現のインデックス 0 を持つ入力にマップされます。

  4. 出力も同様です。

  5. OpenVINO 操作の各属性は、元の操作の属性の 1 つから、または事前定義された定数値によって初期化できます。コピーされた属性の値に式を含めることはできません。値はそのまま受け入れられるため、値のタイプには互換性がある必要があります。

OpExtension クラスは現在、ONNX および TensorFlow フロントエンドで使用できます。PaddlePaddle フロントエンドには操作の名前付き入力と出力がある (インデックス付けされていない) ため、この場合には OpExtension マッピングは適用されません。

次の例では、Identity のタイプを使用した ONNX 操作を OpenVINO テンプレート拡張 Identity クラスにマップします。

#include <openvino/frontend/extension.hpp>
auto extension1 = ov::frontend::OpExtension<TemplateExtension::Identity>("Identity");

// or even simpler if original FW type and OV type of operations match, that is "Identity"
auto extension2 = ov::frontend::OpExtension<TemplateExtension::Identity>();

Identity 操作には属性がないため、マッピングに属性が含まれません。

構築された extension などの拡張オブジェクトは、カスタム操作を含むモデルを読み込む直前に OpenVINO ランタイムに追加するのに使用できます。

ov::Core core;
// Add arbitrary number of extensions before calling read_model method
core.add_extension(ov::frontend::OpExtension<TemplateExtension::Identity>());
core.read_model("/path/to/model.onnx");

または、拡張機能を個別にコンパイルされた共有ライブラリーで構築することもできます。別途コンパイルされたライブラリーは、モデル・オプティマイザーまたは benchmark_app で使用できます。このようなライブラリーを構築してロードする方法は、OpenVINO 拡張性の “拡張機能を備えたライブラリーを作成” の項を参照してください。

操作に複数の入力および/または出力がある場合、それらは順番にマップされます。入出力テンソルの要素のタイプは、周囲の操作で期待されるタイプと一致する必要があります。例えば、カスタム操作が f32 データタイプを生成する場合、この出力を使用する操作も f32 をサポートする必要があります。それ以外は、自動タイプ変換は実行されないため、モデル変換はエラーで失敗します。

標準 OpenVINO 操作への変換

OpExtension クラスは、標準の OpenVINO 操作セットから操作の 1 つにマッピングする必要があり、TemplateExtension::Identity のようなクラスが実装されていない場合に使用できます。

以下はカスタム・フレームワーク操作 ‘MyRelu’ の例です。OpenVINO 操作セットに存在する標準 Relu と数学的に同等ですが、‘MyRelu’ としています。この場合、‘MyRelu’ -> Relu マッピングを使用する必要があります。

from openvino.frontend import OpExtension
core.add_extension(OpExtension("Relu", "MyRelu"))
core.add_extension(ov::frontend::OpExtension<>("Relu", "MyRelu"));

変換された OpenVINO モデルでは、“MyRelu” 操作は、利用可能な最新の OpenVINO 操作セットの標準操作 Relu に置き換えられます。標準操作を使用する場合、OpExtension のテンプレート・パラメーターとして ov::opset8::Relu クラス名を使用する代わりに、文字列タイプ (“Relu”) のみを使用して標準操作を指定できることに注意してください。このメソッドは、標準操作セットからの操作でのみ使用できます。ユーザーのカスタム OpenVINO 操作の場合、TemplateExtension::Identity で示したように、対応するクラスを常にテンプレート・パラメーターとして指定する必要があります。

属性マッピング

上で説明したように、OpExtension は、属性を 1 つずつマップしたり、定数によって初期化する場合に便利です。OpenVINO 操作の属性は名前によって識別されるため、名前付き属性を持つフレームワーク (TensorFlow、PaddlePaddle、ONNX など) では、名前から名前へのマッピングを指定できます。OpenVINO 操作の属性をフレームワーク操作入力の 1 つにマッピングできるフレームワーク (PyTorch など) では、入力インデックス・マッピングに名前があります。

名前付き属性マッピング

フレームワーク表現と OpenVINO 表現の属性セットが名前とタイプで完全に一致する場合、OpExtension コンストラクター・パラメーターで属性マッピングを指定する必要はありません。属性は、OpenVINO 操作に対して定義する visit_attributes メソッドに基づいて自動的に検出され、マッピングされます。

attr1attr2 という名前を持つ 2 つの属性を持つ CustomOperation クラス実装があると考えてください。

class CustomOperation : public ov::op::Op {

    std::string attr1;
    int attr2;

public:

    OPENVINO_OP("CustomOperation");

    bool visit_attributes(ov::AttributeVisitor& visitor) override {
        visitor.on_attribute("attr1", attr1);
        visitor.on_attribute("attr2", attr2);
        return true;
    }

    // ... implement other required methods

また、フレームワーク表現の元のモデルには、同じ attr1attr2 属性を持つ CustomOperation という名前の操作もあります。次のコードを使用します。

core.add_extension(ov::frontend::OpExtension<CustomOperation>());

attr1attr2 は両方とも、フレームワーク表現から OpenVINO 表現に自動的にコピーされます。

何らかの理由で属性の名前が異なっていても、値を “そのまま” コピーできる場合は、OpExtension コンストラクターで属性名のマッピングを渡すことができます。

core.add_extension(ov::frontend::OpExtension<CustomOperation>(
    std::map<std::string, std::string>{ {"attr1", "fw_attr1"}, {"attr2", "fw_attr2"} },
    {}
));

ここで、fw_attr1fw_attr2 は、フレームワーク操作表現の対応する属性の名前です。

属性のコピーが必要ない場合、OpExtension は属性を事前定義された定数値に設定することもできます。同じ CustomOperation で、fw_attr2 からコピーするのではなく、attr2 を値 5 に設定するとします。これには次の操作を実行します。

core.add_extension(ov::frontend::OpExtension<CustomOperation>(
    std::map<std::string, std::string>{ {"attr1", "fw_attr1"} },
    { {"attr2", 5} }
));

したがって、ターゲット OpenVINO 操作の各属性は次のいずれかによって初期化される必要があります。

  1. 名前一致により自動設定

  2. 属性名によってマップされる

  3. 定数値に設定する

これは、OpExtension コンストラクターの引数としてマップを指定することで実現されます。

名前付き入力と出力を使用した属性マッピング

前の例のマッピングでは、フレームワーク・モデル表現における操作の入力と出力が特定の順序であることを前提としているため、フレームワーク操作入力 0 を OpenVINO 操作入力 0 などに直接マッピングできます。常にそうであるとは限りません。PaddlePaddle などのフレームワークでは、操作の入力と出力は名前によって識別され、任意の順序で定義できます。したがって、これを OpenVINO 操作の入力と出力にマップするには、その順序を自身で指定する必要があります。これは、入力用と出力用の 2 つの文字列ベクトルを作成することで行えます。位置 i のフレームワーク操作入力名は、位置 i の OpenVINO 操作入力 (出力についても同様) にマップされます。

次の例を見てみましょう。前と同様に、元のモデルの CustomOperation を OpenVINO CustomOperation にそのままマップします (名前と属性名が一致するように)。今回は、フレームワーク操作の入力と出力は厳密に順序付けされておらず、入力は ABC、出力は XY という名前で識別できます。これらの入力と出力は、入力 ABC が OpenVINO CustomOperation の 1 番目、2 番目、および 3 番目の入力にマップされ、X および Y 出力がそれぞれ OpenVINO CustomOperation の 1 番目と 2 番目の出力にマップされるようにできます。

そのため、このようなカスタム操作は次のように登録できます。

core.add_extension(ov::frontend::OpExtension<CustomOperation>({"A", "B", "C"}, {"X", "Y"}));

2 番目の例は、属性の名前が異なる場合に、名前付きの入力と出力を使用して操作をマップする方法を示しています。

core.add_extension(ov::frontend::OpExtension<CustomOperation>(
    {"A", "B", "C"},
    {"X", "Y"},
    std::map<std::string, std::string>{ {"attr1", "fw_attr1"}, {"attr2", "fw_attr2"} },
    {}
));

そして最後は、名前付き入力と出力を使用して操作をマップする方法を示していますが、(フレームワーク操作を OpenVINO 操作に正しくマップするために) 属性の 1 つを事前定義された値に設定する必要がある場合は次のようになります。

core.add_extension(ov::frontend::OpExtension<CustomOperation>(
    {"A", "B", "C"},
    {"X", "Y"},
    std::map<std::string, std::string>{ {"attr1", "fw_attr1"} },
    { {"attr2", 5} }
));

操作入力からの属性のマッピング

操作が入力リストに属性を持つモデル (PyTorch モデルなど) の場合、入力インデックス・マッピングへの名前を指定できます。例えば、alphabeta の 2 つの属性を持つ ELU アクティベーション関数のバリアントを実装するカスタム OpenVINO オペレーションを作成したとします。

\[CustomElu=\left\lbrace \begin{array}{ll} beta * x & \textrm{if x > 0} \newline alpha * (exp(x) - 1) & \textrm{otherwise} \end{array} \right.\]

以下は、属性を定義する方法を示す CustomElu クラスの一部です。

class CustomElu : public ov::op::Op {
private:
    float m_alpha;
    float m_beta;

public:
    OPENVINO_OP("CustomElu");

    CustomElu() = default;

    CustomElu(const ov::Output<ov::Node>& input, float alpha, float beta) : Op({input}), m_alpha(alpha), m_beta(beta) {
        constructor_validate_and_infer_types();
    }

    void validate_and_infer_types() override {
        set_output_size(1);
        set_output_type(0, get_input_element_type(0), get_input_partial_shape(0));
    }

    bool visit_attributes(ov::AttributeVisitor& visitor) override {
        visitor.on_attribute("alpha", m_alpha);
        visitor.on_attribute("beta", m_beta);
        return true;
    }

    std::shared_ptr<ov::Node> clone_with_new_inputs(const ov::OutputVector& inputs) const override {
        return std::make_shared<CustomElu>(inputs[0], m_alpha, m_beta);
    }
};

CustomElu を PyTorch aten::elu にマッピングする方法の例を見てみましょう (beta1 に等しい場合、CustomEluaten::elu と同じように機能することに注意してください)。aten::elu には入力リストの 2 番目に alpha 属性がありますが、beta はありません。したがって、それを CustomElu にマップするには、次を使用できます。

auto extension = std::make_shared<ov::frontend::OpExtension<CustomElu>>("aten::elu",
                                                                        std::map<std::string, size_t>{{"alpha", 1}},
                                                                        std::map<std::string, ov::Any>{{"beta", 1.0f}});

これにより、alpha が 2 番目の入力にマップされ、beta 属性が定数値 1.0f にマップされます。

このように作成された拡張機能は使用できます。動的ライブラリーの場合は、拡張機能を備えたライブラリーを作成を参照してください。

OPENVINO_FRAMEWORK_MAP マクロを使用したカスタム操作のフロントエンドへのマッピング

OPENVINO_FRAMEWORK_MAP は、OpenVINO 操作のクラス定義内で使用するマクロで、この操作とフロントエンド操作の間のマッピングを指定できます。

次の例について考えてみます。CustomOp 操作 (mode 属性有り) を持つ ONNX モデル、CustomOpV3 操作 (axis 属性有り) を持つ TensorFlow モデル、そして “X” という名前の入力を持つ CustomOp (mode 属性有り) を持つ PaddlePaddle モデルがあると想定します。出力は “Out” という名前で、それらはすべて次のような単一の OpenVINO 操作 CustomOp で実装できます。

#include <openvino/frontend/extension/op.hpp>
#include <openvino/frontend/onnx/extension/op.hpp>
#include <openvino/frontend/tensorflow/extension/op.hpp>
#include <openvino/frontend/paddle/extension/op.hpp>
class CustomOp : public ov::op::Op {
    std::string m_mode;
    int m_axis;

public:
    OPENVINO_OP("CustomOp");
    OPENVINO_FRAMEWORK_MAP(onnx, "CustomOp", { {"mode", "mode"} }, { {"axis", -1} });
    OPENVINO_FRAMEWORK_MAP(tensorflow, "CustomOpV3", { {"axis", "axis"} }, { {"mode", "linear"} });
    OPENVINO_FRAMEWORK_MAP(paddle, {"X"}, {"Out"}, "CustomOp", { {"mode", "mode"} }, { {"axis", -1} });

    bool visit_attributes(ov::AttributeVisitor& visitor) override {
        visitor.on_attribute("mode", m_mode);
        visitor.on_attribute("axis", m_axis);
        return true;
    }

    // ... implement other required methods

このマクロが受け取るパラメーターを詳しく見てみます (2 つの種類があることに注意してください。2 つ目は、入力名と出力名を指定する必要がある PaddlePaddle 操作にマップするものです)。

OPENVINO_FRAMEWORK_MAP(framework, name, attributes_map, attributes_values)
OPENVINO_FRAMEWORK_MAP(framework, input_names, output_names, name, attributes_map, attributes_values)
  • framework - フレームワーク名。

  • name - フレームワークの操作名。OpenVINO カスタム操作名 (OPENVINO_OP マクロの最初のパラメーターとして渡される名前) がフレームワーク操作名と同じで、attributes_mapattributes_values の両方が指定されていない場合、これはオプションです。

  • input_names - 入力の名前を指定する文字列のベクトル (PaddlePaddle を OpenVINO 操作にマップするために必要)。

  • output_names - 出力の名前を指定する文字列のベクトル (PaddlePaddle を OpenVINO 操作にマップするために必要)。

  • attributes_map - OpenVINO 操作属性とフレームワーク操作属性の間のマッピングを提供するために使用されます。キーと値のペアが含まれます。キーは OpenVINO 操作属性名、値は対応するフレームワーク操作属性名です。OpenVINO 操作属性の数とその名前がフレームワーク操作属性と 1 対 1 で一致する場合、このパラメーターはオプションです。

  • attributes_values - attributes_map で指定されていない OpenVINO 操作属性のデフォルト値を提供するために使用されます。キーと値のペアが含まれ、キーは OpenVINO 操作属性名、値はこの属性値です。このパラメーターは、attributes_map にすべての OpenVINO 操作属性が含まれている場合、または attributes_map が提供されていない場合には指定できません。

上の例では、OPENVINO_FRAMEWORK_MAP が 3 回使用されています。まず、OpenVINO CustomOp は ONNX CustomOp 操作にマップされ、m_mode 属性は mode 属性にマップされ、m_axis 属性はデフォルト値 -1 を取得します。次に、OpenVINO CustomOp は TensorFlow CustomOpV3 操作にマッピングされ、m_axis 属性は axis 属性にマッピングされ、m_mode 属性はデフォルト値の線形を取得します。最後に、OpenVINO CustomOp は PaddlePaddle CustomOp 操作にマップされ、m_mode 属性は mode 属性にマップされ、m_axis 属性はデフォルト値 -1 を取得します。このマッピングでは、入力名 “X” と出力名 “Out” も指定します。

最後のステップは、次のようにカスタム操作を登録することです。

ov::Core core;
core.add_extension(ov::frontend::OpExtension<CustomOp>());

重要

特定のフレームワークで操作をマップするには、CMakeLists.txt ファイルでそれぞれのフロントエンド (openvino::frontend::onnxopenvino::frontend::tensorflowopenvino::frontend::paddle) にリンクする必要があります。

target_link_libraries(${TARGET_NAME} PRIVATE openvino::frontend::onnx)

ConversionExtension を使用した複数の操作へのマッピング

前のセクションでは、名前と属性値をオプションで調整して、単一のオペレーションを単一のオペレーションにマップする場合について説明しました。既存の C++ カーネル実装を使用した独自のカスタム操作にはこれで十分です。この場合、操作のフレームワーク表現と OpenVINO 表現は制御下にあり、入力/出力/属性を調整して OpExtension を使用できるようになります。

1 対 1 のマッピングが不可能な場合、複数の操作への分解を考慮する必要があります。これは、冗長であまり自動化されていない ConversionExtension クラスを使用することで実現できます。これにより、任意のコードを記述して、単一のフレームワーク操作を、任意の複雑性を持つ依存関係グラフを構築する OpenVINO 操作に置き換えることができます。

ConversionExtension は、OpenVINO 操作クラスからグラフを構築する関数に単一の操作をマップします。OpenVINO ランタイムでモデルをビルドに従って、OpenVINO 操作クラスを使用して置換用のモデルを構築する方法を学習します。

以下は、次の式に従って、ConversionExtension を使用して ONNX から “ThresholdedRelu” を変換する方法を示しています: ThresholdedRelu(x, alpha) -> Multiply(x, Convert(Greater(x, alpha), type=float))

ThresholdedRelu は、標準の ONNX オペレータの 1 つであり、ONNX フロントエンドによってすぐにネイティブサポートされます。ここでは、ThresholdedRelu の代わりにカスタム操作に同様のサポートを追加する方法を説明するために、これを再実装しています。

import openvino.runtime.opset12 as ops
from openvino.frontend import ConversionExtension
#include <openvino/opsets/opset11.hpp>
def conversion(node):
    input_node = node.get_input(0)
    input_type = input_node.get_element_type()
    greater = ops.greater(input_node, ops.constant([node.get_attribute("alpha")], input_type))
    casted = ops.convert(greater, input_type.get_type_name())
    return ops.multiply(input_node, casted).outputs()

core.add_extension(ConversionExtension("ThresholdedRelu", conversion))
core.add_extension(ov::frontend::ConversionExtension(
    "ThresholdedRelu",
    [](const ov::frontend::NodeContext& node) {
        auto greater = std::make_shared<ov::opset11::Greater>(
            node.get_input(0),
            ov::opset11::Constant::create(ov::element::f32, {}, {node.get_attribute<float>("alpha")}));
        auto casted = std::make_shared<ov::opset11::Convert>(greater, ov::element::f32);
        return ov::OutputVector{ std::make_shared<ov::opset11::Multiply>(node.get_input(0), casted) };
    }));

次の例は、ConversionExtension を使用して PyTorch aten::hardtanh を変換する方法を示し、get_values_from_const_input 関数を使用して入力から属性値をフェッチする方法を示します。

import torch
from openvino.frontend import ConversionExtension, NodeContext
from openvino.tools.mo import convert_model


class HardTanh(torch.nn.Module):
    def __init__(self, min_val, max_val):
        super(HardTanh, self).__init__()
        self.min_val = min_val
        self.max_val = max_val

    def forward(self, inp):
        return torch.nn.functional.hardtanh(inp, self.min_val, self.max_val)


def convert_hardtanh(node: NodeContext):
    inp = node.get_input(0)
    min_value = node.get_values_from_const_input(1)
    max_value = node.get_values_from_const_input(2)
    return ops.clamp(inp, min_value, max_value).outputs()


model = HardTanh(min_val=0.1, max_val=2.0)
hardtanh_ext = ConversionExtension("aten::hardtanh", convert_hardtanh)
ov_model = convert_model(input_model=model, extensions=[hardtanh_ext])

元のフレームワーク操作属性値にアクセスし、入力に接続するには、NodeContext タイプの node オブジェクトが使用されます。3 つのメインメソッドがあります。

  • NodeContext::get_input は、指定されたインデックスを持つ入力を取得します。

  • NodeContext::get_attribute は、指定された名前の属性値を取得します。

  • NodeContext::get_values_from_const_input は、指定された入力インデックスを持つ属性を取得します。

変換関数は、元のフレームワーク操作の対応する出力に同じ順序でマップされるノード出力のベクトルを返す必要があります。

一部のフレームワークでは、変換中に操作の出力名を指定する必要があります。PaddlePaddle 操作の場合、通常、NamedOutputs コンテナを使用してすべての出力に名前を指定します。通常、これらの名前は、PaddlePaddle コードの個々の操作のソースコードにあります。次の例は、top_k_v2 操作のこのような変換を示しています。

core.add_extension(ov::frontend::ConversionExtension("top_k_v2", [](const ov::frontend::NodeContext& node) {
    auto x = node.get_input("X");
    const auto k_expected = node.get_attribute<int>("k", 1);
    auto k_expected_node = ov::opset11::Constant::create(ov::element::i32, {}, {k_expected});

    auto axis = node.get_attribute<int32_t>("axis", -1);
    bool sorted = node.get_attribute<bool>("sorted", true);
    bool largest = node.get_attribute<bool>("largest", true);

    std::string sort_type = sorted ? "value" : "none";
    std::string mode = largest ? "max" : "min";

    auto node_topk = std::make_shared<ov::opset11::TopK>(x, k_expected_node, axis, mode, sort_type);

    ov::frontend::paddle::NamedOutputs named_outputs;
    named_outputs["Out"] = ov::OutputVector{node_topk->output(0)};
    named_outputs["Indices"] = ov::OutputVector{node_topk->output(1)};

    return named_outputs;
}));

TensorFlow フレームワークの場合、操作に複数の出力がある場合、インデックス付き出力アクセスと名前付き出力アクセスの両方を許可する NamedOutputVector 構造体を使用して、出力に名前を割り当てることを推奨します。出力の名前を含む TensorFlow 操作の説明については、tf.raw_ops を参照してください。次の例は、TopKV2 操作の変換を示しています。

core.add_extension(ov::frontend::ConversionExtension("TopKV2", [](const ov::frontend::NodeContext& node) {
    auto input = node.get_input(0);
    auto k_input = node.get_input(1);
    bool sorted = node.get_attribute<bool>("sorted", true);    
    auto mode = ov::opset11::TopK::Mode::MAX;
    auto sort_type = sorted ? ov::opset11::TopK::SortType::SORT_VALUES : ov::opset11::TopK::SortType::SORT_INDICES;
    auto top_k = std::make_shared<ov::opset11::TopK>(input, k_input, -1, mode, sort_type, ov::element::i32, true);
    return ov::frontend::NamedOutputVector{{"values", top_k->output(0)}, {"indices", top_k->output(1)}};
}));