GPU

Descripción general

MediaPipe admite nodos de calculadora para el procesamiento y la renderización de GPU, y permite combinar múltiples nodos de GPU, así como combinarlos con nodos de calculadora basados en CPU. Existen varias APIs de GPU en plataformas móviles (p. ej., OpenGL ES, Metal y Vulkan). MediaPipe no intenta ofrecer una única abstracción de GPU entre APIs. Los nodos individuales se pueden escribir con diferentes APIs, lo que les permite aprovechar las funciones específicas de la plataforma cuando sea necesario.

La compatibilidad con GPU es fundamental para lograr un buen rendimiento en las plataformas de dispositivos móviles, en especial para los videos en tiempo real. MediaPipe permite que los desarrolladores escriban calculadoras compatibles con GPU que admitan el uso de GPU para lo siguiente:

  • Procesamiento en tiempo real integrado en el dispositivo, no solo procesamiento por lotes
  • Renderización y efectos de video, no solo análisis

A continuación, se presentan los principios de diseño para la compatibilidad de GPU en MediaPipe

  • Las calculadoras basadas en GPU deberían poder ubicarse en cualquier parte del gráfico y no necesariamente se deben utilizar para la renderización en pantalla.
  • La transferencia de datos de fotogramas de una calculadora basada en GPU a otra debe ser rápida y no generar operaciones de copiado costosas.
  • La transferencia de datos de fotogramas entre la CPU y la GPU debe ser tan eficiente como lo permita la plataforma.
  • Debido a que las diferentes plataformas pueden requerir técnicas diferentes para obtener el mejor rendimiento, la API debería permitir flexibilidad en la forma en que se implementan las cosas detrás de escena.
  • Las calculadoras deben contar con la máxima flexibilidad para usar la GPU durante toda su operación o parte de ella, y combinarla con la CPU en caso de ser necesario.

Compatibilidad con OpenGL ES

MediaPipe es compatible con OpenGL ES hasta la versión 3.2 en Android y Linux y hasta ES 3.0 en iOS. Además, MediaPipe también es compatible con Metal en iOS.

Se requiere OpenGL ES 3.1 o versiones posteriores (en sistemas Android/Linux) para ejecutar calculadoras y gráficos de inferencia de aprendizaje automático.

MediaPipe permite que los gráficos ejecuten OpenGL en varios contextos de GL. Por ejemplo, esto puede ser muy útil en gráficos en los que se combina una ruta de inferencia de GPU más lenta (p. ej., a 10 FPS) con una ruta de procesamiento de GPU más rápida (p. ej., a 30 FPS): dado que un contexto de GL corresponde a una cola de comandos secuencial, usar el mismo contexto para ambas tareas reduciría la velocidad de fotogramas de renderización.

Un desafío que resuelve el uso de varios contextos de MediaPipe es la capacidad de comunicarse entre ellos. Una situación de ejemplo es una con un video de entrada que se envía a las rutas de renderización y de inferencias, y la renderización debe tener acceso al resultado más reciente de la inferencia.

No se puede acceder a un contexto de OpenGL de varios subprocesos al mismo tiempo. Además, cambiar el contexto de GL activo en el mismo subproceso puede ser lento en algunos dispositivos Android. Por lo tanto, nuestro enfoque es tener un subproceso dedicado por contexto. Cada subproceso emite comandos de GL, lo que crea una cola de comandos en serie en su contexto, que luego ejecuta la GPU de manera asíncrona.

La vida de una calculadora de GPU

En esta sección, se presenta la estructura básica del método Process de una calculadora de GPU derivada de la clase base GlSimpleCalculator. Se muestra la calculadora de GPU LuminanceCalculator como ejemplo. El método LuminanceCalculator::GlRender se llama desde 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();
}

Los principios de diseño mencionados con anterioridad dieron como resultado las siguientes opciones de diseño para la compatibilidad con GPU de MediaPipe:

  • Hay un tipo de datos de GPU, llamado GpuBuffer, que representa datos de imágenes y está optimizado para el uso de GPU. El contenido exacto de este tipo de datos es opaco y específico de la plataforma.
  • Una API de bajo nivel basada en la composición, en la que cualquier calculadora que desea usar la GPU crea y posee una instancia de la clase GlCalculatorHelper. Esta clase ofrece una API independiente de la plataforma para administrar el contexto de OpenGL y configurar texturas para entradas y salidas, etcétera.
  • Es una API de alto nivel basada en subclases, en la que las calculadoras simples que implementan filtros de imágenes subclases de GlSimpleCalculator y solo necesitan anular algunos métodos virtuales con su código OpenGL específico, mientras la superclase se encarga de todo el mantenimiento.
  • Los datos que se deben compartir entre todas las calculadoras basadas en GPU se proporcionan como una entrada externa que se implementa como un servicio de gráficos y se administra a través de la clase GlCalculatorHelper.
  • La combinación de asistentes específicos de la calculadora y un servicio de gráficos compartido nos permite una gran flexibilidad en la administración del recurso de la GPU: podemos tener un contexto separado por calculadora, compartir un contexto único, compartir un bloqueo u otras primitivas de sincronización, etc. Todo esto es administrado por el ayudante y se oculta de las calculadoras individuales.

Convertidores de GpuBuffer a ImageFrame

Proporcionamos dos calculadoras denominadas GpuBufferToImageFrameCalculator y ImageFrameToGpuBufferCalculator. Estas calculadoras convierten entre ImageFrame y GpuBuffer, lo que permite construir gráficos que combinan calculadoras de GPU y CPU. Son compatibles con iOS y Android.

Cuando sea posible, estas calculadoras usarán la funcionalidad específica de la plataforma para compartir datos entre la CPU y la GPU sin copiar.

En el siguiente diagrama se muestra el flujo de datos en una aplicación para dispositivos móviles que captura video de la cámara, lo ejecuta a través de un gráfico de MediaPipe y renderiza el resultado en la pantalla en tiempo real. La línea punteada indica qué partes están dentro del gráfico de MediaPipe propiamente dicho. Esta aplicación ejecuta un filtro de detección de bordes de Canny en la CPU mediante OpenCV, y lo superpone sobre el video original con la GPU.

Cómo interactúan las calculadoras de GPU

Los fotogramas de video de la cámara se ingresan al gráfico como paquetes GpuBuffer. Dos calculadoras en paralelo acceden al flujo de entrada. GpuBufferToImageFrameCalculator convierte el búfer en un ImageFrame, que luego se envía a través de un convertidor de escala de grises y un filtro canny (basado en OpenCV y en ejecución en la CPU), cuya salida luego se vuelve a convertir en GpuBuffer. GlOverlayCalculator, una calculadora de GPU de varias entradas, toma como entrada el GpuBuffer original y el que sale del detector de bordes, y los superpone con un sombreador. Luego, el resultado se envía de vuelta a la aplicación con una calculadora de devolución de llamada, y la aplicación renderiza la imagen en la pantalla con OpenGL.