GPU

概览

MediaPipe 支持用于 GPU 计算和渲染的计算器节点,并允许组合多个 GPU 节点,以及将它们与基于 CPU 的计算器节点混合。移动平台上有几个 GPU API(例如,OpenGL ES、Metal 和 Vulkan)。MediaPipe 不会尝试提供单一的跨 API GPU 抽象。单个节点可以使用不同的 API 进行编写,以便它们能够在需要时利用平台特有功能。

GPU 支持对于在移动平台上获得良好性能至关重要,尤其是对于实时视频。借助 MediaPipe,开发者可以编写与 GPU 兼容的计算器,以支持将 GPU 用于以下用途:

  • 设备端实时处理,而不仅仅是批处理
  • 视频呈现和效果,而不仅仅是分析

以下是 MediaPipe 中 GPU 支持的设计原则

  • 基于 GPU 的计算器应该可以在图表中的任何位置出现,不一定用于在屏幕上呈现。
  • 帧数据从一个基于 GPU 的计算器到另一个基于 GPU 的计算器应该能够快速传输,而且不会产生高昂的复制操作费用。
  • 在 CPU 和 GPU 之间传输帧数据的效率应尽可能高。
  • 由于不同平台可能需要不同的技术才能获得最佳性能,因此该 API 应允许在后台实现内容的方式灵活性。
  • 计算器应尽可能灵活地使用 GPU 执行全部或部分操作,并在必要时将其与 CPU 结合使用。

OpenGL ES 支持

MediaPipe 在 Android/Linux 上支持最高版本为 3.2 的 OpenGL ES,最高版本为 ES 3.0 。此外,MediaPipe 还支持 iOS 上的 Metal。

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

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

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

多个线程无法同时访问 OpenGL 上下文。 此外,在同一线程上切换活跃 GL 上下文也会很慢, 部分 Android 设备因此,我们的方法是使用一个专用线程, 。每个线程都会发出 GL 命令,从而构建串行命令队列 然后由 GPU 异步执行。

GPU 计算器的生命周期

本部分介绍了 GPU 的 Process 方法的基本结构 派生自基类 GlSimpleCalculator 的计算器。GPU 计算器 以 LuminanceCalculator 为例。方法 从 GlSimpleCalculator::Process 调用 LuminanceCalculator::GlRender

// 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 过滤器(两者都是基于 并在 CPU 上运行),其输出随后会转换为 GpuBuffer。多输入 GPU 计算器 GlOverlayCalculator, 输入原始 GpuBuffer 和来自边缘检测器的另一个, 并使用着色器叠加它们。随后,系统会将输出发送回 以及使用回调计算器来呈现图片 传递给屏幕