GPU

Présentation

MediaPipe est compatible avec les nœuds de calculatrices pour le calcul et le rendu GPU, et permet de combiner plusieurs nœuds GPU, ainsi que de les combiner avec des nœuds de calculateur basés sur le processeur. Il existe plusieurs API GPU sur les plates-formes mobiles (OpenGL ES, Metal et Vulkan, par exemple). MediaPipe ne cherche pas à offrir une seule abstraction de GPU multi-API. Les nœuds individuels peuvent être écrits à l'aide de différentes API, ce qui leur permet de bénéficier de fonctionnalités spécifiques à la plate-forme si nécessaire.

La compatibilité avec les GPU est essentielle pour obtenir de bonnes performances sur les plates-formes mobiles, en particulier pour les vidéos en temps réel. MediaPipe permet aux développeurs d'écrire des calculateurs compatibles GPU qui acceptent l'utilisation de GPU pour les tâches suivantes:

  • Traitement en temps réel sur l'appareil, pas seulement le traitement par lot
  • Rendu et effets vidéo, pas seulement l'analyse

Vous trouverez ci-dessous les principes de conception pour la prise en charge des GPU dans MediaPipe.

  • Les calculatrices basées sur GPU doivent pouvoir apparaître n'importe où dans le graphique et ne doivent pas nécessairement être utilisées pour le rendu à l'écran.
  • Le transfert des données de frame d'une calculatrice basée sur un GPU à une autre devrait être rapide et ne pas entraîner d'opérations de copie coûteuses.
  • Le transfert des données de trame entre le CPU et le GPU doit être aussi efficace que le permet la plate-forme.
  • Étant donné que différentes plates-formes peuvent nécessiter différentes techniques pour obtenir des performances optimales, l'API doit permettre une implémentation flexible en arrière-plan.
  • Un calculateur doit bénéficier d'une flexibilité maximale pour utiliser le GPU pour tout ou partie de ses opérations, en le combinant avec le CPU si nécessaire.

Compatibilité avec OpenGL ES

MediaPipe est compatible avec OpenGL ES jusqu'à la version 3.2 sur Android/Linux et jusqu'à ES 3.0 sur iOS. MediaPipe est également compatible avec Metal sur iOS.

OpenGL ES 3.1 ou version ultérieure est requis (sur les systèmes Android/Linux) pour exécuter des calculateurs et des graphiques d'inférence de machine learning.

MediaPipe permet aux graphiques d'exécuter OpenGL dans plusieurs contextes GL. Par exemple, cela peut être très utile dans les graphiques qui combinent un chemin d'inférence GPU plus lent (par exemple, à 10 FPS) avec un chemin de rendu GPU plus rapide (par exemple, à 30 FPS), car un contexte GL correspond à une file d'attente de commandes séquentielle, l'utilisation du même contexte pour les deux tâches réduirait la fréquence d'images du rendu.

L'utilisation de plusieurs contextes résout un défi que permet MediaPipe de communiquer entre eux. Prenons l'exemple d'un scénario avec une vidéo d'entrée envoyée aux chemins de rendu et d'inférence. Le rendu doit avoir accès à la dernière sortie de l'inférence.

Un contexte OpenGL n'est pas accessible par plusieurs threads à la fois. De plus, le changement de contexte GL actif sur le même thread peut être lent sur certains appareils Android. Par conséquent, notre approche consiste à utiliser un thread dédié par contexte. Chaque thread émet des commandes GL, créant ainsi une file d'attente de commandes série en fonction de son contexte, qui est ensuite exécutée par le GPU de manière asynchrone.

Le cycle de vie d'une calculatrice GPU

Cette section présente la structure de base de la méthode de traitement d'un calculateur GPU dérivé de la classe de base GlSimpleCalculator. Le simulateur de GPU LuminanceCalculator est présenté à titre d'exemple. La méthode LuminanceCalculator::GlRender est appelée à partir de 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();
}

Les principes de conception mentionnés ci-dessus ont donné lieu aux choix de conception suivants pour la compatibilité avec les GPU MediaPipe:

  • Nous disposons d'un type de données GPU, appelé GpuBuffer, pour représenter les données d'image, optimisé pour l'utilisation du GPU. Le contenu exact de ce type de données est opaque et spécifique à une plate-forme.
  • API de bas niveau basée sur la composition, où tout calculateur souhaitant utiliser le GPU crée une instance de la classe GlCalculatorHelper et en possède la possession. Cette classe propose une API indépendante de la plate-forme permettant de gérer le contexte OpenGL, de configurer des textures pour les entrées et les sorties, etc.
  • API de haut niveau basée sur le sous-classement, dans laquelle des calculateurs simples implémentant la sous-classe de filtres d'image de GlSimpleCalculator et n'ont besoin de remplacer que quelques méthodes virtuelles par leur code OpenGL spécifique, tandis que la super-classe s'occupe de toute la plomberie.
  • Les données devant être partagées entre tous les calculateurs GPU sont fournies sous la forme d'une entrée externe implémentée en tant que service de graphe et gérée par la classe GlCalculatorHelper.
  • La combinaison d'aides propres aux calculateurs et d'un service de graphe partagé nous offre une grande flexibilité dans la gestion des ressources GPU: nous pouvons avoir un contexte distinct par calculatrice, partager un contexte unique, partager un verrou ou d'autres primitives de synchronisation, etc. Tout cela est géré par l'application auxiliaire et masqué pour les calculateurs individuels.

Convertisseurs GpuBuffer en ImageFrame

Nous mettons à votre disposition deux outils de calcul appelés GpuBufferToImageFrameCalculator et ImageFrameToGpuBufferCalculator. Ces calculateurs convertissent entre ImageFrame et GpuBuffer, ce qui permet de construire des graphiques combinant des calculateurs GPU et CPU. Elles sont compatibles avec iOS et Android.

Dans la mesure du possible, ces calculateurs utilisent des fonctionnalités propres à la plate-forme pour partager les données entre le processeur et le GPU sans avoir à les copier.

Le schéma ci-dessous illustre le flux de données dans une application mobile qui capture le flux vidéo de l'appareil photo, le présente à l'aide d'un graphique MediaPipe et affiche le résultat à l'écran en temps réel. La ligne en pointillés indique les parties qui se trouvent à l'intérieur du graphique MediaPipe. Cette application exécute un filtre de détection de bord Canny sur le CPU à l'aide d'OpenCV et le superpose à la vidéo d'origine à l'aide du GPU.

Interaction entre les calculateurs GPU

Les trames vidéo de l'appareil photo sont transmises au graphe sous forme de paquets GpuBuffer. Le flux d'entrée est accessible par deux calculateurs en parallèle. GpuBufferToImageFrameCalculator convertit le tampon en ImageFrame, qui est ensuite envoyé via un convertisseur en nuances de gris et un filtre canny (tous deux basés sur OpenCV et s'exécutant sur le processeur), dont la sortie est à nouveau convertie en GpuBuffer. Un calculateur GPU à plusieurs entrées, GlOverlayCalculator, prend en entrée le GpuBuffer d'origine et celui provenant du détecteur d'arête, et les superpose à l'aide d'un nuanceur. La sortie est ensuite renvoyée à l'application à l'aide d'un calculateur de rappel, et l'application affiche l'image à l'écran en utilisant OpenGL.