GPU

Panoramica

MediaPipe supporta nodi calcolatrice per il calcolo e il rendering GPU e consente di combinare più nodi GPU e di combinarli con nodi di calcoli basati su CPU. Esistono diverse API GPU sulle piattaforme mobile (ad es. OpenGL ES, Metal e Vulkan). MediaPipe non tenta di offrire una singola astrazione GPU tra API. I singoli nodi possono essere scritti utilizzando API diverse, consentendo loro di sfruttare caratteristiche specifiche della piattaforma quando necessario.

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

  • Elaborazione in tempo reale sul dispositivo, non solo elaborazione batch
  • Effetti e rendering video, non solo analisi

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

  • Le calcolatrici basate su GPU dovrebbero essere presenti in qualsiasi punto del grafico e non essere necessariamente utilizzate per il rendering sullo schermo.
  • Il trasferimento dei dati dei frame da una calcolatrice basata su GPU a un'altra dovrebbe essere veloce e non comportare costose operazioni di copia.
  • Il trasferimento di dati di frame tra CPU e GPU dovrebbe essere efficiente quanto consentito dalla piattaforma.
  • Poiché piattaforme diverse possono richiedere tecniche diverse per ottenere prestazioni ottimali, l'API dovrebbe consentire flessibilità nel modo in cui le cose vengono implementate in background.
  • Un calcolatore deve avere la massima flessibilità nell'utilizzo della GPU per tutte o parte delle sue operazioni, combinandola se necessario con la CPU.

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 inoltre Metal su iOS.

Per l'esecuzione è richiesta OpenGL ES 3.1 o versione successiva (su sistemi Android/Linux) calcolatrici e grafici di inferenza per il machine learning.

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 GPU più lento (ad es. a 10 f/s) con un percorso di rendering GPU più veloce (ad es. a 30 f/s): dato che un contesto GL corrisponde a una coda di comandi sequenziale, utilizzando lo stesso contesto riduce la frequenza fotogrammi del rendering.

Una delle sfide dell'uso di più contesti da parte di MediaPipe è la capacità di possono comunicare tra loro. Uno scenario di esempio è quello con un video in ingresso inviati sia ai percorsi di rendering che di inferenze, e il rendering deve avere all'output più recente dall'inferenza.

Non è possibile accedere a un contesto OpenGL da più thread contemporaneamente. Inoltre, il passaggio del contesto GL attivo sullo stesso thread può essere lento su alcuni dispositivi Android. Il nostro approccio è quindi quello di creare un thread dedicato per contesto. Ogni thread emette comandi GL, creando una coda di comandi seriali al suo contesto, che viene poi eseguito dalla GPU in modo asincrono.

Durata di una calcolatrice GPU

Questa sezione illustra la struttura di base del metodo di processo di una GPU calcolatrice derivata dalla classe base GlSimpleCalculator. Il calcolatore GPU LuminanceCalculator è mostrato come esempio. Il metodo LuminanceCalculator::GlRender è stato 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 citati in precedenza hanno portato alla seguente progettazione scelte per il supporto GPU MediaPipe:

  • È disponibile un tipo di dati GPU, chiamato GpuBuffer, per la rappresentazione dei dati immagine, ottimizzato per l'utilizzo delle 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 voglia utilizzare la GPU crea e possiede un'istanza della classe GlCalculatorHelper. Questo corso offre un'API indipendente dalla piattaforma per gestire il contesto OpenGL, impostare le texture per gli input e gli output e così via.
  • Un'API di alto livello basata sulla creazione di sottoclassi, in cui semplici calcolatrici implementano la sottoclasse dei filtri immagine da GlSimpleCalculator e devono eseguire solo l'override di un paio di metodi virtuali con il loro codice OpenGL specifico, mentre la superclasse si occupa di tutti i lavori idraulici.
  • I dati che devono essere condivisi tra tutte le calcolatrici basate su GPU vengono forniti come input esterno implementato come servizio di grafici e gestito dalla classe GlCalculatorHelper.
  • La combinazione di helper specifici del calcolatore e un servizio di grafici condivisi ci offre una grande flessibilità nella gestione delle risorse GPU: possiamo avere un contesto separato per ogni calcolatore, condividere un singolo contesto, condividere un blocco o altre primitive di sincronizzazione e così via. Tutto questo è gestito dall'helper e nascosto dai singoli calcolatori.

Convertitori da GpuBuffer a ImageFrame

Forniamo due calcolatori chiamati GpuBufferToImageFrameCalculator e ImageFrameToGpuBufferCalculator. Questi calcolatori convertono tra ImageFrame e GpuBuffer, consentendo la creazione di grafici che combinano calcolatori GPU e CPU. Sono supportati sia da iOS che da Android

Se possibile, questi calcolatori utilizzano funzionalità specifiche della piattaforma per condividere i dati tra CPU e GPU senza copiarli.

Il diagramma seguente mostra il flusso di dati in un'applicazione mobile che acquisisce il video dalla videocamera, lo esegue in un grafico MediaPipe e ne esegue il rendering sullo schermo in tempo reale. La linea tratteggiata indica quali parti si trovano all'interno del grafico MediaPipe corretto. Questa applicazione esegue un filtro di rilevamento dei bordi di Canny sulla CPU utilizzando OpenCV e lo sovrappone al video originale con la GPU.

Come interagiscono i calcolatori GPU

I fotogrammi video della videocamera vengono inseriti nel grafico come GpuBuffer pacchetti. La flusso di input è accessibile da due calcolatrici in parallelo. GpuBufferToImageFrameCalculator converte il buffer in un ImageFrame, che viene quindi inviato tramite 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 GpuBuffer di nuovo. GlOverlayCalculator, una calcolatrice GPU con più input, prende come inserisci sia l'elemento GpuBuffer originale sia quello che esce dal rilevatore di bordi, e le sovrappone utilizzando uno shar. L'output viene quindi inviato utilizzando una calcolatrice di callback e l'applicazione esegue il rendering dell'immagine sullo schermo usando OpenGL.