ステートフルな OpenVINO モデルの取得

元のフレームワークがステートを操作する専用 API を提供していない場合、結果として得られる OpenVINO IR モデルはデフォルトではステートフルではありません。これは、ステートも、Assign 操作と ReadValue 操作も含まれないことを意味します。このようなモデルをステートフルにすることもできます (利点を参照)。それには 3 つの方法があります。

  • Optimum-Intel - 選択したモデルに適用できるインテルによる自動化ソリューション (この記事では取り上げません。使用ガイドについては、生成 AI モデルの最適化とデプロイを参照してください)。

  • MakeStateful 変換 - パラメーターと結果のどのペアを置き換えるかを選択します。

  • LowLatency2 変換 - LSTM/RNN/GRU 操作または Loop/TensorIterator 操作の非表示入力およびセル状態入力に接続されたパラメーターと結果のペアを検出して置換します。

MakeStateful 変換

MakeStateful 変換は、ユーザー定義の Parameter と Results のペアを Assign 操作と ReadValue 操作に置き換えることでモデルの構造を変更します。

diagram of MakeStateful Transformation

厳密な構文のみがサポートされます。以下の例に示すように、変換呼び出しは二重引用符 “MakeStateful[…]” で囲み、テンソル名はスペースなしで一重引用符で囲む必要があります: ‘tensor_name_1’。

ステートの命名規則: ほとんどの場合、ステートの名前はパラメーター/結果テンソル名を連結したものです。テンソル名がない場合は、フレンドリー名が使用されます。

例:

detailed diagram of MakeStateful Transformation
    ov::Core core;
    auto ov_model = core.read_model("path_to_the_model");
    std::map<std::string, std::string> tensor_names = {{"tensor_name_1", "tensor_name_4"},
                                                  {"tensor_name_3", "tensor_name_6"}};
    ov::pass::Manager manager;
    manager.register_pass<ov::pass::MakeStateful>(tensor_names);
    manager.run_passes(ov_model);
    ov::Core core;
    auto ov_model = core.read_model("path_to_the_model");
    // Parameter_1, Result_1, Parameter_3, Result_3 are shared_ptr<Parameter/Result> in the ov_model
    std::vector<std::pair<std::shared_ptr<ov::opset8::Parameter>, std::shared_ptr<ov::opset8::Result>>> pairs
            = {/*Parameter_1, Result_1, Parameter_3, Result_3*/};
    ov::pass::Manager manager;
    manager.register_pass<ov::pass::MakeStateful>(pairs);
    manager.run_passes(ov_model);
--input_model <INPUT_MODEL> --transform "MakeStateful[param_res_names={'tensor_name_1':'tensor_name_4','tensor_name_3':'tensor_name_6'}]"

LowLatency2 変換

LowLatency2 変換は、次の例に示すように、Parameter と Results のペアを自動的に検出し、Assign 操作と ReadValue 操作で置き換えることにより、TensorIteratorLoop を含むモデルの構造を変更します。

diagram of LowLatency Transformation

変換を適用した後、上の図に示すように、ReadValue 操作は他の操作を入力として受け取ることができます。これらの入力は、ReadValue 操作の初期化で使用する初期値を設定する必要があります。ただし、このような初期化は、現在のステート API 実装ではサポートされていません。ユーザーがステート API を介して指定しない限り、入力値は無視され、ReadValue 操作の初期値は 0 に設定されます。

LowLatency2 変換の適用

  1. ov::Model を取得します。
    例:

        ov::Core core;
        auto ov_model = core.read_model("path_to_the_model");
    
  2. Reshape 機能を使用して、モデル内の TensorIterator/Loop ノード内の反復数を変更します。

    例えば、モデル入力の sequence_lengths 次元が 1 を超える場合、TensorIterator レイヤー層の number_of_iterations も 1 を超えあることを意味します。モデルの入力を再形成して、sequence_dimension を正確に 1 に設定できます。

        ov_model->reshape({{"X", ov::Shape({1, 1, 16})}});
    

    アンロール: LowLatency2 変換が、内部に反復が 1 つだけある TensorIterator/Loop ノードを含むモデルに適用される場合、これらのノードはアンロールされます。それ以外は、ノードはそのまま残ります。詳細については上の図を参照してください。

  3. LowLatency2 変換を適用します。

        ov::pass::Manager manager;
        manager.register_pass<ov::pass::LowLatency2>();
        manager.run_passes(ov_model);
    

    (オプション) Const 初期化子引数を使用します。

    デフォルトでは、LowLatency2 変換は、前の入力ノードと同じ形状の定数サブグラフを挿入します。ReadValue ノードの初期値はゼロに設定されます。詳細については、下の図を参照してください。use_const_initializer 引数を false に設定することで、このサブグラフの挿入を無効にできます。

        manager.register_pass<ov::pass::LowLatency2>(false);
    
    diagram of constant subgraph initialization

    ステートの命名規則: 元の TensorIterator 操作、本体のパラメーター、および追加のサフィックスである "variable_" + id (ゼロベースのインデックス付け、各 TensorIterator の新しいインデックス付け) のいくつかの名前を連結したものです。これらのルールを使用して、変換を適用した後に挿入された状態の名前を予測できます。
    例:

        // Precondition for ov::Model.
        // TensorIterator and Parameter are created in body of TensorIterator with names
        std::string tensor_iterator_name = "TI_name";
        std::string body_parameter_name = "body_parameter_name";
        std::string idx = "0"; // this is a first variable in the network
    
        // The State will be named "TI_name/param_name/variable_0"
        auto state_name = tensor_iterator_name + "//" + body_parameter_name + "//" + "variable_" + idx;
    
        //! [ov:get_ov_model]
        ov::Core core;
        auto ov_model = core.read_model("path_to_the_model");
        //! [ov:get_ov_model]
        // reshape input if needed
    
        //! [ov:reshape_ov_model]
        ov_model->reshape({{"X", ov::Shape({1, 1, 16})}});
        //! [ov:reshape_ov_model]
    
        //! [ov:apply_low_latency_2]
        ov::pass::Manager manager;
        manager.register_pass<ov::pass::LowLatency2>();
        manager.run_passes(ov_model);
        //! [ov:apply_low_latency_2]
    
        auto hd_specific_model = core.compile_model(ov_model);
        // Try to find the Variable by name
        auto infer_request = hd_specific_model.create_infer_request();
        auto states = infer_request.query_state();
        for (auto& state : states) {
            auto name = state.get_name();
            if (name == state_name) {
                // some actions
            }
        }
    
  4. API を使用。OpenVINO ステート APIステートフル・モデル推論を参照してください。

    diagram showing low latency limitation

    TensorIterator/Loop レイヤーの反復数を変更する唯一の方法は、Reshape 機能を使用することです。ただし、一部のモデルは再形成できない場合があります。これは、通常、形状の値がモデル内のどこかの定数にハード・コーディングされているためです。

    このような場合は、モデル・オプティマイザーのコマンドライン引数 --input および --output を使用して、再形成不可能なレイヤーをトリムします。

    例えば、上の図にあるパラメーターと問題のある定数は、--input Reshape_layer_name コマンドライン・オプションを使用してトリミングできます。次の例に示すように、問題のある定数は OpenVINO を使用して置き換えることもできます。

        // OpenVINO example. How to replace a Constant with hardcoded values of shapes in the network with another one with the new values.
        // Assume we know which Constant (const_with_hardcoded_shape) prevents the reshape from being applied.
        // Then we can find this Constant by name on the network and replace it with a new one with the correct shape.
        ov::Core core;
        auto model = core.read_model("path_to_model");
        // Creating the new Constant with a correct shape.
        // For the example shown in the picture above, the new values of the Constant should be 1, 1, 10 instead of 1, 49, 10
        auto new_const = std::make_shared<ov::opset8::Constant>( /*type, shape, value_with_correct_shape*/ );
        for (const auto& node : model->get_ops()) {
            // Trying to find the problematic Constant by name.
            if (node->get_friendly_name() == "name_of_non_reshapable_const") {
                auto const_with_hardcoded_shape = std::dynamic_pointer_cast<ov::opset8::Constant>(node);
                // Replacing the problematic Constant with a new one. Do this for all the problematic Constants in the network, then
                // you can apply the reshape feature.
                ov::replace_node(const_with_hardcoded_shape, new_const);
            }
        }
    

モデル・オプティマイザーを使用した TensorIterator/ループ操作の取得

ONNX および ONNX 形式でサポートされるフレームワーク: LSTM、RNN、GRU の元のレイヤーは GRU/RNN/LSTM シーケンス操作に変換されます。ONNX ループレイヤーは OpenVINO ループ操作に変換されます。

TensorFlow: BlockLSTM は TensorIterator 操作に変換されます。TensorIterator 本体には LSTM Cell 操作が含まれます。Peepholes や InputForget などの変更はサポートされていません。While レイヤーは TensorIterator に変換されます。TensorIterator 本体には、サポートされる操作を含めることができます。ただし、形状推論 (モデル・オプティマイザー変換) 中に反復回数を計算できない動的ケースはサポートされていません。

TensorFlow 2: While レイヤーはループ操作に変換されます。ループ本体には、サポートされている操作を含めることができます。

OpenVINO API を介したモデルの作成

ステートフルな OpenVINO IR モデルを取得する主なアプローチは、他のフレームワークから変換することです。ただし、モデルを最初から作成することは可能です。その方法については、OpenVINO モデルのビルドを確認してください。

ここでは、ov::SinkVector を使用して ov::Model を作成する例も示します。入力と出力を除くステートを持つモデルは、グラフ変換中に削除されないように、Assign ノードも Model を指す必要があります。これは、例のようにコンストラクターを使用することも、add_sinks(const SinkVector& sinks) メソッドを使用することもできます。また、delete_sink() メソッドを使用してグラフからノードを削除した後、ov::Model からシンクを削除することもできます。

    // ...

    auto input = std::make_shared<ov::opset8::Parameter>(ov::element::f32, ov::Shape{1, 1});
    auto init_const = ov::opset8::Constant::create(ov::element::f32, ov::Shape{1, 1}, {0});

    // The ReadValue/Assign operations must be used in pairs in the network.
    // For each such a pair, its own variable object must be created.
    const std::string variable_name("variable0");
    ov::op::util::VariableInfo var_info = {init_const->get_shape(),
                                           init_const->get_element_type(),
                                           variable_name};
    auto variable = std::make_shared<ov::op::util::Variable>(var_info);

    // Creating ov::Model
    auto read = std::make_shared<ov::opset8::ReadValue>(init_const, variable);
    auto add = std::make_shared<ov::opset8::Add>(input, read);
    auto save = std::make_shared<ov::opset8::Assign>(add, variable);
    auto result = std::make_shared<ov::opset8::Result>(add);

    auto model = std::make_shared<ov::Model>(ov::ResultVector({result}),
                                             ov::SinkVector({save}),
                                             ov::ParameterVector({input}));