GPU

概要

MediaPipe は GPU の計算とレンダリング用の計算ノードをサポートしており、複数の GPU ノードを組み合わせたり、CPU ベースの計算ノードと組み合わせたりできます。モバイル プラットフォームにはいくつかの GPU API が存在します(OpenGL ES、Metal、Vulkan など)。MediaPipe は、単一のクロス API GPU 抽象化は提供しません。個々のノードは異なる API を使用して記述できるため、必要に応じてプラットフォーム固有の機能を利用できます。

モバイル プラットフォームでのパフォーマンス、特にリアルタイム動画のパフォーマンスを向上させるには、GPU のサポートが不可欠です。MediaPipe を使用すると、デベロッパーは次の目的で GPU の使用をサポートする GPU 互換の計算ツールを作成できます。

  • バッチ処理だけでなく、デバイス上のリアルタイム処理
  • 分析だけでなく動画のレンダリングとエフェクト

MediaPipe での GPU サポートの設計原則は次のとおりです。

  • GPU ベースの計算ツールは、グラフ内のどこにでも表示でき、必ずしも画面上のレンダリングに使用する必要はありません。
  • ある GPU ベースの計算ツールから別の GPU ベースの計算ツールへのフレームデータの転送は高速で、コストの高いコピー オペレーションを発生させないようにする必要があります。
  • CPU と GPU 間のフレームデータの転送は、プラットフォームが許す限り効率的に行う必要があります。
  • 最適なパフォーマンスを得るために必要な手法はプラットフォームによって異なる可能性があるため、この API では、裏で実装する方法に柔軟性を持たせる必要があります。
  • 計算ツールでは、演算の全部または一部に GPU を使用し、必要に応じて CPU と組み合わせる際、最大限の柔軟性を持たせる必要があります。

OpenGL ES のサポート

MediaPipe は、Android/Linux ではバージョン 3.2 まで、iOS では ES 3.0 までの OpenGL ES をサポートしています。また、MediaPipe は iOS の Metal もサポートしています。

ML 推論計算ツールおよびグラフを実行するには、OpenGL ES 3.1 以降(Android/Linux システム上)が必要です。

MediaPipe を使用すると、グラフは複数の GL コンテキストで OpenGL を実行できます。たとえば、低速の GPU 推論パス(10 FPS など)と GPU レンダリング パスの高速化(30 FPS など)を組み合わせたグラフで非常に役立ちます。1 つの GL コンテキストが 1 つの連続したコマンドキューに対応しているため、両方のタスクに同じコンテキストを使用すると、レンダリング フレームレートが低下します。

MediaPipe が複数のコンテキストを使用することで解決する課題の一つは、コンテキスト間でのコミュニケーションです。たとえば、入力動画がレンダリング パスと推論パスの両方に送信される場合、レンダリングは推論からの最新の出力にアクセスする必要があります。

OpenGL コンテキストに、複数のスレッドから同時にアクセスすることはできません。さらに、同じスレッドでアクティブな GL コンテキストを切り替えると、一部の Android デバイスでは時間がかかることがあります。したがって、Google のアプローチは、コンテキストごとに 1 つの専用スレッドを使用することです。各スレッドは GL コマンドを発行し、そのコンテキストでシリアル コマンドキューを構築します。その後、GPU によって非同期に実行されます。

GPU 計算ツールのライフサイクル

このセクションでは、基本クラス GlSimpleCalculator から派生した GPU 計算ツールの Process メソッドの基本構造について説明します。GPU 計算ツール LuminanceCalculator は一例として示されています。LuminanceCalculator::GlRender メソッドは GlSimpleCalculator::Process から呼び出されます。

// Converts RGB images into luminance images, still stored in RGB format.
// See GlSimpleCalculator for inputs, outputs and input side packets.
class LuminanceCalculator : public GlSimpleCalculator {
 public:
  absl::Status GlSetup() override;
  absl::Status GlRender(const GlTexture& src,
                        const GlTexture& dst) override;
  absl::Status GlTeardown() override;

 private:
  GLuint program_ = 0;
  GLint frame_;
};
REGISTER_CALCULATOR(LuminanceCalculator);

absl::Status LuminanceCalculator::GlRender(const GlTexture& src,
                                           const GlTexture& dst) {
  static const GLfloat square_vertices[] = {
      -1.0f, -1.0f,  // bottom left
      1.0f,  -1.0f,  // bottom right
      -1.0f, 1.0f,   // top left
      1.0f,  1.0f,   // top right
  };
  static const GLfloat texture_vertices[] = {
      0.0f, 0.0f,  // bottom left
      1.0f, 0.0f,  // bottom right
      0.0f, 1.0f,  // top left
      1.0f, 1.0f,  // top right
  };

  // program
  glUseProgram(program_);
  glUniform1i(frame_, 1);

  // vertex storage
  GLuint vbo[2];
  glGenBuffers(2, vbo);
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  // vbo 0
  glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
  glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), square_vertices,
               GL_STATIC_DRAW);
  glEnableVertexAttribArray(ATTRIB_VERTEX);
  glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, nullptr);

  // vbo 1
  glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
  glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), texture_vertices,
               GL_STATIC_DRAW);
  glEnableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
  glVertexAttribPointer(ATTRIB_TEXTURE_POSITION, 2, GL_FLOAT, 0, 0, nullptr);

  // draw
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

  // cleanup
  glDisableVertexAttribArray(ATTRIB_VERTEX);
  glDisableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
  glDeleteVertexArrays(1, &vao);
  glDeleteBuffers(2, vbo);

  return absl::OkStatus();
}

上記の設計原則により、MediaPipe GPU サポートの設計は以下のようになりました。

  • 画像データを表す GpuBuffer という GPU データ型があり、GPU の使用向けに最適化されています。このデータ型の内容は正確には不透明で、プラットフォーム固有です。
  • コンポジションに基づく低レベル API。GPU を使用する計算ツールは、GlCalculatorHelper クラスのインスタンスを作成して所有します。このクラスは、OpenGL コンテキストの管理、入出力のテクスチャのセットアップなどを行うための、プラットフォームに依存しない API を提供します。
  • サブクラス化をベースとした高レベル API。画像フィルタを実装するシンプルな計算ツールは GlSimpleCalculator からサブクラス化されており、いくつかの仮想メソッドを特定の OpenGL コードでオーバーライドするだけで済みます。その一方で、スーパークラスはすべての処理を担当します。
  • すべての GPU ベースの計算ツール間で共有する必要があるデータは、グラフサービスとして実装され、GlCalculatorHelper クラスによって管理される外部入力として提供されます。
  • 計算ツール固有のヘルパーと共有グラフサービスを組み合わせることで、GPU リソースをより柔軟に管理できます。計算ツールごとに個別のコンテキスト、単一のコンテキスト、ロックなどの同期プリミティブなどを使用できます。これらはすべてヘルパーによって管理され、個々の計算ツールからは非表示になります。

GpuBuffer から ImageFrame へのコンバータ

GpuBufferToImageFrameCalculatorImageFrameToGpuBufferCalculator という 2 つの計算ツールが用意されています。これらの計算ツールでは ImageFrameGpuBuffer が変換されるため、GPU 計算ツールと CPU 計算ツールを組み合わせたグラフを構成できます。iOS と Android の両方でサポートされています。

可能な場合、これらの計算ツールはプラットフォーム固有の機能を使用して、データをコピーせずに CPU と GPU の間で共有します。

次の図は、カメラから動画をキャプチャし、MediaPipe グラフを介して実行し、出力をリアルタイムで画面にレンダリングするモバイルアプリのデータフローを示しています。破線は、MediaPipe グラフの内側にある部分を示しています。このアプリケーションは、OpenCV を使用して CPU で Canny エッジ検出フィルタを実行し、GPU を使用して元の動画の上にオーバーレイします。

GPU 計算ツール間の連携

カメラからの動画フレームは、GpuBuffer パケットとしてグラフにフィードされます。入力ストリームには、2 つの計算ツールが並行してアクセスします。GpuBufferToImageFrameCalculator はバッファを ImageFrame に変換し、グレースケール コンバータと Canny フィルタ(どちらも OpenCV に基づいており、CPU で実行)を介して送信され、その出力が再び GpuBuffer に変換されます。マルチ入力 GPU 計算ツール GlOverlayCalculator は、元の GpuBuffer とエッジ検出器から出力されたものの両方を入力として受け取り、シェーダーを使用してオーバーレイします。出力はコールバック計算ツールによってアプリに返され、アプリは OpenGL を使用して画像を画面にレンダリングします。