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 在 Android/Linux 上最高支持 OpenGL ES 3.2 版,在 iOS 上最高支持 ES 3.0。此外,MediaPipe 还支持 iOS 上的 Metal。

(在 Android/Linux 系统上)需要 OpenGL ES 3.1 或更高版本才能运行机器学习推理计算器和图表。

MediaPipe 允许图表在多个 GL 上下文中运行 OpenGL。例如,在结合较慢的 GPU 推理路径(例如,10 FPS)与更快的 GPU 渲染路径(例如,30 FPS)的图中,这非常有用:因为一个 GL 上下文对应于一个顺序命令队列,因此为两个任务使用相同的上下文会降低渲染帧速率。

MediaPipe 使用多个上下文求解的一个挑战是,无法跨上下文进行通信。例如,一个输入视频会同时发送到渲染路径和推理路径,并且渲染需要访问推理的最新输出。

无法同时由多个线程访问 OpenGL 上下文。此外,在某些 Android 设备上,在同一线程上切换活跃 GL 上下文的速度可能很慢。因此,我们的方法是为每个上下文设置一个专用线程。每个线程都会发出 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 类的实例。此类提供了一个不依赖于平台的 API,用于管理 OpenGL 上下文、为输入和输出设置纹理等。
  • 一个基于子类化的高级 API,其中简易计算器可以从 GlSimpleCalculator 实现图像滤镜子类,并且只需要使用其特定的 OpenGL 代码替换几个虚拟方法,而父类负责所有管道工作。
  • 需要在所有基于 GPU 的计算器之间共享的数据作为外部输入提供,该外部输入以图表服务的形式实现,并由 GlCalculatorHelper 类管理。
  • 计算器专用帮助程序和共享图表服务的组合使我们能够非常灵活地管理 GPU 资源:我们可以为每个计算器提供单独的上下文,共享单一上下文,共享锁定或其他同步基元等。所有这些都由帮助程序管理,并且隐藏在单个计算器中。

GpuBuffer 到 ImageFrame 转换器

我们提供两个计算器,分别是 GpuBufferToImageFrameCalculatorImageFrameToGpuBufferCalculator。这些计算器可在 ImageFrameGpuBuffer 之间进行转换,从而构建结合了 GPU 和 CPU 计算器的图表。iOS 和 Android 均支持

在可能的情况下,这些计算器会使用针对具体平台的功能在 CPU 和 GPU 之间共享数据,而无需复制。

下图显示了移动应用中的数据流,该数据流从相机捕获视频,通过 MediaPipe 图运行视频,并实时将输出呈现在屏幕上。虚线表示 MediaPipe 图表内部的哪些部分。此应用使用 OpenCV 在 CPU 上运行 Canny 边缘检测过滤器,并使用 GPU 将其覆盖在原始视频上。

GPU 计算器如何交互

来自摄像头的视频帧会作为 GpuBuffer 数据包馈送到图表中。输入流由两个计算器并行访问。GpuBufferToImageFrameCalculator 会将缓冲区转换为 ImageFrame,然后通过灰度转换器和 Canny 过滤器(均基于 OpenCV 以及在 CPU 上运行)发送后者,后者的输出会再次转换为 GpuBuffer。多输入 GPU 计算器 GlOverlayCalculator 将原始 GpuBuffer 和来自边缘检测器的输入都用作输入,并使用着色器对其进行叠加。然后,系统会使用回调计算器将输出发送回应用,而应用会使用 OpenGL 将图片渲染到屏幕上。