原文地址:https://developers.google.cn/web/updates/2019/08/get-started-with-gpu-compute-on-the-web
Get started with GPU Compute on the Web
背景
也许你已经知道,计算机的子系统图像处理单元(Graphic Processing Unit, GPU)原来是被用作处理图像的。然而,在过去的10年,GPU已经朝着更加灵活的架构发展,利用GPU的架构,开发者可以实现各种算法,而不仅仅是用来渲染3D图形。这种能力被称为GPU计算,并且这种使用GPU作为协处理器进行多用途的科学计算被称为多用途GPU编程(general-purpose GPU, GPGPU)
GPU计算对最近兴起的机器学习具有不可磨灭的贡献,神经网络卷积运算和其他模型能工利用GPU架构从而运行的更快。然而GPU运算能力正是Web平台所欠缺的能力,“W3C's GPU for the Web Community Group”组织设计了一套API暴露出现代GPU的API以供现在大部分的设备运行。这套API被称为WebGPU
类似WebGL, WebGPU是一类低层次的API。它的功能十分的强大,但是也是十分冗长、难以理解的。但是这些都没关系,在强大的性能面前这些都不算什么。
本篇文章中,我会聚焦在GPU计算的部分,说实话,我也只是粗略介绍,后面的学习还是要靠你自己了。在后续的文章中,我会深入的介绍WebGPU的渲染能力。
注:WebGPU特性在Chrome78以上的版本中可用,它隐藏在experimental flag中。你必须手动的打开这个特性才能使用。在地址栏中输入chrome://flags/#enable-unsafe-webgpu 打开它。 该API目前还不太安全并且在持续的改进中。WebGPU API的沙箱模型还未完全实现,所以有可能读取到其他线程的GPU数据。当使用这个特性时不要浏览其他web网页!
获取GPU
在WebGPU API 获取GPU非常的简单。调用 navigator.gpu.requestAdapter()
,该方法会返回一个JavaScript的promise对象,想象这个adapter就是显卡。它可能是集成显卡,也可能是独立显卡。
一旦你获取到了GPU Adapter,调用adapter.requestDevice() 来获取一个resolve了GPU device的Promise,使用GPU Device对象你就可以做一些GPU计算了。
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
以上两个函数都有一些选项允许你来制定adapter(性能选项) 和 device(extensions, limits)。简单起见,本篇文章中我们就使用默认的配置。
写buffer
接下来让我看看如何使用JavaScript往GPU的内存中写入数据。这个过程并不是那么的直观,因为在现代web浏览器中使用了沙箱模型。
下面的例子展示了如何往GPU的内存中写入4个字节的数据。
device.createBufferMapped()该函数接受buffer的大小和其用途作为参数。它返回一个GPU buffer对象和其关联的原生Arraybuffer。
如果你使用过Arraybuffer,那么往GPU写入数据与操作Arraybuffer十分类似,使用TypedArray来建立视图,并往其中复制数据。
// Get a GPU buffer in a mapped state and an arraybuffer for writing.
const [gpuBuffer, arrayBuffer] = device.createBufferMapped({
size: 4,
usage: GPUBufferUsage.MAP_WRITE
});
new Uin8Array(arrayBuffer).set([0, 1, 2, 3]);
// Unmap buffer so that is can be used later for copy.
gpuWriteBuffer.unmap();
这个时候,这块GPU buffer已经被映射了。也就是说arrayBuffer这个对象属于GPU,并且我们能通过JavaScript对其进行读写的操作。为了让GPU能够访问这块内存,我们还要解除与gpuBuffer 与 arrayBuffer的映射关系。
mapped / unmapped的概念是非常有必要的,它是为了阻止GPU和CPU在同一时间访问同一块内存形成的竞争。
读Buffer
现在,让我们来看看如何从一块GPU Buffer中复制数据到另一块GPU Buffer中并且读取它。
先前我们已经在一块GPU Buffer中写入了数据,现在我们想要将其中的数据复制到另一块GPU Buffer中,那么需要一个新的flag GPUBufferUsage.COPY_SRC。我们使用device.createBuffer这个同步的API来创建第二块未进行映射的GPU Buffer,当其作为复制的目标并且只读则需要使用GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ标志。
// Get a GPU buffer in a mapped state and an arraybuffer for writing.
const [gpuBuffer, arrayBuffer] = device.createBufferMapped({
size: 4,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC
});
new Uin8Array(arrayBuffer).set([0, 1, 2, 3]);
// Unmap buffer so that is can be used later for copy.
gpuWriteBuffer.unmap();
// Get a GPU buffer for reading in an unmapped state
const gpuReadBuffer = device.createBuffer({
size: 4,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});
由于GPU是一个独立的协处理器,所有的GPU命令执行都是异步的。这就是为什么这里有一个包含了建立、执行GPU命令的列表。在WebGPU中, device.createCommandEncoder()方法返回一个GPU command encoder对象,它可以建立一批的"buffered"命令,然后在同一时刻送往GPU中。另一方面,在GPUBuffer上的方法是“unbuffered”的,意味着他们在被调用时是具有原子性的。
一旦你有了GPU command encoder, 如下面调用copyEncoder.copyBufferToBuffer()方法来添加这个命令到命令队列中以便后续执行。最终,finish encoding命令通过调用copyEncoder.finish()方法来提交这个写明到GPU device command 队列。通过device.defaultQueue.submit()API,这个队列负责处理操作命令的提交任务。所有存储在这个数组里的所有命令都会依次序的原子性执行。
// Encode commands for copying buffer to buffer
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
gpuWriteBuffer /* source buffer*/,
0 /*source offset*/,
gpuReadBuffer /* destination buffer */,
0 /* destination offset*/,
4 /*size*/
);
// Submit copy commands
const copyCommands = copyEncoder.finish();
device.defaultQueue.submit([copyCommands]);
这个时候,GPU 命令队列已经被发送了。但是执行是不必要的。为了读取第二块GPU buffer, 调用gpuReadBuffer.mapReadAsync()这个API,一旦命令队列的所有命令执行完成,它返回resolve了包含与第一块GPU buffer相同数据的ArrayBuffer的Promise对象。
// Read buffer
const copyArrayBuffer = await gpuReadBuffer.mapReadAsync();
console.log(new Uint8Array(copyArrayBuffer));
简单的说,一下就是你需要记住的关于buffer内存的操作要点
- GPU buffer必须要是Unmapped状态在提交命令的时候
- 一旦buffer被映射, GPU buffer就能够被JavaScript读写
- 当
mapReadAsync(),mapWriteAsync(),createBufferMapped()这些API被调用时, GPU buffer就被映射了。
着色器编程 (Shader Programming)
那些在GPU上运行的仅仅执行运算(不绘制三角形)的程序被称为计算着色器(compute shaders)。它们被成百上千的GPU核心并行计算以快速的处理大量数据。其输入与输出在WebGPU中都是buffer的形式。
为了展现计算着色器在WebGPU中的用法,我们以矩阵乘法为例子

简而言之,我们接下来将要做:
- 创建三个GPU buffer (其中两个矩阵用来做乘法,另外一个来存储结果)
- 为计算着色器描述输入和输出
- 编译着色器的代码
- 建立计算管线
- 在同一批提交encoded commands 到GPU
- 读取结果
创建GPU Buffers
为了简单起见,矩阵被表示为一个浮点数的数组。第一个元素表示行数,第二个元素表示列数,剩下的则是矩阵实际的数据。

这三个GPU buffer是storage buffer, 作为在compute shader中我们存储和接受数据的地方。这也解释了为什么GPU buffer usage flags包含了
GPUBufferUsage.STORAGE这个标志。包含结果的矩阵GPUBuffer 有GPUBufferUsage.COPY_SRC这个标志,因为当我们读取数据时它会被拷贝到另一块buffer中 。
。。。未完待续