GPU

개요

MediaPipe는 GPU 컴퓨팅 및 렌더링을 위한 계산기 노드를 지원하며 여러 GPU 노드를 결합하거나 이를 CPU 기반 계산기 노드와 함께 사용할 수 있습니다. 모바일 플랫폼에는 여러 가지 GPU API가 있습니다 (예: OpenGL ES, Metal, Vulkan). MediaPipe는 단일 교차 API GPU 추상화를 제공하려고 시도하지 않습니다. 개별 노드는 다양한 API를 사용하여 작성할 수 있으므로 필요할 때 플랫폼별 기능을 활용할 수 있습니다.

GPU 지원은 모바일 플랫폼, 특히 실시간 동영상에서 우수한 성능을 제공하는 데 필수적입니다. MediaPipe를 사용하면 개발자는 다음과 같은 경우에 GPU 사용을 지원하는 GPU 호환 계산기를 작성할 수 있습니다.

  • 일괄 처리만이 아닌 기기 내 실시간 처리
  • 단순한 분석이 아닌 동영상 렌더링 및 효과

다음은 MediaPipe의 GPU 지원을 위한 설계 원칙입니다.

  • GPU 기반 계산기는 그래프의 어디에서나 사용할 수 있어야 하며 화면 렌더링에 반드시 사용되는 것은 아닙니다.
  • 하나의 GPU 기반 계산기에서 다른 계산기로 프레임 데이터를 전송하는 속도가 빠르며, 비용이 많이 드는 복사 작업이 발생하지 않습니다.
  • CPU와 GPU 간의 프레임 데이터 전송은 플랫폼에서 허용하는 한 효율적이어야 합니다.
  • 다양한 플랫폼에서 최상의 성능을 위해 서로 다른 기술이 필요할 수 있으므로 API는 백그라운드에서 구현되는 방식의 유연성을 허용해야 합니다.
  • 계산기는 연산의 전체 또는 일부에 GPU를 사용하고 필요한 경우 CPU와 결합할 수 있는 유연성을 최대한 확보해야 합니다.

OpenGL ES 지원

MediaPipe는 OpenGL ES를 Android/Linux에서 버전 3.2까지, iOS에서 ES 3.0까지 지원합니다. 또한 MediaPipe는 iOS에서 Metal도 지원합니다.

머신러닝 추론 계산기와 그래프를 실행하려면 OpenGL ES 3.1 이상 (Android/Linux 시스템)이 필요합니다.

MediaPipe를 사용하면 그래프가 여러 GL 컨텍스트에서 OpenGL을 실행할 수 있습니다. 예를 들어, 이는 느린 GPU 추론 경로 (예: 10FPS)와 더 빠른 GPU 렌더링 경로 (예: 30FPS)를 결합하는 그래프에서 매우 유용할 수 있습니다. 하나의 GL 컨텍스트가 하나의 순차 명령어 큐에 상응하므로 두 작업에 동일한 컨텍스트를 사용하면 렌더링 프레임 속도가 줄어듭니다.

MediaPipe가 여러 컨텍스트를 사용하여 해결할 수 있는 한 가지 문제는 컨텍스트 간 통신 능력입니다. 예시 시나리오는 렌더링과 추론 경로 모두로 전송되는 입력 동영상이 있으며 렌더링이 추론의 최신 출력에 액세스할 수 있어야 하는 시나리오입니다.

OpenGL 컨텍스트는 동시에 여러 스레드에서 액세스할 수 없습니다. 또한 일부 Android 기기에서는 동일한 스레드에서 활성 GL 컨텍스트를 전환하는 속도가 느릴 수 있습니다. 따라서 컨텍스트당 하나의 전용 스레드를 사용하는 것이 Google의 접근 방식입니다. 각 스레드는 GL 명령어를 실행하여 컨텍스트에 직렬 명령어 큐를 구축합니다. 그러면 GPU에서 비동기식으로 실행됩니다.

GPU 계산기의 수명

이 섹션에서는 기본 클래스 GlSimpleCalculator에서 파생된 GPU 계산기의 Process 메서드 기본 구조를 보여줍니다. GPU 계산기 LuminanceCalculator를 예로 들 수 있습니다. LuminanceCalculator::GlRender 메서드는 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();
}

위에서 언급한 설계 원칙에 따라 MediaPipe GPU 지원을 위한 다음과 같은 디자인 옵션이 나왔습니다.

  • GpuBuffer라는 GPU 데이터 유형이 있습니다. 이 데이터 유형은 GPU 사용에 최적화된 이미지 데이터를 나타냅니다. 이 데이터 유형의 정확한 콘텐츠는 불투명하며 플랫폼에 따라 다릅니다.
  • 컴포지션 기반의 하위 수준 API로, GPU를 활용하려는 모든 계산기가 GlCalculatorHelper 클래스의 인스턴스를 만들고 소유합니다. 이 클래스는 OpenGL 컨텍스트를 관리하고 입력 및 출력의 텍스처를 설정하는 등의 작업을 위해 플랫폼에 구애받지 않는 API를 제공합니다.
  • GlSimpleCalculator의 이미지 필터 서브클래스를 구현하는 간단한 계산기가 특정 OpenGL 코드로 몇 가지 가상 메서드만 재정의하면 되는 서브클래스 기반의 상위 수준 API입니다. 슈퍼클래스가 모든 배관을 처리합니다.
  • 모든 GPU 기반 계산기 간에 공유해야 하는 데이터는 그래프 서비스로 구현되고 GlCalculatorHelper 클래스로 관리되는 외부 입력으로 제공됩니다.
  • 계산기 전용 도우미와 공유 그래프 서비스를 함께 사용하면 GPU 리소스를 매우 유연하게 관리할 수 있습니다. 계산기마다 별도의 컨텍스트를 공유하고 단일 컨텍스트를 공유하고 잠금 또는 기타 동기화 프리미티브를 공유하는 등의 작업을 할 수 있습니다. 이 모든 작업은 도우미에 의해 관리되며 개별 계산기에 표시되지 않습니다.

GpuBuffer-ImageFrame 변환기

Google에서는 GpuBufferToImageFrameCalculatorImageFrameToGpuBufferCalculator라는 두 가지 계산기를 제공합니다. 이 계산기는 ImageFrame에서 GpuBuffer로 변환되므로 GPU와 CPU 계산기를 결합한 그래프를 만들 수 있습니다. iOS와 Android에서 모두 지원됩니다.

가능한 경우 이러한 계산기는 플랫폼별 기능을 사용하여 복사 없이 CPU와 GPU 간에 데이터를 공유합니다.

아래 다이어그램은 카메라에서 동영상을 캡처하고 MediaPipe 그래프를 통해 실행한 다음 화면에 출력을 실시간으로 렌더링하는 모바일 애플리케이션의 데이터 흐름을 보여줍니다. 파선은 MediaPipe 그래프 내에 있는 부분을 나타냅니다. 이 애플리케이션은 OpenCV를 사용하여 CPU에서 Canny 에지 감지 필터를 실행하고 GPU를 사용하여 원본 동영상 위에 오버레이합니다.

GPU 계산기의 상호작용 방식

카메라의 동영상 프레임은 GpuBuffer 패킷으로 그래프에 제공됩니다. 입력 스트림에는 두 개의 계산기가 동시에 액세스할 수 있습니다. GpuBufferToImageFrameCalculator는 버퍼를 ImageFrame로 변환하고 그레이스케일 변환기와 캐니 필터 (OpenCV 기반 및 CPU에서 실행)를 통해 전송합니다. 그러면 출력은 다시 GpuBuffer로 변환됩니다. 다중 입력 GPU 계산기인 GlOverlayCalculator는 원본 GpuBuffer와 에지 감지기에서 나온 값을 모두 입력으로 사용하고 셰이더를 사용하여 오버레이합니다. 그런 다음 콜백 계산기를 사용하여 출력이 애플리케이션으로 다시 전송되고 애플리케이션이 OpenGL을 사용하여 이미지를 화면에 렌더링합니다.