ステートフル・モデルとステート API

“ステートフル・モデル” は、2 つの連続する推論呼び出しの間でデータを暗黙的に保存するモデルです。1 回の実行で保存されるテンソルは、“state” または “variable” と呼ばれる内部メモリーバッファーに保持され、モデルの出力となることはなく、次の実行に渡される可能性があります。対照的に、実行間でデータを受け渡す “ステートレス” モデルでは、生成されたデータはすべて出力され、次回の実行時に再利用するためアプリケーションで処理する必要があります。

example comparison between stateless and stateful model implementations

さらに、モデルに TensorIterator または Loop オペレーションが含まれる場合、それをステートフルにすると、(LowLatency 変換により) 各実行反復から中間値を取得できます。それ以外は、データが使用可能になる前に、実行セット全体が終了する必要があります。

テキスト生成はステートフル・モデルに適した例です。完全な文を出力するには複数の推論呼び出しが必要であり、実行ごとに 1 つの出力トークンが生成されます。実行からの情報はコンテキストとして次の推論に渡され、ステートフル・モデルによってネイティブに処理できます。この条件および他のシナリオでの潜在的な利点は次のとおりです。

  1. モデル実行の高速化 - ステートデータは OpenVINO プラグイン向けに最適化された形式で保存され、モデルをさらに効率良く実行するのに役立ちます。重要なのは、ステートからのデータを頻繁に要求すると、パフォーマンスの向上が鈍化したり、損失につながる可能性があることです。ステートメカニズムは、ステートデータに頻繁にアクセスされない場合にのみ利用してください。

  2. ユーザーコードの簡素化 - 最初の推論呼び出しで初期化値を指定したり、モデル出力から入力にデータをコピーしたりするなどのシナリオで、コードをステートに置き換えることができます。ステートを使用すると、OpenVINO は内部で管理を行い、データ表現の変換による追加のオーバーヘッドをさらに排除できます。

  3. データ処理 - 一部のユースケースでは、データシーケンスの処理が必要です。シーケンスの長さが判明しており、十分に短い場合は、内部にサイクルを含む RNN などのモデルを使用して処理できます。オンラインの音声認識や時間ごとの予測など、長さが不明な場合は、データを小さく分割して段階的に処理できますが、これには分割されたデータ間の依存関係に対処する必要があります。ステートはこの目的を十分に果たします。モデルは推論の実行間に一部のデータを保存し、1 つの依存シーケンスが終了すると、ステートを初期値にリセットして新しいシーケンスを開始できます。

OpenVINO ステートフル・モデル表現

モデルをステートフルにするため、OpenVINO はパラメーター結果のループされたペアを独自の 2 つの操作に置き換えます。

  • ReadValue (仕様を参照) はステートからデータを読み取り、それを出力として返します。

  • Assign (仕様を参照) はデータを入力として受け入れ、次の推論呼び出しに向けてステートに保存します。

これらの操作の各ペアはステートを処理します。ステートは推論の間に自動的に保存され、必要に応じてリセットできます。このように、データをコピーする負担がアプリケーションから OpenVINO に移行され、関連するすべての内部ワークがユーザーから隠蔽されます。

OpenVINO モデルをステートフル・モデルに変換するには、3 つの方法があります。

  • Optimum-Intel - 最もユーザー・フレンドリーなオプションです。必要な最適化はすべて自動的に認識され、適用されます。欠点は、このツールがすべてのモデルで動作するとは限らないことです。

  • MakeStateful 変換- ペアの操作が同じ形状と要素タイプである限り、ユーザーはパラメーターと結果のいずれのペアを置き換えるか選択できます。

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

ステートフル・モデルの推論実行

最も基本的なアプリケーションでは、ステートフル・モデルはそのまま使用できます。追加の制御のため OpenVINO は専用 API を提供しており、そのメソッドを使用すると、推論実行間のステートで保存されたデータの取得と変更の両方を行うことができます。OpenVINO ランタイムは、ov::InferRequest::query_state を使用してモデルからステートのリストを取得し、ov::VariableState クラスを使用してステートを操作します。

`ov::InferRequest` メソッド:
std::vector<VariableState> query_state(); - 指定された推論要求で利用可能なすべてのステートを取得します
void reset_state() - すべてのステートをデフォルト値にリセットします

`ov::VariableState` メソッド:
std::string get_name() const - 対応するステート (Variable) の名前 (variable_id) を返します
void reset() - ステートをデフォルト値にリセットします
void set_state(const Tensor& state) - ステートの新しい値を設定します
Tensor get_state() const - ステートの現在の値を返します
複数のスレッドを使用
複数の独立したシーケンスが関係する場合、独自の推論要求の各セクションを処理するため複数のスレッドが使用される可能性があることに注意してください。ただし、ステートが自動的に渡されないため、1 つのシーケンスに対して複数の推論要求を使用することは推奨されません。代わりに、前回とは異なる推論要求で実行される場合、ov::VariableState::set_state メソッドを使用してステートを “手動” で設定する必要があります。
diagram of how initial state value is set or reset
ステートのリセット
ステートの初期値を設定またはリセットする必要がある場合は常に、ReadValue 操作の初期化サブグラフと特別な reset メソッドが提供されます。
ここで考えるべきことは、リセットすることを決定した場合、ステートを照会してからステートデータを取得することです。
値が未定義になるため、回避する必要があります。

ステートフル・モデルのアプリケーション例

以下は、3 つの独立したデータシーケンスの推論を示すコードの例です。1 つの推論要求で 1 つのスレッドが使用されます。ステートは連続するシーケンスごとにリセットする必要があります。

        // 1. Load inference engine
        std::cout << "Loading Inference Engine" << std::endl;
        ov::Core ie;

        // 2. Read a model
        std::cout << "Loading network files" << std::endl;
        std::shared_ptr<Model> network;
        network = ie.read_model("path_to_ir_xml_from_the_previous_section");
        network->get_parameters()[0]->set_layout("NC");
        set_batch(network, 1);

        // 3. Load network to CPU
        CompiledModel hw_specific_model = ie.compile_model(network, "CPU");

        // 4. Create Infer Request
        InferRequest inferRequest = hw_specific_model.create_infer_request();

        // 5. Reset memory states before starting
        auto states = inferRequest.query_state();
        if (states.size() != 1) {
            std::string err_message = "Invalid queried state number. Expected 1, but got "
                                      + std::to_string(states.size());
            throw std::runtime_error(err_message);
        }
        inferRequest.reset_state();

        // 6. Inference
        std::vector<float> input_data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

        // This example demonstrates how to work with OpenVINO State API.
        // Input_data: some array with 12 float numbers

        // Part1: read the first four elements of the input_data array sequentially.
        // Expected output for the first utterance:
        // sum of the previously processed elements [ 1, 3, 6, 10]

        // Part2: reset state value (set to 0) and read the next four elements.
        // Expected output for the second utterance:
        // sum of the previously processed elements [ 5, 11, 18, 26]

        // Part3: set state value to 5 and read the next four elements.
        // Expected output for the third utterance:
        // sum of the previously processed elements + 5 [ 14, 24, 35, 47]
        auto& target_state = states[0];

        // Part 1
        std::cout << "Infer the first utterance" << std::endl;
        for (size_t next_input = 0; next_input < input_data.size()/3; next_input++) {
            auto in_tensor = inferRequest.get_input_tensor(0);
            std::memcpy(in_tensor.data(), &input_data[next_input], sizeof(float));

            inferRequest.infer();
            auto state_buf = target_state.get_state().data<float>();
            std::cout << state_buf[0] << "\n";
        }

        // Part 2
        std::cout<<"\nReset state between utterances...\n";
        target_state.reset();

        std::cout << "Infer the second utterance" << std::endl;
        for (size_t next_input = input_data.size()/3; next_input < (input_data.size()/3 * 2); next_input++) {
            auto in_tensor = inferRequest.get_input_tensor(0);
            std::memcpy(in_tensor.data(), &input_data[next_input], sizeof(float));

            inferRequest.infer();
            auto state_buf = target_state.get_state().data<float>();
            std::cout << state_buf[0] << "\n";
        }

        // Part 3
        std::cout<<"\nSet state value between utterances to 5...\n";
        std::vector<float> v = {5};
        Tensor tensor(element::f32, Shape{1, 1});
        std::memcpy(tensor.data(), &v[0], sizeof(float));
        target_state.set_state(tensor);

        std::cout << "Infer the third utterance" << std::endl;
        for (size_t next_input = (input_data.size()/3 * 2); next_input < input_data.size(); next_input++) {
            auto in_tensor = inferRequest.get_input_tensor(0);
            std::memcpy(in_tensor.data(), &input_data[next_input], sizeof(float));

            inferRequest.infer();

            auto state_buf = target_state.get_state().data<float>();
            std::cout << state_buf[0] << "\n";
        }

    }
    catch (const std::exception &error) {
        std::cerr << error.what() << std::endl;
        return 1;
    }
    catch (...) {
        std::cerr << "Unknown/internal exception happened" << std::endl;
        return 1;
    }

    std::cout << "Execution successful" << std::endl;

ステートの操作方法を示す例は、他の記事で見つけることができます。