区块链技术博客
www.b2bchain.cn

WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘求职学习资料

本文介绍了WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

文章出处:https://web.dev/gpu-compute/

文章发布时间 文章最后更新 翻译时间 翻译人
2019年8月28日 2021年9月6日 2021年9月12日 四季留歌

版权原作者所有。转载翻译稿请带连接与翻译者。

  • 1 背景
  • 2 访问 GPU
  • 3 写入缓存
  • 4 读取缓存
  • 5 着色器编程
  • 6 GPUBuffer 对象的创建
  • 7 绑定组及其布局对象

使用 WebGPU 可以调用 GPU 的并行计算性能。

1 背景

GPU 最初是拿来画图的设备,但是近些年它的并行计算能力却开辟了另一些领域,允许开发人员实现多种类型的算法,而不是仅仅拿来画图。

这种借助 GPU 并行计算能力的编程称为 GPU计算,使用 GPU 作为主要运算处理器的科学计算称为通用 GPU 编程(General-Purpose GPU 编程,GPGPU)。

译者注:多年后,这段文字会不会写入一些本科毕业论文呢?(笑

GPU 计算为机器学习做出了巨大贡献,因为卷积神经网络和其他模型可以利用该架构在 GPU 上高效运行。但是,Web端缺乏 GPU 的计算功能(WebGL很难做),W3C 的 GPU for the Web 社区组正在设计一个 API 来公开大多数现代图形处理器能用的图形编程接口,这个 API 是 WebGPU。

WebGPU 是一个底层 API,类似 WebGL。它的代码量很长,接口粒度很细,不过没关系,我们关注的是性能。

在本文中,作者介绍 WebGPU 中的 GPU 计算部分,仅作为抛砖引玉,希望各位大佬能玩出花样。之后原作者也将考虑写一些 WebGPU 图形渲染的文章。

2 访问 GPU

在 WebGPU API 中访问 GPU 很容易,调用 navigator.gpu.requestAdapter() 会返回一个 Promise,它会 resolve 一个 GPU 适配器(物理显卡)的 adapter 对象。

一般这个适配器对象会是独显,但是有些情况也可以是核芯显卡。

一旦你有了适配器对象,你可以调用 adapter.requestDevice() 来获取一个能 resolve 一个设备对象的 Promise。

适配器和设备这两个对象的区别见官方 Explainer 文档。

const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { return; } const device = await adapter.requestDevice();

上述俩函数都可以传入 option 对象,用来选择适配器类型和指定设备信息。为了简单起见,这里不传递,即使用默认配置。

3 写入缓存

这部分介绍 JavaScript 是如何把数据写入显存的。

下面的例子介绍了如何把 4bytes 数据写入已经访问到的显存里。调用 device.createBuffer() 并传递几个参数:需要多大的显存,这块显存的作用。

默认情况下,即使不明确传递参数 GPUBufferUsage.MAP_WRITE,申请的这块显存就是拿来写入的。由于 mappedAtCreationtrue,它会在创建时映射这块缓存,然后我们就可以在 JavaScript 代码中对返回的 buffer 对象调用 getMappedRange() 来访问关联在一起的二进制数据了。

如果你用过 ArrayBuffer,那么写入字节数据应该不是什么大问题,接下来要用到 TypedArray 来写入缓存。

// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据 const gpuBuffer = device.createBuffer({   mappedAtCreation: true,   size: 4,   usage: GPUBufferUsage.MAP_WRITE }); const arrayBuffer = gpuBuffer.getMappedRange();  // 通过 TypedArray 向 ArrayBuffer 写数据 new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

状态是映射的显存,这意味着这块显存暂时归 CPU 所有,即能使用 JavaScript 读写它。为了让 GPU 能再次访问这块缓存,它必须取消映射:调用 gpuBuffer.unmap() 即可。

显存映射/解除映射这组概念,是用来防止 GPU 和 CPU 同时访问缓存时产生冲突用的。

4 读取缓存

这一节介绍如何将某个 GPU 缓存对象上的数据复制到另一个 GPU 缓存对象(GPUBuffer Object)。

在某个 GPUBuffer 对象上写入了数据,并要复制到另一个,那么就需要一个新的属性 GPUBufferUsage.COPY_SRC。这次,调用 device.createBuffer() 创建一个没有映射的 GPUBuffer 对象,它的 usage 将设为 GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,它用来存放第一个 GPUBuffer 复制过来的数据,并在复制结束后要在 JavaScript 这边再次把数据读取出来。

// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据 const gpuWriteBuffer = device.createBuffer({   mappedAtCreation: true,   size: 4,   usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC // 👈 注意这里 }); const arrayBuffer = gpuWriteBuffer.getMappedRange();  // 通过 TypedArray 向 ArrayBuffer 写数据 new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);  // 解除显存对象的映射,稍后它就能在 GPU 中进行复制操作 gpuWriteBuffer.unmap();  // 创建一个新的状态为未映射的 GPUBuffer,它之后要拿来读取数据 const gpuReadBuffer = device.createBuffer({   size: 4,   usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ // 👈 注意这里 });

由于 GPU 是一个独立于 CPU 的协处理器,所有 GPU 的指令都是异步执行的,这就是为什么要去创建 GPU 指令组,而且在需要的时候成批提交到 GPU 上的原因(GPUCommandEncoder、GPUQueue)。

在 WebGPU 中,通过调用 device.createCommandEncoder() 返回 GPUCommandEncoder 对象,这个对象能构建并暂存一系列的 GPU 指令,然后在某个时刻提交至 GPU。另一方面,GPUBuffer 对象上的方法是不缓存的,意思是你调用的时候就会立即执行。

获取 GPUCommandEncoder 后,你可以用 copyEncoder.copyBufferToBuffer() 将此命令添加到指令队列以供后续的执行;最后调用 copyEncoder.finish() 完成指令的编码,然后调用 device.queue.submit() 来把这些指令提交给 GPU,然后 GPU 将按顺序执行。

// 创建一个名为 copyEncoder 的指令编码器,用来复制显存 const copyEncoder = device.createCommandEncoder(); copyEncoder.copyBufferToBuffer(   gpuWriteBuffer /* 源显存(对象) */,   0 /* 起始字节(从哪开始读) */,   gpuReadBuffer /* 目标显存(对象) */,   0 /* 起始字节(从哪开始写) */,   4 /* 大小 */ );  // 提交我们写好的复制功能的指令 const copyCommands = copyEncoder.finish(); device.queue.submit([copyCommands]);

此时,指令队列已经发送,但是不一定已经执行。想要读取第二个 GPUBuffer 对象上的数据,你要配合参数 GPUMapMode.READ 调用 gpuReadBuffer.mapAsync() 方法,它返回一个 Promise,你可以用 await 语法来获取 resolve 值。这个 Promise 在 GPUBuffer 被再次映射时 resolve。

然后,使用 gpuReadBuffer.getMappedRange() 来获取映射后的数据,一旦提交的指令执行后,我们获取到的这个数据应该会与第一个 GPUBuffer 的数据是一样的。

// 读取缓存 await gpuReadBuffer.mapAsync(GPUMapMode.READ); const copyArrayBuffer = gpuReadBuffer.getMappedRange(); console.log(new Uint8Array(copyArrayBuffer));

你可以到 glitch 上看看 示例代码1.


简而言之,关于 GPUBuffer 对象(显存)的操作,希望读者记住以下几点:

  • GPUBuffer 对象必须取消映射,才能用于提交队列;
  • 一旦 GPUBuffer 对象被映射了,它就能被 JavaScript 读写;
  • 调用 mapAsync()createBuffer() 方法,且将 mappedAtCreation 属性设为 true 时,GPUBuffer 对象会被映射。

5 着色器编程

在 GPU 上运行的计算代码(仅计算,不绘图)称为 计算着色器。它们由数百个 GPU 核心并行执行。它们的输入和输出是 WebGPU 中的缓存器。

为了说明计算着色器在 WebGPU 中的使用,先列举一下矩阵乘法,这是一种机器学习中的常见计算:

WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘

简而言之,我们要做:

① 创建三个 GPU 缓存器(GPUBuffer 对象,两个用于输入矩阵,一个用于保存输出的结果矩阵)

② 描述计算着色器的输入和输出

③ 编译着色器代码

④ 创建并设置计算管线

⑤ 将编码后的命令批量提交给 GPU

⑥ 从结果缓存器中读取结果矩阵

6 GPUBuffer 对象的创建

为了简单起见,这里使用浮点数组表示矩阵,第一、二个数组元素是行数、列数,其余是矩阵的元素值。

WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘

创建三个 usage 带有 GPUBufferUsage.STORAGE 参数的 GPUBuffer 来存储数据,因为在计算着色器中要存数据和读数据。

用于存储结果的 GPUBuffer 要带有 GPUBufferUsage.COPY_SRC,因为一旦所有的 GPU 指令执行完毕后,这个矩阵将复制到这个 GPUBuffer。

大致代码:

const adapter = await navigator.gpu.requestAdapter(); if (!adapter) {    return;  } const device = await adapter.requestDevice();  // 第一个矩阵 const firstMatrix = new Float32Array([   2 /* 2行 */, 4 /* 4列 */,   1, 2, 3, 4,   5, 6, 7, 8 ]); const gpuBufferFirstMatrix = device.createBuffer({   mappedAtCreation: true,   size: firstMatrix.byteLength,   usage: GPUBufferUsage.STORAGE, }); const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange(); new Float32Array(arrayBufferFirstMatrix).set(firstMatrix); gpuBufferFirstMatrix.unmap();  // 第二个矩阵 const secondMatrix = new Float32Array([   4 /* 4行 */, 2 /* 2列 */,   1, 2,   3, 4,   5, 6,   7, 8 ]); const gpuBufferSecondMatrix = device.createBufferMapped({   mappedAtCreation: true,   size: secondMatrix.byteLength,   usage: GPUBufferUsage.STORAGE, }); const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange(); new Float32Array(arrayBufferSecondMatrix).set(secondMatrix); gpuBufferSecondMatrix.unmap();   // 结果矩阵 const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]); const resultMatrixBuffer = device.createBuffer({   size: resultMatrixBufferSize,   usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });

7 绑定组及其布局对象

绑定组及其布局对象是 WebGPU 中特有的概念(接口)。绑定组表示一组要输入到着色器里的数据,譬如你要做一个菜,那一堆配好的食材就可以叫绑定组。绑定组的布局对象就是告诉着色器绑定组对象长什么样子,即你买的食材是什么,量多少。

在下面的示例代码中,布局对象告诉计算着色器,这里有 3 个绑定的资源,分别编号为 0、1、2,0 号和 1 号对应两个只读存储缓存(read-only-storage),2 号对应一个存储缓存(storage)。

绑定组对象和这个布局对象是关联的,在本例中,即三个 GPUBuffer 分别一个萝卜填一个坑, gpuBufferFirstMatrixgpuBufferSecondMatrix 对象对应 0 号和 1 号坑,resultMatrixBuffer 对应 2号坑。

“` js
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “read-only-storage”
}
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “read-only-storage”
}
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “storage”
}
}
]
});

const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: gpuBufferFirstMatrix
}
},
{
binding: 1,
resource: {
buffer: gpuBufferSecondMatrix

文章出处:https://web.dev/gpu-compute/

文章发布时间 文章最后更新 翻译时间 翻译人
2019年8月28日 2021年9月6日 2021年9月12日 四季留歌

版权原作者所有。转载翻译稿请带连接与翻译者。

  • 1 背景
  • 2 访问 GPU
  • 3 写入缓存
  • 4 读取缓存
  • 5 着色器编程
  • 6 GPUBuffer 对象的创建
  • 7 绑定组及其布局对象

使用 WebGPU 可以调用 GPU 的并行计算性能。

1 背景

GPU 最初是拿来画图的设备,但是近些年它的并行计算能力却开辟了另一些领域,允许开发人员实现多种类型的算法,而不是仅仅拿来画图。

这种借助 GPU 并行计算能力的编程称为 GPU计算,使用 GPU 作为主要运算处理器的科学计算称为通用 GPU 编程(General-Purpose GPU 编程,GPGPU)。

译者注:多年后,这段文字会不会写入一些本科毕业论文呢?(笑

GPU 计算为机器学习做出了巨大贡献,因为卷积神经网络和其他模型可以利用该架构在 GPU 上高效运行。但是,Web端缺乏 GPU 的计算功能(WebGL很难做),W3C 的 GPU for the Web 社区组正在设计一个 API 来公开大多数现代图形处理器能用的图形编程接口,这个 API 是 WebGPU。

WebGPU 是一个底层 API,类似 WebGL。它的代码量很长,接口粒度很细,不过没关系,我们关注的是性能。

在本文中,作者介绍 WebGPU 中的 GPU 计算部分,仅作为抛砖引玉,希望各位大佬能玩出花样。之后原作者也将考虑写一些 WebGPU 图形渲染的文章。

2 访问 GPU

在 WebGPU API 中访问 GPU 很容易,调用 navigator.gpu.requestAdapter() 会返回一个 Promise,它会 resolve 一个 GPU 适配器(物理显卡)的 adapter 对象。

一般这个适配器对象会是独显,但是有些情况也可以是核芯显卡。

一旦你有了适配器对象,你可以调用 adapter.requestDevice() 来获取一个能 resolve 一个设备对象的 Promise。

适配器和设备这两个对象的区别见官方 Explainer 文档。

const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { return; } const device = await adapter.requestDevice();

上述俩函数都可以传入 option 对象,用来选择适配器类型和指定设备信息。为了简单起见,这里不传递,即使用默认配置。

3 写入缓存

这部分介绍 JavaScript 是如何把数据写入显存的。

下面的例子介绍了如何把 4bytes 数据写入已经访问到的显存里。调用 device.createBuffer() 并传递几个参数:需要多大的显存,这块显存的作用。

默认情况下,即使不明确传递参数 GPUBufferUsage.MAP_WRITE,申请的这块显存就是拿来写入的。由于 mappedAtCreationtrue,它会在创建时映射这块缓存,然后我们就可以在 JavaScript 代码中对返回的 buffer 对象调用 getMappedRange() 来访问关联在一起的二进制数据了。

如果你用过 ArrayBuffer,那么写入字节数据应该不是什么大问题,接下来要用到 TypedArray 来写入缓存。

// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据 const gpuBuffer = device.createBuffer({   mappedAtCreation: true,   size: 4,   usage: GPUBufferUsage.MAP_WRITE }); const arrayBuffer = gpuBuffer.getMappedRange();  // 通过 TypedArray 向 ArrayBuffer 写数据 new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

状态是映射的显存,这意味着这块显存暂时归 CPU 所有,即能使用 JavaScript 读写它。为了让 GPU 能再次访问这块缓存,它必须取消映射:调用 gpuBuffer.unmap() 即可。

显存映射/解除映射这组概念,是用来防止 GPU 和 CPU 同时访问缓存时产生冲突用的。

4 读取缓存

这一节介绍如何将某个 GPU 缓存对象上的数据复制到另一个 GPU 缓存对象(GPUBuffer Object)。

在某个 GPUBuffer 对象上写入了数据,并要复制到另一个,那么就需要一个新的属性 GPUBufferUsage.COPY_SRC。这次,调用 device.createBuffer() 创建一个没有映射的 GPUBuffer 对象,它的 usage 将设为 GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,它用来存放第一个 GPUBuffer 复制过来的数据,并在复制结束后要在 JavaScript 这边再次把数据读取出来。

// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据 const gpuWriteBuffer = device.createBuffer({   mappedAtCreation: true,   size: 4,   usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC // 👈 注意这里 }); const arrayBuffer = gpuWriteBuffer.getMappedRange();  // 通过 TypedArray 向 ArrayBuffer 写数据 new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);  // 解除显存对象的映射,稍后它就能在 GPU 中进行复制操作 gpuWriteBuffer.unmap();  // 创建一个新的状态为未映射的 GPUBuffer,它之后要拿来读取数据 const gpuReadBuffer = device.createBuffer({   size: 4,   usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ // 👈 注意这里 });

由于 GPU 是一个独立于 CPU 的协处理器,所有 GPU 的指令都是异步执行的,这就是为什么要去创建 GPU 指令组,而且在需要的时候成批提交到 GPU 上的原因(GPUCommandEncoder、GPUQueue)。

在 WebGPU 中,通过调用 device.createCommandEncoder() 返回 GPUCommandEncoder 对象,这个对象能构建并暂存一系列的 GPU 指令,然后在某个时刻提交至 GPU。另一方面,GPUBuffer 对象上的方法是不缓存的,意思是你调用的时候就会立即执行。

获取 GPUCommandEncoder 后,你可以用 copyEncoder.copyBufferToBuffer() 将此命令添加到指令队列以供后续的执行;最后调用 copyEncoder.finish() 完成指令的编码,然后调用 device.queue.submit() 来把这些指令提交给 GPU,然后 GPU 将按顺序执行。

// 创建一个名为 copyEncoder 的指令编码器,用来复制显存 const copyEncoder = device.createCommandEncoder(); copyEncoder.copyBufferToBuffer(   gpuWriteBuffer /* 源显存(对象) */,   0 /* 起始字节(从哪开始读) */,   gpuReadBuffer /* 目标显存(对象) */,   0 /* 起始字节(从哪开始写) */,   4 /* 大小 */ );  // 提交我们写好的复制功能的指令 const copyCommands = copyEncoder.finish(); device.queue.submit([copyCommands]);

此时,指令队列已经发送,但是不一定已经执行。想要读取第二个 GPUBuffer 对象上的数据,你要配合参数 GPUMapMode.READ 调用 gpuReadBuffer.mapAsync() 方法,它返回一个 Promise,你可以用 await 语法来获取 resolve 值。这个 Promise 在 GPUBuffer 被再次映射时 resolve。

然后,使用 gpuReadBuffer.getMappedRange() 来获取映射后的数据,一旦提交的指令执行后,我们获取到的这个数据应该会与第一个 GPUBuffer 的数据是一样的。

// 读取缓存 await gpuReadBuffer.mapAsync(GPUMapMode.READ); const copyArrayBuffer = gpuReadBuffer.getMappedRange(); console.log(new Uint8Array(copyArrayBuffer));

你可以到 glitch 上看看 示例代码1.


简而言之,关于 GPUBuffer 对象(显存)的操作,希望读者记住以下几点:

  • GPUBuffer 对象必须取消映射,才能用于提交队列;
  • 一旦 GPUBuffer 对象被映射了,它就能被 JavaScript 读写;
  • 调用 mapAsync()createBuffer() 方法,且将 mappedAtCreation 属性设为 true 时,GPUBuffer 对象会被映射。

5 着色器编程

在 GPU 上运行的计算代码(仅计算,不绘图)称为 计算着色器。它们由数百个 GPU 核心并行执行。它们的输入和输出是 WebGPU 中的缓存器。

为了说明计算着色器在 WebGPU 中的使用,先列举一下矩阵乘法,这是一种机器学习中的常见计算:

WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘

简而言之,我们要做:

① 创建三个 GPU 缓存器(GPUBuffer 对象,两个用于输入矩阵,一个用于保存输出的结果矩阵)

② 描述计算着色器的输入和输出

③ 编译着色器代码

④ 创建并设置计算管线

⑤ 将编码后的命令批量提交给 GPU

⑥ 从结果缓存器中读取结果矩阵

6 GPUBuffer 对象的创建

为了简单起见,这里使用浮点数组表示矩阵,第一、二个数组元素是行数、列数,其余是矩阵的元素值。

WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘

创建三个 usage 带有 GPUBufferUsage.STORAGE 参数的 GPUBuffer 来存储数据,因为在计算着色器中要存数据和读数据。

用于存储结果的 GPUBuffer 要带有 GPUBufferUsage.COPY_SRC,因为一旦所有的 GPU 指令执行完毕后,这个矩阵将复制到这个 GPUBuffer。

大致代码:

const adapter = await navigator.gpu.requestAdapter(); if (!adapter) {    return;  } const device = await adapter.requestDevice();  // 第一个矩阵 const firstMatrix = new Float32Array([   2 /* 2行 */, 4 /* 4列 */,   1, 2, 3, 4,   5, 6, 7, 8 ]); const gpuBufferFirstMatrix = device.createBuffer({   mappedAtCreation: true,   size: firstMatrix.byteLength,   usage: GPUBufferUsage.STORAGE, }); const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange(); new Float32Array(arrayBufferFirstMatrix).set(firstMatrix); gpuBufferFirstMatrix.unmap();  // 第二个矩阵 const secondMatrix = new Float32Array([   4 /* 4行 */, 2 /* 2列 */,   1, 2,   3, 4,   5, 6,   7, 8 ]); const gpuBufferSecondMatrix = device.createBufferMapped({   mappedAtCreation: true,   size: secondMatrix.byteLength,   usage: GPUBufferUsage.STORAGE, }); const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange(); new Float32Array(arrayBufferSecondMatrix).set(secondMatrix); gpuBufferSecondMatrix.unmap();   // 结果矩阵 const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]); const resultMatrixBuffer = device.createBuffer({   size: resultMatrixBufferSize,   usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });

7 绑定组及其布局对象

绑定组及其布局对象是 WebGPU 中特有的概念(接口)。绑定组表示一组要输入到着色器里的数据,譬如你要做一个菜,那一堆配好的食材就可以叫绑定组。绑定组的布局对象就是告诉着色器绑定组对象长什么样子,即你买的食材是什么,量多少。

在下面的示例代码中,布局对象告诉计算着色器,这里有 3 个绑定的资源,分别编号为 0、1、2,0 号和 1 号对应两个只读存储缓存(read-only-storage),2 号对应一个存储缓存(storage)。

绑定组对象和这个布局对象是关联的,在本例中,即三个 GPUBuffer 分别一个萝卜填一个坑, gpuBufferFirstMatrixgpuBufferSecondMatrix 对象对应 0 号和 1 号坑,resultMatrixBuffer 对应 2号坑。

“` js
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “read-only-storage”
}
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “read-only-storage”
}
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “storage”
}
}
]
});

const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: gpuBufferFirstMatrix
}
},
{
binding: 1,
resource: {
buffer: gpuBufferSecondMatrix

文章出处:https://web.dev/gpu-compute/

文章发布时间 文章最后更新 翻译时间 翻译人
2019年8月28日 2021年9月6日 2021年9月12日 四季留歌

版权原作者所有。转载翻译稿请带连接与翻译者。

  • 1 背景
  • 2 访问 GPU
  • 3 写入缓存
  • 4 读取缓存
  • 5 着色器编程
  • 6 GPUBuffer 对象的创建
  • 7 绑定组及其布局对象

使用 WebGPU 可以调用 GPU 的并行计算性能。

1 背景

GPU 最初是拿来画图的设备,但是近些年它的并行计算能力却开辟了另一些领域,允许开发人员实现多种类型的算法,而不是仅仅拿来画图。

这种借助 GPU 并行计算能力的编程称为 GPU计算,使用 GPU 作为主要运算处理器的科学计算称为通用 GPU 编程(General-Purpose GPU 编程,GPGPU)。

译者注:多年后,这段文字会不会写入一些本科毕业论文呢?(笑

GPU 计算为机器学习做出了巨大贡献,因为卷积神经网络和其他模型可以利用该架构在 GPU 上高效运行。但是,Web端缺乏 GPU 的计算功能(WebGL很难做),W3C 的 GPU for the Web 社区组正在设计一个 API 来公开大多数现代图形处理器能用的图形编程接口,这个 API 是 WebGPU。

WebGPU 是一个底层 API,类似 WebGL。它的代码量很长,接口粒度很细,不过没关系,我们关注的是性能。

在本文中,作者介绍 WebGPU 中的 GPU 计算部分,仅作为抛砖引玉,希望各位大佬能玩出花样。之后原作者也将考虑写一些 WebGPU 图形渲染的文章。

2 访问 GPU

在 WebGPU API 中访问 GPU 很容易,调用 navigator.gpu.requestAdapter() 会返回一个 Promise,它会 resolve 一个 GPU 适配器(物理显卡)的 adapter 对象。

一般这个适配器对象会是独显,但是有些情况也可以是核芯显卡。

一旦你有了适配器对象,你可以调用 adapter.requestDevice() 来获取一个能 resolve 一个设备对象的 Promise。

适配器和设备这两个对象的区别见官方 Explainer 文档。

const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { return; } const device = await adapter.requestDevice();

上述俩函数都可以传入 option 对象,用来选择适配器类型和指定设备信息。为了简单起见,这里不传递,即使用默认配置。

3 写入缓存

这部分介绍 JavaScript 是如何把数据写入显存的。

下面的例子介绍了如何把 4bytes 数据写入已经访问到的显存里。调用 device.createBuffer() 并传递几个参数:需要多大的显存,这块显存的作用。

默认情况下,即使不明确传递参数 GPUBufferUsage.MAP_WRITE,申请的这块显存就是拿来写入的。由于 mappedAtCreationtrue,它会在创建时映射这块缓存,然后我们就可以在 JavaScript 代码中对返回的 buffer 对象调用 getMappedRange() 来访问关联在一起的二进制数据了。

如果你用过 ArrayBuffer,那么写入字节数据应该不是什么大问题,接下来要用到 TypedArray 来写入缓存。

// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据 const gpuBuffer = device.createBuffer({   mappedAtCreation: true,   size: 4,   usage: GPUBufferUsage.MAP_WRITE }); const arrayBuffer = gpuBuffer.getMappedRange();  // 通过 TypedArray 向 ArrayBuffer 写数据 new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

状态是映射的显存,这意味着这块显存暂时归 CPU 所有,即能使用 JavaScript 读写它。为了让 GPU 能再次访问这块缓存,它必须取消映射:调用 gpuBuffer.unmap() 即可。

显存映射/解除映射这组概念,是用来防止 GPU 和 CPU 同时访问缓存时产生冲突用的。

4 读取缓存

这一节介绍如何将某个 GPU 缓存对象上的数据复制到另一个 GPU 缓存对象(GPUBuffer Object)。

在某个 GPUBuffer 对象上写入了数据,并要复制到另一个,那么就需要一个新的属性 GPUBufferUsage.COPY_SRC。这次,调用 device.createBuffer() 创建一个没有映射的 GPUBuffer 对象,它的 usage 将设为 GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,它用来存放第一个 GPUBuffer 复制过来的数据,并在复制结束后要在 JavaScript 这边再次把数据读取出来。

// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据 const gpuWriteBuffer = device.createBuffer({   mappedAtCreation: true,   size: 4,   usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC // 👈 注意这里 }); const arrayBuffer = gpuWriteBuffer.getMappedRange();  // 通过 TypedArray 向 ArrayBuffer 写数据 new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);  // 解除显存对象的映射,稍后它就能在 GPU 中进行复制操作 gpuWriteBuffer.unmap();  // 创建一个新的状态为未映射的 GPUBuffer,它之后要拿来读取数据 const gpuReadBuffer = device.createBuffer({   size: 4,   usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ // 👈 注意这里 });

由于 GPU 是一个独立于 CPU 的协处理器,所有 GPU 的指令都是异步执行的,这就是为什么要去创建 GPU 指令组,而且在需要的时候成批提交到 GPU 上的原因(GPUCommandEncoder、GPUQueue)。

在 WebGPU 中,通过调用 device.createCommandEncoder() 返回 GPUCommandEncoder 对象,这个对象能构建并暂存一系列的 GPU 指令,然后在某个时刻提交至 GPU。另一方面,GPUBuffer 对象上的方法是不缓存的,意思是你调用的时候就会立即执行。

获取 GPUCommandEncoder 后,你可以用 copyEncoder.copyBufferToBuffer() 将此命令添加到指令队列以供后续的执行;最后调用 copyEncoder.finish() 完成指令的编码,然后调用 device.queue.submit() 来把这些指令提交给 GPU,然后 GPU 将按顺序执行。

// 创建一个名为 copyEncoder 的指令编码器,用来复制显存 const copyEncoder = device.createCommandEncoder(); copyEncoder.copyBufferToBuffer(   gpuWriteBuffer /* 源显存(对象) */,   0 /* 起始字节(从哪开始读) */,   gpuReadBuffer /* 目标显存(对象) */,   0 /* 起始字节(从哪开始写) */,   4 /* 大小 */ );  // 提交我们写好的复制功能的指令 const copyCommands = copyEncoder.finish(); device.queue.submit([copyCommands]);

此时,指令队列已经发送,但是不一定已经执行。想要读取第二个 GPUBuffer 对象上的数据,你要配合参数 GPUMapMode.READ 调用 gpuReadBuffer.mapAsync() 方法,它返回一个 Promise,你可以用 await 语法来获取 resolve 值。这个 Promise 在 GPUBuffer 被再次映射时 resolve。

然后,使用 gpuReadBuffer.getMappedRange() 来获取映射后的数据,一旦提交的指令执行后,我们获取到的这个数据应该会与第一个 GPUBuffer 的数据是一样的。

// 读取缓存 await gpuReadBuffer.mapAsync(GPUMapMode.READ); const copyArrayBuffer = gpuReadBuffer.getMappedRange(); console.log(new Uint8Array(copyArrayBuffer));

你可以到 glitch 上看看 示例代码1.


简而言之,关于 GPUBuffer 对象(显存)的操作,希望读者记住以下几点:

  • GPUBuffer 对象必须取消映射,才能用于提交队列;
  • 一旦 GPUBuffer 对象被映射了,它就能被 JavaScript 读写;
  • 调用 mapAsync()createBuffer() 方法,且将 mappedAtCreation 属性设为 true 时,GPUBuffer 对象会被映射。

5 着色器编程

在 GPU 上运行的计算代码(仅计算,不绘图)称为 计算着色器。它们由数百个 GPU 核心并行执行。它们的输入和输出是 WebGPU 中的缓存器。

为了说明计算着色器在 WebGPU 中的使用,先列举一下矩阵乘法,这是一种机器学习中的常见计算:

WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘

简而言之,我们要做:

① 创建三个 GPU 缓存器(GPUBuffer 对象,两个用于输入矩阵,一个用于保存输出的结果矩阵)

② 描述计算着色器的输入和输出

③ 编译着色器代码

④ 创建并设置计算管线

⑤ 将编码后的命令批量提交给 GPU

⑥ 从结果缓存器中读取结果矩阵

6 GPUBuffer 对象的创建

为了简单起见,这里使用浮点数组表示矩阵,第一、二个数组元素是行数、列数,其余是矩阵的元素值。

WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘

创建三个 usage 带有 GPUBufferUsage.STORAGE 参数的 GPUBuffer 来存储数据,因为在计算着色器中要存数据和读数据。

用于存储结果的 GPUBuffer 要带有 GPUBufferUsage.COPY_SRC,因为一旦所有的 GPU 指令执行完毕后,这个矩阵将复制到这个 GPUBuffer。

大致代码:

const adapter = await navigator.gpu.requestAdapter(); if (!adapter) {    return;  } const device = await adapter.requestDevice();  // 第一个矩阵 const firstMatrix = new Float32Array([   2 /* 2行 */, 4 /* 4列 */,   1, 2, 3, 4,   5, 6, 7, 8 ]); const gpuBufferFirstMatrix = device.createBuffer({   mappedAtCreation: true,   size: firstMatrix.byteLength,   usage: GPUBufferUsage.STORAGE, }); const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange(); new Float32Array(arrayBufferFirstMatrix).set(firstMatrix); gpuBufferFirstMatrix.unmap();  // 第二个矩阵 const secondMatrix = new Float32Array([   4 /* 4行 */, 2 /* 2列 */,   1, 2,   3, 4,   5, 6,   7, 8 ]); const gpuBufferSecondMatrix = device.createBufferMapped({   mappedAtCreation: true,   size: secondMatrix.byteLength,   usage: GPUBufferUsage.STORAGE, }); const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange(); new Float32Array(arrayBufferSecondMatrix).set(secondMatrix); gpuBufferSecondMatrix.unmap();   // 结果矩阵 const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]); const resultMatrixBuffer = device.createBuffer({   size: resultMatrixBufferSize,   usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });

7 绑定组及其布局对象

绑定组及其布局对象是 WebGPU 中特有的概念(接口)。绑定组表示一组要输入到着色器里的数据,譬如你要做一个菜,那一堆配好的食材就可以叫绑定组。绑定组的布局对象就是告诉着色器绑定组对象长什么样子,即你买的食材是什么,量多少。

在下面的示例代码中,布局对象告诉计算着色器,这里有 3 个绑定的资源,分别编号为 0、1、2,0 号和 1 号对应两个只读存储缓存(read-only-storage),2 号对应一个存储缓存(storage)。

绑定组对象和这个布局对象是关联的,在本例中,即三个 GPUBuffer 分别一个萝卜填一个坑, gpuBufferFirstMatrixgpuBufferSecondMatrix 对象对应 0 号和 1 号坑,resultMatrixBuffer 对应 2号坑。

“` js
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “read-only-storage”
}
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “read-only-storage”
}
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: “storage”
}
}
]
});

const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: gpuBufferFirstMatrix
}
},
{
binding: 1,
resource: {
buffer: gpuBufferSecondMatrix

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » WebGPU 通用计算(计算管线、计算着色器)入门:矩阵相乘求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们