ステートフル・モデルとステート API¶
“ステートフル・モデル” は、2 つの連続する推論呼び出しの間でデータを暗黙的に保存するモデルです。1 回の実行で保存されるテンソルは、“state” または “variable” と呼ばれる内部メモリーバッファーに保持され、モデルの出力となることはなく、次の実行に渡される可能性があります。対照的に、実行間でデータを受け渡す “ステートレス” モデルでは、生成されたデータはすべて出力され、次回の実行時に再利用するためアプリケーションで処理する必要があります。
さらに、モデルに TensorIterator または Loop オペレーションが含まれる場合、それをステートフルにすると、(LowLatency 変換により) 各実行反復から中間値を取得できます。それ以外は、データが使用可能になる前に、実行セット全体が終了する必要があります。
テキスト生成はステートフル・モデルに適した例です。完全な文を出力するには複数の推論呼び出しが必要であり、実行ごとに 1 つの出力トークンが生成されます。実行からの情報はコンテキストとして次の推論に渡され、ステートフル・モデルによってネイティブに処理できます。この条件および他のシナリオでの潜在的な利点は次のとおりです。
モデル実行の高速化 - ステートデータは OpenVINO プラグイン向けに最適化された形式で保存され、モデルをさらに効率良く実行するのに役立ちます。重要なのは、ステートからのデータを頻繁に要求すると、パフォーマンスの向上が鈍化したり、損失につながる可能性があることです。ステートメカニズムは、ステートデータに頻繁にアクセスされない場合にのみ利用してください。
ユーザーコードの簡素化 - 最初の推論呼び出しで初期化値を指定したり、モデル出力から入力にデータをコピーしたりするなどのシナリオで、コードをステートに置き換えることができます。ステートを使用すると、OpenVINO は内部で管理を行い、データ表現の変換による追加のオーバーヘッドをさらに排除できます。
-
データ処理 - 一部のユースケースでは、データシーケンスの処理が必要です。シーケンスの長さが判明しており、十分に短い場合は、内部にサイクルを含む 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
クラスを使用してステートを操作します。
std::vector<VariableState> query_state();
- 指定された推論要求で利用可能なすべてのステートを取得しますvoid reset_state()
- すべてのステートをデフォルト値にリセットしますstd::string get_name() const
- 対応するステート (Variable) の名前 (variable_id) を返しますvoid reset()
- ステートをデフォルト値にリセットしますvoid set_state(const Tensor& state)
- ステートの新しい値を設定しますTensor get_state() const
- ステートの現在の値を返しますov::VariableState::set_state
メソッドを使用してステートを “手動” で設定する必要があります。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;
ステートの操作方法を示す例は、他の記事で見つけることができます。