GPU

Visão geral

O MediaPipe oferece suporte a nós de calculadora para computação e renderização de GPU e permite combinar vários nós da GPU, além de combiná-los com nós de calculadora baseados na CPU. Existem várias APIs de GPU em plataformas móveis (por exemplo, OpenGL ES, Metal e Vulkan). O MediaPipe não tenta oferecer uma única abstração de GPU entre APIs. Nós individuais podem ser escritos usando APIs diferentes, o que permite que eles aproveitem recursos específicos da plataforma quando necessário.

O suporte à GPU é essencial para um bom desempenho em plataformas móveis, especialmente para vídeos em tempo real. O MediaPipe permite que os desenvolvedores criem calculadoras compatíveis com GPU compatíveis com o uso da GPU para:

  • Processamento em tempo real no dispositivo, não apenas em lote
  • Renderização e efeitos de vídeo, não apenas análise

Confira abaixo os princípios de design para suporte a GPUs no MediaPipe

  • Calculadoras baseadas em GPU devem poder ocorrer em qualquer lugar do gráfico e não necessariamente ser usadas para renderização na tela.
  • A transferência de dados de frame de uma calculadora baseada em GPU para outra precisa ser rápida e não gerar operações de cópia dispendiosas.
  • A transferência de dados de frame entre a CPU e a GPU precisa ser tão eficiente quanto a plataforma permite.
  • Como cada plataforma pode exigir técnicas diferentes para um melhor desempenho, a API deve permitir flexibilidade na forma como as coisas são implementadas nos bastidores.
  • Uma calculadora precisa ter flexibilidade máxima no uso da GPU em toda a operação ou parte dela, combinando-a com a CPU, se necessário.

Compatibilidade com OpenGL ES

O MediaPipe oferece suporte ao OpenGL ES até a versão 3.2 no Android/Linux e até o ES 3.0 no iOS. Além disso, o MediaPipe também é compatível com Metal no iOS.

O OpenGL ES 3.1 ou versão mais recente é necessário (em sistemas Android/Linux) para executar calculadoras e gráficos de inferência de aprendizado de máquina.

O MediaPipe permite que os gráficos executem o OpenGL em vários contextos GL. Por exemplo, isso pode ser muito útil em gráficos que combinam um caminho de inferência de GPU mais lento (a 10 QPS) com um caminho de renderização de GPU mais rápido (por exemplo, a 30 QPS): já que um contexto GL corresponde a uma fila de comando sequencial, usar o mesmo contexto para as duas tarefas reduziria o frame rate de renderização.

Um desafio do MediaPipe que o uso de vários contextos resolve é a capacidade de se comunicar entre eles. Um cenário de exemplo é um com um vídeo de entrada que é enviado aos caminhos de renderização e inferência, e a renderização precisa ter acesso à saída mais recente da inferência.

Um contexto OpenGL não pode ser acessado por várias linhas de execução ao mesmo tempo. Além disso, mudar o contexto de GL ativo na mesma linha de execução pode ser lento em alguns dispositivos Android. Portanto, nossa abordagem é ter uma linha de execução dedicada por contexto. Cada linha de execução emite comandos GL, criando uma fila de comandos serial no contexto, que é então executada pela GPU de forma assíncrona.

A vida de uma calculadora de GPU

Esta seção apresenta a estrutura básica do método Process de uma calculadora de GPU derivada da classe base GlSimpleCalculator. A calculadora da GPU LuminanceCalculator é mostrada como exemplo. O método LuminanceCalculator::GlRender é chamado em 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();
}

Os princípios de design mencionados acima resultaram nas seguintes opções de design para suporte a GPUs do MediaPipe:

  • Temos um tipo de dados de GPU, GpuBuffer, para representar dados de imagem, otimizado para uso da GPU. O conteúdo exato desse tipo de dados é opaco e específico da plataforma.
  • Uma API de baixo nível baseada em composição, em que qualquer calculadora que queira usar a GPU cria e é proprietária de uma instância da classe GlCalculatorHelper. Essa classe oferece uma API independente de plataforma para gerenciar o contexto do OpenGL, configurar texturas para entradas e saídas etc.
  • Uma API de alto nível baseada em subclasses, em que calculadoras simples que implementam a subclasse de filtros de imagem da GlSimpleCalculator e só precisam substituir alguns métodos virtuais pelo código OpenGL específico, enquanto a superclasse cuida de todo o encanamento.
  • Os dados que precisam ser compartilhados entre todas as calculadoras baseadas em GPU são fornecidos como uma entrada externa, implementada como um serviço de gráfico e gerenciada pela classe GlCalculatorHelper.
  • A combinação de auxiliares específicos para calculadoras e um serviço de gráfico compartilhado proporciona grande flexibilidade no gerenciamento do recurso da GPU: podemos ter um contexto separado por calculadora, compartilhar um único contexto, compartilhar um bloqueio ou outros primitivos de sincronização etc. Tudo isso é gerenciado pelo assistente e ocultado nas calculadoras individuais.

Conversores GpuBuffer para ImageFrame

Oferecemos duas calculadoras chamadas GpuBufferToImageFrameCalculator e ImageFrameToGpuBufferCalculator. Essas calculadoras são convertidas entre ImageFrame e GpuBuffer, permitindo a criação de gráficos que combinam calculadoras de GPU e CPU. Eles são compatíveis com iOS e Android

Quando possível, essas calculadoras usam funcionalidades específicas da plataforma para compartilhar dados entre a CPU e a GPU sem copiar.

O diagrama abaixo mostra o fluxo de dados em um aplicativo para dispositivos móveis que captura vídeos da câmera, os executa em um gráfico do MediaPipe e renderiza a saída na tela em tempo real. A linha tracejada indica quais partes estão dentro do gráfico do MediaPipe correto. Este aplicativo executa um filtro de detecção de borda do Canny na CPU usando o OpenCV e o sobrepõe ao vídeo original usando a GPU.

Como as calculadoras de GPU interagem

Os frames de vídeo da câmera são inseridos no gráfico como pacotes GpuBuffer. O fluxo de entrada é acessado por duas calculadoras em paralelo. O GpuBufferToImageFrameCalculator converte o buffer em um ImageFrame, que é enviado por um conversor de escala de cinza e um filtro canny (ambos baseados no OpenCV e em execução na CPU), cuja saída é convertida novamente em GpuBuffer. A calculadora GPU de várias entradas, a GlOverlayCalculator, usa como entrada o GpuBuffer original e o que sai do detector de borda e os sobrepõe usando um sombreador. Em seguida, a saída é enviada de volta ao aplicativo usando uma calculadora de callback, e o aplicativo renderiza a imagem na tela usando o OpenGL.