GPU

Panoramica

MediaPipe supporta i nodi delle calcolatrici per il calcolo e il rendering della GPU e consente di combinare più nodi GPU, nonché di mischiarli con nodi di calcolo basati su CPU. Esistono diverse API GPU sulle piattaforme mobili (ad esempio OpenGL ES, Metal e Vulkan). MediaPipe non cerca di offrire una singola astrazione GPU tra API. I singoli nodi possono essere scritti utilizzando API diverse, consentendo loro di sfruttare le funzionalità specifiche della piattaforma quando necessario.

Il supporto della GPU è essenziale per ottenere buone prestazioni sulle piattaforme mobile, soprattutto per i video in tempo reale. MediaPipe consente agli sviluppatori di scrivere calcolatrici compatibili con GPU che supportino l'uso della GPU per:

  • Elaborazione in tempo reale sul dispositivo, non solo elaborazione batch
  • Rendering ed effetti video, non solo analisi

Di seguito sono riportati i principi di progettazione per il supporto delle GPU in MediaPipe

  • I calcolatori basati su GPU devono essere presenti ovunque nel grafico e non devono essere necessariamente utilizzati per il rendering sullo schermo.
  • Il trasferimento dei dati dei frame da un calcolatore basato su GPU a un altro dovrebbe essere veloce e non comportare costose operazioni di copia.
  • Il trasferimento di dati dei frame tra CPU e GPU deve essere efficiente quanto consente la piattaforma.
  • Poiché piattaforme diverse possono richiedere tecniche diverse per ottenere le migliori prestazioni, l'API dovrebbe consentire flessibilità nel modo in cui le cose vengono implementate dietro le quinte.
  • Una calcolatrice dovrebbe avere la massima flessibilità nell'utilizzo della GPU per tutte o parte delle sue operazioni, combinandola con la CPU se necessario.

Supporto OpenGL ES

MediaPipe supporta OpenGL ES fino alla versione 3.2 su Android/Linux e fino a ES 3.0 su iOS. MediaPipe supporta anche Metal su iOS.

Per l'esecuzione di calcolatrici e grafici di inferenza del machine learning, è richiesto OpenGL ES 3.1 o versioni successive (su sistemi Android/Linux).

MediaPipe consente ai grafici di eseguire OpenGL in più contesti GL. Ad esempio, questo può essere molto utile nei grafici che combinano un percorso di inferenza della GPU più lento (ad esempio a 10 f/s) con un percorso di rendering della GPU più veloce (ad es. a 30 f/s): poiché un contesto GL corrisponde a una coda di comando sequenziale, l'uso dello stesso contesto per entrambe le attività ridurrebbe la frequenza fotogrammi di rendering.

Una sfida che l'uso di più contesti da parte di MediaPipe risolve è la capacità di comunicare tra loro. Uno scenario di esempio è uno con un video di input che viene inviato sia al percorso di rendering che a quello di inferenza e che il rendering deve avere accesso all'output più recente dall'inferenza.

Non è possibile accedere a un contesto OpenGL per più thread contemporaneamente. Inoltre, il cambio di contesto GL attivo sullo stesso thread può essere lento su alcuni dispositivi Android. Pertanto, il nostro approccio è avere un thread dedicato per ogni contesto. Ogni thread invia comandi GL, creando una coda di comandi seriali sul suo contesto, che viene quindi eseguita dalla GPU in modo asincrono.

Durata di una calcolatrice GPU

Questa sezione presenta la struttura di base del metodo Process di una calcolatrice GPU derivata dalla classe base GlSimpleCalculator. Il calcolatore GPU LuminanceCalculator è mostrato come esempio. Il metodo LuminanceCalculator::GlRender viene chiamato da 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();
}

I principi di progettazione menzionati sopra hanno portato alle seguenti scelte di progettazione per il supporto delle GPU MediaPipe:

  • Abbiamo un tipo di dati GPU, chiamato GpuBuffer, per rappresentare i dati delle immagini, ottimizzato per l'utilizzo della GPU. I contenuti esatti di questo tipo di dati sono opachi e specifici della piattaforma.
  • Un'API di basso livello basata sulla composizione, in cui qualsiasi calcolatore che vuole utilizzare la GPU crea e possiede un'istanza della classe GlCalculatorHelper. Questa classe offre un'API indipendente dalla piattaforma per gestire il contesto OpenGL, configurare le texture per input e output e così via.
  • Un'API di alto livello basata su sottoclassi, in cui semplici calcolatrici che implementano filtri di immagini sottoclasse GlSimpleCalculator devono sostituire solo un paio di metodi virtuali con il loro codice OpenGL specifico, mentre la superclasse si occupa di tutti gli impianti idraulici.
  • I dati che devono essere condivisi tra tutti i calcolatori basati su GPU vengono forniti come input esterno che viene implementato come servizio di grafici e gestito dalla classe GlCalculatorHelper.
  • La combinazione di aiutanti specifici della calcolatrice e un servizio di grafici condiviso ci offre una grande flessibilità nella gestione della risorsa GPU: possiamo avere un contesto separato per ogni calcolatrice, condividere un contesto singolo, condividere un blocco o altre primitive di sincronizzazione e così via. Tutto questo è gestito dall'assistente e nascosto ai singoli calcolatori.

Convertitori da GpuBuffer a ImageFrame

Forniamo due calcolatori chiamati GpuBufferToImageFrameCalculator e ImageFrameToGpuBufferCalculator. Queste calcolatrici convertono da ImageFrame a GpuBuffer, consentendo la creazione di grafici che combinano calcolatrici GPU e CPU. Sono supportate sia su iOS che su Android

Quando possibile, queste calcolatrici utilizzano funzionalità specifiche della piattaforma per condividere i dati tra la CPU e la GPU senza copiare.

Il seguente diagramma mostra il flusso di dati in un'applicazione mobile che acquisisce il video dalla videocamera, li esegue tramite un grafico MediaPipe ed esegue il rendering dell'output sullo schermo in tempo reale. La linea tratteggiata indica le parti appropriate all'interno del grafico MediaPipe. Questa applicazione esegue un filtro per il rilevamento dei bordi Canny sulla CPU utilizzando OpenCV e lo sovrappone al video originale utilizzando la GPU.

Come interagiscono le calcolatrici GPU

I fotogrammi video della fotocamera vengono inviati al grafico come GpuBuffer pacchetti. Due calcolatrici in parallelo accedono al flusso di input. GpuBufferToImageFrameCalculator converte il buffer in un ImageFrame, che viene quindi inviato attraverso un convertitore in scala di grigi e un filtro canny (entrambi basati su OpenCV e in esecuzione sulla CPU), il cui output viene quindi convertito nuovamente in GpuBuffer. Una calcolatrice GPU multi-input, GlOverlayCalculator, prende come input sia l'oggetto GpuBuffer originale sia quello in uscita dal rilevatore dei bordi e li sovrappone utilizzando uno Shader. L'output viene quindi inviato nuovamente all'applicazione utilizzando una calcolatrice di callback, che quindi esegue il rendering dell'immagine sullo schermo utilizzando OpenGL.