GPU

Présentation

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

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 calculatrices compatibles GPU qui prennent en charge l'utilisation du GPU pour:

  • 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 compatibilité avec les GPU dans MediaPipe

  • Les calculatrices basées sur des GPU doivent pouvoir apparaître n'importe où dans le graphique, et pas nécessairement être utilisées pour le rendu à l'écran.
  • Le transfert de données de frame d'un calculateur basé sur GPU à un autre doit être rapide et ne pas entraîner d'opérations de copie coûteuses.
  • Le transfert des données de frame 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 des techniques différentes pour des performances optimales, l'API doit permettre une mise en œuvre flexible en arrière-plan.
  • Une calculatrice doit disposer d'une flexibilité maximale pour utiliser le GPU pour tout ou partie de son fonctionnement, et la combiner avec le processeur 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. En outre, 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 l'exécution des calculateurs et des graphiques d'inférence de ML.

MediaPipe permet aux graphiques d'exécuter OpenGL dans plusieurs contextes GL. Par exemple, ce peut s'avérer très utile dans les graphes combinant un chemin d'inférence GPU plus lent (par exemple, à 10 FPS avec un chemin de rendu GPU plus rapide (par exemple, à 30 FPS): en raison d'un contexte GL correspond à une file d'attente de commandes séquentielles utilisant le même contexte pour les deux les tâches réduiraient la fréquence d'images de rendu.

L'utilisation de contextes multiples permet à MediaPipe de résoudre communiquer entre eux. Prenons un exemple de scénario avec une vidéo d'entrée envoyé aux chemins d'inférence et de rendu. Le rendu doit avoir aux derniers résultats de l'inférence.

Plusieurs threads ne peuvent pas accéder à un contexte OpenGL en même temps. De plus, le changement du contexte GL actif sur le même thread peut être lent sur certains appareils Android. Notre approche consiste donc à utiliser un seul 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é par le GPU de manière asynchrone.

Durée de vie d'un calculateur GPU

Cette section présente la structure de base de la méthode Process d'un GPU. calculatrice dérivée de la classe de base GlSimpleCalculator. Le simulateur de GPU LuminanceCalculator est illustré à titre d'exemple. La méthode LuminanceCalculator::GlRender est appelé depuis 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 abouti à la conception suivante options de 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 à la 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 est propriétaire. Cette classe offre 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 des filtres d'image sous-classent GlSimpleCalculator et ne doivent remplacer que quelques méthodes virtuelles par leur code OpenGL spécifique, tandis que la super-classe s'occupe de toutes les liaisons.
  • Les données qui doivent être partagées entre tous les calculateurs basés sur des GPU sont fournies sous la forme d'une entrée externe implémentée en tant que service de graphes et gérée par la classe GlCalculatorHelper.
  • La combinaison d'assistants spécifiques aux calculatrices et d'un service de graphe partagé nous offre une grande flexibilité pour gérer la ressource GPU: nous pouvons avoir un contexte distinct par calculateur, partager un seul contexte, partager un verrou ou d'autres primitives de synchronisation, etc. Le tout est géré par l'outil d'aide et masqué pour les calculateurs individuels.

Convertisseurs GpuBuffer en ImageFrame

Nous fournissons deux calculateurs appelés GpuBufferToImageFrameCalculator et ImageFrameToGpuBufferCalculator. Ces calculateurs effectuent des conversions entre ImageFrame et GpuBuffer, ce qui permet de créer des graphiques combinant des calculatrices de GPU et de processeur. Elles sont compatibles avec iOS et Android.

Lorsque cela est possible, ces calculateurs utilisent des fonctionnalités propres à la plate-forme pour partager des données entre le processeur et le GPU sans copier.

Le schéma ci-dessous illustre le flux de données dans une application mobile qui enregistre une vidéo à partir de l'appareil photo, l'exécute sur un graphique MediaPipe et affiche le résultat à l'écran en temps réel. La ligne en pointillé indique les parties qui figurent dans le graphique MediaPipe. Cette application exécute un filtre de détection de bords Canny sur le processeur à l'aide d'OpenCV et le superpose à la vidéo d'origine à l'aide du GPU.

Interactions entre les calculatrices GPU

Les images vidéo provenant de l'appareil photo sont transmises au graphique sous forme de paquets GpuBuffer. La le flux d'entrée est accessible par deux calculatrices en parallèle. GpuBufferToImageFrameCalculator convertit le tampon en ImageFrame, qui est ensuite envoyé par un convertisseur en nuances de gris et un filtre Canny (tous deux basés sur OpenCV et s'exécutant sur le CPU), dont la sortie est ensuite convertie en GpuBuffer. Le calculateur GPU multi-entrées, GlOverlayCalculator, prend saisir à la fois l'élément GpuBuffer d'origine et celui qui sort du détecteur de bords, et les superpose à l'aide d'un nuanceur. La sortie est ensuite renvoyée au application à l'aide d'un calculateur de rappels, et elle affiche l'image à l'écran à l'aide d'OpenGL.