まず、マルチチャンネルでのビデオ解析タスクの最適化について見てみましょう。ここではレイテンシーとスループットという重要な概念について説明します。
- レイテンシー (Latency) は、単一フレームタスクの処理に費やされた合計時間です。
- スループット (Throughput) は、単位時間当たりのタスクの合計量です。
シングルチャンネルのビデオ分析タスクでは、ビデオソースの各フレームは、パイプライン (one by one) の形でビデオ解析サービスに送信され、前後の 2 つのフレームには時間依存関係がありますが、理論的には同時に 1 フレームの画像における推論タスクしか処理しないため、多くの場合レイテンシーを最適化するようにします。しかし、マルチチャンネルの場合、各チャンネル間のデータは関連性がないため、できる限り単位時間内で、より多くのチャンネルデータを処理することが求められます。その場合、スループット性能を最適化するようにします。スループットを向上させる最善の方法は、マルチタスクの並列処理を改善し、単一タスクでのマルチスレッドのプロセスで、データ同期によるオーバーヘッドを削減することです。
OpenVINO™ツールキットの推論並列最適化ソリューション
OpenVINO™ ツールキットの並列最適化ソリューションの説明で、注目すべきはいくつかの重要なコンフィギュレーション・パラメーターです。
- スレッド (Thread) : ハードウェア・システムで配分可能な最小並列実行単位で、CPU と GPU におけるこの数字は CPU のコア数と GPU の EU (Execution Unit) 数となり、それぞれ関連があります。
- ストリーム (Stream) : OpenVINO™ ツールキットのマルチスレッドにおけるスナップインの場合、OpenVINO™ ツールキットは 1 つあるいはいくつかのスレッドをストリームでカプセル化して、複数のストリームにおける推論タスクを並列に実行することで、スレッドの合計数を均等に割り当てることができます。
- 推論リクエスト (Infer Request) : 推論タスクにおけるインスタンスのホストで、各推論リクエスト は実行中、個別にストリームに割り当てられます。
OpenVINO™ ツールキットでは、ストリームは並列実行されている推論タスクキューのように見えます。ストリームは 1 つの推論リクエストによる推論を完了すると、待ち状態になっている次の推論リクエストを読み込みます。そのため、実際の開発プロセスでは常に待ち状態の推論リクエストがあるようにし、ストリームが常に稼働するように推論リクエストの数をストリームの数以上に設定する必要があります。
たとえるなら、ストリームは PC で、推論リクエストは労働者のようなものです。労働者は PC を使用することで作業できますが、PC を持たない労働者は、前の労働者が PC で仕事を終えるまで待たなければなりません。この場合も、PC が空くのを待っている労働者が、待機中に入力する項目やツールなどを十分に準備して、PC が空いたらすぐに作業を開始できるようにする必要があります。
OpenVINO™ ツールキットに付属する benchmark_app (性能ベンチマーク・ツール) を使用して、以下の 3 つのパラメータを配置することで、異なるコンフィギュレーションにおける実際の性能への影響をテストできます。
benchmark_app -m MODEL_DIR \
-nstreams NUMBER_STREAMS \
-nthreads NUMBER_THREADS \
-nireq NUMBER_INFER_REQUESTS \
推論リクエストを並列化するために、OpenVINO™ ツールキットには非同期推論インターフェイスの API セットが用意されています。非同期 API を使って推論する場合、推論スレッドは非ブロッキング・モードとなるため、複数の推論要求を同時に並列実行でき、1つの推論タスクを処理する間に、次のタスク処理を開始することができます。そのため、非同期 API は同期 API と比べて、デバイスリソースの使用率を最大化することができます。
マルチチャンネル・ビデオ解析パイプラインの最適化というテーマに戻ります。マルチタスクの並列化率を高めるために、次の 2 つの最適化戦略があります。ここで言うバッチ (Batch) の意味は、事前に複数の入力されたデータを順番にオーバーレイして、インファレンス・リクエストに送信して推論し、その結果を分割することで、データ並列化を達成することも可能ということになります。
- 小さなバッチサイズは、ストリーム数が多い場合に適しています
- 大きなバッチサイズは、ストリーム数が少ない場合に適しています
どちらのソリューションも、GPU においてそれぞれ長所と短所があります。まず、小さなバッチサイズの場合は、ストリーム数が多い場合に適しています。異なるストリーム間で依存性がないため、先に推論を開始したインファレンス・リクエストが結果データを先に得ることができ、単フレームでのデータ推論レイテンシーに敏感なシーンに適しています。この場合問題となるのは、各ストリームの OpenCL キューは、単独の CPU スレッドで維持され、ポーリングメカニズムを使用して結果データを取得する必要があるため、CPU にリソース・オーバーヘッドをもたらします。GPU ストリームの数が多くなると、CPU リソースの使用率も大きくなります。
一方、ストリーム数が少ない場合に適している大きなバッチサイズの場合、こうした状況が起きないために CPU のオーバーヘッドを最小限に抑えることができます。ただし、複数のインプットをパッケージ化する必要があるため、先にデコードされたデータは、後でデコードされたデータと共にインファレンス・リクエストに送信されてから推論され、同時に結果が返されることになるため、レイテンシーに敏感なタスクには適していません。
GPU スループットの最適化については、ストリームの数がボトルネックになると、スループットの向上に役立たないことが benchmark_app で判明しています。しかし、バッチサイズを大きくすることによるスループット性能の向上は、それ以上に効果が大きいため、このソリューションでは、大きなバッチサイズと少数のストリームを組み合わせてスループット性能の最適化を実現します。
ソリューション設計
ここでは、プレゼンテーションで使われたマルチチャネル・ビデオ解析を主に 2 つの部分に分けて、それぞれ oneVPL と OpenVINO™ ツールキットという 2 つのツール・コンポーネントに対応します。
1. ビデオ処理
ビデオ処理のパートでは、oneVPL を使用して各チャンネルデータのデコードおよび前処理タスクを完了し、その結果をデコード結果キューに順番に送信し、後の AI 処理部分が呼び出されるのを待機します。タスクの並列処理を改善するには、各チャンネルのビデオ処理タスクに単独の CPU スレッドを配置する必要があります。そのほか、oneVPL の各セッションが 1 つのデータソースしか処理できないため、各ビデオソース毎に単独のセッションを作成してデコードと VPP 処理をする必要があります。
結果データを返すとき、OpenVINO™ ツールキットの remote tensor の呼び出しができるように、キュー内のデータをチャンネル間で共有される GPU surface メモリーに保存しますが、実際には oneVPL セッションが初期化されるたびに、新しいメモリー・ハンドル・オブジェクトを生成するため、MFXVideoCORE_SetHandle を使用して、各セッションで同じハンドルを強制的に配置します
MFXVideoCORE_SetHandle 配置のハンドル
sts = MFXVideoCORE_SetHandle(session,
static_cast<mfxHandleType>(MFX_HANDLE_VA_DISPLAY),
va_dpy);
2. AI の推論
AI 推論は、本ソリューションのハイライトです。まず、推論タスクを始めるビジー・インファレンス・リクエスト (busy infer request) と、アイドル状態のインファレンス・リクエストを保持するため、個別に 2 つのキューを作成する必要があります。事前にアイドル状態のインファレンス・リクエストに oneVPL で処理された結果データを読み込み、ビジー・インファレンス・リクエストが推論を完了した後、ストリームに送信し推論します。ビジー・インファレンス・リクエストが結果データを出力したら、フリー・インファレンス・リクエストに交代して、フリー・リクエスト・キューに送信し、最初の推論待ちのデコード結果を oneVPL の結果キューから取り出します。このメカニズムにより、ストリームでインファレンス・リクエストが継続的に実行されるようにすることができます。
free request キューを作成
BlockingQueue<ov::InferRequest> free_requests;
for (int i = 0; i < FLAGS_nr; i++)
free_requests.push(compiled_model.create_infer_request());
ビジー・リクエスト・キュー内の インファレンス・リクエストから推論結果データを取り出し、フリー・リクエスト・キューに送信
for (;;) {
auto res = busy_requests.pop();
auto batched_frames = res.first;
auto infer_request = res.second;
if (!infer_request)
break;
infer_request. wait();
ov::Tensor output_tensor = infer_request.get_output_tensor(0);
PrintMultiResults(output_tensor, batched_frames, input_shape);
// When application completes the work with frame surface, it must call release to avoid memory leaks
for (auto frame : batched_frames)
{
frame.first->FrameInterface->Release(frame.first);
}
free_requests.push(infer_request);
}
また、各フリー・インファレンス・リクエストがデコード結果データを読み込む前に、複数のデコード・リクエスト・キューの上位 n 個のデータを、1 つのバッチにパッケージ化し推論するというバッチ化作業を行う必要があります。理論的にはバッチサイズが大きくなるほどスループット性能も向上しますが、GPU 上のメモリーリソースの制約により、大きすぎるバッチサイズは、スループット性能に対してボトルネックを引き起こすため、出来る限り実測値に基づいた GPU の機能に応じた適切なバッチサイズを選択する必要があります。OpenVINO™ ツールキット 2022.1 リリースでは、テストが面倒な場合に使用できる、auto-batching 機能も更新されています。これは GPU のハードウェア・リソース容量と実際のタスク負荷に基づいて、自動的に GPU に適したバッチサイズを配置し、最適な配置パラメータを提供します。
auto-batching 機能について具体的な説明は、以下のページをご参照ください。
OpenVINO™ ツールキット公式ドキュメント「Automatic Batching」
複数のデコード結果を 1 つのバッチとしてパッケージ化します。
for (auto va_surface : batched_frames)
{
mfxResourceType lresourceType;
mfxHDL lresource;
va_surface.first->FrameInterface->GetNativeHandle(va_surface.first,
&lresource,
&lresourceType);
VASurfaceID lvaSurfaceID = *(VASurfaceID *)(lresource);
auto nv12_tensor = shared_va_context.create_tensor_nv12(shape[2], shape[3], lvaSurfaceID);
y_tensors.push_back(nv12_tensor.first);
uv_tensors.push_back(nv12_tensor.second);
}
サンプルプログラムの使用方法
この例はオープンソースで、Ubuntu 20.04 のオペレーティング・システム、第 11 世代インテル® Core™ プロセッサーに内蔵された iGPU、およびインテル® ARC™ A380 シリーズ・グラフィックスによる dGPU 環境で検証されています。
1. 依存関係のインストールとコンパイル
次の Github にある README ドキュメントを参照して、インストールとソースコンパイルを行います。
https://github.com/OpenVINO-dev-contest/decode-infer-on-GPU
2. 出力結果
実行可能ファイルのコンパイルが完了したら、次の規則に従ってマルチ・チャンネル・バージョンの例を実行できます。
$ ./multi_src/multi_source \
-i .. /content/cars_320x240.h265,.. /content/cars_320x240.h265,.. /content/cars_320x240.h265 \
-m ~/vehicle-detection-0200/FP32/vehicle-detection-0200.xml \
-bz 2 \
-nr 4 \
-fr 30
配置可能なパラメータは次のとおりです。
-i = .h265 形式の入力ビデオパスで、マルチビデオ・ソースはコンマで区切られます
-m = IR .xml 形式 IR モデルファイル・パス
-bs = バッチサイズ
-nr = インファレンス・リクエストの数量
-fr = 1 つのビデオソースのデコードされたフレーム数 (既定では最初の 30 フレーム)
出力結果の例は次のようになります。事前に設定されたバッチサイズによってマッチした結果を出力します。この例におけるバッチサイズは 2 です。
libva info: VA-API version 1.12.0
libva info: Trying to open /opt/intel/mediasdk/lib64/iHD_drv_video.so
libva info: Found init function __vaDriverInit_1_12
libva info: va_openDriver() returns 0
Implementation details:
ApiVersion: 2.7
Implementation type: HW
AccelerationMode via: VAAPI
Path: /usr/lib/x86_64-linux-gnu/libmfx-gen.so.1.2.7
Frames [stream_id=1] [stream_id=0]
image0: bbox 204.99, 49.43, 296.43, 144.56, confidence = 0.99805
image0: bbox 91.26, 115.56, 198.41, 221.69, confidence = 0.99609
image0: bbox 36.50, 44.75, 111.34, 134.57, confidence = 0.98535
image0: bbox 77.92, 72.38, 155.06, 164.30, confidence = 0.97510
image1: bbox 204.99, 49.43, 296.43, 144.56, confidence = 0.99805
image1: bbox 91.26, 115.56, 198.41, 221.69, confidence = 0.99609
image1: bbox 36.50, 44.75, 111.34, 134.57, confidence = 0.98535
image1: bbox 77.92, 72.38, 155.06, 164.30, confidence = 0.97510
Frames [stream_id=1] [stream_id=0]
image0: bbox 206.96, 50.41, 299.54, 146.23, confidence = 0.99805
image0: bbox 93.81, 115.29, 200.86, 222.94, confidence = 0.99414
image0: bbox 84.15, 92.91, 178.14, 191.82, confidence = 0.99316
image0: bbox 37.78, 45.82, 113.29, 132.28, confidence = 0.98193
image0: bbox 75.96, 71.88, 154.31, 164.54, confidence = 0.96582
image1: bbox 206.96, 50.41, 299.54, 146.23, confidence = 0.99805
image1: bbox 93.81, 115.29, 200.86, 222.94, confidence = 0.99414
image1: bbox 84.15, 92.91, 178.14, 191.82, confidence = 0.99316
image1: bbox 37.78, 45.82, 113.29, 132.28, confidence = 0.98193
image1: bbox 75.96, 71.88, 154.31, 164.54, confidence = 0.96582
...
decoded and infered 60 frames
Time = 0.328556s
まとめ
バッチ化とマルチスレッドにより、マルチデータソース・タスクの処理における GPU のスループット性能の優位性をさらに高め、CPU 側のタスク負荷を軽減することができます。インテル製のディスクリート GPU シリーズ製品が普及するとともに、この参照設計により開発者が GPU プラットフォームでより良い性能を実現できるようになると思います。