WebGPU 与计算加速初探

avatar
FE @字节跳动

什么是 WebGPU

WebGPU 是一套全新的 Web API 标准,旨在提供高性能 3D 图形和数据并行计算能力,暴露了一系列现代 GPU 能力。基于成熟的 Vulkan、Metal 等标准提供了跨平台的高性能图形接口。

为什么需要 WebGPU

WebGL 的发展

WebGL 1.0 有着良好的浏览器兼容性,但是由于 OpenGL ES 2.0,所以相比成熟的图形应用能力还是不足。而 WebGL 2.0 标准则基于 OpenGL ES 3.0,但由于各种原因,一直难以推行,Safari 直到最近才支持这一特性。

WebGL 2.0 也尝试引入 computing 相关特性,但这次尝试遇到了很多困难,目前该标准制定者也将注意力放在了 WebGPU 的 compute shader 上。

虽然 WebGL 已经是一个成熟的图形标准,并且已经有能力在 web 上实现各种复杂的图形应用和创意,但 OpenGL 本身已经跟不上时代的发展,目前 DirectX 12、Vulkan、Metal 作为现代图形框架,已经不满足于图形渲染绘制的固定管线,而是提供丰富的可编程能力来释放 GPU 的可能性,在这样的趋势下 WebGPU 顺应被提出了。

WebGPU vs WebGL

减少 CPU 开销

  • WebGPU 面向现代 GPU 框架
  • WebGPU 不依赖 canvas 上下文
  • 减少错误校验开销

Pipeline State Object

PSO 是来自 Vulkan 的概念,通过对于不同的状态设置不同的 pipeline 减少过程中切换状态的开销

CommandBuffer

  • WebGPU 有更好的多线程支持,WebGL 虽然可以通过 OffscreenCanvas 来实现多线程,但依然有全局状态机的限制

Compute Shader

Benchmark

WebGPU 的应用

高质量渲染和光线追踪

image.png 加速机器学习等通用计算

视频图像编解码等能力

image.png

WebGPU 现状

浏览器实现

  • WebGPU 在 Chrome 94 中实验性支持,预计于 Chrome 102 shipping,可以通过 #enable-unsafe-webgpu flag 开启
  • WebGPU 在 Firefox 和 Safari 中也开启了实验性支持,但相对来说 Chrome 支持特性更多也更及时
  • Chrome 中的实现基于 Dawn,Firefox 中的实现基于 wgpu

框架支持

  • babylonjs 和 threejs 均对 WebGPU 提供了实验性支持

着色器语言

从 2018 开始 W3C 工作组着手制定相关标准,直到现在现代浏览器都已经提供了实验性支持,具体支持情况和开启方法可以在 Implementation Status 中看到。

在有一段时间中,着色器语言的选择出现了分裂的意见,Google 为代表支持使用 GLSL 4.5,同时需要先经过编译产生二进制着色器语言再进行使用。而 Apple 则建议在其他语言基础之上设计一个新的着色器语言。所以一段时间里出现了两种实现,在 Chrome Canary 和 Safari Technology Preview 中需要两种不同的使用方式。

最终 WGSL 的提出解决了这一问题,重新设计了一套服务于 WebGPU 的着色器语言,现在也被各大浏览器实现。

WebGPU 体验

基本使用

总体流程

绘制一个平面的三角形和正方形需要做的事

CPU

  • 初始化 WebGPU
  • 设置渲染流程
  • 将定点位置和索引信息传入 buffer
  • 将透视矩阵等信息传入 buffer

GPU

  • Vertex Shader
  • Fragment Shader

开启 WebGPU

yarn add -D @webgpu/types
const entry: GPU = navigator.gpu;
if (!entry) {
    throw new Error('WebGPU is not supported on this browser.');
}

Adapter

An adapter identifies an implementation of WebGPU on the system: both an instance of compute/rendering functionality on the platform underlying a browser, and an instance of a browser’s implementation of WebGPU on top of that functionality.

let adapter = await navigator.gpu.requestAdapter({
    powerPreference: 'high-performance'
});

Device

A device is the logical instantiation of an adapter, through which internal objects are created. It can be shared across multiple agents (e.g. dedicated workers).

this.device = await this.adapter.requestDevice();

Canvas

this.context = this.canvas.getContext('webgpu');
this.context.configure({
    device: this.device,
    format: 'bgra8unorm',
    usage: GPUTextureUsage.RENDER_ATTACHMENT,
});

Command Encoder

this.commandEncoder = this.device.createCommandEncoder();
this.renderPassEncoder =
      this.commandEncoder.beginRenderPass(renderPassDescriptor);
this.renderPassEncoder.setViewport(...);

Render Pipeline

createRenderPipeline({
      layout,
      vertex,
      fragment,
      primitive: {
        topology: 'triangle-list',
      },
    })

Buffer

const createBuffer = (arr: Float32Array | Uint16Array, usage: number) => {
    let desc = {
        size: (arr.byteLength + 3) & ~3,
        usage,
        mappedAtCreation: true
    };
    let buffer = device.createBuffer(desc);
    const writeArray =
        arr instanceof Uint16Array
            ? new Uint16Array(buffer.getMappedRange())
            : new Float32Array(buffer.getMappedRange());
    writeArray.set(arr);
    buffer.unmap();
    return buffer;
};

Shader

// vertex shader
export default `
[[block]]struct Uniforms {
  [[size(64)]]uPMatrix: mat4x4<f32>;
  [[size(64)]]uMVMatrix: mat4x4<f32>;
};
[[group(0), binding(0)]]
var<uniform> uniforms: Uniforms;
[[stage(vertex)]]
fn main (
  [[location(0)]] aVertexPosition : vec3<f32>
) -> [[builtin(position)]] vec4<f32> {
  return uniforms.uPMatrix * uniforms.uMVMatrix * vec4<f32>(aVertexPosition, 1.0);
}`;
// fragment shader
export default `
[[stage(fragment)]]
fn main() -> [[location(0)]] vec4<f32> {
    return vec4<f32>(1.0, 1.0, 1.0, 1.0);
}`;

使用框架

babylon 中使用

const engine = new BABYLON.WebGPUEngine(canvas);
await engine.initAsync();

three 中使用

renderer = new WebGPURenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

tfjs 中使用

import * as tf from '@tensorflow/tfjs-backend-webgpu';
    
await tf.ready();
tf.matMul(a, b).print(); 

什么是 GPGPU

A General-Purpose Graphics Processing Unit(GPGPU)即图形处理器通用计算,最早 GPU 被设计为用于游戏等目的,后来在各个场景下 GPU 呈现出比 CPU 更好的效果,尤其是对于计算密集型任务,在 GPU 上进行更多的通用计算任务也成为一种趋势。

在视频编解码、深度学习、数值分析等场景 GPU 的通用计算能力都能起到性能提升的效果,比如通过 cuda 和 OpenCL 加速模型训练过程。

为什么 GPU 适合复杂计算

为什么可以通过 GPU 加速各种通用计算呢,这是由于 GPU 和 CPU 不同的设计决定的。

CPU 本身需要应对各种通用的场景,所以需要额外的空间来进行逻辑判断与分支跳转功能,同时也需要复杂的 ALU 的设计来满足这种需求。为了降低时延,需要巨大的缓存来减少数据通信的时间。

而 GPU 适用于计算密集型任务和适合并行的任务。它的架构中有大量的 ALU,而缓存和控制器在其中则占有比较小的位置。虽然访问 DRAM 获取数据会造成一定的时延,但由于其吞吐量可以同时分配多个线程来进行大规模并行运算。

当然,由于其不具备适用于一般计算任务的能力,所以对于处理的任务也有严格的要求,需要统一无依赖的数据和适用于并行的计算流程。而且,GPU 任务也无法独立于 CPU 运行,之间通信的开销也导致很多任务达不到理论加速效果。所以,在恰当的场景使用 GPU 的并发计算优势,综合 CPU 和 GPU 的优势才能更好的完成任务。

在 web 中应用 GPGPU

web 中也有很多场景满足这种复杂计算的需求,可以利用 GPU 进行性能优化。其中 GPU.jstfjs 等都通过借助 GPU 加速计算任务起到在 web 中进行复杂计算的效果。

在之前的 WebGL 中并没有 Compute Shader 的支持,所以都是通过常规渲染管线来模拟这种过程。

同时通过这种模拟来实现 Compute Shader 也没办法利用 GPU 很多特性如 Share Memory。

GPGPU 应用

矩阵乘法

gpu.createKernel(function(a, b) {
        let sum = 0;
        for (let i = 0; i < 512; i++) {
            sum += a[this.thread.y][i] * b[i][this.thread.x];
        }
        return sum;
    }).setOutput([512, 512])

数据求和

通过 shared memory 更灵活的进行计算操作

算法加速

  • 布局、图论、碰撞等算法

  • 模糊、滤镜、混合等

计算加速方案

  • WASM
  • Web Worker
  • WebGPU / WebGL

WebGPU 的未来

  • 暴露更多的本地 GPU 能力,甚至是特定 GPU 的特性
  • 与 WebXR 等浏览器技术相结合
  • 实时渲染、光线追踪等更复杂的图形学技术可以被应用
  • 通过 Compute Shader 和多线程架构支持更多计算密集应用