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 de GPU, bem como misturá-los com nós de calculadora baseados em 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, permitindo que eles aproveitem recursos específicos da plataforma quando necessário.

A compatibilidade com 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 que ofereçam suporte ao uso de 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

  • As calculadoras baseadas em GPU podem estar presentes em qualquer parte do gráfico e não necessariamente ser usadas para renderização na tela.
  • A transferência de dados de frames de uma calculadora baseada em GPU para outra precisa ser rápida e não incorrer em operações de cópia caras.
  • A transferência de dados de frames entre a CPU e a GPU precisa ser tão eficiente quanto a plataforma permite.
  • Como plataformas diferentes podem exigir técnicas distintas para garantir o melhor desempenho, a API deve permitir flexibilidade na forma como as coisas são implementadas nos bastidores.
  • A calculadora deve ter flexibilidade máxima no uso da GPU para toda a operação ou parte dela, combinando-a com a CPU, se necessário.

Suporte ao 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 o Metal no iOS.

O OpenGL ES 3.1 ou uma versão mais recente (em sistemas Android/Linux) é necessário para a execução gráficos e calculadoras de inferência de machine learning.

O MediaPipe permite que gráficos executem OpenGL em vários contextos GL. Por exemplo, este pode ser muito útil em gráficos que combinam um caminho de inferência de GPU mais lento (por exemplo, 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 comandos sequenciais que usa o mesmo contexto para reduziria o frame rate de renderização.

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

Um contexto do OpenGL não pode ser acessado por várias linhas de execução ao mesmo tempo. Além disso, alternar o contexto GL ativo na mesma linha de execução pode demorar em alguns dispositivos Android. Portanto, nossa abordagem é ter um tópico dedicado por contexto. Cada linha de execução emite comandos GL, criando uma fila de comandos seriais no contexto, que é executado pela GPU de forma assíncrona.

Vida útil de uma calculadora GPU

Esta seção apresenta a estrutura básica do método Process de uma GPU calculadora derivada da classe base GlSimpleCalculator. A calculadora da GPU LuminanceCalculator é mostrado como exemplo. Método 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 no design a seguir opções de suporte a GPUs do MediaPipe:

  • Temos um tipo de dados de GPU, chamado GpuBuffer, para representar dados de imagem, otimizados para uso da GPU. O conteúdo exato desse tipo de dados é opaco e específico da plataforma.
  • Uma API de baixo nível com base na composição, em que qualquer calculadora que queira usar a GPU cria e tem uma instância da classe GlCalculatorHelper. Esta 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 filtros de imagem são subclasses de GlSimpleCalculator e só precisam substituir alguns métodos virtuais pelo código OpenGL específico, enquanto a superclasse cuida de todo o processo.
  • Os dados que precisam ser compartilhados entre todas as calculadoras baseadas em GPU são fornecidos como uma entrada externa que é implementada como um serviço gráfico e é gerenciada pela classe GlCalculatorHelper.
  • A combinação de auxiliares específicos de calculadora e um serviço de gráfico compartilhado permite uma grande flexibilidade no gerenciamento do recurso da GPU: podemos ter um contexto separado por calculadora, compartilhar um único contexto, compartilhar um bloqueio ou outras primitivas de sincronização etc. E tudo isso é gerenciado pelo assistente e oculto das calculadoras individuais.

Conversores de GpuBuffer para ImageFrame

Fornecemos duas calculadoras chamadas GpuBufferToImageFrameCalculator e ImageFrameToGpuBufferCalculator. Essas calculadoras fazem a conversão 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, elas 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, passa por 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. 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 enviados ao gráfico como pacotes GpuBuffer. A o fluxo de entrada é acessado por duas calculadoras em paralelo. GpuBufferToImageFrameCalculator converte o buffer em um ImageFrame, que é enviada por um conversor de escala de cinza e por um filtro Canny (ambos com base no OpenCV e executados na CPU), cuja saída é convertida em um GpuBuffer novamente. Uma calculadora de GPU de várias entradas, GlOverlayCalculator, usa como insira o GpuBuffer original e o que sai do detector de borda. e as sobrepõe usando um sombreador. A saída é enviada de volta usando uma calculadora de callback, e o aplicativo renderiza a imagem à tela usando o OpenGL.