GPU

Überblick

MediaPipe unterstützt Rechnerknoten für GPU-Computing und -Rendering und ermöglicht die Kombination mehrerer GPU-Knoten sowie die Kombination dieser mit CPU-basierten Rechnerknoten. Auf mobilen Plattformen gibt es mehrere GPU-APIs, z. B. OpenGL ES, Metal und Vulkan. MediaPipe versucht nicht, eine einzelne API-übergreifende GPU-Abstraktion anzubieten. Einzelne Knoten können mit verschiedenen APIs geschrieben werden, sodass sie bei Bedarf plattformspezifische Funktionen nutzen können.

GPU-Unterstützung ist für eine gute Leistung auf mobilen Plattformen unerlässlich, insbesondere bei Echtzeit-Videos. Mit MediaPipe können Entwickler GPU-kompatible Rechner schreiben, die die Verwendung von GPU für Folgendes unterstützen:

  • On-Device-Echtzeitverarbeitung, nicht nur Batchverarbeitung
  • Mehr als Analyse von Video-Rendering und -Effekten

Im Folgenden finden Sie die Designprinzipien für die GPU-Unterstützung in MediaPipe

  • GPU-basierte Rechner sollten überall in der Grafik erscheinen können. Sie sollten nicht unbedingt für das Bildschirm-Rendering verwendet werden.
  • Die Übertragung von Framedaten von einem GPU-basierten Rechner auf einen anderen sollte schnell ablaufen und keine teuren Kopiervorgänge verursachen.
  • Die Übertragung von Framedaten zwischen CPU und GPU sollte so effizient sein, wie die Plattform es zulässt.
  • Da unterschiedliche Plattformen möglicherweise unterschiedliche Techniken für die beste Leistung erfordern, sollte die API Flexibilität bei der Implementierung im Hintergrund bieten.
  • Einem Rechner sollte die maximale Flexibilität gegeben sein, wenn es darum geht, die GPU für den gesamten Betrieb oder einen Teil ihres Betriebs zu verwenden und sie gegebenenfalls mit der CPU zu kombinieren.

OpenGL ES-Unterstützung

MediaPipe unterstützt OpenGL ES bis Version 3.2 unter Android/Linux und bis ES 3.0 unter iOS. Außerdem unterstützt MediaPipe Metal auf iOS-Geräten.

Zum Ausführen von Inferenzrechnern und Grafiken für maschinelles Lernen ist OpenGL ES 3.1 oder höher (auf Android-/Linux-Systemen) erforderlich.

MediaPipe ermöglicht die Ausführung von OpenGL in mehreren GL-Kontexten. Dies kann beispielsweise bei Diagrammen sehr nützlich sein, in denen ein langsamerer GPU-Inferenzpfad (z. B. mit 10 fps) mit einem schnelleren GPU-Renderingpfad (z. B. mit 30 fps) kombiniert wird. Da ein GL-Kontext einer sequenziellen Befehlswarteschlange entspricht, würde die Verwendung des gleichen Kontexts für beide Aufgaben die Rendering-Framerate reduzieren.

Eine Herausforderung, die MediaPipe bei der Verwendung mehrerer Kontexte löst, ist die Fähigkeit, über mehrere Kontexte hinweg zu kommunizieren. Ein Beispielszenario ist ein Szenario mit einem Eingabevideo, das sowohl an den Rendering- als auch an den Inferenzpfad gesendet wird, und das Rendering benötigt Zugriff auf die neueste Ausgabe der Inferenz.

Auf einen OpenGL-Kontext kann nicht von mehreren Threads gleichzeitig zugegriffen werden. Außerdem kann das Wechseln des aktiven GL-Kontexts im selben Thread auf einigen Android-Geräten langsam sein. Daher ist es unser Ansatz, pro Kontext einen eigenen Thread zu haben. Jeder Thread gibt GL-Befehle aus und baut eine serielle Befehlswarteschlange für den Kontext auf, die dann asynchron von der GPU ausgeführt wird.

Lebensdauer eines GPU-Rechners

In diesem Abschnitt wird die Grundstruktur der Prozessmethode eines GPU-Rechners beschrieben, der aus der Basisklasse „GlSimpleCalculator“ abgeleitet ist. Als Beispiel wird der GPU-Rechner LuminanceCalculator gezeigt. Die Methode LuminanceCalculator::GlRender wird über GlSimpleCalculator::Process aufgerufen.

// 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();
}

Die oben genannten Designprinzipien haben zu den folgenden Designentscheidungen für die MediaPipe-GPU-Unterstützung geführt:

  • Wir haben einen GPU-Datentyp namens GpuBuffer zur Darstellung von Bilddaten, der für die GPU-Nutzung optimiert ist. Die genauen Inhalte dieses Datentyps sind intransparent und plattformspezifisch.
  • Eine Low-Level-API basierend auf Zusammensetzung, bei der jeder Rechner, der die GPU nutzen möchte, eine Instanz der GlCalculatorHelper-Klasse erstellt und besitzt. Diese Klasse bietet eine plattformunabhängige API zur Verwaltung des OpenGL-Kontexts, zum Einrichten von Texturen für Ein- und Ausgaben usw.
  • Eine auf Unterklassen basierende allgemeine API, bei der einfache Rechner die Unterklasse von Bildfiltern von GlSimpleCalculator implementieren und nur einige virtuelle Methoden mit ihrem spezifischen OpenGL-Code überschreiben müssen. Die Basisklasse kümmert sich um die gesamte Installation.
  • Daten, die von allen GPU-basierten Rechnern gemeinsam genutzt werden müssen, werden als externe Eingabe bereitgestellt, die als Grafikdienst implementiert und von der Klasse GlCalculatorHelper verwaltet wird.
  • Die Kombination aus rechnerspezifischen Hilfsprogrammen und einem gemeinsamen Grafikdienst ermöglicht uns bei der Verwaltung der GPU-Ressource äußerste Flexibilität: Wir können einen separaten Kontext pro Rechner verwenden, einen einzelnen Kontext teilen, eine Sperre oder andere Synchronisierungsprimitive verwenden usw. – all dies wird vom Hilfsprogramm verwaltet und vor den einzelnen Rechnern verborgen.

Von GpuBuffer zu ImageFrame-Convertern

Wir stellen zwei Rechner zur Verfügung: GpuBufferToImageFrameCalculator und ImageFrameToGpuBufferCalculator. Diese Rechner rechnen zwischen ImageFrame und GpuBuffer um und ermöglichen so die Erstellung von Grafiken, in denen GPU- und CPU-Rechner kombiniert werden. Sie werden sowohl auf iOS- als auch auf Android-Geräten unterstützt.

Wenn möglich, nutzen diese Rechner plattformspezifische Funktionen, um Daten zwischen CPU und GPU auszutauschen, ohne zu kopieren.

Das folgende Diagramm zeigt den Datenfluss in einer mobilen Anwendung, die ein Video von der Kamera aufnimmt, es durch ein MediaPipe-Diagramm anzeigt und die Ausgabe auf dem Bildschirm in Echtzeit rendert. Die gestrichelte Linie gibt an, welche Teile sich tatsächlich innerhalb der MediaPipe-Grafik befinden. Diese Anwendung führt mithilfe von OpenCV einen Canny-Kantenerkennungsfilter auf der CPU aus und blendet ihn mithilfe der GPU über das Originalvideo ein.

So interagieren GPU-Rechner

Videoframes von der Kamera werden als GpuBuffer-Pakete in die Grafik eingefügt. Auf den Eingabestream wird von zwei Rechnern parallel zugegriffen. GpuBufferToImageFrameCalculator wandelt den Zwischenspeicher in einen ImageFrame um, der dann über einen Graustufenkonverter und einen Canny-Filter gesendet wird (beide basieren auf OpenCV und werden auf der CPU ausgeführt). Seine Ausgabe wird dann wieder in einen GpuBuffer-Wert konvertiert. Der GPU-Rechner für mehrere Eingaben (GlOverlayCalculator) übernimmt als Eingabe sowohl das ursprüngliche GpuBuffer als auch das des Edge-Detektors und überlagert sie mithilfe eines Shaders. Die Ausgabe wird dann mithilfe eines Callback-Rechners an die Anwendung zurückgesendet. Die Anwendung rendert das Bild dann mit OpenGL auf den Bildschirm.